English 中文 Español Deutsch 日本語 Português
preview
Делаем информационную панель для отображения данных в индикаторах и советниках

Делаем информационную панель для отображения данных в индикаторах и советниках

MetaTrader 5Примеры | 30 августа 2023, 14:03
1 554 3
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

Сегодня начнём с создания информационной панели, в которой можно будет отображать данные, указанные разработчиком. Такая панель будет удобна для наглядного отображения данных на графике и при визуальной отладке, когда быстрее поглядеть интересующие значения на данные на панели, чем отслеживать их в отладчике. Т.е.  в тех случаях, когда идёт отладка именно стратегии в зависимости от величин значений некоторых данных, а не при отладке кода в отладчике.

Панель сделаем в виде прототипа окна данных в терминале и заполним её такими же данными:

Рис.1 Окно данных и информационная панель

Наша пользовательская панель позволит добавить на неё любое необходимое количество требуемых данных, подписать (озаглавить) их и отображать и обновлять показания из кода своей программы.

Панель можно будет перемещать по графику мышкой, закреплять в нужном месте графика и сворачивать/разворачивать её. Для удобства размещения своих данных на панели будет возможность отобразить табличку с указанным количеством строк и столбцов. Данные этой таблицы можно будет распечатать в журнале (координаты X и Y каждой ячейки таблицы) и получить их программно — чтобы прямо при выводе текста и данных указать номер строки и столбца, где эти данные должны быть, ну либо просто распечатать в журнал координаты и из него вписать нужные в свой код. Первый способ удобнее в виду полной его автоматизации. Панель также будет иметь активную кнопку закрытия, но её обработку делегируем в управляющую программу, так как только разработчик программы должен решить как реагировать на нажатие на кнопку закрытия. При нажатии на кнопку в обработчик событий программы будет отправлено пользовательское событие, которое разработчик может обработать по своему усмотрению и замыслу.


Классы для получения табличных данных

Так как данные на панели удобно располагать по каким-либо заранее очерченным (визуально, либо виртуально) координатам, то в первую очередь создадим классы для организации табличных данных. Таблицу можно представить в виде простой сетки, пересечения линий которой и будет координатами ячеек таблицы. По таким координатам и можно будет размещать какие-либо визуальные данные. Таблица имеет некое количество строк (горизонтальные линии) и каждая строка имеет определённое количество ячеек (вертикальные линии). В простой таблице, выполненной в виде сетки, во всех строках расположено одинаковое количество ячеек.

Исходя из этого, нам нужно три класса:

  1. Класс ячейки таблицы,
  2. Класс строки таблицы,
  3. Класс таблицы.

Класс ячейки таблицы включает в себя номер строки и номер столбца в таблице и координаты визуального расположения ячейки таблицы на панели — координата X и Y относительно начала координат таблицы в верхнем-левом углу панели.

Класс строки таблицы включает в себя класс ячейки таблицы. Можно создавать в одной строке требуемое количество ячеек.

Класс таблицы включает в себя список строк таблицы. Строки в таблице можно создавать и добавлять в требуемом количестве.

Рассмотрим вкратце все три класса.


Класс ячейки таблицы

//+------------------------------------------------------------------+
//| Класс ячейки таблицы                                             |
//+------------------------------------------------------------------+
class CTableCell : public CObject
  {
private:
   int               m_row;                     // Строка
   int               m_col;                     // Столбец
   int               m_x;                       // Координата X
   int               m_y;                       // Координата Y
public:
//--- Методы установки значений
   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;
                       }
//--- Методы получения значений
   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 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);
                       }
//--- Конструктор/деструктор
                     CTableCell(const int row,const int column) : m_row(row),m_col(column){}
                    ~CTableCell(void){}
  };

Класс унаследован от базового класса для построения стандартной библиотеки MQL5, так как будет помещаться в списки CArrayObj Стандартной библиотеки MQL5, которые могут размещать в себе только объекты CObject, либо объекты, унаследованные от базового CObject.

Назначение всех переменных и методов достаточно прозрачно и понятно. Переменные служат для хранения значений строки (Row) и столбца (Column) таблицы, а координаты — это относительные координаты левого верхнего угла ячейки таблицы на панели. По этим координатам и можно будет что-либо рисовать или помещать на панели.

Виртуальный метод Compare необходим для поиска и сравнения двух объектов-ячеек таблицы. Метод объявлен в классе базового объекта CObject:

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

возвращает ноль, и должен переопределяться в наследуемых классах.

Так как ячейки таблицы добавляются в строку таблицы, т.е. визуально по горизонтали, то и поиск и сравнение должны проводиться по горизонтальным номерам ячеек — по значению Column. Именно это здесь и делает переопределённый виртуальный метод Compare:

//--- Виртуальный метод сравнения двух объектов
   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 на панели:

//+------------------------------------------------------------------+
//| Класс строк таблиц                                               |
//+------------------------------------------------------------------+
class CTableRow : public CObject
  {
private:
  CArrayObj          m_list_cell;               // Список ячеек
  int                m_row;                     // Номер строки
  int                m_y;                       // Координата Y
public:
//--- Возвращает список ячеек таблицы в строке
   CArrayObj        *GetListCell(void)       { return &this.m_list_cell;         }
//--- Возвращает (1) количество ячеек таблицы в строке (2) индекс строки в таблице
   int               CellsTotal(void)  const { return this.m_list_cell.Total();  }
   int               Row(void)         const { return this.m_row;                }
//--- (1) Устанавливает, (2) возвращает координату Y строки
   void              SetY(const int y)       { this.m_y=y;                       }
   int               Y(void)           const { return this.m_y;                  }
//--- Добавляет новую ячейку таблицы в строку
   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;
                       }
//--- Возвращает указатель на указанную ячейку в строке
   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 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);
                       }
//--- Конструктор/деструктор
                     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 для определения равенства объектов.


Метод, добавляющий новую ячейку в список:

//--- Добавляет новую ячейку таблицы в строку
   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.


Метод, возвращающий указатель на указанную ячейку в строке:

//--- Возвращает указатель на указанную ячейку в строке
   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);
                       }

Метод Search класса CArrayObj Стандартной библиотеки ищет в списке равенство по экземпляру объекта, указатель на который передан в метод. Поэтому здесь мы создаём новый временный объект, указывая в его конструкторе переданный в метод номер столбца, получаем индекс объекта в списке, либо -1 если объект с такими параметрами не найден в списке, обязательно удаляем временный объект и возвращаем указатель на найденный объект в списке.
В случае, если объект не найден, и индекс равен -1, то метод At класса CArrayObj вернёт NULL.


Класс таблицы

Таблица состоит из списка строк, которые в свою очередь состоят из списков ячеек. Т.е. в классе табличных данных по сути есть лишь объект CArrayObj, в который помещаются создаваемые строки, и методы для добавления и получения строк и ячеек таблицы:

//+------------------------------------------------------------------+
//| Класс данных таблиц                                              |
//+------------------------------------------------------------------+
class CTableData : public CObject
  {
private:
   CArrayObj         m_list_rows;               // Список строк
public:
//--- Возвращает список строк таблицы
   CArrayObj        *GetListRows(void)       { return &this.m_list_rows;   }
//--- Добавляет новую строку в таблицу
   bool              AddRow(CTableRow *row)
                       {
                        //--- Устанавливаем флаг сортированного списка
                        this.m_list_rows.Sort();
                        //--- Если такой объект уже есть в списке (поиск вернул индекс объекта, а не -1),
                        //--- сообщаем об этом в журнал и возвращаем 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;
                          }
                        //--- Если не удалось добавить указатель в сортированный список - сообщаем об этом в журнал и возвращаем 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;
                          }
                        //--- Успешно - возвращаем true
                        return true;
                       }
//--- Возвращает указатель на (1) указанную строку, (2) указанную ячейку в указанной строке таблицы
   CTableRow        *GetRow(const int index) { return this.m_list_rows.At(index);   }
   CTableCell       *GetCell(const int row,const int column)
                       {
                        //--- Получаем указатель на объект-строку в списке строк
                        CTableRow *row_obj=this.GetRow(row);
                        //--- Если объект получить не удалось - возвращаем NULL
                        if(row_obj==NULL)
                           return NULL;
                        //--- Получаем указатель на объект-ячейку в строке по номеру столбца и
                        CTableCell *cell=row_obj.GetCell(column);
                        //--- возвращаем результат (указатель на объект, либо NULL)
                        return cell;
                       }
//--- Записывает в переданные в метод переменные координаты X и Y указанной ячейки таблицы
   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();
                       }
//--- Возвращает координату X указанной ячейки таблицы
   int               CellX(const uint row,const uint column)
                       {
                        CTableCell *cell=this.GetCell(row,column);
                        return(cell!=NULL ? cell.X() : WRONG_VALUE);
                       }
//--- Возвращает координату Y указанной ячейки таблицы
   int               CellY(const uint row,const uint column)
                       {
                        CTableCell *cell=this.GetCell(row,column);
                        return(cell!=NULL ? cell.Y() : WRONG_VALUE);
                       }
//--- Возвращает количество (1) строк, (2) столбцов в таблице
   int               RowsTotal(void)            { return this.m_list_rows.Total();  }
   int               ColumnsTotal(void)
                       {
                        //--- Если в списке нет ни одной строки - возвращаем 0
                        if(this.RowsTotal()==0)
                           return 0;
                        //--- Получаем указатель на первую строку и возвращаем количество ячеек в ней
                        CTableRow *row=this.GetRow(0);
                        return(row!=NULL ? row.CellsTotal() : 0);
                       }
//--- Возвращает общее количество ячеек таблицы
   int               CellsTotal(void){ return this.RowsTotal()*this.ColumnsTotal(); }
//--- Очищает списки строк и ячеек таблицы
   void              Clear(void)
                       {
                        //--- В цикле по количеству строк в списке строк таблицы
                        for(int i=0;i<this.m_list_rows.Total();i++)
                          {
                           //--- получаем указатель на очередную строку
                           CTableRow *row=this.m_list_rows.At(i);
                           if(row==NULL)
                              continue;
                           //--- из полученного объекта-строки получаем список ячеек,
                           CArrayObj *list_cell=row.GetListCell();
                           //--- очищаем список ячеек
                           if(list_cell!=NULL)
                              list_cell.Clear();
                          }
                        //--- Очищаем список строк
                        this.m_list_rows.Clear();
                       }                
//--- Распечатывает в журнал данные ячеек таблицы
   void              Print(const uint indent=0)
                       {
                        //--- Печатаем в журнал заголовок
                        ::PrintFormat("Table: Rows: %lu, Columns: %lu",this.RowsTotal(),this.ColumnsTotal());
                        //--- В цикле по строкам таблицы
                        for(int r=0;r<this.RowsTotal();r++)
                           //--- в цикле по ячейкам очередной строки
                           for(int c=0;c<this.ColumnsTotal();c++)
                             {
                              //--- получаем указатель на очередную ячейку и выводим в журнал её данные
                              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());
                             }
                       }
