English 中文 Español Deutsch 日本語 Português
Графические интерфейсы VII: Элементы "Таблицы" (Глава 1)

Графические интерфейсы VII: Элементы "Таблицы" (Глава 1)

MetaTrader 5Примеры | 9 июня 2016, 10:54
4 186 18
Anatoli Kazharski
Anatoli Kazharski

Содержание

 

 

Введение

Более подробно о том, для чего предназначена эта библиотека, можно прочитать в самой первой статье: Графические интерфейсы I: Подготовка структуры библиотеки (Глава 1). В конце статей каждой части представлен список глав со ссылками. Там же есть возможность загрузить к себе на компьютер полную версию библиотеки на текущей стадии разработки. Файлы нужно разместить по тем же директориям, как они расположены в архиве.

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

  • таблицу из текстовых меток;
  • таблицу из полей ввода;
  • нарисованную таблицу.

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

В следующей статье (2 главе VII части) будут рассмотрены следующие элементы:

  • вкладки;
  • вкладки с картинками.

 


Элемент "Таблица из текстовых меток"

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

Перечислим все составные части этого типа таблицы.

  1. Фон.
  2. Текстовые метки.
  3. Вертикальная полоса прокрутки.
  4. Горизонтальная полоса прокрутки.

 Рис. 1. Составные части элемента «Таблица из текстовых меток».

Рис. 1. Составные части элемента «Таблица из текстовых меток».

Далее рассмотрим, как устроен класс этого элемента.

 


Разработка класса CLabelsTable

Создаём файл LabelsTable.mqh и подключаем его к библиотеке (к файлу WndContainer.mqh):

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#include "LabelsTable.mqh"

В файле LabelsTable.mqh нужно создать класс CLabelsTable со стандартными для всех элементов библиотеки методами, а также методом для сохранения указателя на форму, к которой этот элемент будет присоединён. Для всех элементов, которые будут представлены в этой статье, нужно будет делать всё то же самое.

