English 中文 Español Deutsch 日本語 Português
preview
Графический интерфейс: советы и рекомендации по созданию графической библиотеки на MQL

Графический интерфейс: советы и рекомендации по созданию графической библиотеки на MQL

MetaTrader 5Примеры | 23 января 2024, 15:00
539 0
Manuel Alejandro Cercos Perez
Manuel Alejandro Cercos Perez

Введение

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

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

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

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


Структура программы и иерархия объектов

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

Начнем с базовой реализации элемента:

class CElement
{
private:
   //Variable to generate names
   static int        m_element_count;

   void              AddChild(CElement* child);

protected:
   //Chart object name
   string            m_name;

   //Element relations
   CElement*         m_parent;
   CElement*         m_children[];
   int               m_child_count;

   //Position and size
   int               m_x;
   int               m_y;
   int               m_size_x;
   int               m_size_y;

public:
                     CElement();
                    ~CElement();

   void              SetPosition(int x, int y);
   void              SetSize(int x, int y);
   void              SetParent(CElement* parent);

   int               GetGlobalX();
   int               GetGlobalY();

   void              CreateChildren();
   virtual void      Create(){}
};

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

Позиционные переменныеm_x и m_y являются локальными позициями в контексте родительского объекта. Это создает потребность в глобальной позиции функции для определения реального положения объекта на экране. Ниже вы можете увидеть, как мы можем рекурсивно получить глобальную позицию (в данном случае для X):

int CElement::GetGlobalX(void)
{
   if (CheckPointer(m_parent)==POINTER_INVALID)
      return m_x;

   return m_x + m_parent.GetGlobalX();
}

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

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

int CElement::m_element_count = 0;

//+------------------------------------------------------------------+
//| Base Element class constructor                                   |
//+------------------------------------------------------------------+
CElement::CElement(void) : m_child_count(0), m_x(0), m_y(0), m_size_x(100), m_size_y(100)
{
   m_name = "element_"+IntegerToString(m_element_count++);
}

//+------------------------------------------------------------------+
//| Base Element class destructor (delete child objects)             |
//+------------------------------------------------------------------+
CElement::~CElement(void)
{
   for (int i=0; i<m_child_count; i++)
      delete m_children[i];
}

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

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

//+------------------------------------------------------------------+
//| Set parent object (in element hierarchy)                         |
//+------------------------------------------------------------------+
void CElement::SetParent(CElement *parent)
{
   m_parent = parent;
   parent.AddChild(GetPointer(this));
}

//+------------------------------------------------------------------+
//| Add child object reference (function not exposed)                |
//+------------------------------------------------------------------+
void CElement::AddChild(CElement *child)
{
   if (CheckPointer(child)==POINTER_INVALID)
      return;

   ArrayResize(m_children, m_child_count+1);
   m_children[m_child_count] = child;
   m_child_count++;
}
//+------------------------------------------------------------------+
//| First creation of elements (children after)                      |
//+------------------------------------------------------------------+
void CElement::CreateChildren(void)
{
   for (int i=0; i<m_child_count; i++)
   {
      m_children[i].Create();
      m_children[i].CreateChildren();
   }
}

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

class CProgram
{
protected:
   CElement          m_element_holder;

public:

   void              CreateGUI() { m_element_holder.CreateChildren(); }
   void              AddMainElement(CElement* element) { element.SetParent(GetPointer(m_element_holder)); }
};

Прежде чем провести наш первый тест, нам все еще нужно расширить класс Element, так как теперь он пуст. Это позволит нам по-разному управлять разными типами объектов. Сначала мы создадим элемент холста (canvas), используя CCanvas (который на самом деле является растровой меткой). Объекты canvas — наиболее универсальные объекты для создания пользовательских графических интерфейсов, и мы можем сделать практически все что угодно полностью из canvas:

#include <Canvas/Canvas.mqh>
//+------------------------------------------------------------------+
//| Generic Bitmap label element (Canvas)                            |
//+------------------------------------------------------------------+
class CCanvasElement : public CElement
{
protected:
   CCanvas           m_canvas;

public:
                    ~CCanvasElement();

