Графические интерфейсы X: Новые возможности для нарисованной таблицы (build 9)

Anatoli Kazharski | 15 февраля, 2017


Содержание


Введение

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

До сих пор самым развитым типом таблиц в разрабатываемой библиотеке был тип CTable. Эта таблица собирается из полей ввода типа OBJ_EDIT, и дальнейшее её развитие уже проблематично. Например, сложно реализовать удобное ручное изменение ширины столбцов с захватом границ заголовков, ведь мы не можем управлять областью видимости отдельных графических объектов таблицы. Здесь мы уже достигли предела возможностей.

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

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

 Рис. 1. Предыдущая версия нарисованной таблицы.

Рис. 1. Предыдущая версия нарисованной таблицы.

Исправим ситуацию и дополним нарисованную таблицу новыми возможностями. В текущем обновлении займемся следующими функциями:


Форматирование в стиле "Зебра"

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

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

//+------------------------------------------------------------------+
//| Класс для создания нарисованной таблицы                          |
//+------------------------------------------------------------------+
class CCanvasTable : public CElement
  {
private:
   //--- Режим форматирования в стиле "Зебра"
   color             m_is_zebra_format_rows;
   //---
public:
   //--- Форматирование в стиле "Зебра"
   void              IsZebraFormatRows(const color clr)   { m_is_zebra_format_rows=clr;      }
  };

Методы для визуализации этого стиля отличаются в таблицах разных типов. В случае CCanvasTable в обычном режиме фон таблицы (холст для рисования) полностью закрашивается общим цветом ячеек. При включении режима "Зебра" начинает работать цикл. На каждой его итерации рассчитываются координаты для каждой строки, и области поочерёдно закрашиваются двумя цветами. За это отвечает метод FillRectangle(), который используется для рисования закрашенных прямоугольников

class CCanvasTable : public CElement
  {
public:
   //--- Рисует фон строк таблицы
   void              DrawRows(void);
  };
//+------------------------------------------------------------------+
//| Рисует фон строк таблицы                                         |
//+------------------------------------------------------------------+
void CCanvasTable::DrawRows(void)
  {
//--- Если режим форматирования в стиле "Зебра" отключен
   if(m_is_zebra_format_rows==clrNONE)
     {
      //--- Закрасить холст одним цветом
      m_table.Erase(::ColorToARGB(m_cell_color));
      return;
     }
//--- Координаты заголовков
   int x1=0,x2=m_table_x_size;
   int y1=0,y2=0;
//--- Форматирование в стиле "Зебра"
   for(int r=0; r<m_rows_total; r++)
     {
      //--- Расчёт координат
      y1=(r*m_cell_y_size)-r;
      y2=y1+m_cell_y_size;
      //--- Цвет строки
      uint clr=::ColorToARGB((r%2!=0)? m_is_zebra_format_rows : m_cell_color);
      //--- Нарисовать фон строки
      m_table.FillRectangle(x1,y1,x2,y2,clr);
     }
  }

Цвета для строк можно установить на свое усмотрение. В итоге нарисованная таблица в режиме "Зебра" будет выглядеть так:

 Рис. 2. Нарисованная таблица в режиме форматирования в стиле "Зебра".

Рис. 2. Нарисованная таблица в режиме форматирования в стиле "Зебра". 

 


Выделение и снятие выделения строк таблицы

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

class CCanvasTable : public CElement
  {
private:
   //--- Цвет (1) фона и (2) текста выделенной строки
   color             m_selected_row_color;
   color             m_selected_row_text_color;
   //--- (1) Индекс и (2) текст выделенной строки
   int               m_selected_item;
   string            m_selected_item_text;
   //---
public:
   //--- Возвращает (1) индекс и (2) текст выделенной строки в таблице
   int               SelectedItem(void)             const { return(m_selected_item);         }
   string            SelectedItemText(void)         const { return(m_selected_item_text);    }
   //---
private:
   //--- Рисует фон строк таблицы
   void              DrawRows(void);
  };

Режим выделяемости строки можно включить/отключить методом CCanvasTable::SelectableRow():

class CCanvasTable : public CElement
  {
private:
   //--- Режим выделяемой строки
   bool              m_selectable_row;
   //---
public:
   //--- Выделение строки
   void              SelectableRow(const bool flag)       { m_selectable_row=flag;           }
  };

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

class CCanvasTable : public CElement
  {
private:
   //--- Рисует выделенную строку
   void              DrawSelectedRow(void);
  };
