Графические интерфейсы X: Элемент "Время", элемент "Список из чекбоксов" и сортировка таблицы (build 6)

Anatoli Kazharski | 8 декабря, 2016

Содержание


Введение

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

Библиотека продолжает развиваться. Рассмотрим такие элементы, как «Время» и «Список из чекбоксов». Кроме этого, в класс таблицы типа CTable добавлена возможность сортировать данные по возрастанию и убыванию. Об этом и о других обновлениях и будет рассказано в этой статье.

 

Элемент «Время»

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

Перечислим все составные части, из которых будет собираться элемент «Время»:

  • Фон
  • Ярлык
  • Описание
  • Два поля ввода

Рис. 1. Составные части элемента «Время».

Рис. 1. Составные части элемента «Время».

 

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

 

Класс для создания элемента «Время»

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

  • Цвет фона элемента
  • Ярлыки элемента для активного и заблокированного состояния
  • Отступы ярлыка по двум осям (x, y)
  • Текст описания элемента
  • Отступы текстовой метки по двум осям (x, y)
  • Цвет текста в разных состояниях элемента
  • Ширина полей ввода
  • Отступы полей ввода по двум осям (x, y)
  • Состояние элемента (доступен/заблокирован)
  • Режим сброса значений в полях ввода
//+------------------------------------------------------------------+
//| Класс для создания элемента "Время"                              |
//+------------------------------------------------------------------+
class CTimeEdit : public CElement
  {
private:
   //--- Цвет фона элемента
   color             m_area_color;
   //--- Ярлыки элемента в активном и заблокированном состоянии
   string            m_icon_file_on;
   string            m_icon_file_off;
   //--- Отступы ярлыка
   int               m_icon_x_gap;
   int               m_icon_y_gap;
   //--- Текст описания элемента
   string            m_label_text;
   //--- Отступы текстовой метки
   int               m_label_x_gap;
   int               m_label_y_gap;
   //--- Цвета текста в разных состояниях
   color             m_label_color;
   color             m_label_color_hover;
   color             m_label_color_locked;
   color             m_label_color_array[];
   //--- Размеры поля ввода
   int               m_edit_x_size;
   //--- Отступы поля ввода
   int               m_edit_x_gap;
   int               m_edit_y_gap;
   //--- Состояние элемента (доступен/заблокирован)
   bool              m_time_edit_state;
   //--- Режим сброса значения
   bool              m_reset_mode;
   //---
public:
   //--- (1) Цвет фона, (2) отступы ярлыка
   void              AreaColor(const color clr)                     { m_area_color=clr;                  }
   void              IconXGap(const int x_gap)                      { m_icon_x_gap=x_gap;                }
   void              IconYGap(const int y_gap)                      { m_icon_y_gap=y_gap;                }
   //--- (1) Текст описания элемента, (2) отступы текстовой метки
   string            LabelText(void)                          const { return(m_label.Description());     }
   void              LabelText(const string text)                   { m_label.Description(text);         }
   void              LabelXGap(const int x_gap)                     { m_label_x_gap=x_gap;               }
   void              LabelYGap(const int y_gap)                     { m_label_y_gap=y_gap;               }
   //--- Цвета текстовой метки в разных состояниях
   void              LabelColor(const color clr)                    { m_label_color=clr;                 }
   void              LabelColorHover(const color clr)               { m_label_color_hover=clr;           }
   void              LabelColorLocked(const color clr)              { m_label_color_locked=clr;          }
   //--- (1) Размеры поля ввода, (2) отступы полей ввода
   void              EditXSize(const int x_size)                    { m_edit_x_size=x_size;              }
   void              EditXGap(const int x_gap)                      { m_edit_x_gap=x_gap;                }
   void              EditYGap(const int y_gap)                      { m_edit_y_gap=y_gap;                }
   //--- (1) Режим сброса при нажатии на текстовой метке, (2) режим текстового выделения
   bool              ResetMode(void)                                { return(m_reset_mode);              }
   void              ResetMode(const bool mode)                     { m_reset_mode=mode;                 }
  };

Для создания элемента «Время» задействованы пять приватных (private) методов и один публичный (public). Этот элемент относится к составному типу, и в качестве полей ввода будут использоваться уже готовые элементы типа CSpinEdit