//+------------------------------------------------------------------+
//| Класс для создания таблицы из текстовых меток                    |
//+------------------------------------------------------------------+
class CLabelsTable : public CElement
  {
private:
   //--- Указатель на форму, к которой элемент присоединён
   CWindow          *m_wnd;
   //---
public:
                     CLabelsTable(void);
                    ~CLabelsTable(void);
   //--- (1) Сохраняет указатель формы, (2) возвращает указатели на полосы прокрутки
   void              WindowPointer(CWindow &object)               { m_wnd=::GetPointer(object);      }
   //---
public:
   //--- Обработчик событий графика
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Таймер
   virtual void      OnEventTimer(void);
   //--- Перемещение элемента
   virtual void      Moving(const int x,const int y);
   //--- (1) Показ, (2) скрытие, (3) сброс, (4) удаление
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- (1) Установка, (2) сброс приоритетов на нажатие левой кнопки мыши
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
   //--- Сбросить цвет
   virtual void      ResetColors(void) {}
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLabelsTable::CLabelsTable(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLabelsTable::~CLabelsTable(void)
  {
  }

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

  • Высота ряда
  • Отступ первого столбца от левого края элемента
  • Расстояние между столбцами
  • Цвет фона
  • Цвет текста
  • Режим фиксации первой строки
  • Режим фиксации первого столбца
  • Общее количество столбцов
  • Общее количество рядов
  • Количество столбцов видимой части таблицы
  • Количество рядов видимой части таблицы

В этом типе таблицы координаты текстовых меток будут рассчитываться от центральной точки привязки (ANCHOR_CENTER). Поэтому отступ от левого края элемента для текстовых меток первого столбца и расстояние между текстовыми метками каждого столбца будут рассчитываться от центра каждой из них.

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

class CLabelsTable : public CElement
  {
private:
   //--- Высота ряда
   int               m_row_y_size;
   //--- Цвет фона таблицы
   color             m_area_color;
   //--- Цвет текста в таблице по умолчанию
   color             m_text_color;
   //--- Расстояние между точкой привязки первого столбца и левым краем элемента
   int               m_x_offset;
   //--- Расстояние между точками привязки столбцов
   int               m_column_x_offset;
   //--- Режим фиксации первой строки
   bool              m_fix_first_row;
   //--- Режим фиксации первого столбца
   bool              m_fix_first_column; 
   //--- Приоритеты на нажатие левой кнопки мыши
   int               m_zorder;
   int               m_area_zorder;
   //---
public:
   //--- (1) Цвет фона, (2) цвет текста
   void              AreaColor(const color clr)                   { m_area_color=clr;                }
   void              TextColor(const color clr)                   { m_text_color=clr;                }
   //--- (1) Высота ряда, (2) устанавливает расстояние между точкой привязки первого столбца и левым краем таблицы,
   //    (3) устанавливает расстояние между точками привязки столбцов
   void              RowYSize(const int y_size)                   { m_row_y_size=y_size;             }
   void              XOffset(const int x_offset)                  { m_x_offset=x_offset;             }
   void              ColumnXOffset(const int x_offset)            { m_column_x_offset=x_offset;      }
   //--- (1) Возвращает и (2) устанавливает режим фиксации первой строки
   bool              FixFirstRow(void)                      const { return(m_fix_first_row);         }
   void              FixFirstRow(const bool flag)                 { m_fix_first_row=flag;            }
   //--- (1) Возвращает и (2) устанавливает режим фиксации первого столбца
   bool              FixFirstColumn(void)                   const { return(m_fix_first_column);      }
   void              FixFirstColumn(const bool flag)              { m_fix_first_column=flag;         }
  };

Для установки общего и видимого количества столбцов и рядов таблицы напишем методы CLabelsTable::TableSize() и CLabelsTable::VisibleTableSize(). Кроме этого, понадобятся двумерные динамические массивы в виде структур. Одна структура (LTLabels) будет предназначена для создания массива текстовых меток видимой части таблицы. Вторая структура (LTOptions) нужна для хранения всех значений и свойств для каждой ячейки таблицы. В данной реализации здесь будут отображаемые значения (строки) и цвет текста.

В методы CLabelsTable::TableSize() и CLabelsTable::VisibleTableSize() нужно передать два аргумента: количество столбцов и рядов. В начале этих методов осуществляется корректировка переданных значений, если было передано количество столбцов меньше одного, а количество рядов — меньше двух. Затем устанавливаются размеры всем массивам и инициализируются массивы свойств.

Кроме методов для установки размеров таблицы, создадим ещё методы для получения её общих и видимых размеров для столбцов и рядов. 

class CLabelsTable : public CElement
  {
private:
   //--- Массив объектов видимой части таблицы
   struct LTLabels
     {
      CLabel            m_rows[];
     };
   LTLabels          m_columns[];
   //--- Массивы значений и свойств таблицы
   struct LTOptions
     {
      string            m_vrows[];
      color             m_colors[];
     };
   LTOptions         m_vcolumns[];
   //---
public:
   //--- Возвращает общее количество (1) рядов и (2) столбцов
   int               RowsTotal(void)                        const { return(m_rows_total);            }
   int               ColumnsTotal(void)                     const { return(m_columns_total);         }
   //--- Возвращает количество (1) рядов и (2) столбцов видимой части таблицы
   int               VisibleRowsTotal(void)                 const { return(m_visible_rows_total);    }
   int               VisibleColumnsTotal(void)              const { return(m_visible_columns_total); }

   //--- Устанавливает (1) размер таблицы и (2) размер видимой её части
   void              TableSize(const int columns_total,const int rows_total);
   void              VisibleTableSize(const int visible_columns_total,const int visible_rows_total);
  };
//+------------------------------------------------------------------+
//| Устанавливает размер таблицы                                     |
//+------------------------------------------------------------------+
void CLabelsTable::TableSize(const int columns_total,const int rows_total)
  {
//--- Должно быть не менее одного столбца
   m_columns_total=(columns_total<1) ? 1 : columns_total;
//--- Должно быть не менее двух рядов
   m_rows_total=(rows_total<2) ? 2 : rows_total;
//--- Установить размер массиву столбцов
   ::ArrayResize(m_vcolumns,m_columns_total);
//--- Установить размер массивам рядов
   for(int i=0; i<m_columns_total; i++)
     {
      ::ArrayResize(m_vcolumns[i].m_vrows,m_rows_total);
      ::ArrayResize(m_vcolumns[i].m_colors,m_rows_total);
      //--- Инициализация массива цвета текста значением по умолчанию
      ::ArrayInitialize(m_vcolumns[i].m_colors,m_text_color);
     }
  }
//+------------------------------------------------------------------+
//| Устанавливает размер видимой части таблицы                       |
//+------------------------------------------------------------------+
void CLabelsTable::VisibleTableSize(const int visible_columns_total,const int visible_rows_total)
  {
//--- Должно быть не менее одного столбца
   m_visible_columns_total=(visible_columns_total<1) ? 1 : visible_columns_total;
//--- Должно быть не менее двух рядов
   m_visible_rows_total=(visible_rows_total<2) ? 2 : visible_rows_total;
//--- Установить размер массиву столбцов
   ::ArrayResize(m_columns,m_visible_columns_total);
//--- Установить размер массивам рядов
   for(int i=0; i<m_visible_columns_total; i++)
      ::ArrayResize(m_columns[i].m_rows,m_visible_rows_total);
  }

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

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLabelsTable::CLabelsTable(void) : m_fix_first_row(false),
                                   m_fix_first_column(false),
                                   m_row_y_size(18),
                                   m_x_offset(30),
                                   m_column_x_offset(60),
                                   m_area_color(clrWhiteSmoke),
                                   m_text_color(clrBlack),
                                   m_rows_total(2),
                                   m_columns_total(1),
                                   m_visible_rows_total(2),
                                   m_visible_columns_total(1)
  {
//--- Сохраним имя класса элемента в базовом классе
   CElement::ClassName(CLASS_NAME);
//--- Установим приоритеты на нажатие левой кнопки мыши
   m_zorder      =0;
   m_area_zorder =1;
//--- Установим размер таблицы и её видимой части
   TableSize(m_columns_total,m_rows_total);
   VisibleTableSize(m_visible_columns_total,m_visible_rows_total);
  }

Для создания элемента создадим четыре приватных (private) метода и один публичный (public) для внешнего вызова. Чтобы у пользователя была возможность настроить полосы прокрутки этой таблицы, добавим методы, которые возвращают их указатели. 

class CLabelsTable : public CElement
  {
private:
   //--- Объекты для создания таблицы
   CRectLabel        m_area;
   CScrollV          m_scrollv;
   CScrollH          m_scrollh;
   //--- Массив объектов видимой части таблицы
   struct LTLabels
     {
      CLabel            m_rows[];
     };
   LTLabels          m_columns[];
   //---
public:
   //--- Возвращает указатели на полосы прокрутки
   CScrollV         *GetScrollVPointer(void)                const { return(::GetPointer(m_scrollv)); }
   CScrollH         *GetScrollHPointer(void)                const { return(::GetPointer(m_scrollh)); }
   //--- Методы для создания таблицы
   bool              CreateLabelsTable(const long chart_id,const int subwin,const int x,const int y);
   //---
private:
   bool              CreateArea(void);
   bool              CreateLabels(void);
   bool              CreateScrollV(void);
   bool              CreateScrollH(void);
  };

Из методов создания объектов элемента приведём здесь только CLabelsTable::CreateLabels(), который нужен для создания массива текстовых меток. При формировании имени объекта добавляем индексы столбца и ряда. С остальными методами Вы можете подробнее ознакомиться в приложенных в конце статьи файлах. 

//+------------------------------------------------------------------+
//| Создаёт массив текстовых меток                                   |
//+------------------------------------------------------------------+
bool CLabelsTable::CreateLabels(void)
  {
//--- Координаты и отступ
   int x      =CElement::X();
   int y      =0;
   int offset =0;
//--- Столбцы
   for(int c=0; c<m_visible_columns_total; c++)
     {
      //--- Расчёт отступа столбца
      offset=(c>0) ? m_column_x_offset : m_x_offset;
      //--- Расчёт координаты X
      x=x+offset;
      //--- Ряды
      for(int r=0; r<m_visible_rows_total; r++)
        {
         //--- Формирование имени объекта
         string name=CElement::ProgramName()+"_labelstable_label_"+(string)c+"_"+(string)r+"__"+(string)CElement::Id();
         //--- Расчёт координаты Y
         y=(r>0) ? y+m_row_y_size-1 : CElement::Y()+10;
         //--- Создание объекта
         if(!m_columns[c].m_rows[r].Create(m_chart_id,name,m_subwin,x,y))
            return(false);
         //--- Установка свойств
         m_columns[c].m_rows[r].Description(m_vcolumns[c].m_vrows[r]);
         m_columns[c].m_rows[r].Font(FONT);
         m_columns[c].m_rows[r].FontSize(FONT_SIZE);
         m_columns[c].m_rows[r].Color(m_text_color);
         m_columns[c].m_rows[r].Corner(m_corner);
         m_columns[c].m_rows[r].Anchor(ANCHOR_CENTER);
         m_columns[c].m_rows[r].Selectable(false);
         m_columns[c].m_rows[r].Z_Order(m_zorder);
         m_columns[c].m_rows[r].Tooltip("\n");
         //--- Отступы от крайней точки формы
         m_columns[c].m_rows[r].XGap(x-m_wnd.X());
         m_columns[c].m_rows[r].YGap(y-m_wnd.Y());
         //--- Координаты
         m_columns[c].m_rows[r].X(x);
         m_columns[c].m_rows[r].Y(y);
         //--- Сохраним указатель объекта
         CElement::AddToArray(m_columns[c].m_rows[r]);
        }
     }
//---
   return(true);
  }

Понадобятся методы, с помощью которых можно изменить значения и свойства в любой ячейке таблицы в любой момент уже после создания таблицы. Для изменения и получения значения ячейки напишем методы CLabelsTable::SetValue() и CLabelsTable::GetValue(). Для обоих методов в качестве первых двух аргументов нужно передать индексы столбца и ряда таблицы. В методе CLabelsTable::SetValue() в качестве третьего аргумента нужно также передать значение, которое требуется занести в массив по указанным индексам. В начале этих методов производится обязательная проверка на выход из диапазонов массивов. 

class CLabelsTable : public CElement
  {
public:
   //--- Устанавливает значение в указанную ячейку таблицы
   void              SetValue(const int column_index,const int row_index,const string value);
   //--- Получает значение из указанной ячейки таблицы
   string            GetValue(const int column_index,const int row_index);
  };
//+------------------------------------------------------------------+
//| Устанавливает значение по указанным индексам                     |
//+------------------------------------------------------------------+
void CLabelsTable::SetValue(const int column_index,const int row_index,const string value)
  {
//--- Проверка на выход из диапазона столбцов
   int csize=::ArraySize(m_vcolumns);
   if(csize<1 || column_index<0 || column_index>=csize)
      return;
//--- Проверка на выход из диапазона рядов
   int rsize=::ArraySize(m_vcolumns[column_index].m_vrows);
   if(rsize<1 || row_index<0 || row_index>=rsize)
      return;
//--- Установить значение
   m_vcolumns[column_index].m_vrows[row_index]=value;
  }
//+------------------------------------------------------------------+
//| Возвращает значение по указанным индексам                        |
//+------------------------------------------------------------------+
string CLabelsTable::GetValue(const int column_index,const int row_index)
  {
//--- Проверка на выход из диапазона столбцов
   int csize=::ArraySize(m_vcolumns);
   if(csize<1 || column_index<0 || column_index>=csize)
      return("");
//--- Проверка на выход из диапазона рядов
   int rsize=::ArraySize(m_vcolumns[column_index].m_vrows);
   if(rsize<1 || row_index<0 || row_index>=rsize)
      return("");
//--- Вернуть значение
   return(m_vcolumns[column_index].m_vrows[row_index]);
  }

Кроме изменения значений в таблице, пользователю библиотеки может понадобиться изменить цвет текста. Например, положительные значения можно будет отображать зелёным, а отрицательные – красным. Для этих целей напишем метод CLabelsTable::TextColor(). Он подобен методу CLabelsTable::SetValue(), но в качестве третьего аргумента здесь нужно передать цвет. 

class CLabelsTable : public CElement
  {
public:
   //--- Изменяет цвет текста в указанной ячейке таблицы
   void              TextColor(const int column_index,const int row_index,const color clr);
  };
//+------------------------------------------------------------------+
//| Изменяет цвет по указанным индексам                              |
//+------------------------------------------------------------------+
void CLabelsTable::TextColor(const int column_index,const int row_index,const color clr)
  {
//--- Проверка на выход из диапазона столбцов
   int csize=::ArraySize(m_vcolumns);
   if(csize<1 || column_index<0 || column_index>=csize)
      return;
//--- Проверка на выход из диапазона рядов
   int rsize=::ArraySize(m_vcolumns[column_index].m_vrows);
   if(rsize<1 || row_index<0 || row_index>=rsize)
      return;
//--- Установить цвет
   m_vcolumns[column_index].m_colors[row_index]=clr;
  }

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

Назовём этот метод CLabelsTable::UpdateTable(). Если включен режим закреплённых заголовков, то чтобы первый ряд всегда был сверху и/или крайний (левый) столбец всегда был слева, нужно начинать смещение данных со второго (1) индекса массивов. В начале метода объявлены переменные t и l, которым присваивается 1 либо 0, в зависимости от того, какой сейчас режим используется.

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

В конце метода в двойном цикле осуществляется смещение основных данных таблицы, а также цвета ячеек. 

class CLabelsTable : public CElement
  {
public:
   //--- Обновление данных таблицы с учётом последних изменений
   void              UpdateTable(void);
  };
//+------------------------------------------------------------------+
//| Обновление данных таблицы с учётом последних изменений           |
//+------------------------------------------------------------------+
void CLabelsTable::UpdateTable(void)
  {
//--- Смещение на один индекс, если включен режим закреплённых заголовков
   int t=(m_fix_first_row) ? 1 : 0;
   int l=(m_fix_first_column) ? 1 : 0;
//--- Получим текущие позиции ползунков горизонтальной и вертикальной полос прокрутки
   int h=m_scrollh.CurrentPos()+l;
   int v=m_scrollv.CurrentPos()+t;
//--- Смещение заголовков в левом столбце
   if(m_fix_first_column)
     {
      m_columns[0].m_rows[0].Description(m_vcolumns[0].m_vrows[0]);
      //--- Ряды
      for(int r=t; r<m_visible_rows_total; r++)
        {
         if(r>=t && r<m_rows_total)
            m_columns[0].m_rows[r].Description(m_vcolumns[0].m_vrows[v]);
         //---
         v++;
        }
     }
//--- Смещение заголовков в верхнем ряду
   if(m_fix_first_row)
     {
      m_columns[0].m_rows[0].Description(m_vcolumns[0].m_vrows[0]);
      //--- Столбцы
      for(int c=l; c<m_visible_columns_total; c++)
        {
         if(h>=l && h<m_columns_total)
            m_columns[c].m_rows[0].Description(m_vcolumns[h].m_vrows[0]);
         //---
         h++;
        }
     }
//--- Получим текущую позицию ползунка горизонтальной полосы прокрутки
   h=m_scrollh.CurrentPos()+l;
//--- Столбцы
   for(int c=l; c<m_visible_columns_total; c++)
     {
      //--- Получим текущую позицию ползунка вертикальной полосы прокрутки
      v=m_scrollv.CurrentPos()+t;
      //--- Ряды
      for(int r=t; r<m_visible_rows_total; r++)
        {
         //--- Смещение данных таблицы
         if(v>=t && v<m_rows_total && h>=l && h<m_columns_total)
           {
            //--- Корректировка цвета
            m_columns[c].m_rows[r].Color(m_vcolumns[h].m_colors[v]);
            //--- Корректировка значений
            m_columns[c].m_rows[r].Description(m_vcolumns[h].m_vrows[v]);
            v++;
           }
        }
      //---
      h++;
     }
  }

Так же, как это реализовано в элементах с полями ввода и в списках, сделаем и здесь ускоренную перемотку при зажатии левой кнопки мыши над кнопками полос прокрутки. Не будем приводить здесь код метода CLabelsTable::FastSwitching(), так как он очень похож на одноимённые методы, которые уже были рассмотрены в предыдущих статьях в классах CListView, CSpinEdit и CCheckBoxEdit

Код методов для обработки событий элемента можно теперь представить так, как это показано в листинге ниже: 

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CLabelsTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события перемещения курсора
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Выйти, если элемент скрыт
      if(!CElement::IsVisible())
         return;
      //--- Координаты и состояние левой кнопки мыши
      int x=(int)lparam;
      int y=(int)dparam;
      m_mouse_state=(bool)int(sparam);
      //--- Проверка фокуса над таблицей
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      //--- Смещаем список, если управление ползунком полосы прокрутки в действии
      if(m_scrollv.ScrollBarControl(x,y,m_mouse_state) || m_scrollh.ScrollBarControl(x,y,m_mouse_state))
         UpdateTable();
      //---
      return;
     }
//--- Обработка нажатия на объектах
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Если было нажатие на одной из кнопок полос прокрутки таблицы
      if(m_scrollv.OnClickScrollInc(sparam) || m_scrollv.OnClickScrollDec(sparam) ||
         m_scrollh.OnClickScrollInc(sparam) || m_scrollh.OnClickScrollDec(sparam))
         //--- Сдвигает таблицу относительно полосы прокрутки
         UpdateTable();
      //---
      return;
     }
  }
