English 中文 Español Deutsch 日本語 Português
Графические интерфейсы VIII: Элемент "Древовидный список" (Глава 2)

Графические интерфейсы VIII: Элемент "Древовидный список" (Глава 2)

MetaTrader 5Примеры | 29 июня 2016, 12:23
3 268 1
Anatoli Kazharski
Anatoli Kazharski

Содержание

 

 

Введение

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

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

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

 

 

Элемент "Древовидный список"

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

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

В торговом терминале MetaTrader показать/скрыть окно «Навигатор» (см. скриншот ниже) можно с помощью сочетания клавиш ‘Ctrl+N’: 

 Рис. 1. Окно «Навигатор» в торговом терминале MetaTrader.

Рис. 1. Окно «Навигатор» в торговом терминале MetaTrader.

В редакторе кода MetaEditor показать/скрыть окно «Навигатор» (см. скриншот ниже) можно с помощью сочетания клавиш ‘Ctrl+D’: 

 Рис. 2. Окно «Навигатор» в редакторе кода MetaEditor.

Рис. 2. Окно «Навигатор» в редакторе кода MetaEditor.

 

 

Разработка класса CTreeItem для создания пункта древовидного списка

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

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

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

Перечислим составные части пункта древовидного списка.

  1. Фон
  2. Признак наличия локального списка пунктов. Используются картинки в виде стрелок или пиктограмм плюс/минус, отражающих статус списка (открыт/свёрнут).
  3. Ярлык пункта. Например, он может потребоваться для визуального отнесения пункта к какой-либо категории. 
  4. Отображаемое текстовое описание пункта.


Рис. 3. Составные части пункта древовидного списка. 

Рис. 3. Составные части пункта древовидного списка.

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

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

В файле TreeItem.mqh теперь нужно создать класс CTreeItem со стандартными методами для всех элементов библиотеки (см. листинг кода ниже):

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

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

  • Цвета фона пункта в разных состояниях
  • Ярлык пункта
  • Картинки для стрелки, показывающие текущий статус (открытый/свёрнутый) локального списка пункта
  • Отступы текстовой метки
  • Цвета текста в разных состояниях

class CTreeItem : public CElement
  {
private:
   //--- Цвета фона в разных состояниях
   color             m_item_back_color;
   color             m_item_back_color_hover;
   color             m_item_back_color_selected;
   //--- Картинки для стрелки пункта
   string            m_item_arrow_file_on;
   string            m_item_arrow_file_off;
   string            m_item_arrow_selected_file_on;
   string            m_item_arrow_selected_file_off;
   //--- Ярлык пункта
   string            m_icon_file;
   //--- Отступы текстовой метки
   int               m_label_x_gap;
   int               m_label_y_gap;
   //--- Цвета текста в разных состояниях пункта
   color             m_item_text_color;
   color             m_item_text_color_hover;
   color             m_item_text_color_selected;
   //---
public:
   //--- Цвета фона пункта
   void              ItemBackColor(const color clr)                   { m_item_back_color=clr;                    }
   void              ItemBackColorHover(const color clr)              { m_item_back_color_hover=clr;              }
   void              ItemBackColorSelected(const color clr)           { m_item_back_color_selected=clr;           }
   //--- (1) Установка ярлыка для пункта, (2) установка картинок для стрелки пункта
   void              IconFile(const string file_path)                 { m_icon_file=file_path;                    }
   void              ItemArrowFileOn(const string file_path)          { m_item_arrow_file_on=file_path;           }
   void              ItemArrowFileOff(const string file_path)         { m_item_arrow_file_off=file_path;          }
   void              ItemArrowSelectedFileOn(const string file_path)  { m_item_arrow_selected_file_on=file_path;  }
   void              ItemArrowSelectedFileOff(const string file_path) { m_item_arrow_selected_file_off=file_path; }
   //--- (1) Возвращает текст пункта, (2) установка отступов для текстовой метки
   string            LabelText(void)                            const { return(m_label.Description());            }
   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              ItemTextColor(const color clr)                   { m_item_text_color=clr;                    }
   void              ItemTextColorHover(const color clr)              { m_item_text_color_hover=clr;              }
   void              ItemTextColorSelected(const color clr)           { m_item_text_color_selected=clr;           }
  };

Для создания пункта древовидного списка понадобятся четыре приватных (private) метода и один публичный (public). Обратите внимание, что в качестве текстовой метки здесь будет использоваться графический объект типа CEdit. О том, для чего это нужно, мы поговорим более конкретно чуть ниже.

class CTreeItem : public CElement
  {
private:
   //--- Объекты для создания пункта древовидного списка
   CRectLabel        m_area;
   CBmpLabel         m_arrow;
   CBmpLabel         m_icon;
   CEdit             m_label;
   //---
public:
   //--- Методы для создания пункта древовидного списка
   bool              CreateTreeItem(const long chart_id,const int subwin,const int x,const int y,const ENUM_TYPE_TREE_ITEM type,
                                    const int list_index,const int node_level,const string item_text,const bool item_state);
   //---
private:
   bool              CreateArea(void);
   bool              CreateArrow(void);
   bool              CreateIcon(void);
   bool              CreateLabel(void);
  };

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

Перечислим ключевые свойства здесь.

  • Тип пункта может выбираться из двух вариантов: (1) простой пункт (TI_SIMPLE), который не содержит в себе других пунктов и является конечным узлом, и (2) содержащий в себе пункты (TI_HAS_ITEMS). Для этого в файле Enums.mqh добавляется перечисление ENUM_TYPE_TREE_ITEM:  
//+------------------------------------------------------------------+
//| Перечисление типов пункта древовидного списка                    |
//+------------------------------------------------------------------+
enum ENUM_TYPE_TREE_ITEM
  {
   TI_SIMPLE    =0,
   TI_HAS_ITEMS =1
  };

  • Общий индекс в списке.
  • Уровень (номер) узла.
  • Отображаемый текст (описание).
  • Состояние пункта.

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

class CTreeItem : public CElement
  {
private:
   //--- Отступ для стрелки (признака наличия списка)
   int               m_arrow_x_offset;
   //--- Тип пункта
   ENUM_TYPE_TREE_ITEM m_item_type;
   //--- Индекс пункта в общем списке
   int               m_list_index;
   //--- Уровень узла
   int               m_node_level;
   //--- Отображаемый текст пункта
   string            m_item_text;
   //--- Состояние списка пункта (открыт/свёрнут)
   bool              m_item_state;
  };
//+------------------------------------------------------------------+
//| Создаёт пункт древовидного списка                                |
//+------------------------------------------------------------------+
bool CTreeItem::CreateTreeItem(const long chart_id,const int subwin,const int x,const int y,const ENUM_TYPE_TREE_ITEM type,
                               const int list_index,const int node_level,const string item_text,const bool item_state)
  {
//--- Выйти, если нет указателя на форму
   if(::CheckPointer(m_wnd)==POINTER_INVALID)
     {
      ::Print(__FUNCTION__," > Перед созданием пункта списка классу нужно передать "
              "указатель на форму: CTreeItem::WindowPointer(CWindow &object)");
      return(false);
     }
//--- Инициализация переменных
   m_id             =m_wnd.LastId()+1;
   m_chart_id       =chart_id;
   m_subwin         =subwin;
   m_x              =x;
   m_y              =y;
   m_item_type      =type;
   m_list_index     =list_index;
   m_node_level     =node_level;
   m_item_text      =item_text;
   m_item_state     =item_state;
   m_arrow_x_offset =(m_node_level>0)? (12*m_node_level)+5 : 5;
//--- Отступы от крайней точки
   CElement::XGap(CElement::X()-m_wnd.X());
   CElement::YGap(CElement::Y()-m_wnd.Y());
//--- Создание пункта меню
   if(!CreateArea())
      return(false);
   if(!CreateArrow())
      return(false);
   if(!CreateIcon())
      return(false);
   if(!CreateLabel())
      return(false);
//--- Скрыть элемент, если окно диалоговое или оно минимизировано
   if(m_wnd.WindowType()==W_DIALOG || m_wnd.IsMinimized())
      Hide();
//---
   return(true);
  }

Формирование имени объектов элемента будет осуществляться из следующих составляющих (см. листинг кода ниже):

  • Имя программы
  • Индекс элемента
  • Признак принадлежности к элементу («treeitem»)
  • Признак принадлежности к части элемента
  • Общий индекс пункта древовидного списка
  • Идентификатор элемента

В качестве примера приведём один из приватных методов создания объектов-составляющих элемента — CTreeItem::CreateArrow(). По умолчанию в библиотеке уже есть картинки для признака выпадающего списка. Загрузить их к себе на компьютер можно из приложенного архива в конце статьи. При необходимости их можно переопределить перед созданием элемента. 

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

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

//+------------------------------------------------------------------+
//| Создаёт стрелку (признак выпадающего списка)                     |
//+------------------------------------------------------------------+
#resource "\\Images\\EasyAndFastGUI\\Controls\\RightTransp_black.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\RightTransp_white.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\RightTransp_rotate_black.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\RightTransp_rotate_white.bmp"
//---
bool CTreeItem::CreateArrow(void)
  {
//--- Рассчитаем координаты
   int x =CElement::X()+m_arrow_x_offset;
   int y =CElement::Y()+2;
//--- Сохраним координаты для расчёта координат следующих в очереди создания объектов элемента
   m_arrow.X(x);
   m_arrow.Y(y);
//--- Выйдем, если пункт не имеет выпадающего списка
   if(m_item_type!=TI_HAS_ITEMS)
      return(true);
//--- Формирование имени объекта
   string name=CElement::ProgramName()+"_"+(string)CElement::Index()+"_treeitem_arrow_"+(string)m_list_index+"__"+(string)CElement::Id();
//--- Установить картинки по умолчанию
   if(m_item_arrow_file_on=="")
      m_item_arrow_file_on="Images\\EasyAndFastGUI\\Controls\\RightTransp_rotate_black.bmp";
   if(m_item_arrow_file_off=="")
      m_item_arrow_file_off="Images\\EasyAndFastGUI\\Controls\\RightTransp_black.bmp";
   if(m_item_arrow_selected_file_on=="")
      m_item_arrow_selected_file_on="Images\\EasyAndFastGUI\\Controls\\RightTransp_rotate_white.bmp";
   if(m_item_arrow_selected_file_off=="")
      m_item_arrow_selected_file_off="Images\\EasyAndFastGUI\\Controls\\RightTransp_white.bmp";
//--- Установим объект
   if(!m_arrow.Create(m_chart_id,name,m_subwin,x,y))
      return(false);
//--- Установим свойства
   m_arrow.BmpFileOn("::"+m_item_arrow_file_on);
   m_arrow.BmpFileOff("::"+m_item_arrow_file_off);
   m_arrow.State(m_item_state);
   m_arrow.Corner(m_corner);
   m_arrow.GetInteger(OBJPROP_ANCHOR,m_anchor);
   m_arrow.Selectable(false);
   m_arrow.Z_Order(m_arrow_zorder);
   m_arrow.Tooltip("\n");
//--- Отступы от крайней точки
   m_arrow.XGap(x-m_wnd.X());
   m_arrow.YGap(y-m_wnd.Y());
//--- Сохраним указатель объекта
   CElement::AddToArray(m_arrow);
   return(true);
  }

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

При изменении ширины областей будут обновляться (1) X-координаты объектов пунктов списка содержания и (2) ширина объектов пунктов обоих списков. Вот почему здесь для отображаемого текста пунктов используется объект типа CEdit, а не CLabel. Если в этом случае использовать объекты типа CLabel, то при изменении размеров областей списков Вы столкнётесь с проблемой, когда текстовые метки могут выйти за границы формы (окна).

Для обновления X-координаты напишем метод CTreeItem::UpdateX(). Чтобы установить X-координату, её нужно передать в этот метод. В данном случае все расчёты будут производиться в классе древовидного списка. К этому вопросу мы еще вернемся чуть ниже. 

class CTreeItem : public CElement
  {
public:
   void              UpdateX(const int x);
  };
//+------------------------------------------------------------------+
//| Обновление координаты X                                          |
//+------------------------------------------------------------------+
void CTreeItem::UpdateX(const int x)
  {
//--- Обновление общих координат и отступа от крайней точки
   CElement::X(x);
   CElement::XGap(CElement::X()-m_wnd.X());
//--- Координаты и отступ фона
   m_area.X_Distance(CElement::X());
   m_area.XGap(CElement::X()-m_wnd.X());
//--- Координаты и отступ стрелки
   int l_x=CElement::X()+m_arrow_x_offset;
   m_arrow.X(l_x);
   m_arrow.X_Distance(l_x);
   m_arrow.XGap(l_x-m_wnd.X());
//--- Координаты и отступ картинки
   l_x=m_arrow.X()+17;
   m_icon.X(l_x);
   m_icon.X_Distance(l_x);
   m_icon.XGap(l_x-m_wnd.X());
//--- Координаты и отступ текстовой метки
   l_x=(m_icon_file=="")? m_icon.X() : m_icon.X()+m_label_x_gap;
   m_label.X(l_x);
   m_label.X_Distance(l_x);
   m_label.XGap(l_x-m_wnd.X());
  }