class CTimeEdit : public CElement
  {
private:
   //--- Объекты для создания элемента
   CRectLabel        m_area;
   CBmpLabel         m_icon;
   CLabel            m_label;
   CSpinEdit         m_hours;
   CSpinEdit         m_minutes;

   //---
public:
   //--- Методы для создания элемента
   bool              CreateTimeEdit(const long chart_id,const int subwin,const string label_text,const int x_gap,const int y_gap);
   //---
private:
   bool              CreateArea(void);
   bool              CreateIcon(void);
   bool              CreateLabel(void);
   bool              CreateHoursEdit(void);
   bool              CreateMinutesEdit(void);
   //---
public:
   //--- Возвращает указатели полей ввода
   CSpinEdit        *GetHoursEditPointer(void)                      { return(::GetPointer(m_hours));     }
   CSpinEdit        *GetMinutesEditPointer(void)                    { return(::GetPointer(m_minutes));   }

  };

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

class CTimeEdit : public CElement
  {
public:
   //--- Возвращение и установка значений полей ввода
   int               GetHours(void)                           const { return((int)m_hours.GetValue());   }
   int               GetMinutes(void)                         const { return((int)m_minutes.GetValue()); }
   void              SetHours(const uint value)                     { m_hours.ChangeValue(value);        }
   void              SetMinutes(const uint value)                   { m_minutes.ChangeValue(value);      }
  };

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


Элемент «Список с чекбоксами»

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

Список составных частей, из которых будет собираться элемент «Список из чекбоксов»:

  1. Общий фон элемента
  2. Вертикальная полоса прокрутки
  3. Группа чекбоксов:
    • Фон
    • Ярлык
    • Текстовая метка

Рис. 2. Составные части элемента «Список из чек-боксов».

Рис. 2. Составные части элемента «Список из чекбоксов»

 

Далее кратко рассмотрим, чем отличается этот тип списка (CCheckBoxList) от простого списка (CListView), который мы уже рассматривали ранее.

 

Класс для создания списка из чекбоксов

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

//+------------------------------------------------------------------+
//| Класс для создания списка с чекбоксами                          |
//+------------------------------------------------------------------+
class CCheckBoxList : public CElement
  {
private:
   //--- Объекты для создания списка
   CRectLabel        m_area;
   CEdit             m_items[];
   CBmpLabel         m_checks[];
   CLabel            m_labels[];

   CScrollV          m_scrollv;
   //---
public:
   //--- Методы для создания элемента
   bool              CreateCheckBoxList(const long chart_id,const int subwin,const int x_gap,const int y_gap);
   //---
private:
   bool              CreateArea(void);
   bool              CreateList(void);
   bool              CreateItems(void);
   bool              CreateChecks(void);
   bool              CreateLabels(void);

   bool              CreateScrollV(void);
  };

Кроме массива значений пунктов (описание пунктов), понадобится еще и массив состояний чекбоксов (включен/выключен).  Также потребуются соответствующие методы для установки и получения значений по указанному индексу в списке: 

class CCheckBoxList : public CElement
  {
private:
   //--- Массив значений и состояний чекбоксов списка
   string            m_item_value[];
   bool              m_item_state[];
   //---
public:
   //--- Возвращает/cохраняет (1) состояние и (2) текст пункта в списке по указанному индексу
   void              SetItemState(const uint item_index,const bool state);
   void              SetItemValue(const uint item_index,const string value);
   bool              GetItemState(const uint item_index);
   string            GetItemValue(const uint item_index);
  };
//+------------------------------------------------------------------+
//| Установка состояния                                              |
//+------------------------------------------------------------------+
void CCheckBoxList::SetItemState(const uint item_index,const bool state)
  {
   uint array_size=::ArraySize(m_item_state);
//--- Если нет ни одного пункта в списке, сообщить об этом
   if(array_size<1)
      ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, когда в списке есть хотя бы один пункт!");
//--- Корректировка в случае выхода из диапазона
   uint check_index=(item_index>=array_size)? array_size-1 : item_index;
//--- Сохранить значение
   m_item_state[check_index]=state;
//--- Сдвигает список относительно полосы прокрутки
   ShiftList();
  }
//+------------------------------------------------------------------+
//| Получение состояния чекбокса списка                              |
//+------------------------------------------------------------------+
bool CCheckBoxList::GetItemState(const uint item_index)
  {
   uint array_size=::ArraySize(m_item_state);
//--- Если нет ни одного пункта в списке, сообщить об этом
   if(array_size<1)
      ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, когда в списке есть хотя бы один пункт!");
//--- Корректировка в случае выхода из диапазона
   uint check_index=(item_index>=array_size)? array_size-1 : item_index;
//--- Сохранить значение
   return(m_item_state[check_index]);
  }

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


Сортировка таблицы

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

