English Русский Deutsch 日本語 Português
preview
制作仪表板以显示指标和EA中的数据

制作仪表板以显示指标和EA中的数据

MetaTrader 5示例 | 28 二月 2024, 10:51
682 3
Artyom Trishkin
Artyom Trishkin

目录


概述

在本文中,我将创建一个可以显示开发人员指定的数据的仪表板。这样的面板将便于在图表上直观地显示数据和进行可视化调试,因为在面板上查看必要的值比在调试器中跟踪它们更方便。我指的是根据某些数据值调试策略的情况。

我将以终端数据窗口原型的形式制作面板,并用相同的数据填充:


图1数据窗口和仪表板

我们的自定义面板将允许我们向其添加任何所需数量的必要数据,对其进行签名,以及显示和更新程序代码的读数。

应该可以用鼠标在图表周围移动面板,将其固定在所需的图表位置,以及折叠/展开它。为了方便在面板上放置数据,可以显示具有指定行数和列数的表格。该表中的数据可以显示在日志中(每个表单元格的X和Y坐标),并通过编程获得,以指示该数据应位于的行和列的索引,或者只需打印坐标并在显示文本和数据时将所需坐标输入到代码中。第一种方法由于其完全自动化而更加方便。面板也将有一个活动的关闭按钮,但我们将其处理委托给控制程序,因为只有程序开发人员才能决定如何对按下关闭按钮做出反应。单击按钮时,将向程序事件处理程序发送一个自定义事件。开发人员可以自行处理。


用于获取表格数据的类

由于根据可见的或者虚拟的预先确定的坐标在面板上排列数据很方便,我们将首先创建用于排列表格数据的类。表格可以表示为一个简单的网格,其线交点将是表格单元格的坐标。可以将任何可见数据放置在这样的坐标处。该表具有一定数量的行(水平行),每行具有一定数量(垂直行)的单元格。在一个简单的网格表中,所有行都有相同数量的单元格。

基于此,我们需要三个类:

  1. 表格单元格类,
  2. 表格行类,
  3. 表格类。

表格单元格类包括表格中的行索引和列索引,以及面板中表格单元格视觉位置的坐标——相对于面板左上角表格原点的X和Y坐标。

表格行类包括表单元格类。我们可以在一行中创建所需数量的单元格。

表格类包括一个表行列表。可以创建表中的行,并添加所需数量的行。

让我们简单地看一下这三个类。


表格单元格类

//+------------------------------------------------------------------+
//| Table cell class                                                 |
//+------------------------------------------------------------------+
class CTableCell : public CObject
  {
private:
   int               m_row;                     // Row
   int               m_col;                     // Column
   int               m_x;                       // X coordinate
   int               m_y;                       // Y coordinate
public:
//--- Methods of setting values
   void              SetRow(const uint row)     { this.m_row=(int)row;  }
   void              SetColumn(const uint col)  { this.m_col=(int)col;  }
   void              SetX(const uint x)         { this.m_x=(int)x;      }
   void              SetY(const uint y)         { this.m_y=(int)y;      }
   void              SetXY(const uint x,const uint y)
                       {
                        this.m_x=(int)x;
                        this.m_y=(int)y;
                       }
//--- Methods of obtaining values
   int               Row(void)            const { return this.m_row;    }
   int               Column(void)         const { return this.m_col;    }
   int               X(void)              const { return this.m_x;      }
   int               Y(void)              const { return this.m_y;      }
//--- Virtual method for comparing two objects
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CTableCell *compared=node;
                        return(this.Column()>compared.Column() ? 1 : this.Column()<compared.Column() ? -1 : 0);
                       }
//--- Constructor/destructor
                     CTableCell(const int row,const int column) : m_row(row),m_col(column){}
                    ~CTableCell(void){}
  };

该类继承自基类,用于构建MQL5标准库,因为它将搭配MQL5 标准库的CArrayObj列表,而列表只能包含CObject对象或从基本CObject继承的对象。

所有变量和方法的函数都非常透明且易于理解。变量用于存储表格行(Row)和列(Column)的值,而坐标是面板中表格单元格左上角的相对坐标。使用这些坐标,可以绘制一些东西或将其放置在面板上。

需要Compare虚拟方法来查找和比较两个表单元格对象,该方法是在基类CObject类中声明的:

   //--- method of comparing the objects
   virtual int       Compare(const CObject *node,const int mode=0) const { return(0);      }

它返回空值,应在继承的类中重写。

由于表格单元格是添加到表格行中的,即在视觉上是水平的,因此搜索和比较应按水平单元格编号进行,即按列值进行。这正是重写的虚拟Compare方法在这里所做的:

//--- Virtual method for comparing two objects
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CTableCell *compared=node;
                        return(this.Column()>compared.Column() ? 1 : this.Column()<compared.Column() ? -1 : 0);
                       }

如果当前对象的列值大于正在比较的对象的值(传递给方法的指针),则返回1如果列值小于要比较的列值,则返回-1否则,返回零。因此,该方法返回的零值表示被比较的对象的值相等。


表格行类

单元格对象将添加到表格行中。如果一行中的单元格是水平相邻的,则表中的行就是垂直排列的。
这里我们只需要知道行索引及其在面板上的Y坐标