   virtual void      Create();
};

//+------------------------------------------------------------------+
//| Canvas Element destructor (destroy canvas)                       |
//+------------------------------------------------------------------+
CCanvasElement::~CCanvasElement(void)
{
   m_canvas.Destroy();
}

//+------------------------------------------------------------------+
//| Create bitmap label (override)                                   |
//+------------------------------------------------------------------+
void CCanvasElement::Create()
{
   CElement::Create();

   m_canvas.CreateBitmapLabel(0, 0, m_name, GetGlobalX(), GetGlobalY(), m_size_x, m_size_y, COLOR_FORMAT_ARGB_NORMALIZE);
}

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

m_canvas.Erase(ARGB(255, MathRand()%256, MathRand()%256, MathRand()%256));
m_canvas.Update(false);

Мы также создадим класс объекта редактирования (edit). Но почему именно его? Почему бы нам не вносить собственные изменения, рисуя прямо на холсте и отслеживая события клавиатуры? Есть одна вещь, которую невозможно сделать с помощью canvas. Это копирование и вставка текста (по крайней мере вне самого приложения) без DLL. Если вам не требуется эта функциональность для вашей библиотеки, вы можете добавить canvas непосредственно в класс Element и использовать его для каждого типа объекта. Как вы заметили, некоторые вещи по отношению к canvas сделаны по-другому...

class CEditElement : public CElement
{
public:
                    ~CEditElement();

   virtual void      Create();

   string            GetEditText() { return ObjectGetString(0, m_name, OBJPROP_TEXT); }
   void              SetEditText(string text) { ObjectSetString(0, m_name, OBJPROP_TEXT, text); }

};

//+------------------------------------------------------------------+
//| Edit element destructor (remove object from chart)               |
//+------------------------------------------------------------------+
CEditElement::~CEditElement(void)
{
   ObjectDelete(0, m_name);
}

//+------------------------------------------------------------------+
//| Create edit element (override) and set size/position             |
//+------------------------------------------------------------------+
void CEditElement::Create()
{
   CElement::Create();

   ObjectCreate(0, m_name, OBJ_EDIT, 0, 0, 0);

   ObjectSetInteger(0, m_name, OBJPROP_XDISTANCE, GetGlobalX());
   ObjectSetInteger(0, m_name, OBJPROP_YDISTANCE, GetGlobalY());
   ObjectSetInteger(0, m_name, OBJPROP_XSIZE, m_size_x);
   ObjectSetInteger(0, m_name, OBJPROP_YSIZE, m_size_y);
}

В этом случае нам нужно явно установить свойства положения и размера (в Canvas они выполняются внутри CreateBitmapLabel).

Со всеми этими изменениями мы наконец можем провести первый тест:

#include "Basis.mqh"
#include "CanvasElement.mqh"
#include "EditElement.mqh"

input int squares = 5; //Amount of squares
input bool add_edits = true; //Add edits to half of the squares

CProgram program;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   MathSrand((uint)TimeLocal());
   
   //100 is element size by default
   int max_x = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - 100; 
   int max_y = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - 100;
   
   for (int i=0; i<squares; i++)
   {
      CCanvasElement* drawing = new CCanvasElement();
      drawing.SetPosition(MathRand()%max_x, MathRand()%max_y);
      program.AddMainElement(drawing);
      
      if (add_edits && i<=squares/2)
      {
         CEditElement* edit = new CEditElement();
         edit.SetParent(drawing);
         edit.SetPosition(10, 10);
         edit.SetSize(80, 20);
      }
   }
   
   program.CreateGUI();
   ChartRedraw(0);
   
   return(INIT_SUCCEEDED);
}

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

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


Перемещения мыши

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

void CProgram::CreateGUI(void)
{
   ::ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
   m_element_holder.CreateChildren();
}

Они определяют положение мыши (x,y) и состояние кнопок мыши, Ctrl и Shift. Каждый раз, когда хотя бы одно из состояний или позиции изменяется, запускается событие.