На скриншоте ниже показано окно «Инструменты» из редактора кода MetaEditor, с таблицей из трёх столбцов. Сортировка (на убывание) в таблице выполнена по третьему столбцу, в котором размещены даты.

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

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

 

В качестве признака отсортированных данных обычно в заголовке столбца показывают стрелку. Если она направлена вниз, как на скриншоте выше, то это означает, что данные отсортированы на убывание и наоборот.

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

class CTable : public CElement
  {
private:
   //--- Цвет фона заголовков
   color             m_headers_color;
   color             m_headers_color_hover;
   color             m_headers_color_pressed;

   //---
public:
   //--- Цвета фона заголовков
   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;             }

  };

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

class CTable : public CElement
  {
private:
   //--- Режим сортируемых данных по столбцам
   bool              m_is_sort_mode;
   //---
public:
   //--- Режим сортируемых данных
   void              IsSortMode(const bool flag)                                { m_is_sort_mode=flag;                     }
  };

Для изменения цвета заголовков при наведении курсора мыши будет использоваться приватный метод CTable::HeaderColorByHover(). Его вызов осуществляется в обработчике событий элемента. 

class CTable : public CElement
  {
private:
   //--- Изменение цвета заголовка таблицы при наведении курсора мыши
   void              HeaderColorByHover(void);
  };
//+------------------------------------------------------------------+
//| Изменение цвета заголовка таблицы при наведении курсора мыши     |
//+------------------------------------------------------------------+
void CTable::HeaderColorByHover(void)
  {
//--- Выйти, если режим сортируемых столбцов отключен
   if(!m_is_sort_mode || !m_fix_first_row)
      return;
//---
   for(uint c=0; c<m_visible_columns_total; c++)
     {
      //--- Проверим фокус на текущем заголовке
      bool condition=m_mouse.X()>m_columns[c].m_rows[0].X() && m_mouse.X()<m_columns[c].m_rows[0].X2() &&
                     m_mouse.Y()>m_columns[c].m_rows[0].Y() && m_mouse.Y()<m_columns[c].m_rows[0].Y2();
      //---
      if(!condition)
         m_columns[c].m_rows[0].BackColor(m_headers_color);
      else
        {
         if(!m_mouse.LeftButtonState())
            m_columns[c].m_rows[0].BackColor(m_headers_color_hover);
         else
            m_columns[c].m_rows[0].BackColor(m_headers_color_pressed);
        }
     }
  }

Для создания ярлыка-признака отсортированных данных нужно добавить приватный метод CTable::CreateSignSortedData(). Если режим сортировки не был включен перед созданием таблицы, то картинка не будет создана. Если же режим сортировки включен, то картинка будет скрыта сразу после создания, так как изначально данные таблицы не отсортированы. 

class CTable : public CElement
  {
private:
   //--- Объекты для создания таблицы
   CBmpLabel         m_sort_arrow;
   //---
private:
   //--- Методы для создания таблицы
   bool              CreateSignSortedData(void);
  };
//+------------------------------------------------------------------+
//| Создаёт ярлык-стрелку, как признак отсортированных данных        |
//+------------------------------------------------------------------+
bool CTable::CreateSignSortedData(void)
  {
//--- Выйти, если режим сортировки не включен
   if(!m_is_sort_mode)
      return(true);

//--- Формирование имени объекта
   string name=CElement::ProgramName()+"_table_sort_array_"+(string)CElement::Id();
//--- Координаты
   int x =(m_anchor_right_window_side)? m_columns[0].m_rows[0].X()-m_columns[0].m_rows[0].XSize()+m_sort_arrow_x_gap : m_columns[0].m_rows[0].X2()-m_sort_arrow_x_gap;
   int y =(m_anchor_bottom_window_side)? CElement::Y()-m_sort_arrow_y_gap : CElement::Y()+m_sort_arrow_y_gap;
//--- Если не указали картинку для стрелки, значит установка по умолчанию
   if(m_sort_arrow_file_on=="")
      m_sort_arrow_file_on="Images\\EasyAndFastGUI\\Controls\\SpinInc.bmp";
   if(m_sort_arrow_file_off=="")
      m_sort_arrow_file_off="Images\\EasyAndFastGUI\\Controls\\SpinDec.bmp";
//--- Установим объект
   if(!m_sort_arrow.Create(m_chart_id,name,m_subwin,x,y))
      return(false);
//--- Установим свойства
   m_sort_arrow.BmpFileOn("::"+m_sort_arrow_file_on);
   m_sort_arrow.BmpFileOff("::"+m_sort_arrow_file_off);
   m_sort_arrow.Corner(m_corner);
   m_sort_arrow.GetInteger(OBJPROP_ANCHOR,m_anchor);
   m_sort_arrow.Selectable(false);
   m_sort_arrow.Z_Order(m_zorder);
   m_sort_arrow.Tooltip("\n");
//--- Сохраним координаты
   m_sort_arrow.X(x);
   m_sort_arrow.Y(y);
//--- Сохраним размеры (в объекте)
   m_sort_arrow.XSize(m_sort_arrow.X_Size());
   m_sort_arrow.YSize(m_sort_arrow.Y_Size());
//--- Отступы от крайней точки
   m_sort_arrow.XGap((m_anchor_right_window_side)? x : x-m_wnd.X());
   m_sort_arrow.YGap((m_anchor_bottom_window_side)? y : y-m_wnd.Y());
//--- Скрыть объект
   m_sort_arrow.Timeframes(OBJ_NO_PERIODS);
//--- Сохраним указатель объекта
   CElement::AddToArray(m_sort_arrow);
   return(true);
  }

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

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

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