//+------------------------------------------------------------------+
//| Table row class                                                  |
//+------------------------------------------------------------------+
class CTableRow : public CObject
  {
private:
  CArrayObj          m_list_cell;               // Cell list
  int                m_row;                     // Row index
  int                m_y;                       // Y coordinate
public:
//--- Return the list of table cells in a row
   CArrayObj        *GetListCell(void)       { return &this.m_list_cell;         }
//--- Return (1) the number of table cells in a row (2) the row index in the table
   int               CellsTotal(void)  const { return this.m_list_cell.Total();  }
   int               Row(void)         const { return this.m_row;                }
//--- (1) Set and (2) return the Y row coordinate
   void              SetY(const int y)       { this.m_y=y;                       }
   int               Y(void)           const { return this.m_y;                  }
//--- Add a new table cell to the row
   bool              AddCell(CTableCell *cell)
                       {
                        this.m_list_cell.Sort();
                        if(this.m_list_cell.Search(cell)!=WRONG_VALUE)
                          {
                           ::PrintFormat("%s: Table cell with index %lu is already in the list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        if(!this.m_list_cell.InsertSort(cell))
                          {
                           ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        return true;
                       }
//--- Return the pointer to the specified cell in the row
   CTableCell       *GetCell(const int column)
                       {
                        const CTableCell *obj=new CTableCell(this.m_row,column);
                        int index=this.m_list_cell.Search(obj);
                        delete obj;
                        return this.m_list_cell.At(index);
                       }
//--- Virtual method for comparing two objects
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CTableRow *compared=node;
                        return(this.Row()>compared.Row() ? 1 : this.Row()<compared.Row() ? -1 : 0);
                       }
//--- Constructor/destructor
                     CTableRow(const int row) : m_row(row)  { this.m_list_cell.Clear();   }
                    ~CTableRow(void)                        { this.m_list_cell.Clear();   }
  };

包含新添加的单元格对象的CArrayObj列表在类中声明。

Compare虚拟方法中,我们按行索引值(Row)比较对象,因为添加新行时,我们只需要按行索引进行搜索。如果未找到具有此类索引的行,则搜索方法(Search)将返回-1,否则搜索将返回列表中找到的对象位置的索引。Search方法在CArrayObj类中声明和实现:

//+------------------------------------------------------------------+
//| Search of position of element in a sorted array                  |
//+------------------------------------------------------------------+
int CArrayObj::Search(const CObject *element) const
  {
   int pos;
//--- check
   if(m_data_total==0 || !CheckPointer(element) || m_sort_mode==-1)
      return(-1);
//--- search
   pos=QuickSearch(element);
   if(m_data[pos].Compare(element,m_sort_mode)==0)
      return(pos);
//--- not found
   return(-1);
  }

正如我们所看到的,它使用比较两个对象的虚拟Compare方法来确定对象的相等性。


向列表中添加新单元格的方法:

//--- Add a new table cell to the row
   bool              AddCell(CTableCell *cell)
                       {
                        this.m_list_cell.Sort();
                        if(this.m_list_cell.Search(cell)!=WRONG_VALUE)
                          {
                           ::PrintFormat("%s: Table cell with index %lu is already in the list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        if(!this.m_list_cell.InsertSort(cell))
                          {
                           ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        return true;
                       }

由于单元格严格按照列编号(Column)一个接一个地位于列表中,并且我们按排序顺序添加它们,因此列表应首先设置排序列表标志如果搜索未返回-1,则表示该对象已存在于列表中,会将相应的消息发送到日志,并返回false如果未能将对象指针添加到列表,就要发出通知并返回false如果一切正常,则返回true


返回指向行中指定单元格的指针的方法:

//--- Return the pointer to the specified cell in the row
   CTableCell       *GetCell(const int column)
                       {
                        const CTableCell *obj=new CTableCell(this.m_row,column);
                        int index=this.m_list_cell.Search(obj);
                        delete obj;
                        return this.m_list_cell.At(index);
                       }

标准库的CArrayObj类的Search方法根据指针传递给该方法的对象的实例在列表中搜索相等项。因此,在这里,我们创建一个新的临时对象指定在其构造函数中传递给方法的列号,以及在列表中获取对象索引,或者如果在列表中没有找到具有此类参数的对象,则为-1。此外,请确保删除临时对象返回指向列表中找到的对象的指针
如果找不到对象并且索引等于-1,则CArrayObj类的At方法将返回NULL


表格类

该表由行列表组成,而行列表又由单元格列表组成。换言之,表格数据类仅包含CArrayObj对象——要创建的行以及添加和接收表行和单元格的方法:

//+------------------------------------------------------------------+
//| Table data class                                                 |
//+------------------------------------------------------------------+
class CTableData : public CObject
  {
private:
   CArrayObj         m_list_rows;               // List of rows
public:
//--- Return the list of table rows
   CArrayObj        *GetListRows(void)       { return &this.m_list_rows;   }
//--- Add a new row to the table
   bool              AddRow(CTableRow *row)
                       {
                        //--- Set the sorted list flag
                        this.m_list_rows.Sort();
                        //--- If such an object is already in the list (the search returns the object index, not -1),
                        //--- inform of that in the journal and return 'false'
                        if(this.m_list_rows.Search(row)!=WRONG_VALUE)
                          {
                           ::PrintFormat("%s: Table row with index %lu is already in the list",__FUNCTION__,row.Row());
                           return false;
                          }
                        //--- If failed to add the pointer to the sorted list, inform of that and return 'false'
                        if(!this.m_list_rows.InsertSort(row))
                          {
                           ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,row.Row());
                           return false;
                          }
                        //--- Successful - return 'true'
                        return true;
                       }
//--- Return the pointer to the (1) specified row and (2) specified cell in the specified table row
   CTableRow        *GetRow(const int index) { return this.m_list_rows.At(index);   }
   CTableCell       *GetCell(const int row,const int column)
                       {
                        //--- Get a pointer to a string object in a list of strings
                        CTableRow *row_obj=this.GetRow(row);
                        //--- If failed to get the object, return NULL
                        if(row_obj==NULL)
                           .return NULL;
                        //--- Get the pointer to the cell object in the row by a column number and
                        CTableCell *cell=row_obj.GetCell(column);
                        //--- return the result (object pointer or NULL)
                        return cell;
                       }
//--- Write the X and Y coordinates of the specified table cell into the variables passed to the method
   void              CellXY(const uint row,const uint column, int &x, int &y)
                       {
                        x=WRONG_VALUE;
                        y=WRONG_VALUE;
                        CTableCell *cell=this.GetCell(row,column);
                        if(cell==NULL)
                           return;
                        x=cell.X();
                        y=cell.Y();
                       }
//--- Return the X coordinate of the specified table cell
   int               CellX(const uint row,const uint column)
                       {
                        CTableCell *cell=this.GetCell(row,column);
                        return(cell!=NULL ? cell.X() : WRONG_VALUE);
                       }
//--- Return the Y coordinate of the specified table cell
   int               CellY(const uint row,const uint column)
                       {
                        CTableCell *cell=this.GetCell(row,column);
                        return(cell!=NULL ? cell.Y() : WRONG_VALUE);
                       }
//--- Return the number of table (1) rows and (2) columns
   int               RowsTotal(void)            { return this.m_list_rows.Total();  }
   int               ColumnsTotal(void)
                       {
                        //--- If there is no row in the list, return 0
                        if(this.RowsTotal()==0)
                           return 0;
                        //--- Get a pointer to the first row and return the number of cells in it
                        CTableRow *row=this.GetRow(0);
                        return(row!=NULL ? row.CellsTotal() : 0);
                       }
//--- Return the total number of cells in the table
   int               CellsTotal(void){ return this.RowsTotal()*this.ColumnsTotal(); }
//--- Clear lists of rows and table cells
   void              Clear(void)
                       {
                        //--- In the loop by the number of rows in the list of table rows, 
                        for(int i=0;i<this.m_list_rows.Total();i++)
                          {
                           //--- get the pointer to the next row
                           CTableRow *row=this.m_list_rows.At(i);
                           if(row==NULL)
                              continue;
                           //--- get cell list from the obtained row object,
                           CArrayObj *list_cell=row.GetListCell();
                           //--- clear cell list
                           if(list_cell!=NULL)
                              list_cell.Clear();
                          }
                        //--- Clear cell list
                        this.m_list_rows.Clear();
                       }                
//--- Print the table cell data in the journal
   void              Print(const uint indent=0)
                       {
                        //--- Print the header in the journal
                        ::PrintFormat("Table: Rows: %lu, Columns: %lu",this.RowsTotal(),this.ColumnsTotal());
                        //--- In the loop by table rows
                        for(int r=0;r<this.RowsTotal();r++)
                           //--- in the loop by the next row cells,
                           for(int c=0;c<this.ColumnsTotal();c++)
                             {
                              //--- get the pointer to the next cell and display its data in the journal
                              CTableCell *cell=this.GetCell(r,c);
                              if(cell!=NULL)
                                 ::PrintFormat("%*s%-5s %-4lu %-8s %-6lu %-8s %-6lu %-8s %-4lu",indent,"","Row",r,"Column",c,"Cell X:",cell.X(),"Cell Y:",cell.Y());
                             }
                       }
//--- Constructor/destructor
                     CTableData(void)  { this.m_list_rows.Clear();   }
                    ~CTableData(void)  { this.m_list_rows.Clear();   }
  };

这里几乎所有的方法都在代码中进行了注释。我将只注意返回表中的行数和列数以及表单元格总数的方法。

行数是行列表大小。我们获得了表的确切行数:

   int               RowsTotal(void)            { return this.m_list_rows.Total();  }

与行不同,这里的列数仅在假设每行的列数相同的情况下返回。因此,仅返回第一行中的单元格数(列表中具有零索引的行):

   int               ColumnsTotal(void)
                       {
                        //--- If there is no row in the list, return 0
                        if(this.RowsTotal()==0)
                           return 0;
                        //--- Get a pointer to the first row and return the number of cells in it
                        CTableRow *row=this.GetRow(0);
                        return(row!=NULL ? row.CellsTotal() : 0);
                       }

在扩展和最终确定此类时,可以添加返回指定行中单元格数的方法,因此,不通过将表中的(确切的)行数乘以第一行中的单元格数来返回表中的单元格总数(假设如上所述):

   int               CellsTotal(void){ return this.RowsTotal()*this.ColumnsTotal(); }

这对于这个版本的表格数据类的精确计算来说已经足够了,而且还没有必要使事情复杂化——这些只是信息面板类的辅助类,我们将使用表格(晶格)标记将数据放置在面板上。


仪表板类

让我们定义所有可能的鼠标状态:

  • 鼠标按钮(左、右)未被按下,
  • 在面板窗口外按下鼠标按钮,
  • 在面板窗口内按下鼠标按钮,
  • 在面板窗口标题内按下鼠标按钮,
  • 在“关闭”控制元件上按下鼠标按钮,
  • 在“折叠/展开”控制元件上按下鼠标按钮,
  • 在“大头针”控制元件上按下鼠标按钮,
  • 鼠标光标在面板窗口之外,
  • 鼠标光标位于面板窗口内,
  • 鼠标光标位于面板窗口标题内,
  • 鼠标光标位于“关闭”控制元素内,
  • 鼠标光标位于“折叠/展开”控制元素内,
  • 鼠标光标位于“大头针”控制元素内。

让我们创建相应的枚举:

enum ENUM_MOUSE_STATE
  {
   MOUSE_STATE_NOT_PRESSED,
   MOUSE_STATE_PRESSED_OUTSIDE_WINDOW,
   MOUSE_STATE_PRESSED_INSIDE_WINDOW,
   MOUSE_STATE_PRESSED_INSIDE_HEADER,
   MOUSE_STATE_PRESSED_INSIDE_CLOSE,
   MOUSE_STATE_PRESSED_INSIDE_MINIMIZE,
   MOUSE_STATE_PRESSED_INSIDE_PIN,
   MOUSE_STATE_OUTSIDE_WINDOW,
   MOUSE_STATE_INSIDE_WINDOW,
   MOUSE_STATE_INSIDE_HEADER,
   MOUSE_STATE_INSIDE_CLOSE,
   MOUSE_STATE_INSIDE_MINIMIZE,
   MOUSE_STATE_INSIDE_PIN
  };

目前,已经实现了在面板的控制元件上按住或点击鼠标按钮的跟踪。换句话说,第一次按键是用于固定状态的触发器。然而,在Windows应用程序中,这样的触发器是在按下按钮后释放按钮——点击。按住可用于拖动对象。但就目前而言,一个简单的解决方案对我们来说已经足够了——第一次按下将被视为点击或按住。如果我们进一步开发面板,将有可能补充鼠标按钮的处理,以匹配上述行为。

CDashboard信息面板类将由两个元素组成:画布(背景)和工作区,画布上将绘制面板设计和控制元素,工作区上放置的数据将绘制在画布上。工作空间将始终是完全透明的,画布对于表头和其他所有内容将具有单独的透明度值:

图2. 只有标题透明度不同的画布和带边框的字段


标题下的区域由一个框架勾勒,用于在其上放置一个工作区域。该区域完全透明,包含数据文本。此外,标题下方的画布区域可用于可视化设计。在这种情况下,将在其上绘制表格:

图3. 12行4列的表格

一个包含数据的工作区域叠加在设计的画布上。这样,我们就得到了一个完整的面板:

图4. 带有12x2背景表格和数据的面板


我们将把一些面板参数的值保存到全局终端变量中,以便面板记住其状态,并在重新启动时恢复它们-X和Y坐标、最小化状态和面板可移动性标志。以折叠形式将面板固定在图表上时,会保存此固定位置,下次折叠固定面板时,它会显示在保存的位置。

图5. 如果面板以折叠的形式固定,它会“记住”其固定位置


我们可以看到,为了记住折叠面板的绑定位置,需要将其折叠、移动到绑定位置并固定。当面板固定在折叠状态时,会记住其位置。然后可以展开、取消固定和移动它。为了使面板返回到保存的绑定位置,应将其固定并折叠。在没有固定的情况下,面板将折叠到其当前位置。


类的主体:

//+------------------------------------------------------------------+
//| Dashboard class                                                  |
//+------------------------------------------------------------------+
class CDashboard : public CObject
  {
private:
   CCanvas           m_canvas;                  // Canvas
   CCanvas           m_workspace;               // Work space
   CTableData        m_table_data;              // Table cell array
   ENUM_PROGRAM_TYPE m_program_type;            // Program type
   ENUM_MOUSE_STATE  m_mouse_state;             // Mouse button status
   uint              m_id;                      // Object ID
   long              m_chart_id;                // ChartID
   int               m_chart_w;                 // Chart width
   int               m_chart_h;                 // Chart height
   int               m_x;                       // X coordinate
   int               m_y;                       // Y coordinate
   int               m_w;                       // Width
   int               m_h;                       // Height
   int               m_x_dock;                  // X coordinate of the pinned collapsed panel
   int               m_y_dock;                  // Y coordinate of the pinned collapsed panel
   
   bool              m_header;                  // Header presence flag
   bool              m_butt_close;              // Close button presence flag
   bool              m_butt_minimize;           // Collapse/expand button presence flag
   bool              m_butt_pin;                // Pin button presence flag
   bool              m_wider_wnd;               // Flag for exceeding the horizontal size of the window width panel
   bool              m_higher_wnd;              // Flag for exceeding the vertical size of the window height panel
   bool              m_movable;                 // Panel movability flag 
   int               m_header_h;                // Header height
   int               m_wnd;                     // Chart subwindow index
   
   uchar             m_header_alpha;            // Header transparency
   uchar             m_header_alpha_c;          // Current header transparency
   color             m_header_back_color;       // Header background color
   color             m_header_back_color_c;     // Current header background color
   color             m_header_fore_color;       // Header text color
   color             m_header_fore_color_c;     // Current header text color
   color             m_header_border_color;     // Header border color
   color             m_header_border_color_c;   // Current header border color
   
   color             m_butt_close_back_color;   // Close button background color
   color             m_butt_close_back_color_c; // Current close button background color
   color             m_butt_close_fore_color;   // Close button icon color
   color             m_butt_close_fore_color_c; // Current close button color
   
   color             m_butt_min_back_color;     // Expand/collapse button background color
   color             m_butt_min_back_color_c;   // Current expand/collapse button background color
   color             m_butt_min_fore_color;     // Expand/collapse button icon color
   color             m_butt_min_fore_color_c;   // Current expand/collapse button icon color
   
   color             m_butt_pin_back_color;     // Pin button background color
   color             m_butt_pin_back_color_c;   // Current pin button background color
   color             m_butt_pin_fore_color;     // Pin button icon color
   color             m_butt_pin_fore_color_c;   // Current pin button icon color
   
   uchar             m_alpha;                   // Panel transparency
   uchar             m_alpha_c;                 // Current panel transparency
   uchar             m_fore_alpha;              // Text transparency
   uchar             m_fore_alpha_c;            // Current text transparency
   color             m_back_color;              // Background color 

   color             m_back_color_c;            // Current background color 

   color             m_fore_color;              // Text color
   color             m_fore_color_c;            // Current text color
   color             m_border_color;            // Border color
   color             m_border_color_c;          // Current border color
   
   string            m_title;                   // Title text
   string            m_title_font;              // Title font
   int               m_title_font_size;         // Title font size
   string            m_font;                    // Font
   int               m_font_size;               // Font size
   
   bool              m_minimized;               // Collapsed panel window flag 
   string            m_program_name;            // Program name
   string            m_name_gv_x;               // Name of the global terminal variable storing the X coordinate 
   string            m_name_gv_y;               // Name of the global terminal variable storing the Y coordinate
   string            m_name_gv_m;               // Name of the global terminal variable storing the collapsed panel flag 

   string            m_name_gv_u;               // Name of the global terminal variable storing the flag of the pinned panel 

   uint              m_array_wpx[];             // Array of pixels to save/restore the workspace 
   uint              m_array_ppx[];             // Array of pixels to save/restore the panel background 

//--- Return the flag that the panel exceeds (1) the height and (2) the width of the corresponding chart size
   bool              HigherWnd(void)      const { return(this.m_h+2>this.m_chart_h);   }
   bool              WiderWnd(void)       const { return(this.m_w+2>this.m_chart_w);   }
//--- Enable/disable modes of working with the chart
   void              SetChartsTool(const bool flag);
   
//--- Save (1) the working space and (2) the panel background to the pixel array
   void              SaveWorkspace(void);
   void              SaveBackground(void);
//--- Restore (1) the working space and (2) the panel background from the pixel array
   void              RestoreWorkspace(void);
   void              RestoreBackground(void);

//--- Save the pixel array (1) of the working space and the (2) panel background to the file
   bool              FileSaveWorkspace(void);
   bool              FileSaveBackground(void);
//--- Load the pixel array of the (1) working space and (2) the panel background from the file
   bool              FileLoadWorkspace(void);
   bool              FileLoadBackground(void);

//--- Return the subwindow index
   int               GetSubWindow(void) const
                       {
                        return(this.m_program_type==PROGRAM_EXPERT || this.m_program_type==PROGRAM_SCRIPT ? 0 : ::ChartWindowFind());
                       }
   
protected:
//--- (1) Hide, (2) show and (3) bring the panel to the foreground
   void              Hide(const bool redraw=false);
   void              Show(const bool redraw=false);
   void              BringToTop(void);
//--- Return the chart ID
   long              ChartID(void)        const { return this.m_chart_id;              }
//--- Draw the header area
   void              DrawHeaderArea(const string title);
//--- Redraw the header area using a new color and text values
   void              RedrawHeaderArea(const color new_color=clrNONE,const string title="",const color title_new_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- Draw the panel frame
   void              DrawFrame(void);
//--- (1) Draw and (2) redraw the panel closing button
   void              DrawButtonClose(void);
   void              RedrawButtonClose(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- (1) Draw and (2) redraw the panel collapse/expand button
   void              DrawButtonMinimize(void);
   void              RedrawButtonMinimize(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- (1) Draw and (2) redraw the panel pin button
   void              DrawButtonPin(void);
   void              RedrawButtonPin(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);

//--- Return the flag for working in the visual tester
   bool              IsVisualMode(void) const
                       { return (bool)::MQLInfoInteger(MQL_VISUAL_MODE);               }
//--- Return the timeframe description
   string            TimeframeDescription(const ENUM_TIMEFRAMES timeframe) const
                       { return ::StringSubstr(EnumToString(timeframe),7);             }

//--- Return the state of mouse buttons
   ENUM_MOUSE_STATE  MouseButtonState(const int x,const int y,bool pressed);
//--- Shift the panel to new coordinates
   void              Move(int x,int y);

//--- Convert RGB to color
   color             RGBToColor(const double r,const double g,const double b) const;
//--- Write RGB component values to variables
   void              ColorToRGB(const color clr,double &r,double &g,double &b);
//--- Return (1) Red, (2) Green, (3) Blue color components
   double            GetR(const color clr)      { return clr&0xff ;                    }
   double            GetG(const color clr)      { return(clr>>8)&0xff;                 }
   double            GetB(const color clr)      { return(clr>>16)&0xff;                }
//--- Return a new color
   color             NewColor(color base_color, int shift_red, int shift_green, int shift_blue);

//--- Draw a panel
   void              Draw(const string title);
//--- (1) Collapse and (2) expand the panel
   void              Collapse(void);
   void              Expand(void);

//--- Set the (1) X and (2) Y panel coordinates
   bool              SetCoordX(const int coord_x);
   bool              SetCoordY(const int coord_y);
//--- Set the panel (1) width and (2) height
   bool              SetWidth(const int width,const bool redraw=false);
   bool              SetHeight(const int height,const bool redraw=false);

public:
//--- Display the panel
   void              View(const string title)   { this.Draw(title);                    }
//--- Return the (1) CCanvas object, (2) working space, (3) object ID
   CCanvas          *Canvas(void)               { return &this.m_canvas;               }
   CCanvas          *Workspace(void)            { return &this.m_workspace;            }
   uint              ID(void)                   { return this.m_id;                    }
   
//--- Return the panel (1) X and (2) Y coordinates
   int               CoordX(void)         const { return this.m_x;                     }
   int               CoordY(void)         const { return this.m_y;                     }
//--- Return the panel (1) width and (2) height
   int               Width(void)          const { return this.m_w;                     }
   int               Height(void)         const { return this.m_h;                     }

//--- Return the (1) width, (2) height and (3) size of the specified text
   int               TextWidth(const string text)
                       { return this.m_workspace.TextWidth(text);                      }
   int               TextHeight(const string text)
                       { return this.m_workspace.TextHeight(text);                     }
   void              TextSize(const string text,int &width,int &height)
                       { this.m_workspace.TextSize(text,width,height);                 }
   
//--- Set the close button (1) presence, (2) absence flag
   void              SetButtonCloseOn(void);
   void              SetButtonCloseOff(void);
//--- Set the collapse/expand button (1) presence, (2) absence flag
   void              SetButtonMinimizeOn(void);
   void              SetButtonMinimizeOff(void);
   
//--- Set the panel coordinates
   bool              SetCoords(const int x,const int y);
//--- Set the panel size
   bool              SetSizes(const int w,const int h,const bool update=false);
//--- Set panel coordinates and size
   bool              SetParams(const int x,const int y,const int w,const int h,const bool update=false);

//--- Set the transparency of the panel (1) header and (2) working space
   void              SetHeaderTransparency(const uchar value);
   void              SetTransparency(const uchar value);
//--- Set default panel font parameters
   void              SetFontParams(const string name,const int size,const uint flags=0,const uint angle=0);
//--- Display a text message at the specified coordinates
   void              DrawText(const string text,const int x,const int y,const int width=WRONG_VALUE,const int height=WRONG_VALUE);
//--- Draw a (1) background grid (2) with automatic cell size
   void              DrawGrid(const uint x,const uint y,const uint rows,const uint columns,const uint row_size,const uint col_size,const color line_color=clrNONE,bool alternating_color=true);
   void              DrawGridAutoFill(const uint border,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true);
//--- Print grid data (line intersection coordinates)
   void              GridPrint(const uint indent=0)   { this.m_table_data.Print(indent);  }
//--- Write the X and Y coordinate values of the specified table cell to variables
   void              CellXY(const uint row,const uint column, int &x, int &y) { this.m_table_data.CellXY(row,column,x,y);  }
//--- Return the (1) X and (2) Y coordinate of the specified table cell
   int               CellX(const uint row,const uint column)         { return this.m_table_data.CellX(row,column);         }
   int               CellY(const uint row,const uint column)         { return this.m_table_data.CellY(row,column);         }

//--- Event handler
   void              OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
//--- Constructor/destructor
                     CDashboard(const uint id,const int x,const int y, const int w,const int h,const int wnd=-1);
                    ~CDashboard();
  };

声明的变量和类方法在代码中有详细的注释。让我们来看看一些方法的实现。


类的构造函数:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CDashboard::CDashboard(const uint id,const int x,const int y, const int w,const int h,const int wnd=-1) : 
                        m_id(id),
                        m_chart_id(::ChartID()),
                        m_program_type((ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)),
                        m_program_name(::MQLInfoString(MQL_PROGRAM_NAME)),
                        m_wnd(wnd==-1 ? GetSubWindow() : wnd),
                        m_chart_w((int)::ChartGetInteger(m_chart_id,CHART_WIDTH_IN_PIXELS,m_wnd)),
                        m_chart_h((int)::ChartGetInteger(m_chart_id,CHART_HEIGHT_IN_PIXELS,m_wnd)),
                        m_mouse_state(MOUSE_STATE_NOT_PRESSED),
                        m_x(x),
                        m_y(::ChartGetInteger(m_chart_id,CHART_SHOW_ONE_CLICK) ? (y<79 ? 79 : y) : y),
                        m_w(w),
                        m_h(h),
                        m_x_dock(m_x),
                        m_y_dock(m_y),
                        m_header(true),
                        m_butt_close(true),
                        m_butt_minimize(true),
                        m_butt_pin(true),
                        m_header_h(18),
                        
                        //--- Panel header implementation
                        m_header_alpha(128),
                        m_header_alpha_c(m_header_alpha),
                        m_header_back_color(C'0,153,188'),
                        m_header_back_color_c(m_header_back_color),
                        m_header_fore_color(C'182,255,244'),
                        m_header_fore_color_c(m_header_fore_color),
                        m_header_border_color(C'167,167,168'),
                        m_header_border_color_c(m_header_border_color),
                        m_title("Dashboard"),
                        m_title_font("Calibri"),
                        m_title_font_size(-100),
                        
                        //--- close button
                        m_butt_close_back_color(C'0,153,188'),
                        m_butt_close_back_color_c(m_butt_close_back_color),
                        m_butt_close_fore_color(clrWhite),
                        m_butt_close_fore_color_c(m_butt_close_fore_color),
                        
                        //--- collapse/expand button
                        m_butt_min_back_color(C'0,153,188'),
                        m_butt_min_back_color_c(m_butt_min_back_color),
                        m_butt_min_fore_color(clrWhite),
                        m_butt_min_fore_color_c(m_butt_min_fore_color),
                        
                        //--- pin button
                        m_butt_pin_back_color(C'0,153,188'),
                        m_butt_pin_back_color_c(m_butt_min_back_color),
                        m_butt_pin_fore_color(clrWhite),
                        m_butt_pin_fore_color_c(m_butt_min_fore_color),
                        
                        //--- Panel implementation
                        m_alpha(240),
                        m_alpha_c(m_alpha),
                        m_fore_alpha(255),
                        m_fore_alpha_c(m_fore_alpha),
                        m_back_color(C'240,240,240'),
                        m_back_color_c(m_back_color),
                        m_fore_color(C'53,0,0'),
                        m_fore_color_c(m_fore_color),
                        m_border_color(C'167,167,168'),
                        m_border_color_c(m_border_color),
                        m_font("Calibri"),
                        m_font_size(-100),
                        
                        m_minimized(false),
                        m_movable(true)
  {
//--- Set the permission for the chart to send messages about events of moving and pressing mouse buttons,
//--- mouse scroll events, as well as graphical object creation/deletion
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_MOUSE_MOVE,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_MOUSE_WHEEL,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_OBJECT_CREATE,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_OBJECT_DELETE,true);
   
//--- Set the names of global terminal variables to store panel coordinates, collapsed/expanded state and pinning
   this.m_name_gv_x=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_X";
   this.m_name_gv_y=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Y";
   this.m_name_gv_m=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Minimize";
   this.m_name_gv_u=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Unpin";
   
//--- If a global variable does not exist, create it and write the current value,
//--- otherwise - read the value from the terminal global variable into it
//--- X coordinate
   if(!::GlobalVariableCheck(this.m_name_gv_x))
      ::GlobalVariableSet(this.m_name_gv_x,this.m_x);
   else
      this.m_x=(int)::GlobalVariableGet(this.m_name_gv_x);
//--- Y coordinate
   if(!::GlobalVariableCheck(this.m_name_gv_y))
      ::GlobalVariableSet(this.m_name_gv_y,this.m_y);
   else
      this.m_y=(int)::GlobalVariableGet(this.m_name_gv_y);
//--- Collapsed/expanded
   if(!::GlobalVariableCheck(this.m_name_gv_m))
      ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
   else
      this.m_minimized=(int)::GlobalVariableGet(this.m_name_gv_m);
//--- Collapsed/not collapsed
   if(!::GlobalVariableCheck(this.m_name_gv_u))
      ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
   else
      this.m_movable=(int)::GlobalVariableGet(this.m_name_gv_u);

//--- Set the flags for the size of the panel exceeding the size of the chart window
   this.m_higher_wnd=this.HigherWnd();
   this.m_wider_wnd=this.WiderWnd();

//--- If the panel graphical resource is created,
   if(this.m_canvas.CreateBitmapLabel(this.m_chart_id,this.m_wnd,"P"+(string)this.m_id,this.m_x,this.m_y,this.m_w,this.m_h,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      //--- set the canvas font and fill the canvas with the transparent color 
      this.m_canvas.FontSet(this.m_title_font,this.m_title_font_size,FW_BOLD);
      this.m_canvas.Erase(0x00FFFFFF);
     }
//--- otherwise - report unsuccessful object creation to the journal
   else
      ::PrintFormat("%s: Error. CreateBitmapLabel for canvas failed",(string)__FUNCTION__);

//--- If a working space of a graphical resource is created,
   if(this.m_workspace.CreateBitmapLabel(this.m_chart_id,this.m_wnd,"W"+(string)this.m_id,this.m_x+1,this.m_y+this.m_header_h,this.m_w-2,this.m_h-this.m_header_h-1,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      //--- set the font for the working area and fill it with the transparent color 
      this.m_workspace.FontSet(this.m_font,this.m_font_size);
      this.m_workspace.Erase(0x00FFFFFF);
     }
//--- otherwise - report unsuccessful object creation to the journal
   else
      ::PrintFormat("%s: Error. CreateBitmapLabel for workspace failed",(string)__FUNCTION__);
  }

该类有一个含一个参数的构造函数和一个默认创建的构造函数。当然,我们只对参数型感兴趣。它将在创建类对象时使用。一个唯一的对象ID、面板的初始坐标、面板的宽度和高度以及将放置面板的子窗口的索引通过形式参数传递给构造函数。

需要唯一的面板ID,以便类可以创建具有唯一名称的对象。如果在一个图表上使用多个带有面板的指标,则为了避免对象名称冲突,您需要此唯一编号,该编号将在创建面板对象时添加到面板对象的名称中。ID的唯一性应该是可重复的——每次新发布时,编号应该与上次发布时相同。例如,GetTickCount()不适合ID。
如果子窗口索引是默认设置的(-1),则会以编程方式进行搜索,否则我们将使用参数中指定的索引。

默认参数在构造函数初始化列表中设置。一些负责视觉效果的参数具有两个变量——默认值和当前属性值。这对于交互式更改是必要的,例如,当悬停在这些参数负责的面板区域上时。

构造函数主体包含全局终端变量的值。创建了两个图形对象-画布和面板工作区。

所有构造函数代码都有详细的注释。


类的析构函数:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CDashboard::~CDashboard()
  {
//--- Write the current values to global terminal variables
   ::GlobalVariableSet(this.m_name_gv_x,this.m_x);
   ::GlobalVariableSet(this.m_name_gv_y,this.m_y);
   ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
   ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
//--- Delete panel objects
   this.m_canvas.Destroy();
   this.m_workspace.Destroy();
  }

在这里,我们首先将坐标和标志重置为全局终端变量,然后删除画布和工作空间对象。

要使用光标和鼠标按钮与面板交互,我们需要知道光标相对于面板及其控制元素的位置。当光标移动时,我们可以跟踪它的坐标和类事件处理程序中按钮的状态。类事件处理函数具有与标准OnChartEvent处理函数相同的参数:

void  OnChartEvent()
   const int       id,       // event ID 
   const long&     lparam,   // long type event parameter
   const double&   dparam,   // double type event parameter
   const string&   sparam    // string type event parameter
   );

参数

id

[in]ENUM_CHART_Event枚举中的事件ID。

lparam

[in]long类型事件参数

dparam

[in]  double 类型事件参数

sparam

[in]  string 类型事件参数

返回值

没有返回值

注意

使用预定义的OnChartEvent()函数可以处理11种类型的事件。为自定义事件提供了65535个ID,从CHARTEVENT_CUSTOM 到 CHARTEVENT_CUSTOM_LAST。要生成自定义事件,请使用EventChartCustom()函数。

ENUM_CHART_event枚举中的简短事件描述:

  • CHARTEVENT_KEYDOWN——当图表窗口处于焦点位置时,按下键盘上的一个键;
  • CHARTEVENT_MOUSE_MOVE——移动鼠标并点击鼠标按钮(如果图表设置了CHART_EVENT_MOUSE_MOVE=true);
  • CHARTEVENT_OJECT_CREATE——创建图形对象(如果为图表设置了CHART_EVENT_OBJECT_CREATE=true);
  • CHARTEVENT_OJECT_CHANGE--通过属性对话框更改对象属性;
  • CHARTEVENT_OJECT_DELETE—删除图形对象(如果图表设置了CHART_EVENT_OBJECT_DELETE=true);
  • CHARTEVENT_CLICK--单击图表;
  • CHARTEVENT_OJECT_CLICK——鼠标单击属于图表的图形对象;
  • CHARTEVENT_OJECT_DRAG—用鼠标拖动图形对象;
  • CHARTEVENT_OBJECT_ENDEDIT——完成图形对象的编辑输入框中的文本编辑(OBJ_EDIT);
  • CHARTEVENT_CHART_CHANGE--更改图表;
  • CHARTEVENT_CUSTOM+n—自定义事件ID,其中n在0到65535之间。CHARTEVENT_CCUSTOM_LAST包含最后一个可接受的自定义事件ID(CHARTEVENT_CCUSTOM+65535)。

lparam参数包含X坐标,dparam包含Y坐标,而sparam包含用于确定鼠标按钮状态的标志值组合。所有这些参数都需要相对于面板及其元素的坐标进行接收和处理。应该确定状态并将其发送到类事件处理程序,在该处理程序中将指定对所有这些状态的反应。


返回光标和鼠标按钮相对于面板的状态的方法:

//+------------------------------------------------------------------+
//| Returns the state of the mouse cursor and button                 |
//+------------------------------------------------------------------+
ENUM_MOUSE_STATE CDashboard::MouseButtonState(const int x,const int y,bool pressed)
  {
//--- If the button is pressed
   if(pressed)
     {
      //--- If the state has already been saved, exit
      if(this.m_mouse_state!=MOUSE_STATE_NOT_PRESSED)
         return this.m_mouse_state;
      //--- If the button is pressed inside the window 
      if(x>this.m_x && x<this.m_x+this.m_w && y>this.m_y && y<this.m_y+this.m_h)
        {
         //--- If the button is pressed inside the header
         if(y>this.m_y && y<=this.m_y+this.m_header_h)
           {
            //--- Bring the panel to the foreground 
            this.BringToTop();
            //--- Coordinates of the close, collapse/expand and pin buttons 
            int wc=(this.m_butt_close ? this.m_header_h : 0);
            int wm=(this.m_butt_minimize ? this.m_header_h : 0);
            int wp=(this.m_butt_pin ? this.m_header_h : 0);
            //--- If the close button is pressed, return this state
            if(x>this.m_x+this.m_w-wc)
               return MOUSE_STATE_PRESSED_INSIDE_CLOSE;
            //--- If the collapse/expand button is pressed, return this state
            if(x>this.m_x+this.m_w-wc-wm)
               return MOUSE_STATE_PRESSED_INSIDE_MINIMIZE;
            //--- If the pin button is pressed, return this state
            if(x>this.m_x+this.m_w-wc-wm-wp)
               return MOUSE_STATE_PRESSED_INSIDE_PIN;
            //--- If the button is not pressed on the control buttons of the panel, record and return the state of the button press inside the header
            this.m_mouse_state=MOUSE_STATE_PRESSED_INSIDE_HEADER;
            return this.m_mouse_state;
           }
         //--- If a button inside the window is pressed, write the state to a variable and return it
         else if(y>this.m_y+this.m_header_h && y<this.m_y+this.m_h)
           {
            this.m_mouse_state=MOUSE_STATE_PRESSED_INSIDE_WINDOW;
            return this.m_mouse_state;
           }
        }
      //--- The button is pressed outside the window - write the state to a variable and return it
      else
        {
         this.m_mouse_state=MOUSE_STATE_PRESSED_OUTSIDE_WINDOW;
         return this.m_mouse_state;
        }
     }
//--- If the button is not pressed
   else
     {
      //--- Write the state of the unpressed button to the variable
      this.m_mouse_state=MOUSE_STATE_NOT_PRESSED;
      //--- If the cursor is inside the panel 
      if(x>this.m_x && x<this.m_x+this.m_w && y>this.m_y && y<this.m_y+this.m_h)
        {
         //--- If the cursor is inside the header
         if(y>this.m_y && y<=this.m_y+this.m_header_h)
           {
            //--- Specify the width of the close, collapse/expand and pin buttons
            int wc=(this.m_butt_close ? this.m_header_h : 0);
            int wm=(this.m_butt_minimize ? this.m_header_h : 0);
            int wp=(this.m_butt_pin ? this.m_header_h : 0);
            //--- If the cursor is inside the close button, return this state 
            if(x>this.m_x+this.m_w-wc)
               return MOUSE_STATE_INSIDE_CLOSE;
            //--- If the cursor is inside the minimize/expand button, return this state 
            if(x>this.m_x+this.m_w-wc-wm)
               return MOUSE_STATE_INSIDE_MINIMIZE;
            //--- If the cursor is inside the pin button, return this state 
            if(x>this.m_x+this.m_w-wc-wm-wp)
               return MOUSE_STATE_INSIDE_PIN;
            //--- If the cursor is outside the buttons inside the header area, return this state 
            return MOUSE_STATE_INSIDE_HEADER;
           }
         //--- Otherwise, the cursor is inside the working space. Return this state 
         else
            return MOUSE_STATE_INSIDE_WINDOW;
        }
     }
//--- In any other case, return the state of the unpressed mouse button
   return MOUSE_STATE_NOT_PRESSED;
  }

方法逻辑已在代码注释中详细描述。我们只需确定光标、面板及其元素的相互坐标,然后返回状态。按下或释放鼠标按钮的标志会立即发送到该方法。对于每个这样的状态,都有自己的代码块,用于定义按下按钮或释放按钮时的状态。以这种方式使用逻辑非常简单快捷,但也有一些缺点——你无法检测到鼠标点击了控制元素。相反,你只能检测到点击它。通常,当释放鼠标按钮时会记录单击,当按下鼠标按钮时则会记录按住。按照这里使用的逻辑,按下鼠标按钮是唯一被认为是点击并按住的动作。

此方法中获得的状态应发送到事件处理函数,其中每个事件都有自己的处理过程,可以更改面板的行为和外观:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CDashboard::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- If a graphical object is created
   if(id==CHARTEVENT_OBJECT_CREATE)
     {
      this.BringToTop();
      ::ObjectSetInteger(this.m_chart_id,sparam,OBJPROP_SELECTED,true);
     }
//--- If the chart is changed
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      //--- Get the chart subwindow index (it may change when removing the window of any indicator)
      this.m_wnd=this.GetSubWindow();
      //--- Get the new chart size
      int w=(int)::ChartGetInteger(this.m_chart_id,CHART_WIDTH_IN_PIXELS,this.m_wnd);
      int h=(int)::ChartGetInteger(this.m_chart_id,CHART_HEIGHT_IN_PIXELS,this.m_wnd);
      //--- Determine whether the panel dimensions extend beyond the chart window
      this.m_higher_wnd=this.HigherWnd();
      this.m_wider_wnd=this.WiderWnd();
      //--- If the chart height has changed, adjust the panel vertical position
      if(this.m_chart_h!=h)
        {
         this.m_chart_h=h;
         int y=this.m_y;
         if(this.m_y+this.m_h>h-1)
            y=h-this.m_h-1;
         if(y<1)
            y=1;
         this.Move(this.m_x,y);
        }
      //--- If the chart weight has changed, adjust the panel horizontal position
      if(this.m_chart_w!=w)
        {
         this.m_chart_w=w;
         int x=this.m_x;
         if(this.m_x+this.m_w>w-1)
            x=w-this.m_w-1;
         if(x<1)
            x=1;
         this.Move(x,this.m_y);
        }
     }

//--- Declare variables to store the current cursor shift relative to the initial coordinates of the panel
   static int diff_x=0;
   static int diff_y=0;
   
//--- Get the flag of the held mouse button. We also take into account the right button for the visual tester (sparam=="2")
   bool pressed=(!this.IsVisualMode() ? (sparam=="1" || sparam=="" ? true : false) : sparam=="1" || sparam=="2" ? true : false);
//--- Get the cursor X and Y coordinates. Take into account the shift for the Y coordinate when working in the chart subwindow
   int  mouse_x=(int)lparam;
   int  mouse_y=(int)dparam-(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOW_YDISTANCE,this.m_wnd);
//--- Get the state of the cursor and mouse buttons relative to the panel
   ENUM_MOUSE_STATE state=this.MouseButtonState(mouse_x,mouse_y,pressed);
//--- If the cursor moves
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- If a button is pressed inside the working area of the panel
      if(state==MOUSE_STATE_PRESSED_INSIDE_WINDOW)
        {
         //--- Disable chart scrolling, right-click menu and crosshair 
         this.SetChartsTool(false);
         //--- Redraw the header area with the default background color 
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
         return;
        }
      //--- If a button is pressed inside the panel header area
      else if(state==MOUSE_STATE_PRESSED_INSIDE_HEADER)
        {
         //--- Disable chart scrolling, right-click menu and crosshair 
         this.SetChartsTool(false);
         //--- Redraw the header area with a new background color 
         color new_color=this.NewColor(this.m_header_back_color,-10,-10,-10);
         if(this.m_header_back_color_c!=new_color)
           {
            this.RedrawHeaderArea(new_color);
            this.m_canvas.Update();
           }
         //--- Shift the panel following the cursor taking into account the amount of cursor displacement relative to the initial coordinates of the panel
         if(this.m_movable)
            this.Move(mouse_x-diff_x,mouse_y-diff_y);
         return;
        }
        
      //--- If the close button is pressed 
      else if(state==MOUSE_STATE_PRESSED_INSIDE_CLOSE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the close button with a new background color 
         color new_color=this.NewColor(clrRed,0,40,40);
         if(this.m_butt_close_back_color_c!=new_color)
           {
            this.RedrawButtonClose(new_color);
            this.m_canvas.Update();
           }
         //--- Close button press handling should be defined in the program.
         //--- Send the click event of this button to its OnChartEvent handler.
         //--- Event ID 1001,
         //--- lparam=panel ID (m_id),
         //--- dparam=0
         //--- sparam="Close button pressed"
         ushort event=CHARTEVENT_CUSTOM+1;
         ::EventChartCustom(this.m_chart_id,ushort(event-CHARTEVENT_CUSTOM),this.m_id,0,"Close button pressed");
        }
      //--- If the panel collapse/expand button is pressed 
      else if(state==MOUSE_STATE_PRESSED_INSIDE_MINIMIZE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- "flip" the panel collapse flag,
         this.m_minimized=!this.m_minimized;
         //--- redraw the panel taking into account the new state of the flag,
         this.Draw(this.m_title);
         //--- redraw the panel header area 
         this.RedrawHeaderArea();
         //--- If the panel is pinned and expanded, move it to the stored location coordinates
         if(this.m_minimized && !this.m_movable)
            this.Move(this.m_x_dock,this.m_y_dock);
         //--- Update the canvas with chart redrawing and
         this.m_canvas.Update();
         //--- write the state of the panel expand flag to the global terminal variable
         ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
        }
      //--- If the panel pin button is pressed
      else if(state==MOUSE_STATE_PRESSED_INSIDE_PIN)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- "flip" the panel collapse flag,
         this.m_movable=!this.m_movable;
         //--- Redraw the pin button with a new background color
         color new_color=this.NewColor(this.m_butt_pin_back_color,30,30,30);
         if(this.m_butt_pin_back_color_c!=new_color)
            this.RedrawButtonPin(new_color);
         //--- If the panel is collapsed and pinned, save its coordinates
         //--- When expanded and collapsed again, the panel returns to these coordinates
         //--- Relevant for pinning a collapsed panel at the bottom of the screen
         if(this.m_minimized && !this.m_movable)
           {
            this.m_x_dock=this.m_x;
            this.m_y_dock=this.m_y;
           }
         //--- Update the canvas with chart redrawing and
         this.m_canvas.Update();
         //--- write the state of the panel movability flag to the global terminal variable
         ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
        }
        
      //--- If the cursor is inside the panel header area
      else if(state==MOUSE_STATE_INSIDE_HEADER)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a new background color 
         color new_color=this.NewColor(this.m_header_back_color,20,20,20);
         if(this.m_header_back_color_c!=new_color)
           {
            this.RedrawHeaderArea(new_color);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the close button
      else if(state==MOUSE_STATE_INSIDE_CLOSE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a minimal change in the background color
         color new_color=this.NewColor(this.m_header_back_color,0,0,1);
         if(this.m_header_back_color_c!=new_color)
            this.RedrawHeaderArea(new_color);
         //--- Redraw the collapse/expand button with the default background color 
         if(this.m_butt_min_back_color_c!=this.m_butt_min_back_color)
            this.RedrawButtonMinimize(this.m_butt_min_back_color);
         //--- Redraw the pin button with the default background color 
         if(this.m_butt_pin_back_color_c!=this.m_butt_pin_back_color)
            this.RedrawButtonPin(this.m_butt_pin_back_color);
         //--- Redraw the close button with the red background color
         if(this.m_butt_close_back_color_c!=clrRed)
           {
            this.RedrawButtonClose(clrRed);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the collapse/expand button
      else if(state==MOUSE_STATE_INSIDE_MINIMIZE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a minimal change in the background color
         color new_color=this.NewColor(this.m_header_back_color,0,0,1);
         if(this.m_header_back_color_c!=new_color)
            this.RedrawHeaderArea(new_color);
         //--- Redraw the close button with the default background color
         if(this.m_butt_close_back_color_c!=this.m_butt_close_back_color)
            this.RedrawButtonClose(this.m_butt_close_back_color);
         //--- Redraw the pin button with the default background color 
         if(this.m_butt_pin_back_color_c!=this.m_butt_pin_back_color)
            this.RedrawButtonPin(this.m_butt_pin_back_color);
         //--- Redraw the collapse/expand button with a new background color 
         new_color=this.NewColor(this.m_butt_min_back_color,20,20,20);
         if(this.m_butt_min_back_color_c!=new_color)
           {
            this.RedrawButtonMinimize(new_color);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the pin button
      else if(state==MOUSE_STATE_INSIDE_PIN)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a minimal change in the background color
         color new_color=this.NewColor(this.m_header_back_color,0,0,1);
         if(this.m_header_back_color_c!=new_color)
            this.RedrawHeaderArea(new_color);
         //--- Redraw the close button with the default background color
         if(this.m_butt_close_back_color_c!=this.m_butt_close_back_color)
            this.RedrawButtonClose(this.m_butt_close_back_color);
         //--- Redraw the collapse/expand button with the default background color 
         if(this.m_butt_min_back_color_c!=this.m_butt_min_back_color)
            this.RedrawButtonMinimize(this.m_butt_min_back_color);
         //--- Redraw the pin button with a new background color
         new_color=this.NewColor(this.m_butt_pin_back_color,20,20,20);
         if(this.m_butt_pin_back_color_c!=new_color)
           {
            this.RedrawButtonPin(new_color);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the working space
      else if(state==MOUSE_STATE_INSIDE_WINDOW)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with the default background color 
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
        }
      //--- Otherwise (the cursor is outside the panel, and we need to restore the chart parameters) 
      else
        {
         //--- Enable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(true);
         //--- Redraw the header area with the default background color 
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
        }
      //--- Write the cursor shift by X and Y relative to the panel initial coordinates
      diff_x=mouse_x-this.m_x;
      diff_y=mouse_y-this.m_y;
     }
  }

事件处理程序的逻辑在代码中有足够详细的注释。我们应该在这里考虑一些值得注意的要点。

处理创建新图形对象的事件是在一开始就指定的:

//--- If a graphical object is created
   if(id==CHARTEVENT_OBJECT_CREATE)
     {
      this.BringToTop();
      ::ObjectSetInteger(this.m_chart_id,sparam,OBJPROP_SELECTED,true);
     }

它的功能是什么?如果我们创建一个新的图形对象,它会被放置在图表上其他图形对象的上方,并相应地叠加在面板的顶部。因此,当定义这样的事件时,面板立即被带到前台,然后一个新的图形对象被高亮显示。为什么呢?如果不这样做,那么需要多个点进行构建的图形对象(例如趋势线)将无法正常创建——它们的所有控制点都位于同一坐标中,对象本身将不可见。这种情况是由于在创建图形对象的过程中,当面板被带到前台时,图形对象失去了控制。因此,在将面板置于前景之后,应该强制选择新的图形对象。
因此,面板和图形对象在创建时的相互行为如下:

图6. 新的图形对象构建在面板的“下方”,创建时不会失去焦点


事件处理函数对每个状态都有自己的处理块,所有这些块的逻辑都是相同的。例如,处理面板标题上的鼠标单击并按住:

      //--- If a button is pressed inside the panel header area
      else if(state==MOUSE_STATE_PRESSED_INSIDE_HEADER)
        {
         //--- Disable chart scrolling, right-click menu and crosshair 
         this.SetChartsTool(false);
         //--- Redraw the header area with a new background color 
         color new_color=this.NewColor(this.m_header_back_color,-10,-10,-10);
         if(this.m_header_back_color_c!=new_color)
           {
            this.RedrawHeaderArea(new_color);
            this.m_canvas.Update();
           }
         //--- Shift the panel following the cursor taking into account the amount of cursor displacement relative to the initial coordinates of the panel
         if(this.m_movable)
            this.Move(mouse_x-diff_x,mouse_y-diff_y);
         return;
        }

为了防止图表与面板一起移动,将禁用鼠标滚动图表、右键单击菜单和十字光标等事件。由于表头应在视觉上响应鼠标捕捉,因此其颜色会变暗。在将颜色更改为新颜色之前,我们需要检查它是否已经更改。不断地将其更改为相同颜色而浪费CPU资源是没有意义的。如果未禁用移动(面板未固定),我们会将其移动到新的坐标,该坐标是根据鼠标坐标减去光标位置相对于面板左上角的偏移量计算得出的。如果不考虑偏移,面板将精确定位在左上角的光标坐标处。


将面板移动到指定坐标的方法:

//+------------------------------------------------------------------+
//| Move the panel                                                   |
//+------------------------------------------------------------------+
void CDashboard::Move(int x,int y)
  {
   int h=this.m_canvas.Height();
   int w=this.m_canvas.Width();
   if(!this.m_wider_wnd)
     {
      if(x+w>this.m_chart_w-1)
         x=this.m_chart_w-w-1;
      if(x<1)
         x=1;
     }
   else
     {
      if(x>1)
         x=1;
      if(x<this.m_chart_w-w-1)
         x=this.m_chart_w-w-1;
     }
   if(!this.m_higher_wnd)
     {
      if(y+h>this.m_chart_h-2)
         y=this.m_chart_h-h-2;
      if(y<1)
         y=1;
     }
   else
     {
      if(y>1)
         y=1;
      if(y<this.m_chart_h-h-2)
         y=this.m_chart_h-h-2;
     }
   if(this.SetCoords(x,y))
      this.m_canvas.Update();
  }

应移动面板的坐标将传递给该方法。如果更改坐标时面板位于图表外部,则会调整坐标,使面板始终位于图表窗口内部,从任何边缘缩进1个像素。在完成面板坐标的所有检查和调整后,将建立其在图表上位置的新坐标。


设置面板坐标的方法:

//+------------------------------------------------------------------+
//| Set the panel X coordinate                                       |
//+------------------------------------------------------------------+
bool CDashboard::SetCoordX(const int coord_x)
  {
   int x=(int)::ObjectGetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_XDISTANCE);
   if(x==coord_x)
      return true;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_XDISTANCE,coord_x))
      return false;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_XDISTANCE,coord_x+1))
      return false;
   this.m_x=coord_x;
   return true;
  }
//+------------------------------------------------------------------+
//| Set the panel Y coordinate                                       |
//+------------------------------------------------------------------+
bool CDashboard::SetCoordY(const int coord_y)
  {
   int y=(int)::ObjectGetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_YDISTANCE);
   if(y==coord_y)
      return true;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_YDISTANCE,coord_y))
      return false;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_YDISTANCE,coord_y+this.m_header_h))
      return false;
   this.m_y=coord_y;
   return true;
  }