Сначала мы определим этапы, которые может пройти кнопка:неактивная (inactive), когда она не используется, и активная (active), когда она нажата. Также добавим down и up, которые представляют первое активное и неактивное состояния соответственно (изменения состояния щелчка). Поскольку мы можем обнаружить каждую фазу, используя только события мыши, нам даже не нужно использовать события щелчка.

enum EInputState
{
   INPUT_STATE_INACTIVE=0,
   INPUT_STATE_UP=1,
   INPUT_STATE_DOWN=2,
   INPUT_STATE_ACTIVE=3
};

Чтобы упростить задачу, мы централизуем обработку событий мыши, а не будем проводить ее внутри каждого объекта. Таким образом мы сможем легче получать доступ к данным мыши и отслеживать их, что позволит использовать их даже для других типов событий и избежать повторяющихся вычислений. Мы назовем этот класс CInputs, поскольку он также включает в себя кнопки Ctrl и Shift, помимо ввода с помощью мыши.

//+------------------------------------------------------------------+
//| Mouse input processing class                                     |
//+------------------------------------------------------------------+
class CInputs
{
private:
   EInputState m_left_mouse_state;
   EInputState m_ctrl_state;
   EInputState m_shift_state;

   int m_pos_x;
   int m_pos_y;

   void UpdateState(EInputState &state, bool current);

public:
   CInputs();

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

   EInputState GetLeftMouseState() { return m_left_mouse_state; }
   EInputState GetCtrlState() { return m_ctrl_state; }
   EInputState GetShiftState() { return m_shift_state; }
   
   int X() { return m_pos_x; }
   int Y() { return m_pos_y; }
};

//+------------------------------------------------------------------+
//| Inputs constructor (initialize variables)                        |
//+------------------------------------------------------------------+
CInputs::CInputs(void) : m_left_mouse_state(INPUT_STATE_INACTIVE), m_ctrl_state(INPUT_STATE_INACTIVE),
   m_shift_state(INPUT_STATE_INACTIVE), m_pos_x(0), m_pos_y(0)
{
}

//+------------------------------------------------------------------+
//| Mouse input event processing                                     |
//+------------------------------------------------------------------+
void CInputs::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if(id!=CHARTEVENT_MOUSE_MOVE)
      return;

   m_pos_x = (int)lparam;
   m_pos_y = (int)dparam;

   uint state = uint(sparam);

   UpdateState(m_left_mouse_state, ((state & 1) == 1));
   UpdateState(m_ctrl_state, ((state & 8) == 8));
   UpdateState(m_shift_state, ((state & 4) == 4));
}

//+------------------------------------------------------------------+
//| Update state of a button (up, down, active, inactive)            |
//+------------------------------------------------------------------+
void CInputs::UpdateState(EInputState &state, bool current)
{
   if (current)
      state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_ACTIVE : INPUT_STATE_DOWN;
   else
      state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_UP : INPUT_STATE_INACTIVE;
}
//+------------------------------------------------------------------+

В UpdateState проверяем текущее состояние (логическое значение) и последнее состояние, чтобы определить, является ли вход активным/неактивным, и является ли это первым событием после изменения состояния (up/down). Информацию о ctrl и shift мы получаем "бесплатно" в sparam. Также получаем информацию о средней, правой и еще двух дополнительных кнопках мыши. Мы не добавили их в код, но при необходимости вы можете внести необходимые изменения.

Добавим экземпляр входных данных мыши в программу и сделаем ее доступной для каждого объекта с помощью указателя. С каждым событием экземпляр входных данных обновляется. Тип события фильтруется внутри класса входных данных (используются только события перемещения мыши).

class CProgram
{
//...

protected:
   CInputs           m_inputs;

//...

public:
   void              OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam);

//...
};

//+------------------------------------------------------------------+
//| Program constructor (pass inputs reference to holder)            |
//+------------------------------------------------------------------+
CProgram::CProgram(void)
{
   m_element_holder.SetInputs(GetPointer(m_inputs));
}

//+------------------------------------------------------------------+
//| Process chart event                                              |
//+------------------------------------------------------------------+
void CProgram::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   m_inputs.OnEvent(id, lparam, dparam, sparam);

   m_element_holder.OnChartEvent(id, lparam, dparam, sparam);
}

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

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