//--- Конструктор/деструктор
                     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)
                       {
                        //--- Если в списке нет ни одной строки - возвращаем 0
                        if(this.RowsTotal()==0)
                           return 0;
                        //--- Получаем указатель на первую строку и возвращаем количество ячеек в ней
                        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 CDashboard : public CObject
  {
private:
   CCanvas           m_canvas;                  // Канвас
   CCanvas           m_workspace;               // Рабочая область
   CTableData        m_table_data;              // Массив ячеек таблиц
   ENUM_PROGRAM_TYPE m_program_type;            // Тип программы
   ENUM_MOUSE_STATE  m_mouse_state;             // Состояние кнопок мышки
   uint              m_id;                      // Идентификатор объекта
   long              m_chart_id;                // ChartID
   int               m_chart_w;                 // Ширина графика
   int               m_chart_h;                 // Высота графика
   int               m_x;                       // Координата X
   int               m_y;                       // Координата Y
   int               m_w;                       // Ширина
   int               m_h;                       // Высота
   int               m_x_dock;                  // Координата X закреплённой свёрнутой панели
   int               m_y_dock;                  // Координата Y закреплённой свёрнутой панели
   
   bool              m_header;                  // Флаг наличия заголовка
   bool              m_butt_close;              // Флаг наличия кнопки закрытия
   bool              m_butt_minimize;           // Флаг наличия кнопки сворачивания/разворачивания
   bool              m_butt_pin;                // Флаг наличия кнопки закрепления
   bool              m_wider_wnd;               // Флаг превышения горизонтального размера панели ширины окна
   bool              m_higher_wnd;              // Флаг превышения вертикольного размера панели высоты окна
   bool              m_movable;                 // Флаг перемещаемости панели
   int               m_header_h;                // Высота заголовка
   int               m_wnd;                     // Номер подокна графика
   
   uchar             m_header_alpha;            // Прозрачность заголовка
   uchar             m_header_alpha_c;          // Текущая прозрачность заголовка
   color             m_header_back_color;       // Цвет фона заголовка
   color             m_header_back_color_c;     // Текущий цвет фона заголовка
   color             m_header_fore_color;       // Цвет текста заголовка
   color             m_header_fore_color_c;     // Текущий цвет текста заголовка
   color             m_header_border_color;     // Цвет рамки заголовка
   color             m_header_border_color_c;   // Текущий цвет рамки заголовка
   
   color             m_butt_close_back_color;   // Цвет фона кнопки закрытия
   color             m_butt_close_back_color_c; // Текущий цвет фона кнопки закрытия
   color             m_butt_close_fore_color;   // Цвет значка кнопки закрытия
   color             m_butt_close_fore_color_c; // Текущий цвет значка кнопки закрытия
   
   color             m_butt_min_back_color;     // Цвет фона кнопки сворачивания/разворачивания
   color             m_butt_min_back_color_c;   // Текущий цвет фона кнопки сворачивания/разворачивания
   color             m_butt_min_fore_color;     // Цвет значка кнопки сворачивания/разворачивания
   color             m_butt_min_fore_color_c;   // Текущий цвет значка кнопки сворачивания/разворачивания
   
   color             m_butt_pin_back_color;     // Цвет фона кнопки закрепления
   color             m_butt_pin_back_color_c;   // Текущий цвет фона кнопки закрепления
   color             m_butt_pin_fore_color;     // Цвет значка кнопки закрепления
   color             m_butt_pin_fore_color_c;   // Текущий цвет значка кнопки закрепления
   
   uchar             m_alpha;                   // Прозрачность панели
   uchar             m_alpha_c;                 // Текущая прозрачность панели
   uchar             m_fore_alpha;              // Прозрачность текста
   uchar             m_fore_alpha_c;            // Текущая прозрачность текста
   color             m_back_color;              // Цвет фона
   color             m_back_color_c;            // Текущий цвет фона
   color             m_fore_color;              // Цвет текста
   color             m_fore_color_c;            // Текущий цвет текста
   color             m_border_color;            // Цвет рамки
   color             m_border_color_c;          // Текущий цвет рамки
   
   string            m_title;                   // Текст заголовка
   string            m_title_font;              // Фонт заголовка
   int               m_title_font_size;         // Размер шрифта заголовка
   string            m_font;                    // Фонт
   int               m_font_size;               // Размер шрифта
   
   bool              m_minimized;               // Флаг свёрнутого окна панели
   string            m_program_name;            // Имя программы
   string            m_name_gv_x;               // Наименование глобальной переменной терминала, хранящей координату X
   string            m_name_gv_y;               // Наименование глобальной переменной терминала, хранящей координату Y
   string            m_name_gv_m;               // Наименование глобальной переменной терминала, хранящей флаг свёрнутости панели
   string            m_name_gv_u;               // Наименование глобальной переменной терминала, хранящей флаг закреплённой панели

   uint              m_array_wpx[];             // Массив пикселей для сохранения/восстановления рабочей области
   uint              m_array_ppx[];             // Массив пикселей для сохранения/восстановления фона панели

//--- Возвращает флаг превышения (1) высотой, (2) шириной панели соответствующих размеров графика
   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);   }
//--- Включает/выключает режимы работы с графиком
   void              SetChartsTool(const bool flag);
   
//--- Сохраняет (1) рабочую область, (2) фон панели в массив пикселей
   void              SaveWorkspace(void);
   void              SaveBackground(void);
//--- Восстанавливает (1) рабочую область, (2) фон панели из массива пикселей
   void              RestoreWorkspace(void);
   void              RestoreBackground(void);

//--- Сохраняет массив пикселей (1) рабочей области, (2) фона панели в файл
   bool              FileSaveWorkspace(void);
   bool              FileSaveBackground(void);
//--- Загружает массив пикселей (1) рабочей области, (2) фона панели из файла
   bool              FileLoadWorkspace(void);
   bool              FileLoadBackground(void);

//--- Возвращает номер подокна
   int               GetSubWindow(void) const
                       {
                        return(this.m_program_type==PROGRAM_EXPERT || this.m_program_type==PROGRAM_SCRIPT ? 0 : ::ChartWindowFind());
                       }
   
protected:
//--- (1) Скрывает, (2) показывает, (3) переносит на передний план панель
   void              Hide(const bool redraw=false);
   void              Show(const bool redraw=false);
   void              BringToTop(void);
//--- Возвращает идентификатор графика
   long              ChartID(void)        const { return this.m_chart_id;              }
//--- Рисует область заголовка
   void              DrawHeaderArea(const string title);
//--- Перерисовывает область заголовка с новыми значениями цвета и текста
   void              RedrawHeaderArea(const color new_color=clrNONE,const string title="",const color title_new_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- Рисует рамку панели
   void              DrawFrame(void);
//--- (1) Рисует, (2) перерисовывает кнопку закрытия панели
   void              DrawButtonClose(void);
   void              RedrawButtonClose(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- (1) Рисует, (2) перерисовывает кнопку сворачивания/разворачивания панели
   void              DrawButtonMinimize(void);
   void              RedrawButtonMinimize(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- (1) Рисует, (2) перерисовывает кнопку закрепления панели
   void              DrawButtonPin(void);
   void              RedrawButtonPin(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);

//--- Возвращает флаг работы в визуальном тестере
   bool              IsVisualMode(void) const
                       { return (bool)::MQLInfoInteger(MQL_VISUAL_MODE);               }
//--- Возвращает описание таймфрейма
   string            TimeframeDescription(const ENUM_TIMEFRAMES timeframe) const
                       { return ::StringSubstr(EnumToString(timeframe),7);             }

//--- Возвращает состояние кнопок мышки
   ENUM_MOUSE_STATE  MouseButtonState(const int x,const int y,bool pressed);
//--- Смещает панель на новые координаты
   void              Move(int x,int y);

//--- Преобразует RGB в color
   color             RGBToColor(const double r,const double g,const double b) const;
//--- Записывает в переменные значения компонентов RGB
   void              ColorToRGB(const color clr,double &r,double &g,double &b);
//--- Возвращает составляющую цвета (1) Red, (2) Green, (3) Blue
   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;                }
//--- Возвращает новый цвет
   color             NewColor(color base_color, int shift_red, int shift_green, int shift_blue);

//--- Рисует панель
   void              Draw(const string title);
//--- (1) Сворачивает, (2) разворачивает панель
   void              Collapse(void);
   void              Expand(void);

//--- Устанавливает координату (1) X, (2) Y панели
   bool              SetCoordX(const int coord_x);
   bool              SetCoordY(const int coord_y);
//--- Устанавливает (1) ширину, (2) высоту панели
   bool              SetWidth(const int width,const bool redraw=false);
   bool              SetHeight(const int height,const bool redraw=false);

public:
//--- Отображает панель
   void              View(const string title)   { this.Draw(title);                    }
//--- Возвращает объект (1) CCanvas, (2) рабочую область, (3) идентификатор объекта
   CCanvas          *Canvas(void)               { return &this.m_canvas;               }
   CCanvas          *Workspace(void)            { return &this.m_workspace;            }
   uint              ID(void)                   { return this.m_id;                    }
   
//--- Возвращает координату (1) X, (2) Y панели
   int               CoordX(void)         const { return this.m_x;                     }
   int               CoordY(void)         const { return this.m_y;                     }
//--- Возвращает (1) ширину, (2) высоту панели
   int               Width(void)          const { return this.m_w;                     }
   int               Height(void)         const { return this.m_h;                     }

//--- Возвращает (1) ширину, (2) высоту, (3) размеры указанного текста
   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);                 }
   
//--- Устанавливает флаг (1) наличия, (2) отсутствия кнопки закрытия
   void              SetButtonCloseOn(void);
   void              SetButtonCloseOff(void);
//--- Устанавливает флаг (1) наличия, (2) отсутствия кнопки сворачивания/разворачивания
   void              SetButtonMinimizeOn(void);
   void              SetButtonMinimizeOff(void);
   
//--- Устанавливает координаты панели
   bool              SetCoords(const int x,const int y);
//--- Устанавливает размеры панели
   bool              SetSizes(const int w,const int h,const bool update=false);
//--- Устанавливает координаты и размеры панели
   bool              SetParams(const int x,const int y,const int w,const int h,const bool update=false);

//--- Устанавливает прозрачность (1) заголовка, (2) рабочей области панели
   void              SetHeaderTransparency(const uchar value);
   void              SetTransparency(const uchar value);
//--- Устанавливает параметры шрифта панели по умолчанию
   void              SetFontParams(const string name,const int size,const uint flags=0,const uint angle=0);
//--- Выводит текстовое сообщение в указанные координаты
   void              DrawText(const string text,const int x,const int y,const int width=WRONG_VALUE,const int height=WRONG_VALUE);
//--- Рисует (1) фоновую сетку, (2) с автоматическим размером ячеек
   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);
//--- Распечатывает данные сетки (координаты пересечения линий)
   void              GridPrint(const uint indent=0)   { this.m_table_data.Print(indent);  }
//--- Записывает в переменные значения координат X и Y указанной ячейки таблицы
   void              CellXY(const uint row,const uint column, int &x, int &y) { this.m_table_data.CellXY(row,column,x,y);  }
//--- Возвращает координату (1) X, (2) Y указанной ячейки таблицы
   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);         }

//--- Обработчик событий
   void              OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
//--- Конструктор/Деструктор
                     CDashboard(const uint id,const int x,const int y, const int w,const int h,const int wnd=-1);
                    ~CDashboard();
  };

Объявленные переменные и методы класса подробно прокомментированы в коде. Рассмотрим реализацию некоторых методов.


Конструктор класса:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
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),
                        
                        //--- Оформление заголовка панели
                        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),
                        
                        //--- кнопка закрытия
                        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),
                        
                        //--- кнопка сворачивания/разворачивания
                        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),
                        
                        //--- кнопка закрепления
                        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),
                        
                        //--- Оформление панели
                        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)
  {
//--- Устанавливаем для графика разрешения на отправку сообщений о событиях перемещения и нажатия кнопок мышки,
//--- о событиях колёсика мышки и событиях создания и удаления графического объекта
   ::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);
   
//--- Задаём имена глобальным переменным терминала для хранения координат панели, состояния свёрнуто/развернуто и закрепления
   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";
   
//--- Если глобальной переменной не существует - создаём её и записываем текущее значение,
//--- иначе - считываем в неё значение из глобальной переменной терминала
//--- Координата X
   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
   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);
//--- Свёрнуто/развёрнуто
   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);
//--- Закреплено/не закреплено
   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);

//--- Устанавливаем флаги превышения размерами панели размеров окна графика
   this.m_higher_wnd=this.HigherWnd();
   this.m_wider_wnd=this.WiderWnd();

//--- Если графический ресурс панели создан,
   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))
     {
      //--- устанавливаем для канваса шрифт и заполняем канвас прозрачным цветом
      this.m_canvas.FontSet(this.m_title_font,this.m_title_font_size,FW_BOLD);
      this.m_canvas.Erase(0x00FFFFFF);
     }
//--- иначе - сообщаем о неудачном создании объекта в журнал
   else
      ::PrintFormat("%s: Error. CreateBitmapLabel for canvas failed",(string)__FUNCTION__);

//--- Если графический ресурс рабочей области создан,
   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))
     {
      //--- устанавливаем для рабочей области шрифт и заполняем рабочую область прозрачным цветом
      this.m_workspace.FontSet(this.m_font,this.m_font_size);
      this.m_workspace.Erase(0x00FFFFFF);
     }
//--- иначе - сообщаем о неудачном создании объекта в журнал
   else
      ::PrintFormat("%s: Error. CreateBitmapLabel for workspace failed",(string)__FUNCTION__);
  }

Класс имеет один параметрический конструктор и один, создаваемый по умолчанию. Нас, естественно, интересует только параметрический, и при создании объекта класса он и будет использоваться. В конструктор через формальные параметры передаются уникальный идентификатор объекта, начальные координаты панели, её ширина и высота и номер подокна, в котором будет размещена панель.

Уникальный идентификатор панели нужен для того, чтобы класс мог создать объекты с уникальными именами. Если на одном графике использовать несколько индикаторов с панелями, то во-избежания конфликта имён объектов и нужен как раз этот уникальный номер, добавляемый к имени объекта панели при его создании. При этом, уникальность идентификатора должна быть повторяемой — при каждом новом запуске номер должен быть таким же, как и в предыдущий запуск. Т.е. нельзя для идентификатора использовать, например, GetTickCount().
Номер подокна, если указан по умолчанию (-1), то ищется программно, иначе — используется указанный в параметре.

Параметры по умолчанию устанавливаются в списке инициализации конструктора. Для некоторых параметров, отвечающих за внешний вид, созданы по две переменные — значение по умолчанию и текущее значение свойства. Это необходимо для интерактивного изменения, например, цвета при наведении указателя на область панели, за которую отвечают такие параметры.

В теле конструктора устанавливаются значения глобальных переменных терминала и создаются два графических объекта — канвас и рабочая область панели.

Весь код конструктора подробно прокомментирован.


Деструктор класса:

//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
CDashboard::~CDashboard()
  {
//--- Записываем текущие значения в глобальные переменные терминала
   ::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);