class CTable : public CElement
  {
public:
   //--- Устанавливает значение в указанную ячейку таблицы
   void              SetValue(const uint column_index,const uint row_index,const string value="",const uint digits=0);
  };

Для установки типа данных в том или ином столбце таблицы, а также для получения типа нужно воспользоваться методами CTable::DataType():

class CTable : public CElement
  {
public:
   //--- Получение/установка типа данных
   ENUM_DATATYPE     DataType(const uint column_index)                          { return(m_vcolumns[column_index].m_type); }
   void              DataType(const uint column_index,const ENUM_DATATYPE type) { m_vcolumns[column_index].m_type=type;    }
  };

Перед созданием таблицы мы указываем общее количество столбцов и рядов. При этом поле m_type и массив m_digits инициализируются значениями по умолчанию. Изначально тип во всех столбцах строковой (TYPE_STRING), а количество знаков после запятой во всех ячейках равно нулю

//+------------------------------------------------------------------+
//| Устанавливает размер таблицы                                     |
//+------------------------------------------------------------------+
void CTable::TableSize(const uint columns_total,const uint 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(uint i=0; i<m_columns_total; i++)
     {
      ::ArrayResize(m_vcolumns[i].m_vrows,m_rows_total);
      ::ArrayResize(m_vcolumns[i].m_digits,m_rows_total);
      ::ArrayResize(m_vcolumns[i].m_text_align,m_rows_total);
      ::ArrayResize(m_vcolumns[i].m_text_color,m_rows_total);
      ::ArrayResize(m_vcolumns[i].m_cell_color,m_rows_total);
      //--- Инициализация массива цвета фона ячеек значением по умолчанию
      m_vcolumns[i].m_type=TYPE_STRING;
      ::ArrayInitialize(m_vcolumns[i].m_digits,0);
      ::ArrayInitialize(m_vcolumns[i].m_text_align,m_align_mode);
      ::ArrayInitialize(m_vcolumns[i].m_cell_color,m_cell_color);
      ::ArrayInitialize(m_vcolumns[i].m_text_color,m_cell_text_color);
     }
  }

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

  • Алгоритм сортировки.
  • Сравнение значений по указанному условию.
  • Обмен значений элементов массива.

За обмен значений элементов массива таблицы отвечает метод CTable::Swap(). Здесь обмен осуществляется сразу рядами таблицы. Местами меняются не только значения в ячейках, но и цвет текста. 

class CTable : public CElement
  {
private:
   //--- Поменять значения в указанных ячейках местами
   void              Swap(uint c,uint r1,uint r2);
  };
//+------------------------------------------------------------------+
//| Поменять элементы местами                                        |
//+------------------------------------------------------------------+
void CTable::Swap(uint c,uint r1,uint r2)
  {
//--- Пройдёмся в цикле по всем столбцам
   for(uint i=0; i<m_columns_total; i++)
     {
      //--- Меняем местами текст
      string temp_text          =m_vcolumns[i].m_vrows[r1];
      m_vcolumns[i].m_vrows[r1] =m_vcolumns[i].m_vrows[r2];
      m_vcolumns[i].m_vrows[r2] =temp_text;
      //--- Меняем местами цвет текста
      color temp_text_color          =m_vcolumns[i].m_text_color[r1];
      m_vcolumns[i].m_text_color[r1] =m_vcolumns[i].m_text_color[r2];
      m_vcolumns[i].m_text_color[r2] =temp_text_color;
     }
  }