//+------------------------------------------------------------------+
//| Set mouse inputs reference                                       |
//+------------------------------------------------------------------+
void CElement::SetInputs(CInputs* inputs)
{
   m_inputs = inputs;

   for (int i = 0; i < m_child_count; i++)
      m_children[i].SetInputs(inputs);
}

//+------------------------------------------------------------------+
//| Add child object reference (function not exposed)                |
//+------------------------------------------------------------------+
void CElement::AddChild(CElement *child)
{
   if (CheckPointer(child) == POINTER_INVALID)
      return;

   ArrayResize(m_children, m_child_count + 1);
   m_children[m_child_count] = child;
   m_child_count++;

   child.SetInputs(m_inputs);
}

В следующем разделе мы создадим функции обработки событий и добавим несколько событий мыши каждому объекту.


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

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

class CProgram
{
private:
   bool              m_needs_redraw;

//...

public:
   CProgram();

   void              OnTimer();

//...

   void              RequestRedraw()
   {
      m_needs_redraw = true;
   }
};

void CProgram::OnTimer(void)
{
   if (m_needs_redraw)
   {
      ChartRedraw(0);
      m_needs_redraw = false;
   }
}

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

#define TIMER_STEP_MSC (16)

void CProgram::CreateGUI(void)
{
   ::ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
   m_element_holder.CreateChildren();
   ::EventSetMillisecondTimer(TIMER_STEP_MSC);
}

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

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

//+------------------------------------------------------------------+
//| Send event recursively and respond to it (for this element)      |
//+------------------------------------------------------------------+
void CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   for (int i = m_child_count - 1; i >= 0; i--)
      m_children[i].OnChartEvent(id, lparam, dparam, sparam);

   OnEvent(id, lparam, dparam, sparam);
}

В этой библиотеке мы решили рекурсивно передавать события от родительского элемента к дочернему. Это выбор дизайна (его можно сделать по-другому, например, используя прослушиватели или вызывая все объекты последовательно), но, как мы увидим позже, у него есть некоторые преимущества. OnEvent - переопределяемая защищенная функция. Он отделен от OnChartEvent (который является общедоступным), поэтому пользователь не может переопределять передачу событий дочерним объектам и выбирать, как выполнять родительский класс OnEvent (до, после или не выполнять его).

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

Объекты, которые не реагируют ни на одно событие

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

//+------------------------------------------------------------------+
//| Check if mouse is hovering any child element of this object      |
//+------------------------------------------------------------------+
bool CElement::CheckHovers(void)
{
   for (int i = 0; i < m_child_count; i++)
   {
      if (m_children[i].IsMouseHovering())
         return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Process chart event                                              |
//+------------------------------------------------------------------+
void CProgram::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   m_inputs.OnEvent(id, lparam, dparam, sparam);

   if (id == CHARTEVENT_MOUSE_MOVE)
      EnableControls(!m_element_holder.CheckHovers());

   m_element_holder.OnChartEvent(id, lparam, dparam, sparam);
}

//+------------------------------------------------------------------+
//| Enable/Disable chart scroll responses                            |
//+------------------------------------------------------------------+
void CProgram::EnableControls(bool enable)
{
   //Allow or disallow displacing chart
   ::ChartSetInteger(0, CHART_MOUSE_SCROLL, enable);
}

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

Нам придется добавить еще две проверки и приватную логическую переменную в CElement (m_dragged):

//+------------------------------------------------------------------+
//| Send event recursively and respond to it (for this element)      |
//+------------------------------------------------------------------+
void CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   for (int i = m_child_count - 1; i >= 0; i--)
      m_children[i].OnChartEvent(id, lparam, dparam, sparam);

   //Check dragging start
   if (id == CHARTEVENT_MOUSE_MOVE)
   {
      if (IsMouseHovering() && m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN)
         m_dragging = true;

      else if (m_dragging && m_inputs.GetLeftMouseState() == INPUT_STATE_UP)
         m_dragging = false;
   }

   OnEvent(id, lparam, dparam, sparam);
}