Для изменения ширины пункта списка нужно использовать метод CTreeItem::UpdateWidth(): 

class CTreeItem : public CElement
  {
public:
   void              UpdateWidth(const int width);
  };
//+------------------------------------------------------------------+
//| Обновление ширины                                                |
//+------------------------------------------------------------------+
void CTreeItem::UpdateWidth(const int width)
  {
//--- Ширина фона
   CElement::XSize(width);
   m_area.XSize(width);
   m_area.X_Size(width);
//--- Ширина текстовой метки
   int w=CElement::X2()-m_label.X()-1;
   m_label.XSize(w);
   m_label.X_Size(w);
  }

Кроме обновления X-координаты и ширины пунктов, понадобится метод для обновления Y-координаты. Дело в том, что реализация обоих списков элемента подразумевает при реализации элемента  (CTreeView) создание всех пунктов списка сразу, а не только их отображаемое количество, как это сделано в типах CListView, CLabelsTable и CTable рассмотренных в предыдущих статьях серии. Здесь будет использоваться технология, когда непомещающиеся в область списка пункты будут скрываться. То есть, вместо постоянного обновления значений нескольких параметров объектов при прокрутке списка или сворачивании/разворачивании локальных списков пунктов, здесь просто будет осуществляться управление видимостью пунктов. Когда ненужные в данный момент пункты достаточно просто скрыть, то в момент показа нужно обновить Y-координату. Для этого напишем метод CTreeItem::UpdateY(), с кодом которого можно ознакомиться в листинге кода ниже. 

class CTreeItem : public CElement
  {
public:
   void              UpdateY(const int y);
  };
//+------------------------------------------------------------------+
//| Обновление координаты Y                                          |
//+------------------------------------------------------------------+
void CTreeItem::UpdateY(const int y)
  {
//--- Обновление общих координат и отступа от крайней точки
   CElement::Y(y);
   CElement::YGap(CElement::Y()-m_wnd.Y());
//--- Координаты и отступ фона
   m_area.Y_Distance(CElement::Y());
   m_area.YGap(CElement::Y()-m_wnd.Y());
//--- Координаты и отступ стрелки
   int l_y=CElement::Y()+2;
   m_arrow.Y(l_y);
   m_arrow.Y_Distance(l_y);
   m_arrow.YGap(l_y-m_wnd.Y());
//--- Координаты и отступ картинки
   l_y=CElement::Y()+2;
   m_icon.Y(l_y);
   m_icon.Y_Distance(l_y);
   m_icon.YGap(l_y-m_wnd.Y());
//--- Координаты и отступ текстовой метки
   l_y=CElement::Y()+m_label_y_gap;
   m_label.Y(l_y);
   m_label.Y_Distance(l_y);
   m_label.YGap(l_y-m_wnd.Y());
  }

Управление цветом пунктов будет осуществляться в классе древовидного списка (CTreeView). Для этого понадобятся два метода:

  • для изменения цвета пункта относительно указанного состояния;
  • для изменения цвета объекта при наведении курсора.

Код этих методов можно посмотреть в листинге ниже: 

class CTreeItem : public CElement
  {
public:
   //--- Изменение цвета пункта относительно указанного состояния
   void              HighlightItemState(const bool state);
   //--- Изменение цвета по наведению курсора мыши
   void              ChangeObjectsColor(void);
  };
//+------------------------------------------------------------------+
//| Изменение цвета пункта относительно указанного состояния         |
//+------------------------------------------------------------------+
void CTreeItem::HighlightItemState(const bool state)
  {
   m_area.BackColor((state)? m_item_back_color_selected : m_item_back_color);
   m_label.BackColor((state)? m_item_back_color_selected : m_item_back_color);
   m_label.BorderColor((state)? m_item_back_color_selected : m_item_back_color);
   m_label.Color((state)? m_item_text_color_selected : m_item_text_color);
   m_arrow.BmpFileOn((state)? "::"+m_item_arrow_selected_file_on : "::"+m_item_arrow_file_on);
   m_arrow.BmpFileOff((state)? "::"+m_item_arrow_selected_file_off : "::"+m_item_arrow_file_off);
  }
//+------------------------------------------------------------------+
//| Изменение цвета объекта при наведении курсора                    |
//+------------------------------------------------------------------+
void CTreeItem::ChangeObjectsColor(void)
  {
   if(CElement::MouseFocus())
     {
      m_area.BackColor(m_item_back_color_hover);
      m_label.BackColor(m_item_back_color_hover);
      m_label.BorderColor(m_item_back_color_hover);
      m_label.Color(m_item_text_color_hover);
     }
   else
     {
      m_area.BackColor(m_item_back_color);
      m_label.BackColor(m_item_back_color);
      m_label.BorderColor(m_item_back_color);
      m_label.Color(m_item_text_color);
     }
  }

Далее рассмотрим класс, предназначение которого — создание в графическом интерфейсе указателей для курсора мыши. 

 


Класс CPointer для создания указателя курсора мыши

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

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

В той же директории, где расположены файлы, библиотеки создаём файл Pointer.mqh с классом CPointer. Для создания указателя будет использоваться объект типа CBmpLabel. Класс CBmpLabel расположен в файле Objects.mqh, содержание которого было рассмотрено в первой части серии этих статей.

