preview
Price Action Analysis Toolkit Development (Part 76): One-Click Symbol Dashboard for Centralized Multi-Chart Management in MQL5

Price Action Analysis Toolkit Development (Part 76): One-Click Symbol Dashboard for Centralized Multi-Chart Management in MQL5

MetaTrader 5Trading systems |
351 0
Christian Benjamin
Christian Benjamin

Contents


Introduction

When working with MetaTrader 5, opening and managing charts can become a time-consuming process, especially when analyzing multiple symbols. To open a new chart, you must first locate the Market Watch window, search for the required symbol, right-click it, select Chart Window, and then wait for the chart to open before you can begin your analysis.

Closing charts is not much faster. After analyzing several instruments, it is common to end up with many open charts, making the workspace cluttered and difficult to navigate. As the number of charts increases, switching between them or finding the one you need becomes less efficient, slowing down the overall analysis workflow.

The GIF below demonstrates some of these challenges in the standard MT5 chart management process.


The issues discussed above can be summarized as follows:

  1. Opening a chart requires multiple manual steps, making the process slower than necessary.
  2. Finding symbols in the Market Watch window becomes increasingly time-consuming, particularly when many instruments are available.
  3. Closing or organizing multiple open charts is inefficient, resulting in a cluttered workspace.
  4. Frequent chart switching interrupts the analysis workflow, reducing productivity when monitoring multiple symbols.

Presenting the Solution

To address these challenges, this article introduces a centralized chart management panel that simplifies symbol and chart operations. Instead of navigating through the Market Watch window each time, you will be able to open and close charts directly from a single interactive dashboard. The solution also integrates the symbol search functionality into the same panel, allowing symbols to be located and accessed instantly. The result is a fully functional chart management dashboard. It streamlines multi-symbol navigation, reduces unnecessary mouse clicks, and makes working with multiple MT5 charts more efficient.


In the following section, we will implement this solution step by step.


MQL5 Implementation

Project Overview and Setting Up the Files in MetaEditor

To keep the project organized, maintainable, and easy to extend, we divide the implementation into five separate files. Each file has a well-defined responsibility, allowing the user interface, symbol management, and chart operations to remain independent while working together as a complete system.

The table below summarizes the project structure.

File Name
Location
Responsibility
DashboardDefines.mqh
MQL5/Include/ChartDashboard/

Contains all shared definitions, including panel dimensions (PANEL_WIDTH, ROW_HEIGHT), color constants, font settings, and the global object prefix used throughout the dashboard.

CSymbolManager.mqh
MQL5/Include/ChartDashboard/

Retrieves the complete list of available trading symbols, sorts them alphabetically, and performs case-insensitive filtering based on the user's search text.

CChartManager.mqh
MQL5/Include/ChartDashboard/

Provides all chart management functionality, including opening charts, closing charts, and determining whether a chart for a particular symbol is already open.

CPanel.mqh
MQL5/Include/ChartDashboard/

Implements the graphical dashboard. It creates the interface, manages the search box, scrolling controls, and symbol rows, processes user interactions, and refreshes the panel whenever its contents change.

ChartDashboardEA.mq5
MQL5/Experts/ChartDashboard/

Serves as the application's entry point. It initializes all project components, loads the available symbols, creates the dashboard, and handles chart events and timer updates.

To better illustrate the project structure, the directory layout is shown below:


Here is the system architecture:


Before compiling the project, create the following folder structure inside your MetaTrader 5 data directory.

  • Open MetaEditor, then select File → Open Data Folder.
  • Navigate to the MQL5 directory.
  • Inside MQL5, open or create the Include folder, then create a Classes subfolder if it does not already exist.
  • Copy the four .mqh files into MQL5/Include/Classes/.
  • Copy ChartDashboardEA.mq5 into MQL5/Experts/.
  • Return to MetaEditor and compile ChartDashboardEA.mq5.

With the project structure in place, we can now implement each module, beginning with the shared definitions used throughout the dashboard.


1. Creating the Shared Dashboard Definitions

We begin by defining all the constants that will govern the appearance and behavior of our dashboard. Centralizing these definitions in a single include file offers two main advantages:

  • first, it allows us to adjust the panel's dimensions, colors, or fonts from one location without hunting through multiple class files; 
  • second, it ensures consistency across all graphical elements. The file is populated with the geometric parameters, color palette, and control sizes that form the blueprint for our interface.

Panel Geometry and Row Calculations

The first block defines the physical dimensions of the dashboard and the spacing between rows. We'll use these values throughout the CPanel class to position every object precisely.

//--- Panel geometry: position, size, row spacing
#define PANEL_X             10
#define PANEL_Y             20
#define PANEL_WIDTH         550
#define PANEL_HEIGHT        620
#define ROW_HEIGHT          32
#define ROW_MARGIN          2

//--- Maximum visible rows calculated from available height
#define MAX_VISIBLE_ROWS    ((PANEL_HEIGHT - 110) / (ROW_HEIGHT + ROW_MARGIN))

  • PANEL_X and PANEL_Y set the top‑left corner of the panel on the chart (10 pixels from the left edge and 20 pixels from the top).
  • PANEL_WIDTH and PANEL_HEIGHT define the overall size of the dashboard — 550×620 pixels, which is spacious enough to display a meaningful list of symbols without covering the entire chart.
  • ROW_HEIGHT and ROW_MARGIN control the vertical space allocated to each symbol row. With a row height of 32 pixels and a 2‑pixel margin between rows, the list remains readable and compact.
  • MAX_VISIBLE_ROWS is calculated automatically based on the available height (after reserving space for the header, search box, and footer). This macro ensures that the panel dynamically adapts to the defined size without manual adjustments.

Control Sizes

Next, we define the dimensions of the interactive elements — the buttons, the search box, and the status indicator.

//--- Controls size
#define BUTTON_WIDTH        55
#define BUTTON_HEIGHT       24
#define SEARCH_WIDTH        220
#define STATUS_INDICATOR_WIDTH 24

  • BUTTON_WIDTH (55 pixels) and BUTTON_HEIGHT (24 pixels) are used for both the Open and Close buttons. The width is sufficient to display the text comfortably.
  • SEARCH_WIDTH (220 pixels) gives the search edit box enough room for typing longer symbol names.
  • STATUS_INDICATOR_WIDTH (24 pixels) provides a compact area for the status symbol (● or ○), keeping the column narrow to maximize space for the symbol name.

Color Palette

The color palette is designed to create a clean, professional, and easy-on-the-eyes interface. By defining all colors as preprocessor macros, we centralize the styling in a single location, making it easy to adjust the appearance or improve contrast without changing the drawing logic. This also ensures a consistent look and feel across all dashboard elements.

//--- Color palette for the dashboard
#define COLOR_BG            C'230,240,250'
#define COLOR_BORDER        C'150,180,210'
#define COLOR_HEADER_BG     C'200,220,240'
#define COLOR_TITLE         C'20,60,100'
#define COLOR_TEXT          C'40,40,40'
#define COLOR_OPEN          C'0,180,0'
#define COLOR_CLOSED        C'200,50,50'
#define COLOR_BUTTON        C'200,200,200'
#define COLOR_BUTTON_TEXT   C'50,50,50'
#define COLOR_HIGHLIGHT     C'240,248,255'
#define COLOR_SEARCH_BG     C'255,255,255'
#define COLOR_SEARCH_TEXT   C'40,40,40'
#define COLOR_SEARCH_BORDER C'160,180,200'

The palette uses a soft light blue background (COLOR_BG) with a darker blue-gray border (COLOR_BORDER), while the header strip (COLOR_HEADER_BG) and title text (COLOR_TITLE) establish a clear visual hierarchy. The status indicator uses green (COLOR_OPEN) for open charts and red (COLOR_CLOSED) for closed ones, providing immediate visual feedback. Buttons use a neutral gray (COLOR_BUTTON) with dark text (COLOR_BUTTON_TEXT), and alternating row backgrounds (COLOR_BG and COLOR_HIGHLIGHT) improve readability. The search box features a clean white background (COLOR_SEARCH_BG) with dark text (COLOR_SEARCH_TEXT) and a subtle border (COLOR_SEARCH_BORDER).

Font Settings and Other Constants

The final block defines the typography and a few operational constants.

//--- Font settings
#define FONT_NAME           "Segoe UI"
#define FONT_SIZE           10
#define FONT_TITLE_SIZE     12
#define FONT_BOLD           "Segoe UI Bold"

//--- Default timeframe for new charts and object prefix
#define DEFAULT_TIMEFRAME   PERIOD_H1
#define OBJ_PREFIX          "DASH_"

  • FONT_NAME and FONT_SIZE are used for all regular text (symbol names, statuses, button labels). "Segoe UI" is a clean, modern font that renders well on all Windows systems.
  • FONT_TITLE_SIZE (12 points) is slightly larger for the dashboard title.
  • FONT_BOLD is defined but not used in this phase — it is reserved for potential future enhancements.
  • DEFAULT_TIMEFRAME sets the period for newly opened charts. We use PERIOD_H1 (hourly) as a sensible default, but this can be changed to any other timeframe.
  • OBJ_PREFIX is a critical constant. It is prepended to every graphical object name (e.g., DASH_open_3, DASH_sym_0). This prefix allows us to easily identify, group, and delete all dashboard‑related objects without interfering with other chart elements.