//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CLabelsTable::OnEventTimer(void)
  {
//--- Если элемент является выпадающим
   if(CElement::IsDropdown())
      FastSwitching();
//--- Если элемент не выпадающий, то учитываем доступность формы в текущий момент
   else
     {
      //--- Отслеживаем перемотку таблицы, только если форма не заблокирована
      if(!m_wnd.IsLocked())
         FastSwitching();
     }
  }

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

 


Тест таблицы из текстовых меток

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

class CProgram : public CWndEvents
  {
private:
   //--- Таблица из текстовых меток
   CLabelsTable      m_labels_table;
   //---
private:
   //--- Таблица из текстовых меток
#define LABELS_TABLE1_GAP_X   (1)
#define LABELS_TABLE1_GAP_Y   (42)
   bool              CreateLabelsTable(void);
  };

Теперь рассмотрим подробнее код метода CProgram::CreateLabelsTable(). Создадим таблицу, в которой 21 столбец и 100 рядов. Количество видимых столбцов пусть будет 5, а рядов — 10. Зафиксируем верхний ряд и первый столбец таблицы от перемещения. После создания таблицы заполним её случайными значениями (от -1000 до 1000) и установим цвета. Положительные значения будут отображаться зеленым, а отрицательные - красным. После этого, как мы помним, нужно обязательно обновить таблицу для отображения последних изменений

//+------------------------------------------------------------------+
//| Создаёт таблицу из текстовых меток                               |
//+------------------------------------------------------------------+
bool CProgram::CreateLabelsTable(void)
  {
#define COLUMNS1_TOTAL (21)
#define ROWS1_TOTAL    (100)
//--- Сохраним указатель на форму
   m_labels_table.WindowPointer(m_window1);
//--- Координаты
   int x=m_window1.X()+LABELS_TABLE1_GAP_X;
   int y=m_window1.Y()+LABELS_TABLE1_GAP_Y;
//--- Количество видимых столбцов и строк
   int visible_columns_total =5;
   int visible_rows_total    =10;
//--- Установим свойства
   m_labels_table.XSize(400);
   m_labels_table.XOffset(40);
   m_labels_table.ColumnXOffset(75);
   m_labels_table.FixFirstRow(true);
   m_labels_table.FixFirstColumn(true);
   m_labels_table.TableSize(COLUMNS1_TOTAL,ROWS1_TOTAL);
   m_labels_table.VisibleTableSize(visible_columns_total,visible_rows_total);
//--- Создадим таблицу
   if(!m_labels_table.CreateLabelsTable(m_chart_id,m_subwin,x,y))
      return(false);
//--- Заполним таблицу:
//    Первая ячейка пустая
   m_labels_table.SetValue(0,0,"-");
//--- Заголовки для столбцов
   for(int c=1; c<COLUMNS1_TOTAL; c++)
     {
      for(int r=0; r<1; r++)
         m_labels_table.SetValue(c,r,"SYMBOL "+string(c));
     }
//--- Заголовки для рядов, способ выравнивания текста - справа
   for(int c=0; c<1; c++)
     {
      for(int r=1; r<ROWS1_TOTAL; r++)
        m_labels_table.SetValue(c,r,"PARAMETER "+string(r));
     }
//--- Данные и форматирование таблицы (цвет фона и цвет ячеек)
   for(int c=1; c<COLUMNS1_TOTAL; c++)
     {
      for(int r=1; r<ROWS1_TOTAL; r++)
         m_labels_table.SetValue(c,r,string(::rand()%1000-::rand()%1000));
     }
//--- Установим цвет текста в ячейках таблицы
   for(int c=1; c<m_labels_table.ColumnsTotal(); c++)
      for(int r=1; r<m_labels_table.RowsTotal(); r++)
         m_labels_table.TextColor(c,r,((double)m_labels_table.GetValue(c,r)>=0) ? clrGreen : clrRed);
//--- Обновить таблицу
   m_labels_table.UpdateTable();
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(0,m_labels_table);
   return(true);
  }

Протестируем также, как изменяются значения в таблице во время выполнения программы на графике терминала. Для этого в таймер класса пользовательского MQL-приложения CProgram::OnTimerEvent() добавим код, как показано в листинге ниже: 

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CProgram::OnTimerEvent(void)
  {
   CWndEvents::OnTimerEvent();
//--- Обновление второго пункта статусной строки будет производиться каждые 500 миллисекунд
   static int count=0;
   if(count<500)
     {
      count+=TIMER_STEP_MSC;
      return;
     }
//--- Обнулить счётчик
   count=0;
//--- Изменить значение во втором пункте статусной строки
   m_status_bar.ValueToItem(1,::TimeToString(::TimeLocal(),TIME_DATE|TIME_SECONDS));
//--- Заполним таблицу данными
   for(int c=1; c<m_labels_table.ColumnsTotal(); c++)
      for(int r=1; r<m_labels_table.RowsTotal(); r++)
         m_labels_table.SetValue(c,r,string(::rand()%1000-::rand()%1000));
//--- Установим цвет текста в ячейках таблицы
   for(int c=1; c<m_labels_table.ColumnsTotal(); c++)
      for(int r=1; r<m_labels_table.RowsTotal(); r++)
         m_labels_table.TextColor(c,r,((double)m_labels_table.GetValue(c,r)>=0) ? clrGreen : clrRed);
//--- Обновить таблицу
   m_labels_table.UpdateTable();
  }

Вызов метода для создания таблицы из текстовых меток нужно осуществлять в главном методе создания графического интерфейса программы CProgram::CreateExpertPanel(). Ниже показана сокращённая версия этого метода: 

//+------------------------------------------------------------------+
//| Создаёт экспертную панель                                        |
//+------------------------------------------------------------------+
bool CProgram::CreateExpertPanel(void)
  {
//--- Создание формы 1 для элементов управления
//--- Создание элементов управления:
//    Главное меню
//--- Контекстные меню
//--- Создание статусной строки
//--- Таблица из текстовых меток
   if(!CreateLabelsTable())
      return(false);
//--- Перерисовка графика
   m_chart.Redraw();
   return(true);
  }

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

 Рис. 2. Тест элемента «Таблица из текстовых меток».

Рис. 2. Тест элемента «Таблица из текстовых меток».

Всё отлично работает. Теперь рассмотрим класс для создания второго типа таблиц. 


 


Элемент "Таблица из полей ввода"

В отличие от таблицы из текстовых меток, таблица из полей ввода даёт больше гибкости и возможностей. Здесь уже можно не только изменить цвет текста, но также и: 

  • установить режим выравнивания текста в ячейке (слева/справа/по центру);
  • изменить цвет фона и рамки полей ввода;
  • значения в полях ввода можно будет изменять даже вручную, включив соответствующий режим.

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

  1. Фон
  2. Поля ввода
  3. Вертикальная полоса прокрутки
  4. Горизонтальная полоса прокрутки

 Рис. 3. Составные части элемента «Таблица из полей ввода».

Рис. 3. Составные части элемента «Таблица из полей ввода».


Давайте посмотрим, чем же отличается эта таблица в своем классе кода от той, которую мы рассмотрели выше.

 


Разработка класса CTable

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

class CTable : public CElement
  {
private:
   //--- Массив объектов видимой части таблицы
   struct TEdits
     {
      CEdit             m_rows[];
     };
   TEdits            m_columns[];
  };

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

class CTable : public CElement
  {
private:
   //--- Массивы значений и свойств таблицы
   struct TOptions
     {
      string            m_vrows[];
      ENUM_ALIGN_MODE   m_text_align[];
      color             m_text_color[];
      color             m_cell_color[];
     };
   TOptions          m_vcolumns[];
  };

Перечислим те режимы и свойства, которых нет в таблице из текстовых меток.

  • Режим редактируемой таблицы
  • Режим подсветки строки при наведении курсора мыши
  • Режим выделяемого ряда
  • Высота рядов
  • Цвет сетки
  • Цвет фона заголовков
  • Цвет текста заголовков
  • Цвет ячеек при наведении курсора мыши
  • Цвет текста в ячейках по умолчанию
  • Способ выравнивания текста в ячейках по умолчанию
  • Цвет фона выделенной строки
  • Цвет текста выделенной строки

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