//+------------------------------------------------------------------+
//| Check if mouse hovers/drags any child element of this object     |
//+------------------------------------------------------------------+
bool CElement::CheckHovers(void)
{
   for (int i = 0; i < m_child_count; i++)
   {
      if (m_children[i].IsMouseHovering() || m_children[i].IsMouseDragging())
         return true;
   }
   return false;
}

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

Объекты останавливают перетаскивание графиков

К счастью, исправить такое поведение несложно:

//+------------------------------------------------------------------+
//| Check if mouse hovers/drags any child element of this object     |
//+------------------------------------------------------------------+
bool CElement::CheckHovers(void)
{
   EInputState state = m_inputs.GetLeftMouseState();
   bool state_check = state != INPUT_STATE_ACTIVE; //Filter drags that start in chart

   for (int i = 0; i < m_child_count; i++)
   {
      if ((m_children[i].IsMouseHovering() && state_check)
            || m_children[i].IsMouseDragging())
         return true;
   }
   return false;
}

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

Правильная фильтрация перетаскиваний

Теперь, когда всё готово, мы можем добавить функцию перетаскивания в класс прямоугольника. Однако вместо добавления его в CCanvasElement мы расширим класс с помощью наследования. Мы также извлечем рисунок строки из последнего примера, чтобы по умолчанию он был пустым. Поскольку мы уже добавили проверку перетаскивания, мы можем использовать их для обработки события и перемещения объектов. Чтобы изменить положение объекта в графике, нам нужно изменить его переменные, затем обновить его свойства позиции, дочернюю позицию и перерисовать график.

class CElement
{
//...

public:
   void              UpdatePosition();
};

//+------------------------------------------------------------------+
//| Update element (and children) position properties                |
//+------------------------------------------------------------------+
void CElement::UpdatePosition(void)
{
   ObjectSetInteger(0, m_name, OBJPROP_XDISTANCE, GetGlobalX());
   ObjectSetInteger(0, m_name, OBJPROP_YDISTANCE, GetGlobalY());

   for (int i = 0; i < m_child_count; i++)
      m_children[i].UpdatePosition();
}
class CCanvasElement : public CElement
{
protected:
   CCanvas           m_canvas;
   virtual void      DrawCanvas() {}

//...
};

//+------------------------------------------------------------------+
//| Create bitmap label (override)                                   |
//+------------------------------------------------------------------+
void CCanvasElement::Create()
{
//...

   DrawCanvas();
}
//+------------------------------------------------------------------+
//| Canvas class which responds to mouse drag events                 |
//+------------------------------------------------------------------+
class CDragElement : public CCanvasElement
{
private:
   int m_rel_mouse_x, m_rel_mouse_y;

protected:
   virtual void      DrawCanvas();

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

//+------------------------------------------------------------------+
//| Check mouse drag events                                          |
//+------------------------------------------------------------------+
bool CDragElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if (id != CHARTEVENT_MOUSE_MOVE)
      return false;

   if (!IsMouseDragging())
      return false;

   if (m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN) //First click
   {
      m_rel_mouse_x = m_inputs.X() - m_x;
      m_rel_mouse_y = m_inputs.Y() - m_y;
      return true;
   }

   //Move object
   m_x = m_inputs.X() - m_rel_mouse_x;
   m_y = m_inputs.Y() - m_rel_mouse_y;
   UpdatePosition();
   m_program.RequestRedraw();

   return true;
}

//+------------------------------------------------------------------+
//| Custom canvas draw function (fill with random color)             |
//+------------------------------------------------------------------+
void CDragElement::DrawCanvas(void)
{
   m_canvas.Erase(ARGB(255, MathRand() % 256, MathRand() % 256, MathRand() % 256));
   m_canvas.Update(false);
}

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

Несколько объектов движутся одновременно

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

Создадим логическую переменную, чтобы отслеживать, перекрыт ли объект:

class CElement
{
private:

   //...
   bool              m_occluded;

   //...

public:

   //...
   void              SetOccluded(bool occluded)
   {
      m_occluded = occluded;
   }
   bool              IsOccluded()
   {
      return m_occluded;
   }
   //...
};