//--- Удаляем объекты панели
   this.m_canvas.Destroy();
   this.m_workspace.Destroy();
  }

Здесь сначала сбрасываем значения координат и флагов в глобальные переменные терминала, а затем удаляем объекты канваса и рабочей области.

Чтобы взаимодействовать с панелью курсором и кнопками мышки, необходимо знать расположение курсора относительно панели и её управляющих элементов. При перемещении курсора мы можем отследить его координаты и состояния кнопок в обработчике событий класса. Обработчик событий класса имеет те же параметры, что и стандартный обработчик событий OnChartEvent:

void  OnChartEvent()
   const int       id,       // идентификатор события 
   const long&     lparam,   // параметр события типа long
   const double&   dparam,   // параметр события типа double
   const string&   sparam    // параметр события типа string
   );

Параметры

id

[in]  Идентификатор события из перечисления ENUM_CHART_EVENT.

lparam

[in]  Параметр события типа long

dparam

[in]  Параметр события типа double

sparam

[in]  Параметр события типа string

Возвращаемое значение

Нет возвращаемого значения

Примечание

Существуют 11 видов событий, которые можно обрабатывать с помощью предопределенной функции OnChartEvent(). Для пользовательских событий предусмотрено 65535 идентификаторов в диапазоне от CHARTEVENT_CUSTOM до CHARTEVENT_CUSTOM_LAST включительно. Для генерации пользовательского события необходимо использовать функцию EventChartCustom().

Краткое описание событий из перечисления ENUM_CHART_EVENT:

  • CHARTEVENT_KEYDOWN — нажатие клавиатуры, когда окно графика находится в фокусе;
  • CHARTEVENT_MOUSE_MOVE — перемещение мыши и нажатия кнопок мыши (если для графика установлено свойство CHART_EVENT_MOUSE_MOVE=true);
  • CHARTEVENT_OBJECT_CREATE — создание графического объекта (если для графика установлено свойство CHART_EVENT_OBJECT_CREATE=true);
  • CHARTEVENT_OBJECT_CHANGE — изменение свойств объекта через диалог свойств;
  • CHARTEVENT_OBJECT_DELETE — удаление графического объекта (если для графика установлено свойство CHART_EVENT_OBJECT_DELETE=true);
  • CHARTEVENT_CLICK — щелчок мыши на графике;
  • CHARTEVENT_OBJECT_CLICK — щелчок мыши на графическом объекте, принадлежащем графику;
  • CHARTEVENT_OBJECT_DRAG — перемещение графического объекта при помощи мыши;
  • CHARTEVENT_OBJECT_ENDEDIT — окончание редактирования текста в поле ввода графического объекта Edit (OBJ_EDIT);
  • CHARTEVENT_CHART_CHANGE — изменения графика;
  • CHARTEVENT_CUSTOM+n — идентификатор пользовательского события, где n находится в диапазоне от 0 до 65535. CHARTEVENT_CUSTOM_LAST содержит последний допустимый идентификатор пользовательского события (CHARTEVENT_CUSTOM+65535).

В параметре lparam находится координата X, в dparam — координата Y, а в sparam прописываются значения-комбинация флагов для определения состояния кнопок мышки. Все эти параметры нужно получить и обработать относительно координат панели и её элементов, определить состояние и отправить его в обработчик событий класса, где будет прописана реакция на все эти состояния.


Метод, возвращающий состояние курсора и кнопки мыши относительно панели:

//+------------------------------------------------------------------+
//| Возвращает состояние курсора и кнопки мыши                       |
//+------------------------------------------------------------------+
ENUM_MOUSE_STATE CDashboard::MouseButtonState(const int x,const int y,bool pressed)
  {
//--- Если кнопка нажата
   if(pressed)
     {
      //--- Если уже зафиксировано состояние - уходим
      if(this.m_mouse_state!=MOUSE_STATE_NOT_PRESSED)
         return this.m_mouse_state;
      //--- Если нажата кнопка внутри окна
      if(x>this.m_x && x<this.m_x+this.m_w && y>this.m_y && y<this.m_y+this.m_h)
        {
         //--- Если нажата кнопка внутри заголовка
         if(y>this.m_y && y<=this.m_y+this.m_header_h)
           {
            //--- Выводим панель на передний план
            this.BringToTop();
            //--- Координаты кнопок закрытия, сворачивания/разворачивания и закрепления
            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(x>this.m_x+this.m_w-wc)
               return MOUSE_STATE_PRESSED_INSIDE_CLOSE;
            //--- Если нажата кнопка сворачивания/разворачивания - возвращаем это состояние
            if(x>this.m_x+this.m_w-wc-wm)
               return MOUSE_STATE_PRESSED_INSIDE_MINIMIZE;
            //--- Если нажата кнопка закрепления - возвращаем это состояние
            if(x>this.m_x+this.m_w-wc-wm-wp)
               return MOUSE_STATE_PRESSED_INSIDE_PIN;
            //--- Если кнопка нажата не на управляющих кнопках панели - записываем и возвращаем состояние нажатия кнопки внутри заголовка
            this.m_mouse_state=MOUSE_STATE_PRESSED_INSIDE_HEADER;
            return this.m_mouse_state;
           }
         //--- Если нажата кнопка внутри окна - записываем состояние в переменную и возвращаем это состояние
         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;
           }
        }
      //--- Кнопка нажата вне пределов окна - записываем состояние в переменную и возвращаем это состояние
      else
        {
         this.m_mouse_state=MOUSE_STATE_PRESSED_OUTSIDE_WINDOW;
         return this.m_mouse_state;
        }
     }
//--- Если кнопка не нажата
   else
     {
      //--- Записываем в переменную состояние не нажатой кнопки
      this.m_mouse_state=MOUSE_STATE_NOT_PRESSED;
      //--- Если курсор внутри панели
      if(x>this.m_x && x<this.m_x+this.m_w && y>this.m_y && y<this.m_y+this.m_h)
        {
         //--- Если курсор внутри заголовка
         if(y>this.m_y && y<=this.m_y+this.m_header_h)
           {
            //--- Указываем ширину кнопок закрытия, сворачивания/разворачивания и закрепления
            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(x>this.m_x+this.m_w-wc)
               return MOUSE_STATE_INSIDE_CLOSE;
            //--- Если курсор внутри кнопки сворачивания/разворачивания - возвращаем это состояние
            if(x>this.m_x+this.m_w-wc-wm)
               return MOUSE_STATE_INSIDE_MINIMIZE;
            //--- Если курсор внутри кнопки закрепления - возвращаем это состояние
            if(x>this.m_x+this.m_w-wc-wm-wp)
               return MOUSE_STATE_INSIDE_PIN;
            //--- Если курсор за пределами кнопок внутри области заголовка - возвращаем это состояние
            return MOUSE_STATE_INSIDE_HEADER;
           }
         //--- Иначе - Курсор внутри рабочей области - возвращаем это состояние
         else
            return MOUSE_STATE_INSIDE_WINDOW;
        }
     }
//--- В любом ином случае возвращаем состояние не нажатой кнопки мышки
   return MOUSE_STATE_NOT_PRESSED;
  }

Логика метода подробно расписана в комментариях к коду. Просто определяем взаимные координаты курсора и панели и её элементов, и возвращаем состояние. При этом в метод сразу отправляется флаг нажатой или отпущенной кнопки мышки. Для каждого такого состояния есть свой блок кода, определяющий состояния либо при нажатой кнопке, либо при отпущенной. Так использовать логику достаточно просто и быстро. Но есть свои минусы — нельзя определить щелчок мышки по управляющему элементу — только нажатие на него. Обычно щелчок фиксируется при отпускании кнопки мышки, а нажатие и удержание — при нажимании. При логике, используемой здесь, щелчком и нажатием с удержанием является только нажатие кнопки мышки.

Полученные в этом методе состояния нужно отправить в обработчик событий, где на каждое событие прописан свой обработчик, изменяющий поведение и внешний вид панели:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CDashboard::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Если создан графический  объект
   if(id==CHARTEVENT_OBJECT_CREATE)
     {
      this.BringToTop();
      ::ObjectSetInteger(this.m_chart_id,sparam,OBJPROP_SELECTED,true);
     }
//--- Если график изменён
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      //--- Получаем номер подокна графика (он может измениться при удалении окна какого-либо индикатора)
      this.m_wnd=this.GetSubWindow();
      //--- Получаем новые размеры графика
      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);
      //--- Определяем выход размеров панели за пределы окна графика
      this.m_higher_wnd=this.HigherWnd();
      this.m_wider_wnd=this.WiderWnd();
      //--- Если высота графика изменилась - корректируем расположение панели по вертикали
      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(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);
        }
     }

//--- Объявляем переменные для хранения текущего смещения курсора относительно начальных координат панели
   static int diff_x=0;
   static int diff_y=0;
   
//--- Получаем флаг удерживаемой кнопки мышки. Для визуального тестера правую кнопку тоже учитываем (sparam=="2")
   bool pressed=(!this.IsVisualMode() ? (sparam=="1" || sparam=="" ? true : false) : sparam=="1" || sparam=="2" ? true : false);
//--- Получаем координаты X и Y курсора. Для координаты Y учитываем смещение при работе в подокне графика
   int  mouse_x=(int)lparam;
   int  mouse_y=(int)dparam-(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOW_YDISTANCE,this.m_wnd);
//--- Получаем состояние курсора и кнопок мышки относительно панели
   ENUM_MOUSE_STATE state=this.MouseButtonState(mouse_x,mouse_y,pressed);
//--- Если курсор перемещается
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Если нажата кнопка внутри рабочей области панели
      if(state==MOUSE_STATE_PRESSED_INSIDE_WINDOW)
        {
         //--- Отключаем прокрутку графика, меню правой кнопки мышки и перекрестие
         this.SetChartsTool(false);
         //--- Перерисовываем область заголовка с цветом фона по умолчанию
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
         return;
        }
      //--- Если нажата кнопка внутри области заголовка панели
      else if(state==MOUSE_STATE_PRESSED_INSIDE_HEADER)
        {
         //--- Отключаем прокрутку графика, меню правой кнопки мышки и перекрестие
         this.SetChartsTool(false);
         //--- Перерисовываем область заголовка с новым цветом фона
         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();
           }
         //--- Смещаем панель вслед за курсором с учётом величины смещения курсора относительно начальных координат панели
         if(this.m_movable)
            this.Move(mouse_x-diff_x,mouse_y-diff_y);
         return;
        }
        
      //--- Если нажата кнопка закрытия
      else if(state==MOUSE_STATE_PRESSED_INSIDE_CLOSE)
        {
         //--- Отключаем прокрутку графика, меню правой кнопки и перекрестие
         this.SetChartsTool(false);
         //--- Перерисовываем кнопку закрытия с новым цветом фона
         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();
           }
         //--- Обработка нажатия кнопки закрытия должна определяться в программе.
         //--- Отправим в её обработчик OnChartEvent событие нажатия этой кнопки.
         //--- Идентификатор события 1001,
         //--- lparam=идентификатор панели (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");
        }
      //--- Если нажата кнопка сворачивания/разворачивания панели
      else if(state==MOUSE_STATE_PRESSED_INSIDE_MINIMIZE)
        {
         //--- Отключаем прокрутку графика, меню правой кнопки и перекрестие
         this.SetChartsTool(false);
         //--- "переворачиваем" флаг свёрнутости панели,
         this.m_minimized=!this.m_minimized;
         //--- перерисовываем панель с учётом нового состояния флага,
         this.Draw(this.m_title);
         //--- перерисовываем область заголовка панели
         this.RedrawHeaderArea();
         //--- Если панель закреплена и развёрнута - переместим её в запомненные координаты расположения
         if(this.m_minimized && !this.m_movable)
            this.Move(this.m_x_dock,this.m_y_dock);
         //--- Обновляем канвас с перерисовкой графика и
         this.m_canvas.Update();
         //--- записываем в глобальную переменную терминала состояние флага свёрнутости панели
         ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
        }
      //--- Если нажата кнопка закрепления панели
      else if(state==MOUSE_STATE_PRESSED_INSIDE_PIN)
        {
         //--- Отключаем прокрутку графика, меню правой кнопки и перекрестие
         this.SetChartsTool(false);
         //--- "переворачиваем" флаг свёрнутости панели,
         this.m_movable=!this.m_movable;
         //--- Перерисовываем кнопку закрепления с новым цветом фона
         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(this.m_minimized && !this.m_movable)
           {
            this.m_x_dock=this.m_x;
            this.m_y_dock=this.m_y;
           }
         //--- Обновляем канвас с перерисовкой графика и
         this.m_canvas.Update();
         //--- записываем в глобальную переменную терминала состояние флага перемещаемости панели
         ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
        }
        
      //--- Если курсор находится внутри области заголовка панели
      else if(state==MOUSE_STATE_INSIDE_HEADER)
        {
         //--- Отключаем прокрутку графика, меню правой кнопки и перекрестие
         this.SetChartsTool(false);
         //--- Перерисовываем область заголовка с новым цветом фона
         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();
           }
        }
        
      //--- Если курсор находится внутри кнопки закрытия
      else if(state==MOUSE_STATE_INSIDE_CLOSE)
        {
         //--- Отключаем прокрутку графика, меню правой кнопки и перекрестие
         this.SetChartsTool(false);
         //--- Перерисовываем область заголовка с минимальным изменением цвета фона
         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);
         //--- Перерисовываем кнопку сворачивания/разворачивания с цветом фона по умолчанию
         if(this.m_butt_min_back_color_c!=this.m_butt_min_back_color)
            this.RedrawButtonMinimize(this.m_butt_min_back_color);
         //--- Перерисовываем кнопку закрепления с цветом фона по умолчанию
         if(this.m_butt_pin_back_color_c!=this.m_butt_pin_back_color)
            this.RedrawButtonPin(this.m_butt_pin_back_color);
         //--- Перерисовываем кнопку закрытия с красным цветом фона
         if(this.m_butt_close_back_color_c!=clrRed)
           {
            this.RedrawButtonClose(clrRed);
            this.m_canvas.Update();
           }
        }
        
      //--- Если курсор находится внутри кнопки сворачивания/разворачивания
      else if(state==MOUSE_STATE_INSIDE_MINIMIZE)
        {
         //--- Отключаем прокрутку графика, меню правой кнопки и перекрестие
         this.SetChartsTool(false);
         //--- Перерисовываем область заголовка с минимальным изменением цвета фона
         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);
         //--- Перерисовываем кнопку закрытия с цветом фона по умолчанию
         if(this.m_butt_close_back_color_c!=this.m_butt_close_back_color)
            this.RedrawButtonClose(this.m_butt_close_back_color);
         //--- Перерисовываем кнопку закрепления с цветом фона по умолчанию
         if(this.m_butt_pin_back_color_c!=this.m_butt_pin_back_color)
            this.RedrawButtonPin(this.m_butt_pin_back_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();
           }
        }
        
      //--- Если курсор находится внутри кнопки закрепления
      else if(state==MOUSE_STATE_INSIDE_PIN)
        {
         //--- Отключаем прокрутку графика, меню правой кнопки и перекрестие
         this.SetChartsTool(false);
         //--- Перерисовываем область заголовка с минимальным изменением цвета фона
         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);
         //--- Перерисовываем кнопку закрытия с цветом фона по умолчанию
         if(this.m_butt_close_back_color_c!=this.m_butt_close_back_color)
            this.RedrawButtonClose(this.m_butt_close_back_color);
         //--- Перерисовываем кнопку сворачивания/разворачивания с цветом фона по умолчанию
         if(this.m_butt_min_back_color_c!=this.m_butt_min_back_color)
            this.RedrawButtonMinimize(this.m_butt_min_back_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();
           }
        }
        
      //--- Если курсор находится внутри рабочей области
      else if(state==MOUSE_STATE_INSIDE_WINDOW)
        {
         //--- Отключаем прокрутку графика, меню правой кнопки и перекрестие
         this.SetChartsTool(false);
         //--- Перерисовываем область заголовка с цветом фона по умолчанию
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
        }
      //--- Иначе (курсор за пределами панели, и нужно восстановить параметры графика)
      else
        {
         //--- Включаем прокрутку графика, меню правой кнопки и перекрестие
         this.SetChartsTool(true);
         //--- Перерисовываем область заголовка с цветом фона по умолчанию
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
        }
      //--- Записываем смещение курсора по X и Y относительно начальных координат панели
      diff_x=mouse_x-this.m_x;
      diff_y=mouse_y-this.m_y;
     }
  }