class CTable : public CElement
  {
private:
   //--- Высота рядов таблицы
   int               m_row_y_size;
   //--- (1) Цвет фона и (2) рамки фона таблицы
   color             m_area_color;
   color             m_area_border_color;
   //--- Цвет сетки
   color             m_grid_color;
   //--- Цвет фона заголовков
   color             m_headers_color;
   //--- Цвет текста заголовков
   color             m_headers_text_color;
   //--- Цвет ячеек в разных состояниях
   color             m_cell_color;
   color             m_cell_color_hover;
   //--- Цвет текста в ячейках по умолчанию
   color             m_cell_text_color;
   //--- Цвет (1) фона и (2) текста выделенной строки
   color             m_selected_row_color;
   color             m_selected_row_text_color;
   //--- Режим редактируемой таблицы
   bool              m_read_only;
   //--- Режим подсветки строки при наведении курсора мыши
   bool              m_lights_hover;
   //--- Режим выделяемой строки
   bool              m_selectable_row;
   //--- Режим фиксации первой строки
   bool              m_fix_first_row;
   //--- Режим фиксации первого столбца
   bool              m_fix_first_column;
   //--- Способ выравнивания текста в полях ввода по умолчанию
   ENUM_ALIGN_MODE   m_align_mode;
   //---
public:
   //--- Цвет (1) фона и (2) рамки таблицы
   void              AreaColor(const color clr)                        { m_area_color=clr;                }
   void              BorderColor(const color clr)                      { m_area_border_color=clr;         }
   //--- (1) Возвращает и (2) устанавливает режим фиксации первой строки
   bool              FixFirstRow(void)                           const { return(m_fix_first_row);         }
   void              FixFirstRow(const bool flag)                      { m_fix_first_row=flag;            }
   //--- (1) Возвращает и (2) устанавливает режим фиксации первого столбца
   bool              FixFirstColumn(void)                        const { return(m_fix_first_column);      }
   void              FixFirstColumn(const bool flag)                   { m_fix_first_column=flag;         }
   //--- Цвет (1) фона заголовков, (2) текста заголовков и (3) сетки таблицы
   void              HeadersColor(const color clr)                     { m_headers_color=clr;             }
   void              HeadersTextColor(const color clr)                 { m_headers_text_color=clr;        }
   void              GridColor(const color clr)                        { m_grid_color=clr;                }
   //--- Размер рядов по оси Y
   void              RowYSize(const int y_size)                        { m_row_y_size=y_size;             }
   void              CellColor(const color clr)                        { m_cell_color=clr;                }
   void              CellColorHover(const color clr)                   { m_cell_color_hover=clr;          }
   //--- Режимы (1) "Только чтение", (2) подсветка ряда при наведении курсора мыши, (3) выделение ряда
   void              ReadOnly(const bool flag)                         { m_read_only=flag;                }
   void              LightsHover(const bool flag)                      { m_lights_hover=flag;             }
   void              SelectableRow(const bool flag)                    { m_selectable_row=flag;           }
   //--- Способ выравнивания текста в ячейках
   void              TextAlign(const ENUM_ALIGN_MODE align_mode)       { m_align_mode=align_mode;         }
  };

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

  • Общий размер таблицы (общее количество столбцов и рядов)
  • Видимый размер таблицы (видимое количество столбцов и рядов)
  • Способ выравнивания текста в ячейке (слева/справа/по центру)
  • Цвет текста
  • Цвет фона
  • Установка/изменение значения
  • Получение значения

Не будем здесь приводить код этих методов, поскольку аналогичные им уже были рассмотрены на примере таблицы из текстовых меток. После установки свойств таблицу нужно обязательно обновить, вызвав метод CTable::UpdateTable(), чтобы внесённые изменения были в ней отображены. 

class CTable : public CElement
  {
public:
   //--- Устанавливает (1) размер таблицы и (2) размер видимой её части
   void              TableSize(const int columns_total,const int rows_total);
   void              VisibleTableSize(const int visible_columns_total,const int visible_rows_total);
   //--- Установка (1) способа выравнивания текста, (2) цвета текста, (3) цвета фона ячейки
   void              TextAlign(const int column_index,const int row_index,const ENUM_ALIGN_MODE mode);
   void              TextColor(const int column_index,const int row_index,const color clr);
   void              CellColor(const int column_index,const int row_index,const color clr);
   //--- Устанавливает значение в указанную ячейку таблицы
   void              SetValue(const int column_index,const int row_index,const string value);
   //--- Получает значение из указанной ячейки таблицы
   string            GetValue(const int column_index,const int row_index);
   //--- Обновление данных таблицы с учётом последних изменений
   void              UpdateTable(void);
  };

Далее рассмотрим методы для управления таблицей. Всё это будут приватные (private) методы класса для внутреннего использования. В их задачи будут входить:

  • Обработка нажатия на ряде таблицы.
  • Обработка ввода значения в ячейку таблицы.
  • Получение идентификатора из имени объекта.
  • Извлечение индекса столбца из имени объекта.
  • Извлечение индекса ряда из имени объекта.
  • Подсветка выделенного ряда.
  • Изменение цвета ряда таблицы при наведении курсора мыши.
  • Ускоренная перемотка  данных таблицы.

Начнём с метода CTable::OnClickTableRow() для обработки нажатия на ряде таблицы. Здесь в начале метода нужно пройти ряд проверок. Программа выходит из метода в следующих случаях:

  • включен режим редактируемой таблицы;
  • одна из полос прокрутки находится в действии (ползунок в состоянии перемещения);
  • нажатие было не на ячейке таблицы. Это определяется по наличию в имени объекта названия программы и признака принадлежности к ячейке таблицы;
  • Идентификатор элемента не совпадает. Для извлечения идентификатора из имени объекта используется метод CTable::IdFromObjectName(), который уже рассматривался ранее на примере других элементов.

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

Если в итоге оказалось, что нажатие было осуществлено на заголовках (первый ряд), то программа выходит из метода. Иначе генерируется пользовательское сообщение, в котором содержатся (1) идентификатор графика, (2) идентификатор события (ON_CLICK_LIST_ITEM), (3) идентификатор элемента и (4) выделенный индекс ряда. 

class CTable : public CElement
  {
private:
   //--- Обработка нажатия на ряде таблицы
   bool              OnClickTableRow(const string clicked_object);
   //--- Получение идентификатора из имени объекта
   int               IdFromObjectName(const string object_name);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на ряде таблицы                                |
//+------------------------------------------------------------------+
bool CTable::OnClickTableRow(const string clicked_object)
  {
//--- Выйти, если включен режим редактируемой таблицы
   if(!m_read_only)
      return(false);
//--- Выйти, если полоса прокрутки в действии
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      return(false);
//--- Выйдем, если нажатие было не на ячейке таблицы
   if(::StringFind(clicked_object,CElement::ProgramName()+"_table_edit_",0)<0)
      return(false);
//--- Получим идентификатор имени объекта
   int id=IdFromObjectName(clicked_object);
//--- Выйти, если идентификатор не совпадает
   if(id!=CElement::Id())
      return(false);
//--- Для поиска индекса ряда
   int row_index=0;
//--- Смещение на один индекс, если включен режим закреплённых заголовков
   int t=(m_fix_first_row) ? 1 : 0;
//--- Столбцы
   for(int c=0; c<m_visible_columns_total; c++)
     {
      //--- Получим текущую позицию ползунка вертикальной полосы прокрутки
      int v=m_scrollv.CurrentPos()+t;
      //--- Ряды
      for(int r=t; r<m_visible_rows_total; r++)
        {
         //--- Если нажатие было на этой ячейке
         if(m_columns[c].m_rows[r].Name()==clicked_object)
           {
            //--- Сохраним индекс ряда
            m_selected_item=row_index=v;
            //--- Сохраним строку ячейки
            m_selected_item_text=m_columns[c].m_rows[r].Description();
            break;
           }
         //--- Увеличим счётчик ряда
         if(v>=t && v<m_rows_total)
            v++;
        }
     }
//--- Выйти, если нажали на заголовке
   if(m_fix_first_row && row_index<1)
      return(false);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElement::Id(),m_selected_item,"");
   return(true);
  }

Для обработки ввода значения в ячейку, когда включен режим редактируемой таблицы, напишем метод CTable::OnEndEditCell(). В начале этого метода программа тоже должна пройти ряд проверок. Выход из метода осуществляется в следующих случаях:

  • отключен режим редактируемой таблицы;
  • не совпадает имя программы или признак принадлежности к ячейке таблицы;
  • не совпадает идентификатор элемента.

Если все проверки пройдены, то далее, с помощью вспомогательных методов CTable::ColumnIndexFromObjectName() и CTable::RowIndexFromObjectName(), получаем индексы столбца и ряда ячейки в видимой части таблицы (индексы массива графических объектов). Теперь, чтобы получить индексы массива данных, нужно к индексам объектов прибавить текущие положения ползунков полос прокрутки. Далее нужно скорректировать индекс ряда, если включен режим закреплённых заголовков и индекс массива объектов равен нулю. Затем нужно проверить, было ли изменено значение в ячейке. Если это так, то новое значение сохраняется в соответствующем массиве данных и генерируется сообщение с (1) идентификатором графика, (2) идентификатором события (ON_END_EDIT), (3) идентификатором элемента и (4) строкой, которая формируется из индексов столбца и ряда, а также текущего значения в ячейке. В качестве разделителя в этой строке сделаем нижний прочерк «_». 

class CTable : public CElement
  {
private:
   //--- Обработка ввода значения в ячейку таблицы
   bool              OnEndEditCell(const string edited_object);
   //--- Извлекает индекс столбца из имени объекта
   int               ColumnIndexFromObjectName(const string object_name);
   //--- Извлекает индекс ряда из имени объекта
   int               RowIndexFromObjectName(const string object_name);
  };
//+------------------------------------------------------------------+
//| Событие окончания редактирования значения в ячейке               |
//+------------------------------------------------------------------+
bool CTable::OnEndEditCell(const string edited_object)
  {
//--- Выйти, если отключен режим редактируемой таблицы
   if(m_read_only)
      return(false);
//--- Выйдем, если нажатие было не на ячейке таблицы
   if(::StringFind(edited_object,CElement::ProgramName()+"_table_edit_",0)<0)
      return(false);
//--- Получим идентификатор из имени объекта
   int id=IdFromObjectName(edited_object);
//--- Выйти, если идентификатор не совпадает
   if(id!=CElement::Id())
      return(false);
//--- Получим индексы столбца и ряда ячейки
   int c =ColumnIndexFromObjectName(edited_object);
   int r =RowIndexFromObjectName(edited_object);
//--- Получим индексы столбца и ряда в массиве данных
   int vc =c+m_scrollh.CurrentPos();
   int vr =r+m_scrollv.CurrentPos();
//--- Скорректировать индекс ряда, если нажатие было на заголовке
   if(m_fix_first_row && r==0)
      vr=0;
//--- Получим введённое значение
   string cell_text=m_columns[c].m_rows[r].Description();
//--- Если значение в ячейке было изменено
   if(cell_text!=m_vcolumns[vc].m_vrows[vr])
     {
      //--- Сохраним новое значение в массиве
      SetValue(vc,vr,cell_text);
      //--- Отправим сообщение об этом
      ::EventChartCustom(m_chart_id,ON_END_EDIT,CElement::Id(),0,string(vc)+"_"+string(vr)+"_"+cell_text);
     }
//---
   return(true);
  }