//+------------------------------------------------------------------+
//| Рисует выделенную строку                                         |
//+------------------------------------------------------------------+
void CCanvasTable::DrawSelectedRow(void)
  {
//--- Зададим начальные координаты для проверки условия
   int y_offset=(m_selected_item*m_cell_y_size)-m_selected_item;
//--- Координаты
   int x1=0,x2=0,y1=0,y2=0;
//---
   x1=0;
   y1=y_offset;
   x2=m_table_x_size;
   y2=y_offset+m_cell_y_size-1;
//--- Нарисовать прямоугольник с заливкой
   m_table.FillRectangle(x1,y1,x2,y2,::ColorToARGB(m_selected_row_color));
  }

При перерисовке текста используется вспомогательный метод CCanvasTable::TextColor(), который определяет цвет текста в ячейках: 

class CCanvasTable : public CElement
  {
private:
   //--- Возвращает цвет текста ячейки
   uint              TextColor(const int row_index);
  };
//+------------------------------------------------------------------+
//| Возвращает цвет текста ячейки                                    |
//+------------------------------------------------------------------+
uint CCanvasTable::TextColor(const int row_index)
  {
   uint clr=::ColorToARGB((row_index==m_selected_item)? m_selected_row_text_color : m_cell_text_color);
//--- Вернуть цвет заголовка
   return(clr);
  }

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

Здесь в начале метода нужно пройти несколько проверок. Программа выйдет из метода, если:

Если проверки пройдены, то чтобы вычислить координату нажатия, нужно получить текущее смещение от крайней точки холста и Y-координату курсора мыши. После этого в цикле определяем, на какой строке было нажатие. После того, как строка найдена, нужно проверить, не выделена ли она сейчас, и если это так — снять выделение. Если строка выделяется, нужно запомнить ее индекс и текст из первого столбца. После завершения поиска строки в цикле таблица перерисовывается. Отправляется сообщение, которое содержит:

class CCanvasTable : public CElement
  {
private:
   //--- Обработка нажатия на элемент
   bool              OnClickTable(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на элемент                                     |
//+------------------------------------------------------------------+
bool CCanvasTable::OnClickTable(const string clicked_object)
  {
//--- Выйти, если отключен режим выделения строки
   if(!m_selectable_row)
      return(false);
//--- Выйдем, если полоса прокрутки в активном режиме
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      return(false);
//--- Выйдем, если чужое имя объекта
   if(m_table.Name()!=clicked_object)
      return(false);
//--- Получим смещение по оси X и Y
   int xoffset=(int)m_table.GetInteger(OBJPROP_XOFFSET);
   int yoffset=(int)m_table.GetInteger(OBJPROP_YOFFSET);

//--- Определим координаты на поле ввода под курсором мыши
   int y=m_mouse.Y()-m_table.Y()+yoffset;
//--- Определим строку, на которую нажали
   for(int r=0; r<m_rows_total; r++)
     {
      //--- Зададим начальные координаты для проверки условия
      int y_offset=(r*m_cell_y_size)-r;
      //--- Проверка условия по оси Y
      bool y_pos_check=(y>=y_offset && y<y_offset+m_cell_y_size);

      //--- Если нажатие было не на этой строке, перейти к следующей
      if(!y_pos_check)
         continue;
      //--- Если нажали на уже выделенной строке, снять выделение
      if(r==m_selected_item)
        {
         m_selected_item      =WRONG_VALUE;
         m_selected_item_text ="";
         break;
        }
      //--- Сохраним индекс строки
      m_selected_item      =r;
      m_selected_item_text =m_vcolumns[0].m_vrows[r];
      break;
     }
//--- Нарисуем таблицу
   DrawTable();
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,m_selected_item_text);
   return(true);
  }

Нарисованная таблица с выделенной строкой выглядит так:

Рис. 3. Демонстрация выделения и снятия выделения ряда нарисованной таблицы.

Рис. 3. Демонстрация выделения и снятия выделения строки нарисованной таблицы. 

 

Заголовки для столбцов

Любая таблица без заголовков столбцов выглядит как сырая заготовка. В этом типе таблиц заголовки тоже будут рисоваться, но на отдельном холсте. Для этого включаем в класс CCanvasTable ещё один экземпляр класса CRectCanvas  и создаём отдельный метод для создания холста. Не будем приводить здесь код этого метода: он почти такой же, как и для создания таблицы. Отличие — только в размерах, которые мы задаем, и в месторасположении объекта.

class CCanvasTable : public CElement
  {
private:
   //--- Объекты для создания таблицы
   CRectCanvas       m_headers;
   //---
private:
   bool              CreateHeaders(void);
  };

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

Поля и методы, относящиеся к этим свойствам: 

class CCanvasTable : public CElement
  {
private:
   //--- Режим показа заголовков таблицы
   bool              m_show_headers;
   //--- Размер (высота) заголовков
   int               m_header_y_size;
   //--- Цвет заголовков (фон) в разных состояниях
   color             m_headers_color;
   color             m_headers_color_hover;
   color             m_headers_color_pressed;
   //--- Цвет текста заголовков
   color             m_headers_text_color;
   //---
public:
   //--- (1) Режим показа заголовков, высота (2) заголовков
   void              ShowHeaders(const bool flag)         { m_show_headers=flag;             }
   void              HeaderYSize(const int y_size)        { m_header_y_size=y_size;          }
   //--- Цвета (1) фона и (2) текста заголовков
   void              HeadersColor(const color clr)        { m_headers_color=clr;             }
   void              HeadersColorHover(const color clr)   { m_headers_color_hover=clr;       }
   void              HeadersColorPressed(const color clr) { m_headers_color_pressed=clr;     }
   void              HeadersTextColor(const color clr)    { m_headers_text_color=clr;        }
  };

Нужен метод, которым  устанавливаются имена заголовков. Кроме этого, понадобится массив, в котором будут храниться эти значения. Размер массива равен количеству столбцов и будет устанавливаться в том же методе CCanvasTable::TableSize() при установке размеров таблицы. 

class CCanvasTable : public CElement
  {
private:
   //--- Текст заголовков
   string            m_header_text[];
   //---
public:
   //--- Установка текста в указанный заголовок
   void              SetHeaderText(const int column_index,const string value);
  };
//+------------------------------------------------------------------+
//| Заполняет массив заголовков по указанному индексу                |
//+------------------------------------------------------------------+
void CCanvasTable::SetHeaderText(const uint column_index,const string value)
  {
//--- Проверка на выход из диапазона столбцов
   uint csize=::ArraySize(m_vcolumns);
   if(csize<1 || column_index>=csize)
      return;
//--- Установить значение в массив
   m_header_text[column_index]=value;
  }

Выравнивание текста в ячейках и заголовках будет производиться с помощью общего метода CCanvasTable::TextAlign(). Способ выравнивания в ячейках таблицы и в заголовках по оси X совпадает, а по оси Y задаётся переданным значением. В данной версии текст в заголовках по оси Y будет позиционироваться по центру — TA_VCENTER, а в ячейках будет регулироваться отступ от верхнего края ячейки — TA_TOP

class CCanvasTable : public CElement
  {
private:
   //--- Возвращает способ выравнивания текста в указанном столбце
   uint              TextAlign(const int column_index,const uint anchor);
  };
//+------------------------------------------------------------------+
//| Возвращает способ выравнивания текста в указанном столбце        |
//+------------------------------------------------------------------+
uint CCanvasTable::TextAlign(const int column_index,const uint anchor)
  {
   uint text_align=0;
//--- Выравнивание текста для текущего столбца
   switch(m_vcolumns[column_index].m_text_align)
     {
      case ALIGN_CENTER :
         text_align=TA_CENTER|anchor;
         break;
      case ALIGN_RIGHT :
         text_align=TA_RIGHT|anchor;
         break;
      case ALIGN_LEFT :
         text_align=TA_LEFT|anchor;
         break;
     }
//--- Вернуть способ выравнивания
   return(text_align);
  }

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

Рис. 4. Указатель курсора мыши при наведении на границу стыков заголовков.

Рис. 4. Указатель курсора мыши при наведении на границу стыков заголовков.

 

Подготовим такую же картинку и для разрабатываемой библиотеки. В архиве в конце статьи можно скачать папку со всеми изображениями элементов управления библиотеки. В файл Enums.mqh добавим новые идентификаторы в перечисление указателей ENUM_MOUSE_POINTER для изменения размеров по оси X и Y

//+------------------------------------------------------------------+
//| Перечисление типов указателей                                    |
//+------------------------------------------------------------------+
enum ENUM_MOUSE_POINTER
  {
   MP_CUSTOM            =0,
   MP_X_RESIZE          =1,
   MP_Y_RESIZE          =2,
   MP_XY1_RESIZE        =3,
   MP_XY2_RESIZE        =4,
   MP_X_RESIZE_RELATIVE =5,
   MP_Y_RESIZE_RELATIVE =6,

   MP_X_SCROLL          =7,
   MP_Y_SCROLL          =8,
   MP_TEXT_SELECT       =9
  };

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

//+------------------------------------------------------------------+
//|                                                      Pointer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Ресурсы
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp"

//+------------------------------------------------------------------+
//| Класс для создания указателя курсора мыши                        |
//+------------------------------------------------------------------+
class CPointer : public CElement
  {
private:
   //--- Установка картинок для указателя курсора мыши
   void              SetPointerBmp(void);
  };
//+------------------------------------------------------------------+
//| Установка картинок для указателя по типу указателя               |
//+------------------------------------------------------------------+
void CPointer::SetPointerBmp(void)
  {
   switch(m_type)
     {
      ...
      case MP_X_RESIZE_RELATIVE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp";
         break;
      case MP_Y_RESIZE_RELATIVE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp";
         break;

      ...
     }
//--- Если указан пользовательский тип (MP_CUSTOM)
   if(m_file_on=="" || m_file_off=="")
      ::Print(__FUNCTION__," > Для указателя курсора должны быть установлены обе картинки!");
  }

Ещё здесь понадобятся дополнительные поля:

class CCanvasTable : public CElement
  {
private:
   //--- Для определения момента перехода курсора мыши с одного заголовка на другой
   int               m_prev_header_index_focus;
   //--- Состояние захвата границы заголовка для изменения ширины столбца
   int               m_column_resize_control;
  };

Текущий цвет для заголовка в зависимости от текущего режима, положения курсора мыши и состояния левой кнопки мыши можно получить с помощью метода CCanvasTable::HeaderColorCurrent(). Фокус над заголовком будет определяться в методе CCanvasTable::DrawHeaders(), предназначенном для рисования фона заголовков, и передаваться сюда как результат проверки.

class CCanvasTable : public CElement
  {
private:
   //--- Возвращает текущий цвет фона заголовка
   uint              HeaderColorCurrent(const bool is_header_focus);
  };
//+------------------------------------------------------------------+
//| Возвращает текущий цвет фона заголовка                           |
//+------------------------------------------------------------------+
uint CCanvasTable::HeaderColorCurrent(const bool is_header_focus)
  {
   uint clr=clrNONE;
//--- Если нет фокуса
   if(!is_header_focus || !m_headers.MouseFocus())
      clr=m_headers_color;
   else
     {
      //--- Если левая кнопка мыши нажата и не в процессе изменения ширины столбца
      bool condition=(m_mouse.LeftButtonState() && m_column_resize_control==WRONG_VALUE);
      clr=(condition)? m_headers_color_pressed : m_headers_color_hover;
     }
//--- Вернуть цвет заголовка
   return(::ColorToARGB(clr));
  }

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

class CCanvasTable : public CElement
  {
private:
   //--- Отступ от границ разделительных линий для показа указателя мыши в режиме изменения ширины столбцов
   int               m_sep_x_offset;
   //---
private:
   //--- Рисует заголовки
   void              DrawHeaders(void);
  };
//+------------------------------------------------------------------+
//| Рисует фон заголовков                                            |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeaders(void)
  {
//--- Если не в фокусе, сбросить цвет заголовков
   if(!m_headers.MouseFocus())
     {
      m_headers.Erase(::ColorToARGB(m_headers_color));
      return;
     }
//--- Для проверки фокуса над заголовками
   bool is_header_focus=false;
//--- Координаты курсора мыши
   int x=0;
//--- Координаты
   int x1=0,x2=0,y1=0,y2=m_header_y_size;
//--- Получим относительные координаты курсора мыши
   if(::CheckPointer(m_mouse)!=POINTER_INVALID)
     {
      //--- Получим смещение по оси X
      int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
      //--- Определим координаты курсора мыши
      x=m_mouse.X()-m_headers.X()+xoffset;
     }
//--- Очистить фон заголовков
   m_headers.Erase(::ColorToARGB(clrNONE,0));
//--- Отступ с учётом режима изменения ширины столбцов
   int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : 0;
//--- Рисуем фон заголовков
   for(int i=0; i<m_columns_total; i++)
     {
      //--- Рассчитать координаты
      x2+=m_vcolumns[i].m_width;
      //--- Проверим фокус
      if(is_header_focus=x>x1+((i!=0)? sep_x_offset : 0) && x<=x2+sep_x_offset)
         m_prev_header_index_focus=i;

      //--- Нарисовать фон заголовка
      m_headers.FillRectangle(x1,y1,x2,y2,HeaderColorCurrent(is_header_focus));
      //--- Рассчитать отступ для следующего заголовка
      x1+=m_vcolumns[i].m_width;
     }
  }

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

class CCanvasTable : public CElement
  {
private:
   //--- Рисует сетку заголовков таблицы
   void              DrawHeadersGrid(void);
  };
//+------------------------------------------------------------------+
//| Рисует сетку заголовков таблицы                                  |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeadersGrid(void)
  {
//--- Цвет сетки
   uint clr=::ColorToARGB(m_grid_color);
//--- Координаты
   int x1=0,x2=0,y1=0,y2=0;
   x2=m_table_x_size-1;
   y2=m_header_y_size-1;
//--- Нарисовать рамку
   m_headers.Rectangle(x1,y1,x2,y2,clr);
//--- Разделительные линии
   x2=x1=m_vcolumns[0].m_width;
   for(int i=1; i<m_columns_total; i++)
     {
      m_headers.Line(x1,y1,x2,y2,clr);
      x2=x1+=m_vcolumns[i].m_width;
     }
  }

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

class CCanvasTable : public CElement
  {
private:
   //--- Рисует текст заголовков таблицы
   void              DrawHeadersText(void);
  };
//+------------------------------------------------------------------+
//| Рисует текст заголовков таблицы                                  |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeadersText(void)
  {
//--- Для расчёта координат и отступов
   int x=0,y=m_header_y_size/2;
   int column_offset =0;
   uint text_align   =0;
//--- Цвет текста
   uint clr=::ColorToARGB(m_headers_text_color);
//--- Свойства шрифта
   m_headers.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);
//--- Нарисовать текст
   for(int c=0; c<m_columns_total; c++)
     {
      //--- Получим X-координату текста
      x=TextX(c,column_offset);
      //--- Получим способ выравнивания текста
      text_align=TextAlign(c,TA_VCENTER);
      //--- Нарисовать название столбца
      m_headers.TextOut(x,y,CorrectingText(c,0,true),clr,text_align);
     }
  }

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