如果将与面板坐标相等的坐标传递给该方法,则无需再次设置该坐标-只需返回该方法的成功。首先移动画布,然后移动工作空间。考虑到工作空间在画布上的相对位置,工作空间在面板内部的左侧移动一个像素,在顶部移动标题的高度。


设置面板尺寸的方法:

//+------------------------------------------------------------------+
//| Set the panel width                                              |
//+------------------------------------------------------------------+
bool CDashboard::SetWidth(const int width,const bool redraw=false)
  {
   if(width<4)
     {
      ::PrintFormat("%s: Error. Width cannot be less than 4px",(string)__FUNCTION__);
      return false;
     }
   if(width==this.m_canvas.Width())
      return true;
   if(!this.m_canvas.Resize(width,this.m_canvas.Height()))
      return false;
   if(width-2<1)
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   else
     {
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
      if(!this.m_workspace.Resize(width-2,this.m_workspace.Height()))
         return false;
     }
   this.m_w=width;
   return true;
  }
//+------------------------------------------------------------------+
//| Set the panel height                                             |
//+------------------------------------------------------------------+
bool CDashboard::SetHeight(const int height,const bool redraw=false)
  {
   if(height<::fmax(this.m_header_h,1))
     {
      ::PrintFormat("%s: Error. Width cannot be less than %lupx",(string)__FUNCTION__,::fmax(this.m_header_h,1));
      return false;
     }
   if(height==this.m_canvas.Height())
      return true;
   if(!this.m_canvas.Resize(this.m_canvas.Width(),height))
      return false;
   if(height-this.m_header_h-2<1)
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   else
     {
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
      if(!this.m_workspace.Resize(this.m_workspace.Width(),height-this.m_header_h-2))
         return false;
     }
   this.m_h=height;
   return true;
  }