//+------------------------------------------------------------------+
//| Извлекает индекс столбца из имени объекта                        |
//+------------------------------------------------------------------+
int CTable::ColumnIndexFromObjectName(const string object_name)
  {
   ushort u_sep=0;
   string result[];
   int    array_size=0;
//--- Получим код разделителя
   u_sep=::StringGetCharacter("_",0);
//--- Разобьём строку
   ::StringSplit(object_name,u_sep,result);
   array_size=::ArraySize(result)-1;
//--- Проверка выхода за диапазон массива
   if(array_size-3<0)
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      return(WRONG_VALUE);
     }
//--- Вернуть индекс пункта
   return((int)result[array_size-3]);
  }
//+------------------------------------------------------------------+
//| Извлекает индекс ряда из имени объекта                           |
//+------------------------------------------------------------------+
int CTable::RowIndexFromObjectName(const string object_name)
  {
   ushort u_sep=0;
   string result[];
   int    array_size=0;
//--- Получим код разделителя
   u_sep=::StringGetCharacter("_",0);
//--- Разобьём строку
   ::StringSplit(object_name,u_sep,result);
   array_size=::ArraySize(result)-1;
//--- Проверка выхода за диапазон массива
   if(array_size-2<0)
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      return(WRONG_VALUE);
     }
//--- Вернуть индекс пункта
   return((int)result[array_size-2]);
  }

Для выделения строки, когда включен соответствующий режим, предназначен метод CTable::HighlightSelectedItem(). Выделенный ряд будет отличаться другим цветом фона и текста. Эти свойства доступны для изменения пользователем и уже были рассмотрены ранее.

В начале метода (см. листинг кода ниже) стоит две проверки. Программа выходит отсюда, если:

  • включен режим редактирования ячеек таблицы;
  • отключен режим выделения ряда.

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

class CTable : public CElement
  {
private:
   //--- Подсветка выделенного ряда
   void              HighlightSelectedItem(void);
  };
//+------------------------------------------------------------------+
//| Подсветка выделенного ряда                                       |
//+------------------------------------------------------------------+
void CTable::HighlightSelectedItem(void)
  {
//--- Выйти, если один из режимов ("Только чтение", "Выделение строки") отключен
   if(!m_read_only || !m_selectable_row)
      return;
//--- Смещение на один индекс, если включен режим закреплённых заголовков
   int t=(m_fix_first_row) ? 1 : 0;
   int l=(m_fix_first_column) ? 1 : 0;
//--- Получим текущую позицию ползунка горизонтальной полосы прокрутки
   int h=m_scrollh.CurrentPos()+l;
//--- Столбцы
   for(int c=l; c<m_visible_columns_total; c++)
     {
      //--- Получим текущую позицию ползунка вертикальной полосы прокрутки
      int v=m_scrollv.CurrentPos()+t;
      //--- Ряды
      for(int r=t; r<m_visible_rows_total; r++)
        {
         //--- Смещение данных таблицы
         if(v>=t && v<m_rows_total)
           {
            //--- Корректировка с учётом выделенной строки
            color back_color=(m_selected_item==v) ? m_selected_row_color : m_vcolumns[h].m_cell_color[v];
            color text_color=(m_selected_item==v) ? m_selected_row_text_color : m_vcolumns[h].m_text_color[v];
            //--- Корректировка цвета текста и фона ячейки
            m_columns[c].m_rows[r].Color(text_color);
            m_columns[c].m_rows[r].BackColor(back_color);
            v++;
           }
        }
      //---
      h++;
     }
  }

Ещё одним полезным режимом может оказаться подсветка ряда таблицы по наведению курсора мыши. Для этого случая будет использоваться метод CTable::RowColorByHover(). Здесь также есть несколько проверок, при непрохождении которых программа выходит из метода. Выход осуществляется в случаях, если:

  • выключен режим для подсветки ряда при наведении курсора мыши;
  • включен режим редактирования таблицы;
  • форма, к которой присоединён элемент, заблокирована;
  • одна из полос прокрутки находится в действии (перемещение ползунка).

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

class CTable : public CElement
  {
private:
   //--- Изменение цвета ряда таблицы при наведении курсора мыши
   void              RowColorByHover(const int x,const int y);
  };
//+------------------------------------------------------------------+
//| Изменение цвета ряда таблицы при наведении курсора мыши          |
//+------------------------------------------------------------------+
void CTable::RowColorByHover(const int x,const int y)
  {
//--- Выйти, если отключена подсветка ряда при наведении курсора мыши или форма заблокирована
   if(!m_lights_hover || !m_read_only || m_wnd.IsLocked())
      return;
//--- Выйти, если полоса прокрутки в процессе перемещения
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      return;
//--- Смещение на один индекс, если включен режим закреплённых заголовков
   int t=(m_fix_first_row) ? 1 : 0;
   int l=(m_fix_first_column) ? 1 : 0;
//--- Получим текущую позицию ползунка горизонтальной полосы прокрутки
   int h=m_scrollh.CurrentPos()+l;
//--- Столбцы
   for(int c=l; c<m_visible_columns_total; c++)
     {
      //--- Получим текущую позицию ползунка вертикальной полосы прокрутки
      int v=m_scrollv.CurrentPos()+t;
      //--- Ряды
      for(int r=t; r<m_visible_rows_total; r++)
        {
         //--- Проверка для предотвращения выхода из диапазона
         if(v>=t && v<m_rows_total)
           {
            //--- Пропустить, если в режиме "Только чтение", включено выделение ряда и дошли до выделенной строки
            if(m_selected_item==v && m_read_only && m_selectable_row)
              {
               v++;
               continue;
              }
            //--- Подсветить ряд, если курсор над ним
            if(x>m_columns[0].m_rows[r].X() && x<m_columns[m_visible_columns_total-1].m_rows[r].X2() &&
               y>m_columns[c].m_rows[r].Y() && y<m_columns[c].m_rows[r].Y2())
              {
               m_columns[c].m_rows[r].BackColor(m_cell_color_hover);
              }
            //--- Вернуть цвет по умолчанию, если курсор вне области этого ряда
            else
              {
               if(v>=t && v<m_rows_total)
                  m_columns[c].m_rows[r].BackColor(m_vcolumns[h].m_cell_color[v]);
              }
            //---
            v++;
           }
        }
      //---
      h++;
     }
  }

Мы рассмотрели все методы для управления таблицей. С кодом обработчика событий элемента CTable::OnEvent() вы можете подробнее ознакомиться в листинге ниже. Обратите внимание, что метод CTable::HighlightSelectedItem() также используется и после обработки перемещения ползунка вертикальной полосы прокрутки.

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события перемещения курсора
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Выйти, если элемент скрыт
      if(!CElement::IsVisible())
         return;
      //--- Координаты и состояние левой кнопки мыши
      int x=(int)lparam;
      int y=(int)dparam;
      m_mouse_state=(bool)int(sparam);
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      //--- Если полоса прокрутки в действии
      if(m_scrollv.ScrollBarControl(x,y,m_mouse_state) || m_scrollh.ScrollBarControl(x,y,m_mouse_state))
         //--- Смещаем таблицу
         UpdateTable();
      //--- Подсветка выделенного ряда
      HighlightSelectedItem();
      //--- Изменяет цвет ряда таблицы при наведении курсора мыши
      RowColorByHover(x,y);
      return;
     }
//--- Обработка нажатия на объектах
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Если нажали на ряде таблицы
      if(OnClickTableRow(sparam))
        {
         //--- Подсветка выделенного ряда
         HighlightSelectedItem();
         return;
        }
      //--- Если нажали на кнопке полосы прокрутки
      if(m_scrollv.OnClickScrollInc(sparam) || m_scrollv.OnClickScrollDec(sparam) ||
         m_scrollh.OnClickScrollInc(sparam) || m_scrollh.OnClickScrollDec(sparam))
        {
         //--- Обновление данных таблицы с учётом последних изменений
         UpdateTable();
         //--- Подсветка выделенного ряда
         HighlightSelectedItem();
         return;
        }
      return;
     }
//--- Обработка события изменения значения в поле ввода
   if(id==CHARTEVENT_OBJECT_ENDEDIT)
     {
      OnEndEditCell(sparam);
      //--- Сбросить цвета таблицы
      ResetColors();
      return;
     }
  }

 


Тест таблицы из полей ввода

Всё готово для того, чтобы протестировать таблицу из полей ввода. Сделаем копию предыдущего тестового эксперта и удалим из него всё, что связано с таблицей из текстовых меток. Далее в пользовательском классе CProgram создаём экземпляр класса типа CTable, объявляем метод для создания таблицы и отступы от крайней точки формы:

class CProgram : public CWndEvents
  {
private:
   //--- Таблица из полей ввода
   CTable            m_table;
   //---
private:
   //--- Таблица из полей ввода
#define TABLE1_GAP_X          (1)
#define TABLE1_GAP_Y          (42)
   bool              CreateTable(void);
  };

Создадим таблицу, в которой 100 столбцов и 1000 рядов. Видимыми из них будут 6 столбцов и 15 рядов. Закрепим заголовки таблицы (первые ряд и столбец), чтобы они оставались на месте при перемещении ползунков полос прокрутки. Включим режимы для выделения ряда и подсветки рядов при наведении на них курсора мыши. 

После создания элемента заполним массивы таблицы данными и отформатируем её. Например, к тексту в ячейках первого столбца применим способ выравнивания ALIGN_RIGHT (справа). Цвет фона ячеек рядов отформатируем в стиле «зебра», а цвет текста столбцов пусть чередуется двумя цветами – красным и синим. Не забываем обновить таблицу для отображения изменений. 

