English Русский Español Deutsch 日本語 Português
preview
GUI:利用 MQL 创建您自己的图形库的提示和技巧

GUI:利用 MQL 创建您自己的图形库的提示和技巧

MetaTrader 5示例 | 15 一月 2024, 09:42
550 0
Manuel Alejandro Cercos Perez
Manuel Alejandro Cercos Perez

概述

开发 MetaTrader 5 关联环境中的 GUI 函数库是任何人都能想到的最大非特性项目之一,其它还有 AI、(优秀的)神经网络、和......熟练运用您尚未开发出的 GUI 函数库。

最后一点我是半开玩笑的,当然,学习如何使用已经制作完成的函数库更容易(即使外面的 GUI 函数库 非常庞大)! 但是,若我能学会如何使用一个比我自行打造更好的函数库,为什么还要从头开始创建一个呢?

好吧,有几个很好的理由。 您也许考虑到对于您的特定项目它太了,您若需要一些非常特殊的东西,而这些又没包含在函数库中,或者原来的实现不可能做到的功能,您也许就需要扩展它(有些函数库可能很难扩展),它可能有一个漏洞(不包括那些因滥用函数库而引起的错误)......或者您可能只是想学习它。 这些问题中的大多数都可由那些特定函数库的作者来解决,但您只能依赖他们注意到或愿意这样做(譬如扩展功能的情况)。

在本文中,我们的目标不是教导您如何制作一个界面,亦非展示开发一个全功能函数库的步骤。 取而代之,我们将提供一些如何制作一些特殊 GUI 函数库的示例,如此它们就可以作为打造一个函数库的起点,从而解决您已经发现的特定问题,或者初步理解有关已完成 GUI 函数库的庞大代码库内所发生的一切。

总之...我们开始制作一个 “GUI 函数库”


程序结构和对象层次

在开始制作 GUI 函数库之前,我们应该问:什么是 GUI 函数库? 简言之,它是对象的美化层次结构,即跟踪其它(图表)对象,并修改其属性以便生成不同的效果,并触发移动、单击、或更改颜色等事件。 这种层次结构的组织方式可能因实现而异,不过最常见的(也是我最喜欢的)是元素的树结构,其中一个元素可以有其它子元素。

为了创建它,我们将从一个元素的基本实现开始:

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_xm_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];
}

最后,我们定义关系函数 AddChildSetParent,由于在元素之间我们需要两个引用来进行通信:例如,为了获得全局位置,子项需要知道父项的位置,但在父项位置变更时,需要通知子项(我们稍后将在最后一部分实现这些)。 为了避免冗余,我们已将 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 类,因为现在它是空的。 以这种方式,我们就能以不同的方式管理不同类型的对象。 首先,我们将创建一个 CCanvas(实际上是一个位图标签)类型的画布元素。 Canvas 对象是制作自定义 GUI 时最常用的多用途对象,我们几乎可据画布制作整体的一切:

#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 的末尾添加这 2 行,以随机颜色填充画布。 更正确的方式应是扩展画布类,来创建这种特定类型的对象,但现在没有必要加入太多的复杂性。

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

我们还要创建一个编辑对象类。 但为什么这个如此特殊呢? 为什么我们不能自行编辑,直接在画布上绘制,以及跟踪键盘事件,并在其内书写呢? 嗯,有一件事是画布无法完成的,那就是复制粘贴文本(至少在没有 DLL 支持下,与应用程序自身外部往来)。 如果您的函数库不需要此功能,则您可将画布直接添加到 Element 类之中,并将其用于每种类型的对象。 您会注意到,与画布相关的某些事情是以不同的方式完成的......

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);
}

在这种情况下,我们需要显式设置 position 和 size 属性(在 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);
}

该程序将生成一些正方形,并在删除图表时将其从中删除。 注意,每个编辑元素的定位都相对其父元素在同一位置。

目前,这些正方形并没有做太多事情,它们只是“在那里”。 在随后的各章节中,我们将讨论如何为它们添加一些行为。


鼠标输入

如果您曾据图表事件操控,您就会知道单纯靠点击事件不足以创建任何复杂的行为。 创建更佳界面的诀窍是使用鼠标移动事件。 这些事件需要在 EA 启动时激活,因此我们将在创建 GUI 之前激活它们:

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

它们提供鼠标位置(x,y)、鼠标按钮、ctrl 和 shift 的状态。 每当至少一种状态或位置发生变化时,就会触发一个事件。