//+------------------------------------------------------------------+
//|                                                      Pointer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
//+------------------------------------------------------------------+
//| Класс для создания указателя курсора мыши                        |
//+------------------------------------------------------------------+
class CPointer : public CElement
  {
private:
   //--- Объект для создания элемента
   CBmpLabel         m_pointer_bmp;
   /---
public:
                     CPointer(void);
                    ~CPointer(void);
   //---
public:
   //--- Перемещение элемента
   virtual void      Moving(const int x,const int y);
   //--- (1) Показ, (2) скрытие, (3) сброс, (4) удаление
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CPointer::CPointer(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CPointer::~CPointer(void)
  {
  }

В текущей версии библиотеки можно будет установить один из четырёх типов указателей для курсора мыши. Для удобства использования добавим в файл Enums.mqh перечисление ENUM_MOUSE_POINTER (см. листинг кода ниже). Кроме четырёх предустановленных типов, можно будет выбрать пользовательский тип (MP_CUSTOM). 

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

Описание перечисления из листинга выше:

  • MP_CUSTOM — пользовательский тип.
  • MP_X_RESIZE — изменение горизонтальных размеров.
  • MP_Y_RESIZE — изменение вертикальных размеров.
  • MP_XY1_RESIZE — изменение размеров по диагонали 1.
  • MP_XY2_RESIZE — изменение размеров по диагонали 2.

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

//--- Ресурсы
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_resize.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_resize_blue.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_y_resize.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_y_resize_blue.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_xy1_resize.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_xy1_resize_blue.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_xy2_resize.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_xy2_resize_blue.bmp"

Если же вы желаете установить для указателя свои картинки, то тогда из перечисления ENUM_MOUSE_POINTER нужно выбрать тип MP_CUSTOM и воспользоваться методами CPointer::FileOn() и CPointer::FileOff() для указания пути к картинкам. 

class CPointer : public CElement
  {
private:
   //--- Картинки для указателя
   string            m_file_on;
   string            m_file_off;
   //--- Тип указателя
   ENUM_MOUSE_POINTER m_type;
   //---
public:
   //--- Установка ярлыков для указателя
   void              FileOn(const string file_path)       { m_file_on=file_path;         }
   void              FileOff(const string file_path)      { m_file_off=file_path;        }
   //--- Возвращение и установка типа указателя
   ENUM_MOUSE_POINTER Type(void)                    const { return(m_type);              }
   void              Type(ENUM_MOUSE_POINTER type)        { m_type=type;                 }
  };

Ещё здесь понадобятся методы для обновления координат и возвращения/установки состояния указателя: 

class CPointer : public CElement
  {
public:
   //--- Возвращение и установка состояния указателя
   bool              State(void)                    const { return(m_pointer_bmp.State()); }
   void              State(const bool state)              { m_pointer_bmp.State(state);    }
   //--- Обновление координат
   void              UpdateX(const int x)                 { m_pointer_bmp.X_Distance(x);   }
   void              UpdateY(const int y)                 { m_pointer_bmp.Y_Distance(y);   }
  };

По умолчанию в конструкторе класса для указателя установлен тип MP_X_RESIZE

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CPointer::CPointer(void) : m_file_on(""),
                           m_file_off(""),
                           m_type(MP_X_RESIZE)
  {
  }

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

class CPointer : public CElement
  {
private:
   //--- Установка картинок для указателя курсора мыши
   void              SetPointerBmp(void);
  };
//+------------------------------------------------------------------+
//| Установка картинок для указателя по типу указателя               |
//+------------------------------------------------------------------+
void CPointer::SetPointerBmp(void)
  {
   switch(m_type)
     {
      case MP_X_RESIZE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_x_resize_blue.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_resize.bmp";
         break;
      case MP_Y_RESIZE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_y_resize_blue.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_y_resize.bmp";
         break;
      case MP_XY1_RESIZE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_xy1_resize_blue.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_xy1_resize.bmp";
         break;
      case MP_XY2_RESIZE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_xy2_resize_blue.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_xy2_resize.bmp";
         break;
     }
//--- Если указан пользовательский тип (MP_CUSTOM)
   if(m_file_on=="" || m_file_off=="")
      ::Print(__FUNCTION__," > Для указателя курсора должны быть установлены обе картинки!");
  }

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

class CPointer : public CElement
  {
public:
   //--- Создаёт ярлык указателя
   bool              CreatePointer(const long chart_id,const int subwin);
  };
//+------------------------------------------------------------------+
//| Создаёт указатель                                                |
//+------------------------------------------------------------------+
bool CPointer::CreatePointer(const long chart_id,const int subwin)
  {
//--- Формирование имени объекта
   string name=CElement::ProgramName()+"_pointer_bmp_"+(string)CElement::Index()+"__"+(string)CElement::Id();
//--- Установка картинок для указателя
   SetPointerBmp();
//--- Создание объекта
   if(!m_pointer_bmp.Create(m_chart_id,name,m_subwin,0,0))
      return(false);
//--- Установка свойств
   m_pointer_bmp.BmpFileOn("::"+m_file_on);
   m_pointer_bmp.BmpFileOff("::"+m_file_off);
   m_pointer_bmp.Corner(m_corner);
   m_pointer_bmp.Selectable(false);
   m_pointer_bmp.Z_Order(0);
   m_pointer_bmp.Tooltip("\n");
//--- Скрыть объект
   m_pointer_bmp.Timeframes(OBJ_NO_PERIODS);
   return(true);
  }

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

 


Разработка класса CTreeView для создания древовидного списка

Создаём файл TreeView.mqh с классом CTreeView со стандартными методами, как делали это для всех элементов библиотеки, и подключаем его к файлу WndContainer.mqh:

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

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

  1. Фон древовидного списка
  2. Список пунктов древовидного списка
  3. Вертикальная полоса прокрутки древовидного списка
  4. Фон списка содержания
  5. Список пунктов содержания
  6. Вертикальная полоса прокрутки списка содержания
  7. Указатель курсора мыши, чтобы отслеживать, как изменяется ширина области древовидного списка и области содержания


Рис. 4. Составные части элемента «Древовидный список». 

Рис. 4. Составные части элемента «Древовидный список».

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

Для создания элемента «Древовидный список» понадобятся семь приватных (private) методов и один публичный (public): 

class CTreeView : public CElement
  {
private:
   //--- Объекты для создания элемента
   CRectLabel        m_area;
   CRectLabel        m_content_area;
   CTreeItem         m_items[];
   CTreeItem         m_content_items[];
   CScrollV          m_scrollv;
   CScrollV          m_content_scrollv;
   CPointer          m_x_resize;
   //---
public:
   //--- Методы для создания древовидного списка
   bool              CreateTreeView(const long chart_id,const int subwin,const int x,const int y);
   //---
private:
   bool              CreateArea(void);
   bool              CreateContentArea(void);
   bool              CreateItems(void);
   bool              CreateScrollV(void);
   bool              CreateContentItems(void);
   bool              CreateContentScrollV(void);
   bool              CreateXResizePointer(void);
  };

Для примера приведём код только одного из них – метода CTreeView::CreateXResizePointer() для создания указателя курсора мыши (см. листинг кода ниже). Здесь нужно обратить внимание на следующие нюансы:

  • Указатель курсора не будет создан, если одно из условий ниже истинно:
    • не включен режим, позволяющий изменять ширину списков. 
    • включен режим пунктов-вкладок.
  • Отступы указателя здесь будут рассчитываться от системного курсора мыши (этот тип элемента не привязывается к окну, как остальные элементы).
  • Если в элементе нужно несколько указателей, то для каждого нужно установить свой индекс элемента. Так как в текущей версии древовидного списка будет использоваться только один указатель, то здесь это свойство можно пропустить, поскольку по умолчанию в конструкторе указателя он инициализируется значением 0.
  • Нужно обязательно указать идентификатор элемента, к которому подключен указатель. Это нужно, чтобы не возникло конфликтов имён графических объектов из разных элементов, где могут использоваться указатели курсора. 

//+------------------------------------------------------------------+
//| Создаёт указатель курсора изменения ширины                       |
//+------------------------------------------------------------------+
bool CTreeView::CreateXResizePointer(void)
  {
//--- Выйти, если ширину области содержания не нужно изменять или
//    включен режим пунктов-вкладок
   if(!m_resize_content_area_mode || m_tab_items_mode)
      return(true);
//--- Установка свойств
   m_x_resize.XGap(12);
   m_x_resize.YGap(9);
   m_x_resize.Id(CElement::Id());
   m_x_resize.Type(MP_X_RESIZE);
//--- Создание элемента
   if(!m_x_resize.CreatePointer(m_chart_id,m_subwin))
      return(false);
//---
   return(true);
  }

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

Например, нужно сразу учесть некоторые особенности при разработке в будущем классов для создания файловых навигаторов. Обратите внимание на то, как устроен файловый навигатор в проводнике операционной системы Windows. Если включен показ древовидного списка в левой части окна (в Windows 7 он называется «Область переходов»), то в этом списке Вы не увидите пунктов, которые являются файлами – только папки. Все файлы в проводнике системы отображаются только в области содержания (см. скриншот ниже).

Рис. 5. Файловый навигатор в проводнике Windows 7. Слева - древовидный список. Справа - область содержания.

Рис. 5. Файловый навигатор в проводнике Windows 7. Слева - древовидный список. Справа - область содержания.

Но часто возникает необходимость отображения в древовидном списке не только папок, но и файлов. Например, можно сделать так, что при выделении файла в древовидном списке его содержимое отображается в правой области. Поэтому, если класс элемента типа CTreeView используется для создания файлового навигатора, нужно предоставить пользователю библиотеки на выбор два режима: (1) отображать в древовидном списке папки и файлы или (2) только папки. Для этого в файл Enums.mqh добавляется перечисление ENUM_FILE_NAVIGATOR_MODE (см. листинг кода ниже).

//+------------------------------------------------------------------+
//| Перечисление режимов файлового навигатора                        |
//+------------------------------------------------------------------+
enum ENUM_FILE_NAVIGATOR_MODE
  {
   FN_ALL          =0,
   FN_ONLY_FOLDERS =1
  };

Не всегда бывает нужно, чтобы содержимое пункта отображалось в области содержания, поэтому предусмотрим возможность отключить его показ. Например, это могло бы пригодиться, когда древовидный список используется как вкладки, к которым привязаны группы элементов управления библиотеки, подобно тому, как это было представлено в статье Графические интерфейсы VII: Элементы "Вкладки" (Глава 2)

Кроме этого, обеспечим ещё дополнительные режимы для более точной настройки элемента «Древовидный список». Ниже представлен полный список режимов текущей версии.

  • Режим файлового навигатора
  • Режим подсветки при наведении курсора
  • Режим показа содержания пункта в области содержания
  • Режим изменения ширины области содержания
  • Режим пунктов-вкладок

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

class CTreeView : public CElement
  {
private:
   //--- Режим файлового навигатора
   ENUM_FILE_NAVIGATOR_MODE m_file_navigator_mode;
   //--- Режим подсветки при наведении курсора
   bool              m_lights_hover;
   //--- Режим показа содержания пункта в рабочей области
   bool              m_show_item_content;
   //--- Режим изменения ширины списков
   bool              m_resize_list_area_mode;
   //--- Режим пунктов-вкладок
   bool              m_tab_items_mode;
   //---
public:
   //--- (1) Режим файлового навигатора, (2) режим подсветки при наведении курсора мыши, 
   //    (3) режим показа содержания пункта, (4) режим изменения ширины списков, (5) режим пунктов-вкладок
   void              NavigatorMode(const ENUM_FILE_NAVIGATOR_MODE mode) { m_file_navigator_mode=mode;                   }
   void              LightsHover(const bool state)                      { m_lights_hover=state;                         }
   void              ShowItemContent(const bool state)                  { m_show_item_content=state;                    }
   void              ResizeListAreaMode(const bool state)               { m_resize_list_area_mode=state;                }
   void              TabItemsMode(const bool state)                     { m_tab_items_mode=state;                       }
  };

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

  • Ширина области древовидного списка
  • Цвет фона
  • Цвет рамки фона
  • Ширина области содержания
  • Высота пунктов в списках
  • Цвета фона пунктов в разных состояниях
  • Цвета текста пунктов в разных состояниях
  • Картинки для признака наличия содержания в пункте (по умолчанию картинка в виде стрелки вправо)

class CTreeView : public CElement
  {
private:
   //--- Ширина области древовидного списка
   int               m_treeview_area_width;
   //--- Цвет фона и рамки фона
   color             m_area_color;
   color             m_area_border_color;
   //--- Ширина области содержания
   int               m_content_area_width;
   //--- Высота пунктов
   int               m_item_y_size;
   //--- Цвета пунктов в разных состояниях
   color             m_item_back_color_hover;
   color             m_item_back_color_selected;
   //--- Цвета текста в разных состояниях
   color             m_item_text_color;
   color             m_item_text_color_hover;
   color             m_item_text_color_selected;
   //--- Картинки для стрелок
   string            m_item_arrow_file_on;
   string            m_item_arrow_file_off;
   string            m_item_arrow_selected_file_on;
   string            m_item_arrow_selected_file_off;
   //---
public:
   //--- (1) Высота пункта, (2) ширина древовидного списка и (3) списка содержания
   void              ItemYSize(const int y_size)                        { m_item_y_size=y_size;                         }
   void              TreeViewAreaWidth(const int x_size)                { m_treeview_area_width=x_size;                 }
   void              ContentAreaWidth(const int x_size)                 { m_content_area_width=x_size;                  }
   //--- Цвет фона и рамки фона элемента
   void              AreaBackColor(const color clr)                     { m_area_color=clr;                             }
   void              AreaBorderColor(const color clr)                   { m_area_border_color=clr;                      }
   //--- Цвета пунктов в разных состояниях
   void              ItemBackColorHover(const color clr)                { m_item_back_color_hover=clr;                  }
   void              ItemBackColorSelected(const color clr)             { m_item_back_color_selected=clr;               }
   //--- Цвета текста в разных состояниях
   void              ItemTextColor(const color clr)                     { m_item_text_color=clr;                        }
   void              ItemTextColorHover(const color clr)                { m_item_text_color_hover=clr;                  }
   void              ItemTextColorSelected(const color clr)             { m_item_text_color_selected=clr;               }
   //--- Картинки для стрелки пункта
   void              ItemArrowFileOn(const string file_path)            { m_item_arrow_file_on=file_path;               }
   void              ItemArrowFileOff(const string file_path)           { m_item_arrow_file_off=file_path;              }
   void              ItemArrowSelectedFileOn(const string file_path)    { m_item_arrow_selected_file_on=file_path;      }
   void              ItemArrowSelectedFileOff(const string file_path)   { m_item_arrow_selected_file_off=file_path;     }
  };

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

class CTreeView : public CElement
  {
private:
   //--- Индексы выделенных пунктов в списках
   int               m_selected_item_index;
   int               m_selected_content_item_index;
   //---
public:
   //--- (1) Выделяет пункт по индексу и (2) возвращает индекс выделенного пункта
   void              SelectedItemIndex(const int index)                 { m_selected_item_index=index;              }
   int               SelectedItemIndex(void)                      const { return(m_selected_item_index);            }
  };

 


Параметры для формирования списков элемента

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

  • Общий индекс списка

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

Например, для пунктов A, B, C, D, E и F общие индексы списка будут 0, 1, 2, 3, 4 и 5 соответственно. Допустим, пункты B и C находятся в пункте A, а пункт E находится в пункте D. Для наглядности на рисунке ниже показаны два варианта состояния древовидного списка. В левой части рисунка (1) показан полностью развёрнутый список и видно непрерывную последовательную его индексацию. В правой части рисунка (2) показан вариант, когда списки пунктов A и D, в которых содержатся пункты B, C и E, свёрнуты, и при этом сохраняются их прежние индексы, присвоенные во время формирования и инициализации массивов древовидного списка.

 Рис. 6. Слева 1) – полностью развёрнутый список. Справа 2) – свёрнутый список.

Рис. 6. Слева 1) – полностью развёрнутый список. Справа 2) – свёрнутый список.

  • Общий индекс списка предыдущего узла

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

На рисунке ниже видно, что пунктам A, D и H в корневом каталоге присвоены значения -1. Пунктам B и C присвоен общий индекс списка [0] предыдущего узла A, а пунктам E, F и G – общий индекс списка [3] предыдущего узла D.

 Рис. 7. Всем дочерним элементам узла присваивается его общий индекс списка.

Рис. 7. Всем дочерним элементам узла присваивается его общий индекс списка.

  • Описание пункта (отображаемый текст в пункте)

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


  • Номер уровня узла

Счёт начинается с нуля. То есть, пункты в корневом каталоге имеют значение 0. Далее, по мере увеличения уровня вложенности, значение для вложенного пункта увеличивается на единицу. Для однозначного понимания посмотрите на рисунок ниже. На горизонтальной шкале сверху столбцам пунктов присвоены номера их уровней. Уровень узлов A, D и J имеет значение 0. У пунктов B, C, E, I и K это номер 1, а у пунктов F, G и H – номер 2.

Рис. 8. Номер узла привязан к уровню его вложенности. 

Рис. 8. Номер узла привязан к уровню его вложенности.


  • Локальный индекс

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

 Рис. 9. Индексация локальных списков.

Рис. 9. Индексация локальных списков.


  • Локальный индекс предыдущего узла

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

 Рис. 10. Локальные индексы предыдущего узла.

Рис. 10. Локальные индексы предыдущего узла.


  • Количество пунктов (размер массива локального списка узла). 

От этого значения зависит, какой тип (из перечисления ENUM_TYPE_TREE_ITEM) будет присваиваться пункту в поле класса CTreeItem. На нижеприведенной схеме количество пунктов во всех локальных списках представлено в численном виде. Видно, что в пунктах A, D, E и J есть содержание (2, 2, 3 и 1 соответственно), в пунктах B, C, F, G, H, I и K – пусто (0).

 Рис. 11. Количество элементов в локальных списках узлов.

Рис. 11. Количество элементов в локальных списках узлов.

  • Состояние пункта. 

Свёрнут/развёрнут в случае наличия локального списка (содержания). Позволяет сразу перед созданием древовидного списка указать, какие пункты должны быть развёрнуты.

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

Рис. 12. Сводная таблица ключевых (значимых) параметров для определения пунктов в древовидном списке. 

Рис. 12. Сводная таблица ключевых (значимых) параметров для определения пунктов в древовидном списке.

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

А вот при создании обычного древовидного списка его структуру нужно формировать самостоятельно. Но в любом случае в обоих вариантах для этого будет использоваться один и тот же метод CTreeView::AddItem(), с помощью которого можно добавлять пункт в древовидный список с указанными параметрами, которые нужно передать в качестве аргументов. В листинге кода ниже показан код этого метода и список массивов, в которых будут храниться все параметры пунктов. Обратите внимание, что для каждого пункта можно установить уникальную картинку.

class CTreeView : public CElement
  {
private:
   //--- Массивы для всех пунктов древовидного списка (общий список)
   int               m_t_list_index[];
   int               m_t_prev_node_list_index[];
   string            m_t_item_text[];
   string            m_t_path_bmp[];
   int               m_t_item_index[];
   int               m_t_node_level[];
   int               m_t_prev_node_item_index[];
   int               m_t_items_total[];
   int               m_t_folders_total[];
   bool              m_t_item_state[];
   bool              m_t_is_folder[];
   //---
public:
   //--- Добавляет пункт в древовидный список
   void              AddItem(const int list_index,const int list_id,const string item_name,const string path_bmp,const int item_index,
                             const int node_number,const int item_number,const int items_total,const int folders_total,const bool item_state,const bool is_folder=true);
  };