这里所有的都与设置坐标时完全相同-如果传递给方法的大小与面板已经具有的大小相同,那么方法只需返回true。一个值得注意的细节是,工作空间总是比画布小。如果在减小工作空间的大小时,结果发现该大小小于1,那么为了避免调整大小的错误,只需隐藏工作空间而不更改其大小。


同时设置两个坐标、所有尺寸和坐标以及面板尺寸的辅助方法:

//+------------------------------------------------------------------+
//| Set the panel coordinates                                        |
//+------------------------------------------------------------------+
bool CDashboard::SetCoords(const int x,const int y)
  {
   bool res=true;
   res &=this.SetCoordX(x);
   res &=this.SetCoordY(y);
   return res;
  }
//+------------------------------------------------------------------+
//| Set the panel size                                               |
//+------------------------------------------------------------------+
bool CDashboard::SetSizes(const int w,const int h,const bool update=false)
  {
   bool res=true;
   res &=this.SetWidth(w);
   res &=this.SetHeight(h);
   if(res && update)
      this.Expand();
   return res;
  }
//+------------------------------------------------------------------+
//| Set panel coordinates and size                                   |
//+------------------------------------------------------------------+
bool CDashboard::SetParams(const int x,const int y,const int w,const int h,const bool update=false)
  {
   bool res=true;
   res &=this.SetCoords(x,y);
   res &=this.SetSizes(w,h);
   if(res && update)
      this.Expand();
   return res;
  }

这些方法接收参数和更新标志。如果参数和更新标志设置成功,将调用面板展开方法,在该方法中,所有面板元素都将被重新绘制。


绘制标题区域的方法:

//+------------------------------------------------------------------+
//| Draw the header area                                             |
//+------------------------------------------------------------------+
void CDashboard::DrawHeaderArea(const string title)
  {
//--- Exit if the header is not used
   if(!this.m_header)
      return;
//--- Set the title text
   this.m_title=title;
//--- The Y coordinate of the text is located vertically in the center of the header area
   int y=this.m_header_h/2;
//--- Fill the area with color
   this.m_canvas.FillRectangle(0,0,this.m_w-1,this.m_header_h-1,::ColorToARGB(this.m_header_back_color,this.m_header_alpha));
//--- Display the header text
   this.m_canvas.TextOut(2,y,this.m_title,::ColorToARGB(this.m_header_fore_color,this.m_header_alpha),TA_LEFT|TA_VCENTER);
//--- Save the current header background color
   this.m_header_back_color_c=this.m_header_back_color;
//--- Draw control elements (close, collapse/expand and pin buttons) and
   this.DrawButtonClose();
   this.DrawButtonMinimize();
   this.DrawButtonPin();
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

该方法绘制标题区域-在标题坐标处绘制一个矩形区域,并绘制控件-关闭、折叠/展开和固定按钮。将使用默认的标题区域颜色和透明度。


重新绘制标题区域的方法:

//+------------------------------------------------------------------+
//| Redraw header area                                               |
//+------------------------------------------------------------------+
void CDashboard::RedrawHeaderArea(const color new_color=clrNONE,const string title="",const color title_new_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the header is not used or all passed parameters have default values
   if(!this.m_header || (new_color==clrNONE && title=="" && title_new_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- Exit if all passed parameters are equal to those already set
   if(new_color==this.m_header_back_color && title==this.m_title && title_new_color==this.m_header_fore_color && new_alpha==this.m_header_alpha)
      return;
//--- If the title is not equal to the default value, set a new title
   if(title!="")
      this.m_title=title;
//--- Define new background and text colors, and transparency
   color back_clr=(new_color!=clrNONE ? new_color : this.m_header_back_color);
   color fore_clr=(title_new_color!=clrNONE ? title_new_color : this.m_header_fore_color);  
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- The Y coordinate of the text is located vertically in the center of the header area
   int y=this.m_header_h/2;
//--- Fill the area with color
   this.m_canvas.FillRectangle(0,0,this.m_w-1,this.m_header_h-1,::ColorToARGB(back_clr,alpha));
//--- Display the header text
   this.m_canvas.TextOut(2,y,this.m_title,::ColorToARGB(fore_clr,alpha),TA_LEFT|TA_VCENTER);
//--- Save the current header background color, text and transparency
   this.m_header_back_color_c=back_clr;
   this.m_header_fore_color_c=fore_clr;
   this.m_header_alpha_c=alpha;
//--- Draw control elements (close, collapse/expand and pin buttons) and
   this.RedrawButtonClose(back_clr,clrNONE,alpha);
   this.RedrawButtonMinimize(back_clr,clrNONE,alpha);
   this.RedrawButtonPin(back_clr,clrNONE,alpha);
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(true);
  }

新的背景颜色、新的标题文本、新的标题文本颜色和新的透明度将传递给该方法。如果传递的参数与已设置的参数完全相同,则退出该方法。该方法用于更新标题颜色、文本和透明度。


绘制面板边框的方法:

//+------------------------------------------------------------------+
//| Draw the panel frame                                             |
//+------------------------------------------------------------------+
void CDashboard::DrawFrame(void)
  {
   this.m_canvas.Rectangle(0,0,this.m_w-1,this.m_h-1,::ColorToARGB(this.m_border_color,this.m_alpha));
   this.m_border_color_c=this.m_border_color;
   this.m_canvas.Update(false);
  }

在画布周边绘制一个框架,并将设置的颜色保存为当前颜色。


绘制面板关闭按钮的方法:

//+------------------------------------------------------------------+
//| Draws the panel close button                                     |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonClose(void)
  {
//--- Exit if the button is not used
   if(!this.m_butt_close)
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- Button coordinates and size
   int x1=this.m_w-w;
   int x2=this.m_w-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_close_back_color,this.m_header_alpha));
//--- Draw the close button
   this.m_canvas.LineThick(x1+shift+1,y1+shift+1,x2-shift,y2-shift,::ColorToARGB(this.m_butt_close_fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
   this.m_canvas.LineThick(x1+shift+1,y2-shift-1,x2-shift,y1+shift,::ColorToARGB(this.m_butt_close_fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_close_back_color_c=this.m_butt_close_back_color;
   this.m_butt_close_fore_color_c=this.m_butt_close_fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

代码注释中描述了整个逻辑:绘制背景和顶部的关闭图标(十字)。


重新绘制面板关闭按钮的方法:

//+------------------------------------------------------------------+
//| Redraw the panel close button                                    |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonClose(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the button is not used or all passed parameters have default values
   if(!this.m_butt_close || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- Button coordinates and size
   int x1=this.m_w-w;
   int x2=this.m_w-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Define new background and text colors, and transparency
   color back_color=(new_back_color!=clrNONE ? new_back_color : this.m_butt_close_back_color);
   color fore_color=(new_fore_color!=clrNONE ? new_fore_color : this.m_butt_close_fore_color);
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- Draw the close button
   this.m_canvas.LineThick(x1+shift+1,y1+shift+1,x2-shift,y2-shift,::ColorToARGB(fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
   this.m_canvas.LineThick(x1+shift+1,y2-shift-1,x2-shift,y1+shift,::ColorToARGB(fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_close_back_color_c=back_color;
   this.m_butt_close_fore_color_c=fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

为了重新绘制,至少有一个传递的参数应该与当前参数不同。除了选择和设置新的绘图参数外,其余部分与按钮绘图方法相同。


绘制和重绘折叠/展开和固定按钮的其他方法:

//+------------------------------------------------------------------+
//| Draw the panel collapse/expand button                            |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonMinimize(void)
  {
//--- Exit if the button is not used
   if(!this.m_butt_minimize)
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close button is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-w;
   int x2=this.m_w-wc-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_min_back_color,this.m_header_alpha));
//--- If the panel is collapsed, draw a rectangle
   if(this.m_minimized)
      this.m_canvas.Rectangle(x1+shift,y1+shift,x2-shift,y2-shift,::ColorToARGB(this.m_butt_min_fore_color,255));
//--- Otherwise, the panel is expanded. Draw a line segment
   else
      this.m_canvas.LineThick(x1+shift,y2-shift,x2-shift,y2-shift,::ColorToARGB(this.m_butt_min_fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_min_back_color_c=this.m_butt_min_back_color;
   this.m_butt_min_fore_color_c=this.m_butt_min_fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Redraw the panel collapse/expand button                          |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonMinimize(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the button is not used or all passed parameters have default values
   if(!this.m_butt_minimize || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close button is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-w;
   int x2=this.m_w-wc-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Define new background and text colors, and transparency
   color back_color=(new_back_color!=clrNONE ? new_back_color : this.m_butt_min_back_color);
   color fore_color=(new_fore_color!=clrNONE ? new_fore_color : this.m_butt_min_fore_color);
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- If the panel is collapsed, draw a rectangle
   if(this.m_minimized)
      this.m_canvas.Rectangle(x1+shift,y1+shift,x2-shift,y2-shift,::ColorToARGB(fore_color,255));
//--- Otherwise, the panel is expanded. Draw a line segment
   else
      this.m_canvas.LineThick(x1+shift,y2-shift,x2-shift,y2-shift,::ColorToARGB(fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_min_back_color_c=back_color;
   this.m_butt_min_fore_color_c=fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Draw the panel pin button                                        |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonPin(void)
  {
//--- Exit if the button is not used
   if(!this.m_butt_pin)
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close and collapse buttons is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
   int wm=(this.m_butt_minimize ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-wm-w;
   int x2=this.m_w-wc-wm-1;
   int y1=0;
   int y2=w-1;
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_pin_back_color,this.m_header_alpha));
//--- Coordinates of the broken line points
   int x[]={x1+3, x1+6, x1+3,x1+4,x1+6,x1+9,x1+9,x1+10,x1+15,x1+14,x1+13,x1+10,x1+10,x1+9,x1+6};
   int y[]={y1+14,y1+11,y1+8,y1+7,y1+7,y1+4,y1+3,y1+2, y1+7, y1+8, y1+8, y1+11,y1+13,y1+14,y1+11};
//--- Draw the "button" shape 
   this.m_canvas.Polygon(x,y,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- If the movability flag is reset (pinned) - cross out the drawn button
   if(!this.m_movable)
      this.m_canvas.Line(x1+3,y1+2,x1+15,y1+14,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- Remember the current background color and button design
   this.m_butt_pin_back_color_c=this.m_butt_pin_back_color;
   this.m_butt_pin_fore_color_c=this.m_butt_pin_fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Redraw the panel pin button                                      |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonPin(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the button is not used or all passed parameters have default values
   if(!this.m_butt_pin || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close and collapse buttons is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
   int wm=(this.m_butt_minimize ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-wm-w;
   int x2=this.m_w-wc-wm-1;
   int y1=0;
   int y2=w-1;
//--- Define new background and text colors, and transparency
   color back_color=(new_back_color!=clrNONE ? new_back_color : this.m_butt_pin_back_color);
   color fore_color=(new_fore_color!=clrNONE ? new_fore_color : this.m_butt_pin_fore_color);
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- Coordinates of the broken line points
   int x[]={x1+3, x1+6, x1+3,x1+4,x1+6,x1+9,x1+9,x1+10,x1+15,x1+14,x1+13,x1+10,x1+10,x1+9,x1+6};
   int y[]={y1+14,y1+11,y1+8,y1+7,y1+7,y1+4,y1+3,y1+2, y1+7, y1+8, y1+8, y1+11,y1+13,y1+14,y1+11};
//--- Draw the "button" shape 
   this.m_canvas.Polygon(x,y,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- If the movability flag is reset (pinned) - cross out the drawn button
   if(!this.m_movable)
      this.m_canvas.Line(x1+3,y1+2,x1+15,y1+14,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- Remember the current background color and button design
   this.m_butt_pin_back_color_c=back_color;
   this.m_butt_pin_fore_color_c=fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

这些方法与绘制和重新绘制关闭按钮的方法相同。逻辑完全相同,并且写在代码的注释中。


绘制面板的方法:

//+------------------------------------------------------------------+
//| Draw the panel                                                   |
//+------------------------------------------------------------------+
void CDashboard::Draw(const string title)
  {
//--- Set the title text
   this.m_title=title;
//--- If the collapse flag is not set, expand the panel
   if(!this.m_minimized)
      this.Expand();
//--- Otherwise, collapse the panel
   else
      this.Collapse();
//--- Update the canvas without redrawing the chart
   this.m_canvas.Update(false);
//--- Update the working space and redraw the chart
   this.m_workspace.Update();
  }

如果未设置折叠标志,就展开面板(以展开形式绘制)。如果设置了折叠标志,则折叠面板:以折叠的形式绘制面板,只留下标题。


折叠面板的方法:

//+------------------------------------------------------------------+
//| Collapse the panel                                               |
//+------------------------------------------------------------------+
void CDashboard::Collapse(void)
  {
//--- Save the pixels of the working space and the panel background into arrays
   this.SaveWorkspace();
   this.SaveBackground();
//--- Remember the current height of the panel
   int h=this.m_h;
//--- Change the dimensions (height) of the canvas and working space
   if(!this.SetSizes(this.m_canvas.Width(),this.m_header_h))
      return;
//--- Draw the header area
   this.DrawHeaderArea(this.m_title);
//--- Return the saved panel height to the variable
   this.m_h=h;
  }

在折叠面板之前,我们需要将背景和工作空间的所有像素保存到阵列中。这对于通过简单地从像素阵列恢复面板图像和工作空间而不是重新绘制面板来快速扩展面板是必要的。此外,我们可以在面板的背景上绘制一些额外的装饰(例如,铭牌)。它们也将与背景一起保存,然后恢复。


展开面板的方法:

//+------------------------------------------------------------------+
//| Expand the panel                                                 |
//+------------------------------------------------------------------+
void CDashboard::Expand(void)
  {
//--- Resize the panel
   if(!this.SetSizes(this.m_canvas.Width(),this.m_h))
      return;
//--- If the panel background pixels have never been saved into an array
   if(this.m_array_ppx.Size()==0)
     {
      //--- Draw the panel and
      this.m_canvas.Erase(::ColorToARGB(this.m_back_color,this.m_alpha));
      this.DrawFrame();
      this.DrawHeaderArea(this.m_title);
      //--- save the background pixels of the panel and working space into arrays
      this.SaveWorkspace();
      this.SaveBackground();
     }
//--- If the background pixels of the panel and working space were previously saved,
   else
     {
      //--- restore the background pixels of the panel and working space from arrays
      this.RestoreBackground();
      if(this.m_array_wpx.Size()>0)
         this.RestoreWorkspace();
     }
//--- If, after expanding, the panel goes beyond the chart window, adjust the panel location
   if(this.m_y+this.m_canvas.Height()>this.m_chart_h-1)
      this.Move(this.m_x,this.m_chart_h-1-this.m_canvas.Height());
  }

如果保存背景和工作区像素的数组为空,则我们使用绘图方法绘制整个面板。如果数组已经填充,那么只需从数组中恢复面板的背景及其工作空间。


使用颜色的辅助方法:

//+------------------------------------------------------------------+
//| Returns color with a new color component                         |
//+------------------------------------------------------------------+
color CDashboard::NewColor(color base_color, int shift_red, int shift_green, int shift_blue)
  {
   double clR=0, clG=0, clB=0;
   this.ColorToRGB(base_color,clR,clG,clB);
   double clRn=(clR+shift_red  < 0 ? 0 : clR+shift_red  > 255 ? 255 : clR+shift_red);
   double clGn=(clG+shift_green< 0 ? 0 : clG+shift_green> 255 ? 255 : clG+shift_green);
   double clBn=(clB+shift_blue < 0 ? 0 : clB+shift_blue > 255 ? 255 : clB+shift_blue);
   return this.RGBToColor(clRn,clGn,clBn);
  }
//+------------------------------------------------------------------+
//| Convert RGB to color                                             |
//+------------------------------------------------------------------+
color CDashboard::RGBToColor(const double r,const double g,const double b) const
  {
   int int_r=(int)::round(r);
   int int_g=(int)::round(g);
   int int_b=(int)::round(b);
   int clr=0;
   clr=int_b;
   clr<<=8;
   clr|=int_g;
   clr<<=8;
   clr|=int_r;
//---
   return (color)clr;
  }
//+------------------------------------------------------------------+
//| Getting values of the RGB components                             |
//+------------------------------------------------------------------+
void CDashboard::ColorToRGB(const color clr,double &r,double &g,double &b)
  {
   r=GetR(clr);
   g=GetG(clr);
   b=GetB(clr);
  }

当光标与面板控件交互时,需要这些方法来更改颜色。


设置标题透明度的方法:

//+------------------------------------------------------------------+
//| Set the header transparency                                      |
//+------------------------------------------------------------------+
void CDashboard::SetHeaderTransparency(const uchar value)
  {
   this.m_header_alpha=value;
   if(this.m_header_alpha_c!=this.m_header_alpha)
      this.RedrawHeaderArea(clrNONE,NULL,clrNONE,value);
   this.m_header_alpha_c=value;
  }

首先,将传递给方法的透明度值写入存储默认透明度的变量,然后将新值与当前值进行比较。如果值不相等,则会完全重新绘制标题区域。最后,将设置的透明度写入当前透明度。


设置面板透明度的方法:

//+------------------------------------------------------------------+
//| Set the panel transparency                                       |
//+------------------------------------------------------------------+
void CDashboard::SetTransparency(const uchar value)
  {
   this.m_alpha=value;
   if(this.m_alpha_c!=this.m_alpha)
     {
      this.m_canvas.Erase(::ColorToARGB(this.m_back_color,value));
      this.DrawFrame();
      this.RedrawHeaderArea(clrNONE,NULL,clrNONE,value);
      this.m_canvas.Update(false);
     }
   this.m_alpha_c=value;
  }

其逻辑与上述方法类似。如果传递给该方法的透明度不等于当前透明度,则会使用新的透明度完全重新绘制面板。


设置工作空间默认字体参数的方法:

//+------------------------------------------------------------------+
//| Set the default font parameters of the working space             |
//+------------------------------------------------------------------+
void CDashboard::SetFontParams(const string name,const int size,const uint flags=0,const uint angle=0)
  {
   if(!this.m_workspace.FontSet(name,size*-10,flags,angle))
     {
      ::PrintFormat("%s: Failed to set font options. Error %lu",(string)__FUNCTION__,::GetLastError());
      return;
     }
   this.m_font=name;
   this.m_font_size=size*-10;
  }

传递给方法的字体参数(字体名称、大小、标志和角度)设置为工作空间的CCanvas对象,并存储在变量中。

传递给该方法的字体大小乘以-10,原因如TextSetFont函数注释所述:

如果字体名称使用“::”,则从EX5资源加载字体。如果使用扩展名指定字体名称,则从文件加载字体,如果路径以“\”或“/”开头,则相对于MQL5目录搜索文件,否则相对于调用TextSetFont()函数的EX5文件的路径搜索文件。

字体大小是使用正值或负值设置的。该符号定义文本大小是否取决于操作系统设置(字体比例)。

  • 如果大小为正,则在将逻辑字体显示为物理字体时,会将其转换为设备物理单位(像素)。大小与可用字体中符号单元格的高度相对应。在共享使用TextOut()函数显示的文本和使用OBJ_LABEL(“文本标签”)图形对象显示的文本的情况下,不建议使用此选项。
  • 如果大小为负,则假定其设置为逻辑点的十分之一(值-350等于35个逻辑点),并除以10。结果值被转换为设备的物理单位(像素),并对应于可用字体的字符高度的绝对值。要在屏幕上获得OBJ_LABEL对象大小的文本,请将对象属性中指定的字体大小乘以-10。


启用/禁用图表工作模式的方法:

//+------------------------------------------------------------------+
//| Enable/disable modes of working with the chart                   |
//+------------------------------------------------------------------+
void CDashboard::SetChartsTool(const bool flag)
  {
//--- If the 'true' flag is passed and if chart scrolling is disabled
   if(flag && !::ChartGetInteger(this.m_chart_id,CHART_MOUSE_SCROLL))
     {
      //--- enable chart scrolling, right-click menu and crosshair
      ::ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
      ::ChartSetInteger(0,CHART_CONTEXT_MENU,true);
      ::ChartSetInteger(0,CHART_CROSSHAIR_TOOL,true);
     }
//--- otherwise, if the 'false' flag is passed and if chart scrolling is enabled
   else if(!flag && ::ChartGetInteger(this.m_chart_id,CHART_MOUSE_SCROLL))
     {
      //--- disable chart scrolling, right-click menu and crosshair
      ::ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
      ::ChartSetInteger(0,CHART_CONTEXT_MENU,false);
      ::ChartSetInteger(0,CHART_CROSSHAIR_TOOL,false);
     }
  }

根据传递的标志,该方法检查用鼠标滚动图表的状态,并启用或禁用所有使用图表的模式。当光标在面板窗口内或外时,为了不不断发送设置模式的命令,有必要检查滚动模式。切换模式仅在光标进入面板或光标离开其限制时发生。


将文本消息输出到指定坐标的方法:

//+------------------------------------------------------------------+
//| Display a text message at the specified coordinates              |
//+------------------------------------------------------------------+
void CDashboard::DrawText(const string text,const int x,const int y,const int width=WRONG_VALUE,const int height=WRONG_VALUE)
  {
//--- Declare variables to record the text width and height in them
   int w=width;
   int h=height;
//--- If the width and height of the text passed to the method have zero values,
//--- then the entire working space is completely cleared using the transparent color
   if(width==0 && height==0)
      this.m_workspace.Erase(0x00FFFFFF);
//--- Otherwise
   else
     {
      //--- If the passed width and height have default values (-1), we get its width and height from the text 
      if(width==WRONG_VALUE && height==WRONG_VALUE)
         this.m_workspace.TextSize(text,w,h);
      //--- otherwise,
      else
        {
         //--- if the width passed to the method has the default value (-1) - get the width from the text, or
         //--- if the width passed to the method has a value greater than zero, use the width passed to the method, or
         //--- if the width passed to the method has a zero value, use the value 1 for the width
         w=(width ==WRONG_VALUE ? this.m_workspace.TextWidth(text)  : width>0  ? width  : 1);
         //--- if the height passed to the method has a default value (-1), get the height from the text, or
         //--- if the height passed to the method has a value greater than zero, use the height passed to the method, or
         //--- if the height passed to the method has a zero value, use value 1 for the height
         h=(height==WRONG_VALUE ? this.m_workspace.TextHeight(text) : height>0 ? height : 1);
        }
      //--- Fill the space according to the specified coordinates and the resulting width and height with a transparent color (erase the previous entry)
      this.m_workspace.FillRectangle(x,y,x+w,y+h,0x00FFFFFF);
     }
//--- Display the text to the space cleared of previous text and update the working space without redrawing the screen
   this.m_workspace.TextOut(x,y,text,::ColorToARGB(this.m_fore_color));
   this.m_workspace.Update(false);
  }

在画布上显示图形的情况下,使用画布需要像在画布上使用画笔一样在画布上绘制这些图形。画布上绘制的一个图像被放置在首先绘制的另一个图像的顶部。要替换图像,您应该完全重新绘制整个画布,或者计算上一个图像的尺寸,擦除此区域,然后在擦除的区域上绘制下一个图像。

对于文本,我们可以获得画布上已经绘制的文本的尺寸,以便在绘制下一个文本之前擦除该区域。将所有先前绘制的文本和形状存储在对象的某个位置绝非最佳解决方案。因此,在精确性和简单性与最优性之间进行选择。在这里,我们将获得当前文本的尺寸(可能与之前在此位置绘制的文本的宽度不匹配),并在显示下一个文本之前使用这些尺寸擦除之前绘制的文本。

如果前一个文本的宽度与当前文本的宽度不相同,或者更大,则不会全部擦除。对于这种情况,该方法提供用于传递指示擦除区域在高度和宽度上的所需尺寸的参数:

  1. 如果传递给该方法的宽度和高度值等于-1(默认情况下),则等于当前文本宽度和高度的区域将被擦除,
  2. 如果传递零则整个工作空间被完全擦除,
  3. 如果传递的宽度或高度值大于零,则这些值将分别用于宽度和高度。
通常,如果显示文本,如果使用相同的字体和大小,则其高度将等于前一个文本,但宽度可能会有所不同。因此,您可以选择一个宽度,该宽度将仅擦除用于文本的区域(不影响相邻的区域,同时确保擦除前一个文本),并将其传递给方法。


面板类设计用于以表格格式显示数据。为了方便计算面板上显示的数据的坐标和面板的视觉设计,提供了两种方法,即计算表格单元格的坐标和在面板背景上绘制铭牌(如有必要)。

  • 第一种方法根据传递给它的数据计算表格单元格的坐标:面板中表格的初始X和Y坐标、行数、列数、行高和列宽。我们还可以为表格网格线和“交替”行的属性指定自定义的颜色。
  • 第二种方法根据行和列的数量和从面板边缘开始的表格缩进宽度自动计算行和列大小。在第二种方法中,我们还可以为表格网格线和“交替”行的属性指定自己的颜色。


根据给定参数绘制背景网格的方法:

//+------------------------------------------------------------------+
//| Draw the background grid                                         |
//+------------------------------------------------------------------+
void CDashboard::DrawGrid(const uint x,const uint y,const uint rows,const uint columns,const uint row_size,const uint col_size,
                          const color line_color=clrNONE,bool alternating_color=true)
  {
//--- If the panel is collapsed, leave
   if(this.m_minimized)
      return;
//--- Clear all lists of the tabular data object (remove cells from rows and all rows)
   this.m_table_data.Clear();
//--- Line height cannot be less than 2
   int row_h=int(row_size<2 ? 2 : row_size);
//--- Column width cannot be less than 2
   int col_w=int(col_size<2 ? 2 : col_size);
   
//--- The X1 (left) coordinate of the table cannot be less than 1 (to leave one pixel around the perimeter of the panel for the frame)
   int x1=int(x<1 ? 1 : x);
//--- Calculate the X2 coordinate (right) depending on the number of columns and their width
   int x2=x1+col_w*int(columns>0 ? columns : 1);
//--- The Y1 coordinate is located under the panel title area
   int y1=this.m_header_h+(int)y;
//--- Calculate the Y2 coordinate (bottom) depending on the number of lines and their height
   int y2=y1+row_h*int(rows>0 ? rows : 1);
   
//--- Get the color of the table grid lines, either by default or passed to the method
   color clr=(line_color==clrNONE ? C'200,200,200' : line_color);
//--- If the initial X coordinate is greater than 1, draw a table frame
//--- (in case of the coordinate 1, the table frame is the panel frame)
   if(x1>1)
      this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));
//--- In the loop by table rows,
   for(int i=0;i<(int)rows;i++)
     {
      //--- calculate the Y coordinate of the next horizontal grid line (Y coordinate of the next table row)
      int row_y=y1+row_h*i;
      //--- if the flag of "alternating" line colors is passed and the line is even
      if(alternating_color && i%2==0)
        {
         //--- lighten the table background color and draw a background rectangle
         color new_color=this.NewColor(clr,45,45,45);
         this.m_canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha));
        }
      //--- Draw a table grid horizontal line
      this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
      
      //--- Create a new table row object
      CTableRow *row_obj=new CTableRow(i);
      if(row_obj==NULL)
        {
         ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i);
         continue;
        }
      //--- Add it to the list of rows of the tabular data object
      //--- (if adding an object failed, delete the created object)
      if(!this.m_table_data.AddRow(row_obj))
         delete row_obj;
      //--- Set its Y coordinate in the created row object taking into account the offset from the panel title
      row_obj.SetY(row_y-this.m_header_h);
     }
//--- In the loop by table columns,
   for(int i=0;i<(int)columns;i++)
     {
      //--- calculate the X coordinate of the next vertical grid line (X coordinate of the next table row)
      int col_x=x1+col_w*i;
      //--- If the grid line goes beyond the panel, interrupt the loop
      if(x1==1 && col_x>=x1+m_canvas.Width()-2)
         break;
      //--- Draw a vertical line of the table grid
      this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));
      
      //--- Get the number of created rows from the table data object 
      int total=this.m_table_data.RowsTotal();
      //--- In the loop by table rows
      for(int j=0;j<total;j++)
        {
         //--- get the next row
         CTableRow *row=m_table_data.GetRow(j);
         if(row==NULL)
            continue;
         //--- Create a new table cell 
         CTableCell *cell=new CTableCell(row.Row(),i);
         if(cell==NULL)
           {
            ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i);
            continue;
           }
         //--- Add the created cell to the row
         //--- (if adding an object failed, delete the created object)
         if(!row.AddCell(cell))
           {
            delete cell;
            continue;
           }
         //--- In the created cell object, set its X coordinate and the Y coordinate from the row object 
         cell.SetXY(col_x,row.Y());
        }
     }
//--- Update the canvas without redrawing the chart
   this.m_canvas.Update(false);
  }

该方法的逻辑以及绘制表格和在表格数据对象中创建其实例的顺序在几乎每一行的代码中都有详细描述。将需要写入表格数据对象中绘制表格实例的数据来获得每个单元格的坐标,以便在面板上显示数据时方便地指示所需的坐标。我们只需要根据单元格在表中的位置(行和列)指定单元格编号,并在面板上获取该单元格左上角的坐标。


绘制具有自动单元格大小的背景网格的方法

//+------------------------------------------------------------------+
//| Draws the background grid with automatic cell sizing             |
//+------------------------------------------------------------------+
void CDashboard::DrawGridAutoFill(const uint border,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true)
  {
//--- If the panel is collapsed, leave
   if(this.m_minimized)
      return;
//--- X1 (left) table coordinate
   int x1=(int)border;
//--- X2 (right) table coordinate
   int x2=this.m_canvas.Width()-(int)border-1;
//--- Y1 (upper) table coordinate
   int y1=this.m_header_h+(int)border;
//--- Y2 (lower) table coordinate
   int y2=this.m_canvas.Height()-(int)border-1;

//--- Get the color of the table grid lines, either by default or passed to the method
   color clr=(line_color==clrNONE ? C'200,200,200' : line_color);
//--- If the offset from the edge of the panel is greater than zero, draw a table border,
//--- otherwise, the panel border is used as the table border
   if(border>0)
      this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));

//--- Height of the entire table grid
   int greed_h=y2-y1;
//--- Calculate the row height depending on the table height and the number of rows
   int row_h=(int)::round((double)greed_h/(double)rows);
//--- In the loop based on the number of rows
   for(int i=0;i<(int)rows;i++)
     {
      //--- calculate the Y coordinate of the next horizontal grid line (Y coordinate of the next table row)
      int row_y=y1+row_h*i;
      //--- if the flag of "alternating" line colors is passed and the line is even
      if(alternating_color && i%2==0)
        {
         //--- lighten the table background color and draw a background rectangle
         color new_color=this.NewColor(clr,45,45,45);
         this.m_canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha));
        }
      //--- Draw a table grid horizontal line
      this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
      
      //--- Create a new table row object
      CTableRow *row_obj=new CTableRow(i);
      if(row_obj==NULL)
        {
         ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i);
         continue;
        }
      //--- Add it to the list of rows of the tabular data object
      //--- (if adding an object failed, delete the created object)
      if(!this.m_table_data.AddRow(row_obj))
         delete row_obj;
      //--- Set its Y coordinate in the created row object taking into account the offset from the panel title
      row_obj.SetY(row_y-this.m_header_h);
     }
     
//--- Table grid width
   int greed_w=x2-x1;
//--- Calculate the column width depending on the table width and the number of columns
   int col_w=(int)::round((double)greed_w/(double)columns);
//--- In the loop by table columns,
   for(int i=0;i<(int)columns;i++)
     {
      //--- calculate the X coordinate of the next vertical grid line (X coordinate of the next table row)
      int col_x=x1+col_w*i;
      //--- If this is not the very first vertical line, draw it
      //--- (the first vertical line is either the table frame or the panel frame)
      if(i>0)
         this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));
      
      //--- Get the number of created rows from the table data object 
      int total=this.m_table_data.RowsTotal();
      //--- In the loop by table rows
      for(int j=0;j<total;j++)
        {
         //--- get the next row
         CTableRow *row=this.m_table_data.GetRow(j);
         if(row==NULL)
            continue;
         //--- Create a new table cell 
         CTableCell *cell=new CTableCell(row.Row(),i);
         if(cell==NULL)
           {
            ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i);
            continue;
           }
         //--- Add the created cell to the row
         //--- (if adding an object failed, delete the created object)
         if(!row.AddCell(cell))
           {
            delete cell;
            continue;
           }
         //--- In the created cell object, set its X coordinate and the Y coordinate from the row object 
         cell.SetXY(col_x,row.Y());
        }
     }
//--- Update the canvas without redrawing the chart
   this.m_canvas.Update(false);
  }

该方法与上述方法的不同之处仅在于根据面板边缘的表格缩进、行高(表格高度/行数)和列宽(表格宽度/列数)自动计算表格宽度和高度。它也直接在代码中进行了完整的注释。

创建面板并在图表上绘制后,您需要创建(在面板上绘制)这些铭牌。否则,将不会绘制铭牌,但表格数据将全部计算出来并可以使用。换言之,只有在面板上绘制表格的情况下,才需要在图表上显示面板后绘制表格。


为了快速恢复面板和工作空间的背景,我们使用数组在面板折叠之前从工作空间和面板的图像中接收像素。展开面板时,面板的背景和工作空间只是从像素数组中恢复,而不是重新绘制之前在面板上绘制的所有内容。这比保存画布上绘制的所有内容以便以后重新绘制要实用得多。

面板和工作空间有两种方法——将图像保存到像素数组和从像素数组恢复图像。


将工作空间保存到像素数组的方法:

//+------------------------------------------------------------------+
//| Save the working space to the array of pixels                    |
//+------------------------------------------------------------------+
void CDashboard::SaveWorkspace(void)
  {
//--- Calculate the required size of the array (width * height of the working space)
   uint size=this.m_workspace.Width()*this.m_workspace.Height();
//--- If the size of the array is not equal to the calculated one, change it
   if(this.m_array_wpx.Size()!=size)
     {
      ::ResetLastError();
      if(::ArrayResize(this.m_array_wpx,size)!=size)
        {
         ::PrintFormat("%s: ArrayResize failed. Error %lu",(string)__FUNCTION__,::GetLastError());
         return;
        }
     }
   uint n=0;
//--- In the loop along the height of the working space (pixel Y coordinate)
   for(int y=0;y<this.m_workspace.Height();y++)
      //--- in the loop by the working space width (pixel X coordinate)
      for(int x=0;x<this.m_workspace.Width();x++)
        {
         //--- calculate the pixel index in the receiving array
         n=this.m_workspace.Width()*y+x;
         if(n>this.m_array_wpx.Size()-1)
            break;
         //--- copy pixel to the receiving array from the working space X and Y
         this.m_array_wpx[n]=this.m_workspace.PixelGet(x,y);
        }
  }

在两个嵌套循环中遍历每个图像行的每个像素,并将它们复制到接收数组中。


从像素数组恢复工作空间的方法:

//+------------------------------------------------------------------+
//| Restore the working space from the array of pixels               |
//+------------------------------------------------------------------+
void CDashboard::RestoreWorkspace(void)
  {
//--- Exit if the array is empty
   if(this.m_array_wpx.Size()==0)
      return;
   uint n=0;
//--- In the loop along the height of the working space (pixel Y coordinate)
   for(int y=0;y<this.m_workspace.Height();y++)
      //--- in the loop by the working space width (pixel X coordinate)
      for(int x=0;x<this.m_workspace.Width();x++)
        {
         //--- calculate the pixel index in the array
         n=this.m_workspace.Width()*y+x;
         if(n>this.m_array_wpx.Size()-1)
            break;
         //--- copy the pixel from the array to the X and Y coordinates of the working space
         this.m_workspace.PixelSet(x,y,this.m_array_wpx[n]);
        }
  }

在两个嵌套循环中,我们计算数组中每个图像行的每个像素的索引,并将它们从数组复制到图像的X和Y坐标。


将面板背景保存到像素数组的方法:

//+------------------------------------------------------------------+
//| Save the panel background to the pixel array                     |
//+------------------------------------------------------------------+
void CDashboard::SaveBackground(void)
  {
//--- Calculate the required size of the array (panel width * height)
   uint size=this.m_canvas.Width()*this.m_canvas.Height();
//--- If the size of the array is not equal to the calculated one, change it
   if(this.m_array_ppx.Size()!=size)
     {
      ::ResetLastError();
      if(::ArrayResize(this.m_array_ppx,size)!=size)
        {
         ::PrintFormat("%s: ArrayResize failed. Error %lu",(string)__FUNCTION__,::GetLastError());
         return;
        }
     }
   uint n=0;
//--- In the loop by the panel height (pixel Y coordinate)
   for(int y=0;y<this.m_canvas.Height();y++)
      //--- in the loop by the panel width (pixel X coordinate)
      for(int x=0;x<this.m_canvas.Width();x++)
        {
         //--- calculate the pixel index in the receiving array
         n=this.m_canvas.Width()*y+x;
         if(n>this.m_array_ppx.Size()-1)
            break;
         //--- copy pixel to the receiving array from the panel X and Y
         this.m_array_ppx[n]=this.m_canvas.PixelGet(x,y);
        }
  }


从像素数组恢复面板背景的方法

//+------------------------------------------------------------------+
//| Restore the panel background from the array of pixels            |
//+------------------------------------------------------------------+
void CDashboard::RestoreBackground(void)
  {
//--- Exit if the array is empty
   if(this.m_array_ppx.Size()==0)
      return;
   uint n=0;
//--- In the loop by the panel height (pixel Y coordinate)
   for(int y=0;y<this.m_canvas.Height();y++)
      //--- in the loop by the panel width (pixel X coordinate)
      for(int x=0;x<this.m_canvas.Width();x++)
        {
         //--- calculate the pixel index in the array
         n=this.m_canvas.Width()*y+x;
         if(n>this.m_array_ppx.Size()-1)
            break;
         //--- copy the pixel from the array to the X and Y coordinates of the panel
         this.m_canvas.PixelSet(x,y,this.m_array_ppx[n]);
        }
  }


要将对象带到前台,我们需要连续执行两个操作:隐藏对象并立即显示它。每个图形对象都有OBJPROP_TIMEFRAMES属性,负责其在每个时间框架上的可见性。要在所有时间框架上隐藏对象,我们需要将此属性设置为OBJ_NO_PERIODS。因此,为了显示对象,我们需要将OBJPROP_TIMEFRAMES属性设置为OBJ_ALL_PERIODS。


隐藏面板的方法:

//+------------------------------------------------------------------+
//| Hide the panel                                                   |
//+------------------------------------------------------------------+
void CDashboard::Hide(const bool redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   ::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   if(redraw)
      ::ChartRedraw(this.m_chart_id);
  }

对于面板和工作空间对象,OBJPROP_TIMEFRAMES属性设置为OBJ_NO_PERIODS,并重新绘制图表以立即反映更改(如果相应的标志设置为)。


显示面板的方法:

//+------------------------------------------------------------------+
//| Display the panel                                                |
//+------------------------------------------------------------------+
void CDashboard::Show(const bool redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
   if(!this.m_minimized)
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
   if(redraw)
      ::ChartRedraw(this.m_chart_id);
  }

对于面板对象,OBJPROP_TIMEFRAMES属性立即设置为OBJ_ALL_PERIODS。对于工作空间对象,仅当面板处于展开状态时才设置该值。

选中重新绘制标志后,将重新绘制图表以立即显示更改。


将面板带到前台的方法:

//+------------------------------------------------------------------+
//| Bring the panel to the foreground                                |
//+------------------------------------------------------------------+
void CDashboard::BringToTop(void)
  {
   this.Hide(false);
   this.Show(true);
  }

首先,面板和工作空间被隐藏,而无需重新绘制图表,然后它们会立即显示并重新绘制图表。

在某些情况下,当像素数组无法存储在内存中时,需要将其保存到文件中,然后从文件中加载。该类包含用于将像素数组保存到文件并从文件加载它们的方法:

//+------------------------------------------------------------------+
//| Save the pixel array of the working space to a file              |
//+------------------------------------------------------------------+
bool CDashboard::FileSaveWorkspace(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\workspace.bin";
//--- If the saved array is empty, inform of that and return 'false'
   if(this.m_array_wpx.Size()==0)
     {
      ::PrintFormat("%s: Error. The workspace pixel array is empty.",__FUNCTION__);
      return false;
     }
//--- If the array could not be saved to a file, report this and return 'false'
   if(!::FileSave(filename,this.m_array_wpx))
     {
      ::PrintFormat("%s: FileSave '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }
//+------------------------------------------------------------------+
//| Save the pixel array of the panel background to a file           |
//+------------------------------------------------------------------+
bool CDashboard::FileSaveBackground(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\background.bin";
//--- If the saved array is empty, inform of that and return 'false'
   if(this.m_array_ppx.Size()==0)
     {
      ::PrintFormat("%s: Error. The background pixel array is empty.",__FUNCTION__);
      return false;
     }
//--- If the array could not be saved to a file, report this and return 'false'
   if(!::FileSave(filename,this.m_array_ppx))
     {
      ::PrintFormat("%s: FileSave '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }
//+------------------------------------------------------------------+
//| Upload the array of working space pixels from a file             |
//+------------------------------------------------------------------+
bool CDashboard::FileLoadWorkspace(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\workspace.bin";
//--- If failed to upload data from the file into the array, report this and return 'false'
   if(::FileLoad(filename,this.m_array_wpx)==WRONG_VALUE)
     {
      ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }
//+------------------------------------------------------------------+
//| Upload the array of panel background pixels from a file          |
//+------------------------------------------------------------------+
bool CDashboard::FileLoadBackground(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\background.bin";
//--- If failed to upload data from the file into the array, report this and return 'false'
   if(::FileLoad(filename,this.m_array_ppx)==WRONG_VALUE)
     {
      ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }

目前,处理这些方法还没有实现,因为在撰写本文时发现了对它们的需求。未来关于仪表板类的文章可能会使用这些方法。


带仪表板的指标

为了测试仪表板,让我们创建一个简单的指标,绘制一个通常的移动平均值。我们将显示当前的出价和询价,以及鼠标光标当前悬停的烛形数据。

在Indicators文件夹中,使用新的自定义指标创建一个新的TestDashboard文件夹:


接下来,设置参数:


如果需要进一步改进,请选择第一个OnCalculate、OnChartEvent和OnTimer类型:


选择一个要绘制的缓冲区,然后单击“完成”:


我们得到以下模板:

//+------------------------------------------------------------------+
//|                                                TestDashboard.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot MA
#property indicator_label1  "MA"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      InpPeriodMA=10;
input int      InpMethodMA=0;
input int      InpPriceMA=0;
input int      InpPanelX=20;
input int      InpPanelY=20;
input int      InpUniqID=0;
//--- indicator buffers
double         MABuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,MABuffer,INDICATOR_DATA);
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
//--- return value of prev_calculated for the next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
   
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   
  }
//+------------------------------------------------------------------+

让我们将面板对象类文件保存到创建的指标文件夹中,以便包含的面板类文件与指标位于同一文件夹中。在完成面板类后,其最终版本可以放置在交易终端的文件沙盒的Include文件夹中,用于自定义程序。

让我们调整创建的指标模板。包括面板对象类用更容易理解的初始值初始化输入参数固定正在绘制的缓冲区的名称声明全局变量

//+------------------------------------------------------------------+
//|                                                TestDashboard.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot MA
#property indicator_label1  "MA"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- includes
#include "Dashboard.mqh"
//--- input variables
input int                  InpPeriodMA =  10;            /* MA Period   */ // Moving average calculation period
input ENUM_MA_METHOD       InpMethodMA =  MODE_SMA;      /* MA Method   */ // Moving average calculation method
input ENUM_APPLIED_PRICE   InpPriceMA  =  PRICE_CLOSE;   /* MA Price    */ // Moving average calculation price
input int                  InpPanelX   =  20;            /* Dashboard X */ // Panel X coordinate
input int                  InpPanelY   =  20;            /* Dashboard Y */ // Panel Y coordinate
input int                  InpUniqID   =  0;             /* Unique ID   */ // Unique ID for the panel object
//--- indicator buffers
double         BufferMA[];
//--- global variables
CDashboard    *dashboard=NULL;
int            handle_ma;        // Moving Average indicator handle
int            period_ma;        // Moving Average calculation period
int            mouse_bar_index;  // Index of the bar the data is taken from
string         plot_label;       // Name of the graphical indicator series displayed in DataWindow
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {

OnInit()处理函数中,创建移动平均标准指标的句柄,设置指标的参数和要绘制的缓冲区。由于指标是从历史开始到当前数据计算的,我们将按照时间序列设置指标缓冲区的索引。在同一处理函数中创建仪表板对象。创建对象后,立即显示该对象并绘制表格网格。接下来,将表格数据发送到日志中:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,BufferMA,INDICATOR_DATA);

//--- Create indicator handle
   period_ma=(InpPeriodMA<1 ? 1 : InpPeriodMA);
   ResetLastError();
   handle_ma=iMA(Symbol(),PERIOD_CURRENT,period_ma,0,InpMethodMA,InpPriceMA);
   if(handle_ma==INVALID_HANDLE)
     {
      PrintFormat("%s Failed to create MA indicator handle. Error %lu",__FUNCTION__,GetLastError());
      return INIT_FAILED;
     }
//--- Set the indicator parameters
   IndicatorSetInteger(INDICATOR_DIGITS,Digits());
   IndicatorSetString(INDICATOR_SHORTNAME,"Test Dashboard");
//--- Set the parameters of the buffer being drawn
   plot_label="MA("+(string)period_ma+","+StringSubstr(EnumToString(Period()),7)+")";
   PlotIndexSetString(0,PLOT_LABEL,plot_label);
   ArraySetAsSeries(BufferMA,true);

//--- Create the panel object
   dashboard=new CDashboard(InpUniqID,InpPanelX,InpPanelY,200,250);
   if(dashboard==NULL)
     {
      Print("Error. Failed to create dashboard object");
      return INIT_FAILED;
     }
//--- Display the panel with the "Symbol, Timeframe description" header text
   dashboard.View(Symbol()+", "+StringSubstr(EnumToString(Period()),7));
//--- Draw the name plate on the panel background
   dashboard.DrawGridAutoFill(2,12,2);
   //dashboard.DrawGrid(2,1,12,2,19,97);
//--- Display tabular data in the journal
   dashboard.GridPrint(2);
//--- Initialize the variable with the index of the mouse cursor bar
   mouse_bar_index=0;
//--- Successful initialization
   return(INIT_SUCCEEDED);
  }

这里的所有逻辑都在代码中注释。该表是通过自动计算行和列的大小来创建的。一个简单表的创建在代码中被注释掉了。您可以注释掉自动大小表格,取消注释简单的一个并重新编译指示器。考虑到当前表格参数,差异将是微不足道的。

OnDeinit()处理程序中,移除面板,释放指标句柄并删除图表注释:

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- If the panel object exists, delete it
   if(dashboard!=NULL)
      delete dashboard;
//--- Release the handle of the MA indicator
   ResetLastError();
   if(!IndicatorRelease(handle_ma))
      PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError());
//--- Delete all comments
   Comment("");
  }

OnCalculate()处理函数中,所有预定义的时间序列数组都将具有与时间序列类似的索引,以便它们与绘图缓冲区的索引匹配。处理函数代码注释中描述了其他所有内容:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//--- Set indexing for the arrays as in a timeseries
   ArraySetAsSeries(time,true);
   ArraySetAsSeries(open,true);
   ArraySetAsSeries(high,true);
   ArraySetAsSeries(low,true);
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

//--- Check for the minimum number of bars for calculation
   if(rates_total<period_ma) return 0;

//--- Check and calculate the number of calculated bars
   int limit=rates_total-prev_calculated;
//--- If 'limit' is 0, then only the current bar is calculated
//--- If 'limit' is 1 (opening a new bar), then two bars are calculated - the current newly opened one and the previous one
//--- If 'limit' is more than 1, then this is either the first launch of the indicator, or some changes in history - the indicator is completely recalculated
   if(limit>1)
     {
      limit=rates_total-period_ma-1;
      ArrayInitialize(BufferMA,EMPTY_VALUE);
     }
     
//--- Calculate the amount of data copied from the indicator handle to the drawing buffer
   int count=(limit>1 ? rates_total : 1),copied=0;
//--- Prepare data (receive data to the moving average buffer by handle)
   copied=CopyBuffer(handle_ma,0,0,count,BufferMA);
   if(copied!=count) return 0;

//--- Loop of indicator calculation based on the moving average data
   for(int i=limit; i>=0 && !IsStopped(); i--)
     {
      // Here we calculate a certain indicator based on the standard Moving Average data
     }

//--- Receive price and timeseries data and display it on the panel
//--- At the first launch, we display the data of the current bar on the panel
   static bool first=true;
   if(first)
     {
      DrawData(0,TimeCurrent());
      first=false;
     }

//--- Declare the price structure and get the current prices
   MqlTick  tick={0};
   if(!SymbolInfoTick(Symbol(),tick))
      return 0;

//--- If the cursor is on the current bar, display the data of the current bar on the panel
   if(mouse_bar_index==0)
      DrawData(0,time[0]);
//--- Otherwise, display only the Bid and Ask prices on the panel (we update the prices on the panel at each tick)
   else
     {
      dashboard.DrawText("Bid",dashboard.CellX(0,0)+2,dashboard.CellY(0,0)+2); dashboard.DrawText(DoubleToString(tick.bid,Digits()),dashboard.CellX(0,1)+2,dashboard.CellY(0,1)+2,90);
      dashboard.DrawText("Ask",dashboard.CellX(1,0)+2,dashboard.CellY(1,0)+2); dashboard.DrawText(DoubleToString(tick.ask,Digits()),dashboard.CellX(1,1)+2,dashboard.CellY(1,1)+2,90);
     }
   
//--- return value of prev_calculated for the next call
   return(rates_total);
  }

OnChartEvent()处理程序中,指标首先调用OnChartEvent面板处理程序,然后处理必要的事件:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Call the panel event handler
   dashboard.OnChartEvent(id,lparam,dparam,sparam);

//--- If the cursor moves or a click is made on the chart
   if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
     {
      //--- Declare the variables to record time and price coordinates in them
      datetime time=0;
      double price=0;
      int wnd=0;
      //--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price))
        {
         //--- write the bar index where the cursor is located to a global variable
         mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         //--- Display the bar data under the cursor on the panel 
         DrawData(mouse_bar_index,time);
        }
     }

//--- If we received a custom event, display the appropriate message in the journal
   if(id>CHARTEVENT_CUSTOM)
     {
      //--- Here we can implement handling a click on the close button on the panel
      PrintFormat("%s: Event id=%ld, object id (lparam): %lu, event message (sparam): %s",__FUNCTION__,id,lparam,sparam);
     }
  }

在这里,当点击关闭按钮时从面板接收到事件时,如果有必要对此类事件做出反应,我们需要注册其处理过程。有关此事件中程序行为的决定权在于程序开发人员。


按索引显示当前价格和柱形数据的函数:

//+------------------------------------------------------------------+
//| Display data from the specified timeseries index to the panel    |
//+------------------------------------------------------------------+
void DrawData(const int index,const datetime time)
  {
//--- Declare the variables to receive data in them
   MqlTick  tick={0};
   MqlRates rates[1];
//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      return;
//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,1,rates)!=1)
      return;
//--- Display the current prices and data of the specified bar on the panel
   dashboard.DrawText("Bid",        dashboard.CellX(0,0)+2,   dashboard.CellY(0,0)+2);  dashboard.DrawText(DoubleToString(tick.bid,Digits()),       dashboard.CellX(0,1)+2,   dashboard.CellY(0,1)+2,90);
   dashboard.DrawText("Ask",        dashboard.CellX(1,0)+2,   dashboard.CellY(1,0)+2);  dashboard.DrawText(DoubleToString(tick.ask,Digits()),       dashboard.CellX(1,1)+2,   dashboard.CellY(1,1)+2,90);
   dashboard.DrawText("Date",       dashboard.CellX(2,0)+2,   dashboard.CellY(2,0)+2);  dashboard.DrawText(TimeToString(rates[0].time,TIME_DATE),   dashboard.CellX(2,1)+2,   dashboard.CellY(2,1)+2,90);
   dashboard.DrawText("Time",       dashboard.CellX(3,0)+2,   dashboard.CellY(3,0)+2);  dashboard.DrawText(TimeToString(rates[0].time,TIME_MINUTES),dashboard.CellX(3,1)+2,   dashboard.CellY(3,1)+2,90);
   
   dashboard.DrawText("Open",       dashboard.CellX(4,0)+2,   dashboard.CellY(4,0)+2);  dashboard.DrawText(DoubleToString(rates[0].open,Digits()),  dashboard.CellX(4,1)+2,   dashboard.CellY(4,1)+2,90);
   dashboard.DrawText("High",       dashboard.CellX(5,0)+2,   dashboard.CellY(5,0)+2);  dashboard.DrawText(DoubleToString(rates[0].high,Digits()),  dashboard.CellX(5,1)+2,   dashboard.CellY(5,1)+2,90);
   dashboard.DrawText("Low",        dashboard.CellX(6,0)+2,   dashboard.CellY(6,0)+2);  dashboard.DrawText(DoubleToString(rates[0].low,Digits()),   dashboard.CellX(6,1)+2,   dashboard.CellY(6,1)+2,90);
   dashboard.DrawText("Close",      dashboard.CellX(7,0)+2,   dashboard.CellY(7,0)+2);  dashboard.DrawText(DoubleToString(rates[0].close,Digits()), dashboard.CellX(7,1)+2,   dashboard.CellY(7,1)+2,90);
   
   dashboard.DrawText("Volume",     dashboard.CellX(8,0)+2,   dashboard.CellY(8,0)+2);  dashboard.DrawText((string)rates[0].real_volume,            dashboard.CellX(8,1)+2,   dashboard.CellY(8,1)+2,90);
   dashboard.DrawText("Tick Volume",dashboard.CellX(9,0)+2,   dashboard.CellY(9,0)+2);  dashboard.DrawText((string)rates[0].tick_volume,            dashboard.CellX(9,1)+2,   dashboard.CellY(9,1)+2,90);
   dashboard.DrawText("Spread",     dashboard.CellX(10,0)+2,  dashboard.CellY(10,0)+2); dashboard.DrawText((string)rates[0].spread,                 dashboard.CellX(10,1)+2,  dashboard.CellY(10,1)+2,90);
   dashboard.DrawText(plot_label,   dashboard.CellX(11,0)+2,  dashboard.CellY(11,0)+2); dashboard.DrawText(DoubleToString(BufferMA[index],Digits()),dashboard.CellX(11,1)+2,  dashboard.CellY(11,1)+2,90);
//--- Redraw the chart to immediately display all changes on the panel
   ChartRedraw(ChartID());
  }

如果我们注意面板类的DrawText方法,

void CDashboard::DrawText(const string text,const int x,const int y,const int width=WRONG_VALUE,const int height=WRONG_VALUE)

我们可以看到,X和Y坐标在文本之后被传递给方法。这是我们从表格数据中得到的,通过表格单元格的行号和列号来指示其位置。

例如,“出价”和“要价”显示在面板上“出价”0.0(“出价”文本)和0.1(出价)的表格单元格的“地址”处:

dashboard.DrawText("Bid"dashboard.CellX(0,0)+2, dashboard.CellY(0,0)+2);  dashboard.DrawText(DoubleToString(tick.bid,Digits()), dashboard.CellX(0,1)+2, dashboard.CellY(0,1)+2,90);

此处取单元格值

对于“询价”文本:

  • CellX(0,0)--零行零列中的单元格--X坐标值,
  • CellY(0,0)--零行零列中的单元格--Y坐标值。

对于询价价格:

  • CellX(0,1)-零行第一列中的单元格-X坐标值,
  • CellY(0,1)--零行第一列中的单元格--Y坐标值。

第二个单元格中显示的文本宽度的值为90表示当前文本的宽度可能小于前一个文本的宽度。因此,之前的文本不会被完全擦除,并且这两个文本将相互重叠。因此,我们在这里明确指出显示文本的宽度,这保证会擦除之前绘制的文本,但不会擦除相邻的数据,因为写入文本的表字段比90像素宽。

因此,对于每个表格单元格,我们可以在面板中获取其坐标,并在其中显示文本。由于坐标是为表格网格线的交点指示的,因此在X和Y方向的坐标上添加两个像素,以对齐表格单元格内的文本。

编译指标并在图表上启动后,面板上创建和绘制的表中的数据将显示在日志中:

Table: Rows: 12, Columns: 2
  Row   0    Column   0      Cell X:  2      Cell Y:  2   
  Row   0    Column   1      Cell X:  100    Cell Y:  2   
  Row   1    Column   0      Cell X:  2      Cell Y:  21  
  Row   1    Column   1      Cell X:  100    Cell Y:  21  
  Row   2    Column   0      Cell X:  2      Cell Y:  40  
  Row   2    Column   1      Cell X:  100    Cell Y:  40  
  Row   3    Column   0      Cell X:  2      Cell Y:  59  
  Row   3    Column   1      Cell X:  100    Cell Y:  59  
  Row   4    Column   0      Cell X:  2      Cell Y:  78  
  Row   4    Column   1      Cell X:  100    Cell Y:  78  
  Row   5    Column   0      Cell X:  2      Cell Y:  97  
  Row   5    Column   1      Cell X:  100    Cell Y:  97  
  Row   6    Column   0      Cell X:  2      Cell Y:  116 
  Row   6    Column   1      Cell X:  100    Cell Y:  116 
  Row   7    Column   0      Cell X:  2      Cell Y:  135 
  Row   7    Column   1      Cell X:  100    Cell Y:  135 
  Row   8    Column   0      Cell X:  2      Cell Y:  154 
  Row   8    Column   1      Cell X:  100    Cell Y:  154 
  Row   9    Column   0      Cell X:  2      Cell Y:  173 
  Row   9    Column   1      Cell X:  100    Cell Y:  173 
  Row   10   Column   0      Cell X:  2      Cell Y:  192 
  Row   10   Column   1      Cell X:  100    Cell Y:  192 
  Row   11   Column   0      Cell X:  2      Cell Y:  211 
  Row   11   Column   1      Cell X:  100    Cell Y:  211 


如果我们在同一图表上启动两个具有面板的指标,并为面板的唯一ID指定不同的值,则它们将独立工作:


在这里我们可以看到,尽管面板是单独工作的,但每个指示器都有自己的面板。然而,也有一个冲突:当面板移动时,图表也会尝试移动。之所以会发生这种情况,是因为我们移动的一个面板禁止移动图表,而第二个面板在看到光标在其外部时将其打开。

为了消除这种行为,我们可以做的最简单的事情是在全局终端变量中安排一个信号量,其中将写入活动面板的ID。其他元件在那里看到ID以外的东西时,不会干扰图表的管理。

如果我们在测试器可视化模式下运行指标并尝试移动面板,那么它将很难在屏幕上移动。同时,数据可以从测试图表的条形图中获得——当你点击柱形图时,它的数据就会显示在面板上。此外,鼠标右键(按住鼠标右键并沿图表移动光标)有助于指示光标当前所在的面板并显示数据,或者通过标题区域抓住面板并将其移动到所需位置。不幸的是,由于处理事件的实现不完整,我们不得不在测试器可视化模式中使用这些技巧。


结论

今天,我们创建了一个小型面板,可能有助于使用指标制定自定义策略。在接下来的文章中,我们将研究在所有标准指标的EA中包括指标及其数据的处理。


本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/13179

附加的文件 |
Dashboard.mqh (195.26 KB)
TestDashboard.mq5 (24.81 KB)
最近评论 | 前往讨论 (3)
Evgeny
Evgeny | 6 10月 2023 在 00:53

感谢您的分享。

如果需要继承该类,这种实现方式有一个很大的缺点。

1.没有默认构造函数,参数构造函数按大小设置重要参数。例如,如果需要根据类的子类中的参数来计算面板的大小,那么这个逻辑就必须在类外实现,或者在类中实现,但计算要在一个单独的函数中进行,这在一定程度上破坏了逻辑,因为参数构造函数的调用(没有默认构造函数)会出现在初始化列表中。也许更好的解决办法是将初始化从构造函数移到类函数中。

2.带参数的部分被声明为私有,这就不允许后代实现不同的颜色方案或更改页眉大小等功能。

3.我知道这项工作尚未完成,但有些函数还没有实现,例如 SetButtonClose(开/关)、SetButtonMinimize(开/关)。

有了源代码,最终完成它就不成问题了,但还是......

Artyom Trishkin
Artyom Trishkin | 6 10月 2023 在 04:54
Evgeny #:

感谢您的分享。

如果需要继承该类,这种实现方式有一个很大的缺点。

1.没有默认构造函数,参数构造函数按大小设置重要参数。例如,如果需要根据类的子类中的参数来计算面板的大小,那么这个逻辑就必须在类外实现,或者在类中实现,但计算要在一个单独的函数中进行,这在一定程度上破坏了逻辑,因为参数构造函数的调用(没有默认构造函数)将会出现在初始化列表中。也许更好的解决办法是将初始化从构造函数移到类函数中。

2.带参数的部分被声明为私有,这就不允许后代实现不同的颜色方案或改变页眉的大小等。

3.3. 我知道工作还没有完成,但有些函数还没有实现,例如 SetButtonClose(开/关)、SetButtonMinimize(开/关)。

有了源代码,最终完成它就不成问题了,但还是......

这篇文章是一篇教程。其中,面板涵盖了最基本的需求。

1.子代有自己的构造函数,其初始化列表中必须包含父代的构造函数。您可以在其中指定所需的尺寸。

我还没有考虑过颜色方案)以及标题的大小。

这些方法在公共部分声明。奇怪的是,它们并没有被实现。我记得当时我正在测试它们的启用/禁用....。我不得不凭记忆从头开始重写这个类,因为它的第一个版本在磁盘空间耗尽时被 Windows 销毁了。我一定是忘了还原它们。谢谢,我会修好的。

是的,有了源代码--所有的牌都掌握在读者手中。这正是教程文章的目的所在。

Evgeny
Evgeny | 6 10月 2023 在 14:38
Artyom Trishkin #:

这篇文章具有指导性。在这篇文章中,专家小组介绍了最起码的需求。

1.子代有自己的构造函数,其初始化列表必须包含父代的构造函数。在其中指定所需的尺寸。

我甚至还没有考虑过配色方案)以及页眉的尺寸。

这些方法在公共部分声明。奇怪的是,它们并没有被实现。我清楚地记得,我当时正在测试它们的启用/禁用....。我不得不凭记忆从头开始重写这个类,因为它的第一个版本在磁盘空间耗尽时被 Windows 销毁了。我一定是忘了还原它们。谢谢,我会修好的。

是的,有了源代码--所有的牌都掌握在读者手中。这正是教程文章的目的所在。

1.是的,但只要在初始化列表 中调用了祖先构造函数,就必须在某个地方对传入的参数进行计算。特别是如果它不是最原始的。

例如,计算右下角初始显示的 X、Y 位置,根据预期的表格行数计算高度....。这将需要 3 个额外的类函数,而不是在其构造函数中调用祖先构造函数的一段代码中完成的。 虽然它可以工作,但看起来更像一根拐杖。如果能让类的架构更适应进一步的变化,那么这样的代码就不太美观了。(当然,这是一个完美主义者的观点)。

2.小的修饰会使产品更有质量。但总的来说,您的代码很漂亮,您的解决方案也很有趣,读起来很舒服。

3.我很同情您,数据丢失总是令人很不愉快,所以可靠的备份是我们的一切。

谢谢您,我们期待着您的新文章。

了解使用MQL5下单 了解使用MQL5下单
在创建任何交易系统时,我们都需要有效地处理一项任务。这项任务是下单,或者让创建的交易系统自动处理订单,因为它在任何交易系统中都至关重要。因此,您将在本文中找到您需要了解的关于这项任务的大多数主题,以有效地创建您的交易系统。
开发回放系统 — 市场模拟(第 18 部分):跳价和更多跳价(II) 开发回放系统 — 市场模拟(第 18 部分):跳价和更多跳价(II)
显然,目前的衡量度与创建 1-分钟柱线的理想时间相距甚远。这是我们要率先解决的一件事。解决同步问题并不困难。也许这看起来很难,但实际上却很简单。在上一篇文章中,我们没有进行所需的调整,因为它的目的是解释如何把图表上创建 1-分钟柱线的跳价数据转移至市场观察窗口。
用于在EA交易中包含指标的现成模板(第一部分):振荡指标 用于在EA交易中包含指标的现成模板(第一部分):振荡指标
本文从振荡指标类开始研究标准指标,我们将创建现成的模板,用于EA中——声明和设置参数、指标初始化和去初始化,以及从EA中的指标缓冲区接收数据和信号。
MetaTrader 5中的蒙特卡罗置换测试 MetaTrader 5中的蒙特卡罗置换测试
在本文中,我们将了解如何仅使用 Metatrader 5在任何 EA 交易上基于修改的分时数据进行置换测试。