Логика обработчика событий достаточно подробно прокомментирована в коде. Отмечу некоторые моменты.

В самом начале прописана обработка события создания нового графического объекта:

//--- Если создан графический  объект
   if(id==CHARTEVENT_OBJECT_CREATE)
     {
      this.BringToTop();
      ::ObjectSetInteger(this.m_chart_id,sparam,OBJPROP_SELECTED,true);
     }

Для чего нужно, и что она делает? Если создать новый графический объект, то он будет размещён выше остальных графических объектов на графике и, соответственно, будет наложен сверху на панель. Поэтому при определении такого события панель переносится сразу же на передний план. А далее новый графический объект делается выделенным. Зачем? Если этого не сделать, то графические объекты, которым для построения нужны несколько точек, например, трендовая линия, не будут нормально созданы — все их опорные точки будут находиться в одной координате, а сам объект виден не будет. Происходит такое из-за потери управления графическим объектом при его создании, когда панель переносится на передний план. Поэтому новый графический объект необходимо принудительно выделить после переноса панели на передний план.
Таким образом, обоюдное поведение панели и графических объектов при их создании будет таким:

Рис.6 Новый графический объект строится "под" панелью и не теряет фокус при создании


В обработчике событий для каждого состояния предусмотрен свой блок обработки. Логика всех этих блоков идентична. Например, обработка нажатия и удержания кнопки мышки на заголовке панели:

      //--- Если нажата кнопка внутри области заголовка панели
      else if(state==MOUSE_STATE_PRESSED_INSIDE_HEADER)
        {
         //--- Отключаем прокрутку графика, меню правой кнопки мышки и перекрестие
         this.SetChartsTool(false);
         //--- Перерисовываем область заголовка с новым цветом фона
         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();
           }
         //--- Смещаем панель вслед за курсором с учётом величины смещения курсора относительно начальных координат панели
         if(this.m_movable)
            this.Move(mouse_x-diff_x,mouse_y-diff_y);
         return;
        }

Чтобы график не перемещался вместе с панелью, для него отключаются события прокрутки графика мышкой, меню правой кнопки мышки и перекрестие. Так как заголовок должен визуально реагировать на захват мышкой, то его цвет делается темнее. Прежде, чем поменять цвет на новый, необходимо проверить менялся ли он уже — зачем его постоянно менять на один и тот же, забирая ресурсы у процессора? А далее, если перемещение не запрещено (панель не закреплена), то смещаем её на новые координаты, рассчитанные по координатам мышки, минус смещение расположения курсора относительно верхнего левого угла панели. Если смещение не учитывать, то панель будет вставать точно на координаты курсора левым верхним углом.


Метод для перемещения панели на указанные координаты:

//+------------------------------------------------------------------+
//| Перемещает панель                                                |
//+------------------------------------------------------------------+
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 пиксель. По окончании всех проверок и корректировок координат панели устанавливаются новые координаты её расположения на графике.


Методы, устанавливающие координаты панели:

//+------------------------------------------------------------------+
//| Устанавливает координату X панели                                |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Устанавливает координату Y панели                                |
//+------------------------------------------------------------------+
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;
  }

Если в метод передана координата, равная координате панели, то и устанавливать её повторно не нужно — просто возвращается успешность выполнения метода. Сначала смещается канвас, а затем — рабочая область. Смещение рабочей области происходит с учётом её относительного расположения на канвасе — слева на один пиксель внутрь панели, и сверху — на высоту заголовка.


Методы, устанавливающие размеры панели:

//+------------------------------------------------------------------+
//| Устанавливает ширину панели                                      |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Устанавливает высоту панели                                      |
//+------------------------------------------------------------------+
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, то во избежание ошибки изменения размера, рабочая область просто скрывается без изменения её размеров.


Вспомогательные методы, устанавливающие сразу две координаты, сразу все размеры и координаты и размеры панели:

//+------------------------------------------------------------------+
//| Устанавливает координаты панели                                  |
//+------------------------------------------------------------------+
bool CDashboard::SetCoords(const int x,const int y)
  {
   bool res=true;
   res &=this.SetCoordX(x);
   res &=this.SetCoordY(y);
   return res;
  }
//+------------------------------------------------------------------+
//| Устанавливает размеры панели                                     |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Устанавливает координаты и размеры панели                        |
//+------------------------------------------------------------------+
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;
  }

В методы передаются параметры и флаг обновления. После успешной установки параметров и, если установлен флаг обновления, вызывается метод разворачивания панели, в котором производится перерисовка всех элементов панели.


Метод, рисующий область заголовка:

//+------------------------------------------------------------------+
//| Рисует область заголовка                                         |
//+------------------------------------------------------------------+
void CDashboard::DrawHeaderArea(const string title)
  {
//--- Если заголовок не используется - уходим
   if(!this.m_header)
      return;
//--- Устанавливаем текст заголовка
   this.m_title=title;
//--- Координата Y текста расположена по вертикали по центру области заголовка
   int y=this.m_header_h/2;
//--- Заполняем область цветом
   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));
//--- Выводим текст заголовка
   this.m_canvas.TextOut(2,y,this.m_title,::ColorToARGB(this.m_header_fore_color,this.m_header_alpha),TA_LEFT|TA_VCENTER);
//--- Запоминаем текущий цвет фона заголовка
   this.m_header_back_color_c=this.m_header_back_color;
//--- Рисуем управляющие элементы (кнопки закрытия, сворачивания/разворачивания и закрепления) и
   this.DrawButtonClose();
   this.DrawButtonMinimize();
   this.DrawButtonPin();
//--- обновляем канвас без перерисовки экрана
   this.m_canvas.Update(false);
  }

Метод рисует область заголовка — закрашивает прямоугольную область в координатах заголовка и рисует элементы управления — кнопки закрытия, сворачивания/разворачивания и закрепления. Используются цвета и прозрачность, установленные для области заголовка по умолчанию.


Метод, перерисовывающий область заголовка:

//+------------------------------------------------------------------+
//| Перерисовывает область заголовка                                 |
//+------------------------------------------------------------------+
void CDashboard::RedrawHeaderArea(const color new_color=clrNONE,const string title="",const color title_new_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Если заголовок не используется или все переданные параметры имеют значения по умолчанию - уходим
   if(!this.m_header || (new_color==clrNONE && title=="" && title_new_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- Если все переданные параметры равны уже установленным - уходим
   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(title!="")
      this.m_title=title;
//--- Определяем новые цвета фона и текста и прозрачность
   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);
//--- Координата Y текста расположена по вертикали по центру области заголовка
   int y=this.m_header_h/2;
//--- Заполняем область цветом
   this.m_canvas.FillRectangle(0,0,this.m_w-1,this.m_header_h-1,::ColorToARGB(back_clr,alpha));
//--- Выводим текст заголовка
   this.m_canvas.TextOut(2,y,this.m_title,::ColorToARGB(fore_clr,alpha),TA_LEFT|TA_VCENTER);
//--- Запоминаем текущий цвет фона заголовка, текста и прозрачность
   this.m_header_back_color_c=back_clr;
   this.m_header_fore_color_c=fore_clr;
   this.m_header_alpha_c=alpha;
//--- Рисуем управляющие элементы (кнопки закрытия, сворачивания/разворачивания и закрепления) и
   this.RedrawButtonClose(back_clr,clrNONE,alpha);
   this.RedrawButtonMinimize(back_clr,clrNONE,alpha);
   this.RedrawButtonPin(back_clr,clrNONE,alpha);
//--- обновляем канвас без перерисовки экрана
   this.m_canvas.Update(true);
  }

В метод передаются новый цвет фона, новый текст заголовка, новый цвет текста заголовка и новая прозрачность. Если переданы параметры, полностью идентичные уже установленным, то производится выход из метода. Служит для обновления цвета заголовка, его текста и прозрачности.


Метод, рисующий рамку панели:

//+------------------------------------------------------------------+
//| Рисует рамку панели                                              |
//+------------------------------------------------------------------+
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);
  }

Обводит рамкой канвас по периметру и запоминает установленный цвет как текущий.


Метод, рисующий кнопку закрытия панели:

//+------------------------------------------------------------------+
//| Рисует кнопку закрытия панели                                    |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonClose(void)
  {
//--- Если кнопка не используется - уходим
   if(!this.m_butt_close)
      return;
//--- Ширина кнопки равна высоте области заголовка
   int w=this.m_header_h;
//--- Координаты и размеры кнопки
   int x1=this.m_w-w;
   int x2=this.m_w-1;
   int y1=0;
   int y2=w-1;
//--- Смещение левого верхнего угла прямоугольной области рисунка от левого верхнего угла кнопки
   int shift=4;
//--- Рисуем фон кнопки
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_close_back_color,this.m_header_alpha));
//--- Рисуем "Крестик" закрытия
   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);
//--- Запоминаем текущий цвет фона и рисунка кнопки
   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;
//--- обновляем канвас без перерисовки экрана
   this.m_canvas.Update(false);
  }

В комментариях к коду вся логика расписана: рисуем фон, а поверх фона — изображение значка закрытия (крестик).


Метод, перерисовывающий кнопку закрытия панели:

//+------------------------------------------------------------------+
//| Перерисовывает кнопку закрытия панели                            |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonClose(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Если кнопка не используется или все переданные параметры имеют значения по умолчанию - уходим
   if(!this.m_butt_close || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- Ширина кнопки равна высоте области заголовка
   int w=this.m_header_h;
//--- Координаты и размеры кнопки
   int x1=this.m_w-w;
   int x2=this.m_w-1;
   int y1=0;
   int y2=w-1;
//--- Смещение левого верхнего угла прямоугольной области рисунка от левого верхнего угла кнопки
   int shift=4;
//--- Определяем новые цвета фона и текста и прозрачность
   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);