class CCanvasTable : public CElement
  {
private:
   //--- Рисует заголовки таблицы
   void              DrawTableHeaders(void);
  };
//+------------------------------------------------------------------+
//| Рисует заголовки таблицы                                         |
//+------------------------------------------------------------------+
void CCanvasTable::DrawTableHeaders(void)
  {
//--- Выйти, если заголовки отключены
   if(!m_show_headers)
      return;

//--- Рисует заголовки
   DrawHeaders();
//--- Нарисовать сетку
   DrawHeadersGrid();
//--- Нарисовать текст заголовков
   DrawHeadersText();
  }

Фокус на заголовке определяется методом CCanvasTable::CheckHeaderFocus(). Программа выходит из метода в двух случаях:

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

class CCanvasTable : public CElement
  {
private:
   //--- Проверка фокуса на заголовке
   void              CheckHeaderFocus(void);
  };
//+------------------------------------------------------------------+
//| Проверка фокуса на заголовке                                     |
//+------------------------------------------------------------------+
void CCanvasTable::CheckHeaderFocus(void)
  {
//--- Выйти, если (1) заголовки отключены или (2) начали изменение ширины столбца
   if(!m_show_headers || m_column_resize_control!=WRONG_VALUE)
      return;
//--- Координаты заголовков
   int x1=0,x2=0;
//--- Получим смещение по оси X
   int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- Получим относительные координаты курсора мыши
   int x=m_mouse.X()-m_headers.X()+xoffset;
//--- Отступ с учётом режима изменения ширины столбцов
   int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : 0;
//--- Ищем фокус
   for(int i=0; i<m_columns_total; i++)
     {
      //--- Рассчитать координату справа
      x2+=m_vcolumns[i].m_width;
      //--- Если фокус заголовка изменился
      if((x>x1+sep_x_offset && x<=x2+sep_x_offset) && m_prev_header_index_focus!=i)
        {
         m_prev_header_index_focus=WRONG_VALUE;
         break;
        }
      //--- Рассчитать координату слева
      x1+=m_vcolumns[i].m_width;
     }
  }

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

