English 中文 Español Deutsch 日本語 Português
Графические интерфейсы X: Расширенное управление списками и таблицами. Оптимизация кода (build 7)

Графические интерфейсы X: Расширенное управление списками и таблицами. Оптимизация кода (build 7)

MetaTrader 5Примеры | 28 декабря 2016, 15:14
6 587 112
Anatoli Kazharski
Anatoli Kazharski

Содержание


Введение

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

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


Изменения в схеме библиотеки и оптимизация кода

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

Поясним, как это было сделано. Класс CElement переименован в CElementBase. Это базовый класс всех элементов управления библиотеки. Теперь следующим после него наследуемым классом идёт новый класс CElement, в котором расположились многократно повторяющиеся методы во всех элементах. К ним относятся:

  • Метод для сохранения указателя формы, к которой прикрепляется элемент
  • Проверка наличия указателя на форму
  • Проверка идентификатора активированного элемента
  • Расчёт абсолютных координат
  • Расчёт относительных координат от крайней точки формы

Классы CElementBase и CElement находятся в разных файлах, ElementBase.mqh и Element.mqh соответственно. Поэтому файл ElementBase.mqh с базовым классом подключаем к файлу Element.mqh. Так как здесь нужно определение типа CWindows, то подключаем также и файл Window.mqh. В листинге кода ниже показано, как это выглядит:

//+------------------------------------------------------------------+
//|                                                      Element.mqh |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "ElementBase.mqh"
#include "Controls\Window.mqh"
//+------------------------------------------------------------------+
//| Класс для получения параметров мыши                              |
//+------------------------------------------------------------------+
class CElement : public CElementBase
  {
protected:
   //--- Указатель на форму, к которой элемент присоединён
   CWindow          *m_wnd;
   //---
public:
                     CElement(void);
                    ~CElement(void);
   //--- Сохраняет указатель формы
   void              WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); }
   //---