Для корректной сортировки сравнивать значения нужно с приведением к указанному типу в поле m_type. Поэтому был написан отдельный метод CTable::CheckSortCondition().

class CTable : public CElement
  {
private:
   //--- Проверка условия сортировки
   bool              CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction);
  };
//+------------------------------------------------------------------+
//| Сравнение значений по указанному условию сортировки              |
//+------------------------------------------------------------------+
//| direction: true (>), false (<)                                   |
//+------------------------------------------------------------------+
bool CTable::CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction)
  {
   bool condition=false;
//---
   switch(m_vcolumns[column_index].m_type)
     {
      case TYPE_STRING :
        {
         string v1=m_vcolumns[column_index].m_vrows[row_index];
         string v2=check_value;
         condition=(direction)? v1>v2 : v1<v2;
         break;
        }
      //---
      case TYPE_DOUBLE :
        {
         double v1=double(m_vcolumns[column_index].m_vrows[row_index]);
         double v2=double(check_value);
         condition=(direction)? v1>v2 : v1<v2;
         break;
        }
      //---
      case TYPE_DATETIME :
        {
         datetime v1=::StringToTime(m_vcolumns[column_index].m_vrows[row_index]);
         datetime v2=::StringToTime(check_value);
         condition=(direction)? v1>v2 : v1<v2;
         break;
        }
      //---
      default :
        {
         long v1=(long)m_vcolumns[column_index].m_vrows[row_index];
         long v2=(long)check_value;
         condition=(direction)? v1>v2 : v1<v2;
         break;
        }
     }
//---
   return(condition);
  }

Методы CTable::Swap() и CTable::CheckSortCondition() будут использоваться в методе с алгоритмом сортировки. Остановимся немного подробнее на том, какой именно был выбран алгоритм для сортировки данных. Были протестированы десять алгоритмов, включая штатную MQL-сортировку с помощью функции ArraySort():

  • MQL-сортировка
  • Сортировка выборкой
  • Пузырьковая сортировка
  • Шейкерная сортировка
  • Сортировка вставкой
  • Сортировка Шелла
  • Бинарная сортировка
  • Быстрая сортировка
  • Пирамидальная сортировка
  • Сортировка слиянием

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

Рис. 4. График результатов теста различных методов сортировки. Размер массива 100 000 элементов.

Рис. 4. График результатов теста различных методов сортировки. Размер массива 100 000 элементов.


Самым быстрым оказался метод быстрой сортировки. Код этого метода был взят из стандартной библиотеки – метод QuickSort(). В этом тесте он даже показал результат лучше, чем функция ArraySort(). Замеры длительности действия алгоритма для большей точности осуществлялись с помощью функции GetMicrosecondCount(). Оставим только те алгоритмы, которые показали наилучший результат (менее одной секунды). 

Рис. 5. График лучших результатов теста методов сортировки. Размер массива 100 000 элементов.

Рис. 5. График лучших результатов теста методов сортировки. Размер массива 100 000 элементов.


Увеличим размер массива в 10 раз, то есть теперь будем сортировать массив из 1000000 (одного миллиона) элементов.

Рис. 6. График результатов теста методов сортировки. Размер массива 1 000 000 элементов.

Рис. 6. График результатов теста методов сортировки. Размер массива 1 000 000 элементов.


В этом тесте лучшим оказался штатный алгоритм – функция ArraySort(). Ненамного хуже показал себя метод быстрой сортировки, поэтому остановим свой выбор именно на нём. Функция ArraySort() не подходит под нашу задачу, так как: (1) нужна возможность осуществлять сортировку в двух направлениях, (2) в классе CTable используется массив структур и (3) нужно контролировать расположение не только значений в ячейках таблицы, но и другие их свойства. 

В файл Enums.mqh нужно добавить перечисление ENUM_SORT_MODE для двух направлений сортировки:

  • SORT_ASCEND – по возрастанию.
  • SORT_DESCEND – по убыванию.
//+------------------------------------------------------------------+
//| Перечисление режимов сортировки                                  |
//+------------------------------------------------------------------+
enum ENUM_SORT_MODE
  {
   SORT_ASCEND  =0,
   SORT_DESCEND =1
  };

В листинге кода ниже представлена текущая версия алгоритма быстрой сортировки в методе CTable::QuickSort(). Жёлтым маркером отмечены методы CTable::Swap() и CTable::CheckSortCondition(), которые были представлены до этого выше в статье. 