class CCanvasTable : public CElement
  {
private:
   //--- Изменяет цвет заголовков
   void              ChangeHeadersColor(void);
  };
//+------------------------------------------------------------------+
//| Изменение цвета заголовков                                       |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeHeadersColor(void)
  {
//--- Выйти, если заголовки отключены
   if(!m_show_headers)
      return;
//--- Если указатель курсора активирован
   if(m_column_resize.IsVisible() && m_mouse.LeftButtonState())
     {
      //--- Запомним индекс захваченного столбца
      if(m_column_resize_control==WRONG_VALUE)
         m_column_resize_control=m_prev_header_index_focus;
      //---
      return;
     }
//--- Если не в фокусе
   if(!m_headers.MouseFocus())
     {
      //--- Если ещё не отмечено, что не в фокусе
      if(m_prev_header_index_focus!=WRONG_VALUE)
        {
         //--- Сбросить фокус
         m_prev_header_index_focus=WRONG_VALUE;
         //--- Изменить цвет
         DrawTableHeaders();
         m_headers.Update();
        }
     }
//--- Если в фокусе
   else
     {
      //--- Проверить фокус над заголовками
      CheckHeaderFocus();
      //--- Если нет фокуса
      if(m_prev_header_index_focus==WRONG_VALUE)
        {
         //--- Изменить цвет
         DrawTableHeaders();
         m_headers.Update();
        }
     }
  }