protected:
   //--- Проверка наличия указателя на форму
   bool              CheckWindowPointer(void);
   //--- Проверка идентификатора активированного элемента
   bool              CheckIdActivatedElement(void);
  
   //--- Расчёт абсолютных координат
   int               CalculateX(const int x_gap);
   int               CalculateY(const int y_gap);
   //--- Расчёт относительных координат от крайней точки формы
   int               CalculateXGap(const int x);
   int               CalculateYGap(const int y);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CElement::CElement(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CElement::~CElement(void)
  {
  }

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

//+------------------------------------------------------------------+
//| Проверка идентификатора активированного элемента                 |
//+------------------------------------------------------------------+
bool CElement::CheckIdActivatedElement(void)
  {
   return(m_wnd.IdActivatedElement()==CElementBase::Id());
  }
//+------------------------------------------------------------------+
//| Расчёт абсолютной X-координаты                                   |
//+------------------------------------------------------------------+
int CElement::CalculateX(const int x_gap)
  {
   return((CElementBase::AnchorRightWindowSide())? m_wnd.X2()-x_gap : m_wnd.X()+x_gap);
  }
//+------------------------------------------------------------------+
//| Расчёт абсолютной Y-координаты                                   |
//+------------------------------------------------------------------+
int CElement::CalculateY(const int y_gap)
  {
   return((CElementBase::AnchorBottomWindowSide())? m_wnd.Y2()-y_gap : m_wnd.Y()+y_gap);
  }
//+------------------------------------------------------------------+
//| Расчёт относительной X-координаты от крайней точки формы         |
//+------------------------------------------------------------------+
int CElement::CalculateXGap(const int x)
  {
   return((CElementBase::AnchorRightWindowSide())? m_wnd.X2()-x : x-m_wnd.X());
  }
//+------------------------------------------------------------------+
//| Расчёт относительной Y-координаты от крайней точки формы         |
//+------------------------------------------------------------------+
int CElement::CalculateYGap(const int y)
  {
   return((CElementBase::AnchorBottomWindowSide())? m_wnd.Y2()-y : y-m_wnd.Y());
  }

Может возникнуть вопрос: “Почему эти методы не были размещены в старой версии класса CElement?”. Сделать это было невозможно: при подключении файла Window.mqh и компиляции возникала ошибка отсутствия типа, и как следствие — множество других сопутствующих ошибок:

 Рис. 1. Сообщение при компиляции об отсутствии типа CElement.

Рис. 1. Сообщение при компиляции об отсутствии типа CElement


Если же попытаться обойти эту сложность и подключить файл Window.mqh после тела класса CElement, когда в теле этого класса уже объявлен объект типа CWindow, то при компиляции мы увидим уже знакомую ошибку отсутствия указываемого типа:

 Рис. 2. Сообщение при компиляции об отсутствии типа CWindow.

Рис. 2. Сообщение при компиляции об отсутствии типа CWindow


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

 Рис. 3. Часть схемы библиотеки относительно взаимосвязей между формой и элементами.

Рис. 3. Часть схемы библиотеки относительно взаимосвязей между формой и элементами


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

 

Программное управление полосой прокрутки

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

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

//+------------------------------------------------------------------+
//| Класс для управления вертикальной полосой прокрутки              |
//+------------------------------------------------------------------+
class CScrollV : public CScroll
  {
public:
   //--- Перемещает ползунок на указанную позицию
   void              MovingThumb(const int pos=WRONG_VALUE);
  };
//+------------------------------------------------------------------+
//| Перемещает ползунок на указанную позицию                         |
//+------------------------------------------------------------------+
void CScrollV::MovingThumb(const int pos=WRONG_VALUE)
  {
//--- Выйти, если полоса прокрутки не нужна
   if(m_items_total<=m_visible_items_total)
      return;
//--- Для проверки позиции ползунка
   uint check_pos=0;
//--- Скорректируем позицию в случае выхода из диапазона
   if(pos<0 || pos>m_items_total-m_visible_items_total)
      check_pos=m_items_total-m_visible_items_total;
   else
      check_pos=pos;
//--- Запомним позицию ползунка
   CScroll::CurrentPos(check_pos);
//--- Расчёт и установка координаты Y ползунка полосы прокрутки
   CalculateThumbY();
  }

 

Программное управление списками

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

  • Реконструкция списка
  • Добавление пункта в конец списка
  • Очищение списка (удаление всех пунктов)
  • Прокрутка списка

Кроме этого, в рамках оптимизации кода библиотеки в классы списков добавлены приватные методы для повторяющегося кода:

  • Расчёт Y-координаты пункта
  • Расчёт ширины пунктов
  • Расчёт размера списка по оси Y

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

//+------------------------------------------------------------------+
//| Класс для создания списка                                        |
//+------------------------------------------------------------------+
class CListView : public CElement
  {
private:
   //--- Расчёт Y-координаты пункта
   int               CalculationItemY(const int item_index=0);
   //--- Расчёт ширины пунктов
   int               CalculationItemsWidth(void);
   //--- Расчёт размера списка по оси Y
   int               CalculationYSize(void);
//+------------------------------------------------------------------+
//| Расчёт Y-координаты пункта                                       |
//+------------------------------------------------------------------+
int CListView::CalculationItemY(const int item_index=0)
  {
   return((item_index>0)? m_items[item_index-1].Y2()-1 : CElementBase::Y()+1);
  }
//+------------------------------------------------------------------+
//| Расчёт ширины пунктов                                            |
//+------------------------------------------------------------------+
int CListView::CalculationItemsWidth(void)
  {
   return((m_items_total>m_visible_items_total) ? CElementBase::XSize()-m_scrollv.ScrollWidth()-1 : CElementBase::XSize()-2);
  }
//+------------------------------------------------------------------+
//| Расчёт размера списка по оси Y                                   |
//+------------------------------------------------------------------+
int CListView::CalculationYSize(void)
  {
   return(m_item_y_size*m_visible_items_total-(m_visible_items_total-1)+2);
  }

Очищение списка говорит само за себя: все пункты из списка удаляются. Для этого нужно использовать метод CListView::Clear(). Здесь сначала удаляются графические объекты-примитивы, освобождается массив указателей на эти объекты и устанавливаются значения по умолчанию для некоторых полей класса. После этого устанавливается нулевой размер списка и сбрасываются параметры полосы прокрутки. В самом конце метода нужно снова добавить в массив указателей элемента указатель на фон списка, так как до этого он был удалён методом CElementBase::FreeObjectsArray().

//+------------------------------------------------------------------+
//| Класс для создания списка                                        |
//+------------------------------------------------------------------+
class CListView : public CElement
  {
public:
   //--- Очищает список (удаление всех пунктов)
   void              Clear(void);
  };
//+------------------------------------------------------------------+
//| Очищает список (удаление всех пунктов)                           |
//+------------------------------------------------------------------+
void CListView::Clear(void)
  {
//--- Удалить объекты-пункты
   for(int r=0; r<m_visible_items_total; r++)
      m_items[r].Delete();
//--- Очистить массив указателей на объекты
   CElementBase::FreeObjectsArray();
//--- Установить значения по умолчанию
   m_selected_item_text  ="";
   m_selected_item_index =0;
//--- Установить нулевой размер списка
   ListSize(0);
//--- Сбросить значения скролла
   m_scrollv.Hide();
   m_scrollv.MovingThumb(0);
   m_scrollv.ChangeThumbSize(m_items_total,m_visible_items_total);
//--- Добавить фон списка в массив указателей на объекты элемента
   CElementBase::AddToArray(m_area);
  }

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

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

//+------------------------------------------------------------------+
//| Класс для создания списка                                        |
//+------------------------------------------------------------------+
class CListView : public CElement
  {
public:
   //--- Реконструкция списка
   void              Rebuilding(const int items_total,const int visible_items_total);
  };
//+------------------------------------------------------------------+
//| Реконструкция списка                                             |
//+------------------------------------------------------------------+
void CListView::Rebuilding(const int items_total,const int visible_items_total)
  {
//--- Очистка списка
   Clear();
//--- Установим размер списка и его видимой части
   ListSize(items_total);
   VisibleListSize(visible_items_total);
//--- Скорректировать размеры списка
   int y_size=CalculationYSize();
   if(y_size!=CElementBase::YSize())
     {
      m_area.YSize(y_size);
      m_area.Y_Size(y_size);
      CElementBase::YSize(y_size);
     }
//--- Скорректировать размеры полосы прокрутки
   m_scrollv.ChangeThumbSize(m_items_total,m_visible_items_total);
   m_scrollv.ChangeYSize(y_size);
//--- Создать список
   CreateList();
//--- Отобразить полосу прокрутки, если нужно
   if(m_items_total>m_visible_items_total)
     {
      if(CElementBase::IsVisible())
         m_scrollv.Show();
     }
  }

Для создания одного пункта теперь реализован отдельный метод CListView::CreateItem(), так как при добавлении пункта в список в процессе выполнения программы его код будет использоваться в методе CListView::AddItem(), а не только при создании всего списка в цикле в методе CListView::CreateList(). 

В методе CListView::AddItem() принимается только один аргумент – отображаемый текст пункта. По умолчанию это пустая строка. Текст можно добавить и после создания с помощью метода CListView::SetItemValue(). В самом начале метода CListView::AddItem() массив пунктов увеличивается на один элемент. Затем, в случае, если общее количество пунктов на текущий момент не больше видимого количества пунктов, то это означает, что нужно создать графический объект. Если же мы уже вышли за пределы видимого количества, то нужно показать полосу прокрутки и скорректировать размер её ползунка, а также скорректировать ширину пунктов. 

//+------------------------------------------------------------------+
//| Класс для создания списка                                        |
//+------------------------------------------------------------------+
class CListView : public CElement
  {
public:
   //--- Добавляет пункт в список
   void              AddItem(const string value="");
  };
//+------------------------------------------------------------------+
//| Добавляет пункт в список                                         |
//+------------------------------------------------------------------+
void CListView::AddItem(const string value="")
  {
//--- Увеличим размер массива на один элемент
   int array_size=ItemsTotal();
   m_items_total=array_size+1;
   ::ArrayResize(m_item_value,m_items_total);
   m_item_value[array_size]=value;
//--- Если общее кол-во пунктов больше видимого
   if(m_items_total>m_visible_items_total)
     {
      //--- Скорректировать размер ползунка и показать полосу прокрутки
      m_scrollv.ChangeThumbSize(m_items_total,m_visible_items_total);
      if(CElementBase::IsVisible())
         m_scrollv.Show();
      //--- Выйти, если массив меньше одного элемента
      if(m_visible_items_total<1)
         return;
      //--- Расчёт ширины пунктов списка
      int width=CElementBase::XSize()-m_scrollv.ScrollWidth()-1;
      if(width==m_items[0].XSize())
         return;
      //--- Установить новый размер пунктам списка
      for(int i=0; i<m_items_total && i<m_visible_items_total; i++)
        {
         m_items[i].XSize(width);
         m_items[i].X_Size(width);
        }
      //---
      return;
     }
//--- Расчёт координат
   int x=CElementBase::X()+1;
   int y=CalculationItemY(array_size);
//--- Расчёт ширины пунктов списка
   int width=CalculationItemsWidth();
//--- Создание объекта
   CreateItem(array_size,x,y,width);
//--- Подсветка выделенного пункта
   HighlightSelectedItem();
//--- Сохраним текст выделенного пункта
   if(array_size==1)
      m_selected_item_text=m_item_value[0];
  }

Для программной прокрутки списка предназначен метод CListView::Scrolling(). В качестве единственного аргумента принимается номер позиции в списке. По умолчанию установлено значение WRONG_VALUE, что означает смещение списка на последнюю позицию. 

//+------------------------------------------------------------------+
//| Класс для создания списка                                        |
//+------------------------------------------------------------------+
class CListView : public CElement
  {
public:
   //--- Прокрутка списка
   void              Scrolling(const int pos=WRONG_VALUE);
  };
//+------------------------------------------------------------------+
//| Прокрутка списка                                                 |
//+------------------------------------------------------------------+
void CListView::Scrolling(const int pos=WRONG_VALUE)
  {
//--- Выйти, если полоса прокрутки не нужна
   if(m_items_total<=m_visible_items_total)
      return;
//--- Для определения позиции ползунка
   int index=0;
//--- Индекс последней позиции
   int last_pos_index=m_items_total-m_visible_items_total;
//--- Корректировка в случае выхода из диапазона
   if(pos<0)
      index=last_pos_index;
   else
      index=(pos>last_pos_index)? last_pos_index : pos;
//--- Сдвигаем ползунок полосы прокрутки
   m_scrollv.MovingThumb(index);
//--- Сдвигаем список
   UpdateList(index);
  }

Аналогичные методы реализованы также для списка типа CCheckBoxList

 

Оптимизация кода таблицы типа CTable

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

  • Изменение размера массивов ряда
  • Инициализация ячеек значениями по умолчанию
  • Расчёт размера таблицы по оси X
  • Расчёт размера таблицы по оси Y
  • Расчёт X-координаты ячейки
  • Расчёт Y-координаты ячейки
  • Расчёт ширины столбца
  • Изменение ширины столбцов
  • Изменение размера таблицы по оси Y
//+------------------------------------------------------------------+
//| Класс для создания таблицы из полей ввода                        |
//+------------------------------------------------------------------+
class CTable : public CElement
  {
private:
   //--- Изменение размера массивов ряда
   void              RowResize(const uint column_index,const uint new_size);
   //--- Инициализация ячеек значениями по умолчанию
   void              CellInitialize(const uint column_index,const int row_index=WRONG_VALUE);
   //--- Расчёт размера таблицы по оси X
   int               CalculationXSize(void);
   //--- Расчёт размера таблицы по оси Y
   int               CalculationYSize(void);
   //--- Расчёт X-координаты ячейки
   int               CalculationCellX(const int column_index=0);
   //--- Расчёт Y-координаты ячейки
   int               CalculationCellY(const int row_index=0);
   //--- Расчёт ширины столбца
   int               CalculationColumnWidth(const bool is_last_column=false);
   //--- Изменение ширины столбцов
   void              ColumnsXResize(void);
   //--- Изменение размера таблицы по оси Y
   void              YResize(void);
  };

Метод CTable::CalculationColumnWidth() предназначен для расчёта ширины столбцов таблицы и принимает только один аргумент, значение которого равно false. Со значением по умолчанию рассчитывается общая ширина для столбцов. Если передать значение true, то будет рассчитана ширина для последнего столбца. В этом случае используется рекурсивный вызов метода. Разделение на расчёт общей ширины и ширины последнего столбца необходимо, так как при общем расчёте правая граница последнего столбца может не сойтись с правой границей таблицы.

//+------------------------------------------------------------------+
//| Расчёт ширины столбца                                            |
//+------------------------------------------------------------------+
int CTable::CalculationColumnWidth(const bool is_last_column=false)
  {
   int width=0;
//--- Проверка на наличие вертикальной полосы прокрутки
   bool is_scrollv=m_rows_total>m_visible_rows_total;
//---
   if(!is_last_column)
     {
      if(m_visible_columns_total==1)
         width=(is_scrollv)? m_x_size-m_scrollv.ScrollWidth() : width=m_x_size-2;
      else
        {
         if(is_scrollv)
            width=(m_x_size-m_scrollv.ScrollWidth())/int(m_visible_columns_total);
         else
            width=m_x_size/(int)m_visible_columns_total+1;
        }
     }
   else
     {
      width=CalculationColumnWidth();
      int last_column=(int)m_visible_columns_total-1;
      int w=m_x_size-(width*last_column-last_column);
      width=(is_scrollv) ? w-m_scrollv.ScrollWidth()-1 : w-2;
     }
//---
   return(width);
  }

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

//+------------------------------------------------------------------+
//| Изменение ширины столбцов                                        |
//+------------------------------------------------------------------+
void CTable::ColumnsXResize(void)
  {
//--- Расчёт ширины столбцов
   int width=CalculationColumnWidth();
//--- Столбцы
   for(uint c=0; c<m_columns_total && c<m_visible_columns_total; c++)
     {
      //--- Расчёт координаты X
      int x=CalculationCellX(c);
      //--- Корректировка ширины последнего столбца
      if(c+1>=m_visible_columns_total)
         width=CalculationColumnWidth(true);

      //--- Ряды
      for(uint r=0; r<m_rows_total && r<m_visible_rows_total; r++)
        {
         //--- Координаты
         m_columns[c].m_rows[r].X(x);
         m_columns[c].m_rows[r].X_Distance(x);
         //--- Ширина
         m_columns[c].m_rows[r].XSize(width);
         m_columns[c].m_rows[r].X_Size(width);
         //--- Отступы от крайней точки панели
         m_columns[c].m_rows[r].XGap(CalculateXGap(x));
        }
     }
//--- Выйти, если таблица не отсортирована
   if(m_is_sorted_column_index==WRONG_VALUE)
      return;
//--- Смещение на один индекс, если включен режим закреплённых заголовков
   int l=(m_fix_first_column) ? 1 : 0;
//--- Получим текущие позиции ползунков горизонтальной и вертикальной полос прокрутки
   int h=m_scrollh.CurrentPos()+l;
//--- Если не выходим из диапазона
   if(m_is_sorted_column_index>=h && m_is_sorted_column_index<(int)m_visible_columns_total)
     {
      //--- Смещение стрелки на отсортированный столбец таблицы
      ShiftSortArrow(m_is_sorted_column_index);
     }
  }

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

Кроме методов, описанных выше, в рамках оптимизации был реализован отдельный приватный метод CTable::CreateCell() для создания ячейки таблицы. Ещё одно удобное дополнение для таблицы типа CTable в этом обновлении — автоформатирование в стиле «Зебра». Ранее, если пользователю библиотеки нужно было сделать таблицу полосатой для лучшего восприятия массива данных, он должен был воспользоваться методом CTable::CellColor(). То есть, нужно было для всех ячеек таблицы назначить свой цвет. Это не очень удобно и отнимает время. Теперь, чтобы сделать таблицу полосатой, нужно просто перед созданием элемента вызвать метод CTable::IsZebraFormatRows(), передав в качестве единственного аргумента второй цвет. В качестве первого цвета используется значение, которое задаётся методом CTable::CellColor() для всех ячеек таблицы (по умолчанию — белый). 

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

Если второй цвет для форматирования в стиле «Зебра» задан, то везде, где это необходимо, вызывается приватный метод CTable::ZebraFormatRows(). 

//+------------------------------------------------------------------+
//| Класс для создания таблицы из полей ввода                        |
//+------------------------------------------------------------------+
class CTable : public CElement
  {
private:
   //--- Форматирует таблицу в стиле "Зебра"
   void              ZebraFormatRows(void);
  };
//+------------------------------------------------------------------+
//| Форматирует таблицу в стиле "Зебра"                              |
//+------------------------------------------------------------------+
void CTable::ZebraFormatRows(void)
  {
//--- Выйти, если режим отключен
   if(m_is_zebra_format_rows==clrNONE)
      return;
//--- Цвет по умолчанию
   color clr=m_cell_color;
//---
   for(uint c=0; c<m_columns_total; c++)
     {
      for(uint r=0; r<m_rows_total; r++)
        {
         if(m_fix_first_row)
           {
            if(r==0)
               continue;
            //---
            clr=(r%2==0)? m_is_zebra_format_rows : m_cell_color;
           }
         else
            clr=(r%2==0)? m_cell_color : m_is_zebra_format_rows;
         //--- Установить цвет фона ячейки в общий массив
         m_vcolumns[c].m_cell_color[r]=clr;
        }
     }
  }

 

Программное управление таблицей типа CTable

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

  • Реконструкция таблицы
  • Добавление столбца
  • Добавление ряда
  • Очищение таблицы (удаление всех столбцов и рядов)
  • Горизонтальная и вертикальная прокрутка таблицы
//+------------------------------------------------------------------+
//| Класс для создания таблицы из полей ввода                        |
//+------------------------------------------------------------------+
class CTable : public CElement
  {
public:
   //--- Реконструкция таблицы
   void              Rebuilding(const int columns_total,const int visible_columns_total,const int rows_total,const int visible_rows_total);
   //--- Добавляет столбец в таблицу
   void              AddColumn(void);
   //--- Добавляет ряд в таблицу
   void              AddRow(void);
   //--- Очищает таблицу (удаление всех стобцов и рядов)
   void              Clear(void);
   //--- Прокрутка таблицы: (1) вертикальная и (2) горизонтальная
   void              VerticalScrolling(const int pos=WRONG_VALUE);
   void              HorizontalScrolling(const int pos=WRONG_VALUE);
  };

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

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

//+------------------------------------------------------------------+
//| Реконструкция таблицы                                            |
//+------------------------------------------------------------------+
void CTable::Rebuilding(const int columns_total,const int visible_columns_total,const int rows_total,const int visible_rows_total)
  {
//--- Очистка таблицы
   Clear();
//--- Установим размер таблицы и её видимой части
   TableSize(columns_total,rows_total);
   VisibleTableSize(visible_columns_total,visible_rows_total);
//--- Скорректировать размеры полос прокрутки
   m_scrollv.ChangeThumbSize(rows_total,visible_rows_total);
   m_scrollh.ChangeThumbSize(columns_total,visible_columns_total);
//--- Проверка на наличие вертикальной полосы прокрутки
   bool is_scrollv=m_rows_total>m_visible_rows_total;
//--- Проверка на наличие горизонтальной полосы прокрутки
   bool is_scrollh=m_columns_total>m_visible_columns_total;
//--- Рассчитаем размер таблицы по оси Y
   int y_size=CalculationYSize();
//--- Установим новый размер для вертикальной полосы прокрутки
   m_scrollv.ChangeYSize(y_size);
//--- Установим новый размер таблице
   m_y_size=(is_scrollh)? y_size+m_scrollh.ScrollWidth()-1 : y_size;
   m_area.YSize(m_y_size);
   m_area.Y_Size(m_y_size);
//--- Скорректируем положение горизонтальной полосы прокрутки по оси Y
   m_scrollh.YDistance(CElementBase::Y2()-m_scrollh.ScrollWidth());
//--- Если нужна гориз. полоса прокрутки
   if(is_scrollh)
     {
      //--- Установим размер относительно наличия вертикальной полосы прокрутки
      if(!is_scrollv)
         m_scrollh.ChangeXSize(m_x_size);
      else
        {
         //--- Рассчитать и изменить ширину горизонтальной полосы прокрутки
         int x_size=m_area.XSize()-m_scrollh.ScrollWidth()+1;
         m_scrollh.ChangeXSize(x_size);
        }
     }
//--- Создать ячейки таблицы
   CreateCells();
//--- Отобразить полосу прокрутки, если нужно
   if(rows_total>visible_rows_total)
     {
      if(CElementBase::IsVisible())
         m_scrollv.Show();
     }
   if(columns_total>visible_columns_total)
     {
      if(CElementBase::IsVisible())
         m_scrollh.Show();
     }
  }

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

В начале метода CTable::AddColumn() устанавливается размер массиву столбцов и рядов в этом столбце. Затем с помощью метода CTable::CellInitialize() осуществляется инициализация ячеек добавленного столбца значениями по умолчанию. После этого, если общее количество столбцов не больше установленного видимого количества: 

  1. Осуществляется расчёт ширины столбцов
  2. Создаётся определённое количество графических объектов (ячеек таблицы) для добавленного столбца
  3. В случае необходимости осуществляется форматирование таблицы в стиле «Зебра»
  4. И в самом конце метода таблица обновляется

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

//+------------------------------------------------------------------+
//| Добавляет столбец в таблицу                                      |
//+------------------------------------------------------------------+
void CTable::AddColumn(void)
  {
//--- Увеличим размер массива на один элемент
   uint array_size=ColumnsTotal();
   m_columns_total=array_size+1;
   ::ArrayResize(m_vcolumns,m_columns_total);
//--- Установить размер массивам рядов
   RowResize(array_size,m_rows_total);
//--- Инициализация массивов значениями по умолчанию
   CellInitialize(array_size);
//--- Если общее количество столбцов больше видимого количества
   if(m_columns_total>m_visible_columns_total)
     {
      //--- Скорректировать размер таблицы по оси Y
      YResize();
      //--- Если нет вертикальной полосы прокрутки, сделать горизонтальную полосу прокрутки на всю ширину таблицы
      if(m_rows_total<=m_visible_rows_total)
         m_scrollh.ChangeXSize(m_x_size);
      //--- Скорректировать размер ползунка и показать полосу прокрутки
      m_scrollh.ChangeThumbSize(m_columns_total,m_visible_columns_total);
      //--- Показать полосу прокрутки
      if(CElementBase::IsVisible())
         m_scrollh.Show();
      //--- Форматирование рядов в стиле "Зебра"
      ZebraFormatRows();
      //--- Обновить таблицу
      UpdateTable();
      return;
     }
//--- Расчёт ширины столбцов
   int width=CalculationColumnWidth();
//--- Корректировка ширины последнего столбца
   if(m_columns_total>=m_visible_columns_total)
      width=CalculationColumnWidth(true);
//--- Расчёт координаты X
   int x=CalculationCellX(array_size);
//---
   for(uint r=0; r<m_rows_total && r<m_visible_rows_total; r++)
     {
      //--- Расчёт координаты Y
      int y=CalculationCellY(r);
      //--- Создание объекта
      CreateCell(array_size,r,x,y,width);
      //--- Установить соответствующий цвет заголовку
      if(m_fix_first_row && r==0)
         m_columns[array_size].m_rows[r].BackColor(m_headers_color);
     }
//--- Форматирование рядов в стиле "Зебра"
   ZebraFormatRows();
//--- Обновить таблицу
   UpdateTable();
  }

Методы для прокрутки таблицы CTable::VerticalScrolling() и CTable::HorizontalScrolling() практически идентичны тем, что рассматривались в разделе списков, поэтому не будем здесь приводить их код. Вы можете ознакомиться с ними самостоятельно в приложенных к статье файлах в архиве.

Далее создадим тестовое MQL-приложение, которое позволит продемонстрировать новые возможности списков и таблицы типа CTable

 

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

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

  • Кнопка «CLEAR TABLE» для очищения таблицы (удаление всех столбцов и рядов)
  • Кнопка «REBUILD TABLE» для реконструкции таблицы по заданным параметрам в числовых полях ввода
  • Поле ввода «Rows total» для указания общего количества рядов таблицы
  • Поле ввода «Columns total» для указания общего количества столбцов таблицы
  • Поле ввода «Visible rows total» для указания видимого количества рядов таблицы
  • Поле ввода «Visible columns total» для указания видимого количества столбцов таблицы

На скриншоте ниже показано, как это выглядит:

 Рис. 4. Группа элементов на первой вкладке.

Рис. 4. Группа элементов на первой вкладке


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

  • Кнопка «CLEAR LISTS» для очищения списков (удаление всех пунктов)
  • Кнопка «REBUILD LISTS» для реконструкции списков по заданным параметрам в числовых полях ввода
  • Поле ввода «Items total» для указания общего количества пунктов списков
  • Поле ввода «Visible items total» для указания видимого количества пунктов списков

 На скриншоте ниже показаны элементы на второй вкладке. В качестве дополнения на ней созданы ещё два элемента: выпадающий календарь и элемент «Время». 

 Рис. 5. Группа элементов на второй вкладке.

Рис. 5. Группа элементов на второй вкладке


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

//+------------------------------------------------------------------+
//|                                                  TimeCounter.mqh |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Счётчик времени                                                  |
//+------------------------------------------------------------------+
class CTimeCounter
  {
private:
   //--- Шаг счётчика
   uint              m_step;
   //--- Временной интервал
   uint              m_pause;
   //--- Счётчик времени
   uint              m_time_counter;
   //---
public:
                     CTimeCounter(void);
                    ~CTimeCounter(void);
   //--- Установка шага и временного интервала
   void              SetParameters(const uint step,const uint pause);
   //--- Проверяет прохождение указанного временного интервала
   bool              CheckTimeCounter(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTimeCounter::CTimeCounter(void) : m_step(16),
                                   m_pause(1000),
                                   m_time_counter(0)
                                  
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CTimeCounter::~CTimeCounter(void)
  {
  }

С помощью метода CTimeCounter::SetParameters() можно установить шаг приращения счётчика и временной интервал для паузы:

//+------------------------------------------------------------------+
//| Установка шага и временного интервала                            |
//+------------------------------------------------------------------+
void CTimeCounter::SetParameters(const uint step,const uint pause)
  {
   m_step  =step;
   m_pause =pause;
  }

Метод CTimeCounter::CheckTimeCounter() предназначен для проверки прохождения указанного в параметрах класса временного интервала. Если временной интервал пройден, то метод возвращает true.

//+------------------------------------------------------------------+
//| Проверяет прохождение указанного временного интервала            |
//+------------------------------------------------------------------+
bool CTimeCounter::CheckTimeCounter(void)
  {
//--- Увеличим счётчик, если не прошли указанный временной интервал
   if(m_time_counter<m_pause)
     {
      m_time_counter+=m_step;
      return(false);
     }
//--- Обнулить счётчик
   m_time_counter=0;
   return(true);
  }

Прежде чем двигаться дальше, стоит ещё сообщить, что изменилось расположение файлов в директориях разрабатываемой библиотеки. Теперь в директории «MetaTrader 5\MQL5\Include\EasyAndFastGUI\Controls» расположены только те файлы, которые содержат классы элементов управления. Все остальные файлы перенесены в корневую директорию библиотеки: «MetaTrader 5\MQL5\Include\EasyAndFastGUI». Поэтому для подключения библиотеки к файлу своего пользовательского класса нужно написать путь, как показано в листинге ниже. Здесь также показано подключение файла с классом CTimeCounter (будет использоваться в тестовых примерах). 

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <EasyAndFastGUI\WndEvents.mqh>
#include <EasyAndFastGUI\TimeCounter.mqh>

Установку параметров временных счётчиков расположим в конструкторе пользовательского класса:

//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
protected:
   //--- Временные счётчики
   CTimeCounter      m_counter1; // для обновления статусной строки
   CTimeCounter      m_counter2; // для обновления списков и таблицы
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void)
  {
//--- Установка параметров для временных счётчиков
   m_counter1.SetParameters(16,500);
   m_counter2.SetParameters(16,150);
  }

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

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CProgram::OnTimerEvent(void)
  {
   CWndEvents::OnTimerEvent();
...
//--- Пауза между обновлением элементов
   if(m_counter2.CheckTimeCounter())
     {
      //--- Добавить ряд в таблицу, если общее количество меньше указанного
      if(m_table.RowsTotal()<m_spin_edit1.GetValue())
         m_table.AddRow();
      //--- Добавить столбец в таблицу, если общее кол-во меньше указанного
      if(m_table.ColumnsTotal()<m_spin_edit2.GetValue())
         m_table.AddColumn();
      //--- Добавить пункт в список, если общее кол-во меньше указанного
      if(m_listview.ItemsTotal()<m_spin_edit5.GetValue())
        {
         m_listview.AddItem("SYMBOL "+string(m_listview.ItemsTotal()));
         //--- Переместить ползунок полосы прокрутки в конец списка
         m_listview.Scrolling();
        }
      //--- Добавить пункт в список из чек-боксов, если общее кол-во меньше указанного
      if(m_checkbox_list.ItemsTotal()<m_spin_edit5.GetValue())
        {
         m_checkbox_list.AddItem("Checkbox "+string(m_checkbox_list.ItemsTotal()));
         //--- Переместить ползунок полосы прокрутки в конец списка
         m_checkbox_list.Scrolling();
        }
      //--- Перерисовать график
      m_chart.Redraw();
     }
  }

Обработка нажатий на кнопки для очищения и реконструкции списков и таблицы выглядит так: 

//+------------------------------------------------------------------+
//| Обработчик событий графика                                       |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Событие нажатия на кнопке
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      Print(__FUNCTION__," > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
      //--- Событие от первой кнопки
      if(lparam==m_simple_button1.Id())
        {
         //--- Очистить таблицу
         m_table.Clear();
         return;
        }
      //--- Событие от второй кнопки
      if(lparam==m_simple_button2.Id())
        {
         //--- Реконструировать таблицу
         m_table.Rebuilding((int)m_spin_edit3.GetValue(),(int)m_spin_edit4.GetValue(),
                            (int)m_spin_edit1.GetValue(),(int)m_spin_edit2.GetValue());
         //--- Инициализация таблицы
         InitializeTable();
         //--- Обновить таблицу для отображения изменений
         m_table.UpdateTable();
         return;
        }
      //--- Событие от третьей кнопки
      if(lparam==m_simple_button3.Id())
        {
         //--- Очистить списки
         m_listview.Clear();
         m_checkbox_list.Clear();
         return;
        }
      //--- Событие от четвёртой кнопки
      if(lparam==m_simple_button4.Id())
        {
         //--- Реконструировать списки
         m_checkbox_list.Rebuilding((int)m_spin_edit5.GetValue(),(int)m_spin_edit6.GetValue());
         m_listview.Rebuilding((int)m_spin_edit5.GetValue(),(int)m_spin_edit6.GetValue());
         //--- Выделить восьмой пункт в простом списке
         m_listview.SelectItem(7);
         //--- Заполнение списка данными
         int items_total=m_listview.ItemsTotal();
         for(int i=0; i<items_total; i++)
            m_listview.SetItemValue(i,"SYMBOL "+string(i));
         //--- Заполнение списка из чекбоксов данными, отметить чекбоксы через один
         items_total=m_checkbox_list.ItemsTotal();
         for(int i=0; i<items_total; i++)
           {
            m_checkbox_list.SetItemValue(i,"Checkbox "+string(i));
            m_checkbox_list.SetItemState(i,(i%2!=0)? true : false);
           }
         //---
         return;
        }
      //---
      return;
     }
  }

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

 

Заключение

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

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

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


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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (112)
Juer
Juer | 19 мая 2018 в 19:08
Anatoli Kazharski:

Если просто нужно обновить значения, то ничего удалять не нужно.

Если нужно изменить количество строк в таблице, то показан один из способов, как это можно сделать. 

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

Спасибо, посмотрю еще раз.

Но у меня CCanvasTable. А какая разница вообще между CCanvasTable и CTable? Вроде, я и с CCanvasTable создаю таблицу нормально..

Anatoli Kazharski
Anatoli Kazharski | 19 мая 2018 в 19:15
Juer:

Спасибо, посмотрю еще раз.

Но у меня CCanvasTable. А какая разница вообще между CCanvasTable и CTable? Вроде, я и с CCanvasTable создаю таблицу нормально..

Скачайте последнюю версию библиотеки: EasyAndFast и обновлённые файлы с классами в этой статье: Торговый эксперт с графическим интерфейсом: Наполнение функционалом (Часть II)

Используйте класс CTable. CCanvasTable совсем старая версия с минимальными возможностями. 

Anatoli Kazharski
Anatoli Kazharski | 19 мая 2018 в 19:28
Juer:

Скачал, но что-то не могу найти метод IsSortedColumnIndex() или его аналог.

...обновлённые файлы с классами в этой статье: Торговый эксперт с графическим интерфейсом: Наполнение функционалом (Часть II)

Класс CTable:

   //--- (1) Текущее направление сортировки, (2) индекс отсортированного массива
   int               IsSortDirection(void)             const { return(m_last_sort_direction);    }
   int               IsSortedColumnIndex(void)         const { return(m_is_sorted_column_index); }
Anatoli Kazharski
Anatoli Kazharski | 19 мая 2018 в 19:33
Juer:

...

DeleteColumn(), DeleteRow() тоже исчезли.

Может нужно быть просто немного внимательней?

   //--- Реконструкция таблицы
   void              Rebuilding(const int columns_total,const int rows_total,const bool redraw=false);
   //--- Добавляет столбец в таблицу по указанному индексу
   void              AddColumn(const int column_index,const bool redraw=false);
   //--- Удаляет столбец в таблице по указанному индексу
   void              DeleteColumn(const int column_index,const bool redraw=false);
   //--- Добавляет строку в таблицу по указанному индексу
   void              AddRow(const int row_index,const bool redraw=false);
   //--- Удаляет строку в таблице по указанному индексу
   void              DeleteRow(const int row_index,const bool redraw=false);
   //--- Удаляет все строки
   void              DeleteAllRows(const bool redraw=false);
   //--- Очищает таблицу. Остаётся только один столбец и одна строка.
   void              Clear(const bool redraw=false);
Juer
Juer | 19 мая 2018 в 19:35
Anatoli Kazharski:

...обновлённые файлы с классами в этой статье: Торговый эксперт с графическим интерфейсом: Наполнение функционалом (Часть II)

Класс CTable:

Извиняюсь, мой косяк. Копирую в одно место, а смотрю в другом MetaEditor. Спасибо.

Как построить и протестировать стратегию бинарных опционов в Тестере Стратегий MetaTrader 4 Как построить и протестировать стратегию бинарных опционов в Тестере Стратегий MetaTrader 4
Руководство по построению стратегии бинарных опционов и ее тестированию в Тестере Стратегий MetaTrader 4 с использованием утилиты Binary-Options-Strategy-Tester из Маркета на MQL5.com.
3D-моделирование на MQL5 3D-моделирование на MQL5
Временной ряд — это динамическая система, в которой значения некоторой случайной величины поступают последовательно — непрерывно или через некоторые промежутки времени. Переход от плоского к объёмному анализу рынка позволяет по-новому взглянуть на сложные процессы и явления, интересующие исследователя. В статье описаны функции визуализации для 3-D представления двумерных данных.
ZUP - зигзаг универсальный с паттернами Песавенто. Графический интерфейс ZUP - зигзаг универсальный с паттернами Песавенто. Графический интерфейс
За 10 лет, прошедших с момента выхода первой версии платформы ZUP, произошло множество изменений и улучшений. В результате получилась уникальная графическая надстройка к MetaTrader 4, позволяющая быстро и комфортно проводить анализ рыночной информации. В статье рассказывается как работать с графическим интерфейсом индикаторной платформы ZUP.
Графические интерфейсы X: Элемент "Время", элемент "Список из чекбоксов" и сортировка таблицы (build 6) Графические интерфейсы X: Элемент "Время", элемент "Список из чекбоксов" и сортировка таблицы (build 6)
Продолжаем развивать библиотеку для создания графических интерфейсов. На этот раз будут представлены такие элементы, как «Время» и «Список из чекбоксов». Кроме этого, в класс таблицы типа CTable добавлена возможность сортировать данные по возрастанию и убыванию.