首先,我们要定义按钮可以遍及的阶段:不使用时为未激活,单击时为激活。 我们还要加入向下向上,分别代表第一次激活状态和未激活状态(点击状态的变化)。 由于我们仅凭鼠标事件就能检测每个阶段,因此我们甚至不需要用到点击事件。

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 中,我们检查当前状态(布尔值),和上一次状态,以便判定输入是否处于激活/未激活状态,以及它是否为状态变化(向上/向下)之后的初次事件。 我们在 sparam 中能“自由”获得 ctrl 和 shift、以及中键、右键和另外 2 个额外的鼠标按钮的信息。 我们尚未将这些加到代码之中,但如果您想使用它们,可以轻松添加所需的更改。

我们将往程序中添加一个鼠标输入实例,并依据指针令每个对象都可以访问它。 对应每个事件,输入实例都会更新。 事件类型在输入类中内部筛选(仅用到鼠标移动事件)。

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 也许会导致 GUI 闪烁,这就是为什么我更喜欢集中执行它的原因:

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_dragging) 里再为此添加 2 个检查,以及一个私密 bool 变量:

//+------------------------------------------------------------------+
//| 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 函数调用说明,我们可以实现这样的功能,来显示和隐藏 GUI 对象层次结构中的对象:

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-顺序是指显示对象的顺序。 简单地说,X-Y 坐标决定了对象在屏幕上的位置,而 Z-坐标决定了对象的深度或堆叠顺序。 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 设置为该按钮的所有周期),然后窗口提升 Z(将窗口的 OBJPROP_TIMEFRAMES 设置为无周期,然后再设置为所有周期,然后以正确的顺序排列窗口中的所有对象),则按钮将保留在窗口后面。 这个原因似乎是只有对 OBJPROP_TIMEFRAMES 属性的第一次修改才有效,故按钮对象仅在显示时有效引发(且在下一次提升 Z 时无效)。

该问题的一种解决方案可能涉及维护对象队列,并检查可见性、或 Z-顺序的变化。 之后,仅在每个片刻执行一次所有这些操作。 以这种方式,您也需要修改 Show 函数来“标记”该对象,取代直接显示它。 如果您还没有开始出发,我建议您不要太担心这个问题,因为它并不经常发生,也不是关键的事情(即使此刻您应该避免可能出现这个问题的情况)。


结束语

在本文中,我们通览了一些需要知晓的关键函数,在创建 GUI 库时能有效地组合,并提供了一些小示例来验证每一点。 它应当提供了对 GUI 库中一般工作方式的基本认知。 成品函数库并非全功能,相比许多其它 GUI 库中所用的一些函数,这只能达到演示所需的最低限度。

虽然生成的代码相对简单,但应当注意的是,一旦您开始添加更多功能、或制备更多对象类型(尤其是若它们彼此间带有事件关系的子对象时),GUI 库可能会(并且将会)变得更加复杂。 此外,其它函数库的结构也许与此处概括的内容存在各种差异,具体取决于设计决策、或所需的性能/特殊功能。

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/13169

附加的文件 |
GUIArticle.zip (91.13 KB)

该作者的其他文章

如何在 MQL5.com 上造就成功的信号提供者 如何在 MQL5.com 上造就成功的信号提供者
在本文中我的主要目标是为您提供一个简单而精准的步骤说明,助您变身 MQL5.com 上的顶级信号提供者。 借鉴我的知识和经验,我将讲解如何造就一名成功的信号提供者,包括如何寻找、测试、和优化一个优秀的策略。 此外,我将提供有关发布信号、撰写令人信服的推介、以及有效推广和管理信号的提示。
开发回放系统 — 市场模拟(第 16 部分):新的类系统 开发回放系统 — 市场模拟(第 16 部分):新的类系统
我们需要更好地组织我们的工作。 代码正在快速增长,如果现在不做,那么以后就变得更不可能了。 我们分而治之。 MQL5 支持类,可协助实现此任务,但为此,我们需要对类有一定的了解。 大概最让初学者困惑的是继承。 在本文中,我们将看到如何以实用和简单的方式来运用这些机制。
在 MQL5 中利用 ARIMA 模型进行预测 在 MQL5 中利用 ARIMA 模型进行预测
在本文中,我们继续开发构建 ARIMA 模型的 CArima 类,添加支持预测的直观方法。
开发回放系统 — 市场模拟(第 15 部分):模拟器的诞生(V)- 随机游走 开发回放系统 — 市场模拟(第 15 部分):模拟器的诞生(V)- 随机游走
在本文中,我们将完成自有系统模拟器的开发。 于此的主要目标是就上一篇文章中讨论的算法进项配置。 该算法旨在创建随机游走走势。 因此,为了明白今天的讲义,有必要了解以前文章的内容。 如果您尚未跟踪模拟器的开发,我建议您从头开始阅读本系列文章。 否则,您也许对此处将要讲解的内容不明所以。