//+------------------------------------------------------------------+
//| Создаёт таблицу                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateTable(void)
  {
#define COLUMNS1_TOTAL (100)
#define ROWS1_TOTAL    (1000)
//--- Сохраним указатель на форму
   m_table.WindowPointer(m_window1);
//--- Координаты
   int x=m_window1.X()+TABLE1_GAP_X;
   int y=m_window1.Y()+TABLE1_GAP_Y;
//--- Количество видимых столбцов и рядов
   int visible_columns_total =6;
   int visible_rows_total    =15;
//--- Установим свойства перед созданием
   m_table.XSize(600);
   m_table.RowYSize(20);
   m_table.FixFirstRow(true);
   m_table.FixFirstColumn(true);
   m_table.LightsHover(true);
   m_table.SelectableRow(true);
   m_table.TextAlign(ALIGN_CENTER);
   m_table.HeadersColor(C'255,244,213');
   m_table.HeadersTextColor(clrBlack);
   m_table.GridColor(clrLightGray);
   m_table.CellColorHover(clrGold);
   m_table.TableSize(COLUMNS1_TOTAL,ROWS1_TOTAL);
   m_table.VisibleTableSize(visible_columns_total,visible_rows_total);
//--- Создадим элемент управления
   if(!m_table.CreateTable(m_chart_id,m_subwin,x,y))
      return(false);
//--- Заполним таблицу:
//    Первая ячейка пустая
   m_table.SetValue(0,0,"-");
//--- Заголовки для столбцов
   for(int c=1; c<COLUMNS1_TOTAL; c++)
     {
      for(int r=0; r<1; r++)
         m_table.SetValue(c,r,"SYMBOL "+string(c));
     }
//--- Заголовки для рядов, способ выравнивания текста - справа
   for(int c=0; c<1; c++)
     {
      for(int r=1; r<ROWS1_TOTAL; r++)
        {
         m_table.SetValue(c,r,"PARAMETER "+string(r));
         m_table.TextAlign(c,r,ALIGN_RIGHT);
        }
     }
//--- Данные и форматирование таблицы (цвет фона и цвет ячеек)
   for(int c=1; c<COLUMNS1_TOTAL; c++)
     {
      for(int r=1; r<ROWS1_TOTAL; r++)
        {
         m_table.SetValue(c,r,string(c)+":"+string(r));
         m_table.TextColor(c,r,(c%2==0)? clrRed : clrRoyalBlue);
         m_table.CellColor(c,r,(r%2==0)? clrWhiteSmoke : clrWhite);
        }
     }
//--- Обновить таблицу для отображения изменений
   m_table.UpdateTable();
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,m_table);
   return(true);
  }

Вызов метода CProgram::CreateTable() нужно осуществлять в главном методе создания графического интерфейса (см. сокращённую версию в листинге ниже): 

//+------------------------------------------------------------------+
//| Создаёт экспертную панель                                        |
//+------------------------------------------------------------------+
bool CProgram::CreateExpertPanel(void)
  {
//--- Создание формы 1 для элементов управления
//--- Создание элементов управления:
//    Главное меню
//--- Контекстные меню
//--- Создание статусной строки
//--- Таблица из полей ввода
   if(!CreateTable())
      return(false);
//--- Перерисовка графика
   m_chart.Redraw();
   return(true);
  }

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

 Рис. 4. Тест элемента «Таблица из полей ввода».

Рис. 4. Тест элемента «Таблица из полей ввода».

Всё отлично работает. Но если вам понадобится в одной ячейке поместить текст, состоящий более чем из 63 символов, то вы столкнётесь с тем, что текст будет отображаться не полностью. Во всех графических объектах терминала, в которых можно отобразить текст, стоит ограничение на 63 символа. Чтобы обойти это ограничение, нужно использовать класс CCanvas для рисования. В этом классе есть методы для отображения текста, и уже нет никаких ограничений. Мы уже применяли его для рисования некоторых элементов («Разделительная линия» и «Всплывающая подсказка») вот в этих статьях:

Так как 63 символа часто может быть недостаточно для отображения данных, то третий тип таблиц будем рисовать с помощью класса CCanvas

 


Элемент "Нарисованная таблица"

Кроме того, что у нарисованной таблицы не будет ограничений на количество символов в каждой ячейке, открывается ещё одна возможность – настройка ширины каждого столбца без изменения видимой ширины таблицы. В других типах таблиц, которые мы рассмотрели ранее, это было сделать достаточно проблематично. В итоге уже можно отметить несколько преимуществ у нарисованной таблицы перед другими типами этого элемента:

  • нет ограничений на количество символов в каждой ячейке;
  • ширину можно установить отдельно для каждого столбца;
  • для создания таблицы, вместо множества объектов, таких как текстовые метки (OBJ_LABEL) или поля ввода (OBJ_EDIT), используется только один – картинка (OBJ_BITMAP_LABEL).

Перечислим все составные части этого типа таблицы.

  1. Фон
  2. Картинка с нарисованной таблицей
  3. Вертикальная полоса прокрутки
  4. Горизонтальная полоса прокрутки

Рис. 5. Составные части элемента «Нарисованная таблица» 

Рис. 5. Составные части элемента «Нарисованная таблица».

Рассмотрим подробнее, как устроен класс кода для создания этого типа таблицы.

 


Разработка класса CCanvasTable

Для хранения значений и свойств ячеек таблицы создадим структуру CTOptions:

//+------------------------------------------------------------------+
//| Класс для создания нарисованной таблицы                          |
//+------------------------------------------------------------------+
class CCanvasTable : public CElement
  {
private:
   //--- Массив значений и свойства таблицы
   struct CTOptions
     {
      string            m_vrows[];
      int               m_width;
      ENUM_ALIGN_MODE   m_text_align;
     };
   CTOptions         m_vcolumns[];
  };

Инициализация полей структуры CTOptions значениями по умолчанию осуществляется при установке основного размера таблицы (общего количества столбцов и рядов). Ширина всех столбцов пусть будет 100 пикселей, а способ выравнивания текста в ячейках столбцов – ALIGN_CENTER (по центру). 

class CCanvasTable : public CElement
  {
public:
   //--- Устанавливает основной размер таблицы
   void              TableSize(const int columns_total,const int rows_total);
  };
//+------------------------------------------------------------------+
//| Устанавливает размер таблицы                                     |
//+------------------------------------------------------------------+
void CCanvasTable::TableSize(const int columns_total,const int rows_total)
  {
//--- Должно быть не менее одного столбца
   m_columns_total=(columns_total<1) ? 1 : columns_total;
//--- Должно быть не менее двух рядов
   m_rows_total=(rows_total<2) ? 2 : rows_total;
//--- Установить размер массиву столбцов
   ::ArrayResize(m_vcolumns,m_columns_total);
//--- Установить размер массивам рядов
   for(int i=0; i<m_columns_total; i++)
     {
      ::ArrayResize(m_vcolumns[i].m_vrows,m_rows_total);
      //--- Инициализация свойств столбцов значениями по умолчанию
      m_vcolumns[i].m_width      =100;
      m_vcolumns[i].m_text_align =ALIGN_CENTER;
     }
  }

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

class CCanvasTable : public CElement
  {
public:
   //--- Установка (1) режима выравнивания текста и (2) ширины для каждого столбца
   void              TextAlign(const ENUM_ALIGN_MODE &array[]);
   void              ColumnsWidth(const int &array[]);
  };
//+------------------------------------------------------------------+
//| Заполняет массив режимом выравнивания текста                     |
//+------------------------------------------------------------------+
void CCanvasTable::TextAlign(const ENUM_ALIGN_MODE &array[])
  {
   int total=0;
   int array_size=::ArraySize(array);
//--- Выйти, если передан массив нулевого размера
   if(array_size<1)
      return;
//--- Скорректировать значение для предотвращения выхода из диапазона массива 
   total=(array_size<m_columns_total)? array_size : m_columns_total;
//--- Сохранить значения в структуре
   for(int c=0; c<total; c++)
      m_vcolumns[c].m_text_align=array[c];
  }
//+------------------------------------------------------------------+
//| Заполняет массив ширины столбцов                                 |
//+------------------------------------------------------------------+
void CCanvasTable::ColumnsWidth(const int &array[])
  {
   int total=0;
   int array_size=::ArraySize(array);
//--- Выйти, если передан массив нулевого размера
   if(array_size<1)
      return;
//--- Скорректировать значение для предотвращения выхода из диапазона массива 
   total=(array_size<m_columns_total)? array_size : m_columns_total;
//--- Сохранить значения в структуре
   for(int c=0; c<total; c++)
      m_vcolumns[c].m_width=array[c];
  }

До создания таблицы нужно рассчитать её общие размеры, а также размеры видимой её части, относительно заданных параметров (ширина всех столбцов, высота всех рядов и наличие полос прокрутки). Для этой задачи напишем метод CCanvasTable::CalculateTableSize(), код которого представлен в листинге ниже: 

class CCanvasTable : public CElement
  {
private:
   //--- Общий размер и размер видимой части таблицы
   int               m_table_x_size;
   int               m_table_y_size;
   int               m_table_visible_x_size;
   int               m_table_visible_y_size;
//---
private:
   //--- Рассчитывает размеры таблицы
   void              CalculateTableSize(void);
  };
//+------------------------------------------------------------------+
//| Рассчитывает размеры таблицы                                     |
//+------------------------------------------------------------------+
void CCanvasTable::CalculateTableSize(void)
  {
//--- Рассчитаем общую ширину таблицы
   m_table_x_size=0;
   for(int c=0; c<m_columns_total; c++)
      m_table_x_size=m_table_x_size+m_vcolumns[c].m_width;
//--- Ширина таблицы с учётом наличия вертикальной полосы прокрутки
   int x_size=(m_rows_total>m_visible_rows_total) ? m_x_size-m_scrollh.ScrollWidth() : m_x_size-2;
//--- Если ширина всех столбцов меньше ширины таблицы, то будем использовать ширину таблицы
   if(m_table_x_size<m_x_size)
      m_table_x_size=x_size;
//--- Рассчитаем общую высоту таблицы
   m_table_y_size=m_cell_y_size*m_rows_total-(m_rows_total-1);
//--- Зададим размер фрейма для показа фрагмента изображения (видимой части таблицы таблицы)
   m_table_visible_x_size=x_size;
   m_table_visible_y_size=m_cell_y_size*m_visible_rows_total-(m_visible_rows_total-1);
//--- Если есть горизонтальная полоса прокрутки, то скорректировать размер элемента по оси Y
   int y_size=m_cell_y_size*m_visible_rows_total+2-(m_visible_rows_total-1);
   m_y_size=(m_table_x_size>m_table_visible_x_size) ? y_size+m_scrollh.ScrollWidth()-1 : y_size;
  }

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