//+------------------------------------------------------------------+
//| Добавляет пункт в общий массив древовидного списка               |
//+------------------------------------------------------------------+
void CTreeView::AddItem(const int list_index,const int prev_node_list_index,const string item_text,const string path_bmp,const int item_index,
                        const int node_level,const int prev_node_item_index,const int items_total,const int folders_total,const bool item_state,const bool is_folder)
  {
//--- Увеличим размер массивов на один элемент
   int array_size =::ArraySize(m_items);
   m_items_total  =array_size+1;
   ::ArrayResize(m_items,m_items_total);
   ::ArrayResize(m_t_list_index,m_items_total);
   ::ArrayResize(m_t_prev_node_list_index,m_items_total);
   ::ArrayResize(m_t_item_text,m_items_total);
   ::ArrayResize(m_t_path_bmp,m_items_total);
   ::ArrayResize(m_t_item_index,m_items_total);
   ::ArrayResize(m_t_node_level,m_items_total);
   ::ArrayResize(m_t_prev_node_item_index,m_items_total);
   ::ArrayResize(m_t_items_total,m_items_total);
   ::ArrayResize(m_t_folders_total,m_items_total);
   ::ArrayResize(m_t_item_state,m_items_total);
   ::ArrayResize(m_t_is_folder,m_items_total);
//--- Сохраним значения переданных параметров
   m_t_list_index[array_size]           =list_index;
   m_t_prev_node_list_index[array_size] =prev_node_list_index;
   m_t_item_text[array_size]            =item_text;
   m_t_path_bmp[array_size]             =path_bmp;
   m_t_item_index[array_size]           =item_index;
   m_t_node_level[array_size]           =node_level;
   m_t_prev_node_item_index[array_size] =prev_node_item_index;
   m_t_items_total[array_size]          =items_total;
   m_t_folders_total[array_size]        =folders_total;
   m_t_item_state[array_size]           =item_state;
   m_t_is_folder[array_size]            =is_folder;
  }

Перед созданием элемента есть возможность установить его параметры и задать начальную ширину для области древовидного списка и области содержания. По умолчанию, значение в поле класса, в которое устанавливается ширина области содержания, инициализировано значением WRONG_VALUE (см. листинг кода ниже). Это означает, что если ширину для области содержания не установить, то не будут созданы три составные части элемента: (1) фон области содержания, (2) массивы пунктов для этой области и (3) полоса прокрутки для списка в этой области. Этого не произойдет, даже если в параметрах установлен режим «Показывать содержание выделенного пункта в древовидном списке». То есть, отключить показ списка можно, оставив при этом фон, но сделать наоборот нельзя.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTreeView::CTreeView(void) : m_treeview_area_width(180),
                             m_content_area_width(WRONG_VALUE),
                             m_item_y_size(20),
                             m_visible_items_total(13),
                             m_tab_items_mode(false),
                             m_lights_hover(false),
                             m_show_item_content(true)
  {
//--- Сохраним имя класса элемента в базовом классе
   CElement::ClassName(CLASS_NAME);
//--- Установим приоритеты на нажатие левой кнопки мыши
   m_zorder=0;
  }

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

  • Общий индекс списка области содержания
  • Общий индекс древовидного списка
  • Описание пункта (отображаемый текст)

class CTreeView : public CElement
  {
private:
   //--- Массивы для списка содержания пунктов выделенных в древовидном списке (полный список)
   int               m_c_list_index[];
   int               m_c_tree_list_index[];
   string            m_c_item_text[];
  };

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

//+------------------------------------------------------------------+
//| Создаёт список содержания выделенного пункта                     |
//+------------------------------------------------------------------+
bool CTreeView::CreateContentItems(void)
  {
//--- Выйти, если содержание пункта не нужно показывать или
//    если область содержания отключена
   if(!m_show_item_content || m_content_area_width<0)
      return(true);
//--- Координаты и ширина
   int x =m_content_area.X()+1;
   int y =CElement::Y()+1;
   int w =m_content_area.X2()-x-1;
//--- Счётчик количества пунктов
   int c=0;
//--- 
   int items_total=::ArraySize(m_items);
   for(int i=0; i<items_total; i++)
     {
      //--- В этот список не должны попасть пункты из корневого каталога, 
      //    поэтому, если уровень узла меньше 1, перейдём к следующему
      if(m_t_node_level[i]<1)
         continue;
      //--- Увеличить размеры массивов на один элемент
      int new_size=c+1;
      ::ArrayResize(m_content_items,new_size);
      ::ArrayResize(m_c_item_text,new_size);
      ::ArrayResize(m_c_tree_list_index,new_size);
      ::ArrayResize(m_c_list_index,new_size);
      //--- Расчёт координаты Y
      y=(c>0)? y+m_item_y_size-1 : y;
      //--- Передадим объект панели
      m_content_items[c].WindowPointer(m_wnd);
      //--- Установим свойства перед созданием
      m_content_items[c].Index(1);
      m_content_items[c].Id(CElement::Id());
      m_content_items[c].XSize(w);
      m_content_items[c].YSize(m_item_y_size);
      m_content_items[c].IconFile(m_t_path_bmp[i]);
      m_content_items[c].ItemBackColor(m_area_color);
      m_content_items[c].ItemBackColorHover(m_item_back_color_hover);
      m_content_items[c].ItemBackColorSelected(m_item_back_color_selected);
      m_content_items[c].ItemTextColor(m_item_text_color);
      m_content_items[c].ItemTextColorHover(m_item_text_color_hover);
      m_content_items[c].ItemTextColorSelected(m_item_text_color_selected);
      //--- Координаты
      m_content_items[c].X(x);
      m_content_items[c].Y(y);
      //--- Отступы от крайней точки панели
      m_content_items[c].XGap(x-m_wnd.X());
      m_content_items[c].YGap(y-m_wnd.Y());
      //--- Создание объекта
      if(!m_content_items[c].CreateTreeItem(m_chart_id,m_subwin,x,y,TI_SIMPLE,c,0,m_t_item_text[i],false))
         return(false);
      //--- Скрыть элемент
      m_content_items[c].Hide();
      //--- Пункт будет выпадающим элементом
      m_content_items[c].IsDropdown(true);
      //--- Сохранить (1) индекс общего списка содержания, (2) индекс древовидного списка и (3) текст пункта
      m_c_list_index[c]      =c;
      m_c_tree_list_index[c] =m_t_list_index[i];
      m_c_item_text[c]       =m_t_item_text[i];
      //---
      c++;
     }
//--- Сохранить размер списка
   m_content_items_total=::ArraySize(m_content_items);
   return(true);
  }

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

Допустим, что у нас есть древовидный списо из 11 пунктов:

 Рис. 13. Модель древовидного списка из 11-ти пунктов.

Рис. 13. Модель древовидного списка из 11  пунктов.

Так как пункты A, D и J находятся в списке корневого каталога, то они не попадут в полный список пунктов для области содержания. На рисунке ниже, в левой части, (1) показан список всех пунктов древовидного списка. Динамические массивы для хранения параметров этого списка, которые рассматривались в одном из листингов выше, содержат в своих названиях префикс ‘t’ (сокращение от слова ‘tree’). В правой части рисунка изображён (2) список пунктов, которые попали в область содержания. Динамические массивы  для хранения параметров этого списка содержат в своих названиях префикс ‘c’ (сокращение от слова ‘content’).

 Рис. 14. Полные списки для обеих групп.

Рис. 14. Полные списки для обеих групп.

В рассматриваемом примере локальные списки находятся в пунктах A, D, E и J. На рисунке ниже они выделены синим цветом. 

 Рис. 15. Пункты содержащие локальные списки (выделены синим цветом).

Рис. 15. Пункты содержащие локальные списки (выделены синим цветом).

Каждый раз при выделении в древовидном списке пунктов, в которых есть содержание, список в области содержания будет переформировываться. Например, если выделить пункт A, в области содержания сформируется список из пунктов B и C (вариант 1 на рисунке). Если выделить пункт D, то сформируется список из пунктов E и I (вариант 2 на рисунке).

 Рис. 16. Пример формирования списков для области содержания.

Рис. 16. Пример формирования списков для области содержания.

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

Для всего перечисленного выше понадобится один массив для списка отображаемых пунктов в древовидном списке, в нём будут храниться общие индексы древовидного списка, и три массива для списка содержания, в нём будут храниться (1) общие индексы списка содержания, (2) общие индексы древовидного списка и (3) описание (отображаемый текст) пунктов: 

class CTreeView : public CElement
  {
private:
   //--- Массивы для списка отображаемых пунктов древовидного списка
   int               m_td_list_index[];
   //--- Массивы для списка отображаемых пунктов в списке содержания
   int               m_cd_list_index[];
   int               m_cd_tree_list_index[];
   string            m_cd_item_text[];
  };

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

 


Методы для управления списками элемента

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

class CTreeView : public CElement
  {
private:
   //--- (1) Минимальный и (2) максимальный уровень узла
   int               m_min_node_level;
   int               m_max_node_level;
   //--- Количество пунктов в корневом каталоге
   int               m_root_items_total;
   //---
private:
   //--- Определение и установка (1) границ узлов и (2) размера корневого каталога
   void              SetNodeLevelBoundaries(void);
   void              SetRootItemsTotal(void);
  };
//+------------------------------------------------------------------+
//| Определение и установка границ узлов                             |
//+------------------------------------------------------------------+
void CTreeView::SetNodeLevelBoundaries(void)
  {
//--- Определим минимальный и максимальный уровень узлов
   m_min_node_level =m_t_node_level[::ArrayMinimum(m_t_node_level)];
   m_max_node_level =m_t_node_level[::ArrayMaximum(m_t_node_level)];
  }
//+------------------------------------------------------------------+
//| Определение и установка размера корневого каталога               |
//+------------------------------------------------------------------+
void CTreeView::SetRootItemsTotal(void)
  {
//--- Определим количество пунктов в корневом каталоге
   int items_total=::ArraySize(m_items);
   for(int i=0; i<items_total; i++)
     {
      //--- Если это минимальный уровень, увеличим счётчик
      if(m_t_node_level[i]==m_min_node_level)
         m_root_items_total++;
     }
  }

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

class CTreeView : public CElement
  {
private:
   //--- Добавляет пункт в список в области содержания
   void              AddDisplayedTreeItem(const int list_index);
  };
//+------------------------------------------------------------------+
//| Добавляет пункт в массив отображаемых пунктов                    |
//| в древовидном списке                                             |
//+------------------------------------------------------------------+
void CTreeView::AddDisplayedTreeItem(const int list_index)
  {
//--- Увеличим размер массивов на один элемент
   int array_size=::ArraySize(m_td_list_index);
   ::ArrayResize(m_td_list_index,array_size+1);
//--- Сохраним значения переданных параметров
   m_td_list_index[array_size]=list_index;
  }

После того, как массив будет сформирован, нужно переставить пункты и перерисовать элемент, чтобы отобразить последние изменения. Для этих целей создадим ещё один вспомогательный метод CTreeView::RedrawTreeList(). В начале этого метода элемент скрывается. Затем (1) рассчитывается Y-координата для первого пункта, (2) корректируется размер ползунка полосы прокрутки и (3) рассчитывается ширина пунктов с учётом наличия/отсутствия полосы прокрутки. Затем в цикле на каждой итерации для каждого пункта рассчитывается Y-координата и осуществляется обновление свойств. После завершения цикла элемент снова делается видимым. 

class CTreeView : public CElement
  {
private:
   //--- Перерисовка древовидного списка
   void              RedrawTreeList(void);
  };
//+------------------------------------------------------------------+
//| Перерисовка элемента                                             |
//+------------------------------------------------------------------+
void CTreeView::RedrawTreeList(void)
  {
//--- Скрыть элемент
   Hide();
//--- Координата Y первого пункта древовидного списка
   int y=CElement::Y()+1;
//--- Получим количество пунктов
   m_items_total=::ArraySize(m_td_list_index);
//--- Скорректируем размер полосы прокрутки
   m_scrollv.ChangeThumbSize(m_items_total,m_visible_items_total);
//--- Расчёт ширины пунктов древовидного списка
   int w=(m_items_total>m_visible_items_total) ? CElement::XSize()-m_scrollv.ScrollWidth() : CElement::XSize()-2;
//--- Установим новые значения
   for(int i=0; i<m_items_total; i++)
     {
      //--- Расчёт координаты Y для каждого пункта
      y=(i>0)? y+m_item_y_size-1 : y;
      //--- Получим общий индекс пункта в списке
      int li=m_td_list_index[i];
      //--- Обновим координаты и размер
      m_items[li].UpdateY(y);
      m_items[li].UpdateWidth(w);
     }
//--- Показать элемент
   Show();
  }