class CTable : public CElement
  {
private:
   //--- Метод быстрой сортировки
   void              QuickSort(uint beg,uint end,uint column,const ENUM_SORT_MODE mode=SORT_ASCEND);
  };
//+------------------------------------------------------------------+
//| Алгоритм быстрой сортировки                                      |
//+------------------------------------------------------------------+
void CTable::QuickSort(uint beg,uint end,uint column,const ENUM_SORT_MODE mode=SORT_ASCEND)
  {
   uint   r1         =beg;
   uint   r2         =end;
   uint   c          =column;
   string temp       =NULL;
   string value      =NULL;
   uint   data_total =m_rows_total-1;
//--- Выполнять алгоритм, пока левый индекс меньше самого крайнего правого индекса
   while(r1<end)
     {
      //--- Получим значение из середины ряда
      value=m_vcolumns[c].m_vrows[(beg+end)>>1];
      //--- Выполнять алгоритм, пока левый индекс меньше найденного правого индекса
      while(r1<r2)
        {
         //--- Смещать индекс вправо, пока находим значение по указанному условию
         while(CheckSortCondition(c,r1,value,(mode==SORT_ASCEND)? false : true))
           {
            //--- Контроль выхода за границы массива
            if(r1==data_total)
               break;
            r1++;
           }
         //--- Смещать индекс влево, пока находим значение по указанному условию
         while(CheckSortCondition(c,r2,value,(mode==SORT_ASCEND)? true : false))
           {
            //--- Контроль выхода за границы массива
            if(r2==0)
               break;
            r2--;
           }
         //--- Если левый индекс ещё не больше правого
         if(r1<=r2)
           {
            //--- Поменять значения местами
            Swap(c,r1,r2);
            //--- Если дошли до предела слева
            if(r2==0)
              {
               r1++;
               break;
              }
            //---
            r1++;
            r2--;
           }
        }
      //--- Рекурсивное продолжение алгоритма, пока не дойдём до начала диапазона
      if(beg<r2)
         QuickSort(beg,r2,c,mode);
      //--- Cужение диапазона для следующей итерации
      beg=r1;
      r2=end;
     }
  }

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

class CTable : public CElement
  {
private:
   //--- Индекс отсортированного столбца (WRONG_VALUE – таблица не отсортирована)
   int               m_is_sorted_column_index;
   //--- Последнее направление сортировки
   ENUM_SORT_MODE    m_last_sort_direction;
   //---
public:
   //--- Сортировать данные по указанному столбцу
   void              SortData(const uint column_index=0);
  };
//+------------------------------------------------------------------+
//| Сортировать данные по указанному столбцу                         |
//+------------------------------------------------------------------+
void CTable::SortData(const uint column_index=0)
  {
//--- Индекс (с учётом наличия заголовков), с которого нужно начать сортировку
   uint first_index=(m_fix_first_row) ? 1 : 0;
//--- Последний индекс массива
   uint last_index=m_rows_total-1;
//--- В первый раз будет отсортировано по возрастанию, а затем каждый раз в противоложном направлении
   if(m_is_sorted_column_index==WRONG_VALUE || column_index!=m_is_sorted_column_index || m_last_sort_direction==SORT_DESCEND)
      m_last_sort_direction=SORT_ASCEND;
   else
      m_last_sort_direction=SORT_DESCEND;
//--- Запомним индекс последнего отсортированного столбца данных
   m_is_sorted_column_index=(int)column_index;
//--- Сортировка
   QuickSort(first_index,last_index,column_index,m_last_sort_direction);
//--- Обновить таблицу
   UpdateTable();
//--- Установить ярлык в соответствии с направлением сортировки
   m_sort_arrow.State((m_last_sort_direction==SORT_ASCEND)? true : false);
  }

Понадобится метод для обработки нажатия на заголовках столбцов — CTable::OnClickTableHeaders(), когда включен режим сортировки данных. После прохождения всех проверок на принадлежность объекта к этому элементу в цикле определяется индекс заголовка (столбца) и затем таблица сортируется. Сразу после сортировки таблицы генерируется событие с новым идентификатором ON_SORT_DATA. Кроме этого идентификатора события, в сообщении содержится (1) идентификатор элемента, (2) индекс отсортированного столбца и (3) тип данных этого столбца.