Here is the entire DashboardDefines.mqh file with all definitions combined:

//+------------------------------------------------------------------+
//|                                             DashboardDefines.mqh |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                          https://www.mql5.com/en/users/lynnchris |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/lynnchris"
#property version   "1.00"

#ifndef _DASHBOARD_DEFINES_
#define _DASHBOARD_DEFINES_

//--- Panel geometry: position, size, row spacing
#define PANEL_X             10
#define PANEL_Y             20
#define PANEL_WIDTH         550
#define PANEL_HEIGHT        620
#define ROW_HEIGHT          32
#define ROW_MARGIN          2

//--- Maximum visible rows calculated from available height
#define MAX_VISIBLE_ROWS    ((PANEL_HEIGHT - 110) / (ROW_HEIGHT + ROW_MARGIN))

//--- Controls size
#define BUTTON_WIDTH        55
#define BUTTON_HEIGHT       24
#define SEARCH_WIDTH        220
#define STATUS_INDICATOR_WIDTH 24

//--- Color palette for the dashboard
#define COLOR_BG            C'230,240,250'
#define COLOR_BORDER        C'150,180,210'
#define COLOR_HEADER_BG     C'200,220,240'
#define COLOR_TITLE         C'20,60,100'
#define COLOR_TEXT          C'40,40,40'
#define COLOR_OPEN          C'0,180,0'
#define COLOR_CLOSED        C'200,50,50'
#define COLOR_BUTTON        C'200,200,200'
#define COLOR_BUTTON_TEXT   C'50,50,50'
#define COLOR_HIGHLIGHT     C'240,248,255'
#define COLOR_SEARCH_BG     C'255,255,255'
#define COLOR_SEARCH_TEXT   C'40,40,40'
#define COLOR_SEARCH_BORDER C'160,180,200'

//--- Font settings
#define FONT_NAME           "Segoe UI"
#define FONT_SIZE           10
#define FONT_TITLE_SIZE     12
#define FONT_BOLD           "Segoe UI Bold"

//--- Default timeframe for new charts and object prefix
#define DEFAULT_TIMEFRAME   PERIOD_H1
#define OBJ_PREFIX          "DASH_"

#endif
//+------------------------------------------------------------------+


2. Implementing the Symbol Manager

With the dashboard definitions in place, we now turn to the data layer: the CSymbolManager class. Its primary responsibility is to retrieve a complete list of tradable symbols from the broker, maintain them in a sorted order, and provide fast, case‑insensitive filtering based on the user's search input. By encapsulating these tasks in a dedicated class, we keep the data management logic separate from the UI rendering, making the code easier to test and modify.

Private Members

The class defines three private members:

string m_symbols[];     // Full list of symbols
string m_filtered[];    // Filtered list based on search
string m_searchText;    // Current search string

  • m_symbols holds the complete, alphabetically sorted list of all tradable symbols.
  • m_filtered is a subset of m_symbols that matches the current search filter.
  • m_searchText stores the last search string, enabling the class to reapply the filter after a refresh.

Loading and Sorting Symbols

The Initialize() method is responsible for populating the full symbol list. The method calls SymbolsTotal(false) to obtain the total number of tradable symbols before iterating through the list. After the loop, the private SortSymbols() method arranges the list alphabetically using ArraySort(). This sorted order is essential for quick location of symbols by scrolling through the list.

//+------------------------------------------------------------------+
//| Loads all tradable symbols from the broker                       |
//+------------------------------------------------------------------+
bool CSymbolManager::Initialize()
  {
//--- Clear existing list
   ArrayResize(m_symbols, 0);
   int total = SymbolsTotal(false);   
   for(int i = 0; i < total; i++)
     {
      string sym = SymbolName(i, false);
      if(sym != "")
        {
         int sz = ArraySize(m_symbols);
         ArrayResize(m_symbols, sz + 1);
         m_symbols[sz] = sym;
        }
     }
//--- Sort and reset filter
   SortSymbols();
   m_searchText = "";
   return (ArraySize(m_symbols) > 0);
  }

Filtering Symbols

The primary responsibility of this manager is the filtering capability. The Filter(string search) method accepts a search string, converts it to uppercase for case‑insensitive matching, and then scans through the full m_symbols array. Any symbol containing the search substring is copied into the m_filtered array. If the search string is empty, the full list is copied, effectively showing all symbols.

//+------------------------------------------------------------------+
//| Filters the symbol list by a case‑insensitive substring search    |
//+------------------------------------------------------------------+
void CSymbolManager::Filter(const string search)
  {
   m_searchText = search;
   ArrayResize(m_filtered, 0);
//--- If search is empty, return all symbols
   if(search == "")
     {
      ArrayCopy(m_filtered, m_symbols);
      return;
     }
   string searchUpper = search;
   StringToUpper(searchUpper);
//--- Iterate and collect matching symbols
   for(int i = 0; i < ArraySize(m_symbols); i++)
     {
      string symUpper = m_symbols[i];
      StringToUpper(symUpper);
      if(StringFind(symUpper, searchUpper) != -1)
        {
         int sz = ArraySize(m_filtered);
         ArrayResize(m_filtered, sz + 1);
         m_filtered[sz] = m_symbols[i];
        }
     }
  }

This approach is efficient for typical symbol lists (a few hundred instruments) and provides a responsive user experience as the user types in the search box.


Accessor Methods

To retrieve data from the manager, we provide a set of simple accessor functions:

  • GetCount() returns the total number of symbols.
  • GetSymbol(int index) returns the symbol at a given position in the full list.

//+------------------------------------------------------------------+
//| Returns the total number of symbols                              |
//+------------------------------------------------------------------+
int CSymbolManager::GetCount() const
  {
   return ArraySize(m_symbols);
  }

//+------------------------------------------------------------------+
//| Returns the symbol at the given index                            |
//+------------------------------------------------------------------+
string CSymbolManager::GetSymbol(int index) const
  {
   if(index < 0 || index >= ArraySize(m_symbols))
      return("");
   return m_symbols[index];
  }

  • GetFilteredCount() returns the number of symbols that match the current filter.
  • GetFilteredSymbol(int index) returns a symbol from the filtered list by index.
  • GetSearchText() returns the current search string, used by the panel to keep the search box synchronized.

//+------------------------------------------------------------------+
//| Returns the number of symbols after filtering                    |
//+------------------------------------------------------------------+
int CSymbolManager::GetFilteredCount() const
  {
   return ArraySize(m_filtered);
  }

//+------------------------------------------------------------------+
//| Returns a filtered symbol by index                               |
//+------------------------------------------------------------------+
string CSymbolManager::GetFilteredSymbol(int index) const
  {
   if(index < 0 || index >= ArraySize(m_filtered))
      return("");
   return m_filtered[index];
  }

//+------------------------------------------------------------------+
//| Returns the current search string                                |
//+------------------------------------------------------------------+
string CSymbolManager::GetSearchText() const
  {
   return m_searchText;
  }

All methods include bounds checking to prevent out‑of‑range errors.

Refreshing the Data

The Refresh() method allows the panel to reload the symbol list from the broker and reapply the existing filter. This is useful if new symbols become available (e.g., after a broker restart) or if we want to reset the list without restarting the EA. Initialize() is called to reload the data and then Filter(m_searchText) to restore the previous filter state.

//+------------------------------------------------------------------+
//| Refreshes the symbol list (reloads and reapplies filter)         |
//+------------------------------------------------------------------+
void CSymbolManager::Refresh()
  {
   Initialize();
   Filter(m_searchText);
  }

Below is the full CSymbolManager.mqh file with all methods implemented:

//+------------------------------------------------------------------+
//|                                               CSymbolManager.mqh |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                          https://www.mql5.com/en/users/lynnchris |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/lynnchris"
#property version   "1.00"

#ifndef _CSYMBOL_MANAGER_
#define _CSYMBOL_MANAGER_

#include "DashboardDefines.mqh"