Ниже представлен код метода CCanvasTable::CheckColumnResizeFocus(). Он нужен, чтобы определять фокус на границах между заголовками и отвечает за показ/скрытие курсора для изменения ширины столбцов. В начале метода стоят две проверки. Программа выходит из метода, если режим изменения ширины столбцов отключен. Если же режим включен и сейчас идёт процесс изменения ширины столбца, нужно обновить координаты указателя курсора мыши и выйти из метода.

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

class CCanvasTable : public CElement
  {
private:
   //--- Проверка фокуса на границах заголовков для изменения их ширины
   void              CheckColumnResizeFocus(void);
  };
//+------------------------------------------------------------------+
//| Проверка фокуса на границах заголовков для изменения их ширины   |
//+------------------------------------------------------------------+
void CCanvasTable::CheckColumnResizeFocus(void)
  {
//--- Выйти, если режим изменения ширины столбцов отключен
   if(!m_column_resize_mode)
      return;
//--- Выйти, если начали изменение ширины столбца
   if(m_column_resize_control!=WRONG_VALUE)
     {
      //--- Обновить координаты указателя и сделать его видимым
      m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
      return;
     }
//--- Для проверки фокуса над границами заголовков
   bool is_focus=false;
//--- Если курсор мыши в области заголовков
   if(m_headers.MouseFocus())
     {
      //--- Координаты заголовков
      int x1=0,x2=0;
      //--- Получим смещение по оси X
      int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
      //--- Получим относительные координаты курсора мыши
      int x=m_mouse.X()-m_headers.X()+xoffset;
      //--- Ищем фокус
      for(int i=0; i<m_columns_total; i++)
        {
         //--- Расчёт координат
         x1=x2+=m_vcolumns[i].m_width;
         //--- Проверка фокуса
         if(is_focus=x>x1-m_sep_x_offset && x<=x2+m_sep_x_offset)
            break;
        }
      //--- Если есть фокус
      if(is_focus)
        {
         //--- Обновить координаты указателя и сделать его видимым
         m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
         //--- Показать указатель
         m_column_resize.Show();

         return;
        }
     }
//--- Скрыть указатель, если нет фокуса
   if(!m_headers.MouseFocus() || !is_focus)
      m_column_resize.Hide();
  }

Вот что получается в итоге:

 Рис. 5. Заголовки для столбцов.

Рис. 5. Заголовки для столбцов.

 

 


Корректировка длины строки относительно ширины столбца

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

Сделаем так, чтобы длина строки корректировалась автоматически, если она не помещается в ячейке таблицы. Ранее скорректированные строки не будут корректироваться повторно при перерисовке таблицы. Для хранения этих строк добавим в структуру свойств таблицы ещё один массив.

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

В итоге в массиве m_vrows[] будет храниться полный текст, а в массиве m_text[] — его скорректированная версия.

За корректировку длины строки и в заголовках, и в ячейках таблицы будет отвечать метод CCanvasTable::CorrectingText(). После того, как мы определили, с каким текстом работаем, получаем его ширину. Далее проверяем, помещается ли полный текст строки в ячейку с учётом указанных отступов от её краёв. Если помещается, то сохраняем его в массиве m_text[] и выходим из метода. В этой версии скорректированный текст пока сохраняется только для ячеек, но не для заголовков.

Если же текст не помещается, то нужно обрезать лишние символы и добавить многоточие '…'. Многоточие будет указывать на то, что текст для показа сокращен. Реализовать эту процедуру несложно:

1) Получаем длину строки.

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

3) Если уже не осталось ни одного символа, возвращаем пустую строку.

4) До тех пор, пока есть символы, получаем ширину получившейся строки, с учётом многоточия.

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

6) Если строка помещается, то запоминаем её в локальной переменной метода и останавливаем цикл.

7) После этого сохраняем скорректированную строку в массив m_text[] и возвращаем из метода. 

class CCanvasTable : public CElement
  {
private:
   //--- Возвращает откорректированный текст по ширине столбца
   string            CorrectingText(const int column_index,const int row_index,const bool headers=false);
  };
