English Español Deutsch 日本語 Português
Применение контейнеров для компоновки графического интерфейса: класс CGrid

Применение контейнеров для компоновки графического интерфейса: класс CGrid

MetaTrader 5Примеры | 27 января 2016, 14:07
4 952 0
Enrico Lambino
Enrico Lambino

Содержание


1. Введение

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

Настоятельно рекомендуем ознакомиться со статьей о классе CBox, прежде чем приступать к понятиям, которые рассматриваются в этой статье.


2. Цели

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

  • Более глубокая вложенность элементов управления.
  • Для компоновки требуется больше контейнеров.
  • Больше строк кода для выполнения простых операций.

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

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

И аналогично классу CBox необходимо достичь следующих целей:

  • Код должен быть пригоден для многократного использования.
  • Изменение одной составляющей интерфейса должно иметь минимальное воздействие на другие компоненты.
  • Расположение компонентов в пределах интерфейса должно автоматически рассчитываться.

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


3. Класс CGrid

Класс CGrid создает контейнер для одного или нескольких элементов управления графического интерфейса и представляет их в табличном виде. Пример компоновки с использованием экземпляра класса CGrid продемонстрирован на следующей иллюстрации:

Компоновка CGrid

Рисунок 1. Табличная компоновка

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

В примере выше изображена таблица размером 4x4 (4 столбца и 4 строки). Однако мы попытаемся создать класс, который сможет вместить в себя любое количество строк и столбцов в таблице.

Объявим класс CGrid потомком класса CBox. Тем самым мы сможем легко переопределить виртуальные функции родительского класса. Кроме того, это даст нам возможность манипулировать образцами этого класса аналогично образцам CBox:

#include "Box.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CGrid : public CBox
  {
protected:
   int               m_cols;
   int               m_rows;
   int               m_hgap;
   int               m_vgap;
   CSize             m_cell_size;
public:
                     CGrid();
                     CGrid(int rows,int cols,int hgap=0,int vgap=0);
                    ~CGrid();
   virtual int       Type() const {return CLASS_LAYOUT;}
   virtual bool      Init(int rows,int cols,int hgap=0,int vgap=0);
   virtual bool      Create(const long chart,const string name,const int subwin,
                            const int x1,const int y1,const int x2,const int y2);
   virtual int       Columns(){return(m_cols);}
   virtual void      Columns(int cols){m_cols=cols;}
   virtual int       Rows(){return(m_rows);}
   virtual void      Rows(int rows){m_rows=rows;}
   virtual int       HGap(){return(m_hgap);}
   virtual void      HGap(int gap){m_hgap=gap;}
   virtual int       VGap(){return(m_vgap);}
   virtual void      VGap(int gap){m_vgap=gap;}
   virtual bool      Pack();
protected:
   virtual void      CheckControlSize(CWnd *control);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGrid::CGrid()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGrid::CGrid(int rows,int cols,int hgap=0,int vgap=0)
  {
   Init(rows,cols,hgap,vgap);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGrid::~CGrid()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CGrid::Init(int rows,int cols,int hgap=0,int vgap=0)
  {
   Columns(cols);
   Rows(rows);
   HGap(hgap);
   VGap(vgap);
   return(true);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CGrid::Create(const long chart,const string name,const int subwin,
                   const int x1,const int y1,const int x2,const int y2)
  {
   return(CBox::Create(chart,name,subwin,x1,y1,x2,y2));
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CGrid::Pack()
  {
   CSize size=Size();
   m_cell_size.cx = (size.cx-((m_cols+1)*m_hgap))/m_cols;
   m_cell_size.cy = (size.cy-((m_rows+1)*m_vgap))/m_rows;
   int x=Left(),y=Top();
   int cnt=0;
   for(int i=0;i<ControlsTotal();i++)
     {
      CWnd *control=Control(i);
      if(control==NULL)
         continue;
      if(control==GetPointer(m_background))
         continue;
      if(cnt==0 || Right()-(x+m_cell_size.cx)<m_cell_size.cx+m_hgap)
        {
         if(cnt==0)
            y+=m_vgap;            
         else y+=m_vgap+m_cell_size.cy;
         x=Left()+m_hgap;
        }
      else x+=m_cell_size.cx+m_hgap;    
      CheckControlSize(control);
      control.Move(x,y);
      if(control.Type()==CLASS_LAYOUT)
        {
         CBox *container=control;
         container.Pack();
        }
      cnt++;
     }   
   return(true);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGrid::CheckControlSize(CWnd *control)
  {
   control.Size(m_cell_size.cx,m_cell_size.cy);
  }
//+------------------------------------------------------------------+


3.1. Инициализация

Аналогично другим контейнерам и элементам управления мы создаем таблицу путем вызова метода класса Create(). Однако, как и для любого экземпляра CBox, указание позиции элемента управления на данном этапе не является обязательным. Мы просто объявим ширину и высоту элемента управления с помощью свойств x2 и y2. Если бы таблица являлась единственным контейнером (основным контейнером) для прикрепления к клиентской области, можно было бы использовать следующий код (m_main в качестве экземпляра CGrid):

if(!m_main.Create(chart,name+"main",subwin,0,0,CDialog::m_client_area.Width(),CDialog::m_client_area.Height()))
      return(false);

Сразу после ее создания необходимо произвести инициализацию таблицы путем вызова ее метода Init(). Для инициализации образца CGrid потребуется уточнить число столбцов и строк, на которые будет поделена клиентская область (или ее часть), и пробелы (по горизонтали и по вертикали) между каждой ячейкой таблицы. Для инициализации таблицы мы вызовем в исходном коде метод Init(). Следующий код создаст таблицу 4x4 с горизонтальными и вертикальными пробелами между каждой ячейкой величиной в 2 пикселя:

m_main.Init(4,4,2,2);

Метод Init() имеет 4 параметра:

  1. число строк;
  2. число столбцов;
  3. горизонтальный пробел (в пикселях);
  4. вертикальный пробел (в пикселях).

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


3.2. Пробел между элементами управления

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

оставшаяся величина для элементов управления = величина пространства - (пробел * (число ячеек+1))

Вышеуказанная формула используется внутри функции класса Pack().


3.3. Изменение размеров элементов управления

В классе CGrid размер каждого элемента управления в таблице будет изменен, для того чтобы занять всю ячейку. Тем самым при использовании этой компоновки допускается создание или инициализация элементов управления нулевым размером. Элемент управления будет изменен позже, в процессе создания основного диалогового окна (CDialog или CAppDialog), в результате вызова метода Pack() для экземпляра класса.

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

размер x = оставшаяся величина для элементов управления / число столбцов

размер y = оставшаяся величина для элементов управления / число строк

Фактическое изменение размера выполняется внутри метода класса CheckControlSize().


4. Пример №1: Простая таблица кнопок

В качестве примера использования класса CGrid представлена простая таблица кнопок. Скриншот интерфейса приведен ниже:

Простая таблица кнопок

Рисунок 2. Простая таблица кнопок

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

Для создания этой таблицы необходимо сконструировать советник или индикатор, придерживаясь формата, описанного в статье о классе CBox, который по сути имеет сходство с примерами элементов управления MetaTrader. Таким образом, мы создаем основной исходный файл, который содержит объявление экземпляра пользовательского окна CAppDialog (вместе с другими обработчиками событий), и подключаем к нему заголовочный файл, который фактически содержит объявление используемого класса.

Для таблицы 3x3 нам необходим экземпляр CGrid в качестве члена класса, а также набор из 9 кнопок (по одной на каждую ячейку таблицы):

class CGridSampleDialog : public CAppDialog
  {
protected:
   CGrid             m_main;
   CButton           m_button1;
   CButton           m_button2;
   CButton           m_button3;
   CButton           m_button4;
   CButton           m_button5;
   CButton           m_button6;
   CButton           m_button7;
   CButton           m_button8;
   CButton           m_button9;
public:
                     CGridSampleDialog();
                    ~CGridSampleDialog();
  };

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

public:
                     CGridSampleDialog();
                    ~CGridSampleDialog();
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
bool CGridSampleDialog::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
   if(!CreateMain(chart,name,subwin))
      return(false);   
   for(int i=1;i<=9;i++)
     {
      if(!CreateButton(i,chart,"button",subwin))
         return(false);
     }   
   if(!m_main.Pack())
      return(false);
   if(!Add(m_main))
      return(false);
   return(true);
  }
EVENT_MAP_BEGIN(CGridSampleDialog)
EVENT_MAP_END(CAppDialog)

В этом примере карта событий пуста, так как мы не будем присваивать кнопкам обработку событий.

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

protected:
   virtual bool      CreateMain(const long chart,const string name,const int subwin);
   virtual bool      CreateButton(const int button_id,const long chart,const string name,const int subwin);

Используя этот пример, мы можем наблюдать некоторые преимущества CGrid над CBox. Для создания похожей компоновки с использованием только CBox потребовало бы наличия четырех разных контейнеров. Это объясняется тем, что CBox может справиться только с одним столбцом или одной строкой. С другой стороны, с помощью CGrid мы уменьшили число контейнеров с 4 до 1, благодаря чему потребовалось меньше объявлений и строк кода.

bool CGridSampleDialog::CreateMain(const long chart,const string name,const int subwin)
  {
   if(!m_main.Create(chart,name+"main",subwin,0,0,CDialog::m_client_area.Width(),CDialog::m_client_area.Height()))
      return(false);
   m_main.Init(3,3,5,5);
   return(true);
  }

Метод класса CreateMain() отвечает за создание самой таблицы. При создании элемента управления CBox он работает аналогично. Единственным отличием является то, что CGrid требует дополнительного метода, которым является Init(). Для CBox это не требуется.

Реализация метода CreateButton() показана ниже:

bool CGridSampleDialog::CreateButton(const int button_id,const long chart,const string name,const int subwin)
  {
   CButton *button;
   switch(button_id)
     {
      case 1: button = GetPointer(m_button1); break;
      case 2: button = GetPointer(m_button2); break;
      case 3: button = GetPointer(m_button3); break;
      case 4: button = GetPointer(m_button4); break;
      case 5: button = GetPointer(m_button5); break;
      case 6: button = GetPointer(m_button6); break;
      case 7: button = GetPointer(m_button7); break;
      case 8: button = GetPointer(m_button8); break;
      case 9: button = GetPointer(m_button9); break;
      default: return(false);
     }
   if (!button.Create(chart,name+IntegerToString(button_id),subwin,0,0,100,100))
      return(false);
   if (!button.Text(name+IntegerToString(button_id)))
      return(false);
   if (!m_main.Add(button))
      return(false);
   return(true);
  }

Так как процессы создания кнопок схожи, вместо того чтобы использовать метод для создания каждой кнопки, мы будем использовать общую функцию для создания всех кнопок. Это осуществляется в методе класса CreateButton(), показанном выше. Мы вызовем этот метод внутри виртуального метода Create() сразу после того, как создадим диалоговое окно и таблицу. Как указано в примере кода виртуального метода Create(), с этой целью мы реализовали цикл for. Так как кнопки статически объявлены внутри класса, их экземпляры создаются при объявлении, в связи с чем необходимость использования оператора new отпадает. Мы просто получаем указатель (автоматический) каждой кнопки, а потом вызываем каждый из их методов Create().


5. Пример №2: Пятнашки

Во втором примере речь пойдет об игре под названием "пятнашки". Смысл этой игры заключается в том, что пользователю предоставляется набор чисел от 1 до 15 в таблице 4x4. Целью игры является перестановка плиток таким образом, чтобы все цифры были выстроены по порядку, слева направо и сверху вниз. Игра завершается после того, как пользователь выстроил все плитки в правильном порядке, аналогично скриншоту ниже:

Пятнашки

Рисунок 3. Пятнашки

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

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


5.1. Создание диалогового окна

Мы объявляем класс в качестве потомка CAppDialog вместе с его защищенными (или приватными) членами, конструктором и деструктором:

class CSlidingPuzzleDialog : public CAppDialog
  {
protected:
   CGrid             m_main;
   CButton           m_button1;
   CButton           m_button2;
   CButton           m_button3;
   CButton           m_button4;
   CButton           m_button5;
   CButton           m_button6;
   CButton           m_button7;
   CButton           m_button8;
   CButton           m_button9;
   CButton           m_button10;
   CButton           m_button11;
   CButton           m_button12;
   CButton           m_button13;
   CButton           m_button14;
   CButton           m_button15;
   CButton           m_button16;
   CButton          *m_empty_cell;
public:
                     CSlidingPuzzleDialog();
                    ~CSlidingPuzzleDialog();   
  };

Следующий код демонстрирует метод Create() для класса.

Объявление (внутри определения класса, общедоступные методы):

virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);

Реализация:

bool CSlidingPuzzleDialog::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
   if(!CreateMain(chart,name,subwin))
      return(false);
   for(int i=1;i<=16;i++)
     {
      if(!CreateButton(i,chart,"button",subwin))
         return(false);
     }
   m_empty_cell=GetPointer(m_button16);
   if(!m_main.Pack())
      return(false);
   if(!Add(m_main))
      return(false);
   Shuffle();
   return(true);
  }

Мы видим, что у диалогового окна есть функции CreateMain(), которые будут использованы для создания таблицы, а также CreateButton() в цикле for, который применяется при создании кнопок для таблицы. Мы также наблюдаем вызов метода Pack() экземпляра CGrid (для перерасположения элементов управления), и присоединение таблицы к основной клиентской области при помощи метода Add(). Кроме того, выполняется инициализация игры в методе Shuffle().


5.2. Кнопки

Ниже продемонстрирован пример кода метода CreateButton():

bool CSlidingPuzzleDialog::CreateButton(const int button_id,const long chart,const string name,const int subwin)
  {
   CButton *button;
   switch(button_id)
     {
      case 1: button = GetPointer(m_button1); break;
      case 2: button = GetPointer(m_button2); break;
      case 3: button = GetPointer(m_button3); break;
      case 4: button = GetPointer(m_button4); break;
      case 5: button = GetPointer(m_button5); break;
      case 6: button = GetPointer(m_button6); break;
      case 7: button = GetPointer(m_button7); break;
      case 8: button = GetPointer(m_button8); break;
      case 9: button = GetPointer(m_button9); break;
      case 10: button = GetPointer(m_button10); break;
      case 11: button = GetPointer(m_button11); break;
      case 12: button = GetPointer(m_button12); break;
      case 13: button = GetPointer(m_button13); break;
      case 14: button = GetPointer(m_button14); break;
      case 15: button = GetPointer(m_button15); break;
      case 16: button = GetPointer(m_button16); break;
      default: return(false);
     }
   if(!button.Create(chart,name+IntegerToString(button_id),subwin,0,0,100,100))
      return(false);
   if(button_id<16)
     {
      if(!button.Text(IntegerToString(button_id)))
         return(false);
     }
   else if(button_id==16)
     {
      button.Hide();
     }
   if(!m_main.Add(button))
      return(false);
   return(true);
  }

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


5.3. Проверка на наличие ближайшей плитки

Необходимо проверить, существует ли соседняя плитка в указанном направлении. В противном случае пустая ячейка будет обмениваться значениями с кнопкой, которая не существует. Проверка на наличие ближайших плиток осуществляется благодаря функциям HasNorth(), HasSouth(), HasEast() и HasSouth(). В следующем примере показан метод HasNorth():

bool CSlidingPuzzleDialog::HasNorth(CButton *button,int id,bool shuffle=false)
  {
   if(id==1 || id==2 || id==3 || id==4)
      return(false);
   CButton *button_adj=m_main.Control(id-4);
   if(!CheckPointer(button_adj))
      return(false);
   if(!shuffle)
     {
      if(button_adj.IsVisible())
         return(false);
     }
   return(true);
  }

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


5.4. Перестановка плиток

Следующий фрагмент кода содержит метод Shuffle():

void CSlidingPuzzleDialog::Shuffle(void)
  {
   m_empty_cell=m_main.Control(16);
   for(int i=1;i<m_main.ControlsTotal()-1;i++)
     {
      CButton *button=m_main.Control(i);
      button.Text((string)i);
     }
   MathSrand((int)TimeLocal());
   CButton *target=NULL;
   for(int i=0;i<30;i++)
     {
      int empty_cell_id=(int)StringToInteger(StringSubstr(m_empty_cell.Name(),6));
      int random=MathRand()%4+1;
      if(random==1 && HasNorth(m_empty_cell,empty_cell_id,true))
         target= m_main.Control(empty_cell_id-4);
      else if(random==2 && HasEast(m_empty_cell,empty_cell_id,true))
         target=m_main.Control(empty_cell_id+1);
      else if(random==3 && HasSouth(m_empty_cell,empty_cell_id,true))
         target=m_main.Control(empty_cell_id+4);
      else if(random==4 && HasWest(m_empty_cell,empty_cell_id,true))
         target=m_main.Control(empty_cell_id-1);
      if(CheckPointer(target))
         Swap(target);
     }
  }

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

Перед перестановкой плиток сначала необходимо инициализировать значения кнопок значениями по умолчанию. Это необходимо с целью предотвращения любого события, когда игру "пятнашки" становится слишком сложно или даже невозможно решить. Мы делаем это путем переназначения пустой ячейки 16-ой плитке и установки значений соответствующим образом. Мы также присваиваем 16-ую ячейку ранее объявленному указателю пустой ячейки (члену класса).

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

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


5.5. Событие щелчка по кнопке

Для обработки событий щелчка по каждой кнопке требуется объявить обработчик таких событий. Однако в целях уменьшения дублирования кода мы объявим метод класса, который обрабатывает все события щелчков по кнопкам:

CSlidingPuzzleDialog::OnClickButton(CButton *button)
  {
   if(IsMovable(button))
     {
      Swap(button);
      Check();
     }
  }

Функция IsMovable() проверяет, имеет ли плитка с конкретным номером рядом с собой пустую ячейку с помощью функций, включающих в себя основные направления (например HasNorth(), HasSouth()). Если рядом с кнопкой есть пустая ячейка, то ее можно перемещать, поэтому вызывается функция Swap(), заменяющая значение кнопки на значение пустой ячейки. После каждого удачного обмена также вызывается функция Check().

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

CSlidingPuzzleDialog::OnClickButton1(void)
  {
   OnClickButton(GetPointer(m_button1));
  }

Каждый из этих обработчиков событий в конечном итоге вызывает OnClickButton(). Эти методы также необходимо объявить в карте событий:

EVENT_MAP_BEGIN(CSlidingPuzzleDialog)
   ON_EVENT(ON_CLICK,m_button1,OnClickButton1)
   ON_EVENT(ON_CLICK,m_button2,OnClickButton2)
   ON_EVENT(ON_CLICK,m_button3,OnClickButton3)
   ON_EVENT(ON_CLICK,m_button4,OnClickButton4)
   ON_EVENT(ON_CLICK,m_button5,OnClickButton5)
   ON_EVENT(ON_CLICK,m_button6,OnClickButton6)
   ON_EVENT(ON_CLICK,m_button7,OnClickButton7)
   ON_EVENT(ON_CLICK,m_button8,OnClickButton8)
   ON_EVENT(ON_CLICK,m_button9,OnClickButton9)
   ON_EVENT(ON_CLICK,m_button10,OnClickButton10)
   ON_EVENT(ON_CLICK,m_button11,OnClickButton11)
   ON_EVENT(ON_CLICK,m_button12,OnClickButton12)
   ON_EVENT(ON_CLICK,m_button13,OnClickButton13)
   ON_EVENT(ON_CLICK,m_button14,OnClickButton14)
   ON_EVENT(ON_CLICK,m_button15,OnClickButton15)
   ON_EVENT(ON_CLICK,m_button16,OnClickButton16)
EVENT_MAP_END(CAppDialog)

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

Наконец, добавьте общедоступный метод OnEvent() к объявлению класса:

virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);


5.6. Проверка

Потребуется проверить порядок ячеек при каждом щелчке по кнопке, для того чтобы убедиться, что игра в "пятнашки" завершена. Это происходит с помощью метода Check():

bool CSlidingPuzzleDialog::Check(void)
  {
   for(int i=1;i<m_main.ControlsTotal()-1;i++)
     {
      CButton *button=m_main.Control(i);
      if(CheckPointer(button))
        {
         if(button.Text()!=IntegerToString(i))
           {
            Print("status: not solved: "+button.Text()+" "+IntegerToString(i));
            return(false);
           }
        }
     }
   Print("status: solved");
   return(true);
  }

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


6. Класс CGridTk


6.1. Проблемы класса CGrid

При использовании класса CGrid возникают некоторые проблемы.

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

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

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

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

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


6.2. CGridTk: Улучшенный класс CGrid

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

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

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

Мы переименуем класс в GridTk. Его код находится ниже:

#include "Grid.mqh"
//+------------------------------------------------------------------+ 
//|                                                                  |
//+------------------------------------------------------------------+ 
class CGridTk : public CGrid
  {
protected:
   CArrayObj         m_constraints;
public:
                     CGridTk();
                    ~CGridTk();
   bool              Grid(CWnd *control,int row,int column,int rowspan,int colspan);
   bool              Pack();
   CGridConstraints     *GetGridConstraints(CWnd *control);
  };
//+------------------------------------------------------------------+ 
//|                                                                  |
//+------------------------------------------------------------------+ 
CGridTk::CGridTk(void)
  {
  }
//+------------------------------------------------------------------+ 
//|                                                                  |
//+------------------------------------------------------------------+ 
CGridTk::~CGridTk(void)
  {
  }
//+------------------------------------------------------------------+ 
//|                                                                  |
//+------------------------------------------------------------------+ 
bool CGridTk::Grid(CWnd *control,int row,int column,int rowspan=1,int colspan=1)
  {
   CGridConstraints *constraints=new CGridConstraints(control,row,column,rowspan,colspan);
   if(!CheckPointer(constraints))
      return(false);
   if(!m_constraints.Add(constraints))
      return(false);
   return(Add(control));
  }
//+------------------------------------------------------------------+ 
//|                                                                  |
//+------------------------------------------------------------------+ 
bool CGridTk::Pack()
  {
   CGrid::Pack();
   CSize size=Size();
   m_cell_size.cx = (size.cx-(m_cols+1)*m_hgap)/m_cols;
   m_cell_size.cy = (size.cy-(m_rows+1)*m_vgap)/m_rows;   
   for(int i=0;i<ControlsTotal();i++)
     {
      int x=0,y=0,sizex=0,sizey=0;
      CWnd *control=Control(i);
      if(control==NULL)
         continue;
      if(control==GetPointer(m_background))
         continue;
      CGridConstraints *constraints = GetGridConstraints(control);
      if (constraints==NULL)
         continue;   
      int column = constraints.Column();
      int row = constraints.Row();
      x = (column*m_cell_size.cx)+((column+1)*m_hgap);
      y = (row*m_cell_size.cy)+((row+1)*m_vgap);
      int colspan = constraints.ColSpan();
      int rowspan = constraints.RowSpan();
      control.Size(colspan*m_cell_size.cx+((colspan-1)*m_hgap),rowspan*m_cell_size.cy+((rowspan-1)*m_vgap));
      control.Move(x,y);
      if(control.Type()==CLASS_LAYOUT)
        {
         CBox *container=control;
         container.Pack();
        }
     }
   return(true);
  }
//+------------------------------------------------------------------+ 
//|                                                                  |
//+------------------------------------------------------------------+ 
CGridConstraints *CGridTk::GetGridConstraints(CWnd *control)
  {
   for(int i=0;i<m_constraints.Total();i++)
     {
      CGridConstraints *constraints=m_constraints.At(i);
      CWnd *ctrl=constraints.Control();
      if(ctrl==NULL)
         continue;
      if(ctrl==control)
         return(constraints);
     }
   return (NULL);
  }

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

Мы видим, что класс содержит метод CConstraints, о котором вскоре пойдет речь в этом разделе.


6.2.1. Размеры элемента в строках и столбцах

Благодаря заданию размера элементов в строках и столбцах мы сможем теперь определить длину или ширину элемента управления. Это лучше, чем иметь размер табличной ячейки по умолчанию, но недостаточно точно по сравнению с абсолютным расположением. Тем не менее, следует отметить, что класс CGridTk больше не использует метод CheckControlSize() классов CBox и CGrid, а уже самостоятельно выполняет изменение размера элементов управления внутри метода Pack().


6.2.2. Класс CConstraints

Для каждого элемента управления потребуется определить набор ограничений, которые установят, как каждый элемент управления будет расположен в таблице, какие ячейки он займет, а также насколько будет изменен его размер. Мы сможем непосредственно изменять положение и размер элементов управления, как только они будут добавлены с помощью метода Grid() класса CGridTk. Тем не менее, чтобы оставаться последовательными, мы повременим с изменением размера и расположения, пока не вызовем метод Pack() (аналогично тому, что происходит внутри класса CBox). Для этого необходимо сохранить ограничения в памяти, что и является основным предназначением класса CConstraints:

class CGridConstraints : public CObject
  {
protected:
   CWnd             *m_control;
   int               m_row;
   int               m_col;
   int               m_rowspan;
   int               m_colspan;
public:
                     CGridConstraints(CWnd *control,int row,int column,int rowspan=1,int colspan=1);
                    ~CGridConstraints();
   CWnd             *Control(){return(m_control);}
   int               Row(){return(m_row);}
   int               Column(){return(m_col);}
   int               RowSpan(){return(m_rowspan);}
   int               ColSpan(){return(m_colspan);}
  };
//+------------------------------------------------------------------+ 
//|                                                                  |
//+------------------------------------------------------------------+ 
CGridConstraints::CGridConstraints(CWnd *control,int row,int column,int rowspan=1,int colspan=1)
  {
   m_control = control;
   m_row = MathMax(0,row);
   m_col = MathMax(0,column);
   m_rowspan = MathMax(1,rowspan);
   m_colspan = MathMax(1,colspan);
  }
//+------------------------------------------------------------------+ 
//|                                                                  |
//+------------------------------------------------------------------+ 
CGridConstraints::~CGridConstraints()
  {
  }

Из одного только конструктора объекта класса можно сделать вывод, что класс CConstraints сохраняет ряды, столбцы, rowspan и colspan для каждого элемента управления. Но это возможно только при вызове метода Grid(), как видно в реализации класса CGridTk. Кроме того, данный класс выполняет только функцию хранения информации. То как она будет использована, реализуется в рамках CGridTk.


6.3.3. Расположение по умолчанию

Если определенный элемент управления не добавлен к таблице с помощью метода Grid(), то используется расположение по умолчанию. Такой элемент добавляется с помощью метода Add(), что означает, что таблица не имеет каких-либо ограничений (объект CGridConstraints не хранится в экземпляре класса таблицы). Таким образом, обновленные методы CGridTk не имеют воздействия на размещение или изменение размера элементов управления. Этот метод расположения похож на метод CGrid, если используется в качестве запасного метода или расположения по умолчанию. Хранение подобных элементов управления имеет сходство со сложенной из кирпича стенки, начиная с верхней левой части клиентской области, как показано в первом примере.


7. Пример №3: Пятнашки (Улучшенный вариант)

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

  1. Создать кнопку "Новая игра", чтобы не нужно было снова перезагружать советник для запуска новой игры.
  2. Создать элемент управления в диалоговом окне, где будет показан статус игры, тем самым устраняя необходимость открывать вкладку журнала окна в терминале.
  3. Использовать другой размер для новых элементов управления.
  4. Внести несколько косметических поправок — покрасить плитки и показать все ячейки таблицы (по желанию).

Улучшенный вариант пятнашек изображен на скриншоте ниже:

Пятнашки (Улучшенный вариант)

Рисунок 4. Пятнашки (Улучшенный вариант)

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

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

В новом диалоговом окне имеются два дополнительных элемента управления. Нам потребуется объявить методы, которые создадут эти элементы управления, а именно CreateButtonNew() и CreateLabel(). Сначала нам потребуется объявить их членами класса:

protected:
   //защищенный метод вначале

   virtual bool      CreateButtonNew(const long chart,const string name,const int subwin);
   virtual bool      CreateLabel(const long chart,const string name,const int subwin);

   //другие защищенные методы ниже..

Фактическая реализация методов показана ниже:

//+------------------------------------------------------------------+ 
//|                                                                  |
//+------------------------------------------------------------------+ 
bool CSlidingPuzzleDialog::CreateButtonNew(const long chart,const string name,const int subwin)
  {
   if(!m_button_new.Create(chart,name+"buttonnew",m_subwin,0,0,101,101))
      return(false);
   m_button_new.Text("New");
   m_button_new.ColorBackground(clrYellow);
   if(!m_main.Grid(GetPointer(m_button_new),4,0,1,2))
      return(false);
   return(true);
  }
//+------------------------------------------------------------------+ 
//|                                                                  |
//+------------------------------------------------------------------+ 
bool CSlidingPuzzleDialog::CreateLabel(const long chart,const string name,const int subwin)
  {
   if(!m_label.Create(chart,name+"labelnew",m_subwin,0,0,102,102))
      return(false);
   m_label.Text("click new");
   m_label.ReadOnly(true);
   m_label.TextAlign(ALIGN_CENTER);
   if(!m_main.Grid(GetPointer(m_label),4,2,1,2))
      return(false);
   return(true);
  }

Некоторые функции потребуется слегка изменить. Поскольку к таблице добавлены новые элементы управления, потребуется изменить такие методы, как Check(), HasNorth(), HasSouth(), HasWest() и HasEast(). Это необходимо, чтобы убедиться в том, что плитки не обмениваются значениями с неправильным элементом управления. Сначала мы дадим пронумерованным плиткам префикс "block" (в качестве аргумента по CreateButton()), а затем воспользуемся префиксом, чтобы определить, является ли выбранный элемент управления на самом деле пронумерованной плиткой или нет. Следующий код демонстрирует обновленный метод Check():

bool CSlidingPuzzleDialog::Check(void)
  {
   for(int i=0;i<m_main.ControlsTotal();i++)
     {
      CWnd *control=m_main.Control(i);
      if(StringFind(control.Name(),"block")>=0)
        {
         CButton *button=control;
         if(CheckPointer(button))
           {
            if(button.Text()!=IntegerToString(i))
              {
               m_label.Text("not solved");
               return(false);
              }
           }
        }
     }
   m_label.Text("solved");
   m_solved=true;
   return(true);
  }

Здесь мы используем функцию StringFind, чтобы убедиться, что выбранный элемент управления на самом деле является кнопкой и числовой плиткой. Это необходимо, чтобы не получать ошибок вроде "неправильное приведение указателей" при назначении элемента управления экземпляру CButton, что выполняется в одной из следующих строк кода. В этом коде мы также наблюдаем, что вместо того чтобы использовать функцию Print для изображения статуса в окне терминала, мы просто изменяем текст в элементе управления CEdit.


8. Вложенность контейнера

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


9. Преимущества и недостатки

Преимущества:

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

Недостатки:

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


10. Заключение

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

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

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/1998

Прикрепленные файлы |
grid.zip (13.31 KB)
Универсальный торговый эксперт: Торговля в группе и управление портфелем стратегий (Часть 4) Универсальный торговый эксперт: Торговля в группе и управление портфелем стратегий (Часть 4)
В заключительной части серии статей о торговом движке CStrategy мы рассмотрим одновременную работу нескольких торговых алгоритмов, научимся загружать стратегии из XML-файлов, а также представим простую панель для выбора экспертов, находящихся внутри одного исполняемого модуля, и управления их торговыми режимами.
Графические интерфейсы I: Тестируем библиотеку в программах разных типов и в терминале MetaTrader 4 (Глава 5) Графические интерфейсы I: Тестируем библиотеку в программах разных типов и в терминале MetaTrader 4 (Глава 5)
В предыдущей главе первой части серии о графических интерфейсах в класс формы были добавлены методы, которые позволяют управлять формой посредством нажатия на ее элементах управления. В этой статье протестируем проделанную работу в разных типах MQL-программ, таких как индикаторы и скрипты. А поскольку библиотека задумывалась как кросс-платформенная (в рамках торговых платформ MetaTrader), то проведем тесты также и в MetaTrader 4.
Графические интерфейсы II: Элемент "Пункт меню" (Глава 1) Графические интерфейсы II: Элемент "Пункт меню" (Глава 1)
В второй части серии будет показан процесс разработки таких элементов интерфейса, как главное меню и контекстное меню. Также затронем тему рисования элементов и для этого создадим специальный класс. Очень широко будет освещен такой вопрос, как управление событиями программы, в том числе и пользовательскими.
Универсальный торговый эксперт: Пользовательские стратегии и вспомогательные торговые классы (Часть 3) Универсальный торговый эксперт: Пользовательские стратегии и вспомогательные торговые классы (Часть 3)
В этой статье мы продолжим описание алгоритмов торгового движка CStrategy. В третьей части серии статей подробно разобраны примеры написания конкретных торговых стратегий с использованием данного подхода. Также большое внимание уделено вспомогательным алгоритмам — системе логирования эксперта и доступу к биржевым данным с помощью обычного индексатора (Close[1], Open[0] и т.п.).