//+------------------------------------------------------------------+
//| Class CSymbolManager                                             |
//| Manages the list of tradable symbols and provides filtering      |
//+------------------------------------------------------------------+
class CSymbolManager
  {
private:
   string            m_symbols[];     // Full list of symbols
   string            m_filtered[];    // Filtered list based on search
   string            m_searchText;    // Current search string

   //+------------------------------------------------------------------+
   //| Sorts the symbol list alphabetically                             |
   //+------------------------------------------------------------------+
   void              SortSymbols();

public:
   //+------------------------------------------------------------------+
   //| Loads all tradable symbols from the broker                       |
   //+------------------------------------------------------------------+
   bool              Initialize();

   //+------------------------------------------------------------------+
   //| Returns the total number of symbols                              |
   //+------------------------------------------------------------------+
   int               GetCount() const;

   //+------------------------------------------------------------------+
   //| Returns the symbol at the given index                            |
   //+------------------------------------------------------------------+
   string            GetSymbol(int index) const;

   //+------------------------------------------------------------------+
   //| Filters the symbol list by a case‑insensitive substring search    |
   //+------------------------------------------------------------------+
   void              Filter(const string search);

   //+------------------------------------------------------------------+
   //| Returns the number of symbols after filtering                    |
   //+------------------------------------------------------------------+
   int               GetFilteredCount() const;

   //+------------------------------------------------------------------+
   //| Returns a filtered symbol by index                               |
   //+------------------------------------------------------------------+
   string            GetFilteredSymbol(int index) const;

   //+------------------------------------------------------------------+
   //| Returns the current search string                                |
   //+------------------------------------------------------------------+
   string            GetSearchText() const;

   //+------------------------------------------------------------------+
   //| Refreshes the symbol list (reloads and reapplies filter)         |
   //+------------------------------------------------------------------+
   void              Refresh();
  };

//+------------------------------------------------------------------+
//| Sorts the symbol list alphabetically                             |
//+------------------------------------------------------------------+
void CSymbolManager::SortSymbols()
  {
   ArraySort(m_symbols);
  }

//+------------------------------------------------------------------+
//| Loads all tradable symbols from the broker                       |
//+------------------------------------------------------------------+
bool CSymbolManager::Initialize()
  {
//--- Clear existing list
   ArrayResize(m_symbols, 0);
   int total = SymbolsTotal(false);   // Get only tradable symbols
   for(int i = 0; i < total; i++)
     {
      string sym = SymbolName(i, false);
      if(sym != "")
        {
         int sz = ArraySize(m_symbols);
         ArrayResize(m_symbols, sz + 1);
         m_symbols[sz] = sym;
        }
     }
//--- Sort and reset filter
   SortSymbols();
   m_searchText = "";
   return (ArraySize(m_symbols) > 0);
  }

//+------------------------------------------------------------------+
//| Returns the total number of symbols                              |
//+------------------------------------------------------------------+
int CSymbolManager::GetCount() const
  {
   return ArraySize(m_symbols);
  }

//+------------------------------------------------------------------+
//| Returns the symbol at the given index                            |
//+------------------------------------------------------------------+
string CSymbolManager::GetSymbol(int index) const
  {
   if(index < 0 || index >= ArraySize(m_symbols))
      return("");
   return m_symbols[index];
  }

//+------------------------------------------------------------------+
//| Filters the symbol list by a case‑insensitive substring search    |
//+------------------------------------------------------------------+
void CSymbolManager::Filter(const string search)
  {
   m_searchText = search;
   ArrayResize(m_filtered, 0);
//--- If search is empty, return all symbols
   if(search == "")
     {
      ArrayCopy(m_filtered, m_symbols);
      return;
     }
   string searchUpper = search;
   StringToUpper(searchUpper);
//--- Iterate and collect matching symbols
   for(int i = 0; i < ArraySize(m_symbols); i++)
     {
      string symUpper = m_symbols[i];
      StringToUpper(symUpper);
      if(StringFind(symUpper, searchUpper) != -1)
        {
         int sz = ArraySize(m_filtered);
         ArrayResize(m_filtered, sz + 1);
         m_filtered[sz] = m_symbols[i];
        }
     }
  }

//+------------------------------------------------------------------+
//| Returns the number of symbols after filtering                    |
//+------------------------------------------------------------------+
int CSymbolManager::GetFilteredCount() const
  {
   return ArraySize(m_filtered);
  }

//+------------------------------------------------------------------+
//| Returns a filtered symbol by index                               |
//+------------------------------------------------------------------+
string CSymbolManager::GetFilteredSymbol(int index) const
  {
   if(index < 0 || index >= ArraySize(m_filtered))
      return("");
   return m_filtered[index];
  }

//+------------------------------------------------------------------+
//| Returns the current search string                                |
//+------------------------------------------------------------------+
string CSymbolManager::GetSearchText() const
  {
   return m_searchText;
  }

//+------------------------------------------------------------------+
//| Refreshes the symbol list (reloads and reapplies filter)         |
//+------------------------------------------------------------------+
void CSymbolManager::Refresh()
  {
   Initialize();
   Filter(m_searchText);
  }

#endif
//+------------------------------------------------------------------+


3. Implementing the Chart Manager

Next, we implement the CChartManager class. It encapsulates chart-related operations in a reusable component. Its responsibilities include opening a new chart for a given symbol, closing an existing chart, and checking whether a chart is currently open — all while abstracting away the underlying MetaTrader 5 API calls.

Private Members

The class maintains a single private member:

ENUM_TIMEFRAMES   m_defaultTF;   // Default timeframe for new charts

This stores the timeframe that will be used when Open() is called without an explicit period. We set the default via the DEFAULT_TIMEFRAME macro defined in DashboardDefines.mqh (we use PERIOD_H1), but it can be easily modified in the constructor or via a setter if needed.

Constructor

The constructor initializes the default timeframe using the macro. This simple initialization ensures that every instance starts with a consistent configuration.

//+------------------------------------------------------------------+
//| Constructor – initializes the default timeframe                  |
//+------------------------------------------------------------------+
CChartManager::CChartManager() : m_defaultTF(DEFAULT_TIMEFRAME)
  {
//--- Initialize manager with default timeframe from defines
  }

Opening a Chart

The Open(string symbol) method attempts to open a new chart for the specified symbol using the default timeframe. The method first validates the input. If the symbol string is empty, it immediately returns false. Otherwise, we call ChartOpen(symbol, m_defaultTF), which returns a chart ID on success or -1 on failure. The method returns true only if ChartOpen() returns a valid chart ID.

//+------------------------------------------------------------------+
//| Opens a new chart for the specified symbol                       |
//+------------------------------------------------------------------+
bool CChartManager::Open(string symbol)
  {
//--- Validate input parameter - empty symbol is not allowed
   if(symbol == "")
      return false;

//--- Attempt to open a new chart with default timeframe
   long chart = ChartOpen(symbol, m_defaultTF);

//--- Return true if chart was opened successfully (valid chart ID)
   return (chart != -1);
  }


Note that ChartOpen does not fail if a chart for the same symbol already exists — it simply opens a new one. This behavior is acceptable for our dashboard, as the user may wish to have multiple timeframes for the same instrument. However, if we later want to limit to a single chart per symbol, we could add a check within this method.

Closing a Chart

The Close(string symbol) method uses the GetChartID() helper to retrieve the chart ID associated with the specified symbol. If no matching chart is found (that is, GetChartID() returns -1), the method immediately returns false. Otherwise, we call ChartClose(chart) to close the chart, and the method returns the result of the operation.

//+------------------------------------------------------------------+
//| Closes the first chart found for the specified symbol            |
//+------------------------------------------------------------------+
bool CChartManager::Close(string symbol)
  {
//--- Get the chart ID for the symbol
   long chart = GetChartID(symbol);

//--- If no chart found, return false
   if(chart == -1)
      return false;

//--- Close the chart and return the result
   return ChartClose(chart);
  }

Checking if a Chart is Open

The IsOpen(string symbol) method performs a simple boolean check. It uses the GetChartID() helper and returns true if a valid chart ID is found, or false otherwise. This method is called during the panel's refresh cycle to update the chart status indicators.

//+------------------------------------------------------------------+
//| Checks if any chart for the specified symbol is open             |
//+------------------------------------------------------------------+
bool CChartManager::IsOpen(string symbol)
  {
//--- Check if GetChartID returns a valid chart ID
   return (GetChartID(symbol) != -1);
  }

Finding a Chart ID

The main chart management logic is implemented in the GetChartID(string symbol) method. We use the ChartFirst() and ChartNext() functions to iterate through all open charts. For each chart, the method retrieves the associated symbol using ChartSymbol(curr) and compares it with the requested symbol. If a match is found, the corresponding chart ID is returned immediately. Otherwise, after all open charts have been examined, the method returns -1.

//+------------------------------------------------------------------+
//| Finds and returns the Chart ID of the first chart with given symbol |
//+------------------------------------------------------------------+
long CChartManager::GetChartID(string symbol)
  {
//--- Get the first chart in the chart list
   long curr = ChartFirst();

//--- Iterate through all open charts
   while(curr != -1)
     {
      //--- Check if the current chart's symbol matches the requested symbol
      if(ChartSymbol(curr) == symbol)
         return curr;  //--- Return the chart ID when match is found

      //--- Move to the next chart in the list
      curr = ChartNext(curr);
     }

//--- No matching chart found - return -1
   return -1;
  }

This approach is simple and works well for the typical number of open charts (usually under 50). However, if the user maintains many charts, we could consider caching the mapping, but for our purposes, this linear scan is sufficient.

Activating a Chart (Placeholder)

The Activate() method is included as a placeholder for a future enhancement. In Phase 1, it does nothing — but it serves as a reminder that we could later add functionality to bring the chart to the foreground or set focus. For now, we leave it empty.