class CTable : public CElement
  {
private:
   //--- Обработка нажатия на заголовках таблицы
   bool              OnClickTableHeaders(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на заголовке таблицы                           |
//+------------------------------------------------------------------+
bool CTable::OnClickTableHeaders(const string clicked_object)
  {
//--- Выйти, если режим сортировки отключен
   if(!m_is_sort_mode)
      return(false);

//--- Выйдем, если нажатие было не на ячейке таблицы
   if(::StringFind(clicked_object,CElement::ProgramName()+"_table_edit_",0)<0)
      return(false);
//--- Получим идентификатор имени объекта
   int id=CElement::IdFromObjectName(clicked_object);
//--- Выйти, если идентификатор не совпадает
   if(id!=CElement::Id())
      return(false);
//--- Выйти, если это не заголовок таблицы
   if(RowIndexFromObjectName(clicked_object)>0)
      return(false);
//--- Для определения индекса столбца
   uint column_index=0;
//--- Смещение на один индекс, если включен режим закреплённых заголовков
   int l=(m_fix_first_column) ? 1 : 0;
//--- Получим текущую позицию ползунка горизонтальной полосы прокрутки
   int h=m_scrollh.CurrentPos()+l;
//--- Столбцы
   for(uint c=l; c<m_visible_columns_total; c++)
     {
      //--- Если нажатие было на этой ячейке
      if(m_columns[c].m_rows[0].Name()==clicked_object)
        {
         //--- Получим индекс столбца
         column_index=(m_fix_first_column && c==0) ? 0 : h;
         break;
        }
      //---
      h++;
     }
//--- Сортировка данных по указанному столбцу
   SortData(column_index);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_SORT_DATA,CElement::Id(),m_is_sorted_column_index,::EnumToString(DataType(column_index)));
   return(true);
  }

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

class CTable : public CElement
  {
private:
   //--- Смещение стрелки-признака сортировки
   void              ShiftSortArrow(const uint column);
  };
//+------------------------------------------------------------------+
//| Смещение стрелки на отсортированный столбец таблицы              |
//+------------------------------------------------------------------+
void CTable::ShiftSortArrow(const uint column)
  {
//--- Показать объект, если элемент не скрыт
   if(CElement::IsVisible())
      m_sort_arrow.Timeframes(OBJ_ALL_PERIODS);
//--- Рассчитать и установить координату
   int x=m_columns[column].m_rows[0].X2()-m_sort_arrow_x_gap;
   m_sort_arrow.X(x);
   m_sort_arrow.X_Distance(x);
//--- Отступ от крайней точки
   m_sort_arrow.XGap((m_anchor_right_window_side)? m_wnd.X2()-x : x-m_wnd.X());
  }

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

//+------------------------------------------------------------------+
//| Обновление данных таблицы с учётом последних изменений           |
//+------------------------------------------------------------------+
void CTable::UpdateTable(void)
  {
//...

//--- Смещение заголовков в верхнем ряду
   if(m_fix_first_row)
     {
      //--- Для определения смещения ярлыка сортировки
      bool is_shift_sort_arrow=false;
      //--- Столбцы
      for(uint c=l; c<m_visible_columns_total; c++)
        {
         //--- Если не выходим из диапазона
         if(h>=l && h<m_columns_total)
           {
            //--- Если нашли отсортированный столбец
            if(!is_shift_sort_arrow && m_is_sort_mode && h==m_is_sorted_column_index)
              {
               is_shift_sort_arrow=true;
               //--- Скорректируем ярлык сортировки
               uint column=h-(h-c);
               if(column>=l && column<m_visible_columns_total)
                  ShiftSortArrow(column);
              }

            //--- Корректировка (1) значений, (2) цвета фона, (3) цвета текста и (4) выравнивания текста в ячейках
            SetCellParameters(c,0,m_vcolumns[h].m_vrows[0],m_headers_color,m_headers_text_color,m_vcolumns[h].m_text_align[0]);
           }
         //---
         h++;
        }
      //--- Если отсортированный столбец есть, но не был найден
      if(!is_shift_sort_arrow && m_is_sort_mode && m_is_sorted_column_index!=WRONG_VALUE)
        {
         //--- Скрыть, если индекс больше нуля
         if(m_is_sorted_column_index>0 || !m_fix_first_column)
            m_sort_arrow.Timeframes(OBJ_NO_PERIODS);
         //--- Установить на заголовок первого столбца
         else
            ShiftSortArrow(0);
        }

     }
//...

  }

В конце статьи можно скачать файлы с тестовым экспертом, в котором можно самостоятельно протестировать, как это работает.

 

Другие обновления библиотеки

В качестве дополнительных возможностей в билд включены следующие обновления библиотеки:

1. Теперь для каждого элемента можно установить свой шрифт и его размер. Для этого в базовый класс элементов CElement добавлены соответствующие поля и методы. По умолчанию установлен шрифт «Calibri», а размер шрифта 8 пунктов

class CElement
  {
protected:
   //--- Шрифт
   string            m_font;
   int               m_font_size;
   //---
public:
   //--- (1) Шрифт и (2) размер шрифта
   void              Font(const string font)                         { m_font=font;                          }
   string            Font(void)                                const { return(m_font);                       }
   void              FontSize(const int font_size)                   { m_font_size=font_size;                }
   int               FontSize(void)                            const { return(m_font_size);                  }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CElement::CElement(void) : m_font("Calibri"),
                           m_font_size(8)
  {
  }

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

//+------------------------------------------------------------------+
//| Создаёт текстовую метку чекбокса                                 |
//+------------------------------------------------------------------+
bool CCheckBox::CreateLabel(void)
  {
//--- Формирование имени объекта
   string name=CElement::ProgramName()+"_checkbox_lable_"+(string)CElement::Id();
//--- Координаты
   int x =(m_anchor_right_window_side)? m_x-m_label_x_gap : m_x+m_label_x_gap;
   int y =(m_anchor_bottom_window_side)? m_y-m_label_y_gap : m_y+m_label_y_gap;
//--- Цвет текста относительно состояния
   color label_color=(m_check_button_state) ? m_label_color : m_label_color_off;
//--- Установим объект
   if(!m_label.Create(m_chart_id,name,m_subwin,x,y))
      return(false);
//--- Установим свойства
   m_label.Description(m_label_text);
   m_label.Font(CElement::Font());
   m_label.FontSize(CElement::FontSize());

   m_label.Color(label_color);
   m_label.Corner(m_corner);
   m_label.Anchor(m_anchor);
   m_label.Selectable(false);
   m_label.Z_Order(m_zorder);
   m_label.Tooltip("\n");
//--- Отступы от крайней точки
   m_label.XGap((m_anchor_right_window_side)? x : x-m_wnd.X());
   m_label.YGap((m_anchor_bottom_window_side)? y : y-m_wnd.Y());
//--- Инициализация массива градиента
   CElement::InitColorArray(label_color,m_label_color_hover,m_label_color_array);
//--- Сохраним указатель объекта
   CElement::AddToArray(m_label);
   return(true);
  }

 

2. Отступы от крайней точки формы для каждого элемента графического интерфейса теперь нужно передавать напрямую в метод создания элемента. Расчёт будет осуществляться автоматически. Для примера в листинге ниже показан метод создания выпадающего календаря из пользовательского класса CProgram

//+------------------------------------------------------------------+
//| Создаёт выпадающий календарь                                     |
//+------------------------------------------------------------------+
bool CProgram::CreateDropCalendar(const int x_gap,const int y_gap,const string text)
  {
//--- Передать объект панели
   m_drop_calendar.WindowPointer(m_window);
//--- Закрепить за 2-ой вкладкой
   m_tabs.AddToElementsArray(1,m_drop_calendar);
//--- Установим свойства перед созданием
   m_drop_calendar.XSize(140);
   m_drop_calendar.YSize(20);
   m_drop_calendar.AreaBackColor(clrWhite);
//--- Создадим элемент управления
   if(!m_drop_calendar.CreateDropCalendar(m_chart_id,m_subwin,text,x_gap,y_gap))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(0,m_drop_calendar);
   return(true);
  }

 


Приложение для теста элемента

Теперь напишем тестовое MQL-приложение, в котором Вы сможете протестировать все новые элементы. Создадим графический интерфейс, в котором будет главное меню (CMenuBar) с выпадающими контекстными меню, статусной строкой и двумя вкладками. На первой вкладке будет таблица типа CTable, с включенным режимом сортировки. 

В таблице первые три столбца сделаем со следующими типами данных:

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

Рис. 7. Пример отсортированной (на возрастание) таблицы по первому второму столбцу.

Рис. 7. Пример отсортированной (на возрастание) таблицы по первому второму столбцу.


На второй вкладке создадим четыре элемента: 

  • Выпадающий календарь (класс CDropCalendar).
  • Элемент «Время» (класс CTimeEdit).
  • Список из чекбоксов (класс CCheckBoxList).
  • Простой список (класс CListView).

Ниже на скриншоте показано, как это выглядит в графическом интерфейсе тестового MQL-приложения:

Рис. 8. Элементы управления на второй вкладке.

Рис. 8. Элементы управления на второй вкладке.


Исходный код этого тестового приложения приложен в конце статьи. 

 

Заключение

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

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

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


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

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