//--- Рисуем фон кнопки
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- Рисуем "Крестик" закрытия
   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);
//--- Запоминаем текущий цвет фона и рисунка кнопки
   this.m_butt_close_back_color_c=back_color;
   this.m_butt_close_fore_color_c=fore_color;
//--- обновляем канвас без перерисовки экрана
   this.m_canvas.Update(false);
  }

Для перерисовки необходимо чтобы хотя бы один из переданных параметров отличался от текущего. Остальное — идентично методу рисования кнопки, кроме выбора и установки новых параметров отрисовки.


Остальные методы рисования и перерисовки кнопок сворачивания/разворачивания и закрепления:

//+------------------------------------------------------------------+
//| Рисует кнопку сворачивания/разворачивания панели                 |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonMinimize(void)
  {
//--- Если кнопка не используется - уходим
   if(!this.m_butt_minimize)
      return;
//--- Ширина кнопки равна высоте области заголовка
   int w=this.m_header_h;
//--- Ширина кнопки закрытия равна нулю, если кнопка не используется
   int wc=(this.m_butt_close ? w : 0);
//--- Координаты и размеры кнопки
   int x1=this.m_w-wc-w;
   int x2=this.m_w-wc-1;
   int y1=0;
   int y2=w-1;
//--- Смещение левого верхнего угла прямоугольной области рисунка от левого верхнего угла кнопки
   int shift=4;
//--- Рисуем фон кнопки
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_min_back_color,this.m_header_alpha));
//--- Если панель свёрнута - рисуем прямоугольник
   if(this.m_minimized)
      this.m_canvas.Rectangle(x1+shift,y1+shift,x2-shift,y2-shift,::ColorToARGB(this.m_butt_min_fore_color,255));
//--- Иначе - панель развёрнута - рисуем отрезок линии
   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);
//--- Запоминаем текущий цвет фона и рисунка кнопки
   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;
//--- обновляем канвас без перерисовки экрана
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Перерисовывает кнопку сворачивания/разворачивания панели         |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonMinimize(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Если кнопка не используется или все переданные параметры имеют значения по умолчанию - уходим
   if(!this.m_butt_minimize || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- Ширина кнопки равна высоте области заголовка
   int w=this.m_header_h;
//--- Ширина кнопки закрытия равна нулю, если кнопка не используется
   int wc=(this.m_butt_close ? w : 0);
//--- Координаты и размеры кнопки
   int x1=this.m_w-wc-w;
   int x2=this.m_w-wc-1;
   int y1=0;
   int y2=w-1;
//--- Смещение левого верхнего угла прямоугольной области рисунка от левого верхнего угла кнопки
   int shift=4;
//--- Определяем новые цвета фона и текста и прозрачность
   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);
//--- Рисуем фон кнопки
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- Если панель свёрнута - рисуем прямоугольник
   if(this.m_minimized)
      this.m_canvas.Rectangle(x1+shift,y1+shift,x2-shift,y2-shift,::ColorToARGB(fore_color,255));
//--- Иначе - панель развёрнута - рисуем отрезок линии
   else
      this.m_canvas.LineThick(x1+shift,y2-shift,x2-shift,y2-shift,::ColorToARGB(fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Запоминаем текущий цвет фона и рисунка кнопки
   this.m_butt_min_back_color_c=back_color;
   this.m_butt_min_fore_color_c=fore_color;
//--- обновляем канвас без перерисовки экрана
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Рисует кнопку закрепления панели                                 |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonPin(void)
  {
//--- Если кнопка не используется - уходим
   if(!this.m_butt_pin)
      return;
//--- Ширина кнопки равна высоте области заголовка
   int w=this.m_header_h;
//--- Ширина кнопки закрытия и кнопки сворачивания равна нулю, если кнопка не используется
   int wc=(this.m_butt_close ? w : 0);
   int wm=(this.m_butt_minimize ? w : 0);
//--- Координаты и размеры кнопки
   int x1=this.m_w-wc-wm-w;
   int x2=this.m_w-wc-wm-1;
   int y1=0;
   int y2=w-1;
//--- Рисуем фон кнопки
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_pin_back_color,this.m_header_alpha));
//--- Координаты точек ломаной линии
   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};
//--- Рисуем фигурку "кнопки" 
   this.m_canvas.Polygon(x,y,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- Если флаг перемещаемости сброшен (закреплено) - перечёркиваем нарисованную кнопку
   if(!this.m_movable)
      this.m_canvas.Line(x1+3,y1+2,x1+15,y1+14,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- Запоминаем текущий цвет фона и рисунка кнопки
   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;
//--- обновляем канвас без перерисовки экрана
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Перерисовывает кнопку закрепления панели                         |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonPin(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Если кнопка не используется или все переданные параметры имеют значения по умолчанию - уходим
   if(!this.m_butt_pin || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- Ширина кнопки равна высоте области заголовка
   int w=this.m_header_h;
//--- Ширина кнопки закрытия и кнопки сворачивания равна нулю, если кнопка не используется
   int wc=(this.m_butt_close ? w : 0);
   int wm=(this.m_butt_minimize ? w : 0);
//--- Координаты и размеры кнопки
   int x1=this.m_w-wc-wm-w;
   int x2=this.m_w-wc-wm-1;
   int y1=0;
   int y2=w-1;
//--- Определяем новые цвета фона и текста и прозрачность
   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);
//--- Рисуем фон кнопки
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- Координаты точек ломаной линии
   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};
//--- Рисуем фигурку "кнопки" 
   this.m_canvas.Polygon(x,y,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- Если флаг перемещаемости сброшен (закреплено) - перечёркиваем нарисованную кнопку
   if(!this.m_movable)
      this.m_canvas.Line(x1+3,y1+2,x1+15,y1+14,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- Запоминаем текущий цвет фона и рисунка кнопки
   this.m_butt_pin_back_color_c=back_color;
   this.m_butt_pin_fore_color_c=fore_color;
//--- обновляем канвас без перерисовки экрана
   this.m_canvas.Update(false);
  }

Методы идентичны методам рисования и перерисовки кнопки закрытия. Логика точно такая же, и она прописана в комментариях к коду.


Метод, рисующий панель:

//+------------------------------------------------------------------+
//| Рисует панель                                                    |
//+------------------------------------------------------------------+
void CDashboard::Draw(const string title)
  {
//--- Устанавливаем текст заголовка
   this.m_title=title;
//--- Если флаг свёрнутости не установлен - разворачиваем панель
   if(!this.m_minimized)
      this.Expand();
//--- Иначе - сворачиваем панель
   else
      this.Collapse();
//--- Обновляем канвас без перерисовки чарта
   this.m_canvas.Update(false);
//--- Обновляем рабочую область с перерисовкой графика
   this.m_workspace.Update();
  }

Если флаг свёрнутости не установлен — разворачиваем панель, т.е., рисуем её в развёрнутом виде. Если флаг свёрнутости установлен, то сворачиваем панель: рисуем панель в свёрнутом виде — только заголовок.


Метод, сворачивающий панель:

//+------------------------------------------------------------------+
//| Сворачивает панель                                               |
//+------------------------------------------------------------------+
void CDashboard::Collapse(void)
  {
//--- Сохраняем в массивы пиксели рабочей области и фона панели
   this.SaveWorkspace();
   this.SaveBackground();
//--- Запоминаем текущую высоту панели
   int h=this.m_h;
//--- Изменяем размеры (высоту) канваса и рабочей области
   if(!this.SetSizes(this.m_canvas.Width(),this.m_header_h))
      return;
//--- Рисуем область заголовка
   this.DrawHeaderArea(this.m_title);
//--- Возвращаем в переменную запомненную высоту панели
   this.m_h=h;
  }

Перед тем, как свернуть панель из развёрнутого состояния, нужно сохранить все пиксели фона и рабочей области в массивы. Это нужно для быстрого разворачивания панели — чтобы не рисовать её заново, а просто восстановить изображения панели и рабочей области из массивов пикселей. Кроме того, на фоне панели что-либо могло быть нарисовано дополнительно в качестве оформления — в данном случае это может быть табличка. Она тоже сохранится вместе с фоном, а далее — восстановится.


Метод, разворачивающий панель:

//+------------------------------------------------------------------+
//| Разворачивает панель                                             |
//+------------------------------------------------------------------+
void CDashboard::Expand(void)
  {
//--- Изменяем размеры панели
   if(!this.SetSizes(this.m_canvas.Width(),this.m_h))
      return;
//--- Если ещё ни разу пиксели фона панели не сохранялись в массив
   if(this.m_array_ppx.Size()==0)
     {
      //--- Рисуем панель и
      this.m_canvas.Erase(::ColorToARGB(this.m_back_color,this.m_alpha));
      this.DrawFrame();
      this.DrawHeaderArea(this.m_title);
      //--- сохраняем пиксели фона панели и рабочей области в массивы
      this.SaveWorkspace();
      this.SaveBackground();
     }
//--- Если пиксели фона панели и рабочей области сохранялись ранее,
   else
     {
      //--- восстанавливаем пиксели фона панели и рабочей области из массивов
      this.RestoreBackground();
      if(this.m_array_wpx.Size()>0)
         this.RestoreWorkspace();
     }
//--- Если после разворачивания панель выходит за пределы окна графика - корректируем расположение панели
   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());
  }

Если массивы, в которые сохраняются пиксели фона и рабочей области, пустые, то рисуем всю панель при помощи методов рисования. Если же массивы уже заполнены, то просто восстанавливаем фон панели и её рабочую область из массивов.


Вспомогательные методы для работы с цветом:

//+------------------------------------------------------------------+
//| Возвращает цвет с новой цветовой составляющей                    |
//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+
//| Преобразует RGB в 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;
  }
//+------------------------------------------------------------------+
//| Получение значений компонентов RGB                               |
//+------------------------------------------------------------------+
void CDashboard::ColorToRGB(const color clr,double &r,double &g,double &b)
  {
   r=GetR(clr);
   g=GetG(clr);
   b=GetB(clr);
  }

Методы необходимы для изменения цвета при взаимодействии курсора с элементами управления на панели.


Метод, устанавливающий прозрачность заголовка:

//+------------------------------------------------------------------+
//| Устанавливает прозрачность заголовка                             |
//+------------------------------------------------------------------+
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;
  }

Сначала переданное в метод значение прозрачности записывается в переменную, хранящую прозрачность по умолчанию, затем новое значение сравнивается с текущим. Если значения не равны, то область заголовка полностью перерисовывается. В конце в текущую прозрачность записывается установленная.


Метод, устанавливающий прозрачность панели:

//+------------------------------------------------------------------+
//| Устанавливает прозрачность панели                                |
//+------------------------------------------------------------------+
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;
  }

Логика аналогична вышерассмотренному методу. Если переданная в метод прозрачность не равна текущей, то панель полностью перерисовывается с новой прозрачностью.


Метод, устанавливающий параметры шрифта рабочей области по умолчанию:

//+------------------------------------------------------------------+
//| Устанавливает параметры шрифта рабочей области по умолчанию      |
//+------------------------------------------------------------------+
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. Если имя шрифта name указано с расширением, то шрифт загружается из файла, при этом – если путь начинается с "\" или "/", то файл ищется относительно каталога MQL5, иначе ищется относительно пути EX5-файла, вызвавшего функцию TextSetFont().

Размер шрифта задается положительными или отрицательными значениями, знак определяет зависимость размера текста от настроек операционной системы (масштаба шрифта).

  • Если размер задается положительным числом, то при отображении логического шрифта в физический происходит преобразование размера в физические единицы измерения устройства (пиксели) и этот размер соответствует высоте ячеек символов из доступных шрифтов. Не рекомендуется в тех случаях, когда предполагается совместное использование на графике текстов, выведенных функцией TextOut(), и текстов, отображаемых с помощью графического объекта OBJ_LABEL ("Текстовая метка").
  • Если размер задается отрицательным числом, то указанный размер предполагается заданным в десятых долях логического пункта (значение -350 равно 35 логических пунктов) и делится на 10, а затем полученное значение преобразуется в физические единицы измерения устройства (пиксели) и соответствует абсолютному значению высоты символа из доступных шрифтов. Чтобы получить на экране текст такого же размера, как и в объекте OBJ_LABEL, возьмите указанный в свойствах объекта размер шрифта и умножьте на -10.


Метод, включающий/выключающий режимы работы с графиком:

//+------------------------------------------------------------------+
//| Включает/выключает режимы работы с графиком                      |
//+------------------------------------------------------------------+
void CDashboard::SetChartsTool(const bool flag)
  {
//--- Если передан флаг true и если прокрутка графика отключена
   if(flag && !::ChartGetInteger(this.m_chart_id,CHART_MOUSE_SCROLL))
     {
      //--- включаем прокрутку графика, меню правой кнопки мышки и перекрестие
      ::ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
      ::ChartSetInteger(0,CHART_CONTEXT_MENU,true);
      ::ChartSetInteger(0,CHART_CROSSHAIR_TOOL,true);
     }
//--- иначе, если передан флаг false и если прокрутка графика включена
   else if(!flag && ::ChartGetInteger(this.m_chart_id,CHART_MOUSE_SCROLL))
     {
      //--- отключаем прокрутку графика, меню правой кнопки мышки и перекрестие
      ::ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
      ::ChartSetInteger(0,CHART_CONTEXT_MENU,false);
      ::ChartSetInteger(0,CHART_CROSSHAIR_TOOL,false);
     }
  }