//+------------------------------------------------------------------+
//| Activates the chart (placeholder - not implemented in Phase 1)   |
//+------------------------------------------------------------------+
void CChartManager::Activate(string symbol)
  {
//--- This function is reserved for future implementation
//--- Currently does nothing as per Phase 1 requirements
  }

Here is the full CChartManager.mqh file with all methods implemented:

//+------------------------------------------------------------------+
//|                                                CChartManager.mqh |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                          https://www.mql5.com/en/users/lynnchris |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/lynnchris"
#property version   "1.00"

#ifndef _CCHART_MANAGER_
#define _CCHART_MANAGER_

//--- Include required definitions
#include "DashboardDefines.mqh"

//+------------------------------------------------------------------+
//| Class CChartManager                                              |
//| Manages chart operations for symbols in the trading platform     |
//+------------------------------------------------------------------+
class CChartManager
  {
private:
   ENUM_TIMEFRAMES   m_defaultTF;   // Default timeframe for new charts

public:
   //+------------------------------------------------------------------+
   //| Constructor – sets the default timeframe                         |
   //+------------------------------------------------------------------+
                     CChartManager();

   //+------------------------------------------------------------------+
   //| Opens a new chart for the given symbol                           |
   //+------------------------------------------------------------------+
   bool              Open(string symbol);

   //+------------------------------------------------------------------+
   //| Closes the first chart found for the given symbol                |
   //+------------------------------------------------------------------+
   bool              Close(string symbol);

   //+------------------------------------------------------------------+
   //| Checks whether a chart for the given symbol is open              |
   //+------------------------------------------------------------------+
   bool              IsOpen(string symbol);

   //+------------------------------------------------------------------+
   //| Returns the Chart ID of the first chart with the given symbol    |
   //+------------------------------------------------------------------+
   long              GetChartID(string symbol);

   //+------------------------------------------------------------------+
   //| (Optional) Activates the chart (not used in Phase 1)             |
   //+------------------------------------------------------------------+
   void              Activate(string symbol);
  };

//+------------------------------------------------------------------+
//| Constructor – initializes the default timeframe                  |
//+------------------------------------------------------------------+
CChartManager::CChartManager() : m_defaultTF(DEFAULT_TIMEFRAME)
  {
//--- Initialize manager with default timeframe from defines
  }

//+------------------------------------------------------------------+
//| Opens a new chart for the specified symbol                       |
//+------------------------------------------------------------------+
bool CChartManager::Open(string symbol)
  {
//--- Validate input parameter - empty symbol is not allowed
   if(symbol == "")
      return false;

//--- Attempt to open a new chart with default timeframe
   long chart = ChartOpen(symbol, m_defaultTF);

//--- Return true if chart was opened successfully (valid chart ID)
   return (chart != -1);
  }

//+------------------------------------------------------------------+
//| Closes the first chart found for the specified symbol            |
//+------------------------------------------------------------------+
bool CChartManager::Close(string symbol)
  {
//--- Get the chart ID for the symbol
   long chart = GetChartID(symbol);

//--- If no chart found, return false
   if(chart == -1)
      return false;

//--- Close the chart and return the result
   return ChartClose(chart);
  }

//+------------------------------------------------------------------+
//| Checks if any chart for the specified symbol is open             |
//+------------------------------------------------------------------+
bool CChartManager::IsOpen(string symbol)
  {
//--- Check if GetChartID returns a valid chart ID
   return (GetChartID(symbol) != -1);
  }

//+------------------------------------------------------------------+
//| Finds and returns the Chart ID of the first chart with given symbol |
//+------------------------------------------------------------------+
long CChartManager::GetChartID(string symbol)
  {
//--- Get the first chart in the chart list
   long curr = ChartFirst();

//--- Iterate through all open charts
   while(curr != -1)
     {
      //--- Check if the current chart's symbol matches the requested symbol
      if(ChartSymbol(curr) == symbol)
         return curr;  //--- Return the chart ID when match is found

      //--- Move to the next chart in the list
      curr = ChartNext(curr);
     }

//--- No matching chart found - return -1
   return -1;
  }

//+------------------------------------------------------------------+
//| Activates the chart (placeholder - not implemented in Phase 1)   |
//+------------------------------------------------------------------+
void CChartManager::Activate(string symbol)
  {
//--- This function is reserved for future implementation
//--- Currently does nothing as per Phase 1 requirements
  }

#endif
//+------------------------------------------------------------------+


4. Implementing the Dashboard Panel

With the data and action layers complete, we can now implement the centerpiece of the project—the CPanel class. This class integrates the data and chart management layers into a single graphical dashboard. The CPanel coordinates the interaction between the CSymbolManager and CChartManager, presenting their functionality through a single graphical interface.

Class Members

The class maintains several private members that track the state of the dashboard:

//+------------------------------------------------------------------+
//| Class CPanel                                                     |
//| Draws and manages the dashboard UI (symbol list, status, buttons)|
//+------------------------------------------------------------------+
class CPanel
  {
private:
   CSymbolManager    *m_symMgr;       // Symbol manager reference
   CChartManager     *m_chartMgr;     // Chart manager reference

   int               m_scrollOffset;  // Current scroll position
   string            m_searchText;    // Last search text
   bool              m_needRedraw;    // Flag to trigger redraw
   int               m_maxRows;       // Number of visible rows

   //--- Status cache to avoid frequent IsOpen() calls
   struct StatusCache
     {
      string         symbol;
      bool           isOpen;
     };
   StatusCache       m_statusCache[];

   //+------------------------------------------------------------------+
   //| Creates a graphical object with standard properties              |
   //+------------------------------------------------------------------+
   bool              CreateObject(string name, ENUM_OBJECT type, int x, int y,
                                  int w, int h, color clr, string text = "",
                                  int fontSize = FONT_SIZE, string tooltip = "");

   //+------------------------------------------------------------------+
   //| Removes all dashboard objects from the chart                     |
   //+------------------------------------------------------------------+
   void              ClearObjects();

   //+------------------------------------------------------------------+
   //| Returns the symbol for the given row index (including scroll)    |
   //+------------------------------------------------------------------+
   string            GetSymbolForRow(int rowIndex);

   //+------------------------------------------------------------------+
   //| Updates the cached open status for a symbol                      |
   //+------------------------------------------------------------------+
   void              UpdateStatusCache(string symbol, bool isOpen);

   //+------------------------------------------------------------------+
   //| Retrieves the cached open status for a symbol                    |
   //+------------------------------------------------------------------+
   bool              GetCachedStatus(string symbol);

   //+------------------------------------------------------------------+
   //| Draws the header row with column labels                          |
   //+------------------------------------------------------------------+
   void              DrawHeader(int y);

   //+------------------------------------------------------------------+
   //| Draws the rows of symbols with status and buttons                |
   //+------------------------------------------------------------------+
   void              DrawRows(int startY);

   //+------------------------------------------------------------------+
   //| Updates the scroll buttons' enabled/disabled state               |
   //+------------------------------------------------------------------+
   void              UpdateScrollButtons();

   //+------------------------------------------------------------------+
   //| Scrolls the list up by one row                                   |
   //+------------------------------------------------------------------+
   void              ScrollUp();

   //+------------------------------------------------------------------+
   //| Scrolls the list down by one row                                 |
   //+------------------------------------------------------------------+
   void              ScrollDown();

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
                     CPanel();

   //+------------------------------------------------------------------+
   //| Destructor                                                       |
   //+------------------------------------------------------------------+
                    ~CPanel();

   //+------------------------------------------------------------------+
   //| Sets the symbol and chart manager references                     |
   //+------------------------------------------------------------------+
   void              SetManagers(CSymbolManager *symMgr, CChartManager *chartMgr);

   //+------------------------------------------------------------------+
   //| Draws the entire dashboard from scratch                          |
   //+------------------------------------------------------------------+
   void              Draw();

   //+------------------------------------------------------------------+
   //| Refreshes the dashboard – updates filter and status              |
   //+------------------------------------------------------------------+
   void              Refresh();

   //+------------------------------------------------------------------+
   //| Handles clicks on dashboard objects                              |
   //+------------------------------------------------------------------+
   void              HandleClick(string objectName);
  };


The two manager pointers connect the panel to its data source and chart management operations. The scroll position keeps track of how far the user has scrolled through the filtered symbol list, while the m_needRedraw flag signals when the UI needs to be redrawn, avoiding unnecessary updates. The m_maxRows member stores the maximum number of rows that fit within the panel, ensuring that only the visible rows are rendered. Finally, we use the StatusCache structure and array to cache the open/closed status of each symbol, allowing the panel to detect changes without repeatedly calling m_chartMgr.IsOpen() during the refresh cycle.

The Status Cache

Before implementing the drawing logic, it is useful to examine the status cache in more detail. The UpdateStatusCache() and GetCachedStatus() methods work together to maintain a local copy of each symbol's chart status. When drawing a row, we call UpdateStatusCache(symbol, isOpen) to store the current state. During the refresh cycle, the cached value is compared with the current status. If they differ, the panel knows that a redraw is required. The cache stores the previous open/closed state for comparison during the refresh cycle.