Все перечисленные выше поля и методы будут вызываться в методе CTreeView::UpdateTreeViewList(), где и будет формироваться и обновляться древовидный список (см. листинг кода ниже). Рассмотрим этот метод подробнее. 

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

  • общих индексов списка предыдущих узлов;
  • локальных индексов пунктов;
  • количества пунктов в узле;
  • количества папок в узле.

Начальный размер массивов будет на два элемента больше, чем максимальное количество узлов в древовидном списке. Это нужно, чтобы значения текущего пункта (в цикле) запоминались в следующий элемент массивов, и уже на следующей итерации по текущему индексу цикла проверялись значения предыдущего пункта. Изначально массивы инициализируются значением -1. Обратите внимание также на то, что каждый раз, когда программа заходит в этот метод, буфер массива m_td_list_index[], предназначенного для формирования древовидного списка, освобождается, и массиву устанавливается нулевой размер. Ещё нам понадобятся дополнительный счётчик пунктов (ii) и переменная (end_list) для установки флага последнего пункта в корневом каталоге. 

После объявления всех локальных массивов и переменных начинает свою работу главный цикл метода CTreeView::UpdateTreeViewList(). Он будет работать до тех пор, пока:

  • счётчик узлов (nl) не превысит установленный максимум;
  • мы не дошли до последнего пункта в корневом каталоге (после проверки всех вложенных в него пунктов);
  • программу не удалил пользователь. 

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

Если прошли эти условия, то далее следуют ещё три проверки. Программа перейдёт к следующему пункту в трёх нижеописанных случаях.

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

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

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

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

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

После того, как формирование древовидного списка завершено, в самом конце метода осуществляется перерисовка элемента

class CTreeView : public CElement
  {
private:
   //--- Обновляет древовидный список
   void              UpdateTreeViewList(void);
  };
//+------------------------------------------------------------------+
//| Обновляет древовидный список                                     |
//+------------------------------------------------------------------+
void CTreeView::UpdateTreeViewList(void)
  {
//--- Массивы для контроля последовательности пунктов:
   int l_prev_node_list_index[]; // общий индекс списка предыдущего узла 
   int l_item_index[];           // локальный индекс пункта
   int l_items_total[];          // количество пунктов в узле
   int l_folders_total[];        // количество папок в узле
//--- Зададим начальный размер массивов
   int begin_size=m_max_node_level+2;
   ::ArrayResize(l_prev_node_list_index,begin_size);
   ::ArrayResize(l_item_index,begin_size);
   ::ArrayResize(l_items_total,begin_size);
   ::ArrayResize(l_folders_total,begin_size);
//--- Инициализация массивов
   ::ArrayInitialize(l_prev_node_list_index,-1);
   ::ArrayInitialize(l_item_index,-1);
   ::ArrayInitialize(l_items_total,-1);
   ::ArrayInitialize(l_folders_total,-1);
//--- Освобождаем массив отображаемых пунктов древовидного списка
   ::ArrayFree(m_td_list_index);
//--- Счётчик локальных индексов пунктов
   int ii=0;
//--- Для установки флага последнего пункта в корневом каталоге
   bool end_list=false;
//--- Собираем отображаемые пункты в массив. Цикл будет работать до тех пор, пока: 
//    1: счётчик узлов не больше максимального;
//    2: не дошли до последнего пункта (после проверки всех вложенных в него пунктов);
//    3: программу не удалил пользователь.
   int items_total=::ArraySize(m_items);
   for(int nl=m_min_node_level; nl<=m_max_node_level && !end_list; nl++)
     {
      for(int i=0; i<items_total && !::IsStopped(); i++)
        {
         //--- Если включен режим "Отображать только папки"
         if(m_file_navigator_mode==FN_ONLY_FOLDERS)
           {
            //--- Если это файл, перейти к следующему пункту
            if(!m_t_is_folder[i])
               continue;
           }
         //--- Если (1) это не наш узел или (2) последовальность локальных индексов пунктов не соблюдается,
         //    перейдём к следующему
         if(nl!=m_t_node_level[i] || m_t_item_index[i]<=l_item_index[nl])
            continue;
         //--- Перейдём к следующему пункту, если сейчас не в корневом каталоге и 
         //    общий индекс списка предыдущего узла не равен аналогичному в памяти
         if(nl>m_min_node_level && m_t_prev_node_list_index[i]!=l_prev_node_list_index[nl])
            continue;
         //--- Запомним локальный индекс пункта, если следующий будет не меньше размера локального списка
         if(m_t_item_index[i]+1>=l_items_total[nl])
            ii=m_t_item_index[i];
         //--- Если список текущего пункта развёрнут
         if(m_t_item_state[i])
           {
            //--- Добавим пункт в массив отображаемых пунктов в древовидном списке
            AddDisplayedTreeItem(i);
            //--- Запомним текущие значения и перейдём к следующему узлу
            int n=nl+1;
            l_prev_node_list_index[n] =m_t_list_index[i];
            l_item_index[nl]          =m_t_item_index[i];
            l_items_total[n]          =m_t_items_total[i];
            l_folders_total[n]        =m_t_folders_total[i];
            //--- Обнулим счётчик локальных индексов пунктов
            ii=0;
            //--- Перейдём к следующему узлу
            break;
           }
         //--- Добавим пункт в массив отображаемых пунктов в древовидном списке
         AddDisplayedTreeItem(i);
         //--- Увеличим счётчик локальных индексов пунктов
         ii++;
         //--- Если дошли до последнего пункта в корневом каталоге
         if(nl==m_min_node_level && ii>=m_root_items_total)
           {
            //--- Установим флаг и завершим текущий цикл
            end_list=true;
            break;
           }
         //--- Если до последнего пункта в корневом каталоге ещё не дошли
         else if(nl>m_min_node_level)
           {
            //--- Получим количество пунктов в текущем узле
            int total=(m_file_navigator_mode==FN_ONLY_FOLDERS)? l_folders_total[nl]: l_items_total[nl];
            //--- Если это не последний локальный индекс пункта, перейдём к следующему
            if(ii<total)
               continue;
            //--- Если дошли до последнего локального индекса, то 
            //    нужно вернуться на предыдущий узел и продолжить с пункта, на котором остановились
            while(true)
              {
               //--- Сбросим значения текущего узла в перечисленных ниже массивах
               l_prev_node_list_index[nl] =-1;
               l_item_index[nl]           =-1;
               l_items_total[nl]          =-1;
               //--- Уменьшаем счётчик узлов, пока соблюдается равенство в количестве пунктов в локальных списках 
               //    или не дошли до корневого каталога
               if(l_item_index[nl-1]+1>=l_items_total[nl-1])
                 {
                  if(nl-1==m_min_node_level)
                     break;
                  //---
                  nl--;
                  continue;
                 }
               //---
               break;
              }
            //--- Перейдём на предыдущий узел
            nl=nl-2;
            //--- Обнулим счётчик локальных индексов пунктов и перейдём к следующему узлу
            ii=0;
            break;
           }
        }
     }
//--- Перерисовка элемента
   RedrawTreeList();
  }

Смещение списков относительно ползунков полосы прокрутки будет осуществляться с помощью методов CTreeView::ShiftTreeList() и CTreeView::ShiftContentList(). Основная логика этих методов практически идентична, поэтому в качестве примера приведём здесь код только одного из них (для древовидного списка). 

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

class CTreeView : public CElement
  {
private:
   //--- Смещение списков
   void              ShiftTreeList(void);
   void              ShiftContentList(void);
  };
//+------------------------------------------------------------------+
//| Сдвигает древовидный список относительно полосы прокрутки        |
//+------------------------------------------------------------------+
void CTreeView::ShiftTreeList(void)
  {
//--- Скрыть все пункты в древовидном списке
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
      m_items[i].Hide();
//--- Если нужна полоса прокрутки
   bool is_scroll=m_items_total>m_visible_items_total;
//--- Расчёт ширины пунктов списка
   int w=(is_scroll)? m_area.XSize()-m_scrollv.ScrollWidth()-1 : m_area.XSize()-2;
//--- Определение позиции скролла
   int v=(is_scroll)? m_scrollv.CurrentPos() : 0;
   m_scrollv.CurrentPos(v);
//--- Координата Y первого пункта древовидного списка
   int y=CElement::Y()+1;
//---
   for(int r=0; r<m_visible_items_total; r++)
     {
      //--- Проверка для предотвращения выхода из диапазона
      if(v>=0 && v<m_items_total)
        {
         //--- Рассчитаем координату Y
         y=(r>0)? y+m_item_y_size-1 : y;
         //--- Получим общий индекс пункта древовидного списка
         int li=m_td_list_index[v];
         //--- Установить координаты и ширину
         m_items[li].UpdateX(m_area.X()+1);
         m_items[li].UpdateY(y);
         m_items[li].UpdateWidth(w);
         //--- Показать пункт
         m_items[li].Show();
         v++;
        }
     }
//--- Перерисовать полосу прокрутки
   if(is_scroll)
      m_scrollv.Reset();
  }

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

  • Уровни узлов
  • Локальные индексы пунктов
  • Общие индексы пунктов.

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

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

class CTreeView : public CElement
  {
private:
   //--- Обновляет список содержания
   void              UpdateContentList(void);
  };
//+------------------------------------------------------------------+
//| Обновляет список содержания                                      |
//+------------------------------------------------------------------+
void CTreeView::UpdateContentList(void)
  {
//--- Индекс выделенного пункта
   int li=m_selected_item_index;
//--- Освободим массивы списка содержания
   ::ArrayFree(m_cd_item_text);
   ::ArrayFree(m_cd_list_index);
   ::ArrayFree(m_cd_tree_list_index);
//--- Сформируем список содержания
   int items_total=::ArraySize(m_items);
   for(int i=0; i<items_total; i++)
     {
      //--- Если совпадают (1) уровни узлов и (2) локальные индексы пунктов, а также
      //    (3) индекс предыдущего узла с индексом выделенного пункта
      if(m_t_node_level[i]==m_t_node_level[li]+1 && 
         m_t_prev_node_item_index[i]==m_t_item_index[li] &&
         m_t_prev_node_list_index[i]==li)
        {
         //--- Увеличим массивы отображаемых пунктов списка содержания
         int size     =::ArraySize(m_cd_list_index);
         int new_size =size+1;
         ::ArrayResize(m_cd_item_text,new_size);
         ::ArrayResize(m_cd_list_index,new_size);
         ::ArrayResize(m_cd_tree_list_index,new_size);
         //--- Сохраним в массивах текст пункта и общий индекс древовидного списка
         m_cd_item_text[size]       =m_t_item_text[i];
         m_cd_tree_list_index[size] =m_t_list_index[i];
        }
     }
//--- Если в итоге список не пустой, заполним массив общих индексов списка содержания
   int cd_items_total=::ArraySize(m_cd_list_index);
   if(cd_items_total>0)
     {
      //--- Счётчик пунктов
      int c=0;
      //--- Пройдёмся по списку
      int c_items_total=::ArraySize(m_c_list_index);
      for(int i=0; i<c_items_total; i++)
        {
         //--- Если описание и общие индексы пунктов древовидного списка совпадают
         if(m_c_item_text[i]==m_cd_item_text[c] && 
            m_c_tree_list_index[i]==m_cd_tree_list_index[c])
           {
            //--- Сохраним общий индекс списка содержания и перейдём к следующему
            m_cd_list_index[c]=m_c_list_index[i];
            c++;
            //--- Выйти из цикла, если дошли конца отображаемого списка
            if(c>=cd_items_total)
               break;
           }
        }
     }
//--- Скорректировать размер ползунка полосы прокрутки
   m_content_scrollv.ChangeThumbSize(cd_items_total,m_visible_items_total);
//--- Скорректировать список содержания пункта
   ShiftContentList();
  }

 


Управление шириной областей списков

Теперь подробнее рассмотрим, как работает режим изменения ширины списков. Для этого понадобятся: (1) один основной приватный метод CTreeView::ResizeListArea(), в котором будут осуществляться все основные проверки и — по результатам этих проверок — изменение ширины списков, а также (2) четыре вспомогательных приватных метода для решения нижеописанных задач.

  • Метод CTreeView::CheckXResizePointer() — проверка готовности для изменения ширины списков. Здесь код метода состоит из двух блоков, привязанных к одному условию. Если указатель курсора не активирован, но курсор мыши находится в его области, обновляются координаты указателя, и он делается видимым. Если при этом левая кнопка мыши нажата, то указатель активируется. Если первое условие метода не исполняется, то указатель курсора дезактивируется и скрывается.

class CTreeView : public CElement
  {
private:
   //--- Проверка готовности для изменения ширины списков
   void              CheckXResizePointer(const int x,const int y);
  };