В зависимости от переданного флага, метод проверяет состояние прокрутки графика мышкой и либо включает все режимы работы с графиком, либо их отключает. Проверка режима прокрутки нужна для того, чтобы постоянно не отсылать команду на установку режимов при нахождении курсора в пределах или за пределами окна панели. Переключение режимов происходит только в момент захода курсора внутрь панели, либо при выходе курсора за её пределы.


Метод, выводящий текстовое сообщение в указанные координаты:

//+------------------------------------------------------------------+
//| Выводит текстовое сообщение в указанные координаты               |
//+------------------------------------------------------------------+
void CDashboard::DrawText(const string text,const int x,const int y,const int width=WRONG_VALUE,const int height=WRONG_VALUE)
  {
//--- Объявим переменные для записи в них ширины и высоты текста
   int w=width;
   int h=height;
//--- Если ширина и высота текста, переданные в метод, имеют нулевые значения,
//--- то полностью очищается всё паространство рабочей области прозрачным цветом
   if(width==0 && height==0)
      this.m_workspace.Erase(0x00FFFFFF);
//--- Иначе
   else
     {
      //--- Если переданные ширина и высота имеют значения по умолчанию (-1) - получаем из текста его ширину и высоту
      if(width==WRONG_VALUE && height==WRONG_VALUE)
         this.m_workspace.TextSize(text,w,h);
      //--- иначе,
      else
        {
         //--- если ширина, переданная в метод, имеет значение по умолчанию (-1) - получаем ширину из текста, либо
         //--- если ширина, переданная в метод, имеет значение больше нуля - используем переданную в метод ширину, либо
         //--- если ширина, переданная в метод, имеет нулевое значение, используем значение 1 для ширины
         w=(width ==WRONG_VALUE ? this.m_workspace.TextWidth(text)  : width>0  ? width  : 1);
         //--- если высота, переданная в метод, имеет значение по умолчанию (-1) - получаем высоту из текста, либо
         //--- если высота, переданная в метод, имеет значение больше нуля - используем переданную в метод высоту, либо
         //--- если высота, переданная в метод, имеет нулевое значение, используем значение 1 для высоты
         h=(height==WRONG_VALUE ? this.m_workspace.TextHeight(text) : height>0 ? height : 1);
        }
      //--- Заполняем пространство по указанным координатам и полученной шириной и высотой прозрачным цветом (стираем прошлую запись)
      this.m_workspace.FillRectangle(x,y,x+w,y+h,0x00FFFFFF);
     }
//--- Выводим текст на очищенное от прошлого текста места и обновляем рабочую область без перерисовки экрана
   this.m_workspace.TextOut(x,y,text,::ColorToARGB(this.m_fore_color));
   this.m_workspace.Update(false);
  }

Работа с канвасом в случае вывода на него графики заключается в рисовании этй графики на канвасе как кистью на холсте. Одно изображение, нарисованное на холсте, ложится на другое изображение, нарисованное первым. Чтобы заменить изображение, необходимо либо полностью перерисовать весь холст, либо вычислить размеры прошлого изображения, стереть эту область и нарисовать очередное изображение на стёртой области.

Для текстов мы можем получить размеры уже нарисованного текста на холсте, чтобы стереть эту область перед выводом следующего текста. Но хранить где-то в объекте все ранее нарисованные тексты и фигуры не оптимально. Поэтому здесь сделан выбор между точностью и простотой с оптимальностью. Здесь мы будем получать размеры текущего текста (которые по ширине могут не соответствовать ранее нарисованному в этом месте тексту), и эти размеры использовать, чтобы стереть ранее нарисованный текст перед выводом следующего.

Если прошлый текст по ширине не совпадает с текущим, а вернее — больше, то он будет стёрт не весь. Для таких случаев в методе предусмотрена передача параметров с указанием нужных размеров стираемой области по высоте и ширине:

  1. Если переданные в метод значения ширины и высоты равны -1 (по умолчанию), значит стираться будет область равная ширине и высоте текущего текста,
  2. Если переданы нули — полностью стирается вся рабочая область,
  3. Если передано значение ширины или высоты больше нуля, то эти значения и будут использованы для ширины и высоты соответственно.
Обычно, если выводится текст, то его высота будет равна предыдущему в случае использования того же шрифта с тем же размером. А вот ширина может отличаться. Поэтому можно ширину подобрать и передать в метод такую, которая сотрёт только область, предназначенную для текста, не затронув соседние, и при этом, чтобы гарантированно стереть прошлый текст.


Класс панели рассчитан на вывод данных в табличном формате. Для удобства рассчёта координат выводимых на панель данных и визуального оформления панели, предусмотрены два метода, рассчитывающие координаты ячеек таблиц и рисующих таблички (если нужно) на фоне панели.

  • Первый метод рассчитывает координаты ячеек таблицы по переданным в него данным: начальные координаты X и Y таблицы на панели, количество строк, столбцов, высота строк и ширина столбцов. Также можно указать свой цвет линий сетки таблицы и признак "чередующихся" строк.
  • Второй метод рассчитывает размеры строк и столбцов автоматически в зависимости от их количества и ширины отступа таблицы от краёв панели. В нём также можно указать свой цвет линий сетки таблицы и признак "чередующихся" строк.


Метод, рисующий фоновую сетку по заданным параметрам:

//+------------------------------------------------------------------+
//| Рисует фоновую сетку                                             |
//+------------------------------------------------------------------+
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(this.m_minimized)
      return;
//--- Очищаем все списки объекта табличных данных (удаляем ячейки из строк и все строки)
   this.m_table_data.Clear();
//--- Высота строки не может быть меньше 2
   int row_h=int(row_size<2 ? 2 : row_size);
//--- Ширина столбца не может быть меньше 2
   int col_w=int(col_size<2 ? 2 : col_size);
   
//--- Координата X1 (слева) таблицы не может быть меньше 1 (чтобы оставить один пиксель по периметру панели для рамки)
   int x1=int(x<1 ? 1 : x);
//--- Рассчитываем координату X2 (справа) в зависимости от количества столбцов и их ширины
   int x2=x1+col_w*int(columns>0 ? columns : 1);
//--- Координата Y1 находится под областью заголовка панели
   int y1=this.m_header_h+(int)y;
//--- Рассчитываем координату Y2 (снизу) в зависимости от количества строк и их высоты
   int y2=y1+row_h*int(rows>0 ? rows : 1);
   
//--- Получаем цвет линий сетки таблицы, либо по умолчанию, либо переданный в метод
   color clr=(line_color==clrNONE ? C'200,200,200' : line_color);
//--- Если начальная координата X больше 1 - рисуем рамку таблицы
//--- (при координате 1 рамкой таблицы выступает рамка панели)
   if(x1>1)
      this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));
//--- В цикле во строкам таблицы
   for(int i=0;i<(int)rows;i++)
     {
      //--- рассчитываем координату Y очередной горизонтальной линии сетки (координата Y очередной строки таблицы)
      int row_y=y1+row_h*i;
      //--- если передан флаг "чередующихся" цветов строк и строка чётная
      if(alternating_color && i%2==0)
        {
         //--- осветляем цвет фона таблицы и рисуем фоновый прямоугольник
         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));
        }
      //--- Рисуем горизонтальную линию сетки таблицы
      this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
      
      //--- Создаём новый объект строки таблицы
      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;
        }
      //--- Добавляем его в список строк объекта табличных данных
      //--- (если добавить объект не удалось - удаляем созданный объект)
      if(!this.m_table_data.AddRow(row_obj))
         delete row_obj;
      //--- Устанавливаем в созданном объекте-строке его координату Y с учётом смещения от заголовка панели
      row_obj.SetY(row_y-this.m_header_h);
     }
//--- В цикле по столбцам таблицы
   for(int i=0;i<(int)columns;i++)
     {
      //--- рассчитываем координату X очередной вертикальной линии сетки (координата X очередного столбца таблицы)
      int col_x=x1+col_w*i;
      //--- Если линия сетки вышла за пределы панели - прерываем цикл
      if(x1==1 && col_x>=x1+m_canvas.Width()-2)
         break;
      //--- Рисуем вертикальную линию сетки таблицы
      this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));
      
      //--- Получаем из объекта табличных данных количество созданных строк
      int total=this.m_table_data.RowsTotal();
      //--- В цикле по строкам таблицы
      for(int j=0;j<total;j++)
        {
         //--- получаем очередную строку
         CTableRow *row=m_table_data.GetRow(j);
         if(row==NULL)
            continue;
         //--- Создаём новую ячейку таблицы
         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;
           }
         //--- Добавляем созданную ячейку в строку
         //--- (если добавить объект не удалось - удаляем созданный объект)
         if(!row.AddCell(cell))
           {
            delete cell;
            continue;
           }
         //--- Устанавливаем в созданном объекте-ячейке его координату X и координату Y из объекта-строки
         cell.SetXY(col_x,row.Y());
        }
     }
//--- Обновляем канвас без перерисовки графика
   this.m_canvas.Update(false);
  }

Логика метода и последовательность рисования таблицы и создания её экземпляра в объекте табличных данных подробно расписаны прямо в коде практически под каждой строкой. Те данные, которые записываются в экземпляр нарисованной таблицы в объекте табличных данных, нужны будут для получения координат каждой ячейки — чтобы удобно было при выводе данных на панель указать нужные координаты. Достаточно лишь указать номер ячейки по её расположению в таблице (Row и Column) и получить координаты верхнего левого угла этой ячейки на панели.


Метод, рисующий фоновую сетку с автоматическим размером ячеек:

//+------------------------------------------------------------------+
//| Рисует фоновую сетку с автоматическим размером ячеек             |
//+------------------------------------------------------------------+
void CDashboard::DrawGridAutoFill(const uint border,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true)
  {
//--- Если панель свёрнута - уходим
   if(this.m_minimized)
      return;
//--- Координата X1 (левая) таблицы
   int x1=(int)border;
//--- Координата X2 (правая) таблицы
   int x2=this.m_canvas.Width()-(int)border-1;
//--- Координата Y1 (верхняя) таблицы
   int y1=this.m_header_h+(int)border;
//--- Координата Y2 (нижняя) таблицы
   int y2=this.m_canvas.Height()-(int)border-1;

//--- Получаем цвет линий сетки таблицы, либо по умолчанию, либо переданный в метод
   color clr=(line_color==clrNONE ? C'200,200,200' : line_color);
//--- Если отступ от края панели больше нуля - рисуем рамку таблицы
//--- иначе - рамкой таблицы выступает рамка панели
   if(border>0)
      this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));

//--- Высота всей сетки таблицы
   int greed_h=y2-y1;
//--- Рассчитываем высоту строки в зависимости от высоты таблицы и количества строк
   int row_h=(int)::round((double)greed_h/(double)rows);
//--- В цикле по количеству строк
   for(int i=0;i<(int)rows;i++)
     {
      //--- рассчитываем координату Y очередной горизонтальной линии сетки (координата Y очередной строки таблицы)
      int row_y=y1+row_h*i;
      //--- если передан флаг "чередующихся" цветов строк и строка чётная
      if(alternating_color && i%2==0)
        {
         //--- осветляем цвет фона таблицы и рисуем фоновый прямоугольник
         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));
        }
      //--- Рисуем горизонтальную линию сетки таблицы
      this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
      
      //--- Создаём новый объект строки таблицы
      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;
        }
      //--- Добавляем его в список строк объекта табличных данных
      //--- (если добавить объект не удалось - удаляем созданный объект)
      if(!this.m_table_data.AddRow(row_obj))
         delete row_obj;
      //--- Устанавливаем в созданном объекте-строке его координату Y с учётом смещения от заголовка панели
      row_obj.SetY(row_y-this.m_header_h);
     }
     
//--- Ширина сетки таблицы
   int greed_w=x2-x1;
//--- Рассчитываем ширину столбца в зависимости от ширины таблицы и количества столбцов
   int col_w=(int)::round((double)greed_w/(double)columns);
//--- В цикле по столбцам таблицы
   for(int i=0;i<(int)columns;i++)
     {
      //--- рассчитываем координату X очередной вертикальной линии сетки (координата X очередного столбца таблицы)
      int col_x=x1+col_w*i;
      //--- Если это не самая первая вертикальная линия - рисуем её
      //--- (первой вертикальной линией выступает либо рамка таблицы, либо рамка панели)
      if(i>0)
         this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));
      
      //--- Получаем из объекта табличных данных количество созданных строк
      int total=this.m_table_data.RowsTotal();
      //--- В цикле по строкам таблицы
      for(int j=0;j<total;j++)
        {
         //--- получаем очередную строку
         CTableRow *row=this.m_table_data.GetRow(j);
         if(row==NULL)
            continue;
         //--- Создаём новую ячейку таблицы
         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;
           }
         //--- Добавляем созданную ячейку в строку
         //--- (если добавить объект не удалось - удаляем созданный объект)
         if(!row.AddCell(cell))
           {
            delete cell;
            continue;
           }
         //--- Устанавливаем в созданном объекте-ячейке его координату X и координату Y из объекта-строки
         cell.SetXY(col_x,row.Y());
        }
     }