//+------------------------------------------------------------------+
//| Updates the cached open status for a symbol                      |
//+------------------------------------------------------------------+
void CPanel::UpdateStatusCache(string symbol, bool isOpen)
  {
//--- Find existing entry and update
   for(int i = 0; i < ArraySize(m_statusCache); i++)
     {
      if(m_statusCache[i].symbol == symbol)
        {
         m_statusCache[i].isOpen = isOpen;
         return;
        }
     }
//--- Add new entry
   int sz = ArraySize(m_statusCache);
   ArrayResize(m_statusCache, sz + 1);
   m_statusCache[sz].symbol = symbol;
   m_statusCache[sz].isOpen = isOpen;
  }

//+------------------------------------------------------------------+
//| Retrieves the cached open status for a symbol                    |
//+------------------------------------------------------------------+
bool CPanel::GetCachedStatus(string symbol)
  {
   for(int i = 0; i < ArraySize(m_statusCache); i++)
      if(m_statusCache[i].symbol == symbol)
         return m_statusCache[i].isOpen;
   return false;
  }

The Constructor and Destructor

The constructor initializes the member variables to safe default values. The destructor is left empty because the panel does not manage any resources directly. Instead, we rely on the manager classes, which are cleaned up in the main Expert Advisor's OnDeinit() function.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CPanel::CPanel() : m_symMgr(NULL), m_chartMgr(NULL),
   m_scrollOffset(0), m_needRedraw(true), m_maxRows(0) {}

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CPanel::~CPanel() {}

//+------------------------------------------------------------------+
//| Sets the symbol and chart manager references                     |
//+------------------------------------------------------------------+
void CPanel::SetManagers(CSymbolManager *symMgr, CChartManager *chartMgr)
  {
   m_symMgr = symMgr;
   m_chartMgr = chartMgr;
  }

The SetManagers() method provides a way to inject the manager references after the object is constructed. This separation allows the panel to be instantiated before the managers are fully ready, which is useful for initialization ordering.

Creating and Managing GUI Objects

The CreateObject() method is a central utility that abstracts the creation of all graphical objects. It accepts the object name, type, position, size, color, text, and an optional tooltip. Before creating a new object, the method deletes any existing object with the same name to prevent duplicates. We then configure the object according to its type—for example, buttons receive a background color and border, while labels receive only a text color. This centralized creation point simplifies the drawing logic and ensures consistent styling throughout the panel.

//+------------------------------------------------------------------+
//| Creates a graphical object with standard properties              |
//+------------------------------------------------------------------+
bool CPanel::CreateObject(string name, ENUM_OBJECT type, int x, int y,
                          int w, int h, color clr, string text,
                          int fontSize, string tooltip)
  {
//--- Delete existing object with same name
   if(ObjectFind(0, name) >= 0)
      ObjectDelete(0, name);
//--- Create the object
   if(!ObjectCreate(0, name, type, 0, 0, 0))
      return false;

//--- Set position
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);

//--- Set size for non-label objects
   if(type != OBJ_LABEL)
     {
      ObjectSetInteger(0, name, OBJPROP_XSIZE, w);
      ObjectSetInteger(0, name, OBJPROP_YSIZE, h);
     }

//--- Set color and appearance based on type
   if(type == OBJ_LABEL)
     {
      ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
     }
   else
      if(type == OBJ_BUTTON)
        {
         ObjectSetInteger(0, name, OBJPROP_BGCOLOR, clr);
         ObjectSetInteger(0, name, OBJPROP_COLOR, COLOR_BUTTON_TEXT);
         ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, C'160,160,160');
         ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
         ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
        }
      else
         if(type == OBJ_RECTANGLE_LABEL)
           {
            ObjectSetInteger(0, name, OBJPROP_BGCOLOR, clr);
            ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, C'200,200,200');
            ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
            ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
           }
         else
            if(type == OBJ_EDIT)
              {
               ObjectSetInteger(0, name, OBJPROP_BGCOLOR, clr);
               ObjectSetInteger(0, name, OBJPROP_COLOR, COLOR_SEARCH_TEXT);
               ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, COLOR_SEARCH_BORDER);
               ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
               ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
              }

//--- Set text, font, and tooltip
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
   ObjectSetString(0, name, OBJPROP_FONT, FONT_NAME);

   if(tooltip != "")
      ObjectSetString(0, name, OBJPROP_TOOLTIP, tooltip);

   return true;
  }

The ClearObjects() method removes all dashboard objects from the chart by iterating through all objects and deleting those whose names begin with the OBJ_PREFIX ("DASH_"). This ensures a clean slate before each full redraw.

//+------------------------------------------------------------------+
//| Removes all dashboard objects from the chart                     |
//+------------------------------------------------------------------+
void CPanel::ClearObjects()
  {
   int total = ObjectsTotal(0);
   for(int i = total - 1; i >= 0; i--)
     {
      string name = ObjectName(0, i);
      if(StringFind(name, OBJ_PREFIX) == 0)
         ObjectDelete(0, name);
     }
  }

Drawing the Interface

The Draw() method is the entry point for rendering the entire dashboard. We begin by clearing all existing objects, then draw the main panel background, the header strip, the title, the search box, and the scroll buttons. Next, the method calculates the available vertical space and determines how many rows can fit within the panel (m_maxRows). Finally, we call DrawHeader() and DrawRows() to populate the symbol list.

//+------------------------------------------------------------------+
//| Draws the entire dashboard from scratch                          |
//+------------------------------------------------------------------+
void CPanel::Draw()
  {
   ClearObjects();

//--- Main panel background
   CreateObject(OBJ_PREFIX + "bg", OBJ_RECTANGLE_LABEL, PANEL_X, PANEL_Y,
                PANEL_WIDTH, PANEL_HEIGHT, COLOR_BG, "");
   ObjectSetInteger(0, OBJ_PREFIX + "bg", OBJPROP_BORDER_COLOR, COLOR_BORDER);
   ObjectSetInteger(0, OBJ_PREFIX + "bg", OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, OBJ_PREFIX + "bg", OBJPROP_WIDTH, 1);

//--- Colored header strip
   int headerHeight = 40;
   CreateObject(OBJ_PREFIX + "header_strip", OBJ_RECTANGLE_LABEL,
                PANEL_X + 1, PANEL_Y + 1, PANEL_WIDTH - 2, headerHeight,
                COLOR_HEADER_BG, "");
   ObjectSetInteger(0, OBJ_PREFIX + "header_strip", OBJPROP_BORDER_COLOR, COLOR_BORDER);
   ObjectSetInteger(0, OBJ_PREFIX + "header_strip", OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, OBJ_PREFIX + "header_strip", OBJPROP_WIDTH, 1);

//--- Title
   CreateObject(OBJ_PREFIX + "title", OBJ_LABEL, PANEL_X + 15, PANEL_Y + 10,
                300, 30, COLOR_TITLE, "Multi Chart Dashboard", FONT_TITLE_SIZE, "");

//--- Search box
   int searchY = PANEL_Y + 45;
   CreateObject(OBJ_PREFIX + "search", OBJ_EDIT, PANEL_X + 15, searchY,
                SEARCH_WIDTH, 28, COLOR_SEARCH_BG, "Search symbols...");
   ObjectSetInteger(0, OBJ_PREFIX + "search", OBJPROP_BGCOLOR, COLOR_SEARCH_BG);
   ObjectSetInteger(0, OBJ_PREFIX + "search", OBJPROP_COLOR, COLOR_SEARCH_TEXT);
   ObjectSetString(0, OBJ_PREFIX + "search", OBJPROP_TEXT, "");
   ObjectSetInteger(0, OBJ_PREFIX + "search", OBJPROP_BORDER_COLOR, COLOR_SEARCH_BORDER);
   ObjectSetInteger(0, OBJ_PREFIX + "search", OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, OBJ_PREFIX + "search", OBJPROP_WIDTH, 1);

//--- Scroll buttons (up/down)
   int scrollX = PANEL_X + PANEL_WIDTH - 35;
   CreateObject(OBJ_PREFIX + "scroll_up", OBJ_BUTTON, scrollX, searchY,
                24, 24, COLOR_BUTTON, "▲", FONT_SIZE, "Scroll up");
   CreateObject(OBJ_PREFIX + "scroll_down", OBJ_BUTTON, scrollX, searchY + 28,
                24, 24, COLOR_BUTTON, "▼", FONT_SIZE, "Scroll down");

//--- Compute vertical positions for header and rows
   int headerY = searchY + 28 + 10;
   int rowsStartY = headerY + ROW_HEIGHT + 5;

   int availableHeight = PANEL_HEIGHT - (rowsStartY - PANEL_Y) - 10;
   m_maxRows = availableHeight / (ROW_HEIGHT + ROW_MARGIN);
   if(m_maxRows < 1)
      m_maxRows = 1;

//--- Draw header and rows
   DrawHeader(headerY);
   m_searchText = "";
   m_symMgr.Filter("");
   m_scrollOffset = 0;
   DrawRows(rowsStartY);
   m_needRedraw = false;
  }