Затем мы можем использовать OnChartEvent как способ "общения" между объектами. Для этого мы возвращаем логическое значение. Если бы объект получил событие, было бы true. Если объект был перетащен (даже если объект не отреагировал на перетаскивание) или если объект перекрыт (например, дочерним объектом), он также вернет true, поскольку это также заблокирует события для объектов, находящихся под ним.

bool CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   for (int i = m_child_count - 1; i >= 0; i--)
   {
      m_children[i].SetOccluded(IsOccluded());
      if (m_children[i].OnChartEvent(id, lparam, dparam, sparam))
         SetOccluded(true);
   }
   //Check dragging start
   if (id == CHARTEVENT_MOUSE_MOVE && !IsOccluded())
   {
      if (IsMouseHovering() && m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN)
         m_dragging = true;

      else if (m_dragging && m_inputs.GetLeftMouseState() == INPUT_STATE_UP)
         m_dragging = false;
   }

   return OnEvent(id, lparam, dparam, sparam) || IsMouseDragging() || IsOccluded();
}

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

bool CDragElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if (IsOccluded()) return false;

   if (id != CHARTEVENT_MOUSE_MOVE)
      return false;

   //...
}

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

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

Объекты правильно реагируют на перетаскивание

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


Показ и скрытие объектов

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

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

ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); //Show
ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); //Hide

Это свойство определяет, "на каких таймфреймах объект отображается, а на каких нет". Вы можете отображать объекты только в некоторых таймфреймах, но вряд ли нам это здесь понадобится.

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

class CElement
{
private:
   //...
   bool              m_hidden;
   bool              m_hidden_parent;

   void              HideObject();
   void              HideByParent();
   void              HideChildren();

   void              ShowObject();
   void              ShowByParent();
   void              ShowChildren();
   //...

public:
   
   //...
   void              Hide();
   void              Show();
   bool              IsHidden()
   {
      return m_hidden || m_hidden_parent;
   }
};

//+------------------------------------------------------------------+
//| Display element (if parent is also visible)                      |
//+------------------------------------------------------------------+
void CElement::Show(void)
{

   if (!IsHidden())

      return;
        
   m_hidden = false;
   ShowObject();
        

   if (CheckPointer(m_program) != POINTER_INVALID)
      m_program.RequestRedraw();
}

//+------------------------------------------------------------------+
//| Hide element                                                     |
//+------------------------------------------------------------------+
void CElement::Hide(void)
{
   m_hidden = true;

   if (m_hidden_parent)
      return;

   HideObject();

   if (CheckPointer(m_program) != POINTER_INVALID)
      m_program.RequestRedraw();
}

//+------------------------------------------------------------------+
//| Change visibility property to show (not exposed)                 |
//+------------------------------------------------------------------+
void CElement::ShowObject(void)
{
   if (IsHidden()) //Parent or self
      return;
        
   ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   ShowChildren();
}

//+------------------------------------------------------------------+
//| Show object when not hidden and parent is shown (not exposed)    |
//+------------------------------------------------------------------+
void CElement::ShowByParent(void)
{
   m_hidden_parent = false;
   ShowObject();
}

//+------------------------------------------------------------------+
//| Show child objects recursively (not exposed)                     |
//+------------------------------------------------------------------+
void CElement::ShowChildren(void)
{
   for (int i = 0; i < m_child_count; i++)
      m_children[i].ShowByParent();
}

//+------------------------------------------------------------------+
//| Change visibility property to hide (not exposed)                                                                  |
//+------------------------------------------------------------------+
void CElement::HideObject(void)
{
   ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   HideChildren();
}

//+------------------------------------------------------------------+
//| Hide element when parent is hidden (not exposed)                 |
//+------------------------------------------------------------------+
void CElement::HideByParent(void)
{
   m_hidden_parent = true;
   if (m_hidden)
      return;

   HideObject();
}

//+------------------------------------------------------------------+
//| Hide child objects recursively (not exposed)                     |
//+------------------------------------------------------------------+
void CElement::HideChildren(void)
{
   for (int i = 0; i < m_child_count; i++)
      m_children[i].HideByParent();
}

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