//--- Обновляем канвас без перерисовки графика
   this.m_canvas.Update(false);
  }

Данный метод отличается от вышерассмотренного лишь автоматическим рассчётом ширины и высоты таблицы в зависимости от отступа таблицы от края панели, высоты строк (высота таблицы / количество строк) и ширины столбцов (ширина таблицы / количество столбцов). Таже полностью прокомментирован прямо в коде.

Создавать (рисовать на панели) эти таблички нужно уже после создания панели и её отрисовки на графике. В противном случае, табличка нарисована не будет, но табличные данные при этом будут все рассчитаны, и ими можно будет пользоваться. Т.е. рисование таблицы после отображения панели на графике необходимо лишь в случаях, когда табличка должна быть нарисована на панели.


Для быстрого восстановления фона панели и рабочей области предусмотрены массивы, в которые перед, например, сворачиванием панели копируются пиксели изображения рабочей области и панели. При разворачивании панели вместо повторного рисования всего ранее нарисованного на ней, просто восстанавливаются фон панели и рабочей области из массивов пикселей. Это гораздо практичнее, чем где-то запоминать всё, что, и с какими параметрами рисовалось на канвасе, чтобы потом заново перерисовывать.

Есть по два метода для панели и рабочей области — для сохранения изображения в массив пикселей и для восстановления изображения из массива пикселей.


Метод, сохраняющий рабочую область в массив пикселей:

//+------------------------------------------------------------------+
//| Сохраняет рабочую область в массив пикселей                      |
//+------------------------------------------------------------------+
void CDashboard::SaveWorkspace(void)
  {
//--- Рассчитываем необходимый размер массива (ширина * высота рабочей области)
   uint size=this.m_workspace.Width()*this.m_workspace.Height();
//--- Если размер массива не равен рассчитанному - изменяем его
   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;
//--- В цикле по высоте рабочей области (координата Y пикселя)
   for(int y=0;y<this.m_workspace.Height();y++)
      //--- в цикле по ширине рабочей области (координата X пикселя)
      for(int x=0;x<this.m_workspace.Width();x++)
        {
         //--- рассчитываем индекс пикселя в приёмном массиве
         n=this.m_workspace.Width()*y+x;
         if(n>this.m_array_wpx.Size()-1)
            break;
         //--- копируем пиксель в приёмный массив из X и Y рабочей области
         this.m_array_wpx[n]=this.m_workspace.PixelGet(x,y);
        }
  }

В двух вложенных циклах проходим по каждому пикселю каждой строчки изображения и копируем их в приёмный массив.


Метод, восстанавливающий рабочую область из массива пикселей:

//+------------------------------------------------------------------+
//| Восстанавливает рабочую область из массива пикселей              |
//+------------------------------------------------------------------+
void CDashboard::RestoreWorkspace(void)
  {
//--- Если массив пустой - уходим
   if(this.m_array_wpx.Size()==0)
      return;
   uint n=0;
//--- В цикле по высоте рабочей области (координата Y пикселя)
   for(int y=0;y<this.m_workspace.Height();y++)
      //--- в цикле по ширине рабочей области (координата X пикселя)
      for(int x=0;x<this.m_workspace.Width();x++)
        {
         //--- рассчитываем индекс пикселя в массиве
         n=this.m_workspace.Width()*y+x;
         if(n>this.m_array_wpx.Size()-1)
            break;
         //--- копируем пиксель из массива в координаты X и Y рабочей области
         this.m_workspace.PixelSet(x,y,this.m_array_wpx[n]);
        }
  }

В двух вложенных циклах рассчитываем индекс каждого пикселя каждой строчки изображения в массиве и копируем их из массива в координаты X и Y изображения.


Метод, сохраняющий фон панели в массив пикселей:

//+------------------------------------------------------------------+
//| Сохраняет фон панели в массив пикселей                           |
//+------------------------------------------------------------------+
void CDashboard::SaveBackground(void)
  {
//--- Рассчитываем необходимый размер массива (ширина * высота панели)
   uint size=this.m_canvas.Width()*this.m_canvas.Height();
//--- Если размер массива не равен рассчитанному - изменяем его
   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;
//--- В цикле по высоте панели (координата Y пикселя)
   for(int y=0;y<this.m_canvas.Height();y++)
      //--- в цикле по ширине панели (координата X пикселя)
      for(int x=0;x<this.m_canvas.Width();x++)
        {
         //--- рассчитываем индекс пикселя в приёмном массиве
         n=this.m_canvas.Width()*y+x;
         if(n>this.m_array_ppx.Size()-1)
            break;
         //--- копируем пиксель в приёмный массив из X и Y панели
         this.m_array_ppx[n]=this.m_canvas.PixelGet(x,y);
        }
  }


Метод, восстанавливающий фон панели из массива пикселей:

//+------------------------------------------------------------------+
//| Восстанавливает фон панели из массива пикселей                   |
//+------------------------------------------------------------------+
void CDashboard::RestoreBackground(void)
  {
//--- Если массив пустой - уходим
   if(this.m_array_ppx.Size()==0)
      return;
   uint n=0;
//--- В цикле по высоте панели (координата Y пикселя)
   for(int y=0;y<this.m_canvas.Height();y++)
      //--- в цикле по ширине панели (координата X пикселя)
      for(int x=0;x<this.m_canvas.Width();x++)
        {
         //--- рассчитываем индекс пикселя в массиве
         n=this.m_canvas.Width()*y+x;
         if(n>this.m_array_ppx.Size()-1)
            break;
         //--- копируем пиксель из массива в координаты X и Y панели
         this.m_canvas.PixelSet(x,y,this.m_array_ppx[n]);
        }
  }


Чтобы перенести объект на передний план, необходимо выполнить подряд две операции: скрыть объект и сразу отобразить его. У каждого графического объекта есть свойство OBJPROP_TIMEFRAMES, отвечающее за его видимость на каждом из таймфреймов. Чтобы скрыть объект на всех таймфреймах, нужно для этого свойства установить значение OBJ_NO_PERIODS. Соответственно, чтобы отобразить объект, нужно установить свойству OBJPROP_TIMEFRAMES значение OBJ_ALL_PERIODS.


Метод, скрывающий панель:

//+------------------------------------------------------------------+
//| Скрывает панель                                                  |
//+------------------------------------------------------------------+
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, и для немедленного отображения изменений график перерисовывается (если соответствующий флаг установлен).


Метод, отображающий панель:

//+------------------------------------------------------------------+
//| Показывает панель                                                |
//+------------------------------------------------------------------+
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. А для объекта рабочей области значение устанавливается только, если панель находится в развёрнутом состоянии.

При взведённом флаге перерисовки график перерисовывается для немедленного отображения изменений.


Метод, переносящий панель на передний план:

//+------------------------------------------------------------------+
//| Переносит панель на передний план                                |
//+------------------------------------------------------------------+
void CDashboard::BringToTop(void)
  {
   this.Hide(false);
   this.Show(true);
  }

Сначала панель и рабочая область скрываются без перерисовки графика, затем сразу же отображаются и график перерисовывается.

Для некоторых случаев, когда массивы пикселей нет возможности держать в памяти, их нужно сохранять в файлы и затем от туда загружать. В классе подготовлены методы для сохранения массивов пикселей в файл и загрузки их из файла:

//+------------------------------------------------------------------+
//| Сохраняет массив пикселей рабочей области в файл                 |
//+------------------------------------------------------------------+
bool CDashboard::FileSaveWorkspace(void)
  {
//--- Определяем папку и имя файла
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\workspace.bin";
//--- Если сохраняемый массив пустой - сообщаем об этом и возвращаем false
   if(this.m_array_wpx.Size()==0)
     {
      ::PrintFormat("%s: Error. The workspace pixel array is empty.",__FUNCTION__);
      return false;
     }
//--- Если массив не удалось сохранить в файл - сообщаем об этом и возвращаем false
   if(!::FileSave(filename,this.m_array_wpx))
     {
      ::PrintFormat("%s: FileSave '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Успешно, возвращаем true
   return true;
  }
//+------------------------------------------------------------------+
//| Сохраняет массив пикселей фона панели в файл                     |
//+------------------------------------------------------------------+
bool CDashboard::FileSaveBackground(void)
  {
//--- Определяем папку и имя файла
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\background.bin";
//--- Если сохраняемый массив пустой - сообщаем об этом и возвращаем false
   if(this.m_array_ppx.Size()==0)
     {
      ::PrintFormat("%s: Error. The background pixel array is empty.",__FUNCTION__);
      return false;
     }
//--- Если массив не удалось сохранить в файл - сообщаем об этом и возвращаем false
   if(!::FileSave(filename,this.m_array_ppx))
     {
      ::PrintFormat("%s: FileSave '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Успешно, возвращаем true
   return true;
  }
//+------------------------------------------------------------------+
//| Загружает массив пикселей рабочей области из файла               |
//+------------------------------------------------------------------+
bool CDashboard::FileLoadWorkspace(void)
  {
//--- Определяем папку и имя файла
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\workspace.bin";
//--- Если не удалось загрузить данные из файла в массив, сообщаем об этом и возвращаем false
   if(::FileLoad(filename,this.m_array_wpx)==WRONG_VALUE)
     {
      ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Успешно, возвращаем true
   return true;
  }
//+------------------------------------------------------------------+
//| Загружает массив пикселей фона панели из файла                   |
//+------------------------------------------------------------------+
bool CDashboard::FileLoadBackground(void)
  {
//--- Определяем папку и имя файла
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\background.bin";
//--- Если не удалось загрузить данные из файла в массив, сообщаем об этом и возвращаем false
   if(::FileLoad(filename,this.m_array_ppx)==WRONG_VALUE)
     {
      ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Успешно, возвращаем true
   return true;
  }

На данный момент работа с этими методами не реализована, так как необходимость в них была обнаружена уже в момент написания статьи. В последующих статьях, где будет использоваться класс информационной панели, эти методы, скорее всего, уже будут использоваться.


Индикатор с информационной панелью

Для проверки работы информационной панели создадим простой индикатор, рисующий обычную скользящую среднюю. На панель будем выводить текущие Bid и Ask и данные свечи, над которой находится курсор мышки.

В папке 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 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   */ // Период расчёта скользящей средней
input ENUM_MA_METHOD       InpMethodMA =  MODE_SMA;      /* MA Method   */ // Метод расчёта скользящей средней
input ENUM_APPLIED_PRICE   InpPriceMA  =  PRICE_CLOSE;   /* MA Price    */ // Цена расчёта скользящей средней
input int                  InpPanelX   =  20;            /* Dashboard X */ // Координата X панели
input int                  InpPanelY   =  20;            /* Dashboard Y */ // Координата Y панели
input int                  InpUniqID   =  0;             /* Unique ID   */ // Уникальный идентификатор для объекта-панели
//--- indicator buffers
double         BufferMA[];
//--- global variables
CDashboard    *dashboard=NULL;
int            handle_ma;        // Хэндл индикатора Moving Average
int            period_ma;        // Период расчёта  Moving Average
int            mouse_bar_index;  // Индекс бара, с которого берутся данные
string         plot_label;       // Имя индикаторной графической серии для отображения в окне DataWindow
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {

В обработчике OnInit() создадим хэндл стандартного индикатора Moving Average, установим параметры индикатора и рисуемого буфера. Так как просчёт индикатора выполняется от начала истории к текущим данным, то установим буферу индикатора индексацию как в таймсерии. Объект информационной панели тоже создадим в этом же обработчике. Сразу же после создания объекта отобразим его и нарисуем сетку таблицы. После выведем в журнал табличные данные:

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

//--- Создаём хэндл индикатора
   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;
     }
//--- Устанавливаем параметры индикатора
   IndicatorSetInteger(INDICATOR_DIGITS,Digits());
   IndicatorSetString(INDICATOR_SHORTNAME,"Test Dashboard");
//--- Устанавливаем параметры рисуемого буфера
   plot_label="MA("+(string)period_ma+","+StringSubstr(EnumToString(Period()),7)+")";
   PlotIndexSetString(0,PLOT_LABEL,plot_label);
   ArraySetAsSeries(BufferMA,true);

//--- Создаём объект панели
   dashboard=new CDashboard(InpUniqID,InpPanelX,InpPanelY,200,250);
   if(dashboard==NULL)
     {
      Print("Error. Failed to create dashboard object");
      return INIT_FAILED;
     }
//--- Отображаем панель с текстом в заголовке "Символ, Описание таймфрейма"
   dashboard.View(Symbol()+", "+StringSubstr(EnumToString(Period()),7));
//--- Рисуем табличку на фоне панели
   dashboard.DrawGridAutoFill(2,12,2);
   //dashboard.DrawGrid(2,1,12,2,19,97);
//--- Выводим в журнал табличные данные
   dashboard.GridPrint(2);
//--- Инициализируем переменную с индексом бара указателя мышки
   mouse_bar_index=0;
//--- Успешная инициализация
   return(INIT_SUCCEEDED);
  }

Вся логика здесь прокомментирована в коде. Таблица создаётся с автоматическим расчётом размеров строк и столбцов. Создание простой таблицы закомментировано в коде. Можно поменять местами — закомментировать автоматическую табличку и раскомментировать простую и перекомпилировать индикатор. Разница будет совсем небольшой при данных параметрах таблиц.

В обработчике OnDeinit() удалим панель, освободим хэндл индикатора и сотрём комментарии на графике:

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Если объект панели существует - удаляем
   if(dashboard!=NULL)
      delete dashboard;
//--- Освобождаем хэндл индикатора МА
   ResetLastError();
   if(!IndicatorRelease(handle_ma))
      PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError());
//--- Стираем все комментарии
   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[])
  {
//--- Устанавливаем массивам индексацию как в таймсерии
   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);

//--- Проверка на минимальное количество баров для расчёта
   if(rates_total<period_ma) return 0;

//--- Проверка и расчёт количества просчитываемых баров
   int limit=rates_total-prev_calculated;
//--- Если limit равен 0, то просчитывается только текущий бар
//--- Если limit равен 1 (открытие нового бара), то просчитываются два бара - текущий вновь открытый и предыдущий
//--- Если limit более 1, то это либо первый запуск индикатора, либо какие-то изменения в истории - индикатор полностью пересчитывается
   if(limit>1)
     {
      limit=rates_total-period_ma-1;
      ArrayInitialize(BufferMA,EMPTY_VALUE);
     }
     
//--- Расчёт количества копируемых данных из хэндла индикатора в рисуемый буфер
   int count=(limit>1 ? rates_total : 1),copied=0;
//--- Подготовка данных (получение данных в буфер скользящей средней по хэндлу)
   copied=CopyBuffer(handle_ma,0,0,count,BufferMA);
   if(copied!=count) return 0;

//--- Цикл расчёта индикатора по данным скользящей средней
   for(int i=limit; i>=0 && !IsStopped(); i--)
     {
      // Здесь рассчитываем некий индикатор по данным стандартного Moving Average
     }

//--- Получаем данные цен и таймсерии и выводим на панель
//--- При первом запуске выводим на панель данные текущего бара
   static bool first=true;
   if(first)
     {
      DrawData(0,TimeCurrent());
      first=false;
     }

//--- Объявляем структуру цен и получаем текущие цены
   MqlTick  tick={0};
   if(!SymbolInfoTick(Symbol(),tick))
      return 0;

//--- Если курсор находится на текущем баре - выводим на панель все данные текущего бара
   if(mouse_bar_index==0)
      DrawData(0,time[0]);
//--- Иначе - выводим на панель только цены Bid и Ask (обновляем цены на панели на каждом тике)
   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 next call
   return(rates_total);
  }

В обработчике OnChartEvent() индикатора сначала вызываем обработчик OnChartEvent панели, затем обрабатываем нужные события:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Вызываем обработчик событий панели
   dashboard.OnChartEvent(id,lparam,dparam,sparam);

//--- Если курсор перемещается или щелчок по графику
   if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
     {
      //--- Объявляем переменные для записи в них координат времени и цены
      datetime time=0;
      double price=0;
      int wnd=0;
      //--- Если координаты курсора преобразованы в дату и время
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price))
        {
         //--- записываем индекс бара, где расположен курсор в глобальную переменную
         mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         //--- Выводим данные бара под курсором на панель
         DrawData(mouse_bar_index,time);
        }
     }