The DrawHeader() method creates the column labels ("Symbol", "Status", "Actions") and a separator line. We position the labels using the PANEL_X offsets defined in the shared definitions file, ensuring consistency with the row layout.

//+------------------------------------------------------------------+
//| Draws the header row with column labels                          |
//+------------------------------------------------------------------+
void CPanel::DrawHeader(int y)
  {
   int sepWidth = PANEL_WIDTH - 10 - 45;
//--- Separator line
   CreateObject(OBJ_PREFIX + "sep", OBJ_RECTANGLE_LABEL, PANEL_X + 5, y - 2,
                sepWidth, 2, COLOR_BORDER, "");

//--- Column labels
   CreateObject(OBJ_PREFIX + "hdr_sym", OBJ_LABEL, PANEL_X + 15, y,
                100, ROW_HEIGHT, COLOR_TITLE, "Symbol", FONT_SIZE, "");
   CreateObject(OBJ_PREFIX + "hdr_status", OBJ_LABEL, PANEL_X + 120, y,
                60, ROW_HEIGHT, COLOR_TITLE, "Status", FONT_SIZE, "");
   CreateObject(OBJ_PREFIX + "hdr_actions", OBJ_LABEL, PANEL_X + 198, y,
                60, ROW_HEIGHT, COLOR_TITLE, "Actions", FONT_SIZE, "");
  }

The DrawRows() method is the most complex part of the panel. We iterate through the visible rows, retrieving the corresponding symbol through GetSymbolForRow(). For each valid symbol, the method draws:

  • A row background with alternating colors (COLOR_BG for even rows and COLOR_HIGHLIGHT for odd rows).
  • The symbol name as a label.
  • A status indicator (● for open, ○ for closed) with the appropriate color.
  • An Open button (gray if the chart is already open, green otherwise).
  • A Close button (red if the chart is open, gray otherwise).

For rows beyond the filtered list (that is, empty rows), we delete any existing objects to keep the display clean.

//+------------------------------------------------------------------+
//| Draws the rows of symbols with status and buttons                |
//+------------------------------------------------------------------+
void CPanel::DrawRows(int startY)
  {
   int filteredCount = m_symMgr.GetFilteredCount();
   int rowsToDraw = MathMin(m_maxRows, filteredCount - m_scrollOffset);
   if(rowsToDraw < 0)
      rowsToDraw = 0;

   for(int row = 0; row < m_maxRows; row++)
     {
      int curY = startY + row * (ROW_HEIGHT + ROW_MARGIN);
      string symbol = GetSymbolForRow(row);
      bool validRow = (symbol != "");

      //--- Row background (alternating colors)
      string bgName = OBJ_PREFIX + "bg_" + IntegerToString(row);
      if(validRow)
        {
         color bg = (row % 2 == 0) ? COLOR_BG : COLOR_HIGHLIGHT;
         CreateObject(bgName, OBJ_RECTANGLE_LABEL, PANEL_X + 5, curY,
                      PANEL_WIDTH - 10, ROW_HEIGHT, bg, "");
        }
      else
        {
         if(ObjectFind(0, bgName) >= 0)
            ObjectDelete(0, bgName);
        }

      //--- Symbol label
      string symName = OBJ_PREFIX + "sym_" + IntegerToString(row);
      if(validRow)
         CreateObject(symName, OBJ_LABEL, PANEL_X + 15, curY + 6,
                      100, ROW_HEIGHT, COLOR_TEXT, symbol, FONT_SIZE, "");
      else
        {
         if(ObjectFind(0, symName) >= 0)
            ObjectDelete(0, symName);
        }

      //--- Status indicator (open/closed)
      string statusName = OBJ_PREFIX + "status_" + IntegerToString(row);
      if(validRow)
        {
         bool isOpen = m_chartMgr.IsOpen(symbol);
         UpdateStatusCache(symbol, isOpen);
         string indicator = isOpen ? "●" : "○";
         color clr = isOpen ? COLOR_OPEN : COLOR_CLOSED;
         CreateObject(statusName, OBJ_LABEL, PANEL_X + 120, curY + 4,
                      STATUS_INDICATOR_WIDTH, ROW_HEIGHT, clr, indicator,
                      FONT_SIZE + 2, isOpen ? "Chart open" : "Chart closed");
        }
      else
        {
         if(ObjectFind(0, statusName) >= 0)
            ObjectDelete(0, statusName);
        }

      //--- Open button
      string openName = OBJ_PREFIX + "open_" + IntegerToString(row);
      if(validRow)
        {
         bool isOpen = m_chartMgr.IsOpen(symbol);
         color btnClr = isOpen ? COLOR_BUTTON : COLOR_OPEN;
         string tip = isOpen ? "Already open" : "Open chart for " + symbol;
         CreateObject(openName, OBJ_BUTTON, PANEL_X + 170, curY + 3,
                      BUTTON_WIDTH, BUTTON_HEIGHT, btnClr, "Open",
                      FONT_SIZE, tip);
         ObjectSetInteger(0, openName, OBJPROP_STATE, false);
         ObjectSetInteger(0, openName, OBJPROP_BGCOLOR, btnClr);
        }
      else
        {
         if(ObjectFind(0, openName) >= 0)
            ObjectDelete(0, openName);
        }

      //--- Close button
      string closeName = OBJ_PREFIX + "close_" + IntegerToString(row);
      if(validRow)
        {
         bool isOpen = m_chartMgr.IsOpen(symbol);
         color btnClr = isOpen ? COLOR_CLOSED : COLOR_BUTTON;
         string tip = isOpen ? "Close chart for " + symbol : "Not open";
         CreateObject(closeName, OBJ_BUTTON, PANEL_X + 170 + BUTTON_WIDTH + 6,
                      curY + 3, BUTTON_WIDTH, BUTTON_HEIGHT, btnClr,
                      "Close", FONT_SIZE, tip);
         ObjectSetInteger(0, closeName, OBJPROP_STATE, false);
         ObjectSetInteger(0, closeName, OBJPROP_BGCOLOR, btnClr);
        }
      else
        {
         if(ObjectFind(0, closeName) >= 0)
            ObjectDelete(0, closeName);
        }
     }

   UpdateScrollButtons();
  }

Managing Scroll

The ScrollUp() and ScrollDown() methods adjust the m_scrollOffset variable, which represents the starting index of the visible rows. We check the scroll boundaries before updating the offset. ScrollUp() decrements the offset only if it is greater than zero, while ScrollDown() increments it only if the offset plus the number of visible rows is less than the total number of filtered symbols. After updating the offset, the methods set m_needRedraw = true to trigger a redraw of the panel.

//+------------------------------------------------------------------+
//| Updates the scroll buttons' enabled/disabled state               |
//+------------------------------------------------------------------+
void CPanel::UpdateScrollButtons()
  {
   int total = m_symMgr.GetFilteredCount();
   bool canUp = (m_scrollOffset > 0);
   bool canDown = (m_scrollOffset + m_maxRows < total);

//--- Up button
   if(ObjectFind(0, OBJ_PREFIX + "scroll_up") >= 0)
     {
      ObjectSetInteger(0, OBJ_PREFIX + "scroll_up", OBJPROP_STATE, false);
      ObjectSetInteger(0, OBJ_PREFIX + "scroll_up", OBJPROP_BGCOLOR,
                       canUp ? COLOR_BUTTON : COLOR_BG);
      ObjectSetInteger(0, OBJ_PREFIX + "scroll_up", OBJPROP_BORDER_COLOR,
                       canUp ? C'160,160,160' : C'200,200,200');
     }
//--- Down button
   if(ObjectFind(0, OBJ_PREFIX + "scroll_down") >= 0)
     {
      ObjectSetInteger(0, OBJ_PREFIX + "scroll_down", OBJPROP_STATE, false);
      ObjectSetInteger(0, OBJ_PREFIX + "scroll_down", OBJPROP_BGCOLOR,
                       canDown ? COLOR_BUTTON : COLOR_BG);
      ObjectSetInteger(0, OBJ_PREFIX + "scroll_down", OBJPROP_BORDER_COLOR,
                       canDown ? C'160,160,160' : C'200,200,200');
     }
  }

//+------------------------------------------------------------------+
//| Scrolls the list up by one row                                   |
//+------------------------------------------------------------------+
void CPanel::ScrollUp()
  {
   if(m_scrollOffset > 0)
     {
      m_scrollOffset--;
      m_needRedraw = true;
     }
  }

//+------------------------------------------------------------------+
//| Scrolls the list down by one row                                 |
//+------------------------------------------------------------------+
void CPanel::ScrollDown()
  {
   int total = m_symMgr.GetFilteredCount();
   if(m_scrollOffset + m_maxRows < total)
     {
      m_scrollOffset++;
      m_needRedraw = true;
     }
  }

The UpdateScrollButtons() method visually enables or disables the scroll buttons based on the current position. The background and border colors indicate whether scrolling is possible.