class CCanvasTable : public CElement
  {
private:
   //--- Цвет сетки
   color             m_grid_color;
   //--- Размер (высота) ячеек
   int               m_cell_y_size;
//---
public:
   //--- Цвет сетки
   void              GridColor(const color clr)           { m_grid_color=clr;                }
//---
private:
   //--- Рисует сетку
   void              DrawGrid(void);
  };
//+------------------------------------------------------------------+
//| Рисует сетку                                                     |
//+------------------------------------------------------------------+
void CCanvasTable::DrawGrid(void)
  {
//--- Цвет сетки
   uint clr=::ColorToARGB(m_grid_color,255);
//--- Размер холста для рисования
   int x_size =m_canvas.XSize()-1;
   int y_size =m_canvas.YSize()-1;
//--- Координаты
   int x1=0,x2=0,y1=0,y2=0;
//--- Горизонтальные линии
   x1=0;
   y1=0;
   x2=x_size;
   y2=0;
   for(int i=0; i<=m_rows_total; i++)
     {
      m_canvas.Line(x1,y1,x2,y2,clr);
      y2=y1+=m_cell_y_size-1;
     }
//--- Вертикальные линии
   x1=0;
   y1=0;
   x2=0;
   y2=y_size;
   for(int i=0; i<m_columns_total; i++)
     {
      m_canvas.Line(x1,y1,x2,y2,clr);
      x2=x1+=m_vcolumns[i].m_width;
     }
//--- Справа
   x1=x_size;
   y1=0;
   x2=x_size;
   y2=y_size;
   m_canvas.Line(x1,y1,x2,y2,clr);
  }

Метод CCanvasTable::DrawText() для рисования текста сложнее, чем метод для рисования сетки. Здесь нужно учитывать не только способ выравнивания текста текущего столбца, но также и предыдущего, чтобы правильно рассчитывать отступы. Для способов выравнивания слева и справа установим отступ от края ячейки в 10 пикселей. Отступ от верхнего края ячейки зададим в 3 пикселя. Более подробно с кодом этого метода можете ознакомиться в листинге ниже: 

class CCanvasTable : public CElement
  {
private:
   //--- Цвет текста
   color             m_cell_text_color;
//---
public:
   //--- Цвет текста таблицы
   void              TextColor(const color clr)           { m_cell_text_color=clr;           }
//---
private:
   //--- Рисует текст
   void              DrawText(void);
  };
//+------------------------------------------------------------------+
//| Рисует текст                                                     |
//+------------------------------------------------------------------+
void CCanvasTable::DrawText(void)
  {
//--- Для расчёта координат и отступов
   int  x             =0;
   int  y             =0;
   uint text_align    =0;
   int  column_offset =0;
   int  cell_x_offset =10;
   int  cell_y_offset =3;
//--- Цвет текста
   uint clr=::ColorToARGB(m_cell_text_color,255);
//--- Свойства шрифта
   m_canvas.FontSet(FONT,-80,FW_NORMAL);
//--- Столбцы
   for(int c=0; c<m_columns_total; c++)
     {
      //--- Расчёты отступа для первого столбца
      if(c==0)
        {
         //--- Выравнивание текста в ячейках по установленному режиму для каждого столбца
         switch(m_vcolumns[0].m_text_align)
           {
            //--- По центру
            case ALIGN_CENTER :
               column_offset=column_offset+m_vcolumns[0].m_width/2;
               x=column_offset;
               break;
               //--- Cправа
            case ALIGN_RIGHT :
               column_offset=column_offset+m_vcolumns[0].m_width;
               x=column_offset-cell_x_offset;
               break;
               //--- Слева
            case ALIGN_LEFT :
               x=column_offset+cell_x_offset;
               break;
           }
        }
      //--- Расчёты отступов для всех столбцов кроме первого
      else
        {
         //--- Выравнивание текста в ячейках по установленному режиму для каждого столбца
         switch(m_vcolumns[c].m_text_align)
           {
            //--- По центру
            case ALIGN_CENTER :
               //--- Расчёт отступа относительно выравнивания в предыдущем столбце
               switch(m_vcolumns[c-1].m_text_align)
                 {
                  case ALIGN_CENTER :
                     column_offset=column_offset+(m_vcolumns[c-1].m_width/2)+(m_vcolumns[c].m_width/2);
                     break;
                  case ALIGN_RIGHT :
                     column_offset=column_offset+(m_vcolumns[c].m_width/2);
                     break;
                  case ALIGN_LEFT :
                     column_offset=column_offset+m_vcolumns[c-1].m_width+(m_vcolumns[c].m_width/2);
                     break;
                 }
               //---
               x=column_offset;
               break;
               //--- Справа
            case ALIGN_RIGHT :
               //--- Расчёт отступа относительно выравнивания в предыдущем столбце
               switch(m_vcolumns[c-1].m_text_align)
                 {
                  case ALIGN_CENTER :
                     column_offset=column_offset+(m_vcolumns[c-1].m_width/2)+m_vcolumns[c].m_width;
                     x=column_offset-cell_x_offset;
                     break;
                  case ALIGN_RIGHT :
                     column_offset=column_offset+m_vcolumns[c].m_width;
                     x=column_offset-cell_x_offset;
                     break;
                  case ALIGN_LEFT :
                     column_offset=column_offset+m_vcolumns[c-1].m_width+m_vcolumns[c].m_width;
                     x=column_offset-cell_x_offset;
                     break;
                 }
               //---
               break;
               //--- Слева
            case ALIGN_LEFT :
               //--- Расчёт отступа относительно выравнивания в предыдущем столбце
               switch(m_vcolumns[c-1].m_text_align)
                 {
                  case ALIGN_CENTER :
                     column_offset=column_offset+(m_vcolumns[c-1].m_width/2);
                     x=column_offset+cell_x_offset;
                     break;
                  case ALIGN_RIGHT :
                     x=column_offset+cell_x_offset;
                     break;
                  case ALIGN_LEFT :
                     column_offset=column_offset+m_vcolumns[c-1].m_width;
                     x=column_offset+cell_x_offset;
                     break;
                 }
               //---
               break;
           }
        }
      //--- Ряды
      for(int r=0; r<m_rows_total; r++)
        {
         //---
         y+=(r>0) ? m_cell_y_size-1 : cell_y_offset;
         //---
         switch(m_vcolumns[c].m_text_align)
           {
            case ALIGN_CENTER :
               text_align=TA_CENTER|TA_TOP;
               break;
            case ALIGN_RIGHT :
               text_align=TA_RIGHT|TA_TOP;
               break;
            case ALIGN_LEFT :
               text_align=TA_LEFT|TA_TOP;
               break;
           }
         //--- Нарисовать текст
         m_canvas.TextOut(x,y,m_vcolumns[c].m_vrows[r],clr,text_align);
        }
      //--- Обнулить координату Y для следующего цикла
      y=0;
     }
  }

В первых двух типах таблиц, которые мы рассмотрели, смещение данных с помощью полос прокрутки осуществлялось методом изменения значений в объектах (текстовые метки и поля ввода) видимой части таблицы. Здесь же мы будем использовать метод смещения прямоугольной области видимости изображения. То есть, изначально размеры таблицы (которая в данном случае является картинкой) равны сумме ширины всех столбцов и высоты всех рядов. Это размеры исходного изображения, в пределах которого можно перемещать область видимости. Размеры области видимости можно изменить в любой момент, но здесь они будут заданы сразу после создания элемента в методе CCanvasTable::CreateCells(). 

Установить размер области видимости можно с помощью свойств OBJPROP_XSIZE и OBJPROP_YSIZE, а осуществить смещение этой области (в пределах исходного изображения) можно с помощью свойств OBJPROP_XOFFSET и OBJPROP_YOFFSET (см. пример в листинге кода ниже): 

//--- Установим размеры видимой области
   ::ObjectSetInteger(m_chart_id,name,OBJPROP_XSIZE,m_table_visible_x_size);
   ::ObjectSetInteger(m_chart_id,name,OBJPROP_YSIZE,m_table_visible_y_size);
//--- Зададим смещение фрейма внутри изображения по осям X и Y
   ::ObjectSetInteger(m_chart_id,name,OBJPROP_XOFFSET,0);
   ::ObjectSetInteger(m_chart_id,name,OBJPROP_YOFFSET,0);

Для смещения области видимости относительно текущего положения ползунков полос прокрутки напишем простой метод CCanvasTable::ShiftTable(). Смещение по вертикали будет осуществляться шагом, равным высоте ряда, а по горизонтали – попиксельно (см. листинг кода ниже): 

class CCanvasTable : public CElement
  {
public:
   //--- Смещение таблицы относительно позиций полос прокрутки
   void              ShiftTable(void);
  };
//+------------------------------------------------------------------+
//| Сдвигает таблицу относительно полос прокрутки                    |
//+------------------------------------------------------------------+
void CCanvasTable::ShiftTable(void)
  {
//--- Получим текущие позиции ползунков горизонтальной и вертикальной полос прокрутки
   int h=m_scrollh.CurrentPos();
   int v=m_scrollv.CurrentPos();
//--- Расчёт положения таблицы относительно ползунков полос прокрутки
   long c=h;
   long r=v*(m_cell_y_size-1);
//--- Смещение таблицы
   ::ObjectSetInteger(m_chart_id,m_canvas.Name(),OBJPROP_XOFFSET,c);
   ::ObjectSetInteger(m_chart_id,m_canvas.Name(),OBJPROP_YOFFSET,r);
  }

Код общего метода CCanvasTable::DrawTable() для рисования таблицы тогда будет таким, как это показано в листинге ниже: 

class CCanvasTable : public CElement
  {
public:
   //--- Рисует таблицу с учётом последних внесённых изменений
   void              DrawTable(void);
  };