//+------------------------------------------------------------------+
//| Возвращает откорректированный текст по ширине столбца            |
//+------------------------------------------------------------------+
string CCanvasTable::CorrectingText(const int column_index,const int row_index,const bool headers=false)
  {
//--- Получим текущий текст
   string corrected_text=(headers)? m_header_text[column_index]: m_vcolumns[column_index].m_vrows[row_index];
//--- Отступы от краёв ячейки по оси X
   int x_offset=m_text_x_offset*2;
//--- Получим указатель на объект холста
   CRectCanvas *obj=(headers)? ::GetPointer(m_headers) : ::GetPointer(m_table);
//--- Получим ширину текста
   int full_text_width=obj.TextWidth(corrected_text);
//--- Если помещаемся в ячейку, сохраним скорректированный текст в отдельный массив и вернём его
   if(full_text_width<=m_vcolumns[column_index].m_width-x_offset)
     {
      //--- Если это не заголовки, сохраним откорректированный текст
      if(!headers)
         m_vcolumns[column_index].m_text[row_index]=corrected_text;
      //---
      return(corrected_text);
     }
//--- Если текст не помещается в ячейку, нужно скорректировать его (обрезать лишние символы и добавить многоточие)
   else
     {
      //--- Для работы со строкой
      string temp_text="";
      //--- Получим длину строки
      int total=::StringLen(corrected_text);
      //--- Будем удалять у строки по одному символу, пока не достигнем нужной ширины текста
      for(int i=total-1; i>=0; i--)
        {
         //--- Удалим один символ
         temp_text=::StringSubstr(corrected_text,0,i);
         //--- Если ничего не осталось, оставим пустую строку
         if(temp_text=="")
           {
            corrected_text="";
            break;
           }
         //--- Добавим многоточие перед проверкой
         int text_width=obj.TextWidth(temp_text+"...");
         //--- Если помещаемся в ячейку
         if(text_width<m_vcolumns[column_index].m_width-x_offset)
           {
            //--- Сохраняем текст и останавливаем цикл
            corrected_text=temp_text+"...";
            break;
           }
        }

     }
//--- Если это не заголовки, сохраним откорректированный текст
   if(!headers)
      m_vcolumns[column_index].m_text[row_index]=corrected_text;
//--- Вернём скорректированный текст
   return(corrected_text);
  }

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

Метод CCanvasTable::Text() будет определять, нужно ли для указанного столбца корректировать текст или достаточно отправить уже скорректированную ранее версию. Его код выглядит так: 
class CCanvasTable : public CElement
  {
private:
   //--- Возвращает текст
   string            Text(const int column_index,const int row_index);
  };
//+------------------------------------------------------------------+
//| Возвращает текст                                                 |
//+------------------------------------------------------------------+
string CCanvasTable::Text(const int column_index,const int row_index)
  {
   string text="";
//--- Корректируем текст, если не в режиме изменения ширины столбца
   if(m_column_resize_control==WRONG_VALUE)
      text=CorrectingText(column_index,row_index);
//--- Если же в режиме изменения ширины столбца, то...
   else
     {
      //--- ...корректируем текст только для того столбца, ширину которого изменяем
      if(column_index==m_column_resize_control)
         text=CorrectingText(column_index,row_index);
      //--- Для всех остальных используем уже ранее откорректированный текст
      else
         text=m_vcolumns[column_index].m_text[row_index];
     }
//--- Вернём текст
   return(text);
  }

Код метода CCanvasTable::ChangeColumnWidth(), предназначенного для изменения ширины столбца, представлен ниже.

Минимальную ширину столбца установим в 30 пикселей. Программа выйдет из метода, если показ заголовков отключен. Если проверка пройдена, то далее проверяется фокус на границах заголовков. Если после этой проверки оказалось, что процесс не запущен/закончен, то вспомогательные переменные обнуляются и программа выходит из метода. Если же процесс запущен, то дальше получаем относительную X-координату курсора. Если только что начался процесс, то нужно запомнить текущие значения X-координаты курсора (переменная x_fixed) и ширины захваченного столбца (переменная prev_width). Предназначенные для этого локальные переменные статичны. Поэтому каждый раз при заходе в этот метод их значения будут сохраняться, пока не будут обнулены при завершении процесса. 

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