Refreshing the Panel

The Refresh() method is called periodically by the Expert Advisor's timer. It performs two critical checks:

  • The method reads the current text from the search edit box. If the text has changed, we apply the new filter and adjust the scroll offset to ensure the current view remains valid.
  • For each visible symbol, the cached status is compared with the current chart status returned by the chart manager. If a change is detected, the method triggers a redraw.

If either condition sets m_needRedraw to true, we call DrawRows() to update only the rows instead of the entire panel. This partial redraw is more efficient than redrawing the complete dashboard.

//+------------------------------------------------------------------+
//| Refreshes the dashboard – updates filter and status              |
//+------------------------------------------------------------------+
void CPanel::Refresh()
  {
   if(!m_symMgr || !m_chartMgr)
      return;

//--- Read search text from edit box
   string currentSearch = "";
   if(ObjectFind(0, OBJ_PREFIX + "search") >= 0)
      currentSearch = ObjectGetString(0, OBJ_PREFIX + "search", OBJPROP_TEXT);
   if(currentSearch != m_searchText)
     {
      m_searchText = currentSearch;
      m_symMgr.Filter(m_searchText);
      if(m_scrollOffset >= m_symMgr.GetFilteredCount())
         m_scrollOffset = MathMax(0, m_symMgr.GetFilteredCount() - m_maxRows);
      m_needRedraw = true;
     }

//--- Check if any open status changed (compare with cache)
   int filteredCount = m_symMgr.GetFilteredCount();
   for(int i = 0; i < MathMin(filteredCount, m_maxRows + m_scrollOffset); i++)
     {
      string sym = m_symMgr.GetFilteredSymbol(i);
      if(sym == "")
         continue;
      bool nowOpen = m_chartMgr.IsOpen(sym);
      bool cached = GetCachedStatus(sym);
      if(nowOpen != cached)
        {
         m_needRedraw = true;
         break;
        }
     }

//--- Redraw if needed
   if(m_needRedraw)
     {
      int searchY = PANEL_Y + 45;
      int headerY = searchY + 28 + 10;
      int rowsStartY = headerY + ROW_HEIGHT + 5;
      DrawRows(rowsStartY);
      m_needRedraw = false;
     }
  }

Handling User Clicks

We implement the HandleClick() method to process clicks on the dashboard's interactive elements. The method first checks whether the user clicked one of the scroll buttons. If so, we call the corresponding scroll method and return. For the Open and Close buttons, the method extracts the row index from the object name, retrieves the corresponding symbol, and we call m_chartMgr.Open(symbol) or m_chartMgr.Close(symbol) as appropriate.

//+------------------------------------------------------------------+
//| Handles clicks on dashboard objects                              |
//+------------------------------------------------------------------+
void CPanel::HandleClick(string objectName)
  {
//--- Scroll buttons
   if(objectName == OBJ_PREFIX + "scroll_up")
     {
      ScrollUp();
      return;
     }
   if(objectName == OBJ_PREFIX + "scroll_down")
     {
      ScrollDown();
      return;
     }

//--- Parse row index from button names: "DASH_open_3" -> row=3
   int pos = StringFind(objectName, "_");
   if(pos == -1)
      return;
   string suffix = StringSubstr(objectName, pos + 1);
   int row = -1;
   if(StringFind(suffix, "open_") == 0)
      row = (int)StringToInteger(StringSubstr(suffix, 5));
   else
      if(StringFind(suffix, "close_") == 0)
         row = (int)StringToInteger(StringSubstr(suffix, 6));
      else
         return;

   if(row < 0 || row >= m_maxRows)
      return;
   string symbol = GetSymbolForRow(row);
   if(symbol == "")
      return;

//--- Perform action (open or close)
   if(StringFind(objectName, "open_") != -1)
      m_chartMgr.Open(symbol);
   else
      if(StringFind(objectName, "close_") != -1)
         m_chartMgr.Close(symbol);

   m_needRedraw = true;
   Refresh();
  }

The complete implementation is omitted from this section due to its length. All source files are available as a ZIP archive attached at the end of this article.


5. Assembling the Expert Advisor

With the supporting classes complete, we move to the final piece: the main Expert Advisor file, ChartDashboardEA.mq5. This file serves as the entry point for the entire application, orchestrating the initialization, execution, and cleanup of the dashboard. Its responsibilities are threefold: instantiating the manager and panel objects, setting up the timer for periodic refreshes, and handling chart events such as object clicks and edit box submissions.

Global Instance Variables

We begin by declaring global pointers to the three core objects. These are declared at the top of the file so they can be accessed from any of the EA's event handlers.

//--- Global instances
CSymbolManager *g_symMgr = NULL;
CChartManager  *g_chartMgr = NULL;
CPanel         *g_panel = NULL;

  • g_symMgr manages the symbol list and filtering.
  • g_chartMgr handles opening, closing, and checking chart status.
  • g_panel draws the dashboard and coordinates user interactions.

Using global pointers simplifies the code by avoiding the need to pass references through event handlers. However, we must ensure they are properly cleaned up in OnDeinit() to prevent memory leaks.

Initialization (OnInit)