//--- Если получили пользовательское событие - выводим об этом сообщение в журнал
   if(id>CHARTEVENT_CUSTOM)
     {
      //--- Здесь может быть обработка щелчка по кнопке закрытия на панели
      PrintFormat("%s: Event id=%ld, object id (lparam): %lu, event message (sparam): %s",__FUNCTION__,id,lparam,sparam);
     }
  }

Здесь, при получении из панели события нажатия на кнопку закрытия, и при необходимости реакции на такое событие, нужно прописать его обработку. Решение о поведении программы на это событие лежит на разработчике программы.


Функция, выводящая данные текущих цен и бара по индексу:

//+------------------------------------------------------------------+
//| Выводит данные с указанного индекса таймсерии на панель          |
//+------------------------------------------------------------------+
void DrawData(const int index,const datetime time)
  {
//--- Объявляем переменные для получения в них данных
   MqlTick  tick={0};
   MqlRates rates[1];
//--- Если текущие цены получить не удалось - уходим
   if(!SymbolInfoTick(Symbol(),tick))
      return;
//--- Если данные бара по указанному индексу получить не удалось - уходим
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,1,rates)!=1)
      return;
//--- Выводим на панель текущие цены и данные указанного бара
   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);
//--- Перерисовываем график для немедленного отображения всех изменений на панели
   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. Вот их-то мы и получаем из табличных данных, указывая расположение ячейки таблицы по её номеру строки и столбца.

Например, цены Bid и Ask выводятся на панель по "адресу" ячеек таблицы для Bid 0,0 (текст 'Bid') и 0,1 (значение цены Bid):

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);

Здесь берутся значения ячеек

для текста "Bid":

  • CellX(0,0) — ячейка в нулевой строке и нулевом столбце — значение координаты X,
  • CellY0,0) — ячейка в нулевой строке и нулевом столбце — значение координаты Y.

для значения цены Bid:

  • CellX(0,1) — ячейка в нулевой строке и первом столбце — значение координаты X,
  • CellY0,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 


Если запустить два индикатора с панелью на одном графике, указав разные значения уникального идентификатора панели, то они будут работать независимо друг от друга:


Здесь видно, что хоть панели и работают раздельно — для каждого индикатора своя, но есть у них и конфликт: при перемещении панелей график тоже пытается перемещаться. Происходит это по причине, что одна панель, которую перемещаем, отключает перемещение графика, а вторая, видя, что курсор находится за её пределами — включает.

Чтобы избавиться от такого поведения, самое простое, что можно сделать — это организовать в глобальных переменных терминала семафор, в который будет записываться идентификатор активной панели. Остальные, видя там не свой идентификатор, не будут пытаться управлять графиком.

Если же индикатор запустить в визуальном режиме тестера и попытаться двигать панель, то с неким трудом она будет перемещаться по экрану. При этом, что весьма полезно, данные можно получать от баров тестируемого графика — при щелчке по бару, его данные отобразятся на панели. Также правая кнопка мышки (её удержание и перемещение курсора по графику) помогает указать панели где сейчас находится курсор и отобразить данные, либо захватить мышкой панель за область заголовка и сместить на нужное место. К сожалению, в визуальном режиме тестера, из-за неполной реализации работы с событиями приходится прибегать к таким ухищрениям.


Заключение

Сегодня мы создали небольшую панель, которая может стать помощником при разработке своих стратегий по индикаторам, и далее, в последующих статьях рассмотрим подключение индикаторов и работу с их данными в советниках для всех стандартных индикаторов.


Прикрепленные файлы |
Dashboard.mqh (195.8 KB)
TestDashboard.mq5 (24.57 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Evgeny
Evgeny | 6 окт. 2023 в 00:53

Спасибо, что делитесь.

У данной реализации класса есть большой недостаток, если есть потребность в его наследовании.

1. Нет конструктора по умолчанию, а параметрический задает важные параметры по размеру. Например, если нужно рассчитывать размер панели на основе своих параметров в потомке класса, то эту логику придется реализовать либо вне класса, либо в классе, но с выносом расчета в отдельную функцию, что несколько бьет по логике, так как вызов параметрического конструктора, без наличия дефолтного, будет находиться в списке инициализации. Возможно лучшим решением было бы вынести начальную инициализацию из конструктора в функцию класса.

2. Секция с параметрами объявлена как private, что не дает в потомке реализовать например другую цветовую схему или изменить размер заголовка

3. Понимаю, что работа не закончена, но некоторые функции не реализованы, например SetButtonClose(On/Off), SetButtonMinimize(On/Off)

С наличием исходника допиливание проблем не составляет, но все же...

Artyom Trishkin
Artyom Trishkin | 6 окт. 2023 в 04:54
Evgeny #:

Спасибо, что делитесь.

У данной реализации класса есть большой недостаток, если есть потребность в его наследовании.

1. Нет конструктора по умолчанию, а параметрический задает важные параметры по размеру. Например, если нужно рассчитывать размер панели на основе своих параметров в потомке класса, то эту логику придется реализовать либо вне класса, либо в классе, но с выносом расчета в отдельную функцию, что несколько бьет по логике, так как вызов параметрического конструктора, без наличия дефолтного, будет находиться в списке инициализации. Возможно лучшим решением было бы вынести начальную инициализацию из конструктора в функцию класса.

2. Секция с параметрами объявлена как private, что не дает в потомке реализовать например другую цветовую схему или изменить размер заголовка

3. Понимаю, что работа не закончена, но некоторые функции не реализованы, например SetButtonClose(On/Off), SetButtonMinimize(On/Off)

С наличием исходника допиливание проблем не составляет, но все же...

Статья обучающая. В ней панель покрывает минимальные потребности.

1. В потомке есть свой конструктор, в его списке инициализации должен быть конструктор родителя. В нём и указываете требуемые размеры.

2. О цветовых схемах не вспоминал даже) Равно, как и о размере заголовка.

3. Такие методы объявлены в публичной секции. Странно, что не реализованы. Точно помню, что тестировал их включение/отключение... Мне пришлось этот класс переписывать с нуля по памяти, так как его первую версию уничтожил Windows при нехватке места на диске. Наверное тогда и забыл их восстановить. Спасибо, поправлю.

Да, с наличием исходного кода - все карты в руки читателям. На то и направлены обучающие статьи.

Evgeny
Evgeny | 6 окт. 2023 в 14:38
Artyom Trishkin #:

Статья обучающая. В ней панель покрывает минимальные потребности.

1. В потомке есть свой конструктор, в его списке инициализации должен быть конструктор родителя. В нём и указываете требуемые размеры.

2. О цветовых схемах не вспоминал даже) Равно, как и о размере заголовка.

3. Такие методы объявлены в публичной секции. Странно, что не реализованы. Точно помню, что тестировал их включение/отключение... Мне пришлось этот класс переписывать с нуля по памяти, так как его первую версию уничтожил Windows при нехватке места на диске. Наверное тогда и забыл их восстановить. Спасибо, поправлю.

Да, с наличием исходного кода - все карты в руки читателям. На то и направлены обучающие статьи.

1. Да, но просто когда вызов конструктора предка идет в  списке инициализации, то расчет параметров, которые будут в него переданы надо куда-то выносить. Особенно, если он не самый примитивный. 

Например расчет положения X, Y для начального показа в правом нижнем углу, расчет высоты в зависимости от предполагаемого количества строк таблицы.. Это потребует 3 дополнительных функции класса, а не все одним куском кода в своем конструкторе с последующим вызовом конструктора предка.  Это выглядит больше как костыль, хотя и работает. Ну не очень красивый код получается, когда можно сделать архитектуру класса более приспособленной к дальнейшим изменениям. (Конечно замечание из заметок перфекциониста)

2. Мелкие украшательства делают продукт качественней. Но у Вас в целом код красивый и решения интересные, приятно его читать.

3. Сочувствую, потеря данных всегда очень неприятна, поэтому надежный бэкап наше все.

Спасибо, ждем новых статей.

Разработка системы репликации - Моделирование рынка (Часть 09): Пользовательские события Разработка системы репликации - Моделирование рынка (Часть 09): Пользовательские события
Здесь мы увидим, как активировать пользовательские события и проработать вопрос о том, как индикатор сообщает о состоянии сервиса репликации/моделирования.
Разработка системы репликации - Моделирование рынка (Часть 08): Блокировка индикатора Разработка системы репликации - Моделирование рынка (Часть 08): Блокировка индикатора
В этой статье мы рассмотрим, как заблокировать индикатор при простом использовании языка MQL5, и сделаем это очень интересным и удивительным способом.
Теория категорий в MQL5 (Часть 11): Графы Теория категорий в MQL5 (Часть 11): Графы
Статья продолжает серию о реализации теории категорий в MQL5. Здесь мы рассмотрим, как теория графов может быть интегрирована с моноидами и другими структурами данных при разработке стратегии закрытия торговой системы.
Нейросети — это просто (Часть 56): Использование ядерной нормы для стимулирования исследования Нейросети — это просто (Часть 56): Использование ядерной нормы для стимулирования исследования
Исследование окружающей среды в задачах обучения с подкреплением является актуальной проблемой. Ранее мы уже рассматривали некоторые подходы. И сегодня я предлагаю познакомиться с ещё одним методом, основанным на максимизации ядерной нормы. Он позволяет агентам выделять состояния среды с высокой степенью новизны и разнообразия.