В этом случае Show (показать) и Hide (скрыть) — единственные видимые снаружи функции, позволяющие изменить видимость объекта. В основном функции используются для изменения флагов видимости и вызова ObjectSetProperty при необходимости. Кроме того, флаги дочерних объектов изменяются рекурсивно. Есть другие проверки, которые позволяют избежать ненужных вызовов функций (например, скрытие дочернего объекта, когда он уже скрыт). Наконец, следует отметить, что перерисовка графика необходима для отображения изменений видимости, поэтому в обоих случаях мы вызываем RequestRedraw.

Нам также необходимо скрывать объекты при создании, поскольку теоретически они могут быть помечены как скрытые до того, как будут созданы:

void CElement::CreateChildren(void)
{
   for (int i = 0; i < m_child_count; i++)
   {
      m_children[i].Create();
      m_children[i].CreateChildren();
   }

   if (IsHidden()) HideObject();
}

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

class CHideElement : public CDragElement
{
private:
   int               key_id;

protected:
   virtual void      DrawCanvas();

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

public:
   CHideElement(int id);
};

//+------------------------------------------------------------------+
//| Hide element constructor (set keyboard ID)                       |
//+------------------------------------------------------------------+
CHideElement::CHideElement(int id) : key_id(id)
{
}

//+------------------------------------------------------------------+
//| Hide element when its key is pressed (inherit drag events)       |
//+------------------------------------------------------------------+
bool CHideElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        
   bool drag = CDragElement::OnEvent(id, lparam, dparam, sparam);

   if (id != CHARTEVENT_KEYDOWN)
      return drag;

   if (lparam == '0' + key_id) //Toggle hide/show
   {
      if (IsHidden())
         Show();
      else
         Hide();
   }

   return true;
}

//+------------------------------------------------------------------+
//| Draw canvas function (fill with color and display number ID)     |
//+------------------------------------------------------------------+
void CHideElement::DrawCanvas(void)
{
   m_canvas.Erase(ARGB(255, MathRand() % 256, MathRand() % 256, MathRand() % 256));
   m_canvas.FontSet("Arial", 50);
   m_canvas.TextOut(25, 25, IntegerToString(key_id), ColorToARGB(clrWhite));
   m_canvas.Update(false);
}

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

int OnInit()
{
   MathSrand((uint)TimeLocal());

   //100 is element size by default
   int max_x = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - 100;
   int max_y = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - 100;

   for (int i = 0; i < 10; i++)
   {
      CHideElement* drawing = new CHideElement(i);
      drawing.SetPosition(MathRand() % max_x, MathRand() % max_y);
      program.AddMainElement(drawing);
   }

   program.CreateGUI();
   ChartRedraw(0);

   return(INIT_SUCCEEDED);
}

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

Переключение видимости объектов


Z-порядок (и его изменение)

Z-порядок (Z Order) относится к последовательности, в которой отображаются объекты. Проще говоря, координаты X и Y определяют положение объекта на экране, а координата Z определяет его глубину или порядок расположения. Объекты с более низкими значениями Z отображаются над объектами с более высокими значениями.

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

Однако, если мы поиграемся с последним примером, то заметим кое-что интересное...

Изменение порядка Z при отображении

Если скрыть и показать объект, он снова появится над всеми остальными! Означает ли это, что мы можем мгновенно скрыть и показать объект, и он будет отображаться выше всех остальных? Да!

Чтобы проверить это, нам нужно будет лишь немного изменить наш последний производный класс (CHideElement), чтобы вместо переключения видимости при каждом вводе с клавиатуры он повышал Z-порядок этого конкретного объекта. Мы также изменим название класса...

bool CRaiseElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{

   bool drag = CDragElement::OnEvent(id, lparam, dparam, sparam);

   if (id != CHARTEVENT_KEYDOWN)
      return drag;

   if (lparam == '0' + key_id)
   {
      ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
      ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
      if (CheckPointer(m_program) != POINTER_INVALID)
         m_program.RequestRedraw();
   }

   return true;
}

Как всегда, нельзя забывать, что после изменения Z-порядка необходима перерисовка. Если мы запустим тест, то увидим вот что:

Изменение Z-порядка

Как видите, мы можем легко поднимать объекты по своему желанию. Мы не будем реализовывать эту конкретную функцию в библиотеке, чтобы не усложнять коды в статье (кроме того, этот эффект можно получить, вызвав Hide, а затем сразу после этого вызвав Show). Более того, с помощью Z-ордеров можно сделать и другие вещи. Что если мы хотим поднять объект только на один уровень? Или хотим уменьшить его Z-порядок до минимума? Во всех случаях решение состоит в вызове подъема Z-порядка в необходимом количестве объектов в ожидаемом правильном порядке (снизу вверх). В первом случае вы должны вызвать функцию для объекта, который хотите поднять на один уровень, а затем для всех объектов, находящихся выше него, в порядке их сортировки. Во втором случае вы бы сделали это, но для всех объектов (даже если вы могли бы игнорировать тот, который доходит до нижнего Z-порядка).

Тем не менее, есть подвох при реализации систем Z-порядка: вы не можете показывать объект, который скрыт иизменить его Z-порядок в том же фрейме. Предположим, что вы показываете кнопку внутри окна, но в то же время вы хотите поднять это окно: если вы вызовите Show для кнопки (устанавливает OBJPROP_TIMEFRAMES на all periods для этой кнопки) и после этого поднимете Z для окна (которое устанавливает для OBJPROP_TIMEFRAMES значение no periods, а затем all periods для окна, а затем для всех объектов в окне в правильном порядке), тогда кнопка останется за окном. Причина этого, по-видимому, в том, что действуют только первые изменения свойства OBJPROP_TIMEFRAMES, поэтому объект кнопки поднимается только при его отображении (а не при следующем поднятии Z).

Одним из решений этой проблемы может быть поддержание очереди объектов и проверка изменений видимости или Z-порядка. Затем все они выполняются только один раз за кадр. Таким образом, вам также придется изменить функцию Show, чтобы "пометить" этот объект, а не показывать его напрямую. Если вы только начинаете, я бы рекомендовал не слишком беспокоиться об этом, поскольку такие ситуации редки и некритичны (хотя вам следует избегать ситуаций, когда эта проблема может возникнуть на текущем этапе).


Заключение

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

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

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

Прикрепленные файлы |
GUIArticle.zip (91.13 KB)
Популяционные алгоритмы оптимизации: Бинарный генетический алгоритм (Binary Genetic Algorithm, BGA). Часть I Популяционные алгоритмы оптимизации: Бинарный генетический алгоритм (Binary Genetic Algorithm, BGA). Часть I
В этой статье мы проведем исследование различных методов, применяемых в бинарных генетических алгоритмах и других популяционных алгоритмах. Мы рассмотрим основные компоненты алгоритма, такие как селекция, кроссовер и мутация, а также их влияние на процесс оптимизации. Кроме того, мы изучим способы представления информации и их влияние на результаты оптимизации.
Эластичная чистая регрессия с использованием покоординатного спуска в MQL5 Эластичная чистая регрессия с использованием покоординатного спуска в MQL5
В этой статье мы исследуем практическую реализацию эластичной чистой регрессии (elastic net regression), чтобы минимизировать переобучение и в то же время автоматически отделять полезные предикторы от тех, которые имеют небольшую прогностическую силу.
Теория категорий в MQL5 (Часть 20): Самовнимание и трансформер Теория категорий в MQL5 (Часть 20): Самовнимание и трансформер
Немного отвлечемся от наших постоянных тем и рассмотрим часть алгоритма ChatGPT. Есть ли у него какие-то сходства или понятия, заимствованные из естественных преобразований? Попытаемся ответить на эти и другие вопросы, используя наш код в формате класса сигнала.
Нейросети — это просто (Часть 73): АвтоБоты прогнозирования ценового движения Нейросети — это просто (Часть 73): АвтоБоты прогнозирования ценового движения
Мы продолжаем рассмотрения алгоритмов обучения моделей прогнозирования траекторий. И в данной статье я предлагаю Вам познакомиться с методом под названием “AutoBots”.