The OnInit() function is called when the Expert Advisor is attached to a chart. During initialization, OnInit() performs the following steps:

  1. Create and initialize the symbol manager: We instantiate CSymbolManager and call its Initialize() method to load the list of tradable symbols from the broker. If the initialization fails—for example, if no symbols are available—the function prints an error message and returns INIT_FAILED, preventing the Expert Advisor from starting.
  2. Create the chart manager and panel: We instantiate CChartManager and CPanel. We then connect the panel to both managers using the SetManagers() method, providing it with access to the symbol data and chart management operations.
  3. Draw the dashboard: We call the Draw() method to render the entire panel. At this point, the symbol list has been loaded, filtered using an empty search string (displaying all symbols), and drawn on the chart.
  4. Start the timer: We call EventSetTimer(1) to trigger the OnTimer() event every second. The timer drives the periodic refresh of the dashboard, ensuring that the status indicators and symbol list remain up to date.
  5. Return success: If all initialization steps complete successfully, the function returns INIT_SUCCEEDED, indicating that the Expert Advisor is ready to run.

      //+------------------------------------------------------------------+
      //| Expert initialization function                                   |
      //+------------------------------------------------------------------+
      int OnInit()
        {
      //--- Create and initialize symbol manager
         g_symMgr = new CSymbolManager();
         if(!g_symMgr.Initialize())
           {
            Print("Failed to load symbols");
            return INIT_FAILED;
           }
      
      //--- Create chart manager and panel
         g_chartMgr = new CChartManager();
         g_panel = new CPanel();
         g_panel.SetManagers(g_symMgr, g_chartMgr);
         g_panel.Draw();
      
      //--- Start timer for periodic refresh
         EventSetTimer(1);
         return INIT_SUCCEEDED;
        }

      Deinitialization (OnDeinit)

      The OnDeinit() function is called when the Expert Advisor is removed from the chart, the terminal is closed, or the user manually stops the EA. Its role is to perform a clean shutdown:

      1. Stop the timer: EventKillTimer() prevents any further timer events from firing after the EA has been unloaded.
      2. Delete objects: We delete the panel and the two managers in reverse order of creation. Deleting the panel first ensures that any references it holds to the managers are released before the managers themselves are destroyed.
      3. Remove graphical objects: Finally, we call ObjectsDeleteAll(0, OBJ_PREFIX) to remove all dashboard objects from the chart. This is a safety net in case any objects were not properly cleaned up by the panel's destructor.

        //+------------------------------------------------------------------+
        //| Expert deinitialization function                                 |
        //+------------------------------------------------------------------+
        void OnDeinit(const int reason)
          {
        //--- Kill timer and clean up objects
           EventKillTimer();
           if(g_panel)
              delete g_panel;
           if(g_chartMgr)
              delete g_chartMgr;
           if(g_symMgr)
              delete g_symMgr;
           ObjectsDeleteAll(0, OBJ_PREFIX);
          }
        

        Timer Handler (OnTimer)

        The OnTimer() function is called every second (as configured in OnInit). Its sole purpose is to refresh the dashboard by calling g_panel.Refresh(). This method checks for changes in the search text and updates the status indicators, ensuring that the dashboard always reflects the current state of open charts.

        //+------------------------------------------------------------------+
        //| Timer function                                                   |
        //+------------------------------------------------------------------+
        void OnTimer()
          {
        //--- Refresh the panel periodically to update statuses
           if(g_panel)
              g_panel.Refresh();
          }

        The timer interval of one second strikes a good balance between responsiveness and performance. The refresh operation is lightweight because the panel only redraws rows when something has actually changed — it does not perform a full redraw on every tick.

        Chart Event Handler (OnChartEvent)

        The OnChartEvent() function processes user interactions with the chart. We are interested in two specific events:

        • CHARTEVENT_OBJECT_CLICK: This event fires when the user clicks on any graphical object on the chart. The object name (sparam) is passed to HandleClick(), which determines whether the click was on an Open/Close button or a scroll button and performs the appropriate action.
        • CHARTEVENT_OBJECT_ENDEDIT: This event fires when the user finishes editing text in an edit box (i.e., the search box). The handler checks whether the object name matches the search box prefix (OBJ_PREFIX + "search"). If so, we call g_panel.Refresh() to apply the new search filter. The refresh will read the updated text from the edit box and update the symbol list accordingly.

          //+------------------------------------------------------------------+
          //| Chart event handler                                              |
          //+------------------------------------------------------------------+
          void OnChartEvent(const int id,
                            const long &lparam,
                            const double &dparam,
                            const string &sparam)
            {
          //--- Handle clicks on dashboard objects
             if(id == CHARTEVENT_OBJECT_CLICK)
               {
                if(g_panel)
                   g_panel.HandleClick(sparam);
               }
          
          //--- Handle edit finish on the search box
             if(id == CHARTEVENT_OBJECT_ENDEDIT)
               {
                if(StringFind(sparam, OBJ_PREFIX + "search") == 0)
                   if(g_panel)
                      g_panel.Refresh();
               }
            }
          Below is the full ChartDashboardEA.mq5 file with all methods implemented:
          //+------------------------------------------------------------------+
          //|                                             ChartDashboardEA.mq5 |
          //|                                  Copyright 2026, MetaQuotes Ltd. |
          //|                          https://www.mql5.com/en/users/lynnchris |
          //+------------------------------------------------------------------+
          #property copyright "Copyright 2026, MetaQuotes Ltd."
          #property link      "https://www.mql5.com/en/users/lynnchris"
          #property version   "1.00"
          #property strict
          
          //--- Include all required classes
          #include <Classes/DashboardDefines.mqh>
          #include <Classes/CSymbolManager.mqh>
          #include <Classes/CChartManager.mqh>
          #include <Classes/CPanel.mqh>
          
          //--- Global instances
          CSymbolManager *g_symMgr = NULL;
          CChartManager  *g_chartMgr = NULL;
          CPanel         *g_panel = NULL;
          
          //+------------------------------------------------------------------+
          //| Expert initialization function                                   |
          //+------------------------------------------------------------------+
          int OnInit()
            {
          //--- Create and initialize symbol manager
             g_symMgr = new CSymbolManager();
             if(!g_symMgr.Initialize())
               {
                Print("Failed to load symbols");
                return INIT_FAILED;
               }
          
          //--- Create chart manager and panel
             g_chartMgr = new CChartManager();
             g_panel = new CPanel();
             g_panel.SetManagers(g_symMgr, g_chartMgr);
             g_panel.Draw();
          
          //--- Start timer for periodic refresh
             EventSetTimer(1);
             return INIT_SUCCEEDED;
            }
          
          //+------------------------------------------------------------------+
          //| Expert deinitialization function                                 |
          //+------------------------------------------------------------------+
          void OnDeinit(const int reason)
            {
          //--- Kill timer and clean up objects
             EventKillTimer();
             if(g_panel)
                delete g_panel;
             if(g_chartMgr)
                delete g_chartMgr;
             if(g_symMgr)
                delete g_symMgr;
             ObjectsDeleteAll(0, OBJ_PREFIX);
            }
          
          //+------------------------------------------------------------------+
          //| Timer function                                                   |
          //+------------------------------------------------------------------+
          void OnTimer()
            {
          //--- Refresh the panel periodically to update statuses
             if(g_panel)
                g_panel.Refresh();
            }
          
          //+------------------------------------------------------------------+
          //| Chart event handler                                              |
          //+------------------------------------------------------------------+
          void OnChartEvent(const int id,
                            const long &lparam,
                            const double &dparam,
                            const string &sparam)
            {
          //--- Handle clicks on dashboard objects
             if(id == CHARTEVENT_OBJECT_CLICK)
               {
                if(g_panel)
                   g_panel.HandleClick(sparam);
               }
          
          //--- Handle edit finish on the search box
             if(id == CHARTEVENT_OBJECT_ENDEDIT)
               {
                if(StringFind(sparam, OBJ_PREFIX + "search") == 0)
                   if(g_panel)
                      g_panel.Refresh();
               }
            }
          //+------------------------------------------------------------------+

          Outcomes

          After implementing all five files, we compiled the Expert Advisor in MetaEditor. We then tested it in MetaTrader 5 to verify that every component worked as expected. The GIF below demonstrates the dashboard in action. The navigation arrows scroll through the available symbols smoothly, the search box filters symbols instantly, and the chart control buttons correctly open and close charts with a single click.


          The testing confirms that all major features operate as intended, resulting in a responsive and user-friendly interface. Instead of relying on the traditional Market Watch workflow, we now have a centralized chart and symbol management system that enables faster chart navigation, efficient symbol searching, and streamlined chart control from a single interactive dashboard.

          This implementation successfully achieves the objective set at the beginning of the article by simplifying multi-chart management and providing a more efficient workflow for MT5 users. 


          Conclusion

          We have built a complete interactive dashboard for managing charts in MetaTrader 5 using object-oriented programming principles. The project consists of five files:

          • DashboardDefines.mqh — Centralizes all constants and configurations.
          • CSymbolManager.mqh — Manages the symbol list and provides filtering.
          • CChartManager.mqh — Handles chart operations (open, close, status check).
          • CPanel.mqh — Draws the GUI and coordinates user interactions.
          • ChartDashboardEA.mq5 — Assembles all components and serves as the entry point.

          Keeping symbol and chart management in one place makes opening and closing charts much easier. The dashboard's search and scroll capabilities make it easy to navigate large symbol lists, while the real-time status indicators provide instant feedback on which charts are already open. The result is a professional, responsive tool that accelerates multi-chart analysis and enhances overall productivity in MetaTrader 5.

          Attachments

          The table below summarizes the attached files, their installation locations, and their respective responsibilities.

          File Name Location Description
          DashboardDefines.mqh
          MQL5\Include\ChartDashboard\
          Shared dashboard definitions, including layout, colors, fonts, and object prefixes.
          CSymbolManager.mqh
          MQL5\Include\ChartDashboard\
          Implements symbol loading, sorting, filtering, and search functionality.
          CChartManager.mqh
          MQL5\Include\ChartDashboard\
          Implements chart management operations, including opening, closing, and status checks.
          CPanel.mqh
          MQL5\Include\ChartDashboard\
          Implements the dashboard interface, rendering, scrolling, refreshing, and user interaction.
          ChartDashboardEA.mq5
          MQL5\Experts\ChartDashboard\
          Main Expert Advisor that initializes the managers, displays the dashboard, and processes events.
          MQL5.zip   Root Archive Complete project archive containing all source files arranged in the required MetaTrader 5 directory structure. Extract the archive into the terminal's Data Folder so all files are placed automatically in their correct locations.


          Attached files |
          CSymbolManager.mqh (7.43 KB)
          CChartManager.mqh (5.58 KB)
          CPanel.mqh (23.62 KB)
          MQL5.zip (10.25 KB)
          Formulating Dynamic Multi-Pair EA (Part 10): Asymmetric Stop-Loss Logic Based on Pair-Specific Volatility Signatures Formulating Dynamic Multi-Pair EA (Part 10): Asymmetric Stop-Loss Logic Based on Pair-Specific Volatility Signatures
          The EA learns each symbol's volatility profile before trading by processing 1000 bars and summarizing candle ranges, bodies and wicks, noise ratio, trend runs, pullback size, and true‑range dispersion. A classifier assigns regime and structure labels per pair. The stop‑loss optimizer maps those labels to a symbol‑specific ATR multiplier, and the risk module sizes lots to maintain constant percentage risk.
          Automating Classic Market Methods in MQL5 (Part 5): The Original Turtle Trading Rules Automating Classic Market Methods in MQL5 (Part 5): The Original Turtle Trading Rules
          This article builds a complete MQL5 Expert Advisor that implements the original Turtle Trading rules from Curtis Faith. It covers both systems: 20/55-day breakouts, the System 1 skip rule, N (Wilder ATR) for volatility-adjusted sizing, a four‑unit pyramid with N/2 adds, a unified 2N stop, and 10/20-day exits. You will get compilable code, implementation details, and a backtesting procedure on EURUSD.
          Trading Options Without Options (Part 3): Complex Option Strategies Trading Options Without Options (Part 3): Complex Option Strategies
          The article discusses flat (non-directional) and trend-following (directional) option strategies and their implementation in MQL5. The EA described in the previous article is updated. The display of option levels has been added. Now it is time to examine the strategies used by options traders in practice and put them into action.
          Automating Classic Market Methods in MQL5 (Part 4): Mark Minervini's Trend Template Automating Classic Market Methods in MQL5 (Part 4): Mark Minervini's Trend Template
          This article presents TrendTemplateEA, an Expert Advisor implementing Mark Minervini's eight-condition trend template for daily forex charts. It evaluates all conditions on every bar and enters only when they are simultaneously satisfied, using RSI above 50 in place of the stock market RS rating. The entry trigger is a 20-bar high breakout on expanding volume, with all rules coded and testable in MQL5.