class CCanvasTable : public CElement
  {
private:
   //--- Минимальная ширина для столбцов
   int               m_min_column_width;
   //---
private:
   //--- Изменяет ширину захваченного столбца
   void              ChangeColumnWidth(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) : m_min_column_width(30)
  {
   ...
  }
//+------------------------------------------------------------------+
//| Изменяет ширину захваченного столбца                             |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeColumnWidth(void)
  {
//--- Выйти, если заголовки отключены
   if(!m_show_headers)
      return;
//--- Проверка фокуса на границах заголовков
   CheckColumnResizeFocus();
//--- Вспомогательные переменные
   static int x_fixed    =0;
   static int prev_width =0;
//--- Если закончили, сбросим значения
   if(m_column_resize_control==WRONG_VALUE)
     {
      x_fixed    =0;
      prev_width =0;
      return;
     }
//--- Получим смещение по оси X
   int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- Получим относительные координаты курсора мыши
   int x=m_mouse.X()-m_headers.X()+xoffset;
//--- Если только начали изменение ширины столбца
   if(x_fixed<1)
     {
      //--- Запомним текущую X-координату и ширину столбца
      x_fixed    =x;
      prev_width =m_vcolumns[m_column_resize_control].m_width;
     }
//--- Рассчитаем новую ширину для столбца
   int new_width=prev_width+(x-x_fixed);
//--- Оставить без изменений, если меньше установленного ограничения
   if(new_width<m_min_column_width)
      return;
//--- Сохраним новую ширину столбца
   m_vcolumns[m_column_resize_control].m_width=new_width;
//--- Рассчитать размеры таблицы
   CalculateTableSize();
//--- Установить новый размер таблицы
   ChangeTableSize();
//--- Нарисуем таблицу
   DrawTable();
  }

Вот что получилось в итоге:

 Рис. 5. Корректировка длины строки относительно изменяемой ширины столбца.

Рис. 5. Корректировка длины строки относительно изменяемой ширины столбца. 

 


Обработка событий

Управление цветом объектов таблицы и изменение ширины её столбцов осуществляет обработчик элемента по событию перемещения курсора (CHARTEVENT_MOUSE_MOVE). 

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события перемещения курсора
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Выйти, если элемент скрыт
      if(!CElementBase::IsVisible())
         return;
      //--- Выйти, если номера подокон не совпадают
      if(!CElementBase::CheckSubwindowNumber())
         return;
      //--- Проверка фокуса над элементами
      CElementBase::CheckMouseFocus();
      m_headers.MouseFocus(m_mouse.X()>m_headers.X() && m_mouse.X()<m_headers.X2() &&
                           m_mouse.Y()>m_headers.Y() && m_mouse.Y()<m_headers.Y2());
      //--- Если полоса прокрутки в действии
      if(m_scrollv.ScrollBarControl() || m_scrollh.ScrollBarControl())
        {
         ShiftTable();
         return;
        }
      //--- Изменение цвета объектов
      ChangeObjectsColor();
      //--- Изменить ширину захваченного столбца
      ChangeColumnWidth();
      return;
     }
   ...
  }

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

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#define ON_CHANGE_MOUSE_LEFT_BUTTON (33) // Изменение состояния левой кнопки мыши

Кроме этого, в класс для получения текущих параметров мыши (CMouse) был добавлен метод CMouse::CheckChangeLeftButtonState() для определения момента изменения состояния левой кнопки мыши. Этот метод вызывается в обработчике класса. Если состояние левой кнопки мыши изменилось, из метода отправляется сообщение с идентификатором ON_CHANGE_MOUSE_LEFT_BUTTON. Это сообщение потом можно принять и обработать в любом элементе управления. 

//+------------------------------------------------------------------+
//| Класс для получения параметров мыши                              |
//+------------------------------------------------------------------+
class CMouse
  {
private:
   //--- Проверка изменения состояния левой кнопки мыши
   bool              CheckChangeLeftButtonState(const string mouse_state);
  };
//+------------------------------------------------------------------+
//| Обработка событий перемещения курсора мыши                       |
//+------------------------------------------------------------------+
void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события перемещения курсора
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Координаты и состояние левой кнопки мыши
      m_x                 =(int)lparam;
      m_y                 =(int)dparam;
      m_left_button_state =CheckChangeLeftButtonState(sparam);
      ...
     }
  }
//+------------------------------------------------------------------+
//| Проверка изменения состояния левой кнопки мыши                   |
//+------------------------------------------------------------------+
bool CMouse::CheckChangeLeftButtonState(const string mouse_state)
  {
   bool left_button_state=(bool)int(mouse_state);
//--- Отправим сообщение об изменении состояния левой кнопки мыши
   if(m_left_button_state!=left_button_state)
      ::EventChartCustom(m_chart.ChartId(),ON_CHANGE_MOUSE_LEFT_BUTTON,0,0.0,"");
//---
   return(left_button_state);
  }

В классе CCanvasTable обработка события с идентификатором ON_CHANGE_MOUSE_LEFT_BUTTON нужна:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Изменение состояния левой кнопки мыши
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_MOUSE_LEFT_BUTTON)
     {
      //--- Выйти, если заголовки отключены
      if(!m_show_headers)
         return;
      //--- Если левая кнопка мыши отжата
      if(!m_mouse.LeftButtonState())
        {
         //--- Сбросить режим изменения ширины
         m_column_resize_control=WRONG_VALUE;
         //--- Скрыть указатель
         m_column_resize.Hide();
         //--- Скорректируем полосу прокрутки с учётом последних изменений
         HorizontalScrolling(m_scrollh.CurrentPos());
        }
      //--- Сбросить индекс последнего фокуса заголовка
      m_prev_header_index_focus=WRONG_VALUE;
      //--- Изменение цвета объектов
      ChangeObjectsColor();
     }
  }

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

 

Заключение

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

Сейчас схема библиотеки для создания графических интерфейсов выглядит так.

 Рис. 6. Структура библиотеки на текущей стадии разработки.

Рис. 6. Структура библиотеки на текущей стадии разработки.

 

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

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