//+------------------------------------------------------------------+
//| Проверка готовности для изменения ширины списков                 |
//+------------------------------------------------------------------+
void CTreeView::CheckXResizePointer(const int x,const int y)
  {
//--- Если указатель не активирован, но курсор мыши в его области
   if(!m_x_resize.State() && 
      y>m_area.Y() && y<m_area.Y2() && x>m_area.X2()-2 && x<m_area.X2()+3)
     {
      //--- Обновить координаты указателя и сделать его видимым
      int l_x=x-m_x_resize.XGap();
      int l_y=y-m_x_resize.YGap();
      m_x_resize.Moving(l_x,l_y);
      m_x_resize.Show();
      //--- Установить флаг видимости
      m_x_resize.IsVisible(true);
      //--- Если левая кнопка мыши нажата
      if(m_mouse_state)
         //--- Активируем указатель
         m_x_resize.State(true);
     }
   else
     {
      //--- Если левая кнопка мыши отжата
      if(!m_mouse_state)
        {
         //--- Дезактивировать и скрыть указатель
         m_x_resize.State(false);
         m_x_resize.Hide();
         //--- Снять флаг видимости
         m_x_resize.IsVisible(false);
        }
     }
  }

  • Метод CTreeView::CheckOutOfArea() — проверка на выход за ограничения. Нет смысла изменять ширину списков так, чтобы полностью исключить из видимости какой-то один из них. Поэтому установим ограничение в 80 пикселей. Сделаем так, что если курсор вышел за установленное ограничение по горизонтали, то перемещение указателя будет возможно только по вертикали, но в рамках области смыкания областей списков. 

class CTreeView : public CElement
  {
private:
   //--- Проверка на выход за ограничения
   bool              CheckOutOfArea(const int x,const int y);
  };
//+------------------------------------------------------------------+
//| Проверка на выход за ограничения                                 |
//+------------------------------------------------------------------+
bool CTreeView::CheckOutOfArea(const int x,const int y)
  {
//--- Ограничение
   int area_limit=80;
//--- Если выходим за границы элемента по горизонтали ...
   if(x<m_area.X()+area_limit || x>m_content_area.X2()-area_limit)
     {
      // ... перемещаем указатель только по вертикали, не выходя за границы
      if(y>m_area.Y() && y<m_area.Y2())
         m_x_resize.UpdateY(y-m_x_resize.YGap());
      //--- Не изменять ширину списков
      return(false);
     }
//--- Изменить ширину списков
   return(true);
  }

  • Методы CTreeView::UpdateTreeListWidth() и CTreeView::UpdateContentListWidth() – для обновления (1) ширины древовидного списка и (2) ширины списка в области содержания. В древовидном списке будет смещаться только правая его граница. Для этого нужно изменять только его ширину. Кроме этого нужно обновлять координаты полосы прокрутки. Для списка в области содержания нужно одновременно обновлять его ширину и X-координату, чтобы визуально смещалась только левая граница. 

class CTreeView : public CElement
  {
private:
   //--- Обновление ширины древовидного списка
   void              UpdateTreeListWidth(const int x);
   //--- Обновление ширины списка в области содержания
   void              UpdateContentListWidth(const int x);
  };
//+------------------------------------------------------------------+
//| Обновление ширины древовидного списка                            |
//+------------------------------------------------------------------+
void CTreeView::UpdateTreeListWidth(const int x)
  {
//--- Рассчитаем и установим ширину области древовидного списка
   m_area.X_Size(x-m_area.X());
   m_area.XSize(m_area.X_Size());
//--- Рассчитаем и установим ширину для пунктов в древовидном списке с учётом полосы прокрутки
   int l_w=(m_items_total>m_visible_items_total) ? m_area.XSize()-m_scrollv.ScrollWidth()-4 : m_area.XSize()-1;
   int items_total=::ArraySize(m_items);
   for(int i=0; i<items_total; i++)
      m_items[i].UpdateWidth(l_w);
//--- Рассчитаем и установим координаты для полосы прокрутки древовидного списка
   m_scrollv.X(m_area.X2()-m_scrollv.ScrollWidth());
   m_scrollv.XDistance(m_scrollv.X());
  }
//+------------------------------------------------------------------+
//| Обновление ширины списка в области содержания                    |
//+------------------------------------------------------------------+
void CTreeView::UpdateContentListWidth(const int x)
  {
//--- Рассчитаем и установим координату X, отступ и ширину для области содержания
   int l_x=m_area.X2()-1;
   m_content_area.X(l_x);
   m_content_area.X_Distance(l_x);
   m_content_area.XGap(l_x-m_wnd.X());
   m_content_area.XSize(CElement::X2()-m_content_area.X());
   m_content_area.X_Size(m_content_area.XSize());
//--- Рассчитаем и установим координату X и ширину для пунктов в списке содержания
   l_x=m_content_area.X()+1;
   int l_w=(m_content_items_total>m_visible_items_total) ? m_content_area.XSize()-m_content_scrollv.ScrollWidth()-4 : m_content_area.XSize()-2;
   int total=::ArraySize(m_content_items);
   for(int i=0; i<total; i++)
     {
      m_content_items[i].UpdateX(l_x);
      m_content_items[i].UpdateWidth(l_w);
     }
  }

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

  • Если отключен режим для изменения ширины списков
  • Если область содержания отключена
  • Если включен режим пунктов-вкладок
  • Если полоса прокрутки активна (ползунок находится в режиме перемещения)

Если все эти проверки пройдены, то далее вызывается метод CTreeView::CheckXResizePointer() для определения готовности состояния на изменение ширины списков. Если в результате оказалось, что указатель отключен, то нужно разблокировать ранее заблокированную им форму. 

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

//+------------------------------------------------------------------+
//| Управляет шириной списков                                        |
//+------------------------------------------------------------------+
void CTreeView::ResizeListArea(const int x,const int y)
  {
//--- Выйти, (1) если ширину области содержания не нужно изменять или
//    (2) если область содержания отключена или (3) включен режим пунктов-вкладок
   if(!m_resize_list_area_mode || m_content_area_width<0 || m_tab_items_mode)
      return;
//--- Выйти, если полоса прокрутки активна
   if(m_scrollv.ScrollState())
      return;
//--- Проверка готовности для изменения ширины списков
   CheckXResizePointer(x,y);
//--- Если указатель отключен, разблокировать форму
   if(!m_x_resize.State())
     {
      //--- Разблокировать форму может только тот, кто её заблокировал
      if(m_wnd.IsLocked() && m_wnd.IdActivatedElement()==CElement::Id())
        {
         m_wnd.IsLocked(false);
         m_wnd.IdActivatedElement(WRONG_VALUE);
         return;
        }
     }
   else
     {
      //--- Проверка на выход за установленные ограничения 
      if(!CheckOutOfArea(x,y))
         return;
      //--- Заблокируем форму и запомним идентификатор активного элемента
      m_wnd.IsLocked(true);
      m_wnd.IdActivatedElement(CElement::Id());
      //--- Установим X-координату объекту по центру курсора мыши
      m_x_resize.UpdateX(x-m_x_resize.XGap());
      //--- Y-координату устанавливаем, только если не вышли за область элемента
      if(y>m_area.Y() && y<m_area.Y2())
         m_x_resize.UpdateY(y-m_x_resize.YGap());
      //--- Обновление ширины древовидного списка
      UpdateTreeListWidth(x);
      //--- Обновление ширины списка содержания
      UpdateContentListWidth(x);
      //--- Обновить координаты и размеры списков
      ShiftTreeList();
      ShiftContentList();
      //--- Перерисовать указатель
      m_x_resize.Reset();
     }
  }

В качестве промежуточного итога можно привести блок кода из обработчика элемента CTreeView::OnEvent(), в котором обрабатывается событие перемещения мыши CHARTEVENT_MOUSE_MOVE. Многие методы, рассмотренные выше, используются именно здесь. 

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CTreeView::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события перемещения курсора  
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Выйти, если элемент скрыт
      if(!CElement::IsVisible())
         return;
      //--- Координаты и состояние левой кнопки мыши
      int x=(int)lparam;
      int y=(int)dparam;
      m_mouse_state=(bool)int(sparam);
      //--- Проверка фокуса над списком
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      //--- Смещаем древовидный список, если управление ползунком полосы прокрутки в действии
      if(m_scrollv.ScrollBarControl(x,y,m_mouse_state))
        {
         ShiftTreeList();
         return;
        }
      //--- Смещаем список содержания, если управление ползунком полосы прокрутки в действии
      if(m_content_scrollv.ScrollBarControl(x,y,m_mouse_state))
        {
         ShiftContentList();
         return;
        }
      //--- Управление шириной области содержания
      ResizeListArea(x,y);
      //--- Выйти, если форма заблокирована
      if(m_wnd.IsLocked())
         return;
      //--- Изменение цвета по наведению курсора мыши
      ChangeObjectsColor();
      return;
     }
  }

 

 

Режим пунктов-вкладок

Теперь разберёмся, как будет работать режим вкладок в древовидном списке. На текущий момент в библиотеке уже есть два класса для создания вкладок: CTabs и CIconTabs. Подробнее о том, как они устроены, можно почитать в статье Графические интерфейсы VII: Элементы "Вкладки" (Глава 2). В этих классах вкладки создаются в ряд по горизонтали или по вертикали. В древовидном списке элементы можно было бы располагать по категориям, что даёт существенное удобство, когда элементов в графическом интерфейсе приложения очень много и их нужно как-то сгруппировать.

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

На рисунке ниже показан пример порядка индексации пунктов-вкладок. Пункты A, D, G и H не являются вкладками, так как содержат в себе списки.

 Рис. 17. Индексы пунктов-вкладок.

Рис. 17. Индексы пунктов-вкладок.

Для решения этой задачи нам понадобится структура (с объявлением массива её экземпляров), в которой будет динамический массив для хранения указателей на элементы и поле, в котором будет храниться индекс вкладки:

class CTreeView : public CElement
  {
private:
   //--- Структура элементов закреплённых за каждым пунктом-вкладкой
   struct TVElements
     {
      CElement         *elements[];
      int               list_index;
     };
   TVElements        m_tab_items[];
  };

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

Далее, если включен показ содержания пунктов, то по умолчанию будет выделен первый пункт в списке. Если же показ содержания пунктов отключен, то далее, в случае выхода из диапазона, индекс корректируется. Затем выделяется указанная в свойствах пункт-вкладка. 

class CTreeView : public CElement
  {
private:
   //--- Формирует массив пунктов-вкладок
   void              GenerateTabItemsArray(void);
  };
//+------------------------------------------------------------------+
//| Формирует массив пунктов-вкладок                                 |
//+------------------------------------------------------------------+
void CTreeView::GenerateTabItemsArray(void)
  {
//--- Выйти, если режим пунктов-вкладок отключен
   if(!m_tab_items_mode)
      return;
//--- Добавим в массив пунктов-вкладок только пустые пункты
   int items_total=::ArraySize(m_items);
   for(int i=0; i<items_total; i++)
     {
      //--- Если в этом пункте есть другие пункты, перейдём к следующему
      if(m_t_items_total[i]>0)
         continue;
      //--- Увеличим размер массива пунктов-вкладок на один элемент
      int array_size=::ArraySize(m_tab_items);
      ::ArrayResize(m_tab_items,array_size+1);
      //--- Сохраним общий индекс пункта
      m_tab_items[array_size].list_index=i;
     }
//--- Если отключен показ содержания пунктов
   if(!m_show_item_content)
     {
      //--- Получим размер массива пунктов-вкладок
      int tab_items_total=::ArraySize(m_tab_items);
      //--- Скорректируем индекс, если выход из диапазона
      if(m_selected_item_index>=tab_items_total)
         m_selected_item_index=tab_items_total-1;
      //--- Отключим выделение текущего пункта в списке
      m_items[m_selected_item_index].HighlightItemState(false);
      //--- Индекс выделенной вкладки
      int tab_index=m_tab_items[m_selected_item_index].list_index;
      m_selected_item_index=tab_index;
      //--- Выделим этот пункт
      m_items[tab_index].HighlightItemState(true);
     }
  }

Для присоединения какого-либо элемента управления к вкладке в древовидном списке нужно будет воспользоваться методом CTreeView::AddToElementsArray(). У этого метода только два аргумента: (1) индекс пункта-вкладки и (2) объект типа CElement, указатель которого нужно сохранить в массив указанного пункта-вкладки. 

class CTreeView : public CElement
  {
public:
   //--- Добавляет элемент в массив пункта-вкладки
   void              AddToElementsArray(const int item_index,CElement &object);
  };