//+------------------------------------------------------------------+
//| Рисует таблицу                                                   |
//+------------------------------------------------------------------+
void CCanvasTable::DrawTable(void)
  {
//--- Сделать фон прозрачным
   m_canvas.Erase(::ColorToARGB(clrNONE,0));
//--- Нарисовать сетку
   DrawGrid();
//--- Нарисовать текст
   DrawText();
//--- Отобразить последние нарисованные изменения
   m_canvas.Update();
//--- Смещение таблицы относительно полос прокрутки
   ShiftTable();
  }

Всё готово для теста этого типа таблицы. 

 


Тест нарисованной таблицы

Для теста сделаем копию предыдущего тестового эксперта и удалим из него всё, что связано с таблицей типа CTable. В пользовательском классе CProgram нужно создать экземпляр класса типа CCanvasTable, объявить (1) метод CProgram::CreateCanvasTable() для создания таблицы и (2) отступы от крайней точки формы, как это показано в листинге кода ниже:

class CProgram : public CWndEvents
  {
private:
   //--- Нарисованная таблица
   CCanvasTable      m_canvas_table;
   //---
private:
   //--- Нарисованная таблица
#define TABLE1_GAP_X          (1)
#define TABLE1_GAP_Y          (42)
   bool              CreateCanvasTable(void);
  };

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

Для примера сделаем ширину всех столбцов (кроме первых двух) равной 70 пикселям. Для первого и второго столбца установим ширину 100 и 90 пикселей соответственно. Во всех столбцах способ выравнивания текста пусть будет по центру, кроме первого (по правому краю), второго (по левому краю) и третьего (по правому краю). Полный код метода CProgram::CreateCanvasTable() смотрите в листинге ниже:

//+------------------------------------------------------------------+
//| Создаёт нарисованную таблицу                                     |
//+------------------------------------------------------------------+
bool CProgram::CreateCanvasTable(void)
  {
#define COLUMNS1_TOTAL 15
#define ROWS1_TOTAL    1000
//--- Сохраним указатель на форму
   m_canvas_table.WindowPointer(m_window1);
//--- Координаты
   int x=m_window1.X()+TABLE1_GAP_X;
   int y=m_window1.Y()+TABLE1_GAP_Y;
//--- Количество видимых строк
   int visible_rows_total=16;
//--- Массив ширины столбцов
   int width[COLUMNS1_TOTAL];
   ::ArrayInitialize(width,70);
   width[0]=100;
   width[1]=90;
//--- Массив выравнивания текста в столбцах
   ENUM_ALIGN_MODE align[COLUMNS1_TOTAL];
   ::ArrayInitialize(align,ALIGN_CENTER);
   align[0]=ALIGN_RIGHT;
   align[1]=ALIGN_LEFT;
   align[2]=ALIGN_RIGHT;
//--- Установим свойства перед созданием
   m_canvas_table.XSize(601);
   m_canvas_table.TableSize(COLUMNS1_TOTAL,ROWS1_TOTAL);
   m_canvas_table.VisibleTableSize(0,visible_rows_total);
   m_canvas_table.TextAlign(align);
   m_canvas_table.ColumnsWidth(width);
   m_canvas_table.GridColor(clrLightGray);
//--- Заполним таблицу данными
   for(int c=0; c<COLUMNS1_TOTAL; c++)
      for(int r=0; r<ROWS1_TOTAL; r++)
         m_canvas_table.SetValue(c,r,string(c)+":"+string(r));
//--- Создадим элемент управления
   if(!m_canvas_table.CreateTable(m_chart_id,m_subwin,x,y))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,m_canvas_table);
   return(true);
  }

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

//+------------------------------------------------------------------+
//| Создаёт торговую панель                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateExpertPanel(void)
  {
//--- Создание формы 1 для элементов управления
//--- Создание элементов управления:
//    Главное меню
//--- Контекстные меню
//--- Создание статусной строки
//--- Создание нарисованной таблицы
   if(!CreateCanvasTable())
      return(false);
//--- Перерисовка графика
   m_chart.Redraw();
   return(true);
  }

Теперь нужно скомпилировать программу и запустить её на графике. Результат показан на скриншоте ниже:

 Рис. 6. Тест элемента «Нарисованная таблица» в эксперте.

Рис. 6. Тест элемента «Нарисованная таблица» в эксперте.

В пятой главе первой части серии мы тестировали установку формы в скриптах. Таблицы без полос прокрутки можно использовать в этом типе MQL-приложений. Для примера добавим на форму в скрипте нарисованную таблицу и будем обновлять данные в ней каждые 250 миллисекунд. Добавьте в пользовательский класс приложения код для создания таблицы, как это было показано ранее. Кроме этого, в обработчик событий скрипта CProgram::OnEvent() нужно добавить код, как это показано в листинге ниже. Теперь через заданный временной интервал данные во втором столбце будут изменяться. 

//+------------------------------------------------------------------+
//| События                                                          |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int milliseconds)
  {
   static int count =0;  // Счётчик
   string     str   =""; // Строка для заголовка
//--- Формирование заголовка с ходом процесса
   switch(count)
     {
      case 0 : str="SCRIPT PANEL";     break;
      case 1 : str="SCRIPT PANEL .";   break;
      case 2 : str="SCRIPT PANEL ..";  break;
      case 3 : str="SCRIPT PANEL ..."; break;
     }
//--- Обновить строку заголовка
   m_window.CaptionText(str);
//--- Изменить данные в первом столбце таблицы
   for(int r=0; r<13; r++)
      m_canvas_table.SetValue(1,r,string(::rand()));
//--- Отобразить новые данные таблицы
   m_canvas_table.DrawTable();
//--- Перерисовка графика
   m_chart.Redraw();
//--- Увеличим счётчик
   count++;
//--- Если больше трёх, обнулить
   if(count>3)
      count=0;
//--- Пауза
   ::Sleep(milliseconds);
  }

Скомпилируйте программу и запустите скрипт на графике. Должен получиться такой результат: 

 Рис. 7. Тест элемента «Нарисованная таблица» в скрипте.

Рис. 7. Тест элемента «Нарисованная таблица» в скрипте.

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

 


Заключение

В этой статье были представлены целых три класса для создания таких важных элементов интерфейса, как «Таблицы». Каждый из этих классов имеет свои уникальные возможности, каждая из которых подходит для наилучшего решения определенных задач. Так, например, класс CTable даёт возможность создать таблицу с редактируемыми полями ввода, при этом таблицу можно отформатировать, представив её в более дружелюбном и понятном виде для конечного пользователя. Используя для создания таблицы класс CCanvasTable, вы не столкнётесь с проблемой ограничения количества символов в ячейках, а технология смещения таблицы в области видимости изображения позволяет устанавливать столбцам разную ширину. Это не окончательные версии таблиц, и их можно и нужно развивать дальше.

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

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

Список статей (глав) седьмой части:


Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (18)
Artyom Trishkin
Artyom Trishkin | 8 июн. 2017 в 21:06
traderEvgen:

даа, видел. Но там для mql5, мне еще mql4 нужно 

Библиотека работает под МТ4. Небольшие изменения, и ничем не отличается от МТ5 за исключением некоторых чисто МТ5-фишек как объекты-графики и прочие вкусности.

Таблицы точно в МТ4 работают так же, как и в МТ5

traderEvgen
traderEvgen | 12 июн. 2017 в 13:42
Artyom Trishkin:

Библиотека работает под МТ4. Небольшие изменения, и ничем не отличается от МТ5 за исключением некоторых чисто МТ5-фишек как объекты-графики и прочие вкусности.

Таблицы точно в МТ4 работают так же, как и в МТ5

как это исправить ?

1



123

Artyom Trishkin
Artyom Trishkin | 12 июн. 2017 в 17:41
traderEvgen:

как это исправить ?




Одни ошибки вытекают из других. Править нужно вкупе всю библиотеку.
traderEvgen
traderEvgen | 12 июн. 2017 в 17:55
Artyom Trishkin:
Одни ошибки вытекают из других. Править нужно вкупе всю библиотеку.

вот уже пол дня этим занимаюсь 
Как копировать сигналы с помощью советника по своим правилам? Как копировать сигналы с помощью советника по своим правилам?
При подписке на сигналы может возникнуть такая ситуация: у Вашего торгового счёта кредитное плечо 1:100, провайдер имеет кредитное плечо 1:500 и торгует минимальным лотом, а Ваши торговые балансы практически равны — при этом коэффициент копирования будет от 10% до 15%. Эта статья расскажет, как в таком случае увеличить коэффициент копирования.
Создаем помощника в ручной торговле Создаем помощника в ручной торговле
Количество торговых роботов для работы на валютных рынках в последнее время растет как снежный ком. В них закладываются различные концепции и стратегии, но беспроигрышный образец искусственного интеллекта создать еще никому не удалось. Поэтому многие трейдеры остаются приверженцами ручной торговли. Но и для таких специалистов создаются роботизированные помощники, так называемые торговые панели. Данная статья — еще один пример создания торговой панели "с нуля".
Доработка тестера стратегий для оптимизации индикаторов на примерах тренда и флета Доработка тестера стратегий для оптимизации индикаторов на примерах тренда и флета
При торговле по различным стратегиям зачастую требуется определить, трендовый сейчас рынок или флетовый. С этой целью разрабатывается множество индикаторов. Но как определить, справится ли индикатор с поставленной задачей? Как выяснить средний диапазон состояний флета и тренда для определения наших стопов и целей? В настоящей статье предлагается использовать для этого тестер стратегий, тем самым продемонстрировав, что он годится не только для оптимизации роботов под определенные нужды. В качестве тестового индикатора используем давно известный нам ADX.
Графические интерфейсы VI: Элементы "Слайдер" и "Двухсторонний слайдер" (Глава 2) Графические интерфейсы VI: Элементы "Слайдер" и "Двухсторонний слайдер" (Глава 2)
В предыдущей статье разрабатываемая библиотека была пополнена сразу четырьмя довольно часто используемыми в графических интерфейсах элементами управления: «чекбокс», «поле ввода», «поле ввода с чекбоксом» и «комбобокс с чекбоксом». Вторая глава шестой части серии будет посвящена таким элементам управления, как слайдер и двухсторонний слайдер.