//+------------------------------------------------------------------+
//| Добавляет элемент в массив указанной вкладки                     |
//+------------------------------------------------------------------+
void CTreeView::AddToElementsArray(const int tab_index,CElement &object)
  {
//--- Проверка на выход из диапазона
   int array_size=::ArraySize(m_tab_items);
   if(array_size<1 || tab_index<0 || tab_index>=array_size)
      return;
//--- Добавим указатель переданного элемента в массив указанной вкладки
   int size=::ArraySize(m_tab_items[tab_index].elements);
   ::ArrayResize(m_tab_items[tab_index].elements,size+1);
   m_tab_items[tab_index].elements[size]=::GetPointer(object);
  }

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

class CTreeView : public CElement
  {
public:
   //--- Показать элементы только выделенного пункта-вкладки
   void              ShowTabElements(void);
  };
//+------------------------------------------------------------------+
//| Показывает элементы только выделенного пункта-вкладки            |
//+------------------------------------------------------------------+
void CTreeView::ShowTabElements(void)
  {
//--- Выйти, если элемент скрыт или режим пунктов-вкладок отключен
   if(!CElement::IsVisible() || !m_tab_items_mode)
      return;
//--- Индекс выделенной вкладки
   int tab_index=WRONG_VALUE;
//--- Определим индекс выделенной вкладки
   int tab_items_total=::ArraySize(m_tab_items);
   for(int i=0; i<tab_items_total; i++)
     {
      if(m_tab_items[i].list_index==m_selected_item_index)
        {
         tab_index=i;
         break;
        }
     }
//--- Покажем элементы только выделенной вкладки
   for(int i=0; i<tab_items_total; i++)
     {
      //--- Получим количество элементов присоединённых к вкладке
      int tab_elements_total=::ArraySize(m_tab_items[i].elements);
      //--- Если выделен этот пункт-вкладка
      if(i==tab_index)
        {
         //--- Показать элементы
         for(int j=0; j<tab_elements_total; j++)
            m_tab_items[i].elements[j].Reset();
        }
      else
        {
         //--- Скрыть элементы
         for(int j=0; j<tab_elements_total; j++)
            m_tab_items[i].elements[j].Hide();
        }
     }
  }

 

 

Методы для обработки событий

При выборе того или иного пункта в списках будет генерироваться сообщение о том, что путь к пункту древовидного списка изменился. Это событие потом можно будет принимать в файловых навигаторах для определения пути к файлу. Для реализации задуманного нам понадобится идентификатор ON_CHANGE_TREE_PATH в файле Defines.mqh (см. листинг кода ниже):

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#define ON_CHANGE_TREE_PATH       (23) // Путь в древовидном списке изменён

Для определения пути к выделенному пункту напишем публичный метод CTreeView::CurrentFullPath(), который будем вызывать в файловом навигаторе, чтобы получить путь к файлу. Ещё понадобится поле и метод для получения выделенного файла в списках элемента. Они будут актуальны только тогда, когда древовидный список используется в составе файлового навигатора. Давайте подробнее рассмотрим код метода CTreeView::CurrentFullPath().

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

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

  • Индекс общего списка должен совпадать с индексом общего списка предыдущего узла.
  • Индекс пункта локального списка должен совпадать с индексом пункта предыдущего узла.
  • Должна соблюдаться последовательность узлов на убывание.

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

Затем в отдельном цикле формируется строка (полный путь к выделенному пункту) с добавлением разделителя «\\». Далее, если выделенный пункт в древовидном списке – папка, то проверим, есть ли выделенный пункт в списке области содержания и является ли он файлом.  Если включен режим "показывать в древовидном списке файлы", то когда выделен файл, после формирования строки его название сразу сохраняется.

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

class CTreeView : public CElement
  {
private:
   //--- Текст выделенного пункта в списке. 
   //    Только для файлов в случае использования класса для создания файлового навигатора.
   //    Если в списке выбран не файл, то в этом поле должна быть пустая строка "".
   string            m_selected_item_file_name;
   //---
public:
   //--- Возвращает название файла
   string            SelectedItemFileName(void)                   const { return(m_selected_item_file_name);        }
   //--- Возвращает полный путь выделенного пункта
   string            CurrentFullPath(void);
  };
//+------------------------------------------------------------------+
//| Возвращает полный текущий путь                                   |
//+------------------------------------------------------------------+
string CTreeView::CurrentFullPath(void)
  {
//--- Для формирования директории к выделенному пункту
   string path="";
//--- Индекс выделенного пункта
   int li=m_selected_item_index;
//--- Массив для формирования директории
   string path_parts[];
//--- Получим описание (текст) выделенного пункта древовидного списка,
//    но только, если это папка
   if(m_t_is_folder[li])
     {
      ::ArrayResize(path_parts,1);
      path_parts[0]=m_t_item_text[li];
     }
//--- Пройдёмся по всему списку
   int total=::ArraySize(m_t_list_index);
   for(int i=0; i<total; i++)
     {
      //--- Рассматриваем только папки.
      //    Если файл, переходим к следующему пункту.
      if(!m_t_is_folder[i])
         continue;
      //--- Если индекс общего списка совпадает с индексом общего списка предыдущего узла и
      //    индекс пункта локального списка совпадает с индексом пункта предыдущего узла и
      //    соблюдается последовательность уровней узлов
      if(m_t_list_index[i]==m_t_prev_node_list_index[li] &&
         m_t_item_index[i]==m_t_prev_node_item_index[li] &&
         m_t_node_level[i]==m_t_node_level[li]-1)
        {
         //--- Увеличим массив на один элемент и сохраним описание пункта
         int sz=::ArraySize(path_parts);
         ::ArrayResize(path_parts,sz+1);
         path_parts[sz]=m_t_item_text[i];
         //--- Запомним индекс для следующей проверки
         li=i;
         //--- Если дошли до нулевого уровня узла, выходим из цикла
         if(m_t_node_level[i]==0 || i<=0)
            break;
         //--- Сбросить счётчик цикла
         i=-1;
        }
     }
//--- Сформировать строку - полный путь к выделенному пункту в древовидном списке
   total=::ArraySize(path_parts);
   for(int i=total-1; i>=0; i--)
      ::StringAdd(path,path_parts[i]+"\\");
//--- Если выделенный в древовидном списке пункт - папка
   if(m_t_is_folder[m_selected_item_index])
     {
      m_selected_item_file_name="";
      //--- Если пункт в области содержания выделен
      if(m_selected_content_item_index>0)
        {
         //--- Если выделенный пункт - файл, сохраним его название
         if(!m_t_is_folder[m_c_tree_list_index[m_selected_content_item_index]])
            m_selected_item_file_name=m_c_item_text[m_selected_content_item_index];
        }
     }
//--- Если выделенный в древовидном списке пункт - файл
   else
//--- Сохраним его название
      m_selected_item_file_name=m_t_item_text[m_selected_item_index];
//--- Вернуть директорию
   return(path);
  }

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

  • Нажатие на кнопку сворачивания/разворачивания локального списка пункта – метод CTreeView::OnClickItemArrow(). Здесь, если окажется, что имя графического объекта не из древовидного списка или идентификатор элемента в имени объекта не совпадает с идентификатором древовидного списка, программа выходит из метода. Затем (1) нужно получить состояние кнопки пункта и изменить на противоположное, (2) обновить древовидный список отобразив последние изменения, (3) рассчитать положение ползунка полосы прокрутки и (4) в случае, если включен соответствующий режим, показать элементы только выделенного в текущий момент пункта-вкладки. 

class CTreeView : public CElement
  {
private:
   //--- Обработка нажатия на кнопке сворачивания/разворачивания списка пункта
   bool              OnClickItemArrow(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Нажатие на кнопку сворачивания/разворачивания списка пункта      |
//+------------------------------------------------------------------+
bool CTreeView::OnClickItemArrow(const string clicked_object)
  {
//--- Выйдем, если чужое имя объекта
   if(::StringFind(clicked_object,CElement::ProgramName()+"_0_treeitem_arrow_",0)<0)
      return(false);
//--- Получим идентификатор из имени объекта
   int id=IdFromObjectName(clicked_object);
//--- Выйти, если идентификаторы не совпадают
   if(id!=CElement::Id())
      return(false);
//--- Получим индекс пункта в общем списке
   int list_index=IndexFromObjectName(clicked_object);
//--- Получим состояние стрелки пункта и установим противоположное
   m_t_item_state[list_index]=!m_t_item_state[list_index];
   ((CChartObjectBmpLabel*)m_items[list_index].Object(1)).State(m_t_item_state[list_index]);
//--- Обновить древовидный список
   UpdateTreeViewList();
//--- Рассчитать положение ползунка полосы прокрутки
   m_scrollv.CalculateThumbY();
//--- Показать элементы выделенного пункта-вкладки
   ShowTabElements();
   return(true);
  }

  • Нажатие на пункт в древовидном списке – метод CTreeView::OnClickItem(). Здесь в самом начале тоже проходятся несколько проверок, таких же, как и в предыдущем пункте описания. Помимо прочего, выход из метода осуществляется еще и тогда, если одна из полос прокрутки в элементе активна в текущий момент. 

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

После цикла, если дошли до этого момента, то был выделен новый пункт. Значит, нужно сбросить индекс выделенного пункта в списке области содержания, а также его цвет. Например, именно так это реализовано в проводнике операционной системы Windows 7. Затем список в области содержания обновляется для отображения последних изменений. В самом конце генерируется сообщение с идентификатором события ON_CHANGE_TREE_PATH, которое может быть обработано в обработчике событий пользовательского приложения или в каком-либо другом элементе.  

class CTreeView : public CElement
  {
private:
   //--- Обработка нажатия на пункте древовидного списка
   bool              OnClickItem(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Нажатие на пункте в древовидном списке                           |
//+------------------------------------------------------------------+
bool CTreeView::OnClickItem(const string clicked_object)
  {
//--- Выйдем, если полоса прокрутки в активном режиме
   if(m_scrollv.ScrollState() || m_content_scrollv.ScrollState())
      return(false);
//--- Выйдем, если чужое имя объекта
   if(::StringFind(clicked_object,CElement::ProgramName()+"_0_treeitem_area_",0)<0)
      return(false);
//--- Получим идентификатор из имени объекта
   int id=IdFromObjectName(clicked_object);
//--- Выйти, если идентификаторы не совпадают
   if(id!=CElement::Id())
      return(false);
//--- Получим текущую позицию ползунка полосы прокрутки
   int v=m_scrollv.CurrentPos();
//--- Пройдёмся по списку
   for(int r=0; r<m_visible_items_total; r++)
     {
      //--- Проверка для предотвращения выхода из диапазона
      if(v>=0 && v<m_items_total)
        {
         //--- Получим общий индекс пункта
         int li=m_td_list_index[v];
         //--- Если выбран этот пункт в списке
         if(m_items[li].Object(0).Name()==clicked_object)
           {
            //--- Выйдем, если этот пункт уже выделен
            if(li==m_selected_item_index)
               return(false);
            //--- Если включен режим пунктов-вкладок и отключен режим показа содержания,
            //    не будем выделять пункты без списка
            if(m_tab_items_mode && !m_show_item_content)
              {
               //--- Если текущий пункт не содержит в себе списка, остановим цикл
               if(m_t_items_total[li]>0)
                  break;
              }
            //--- Установим цвет предыдущему выделенному пункту
            m_items[m_selected_item_index].HighlightItemState(false);
            //--- Запомним индекс для текущего и изменим его цвет
            m_selected_item_index=li;
            m_items[li].HighlightItemState(true);
            break;
           }
         v++;
        }
     }
//--- Сбросить цвета в области содержания
   if(m_selected_content_item_index>=0)
      m_content_items[m_selected_content_item_index].HighlightItemState(false);
//--- Сброс выделенного пункта
   m_selected_content_item_index=WRONG_VALUE;
//--- Обновить список содержания
   UpdateContentList();
//--- Рассчитать положение ползунка полосы прокрутки
   m_content_scrollv.CalculateThumbY();
//--- Скорректировать список содержания
   ShiftContentList();
//--- Показать элементы выделенного пункта-вкладки
   ShowTabElements();
//--- Отправить сообщение о выборе новой директории в древовидном списке
   ::EventChartCustom(m_chart_id,ON_CHANGE_TREE_PATH,0,0,"");
   return(true);
  }

  • Нажатие на пункт в списке области содержания – метод CTreeView::OnClickContentListItem(). Код этого метода подобен тому, что описывался в предыдущем пункте описания и даже существенно проще, поэтому не будем приводить его здесь. Следует только упомянуть, что при нажатии на пункт в области содержания тоже генерируется событие с идентификатором ON_CHANGE_TREE_PATH, что, например, в файловом навигаторе может означать выбор файла.

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

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CTreeView::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события перемещения курсора  
//...
//--- Обработка события нажатия левой кнопки мыши на объекте
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Выйти, если в режиме изменения размера области списка содержания
      if(m_x_resize.IsVisible() || m_x_resize.State())
         return;
      //--- Обработка нажатия на стрелке пункта
      if(OnClickItemArrow(sparam))
         return;
      //--- Обработка нажатия на пункте древовидного списка
      if(OnClickItem(sparam))
         return;
      //--- Обработка нажатия на пункте в списке содержания
      if(OnClickContentListItem(sparam))
         return;
      //--- Сдвигает список относительно полосы прокрутки
      if(m_scrollv.OnClickScrollInc(sparam) || m_scrollv.OnClickScrollDec(sparam))
         ShiftTreeList();
      if(m_content_scrollv.OnClickScrollInc(sparam) || m_content_scrollv.OnClickScrollDec(sparam))
         ShiftContentList();
      //---
      return;
     }
  }

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



Интеграция элемента в движок библиотеки

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

#include "TreeItem.mqh"
#include "TreeView.mqh"
//+------------------------------------------------------------------+
//| Класс для хранения всех объектов интерфейса                      |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
   //--- Массив окон
   CWindow          *m_windows[];
   //--- Структура массивов элементов
   struct WindowElements
     {
      //--- Древовидные списки
      CTreeView        *m_treeview_lists[];
     };
   //--- Массив массивов элементов для каждого окна
   WindowElements    m_wnd[];
   //---
public:
   //--- Количество древовидных списков
   int               TreeViewListsTotal(const int window_index);
   //---
private:
   //--- Сохраняет указатели на элементы древовидных списков
   bool              AddTreeViewListsElements(const int window_index,CElement &object);
  };

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

//+------------------------------------------------------------------+
//| Событие ON_WINDOW_UNROLL                                         |
//+------------------------------------------------------------------+
bool CWndEvents::OnWindowUnroll(void)
  {
//--- Если сигнал "Развернуть форму"
   if(m_id!=CHARTEVENT_CUSTOM+ON_WINDOW_UNROLL)
      return(false);
//--- Индекс активного окна
   int awi=m_active_window_index;
//--- Если идентификатор окна и номер подокна совпадают
   if(m_lparam==m_windows[awi].Id() && (int)m_dparam==m_subwin)
     {
      int elements_total=CWndContainer::ElementsTotal(awi);
      for(int e=0; e<elements_total; e++)
        {
         //--- Сделать видимыми все элементы кроме формы и ...
         if(m_wnd[awi].m_elements[e].ClassName()!="CWindow")
           {
            //--- ... тех элементов, которые являются выпадающими
            if(!m_wnd[awi].m_elements[e].IsDropdown())
               m_wnd[awi].m_elements[e].Show();
           }
        }
      //--- Если есть вкладки, то показать элементы только выделенной
      int tabs_total=CWndContainer::TabsTotal(awi);
      for(int t=0; t<tabs_total; t++)
         m_wnd[awi].m_tabs[t].ShowTabElements();
      //--- Если есть древовидные списки, то показать элементы только выделенного пункта-вкладки
      int treeview_total=CWndContainer::TreeViewListsTotal(awi);
      for(int tv=0; tv<treeview_total; tv++)
         m_wnd[awi].m_treeview_lists[tv].ShowTabElements();
     }
//--- Обновить расположение всех элементов
   MovingWindow();
   m_chart.Redraw();
   return(true);
  }

Точно такой же цикл нужно добавить в метод CWndEvents::OnOpenDialogBox() сразу после аналогичного цикла для элементов типа CTabs (вкладки). Не будем приводить здесь этот код для экономии места в статье. 

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

::ArrayFree(m_wnd[w].m_treeview_lists);

 

 

Тест элемента "Древовидный список"

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

class CProgram : public CWndEvents
  {
private:
   //--- Древовидный список
   CTreeView         m_treeview;
   //---
private:
   //--- Древовидный список
#define TREEVIEW1_GAP_X       (2)
#define TREEVIEW1_GAP_Y       (43)
   bool              CreateTreeView(void);
  };

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

Для каждой группы пунктов установим свои картинки. Первый пункт будет содержать список. Для примера сделаем его развёрнутым сразу при создании элемента (состояние true), а списки других пунктов, у которых они есть – оставим свёрнутыми (состояние false). Включим режимы (1) подсвечивать пункт при наведении курсора мыши, (2) показывать содержание пункта в соседней области и (3) изменение ширины областей списков

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

//+------------------------------------------------------------------+
//| Создаёт древовидный список                                       |
//+------------------------------------------------------------------+
bool CProgram::CreateTreeView(void)
  {
//--- Количество пунктов в древовидном списке
#define TREEVIEW_ITEMS 25
//--- Сохраним указатель на окно
   m_treeview.WindowPointer(m_window1);
//--- Координаты
   int x=m_window1.X()+TREEVIEW1_GAP_X;
   int y=m_window1.Y()+TREEVIEW1_GAP_Y;
//--- Сформируем массивы для древовидного списка:
//    Картинки для пунктов
#define A "Images\\EasyAndFastGUI\\Icons\\bmp16\\advisor.bmp"   // Эксперт
#define I "Images\\EasyAndFastGUI\\Icons\\bmp16\\indicator.bmp" // Индикатор
#define S "Images\\EasyAndFastGUI\\Icons\\bmp16\\script.bmp"    // Скрипт
   string path_bmp[TREEVIEW_ITEMS]=
     {A,I,I,I,I,I,I,I,I,S,S,S,S,S,S,S,S,S,S,S,S,S,S,A,A};
//--- Описание пунктов (отображаемый текст)
   string item_text[TREEVIEW_ITEMS]=
     {"Advisor01","Indicators","01","02","03","04","05","06","07",
      "Scripts","01","02","03","04","05","06","07","08","09","10","11","12","13",
      "Advisor02","Advisor03"};
//--- Индексы общего списка предыдущих узлов
   int prev_node_list_index[TREEVIEW_ITEMS]=
     {-1,0,1,1,1,1,1,1,1,0,9,9,9,9,9,9,9,9,9,9,9,9,9,-1,-1};
//--- Индексы пунктов в локальных списках
   int item_index[TREEVIEW_ITEMS]=
     {0,0,0,1,2,3,4,5,6,1,0,1,2,3,4,5,6,7,8,9,10,11,12,1,2};
//--- Номер уровня узла
   int node_level[TREEVIEW_ITEMS]=
     {0,1,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0};
//--- Локальные индексы пунктов предыдущих узлов
   int prev_node_item_index[TREEVIEW_ITEMS]=
     {-1,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,-1,-1};
//--- Количество пунктов в локальных списках
   int items_total[TREEVIEW_ITEMS]=
     {2,7,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
//--- Состояние списка пункта
   bool item_state[TREEVIEW_ITEMS]=
     {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
//--- Установим свойства перед созданием
   m_treeview.TreeViewAreaWidth(180);
   m_treeview.ContentAreaWidth(0);
   m_treeview.VisibleItemsTotal(10);
   m_treeview.LightsHover(true);
   m_treeview.ShowItemContent(true);
   m_treeview.ResizeListAreaMode(true);
//--- Свойства полос прокрутки
   m_treeview.GetScrollVPointer().AreaBorderColor(clrLightGray);
   m_treeview.GetContentScrollVPointer().AreaBorderColor(clrLightGray);
//--- Добавим пункты
   for(int i=0; i<TREEVIEW_ITEMS; i++)
      m_treeview.AddItem(i,prev_node_list_index[i],item_text[i],path_bmp[i],
                         item_index[i],node_level[i],prev_node_item_index[i],items_total[i],0,item_state[i],true);
//--- Создать древовидный список
   if(!m_treeview.CreateTreeView(m_chart_id,m_subwin,x,y))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(0,m_treeview);
   return(true);
  }

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

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

 Рис. 18. Тест элемента древовидный список. Развёрнут список только первого пункта.

Рис. 18. Тест элемента "Древовидный список". Развёрнут список только первого пункта.

В качестве демонстрации развернём списки всех пунктов. Для этого нужно нажать на кнопку-стрелку пункта. Сделаем это и выделим пункт с описанием «Scripts», чтобы его список отобразился в области содержания. Результат смотрите на рисунке ниже. Видно, что когда количество пунктов не помещается в диапазон 10 видимых пунктов, появляются полосы прокрутки. В области содержания выделен третий пункт. Видно также, что при наведении курсора мыши на границу смыкания двух областей списков элемента, появляется пользовательский указатель (двухсторонняя стрелка) курсора мыши.

Рис. 19. Развёрнуты списки всех пунктов. 

Рис. 19. Развёрнуты списки всех пунктов.

Создадим ещё одного эксперта для тестов, в котором продемонстрируем режим пунктов-вкладок. Для примера сделаем три разворачивающихся списка по схеме, описанной на рисунке ниже:

 Рис. 20. Схема древовидного списка.

Рис. 20. Схема древовидного списка.

За пунктами-вкладками списков «Advisors» и «Indicators» закрепим такие элементы, как чекбоксы (CCheckBox) и таблицы типа CTable. Пункты-вкладки списка «Scripts» оставим пустыми, чтобы у вас осталась возможность быстро попрактиковаться. Весь код приводить не будем. Стоит отметить только, какие ключевые режимы и свойства будут использоваться для этого варианта: (1) включен режим пунктов-вкладок, (2) показ содержимого пунктов отключен и (3) выделим третий пункт-вкладку

//...
   m_treeview.TabItemsMode(true);
   m_treeview.LightsHover(true);
   m_treeview.ShowItemContent(false);
   m_treeview.SelectedItemIndex((m_treeview.SelectedItemIndex()==WRONG_VALUE) ? 3 : m_treeview.SelectedItemIndex());
//--- Свойства полос прокрутки
//...

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

 Рис. 21. Тест режима пунктов-вкладок.

Рис. 21. Тест режима пунктов-вкладок. 

 


Заключение

В этой статье (второй главе восьмой части серии) мы рассмотрели один из самых сложных составных элементов библиотеки графического интерфейса - «Древовидный список». Всего в статье было представлено три класса элементов: 

  • Класс CPointer для создания пользовательского указателя для курсора мыши.
  • Класс CTreeItem для создания пункта древовидного списка.
  • Класс CTreeView для создания древовидного списка.

В следующей статье разовьём эту тему дальше и создадим довольно полезный класс кода, с помощью которого в итоге можно будет легко и просто создать файловый навигатор в своём MQL-приложении. 

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

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


Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Sergey Likho
Sergey Likho | 11 июн. 2020 в 11:36

Добрый день. 


Статье 4 года. Мой комментарий будет первым. 

1. Есть какой-то быстрый способ как обновить список файлов в директории? 

2. У меня не работает выбор нулевого элемента в списке в директории Files.  m_navigator.SelectedFile()  Возвращает пустое значение ""

 (Пример на gif, при нажатие на кнопку в статусную строку выводится имя файла)


Анатолий, Большое спасибо за проделанную работу!

Текстовые файлы для хранения входных параметров советников, индикаторов и скриптов Текстовые файлы для хранения входных параметров советников, индикаторов и скриптов
В статье рассмотрены вопросы хранения динамических объектов, массивов и других переменных в качестве свойств советников, индикаторов и скриптов в текстовых файлах. Они служат удобным дополнением к функционалу стандартных средств, предлагаемых языками MQL.
Универсальный торговый эксперт: Интеграция со стандартными модулями сигналов MetaTrader (часть 7) Универсальный торговый эксперт: Интеграция со стандартными модулями сигналов MetaTrader (часть 7)
Эта часть статьи посвящена интеграции торгового движка CStrategy с модулями сигналов, входящих в стандартную библиотеку MetaTrader. Материал описывает способы работы с сигналами и создание пользовательских стратегий на их основе.
Графические интерфейсы VIII: Элемент "Файловый навигатор" (Глава 3) Графические интерфейсы VIII: Элемент "Файловый навигатор" (Глава 3)
В предыдущих главах восьмой части серии наша библиотека пополнилась несколькими классами для создания указателей для курсора мыши, календарей и древовидных списков. В настоящей статье рассмотрим элемент «Файловый навигатор», который тоже можно будет использовать в качестве части графического интерфейса MQL-приложения.
LifeHack для трейдера: индикаторы баланса, просадки, загрузки и тиков во время тестирования LifeHack для трейдера: индикаторы баланса, просадки, загрузки и тиков во время тестирования
Как сделать тестирование более наглядным? Ответ прост: нужно использовать в тестере один или несколько индикаторов — тиковый индикатор, индикатор баланса и эквити, индикатор просадки и загрузки депозита. Это позволит визуально отслеживать или природу тиков, или изменение баланса и эквити, или просадку и загрузку депозита.