下载MetaTrader 5

图形界面 XI: 渲染控件 (统合构建14.2)

22 八月 2017, 17:44
Anatoli Kazharski
1
1 152

内容

概述

首篇文章 图形界面 I: 函数库结构的准备 (第 1 章) 详细研究了这个函数库。本系列每篇文章的最后, 提供了当前开发阶段的完整版函数库。文件必须放置于存档中所在的相同目录下。

在更新版的函数库中, 所有控件将在 OBJ_BITMAP_LABEL 类型的单独图形对象上绘制。此外, 我们将继续描述函数库代码的全局优化。此描述已在 以前的文章 中开始。现在我们来研究函数库中核心类的变化。新版本的函数库已经变得更加面向对象。代码变得更加简明易懂。这有助于用户根据自己的任务独立开发函数库。 


绘制控件的方法

已在 CElement 类已声明了一个 画布类的实例。其方法允许创建一个对象来绘制和删除它。若有必要, 可以获得它的指针

class CElement : public CElementBase
  {
protected:
   //--- 绘制控件的画布
   CRectCanvas       m_canvas;
   //---
public:
   //--- 返回指向控件画布的指针
   CRectCanvas      *CanvasPointer(void) { return(::GetPointer(m_canvas)); }
  };

现在有一个通用的方法来创建一个用于绘制控件外观的对象 (画布)。它位于 CElement 基类中, 可以从函数库的所有控件类访问。CElement::CreateCanvas() 方法用于创建此类型的图形对象。作为参数, 必须传递 (1) 名称, (2) 坐标, (3) 维度和 (4) 颜色格式。省缺格式为 COLOR_FORMAT_ARGB_NORMALIZE, 这可令控件变得透明。如果传递了无效维度, 它们将在方法的开头被修正。一旦在运行 MQL 应用程序的图表上对象了创建并加载, 将会为其设置基本属性, 这在以前的所有控件类中不断重复。

class CElement : public CElementBase
  {
public:
   //--- 创建画布
   bool              CreateCanvas(const string name,const int x,const int y,
                                  const int x_size,const int y_size,ENUM_COLOR_FORMAT clr_format=COLOR_FORMAT_ARGB_NORMALIZE);
  };
//+------------------------------------------------------------------+
//| 创建绘制控件的画布                                                |
//+------------------------------------------------------------------+
bool CElement::CreateCanvas(const string name,const int x,const int y,
                            const int x_size,const int y_size,ENUM_COLOR_FORMAT clr_format=COLOR_FORMAT_ARGB_NORMALIZE)
  {
//--- 调整尺寸
   int xsize =(x_size<1)? 50 : x_size;
   int ysize =(y_size<1)? 20 : y_size;
//--- 重置最后的错误
   ::ResetLastError();
//--- 创建对象
   if(!m_canvas.CreateBitmapLabel(m_chart_id,m_subwin,name,x,y,xsize,ysize,clr_format))
     {
      ::Print(__FUNCTION__," > 创建绘制控件的画布失败 ("+m_class_name+"): ",::GetLastError());
      return(false);
     }
//--- 重置最后的错误
   ::ResetLastError();
//--- 获取指向基类的指针
   CChartObject *chart=::GetPointer(m_canvas);
//--- 挂载到图表
   if(!chart.Attach(m_chart_id,name,(int)m_subwin,(int)1))
     {
      ::Print(__FUNCTION__," > 将绘图画布挂载到图表失败: ",::GetLastError());
      return(false);
     }
//--- 属性
   m_canvas.Tooltip("\n");
   m_canvas.Corner(m_corner);
   m_canvas.Selectable(false);
//--- 除窗体外, 所有控件的优先级高于主控件
   Z_Order((dynamic_cast<CWindow*>(&this)!=NULL)? 0 : m_main.Z_Order()+1);
//--- 坐标
   m_canvas.X(x);
   m_canvas.Y(y);
//--- 大小
   m_canvas.XSize(x_size);
   m_canvas.YSize(y_size);
//--- 距极点的偏移
   m_canvas.XGap(CalculateXGap(x));
   m_canvas.YGap(CalculateYGap(y));
   return(true);
  }

我们来转进到绘制控件的基本方法。它们都位于 CElement 类中, 并声明为 virtual。 

首先来绘制背景。在基本版中, 它只是简单地使用 CElement::DrawBackground() 方法填充颜色。如有必要, 可以启用透明度。为此, 请使用 CElement::Alpha() 方法, Alpha 通道值从 0255 作为参数传递。零值意味着完全透明。在当前版本中, 透明度仅适用于背景填充和边框。文字和图像将保持完全不透明, 并清除所有 alpha 通道值。

class CElement : public CElementBase
  {
protected:
   //--- alpha 通道值 (控件的透明度)
   uchar             m_alpha;
   //---
public:
   //--- alpha 通道值 (控件的透明度)
   void              Alpha(const uchar value)                        { m_alpha=value;                   }
   uchar             Alpha(void)                               const { return(m_alpha);                 }
   //---
protected:
   //--- 绘制背景
   virtual void      DrawBackground(void);
  };
//+------------------------------------------------------------------+
//| 绘制背景                                                         |
//+------------------------------------------------------------------+
void CElement::DrawBackground(void)
  {
   m_canvas.Erase(::ColorToARGB(m_back_color,m_alpha));
  }

通常需要为特定的控件画一个边框。CElement::DrawBorder() 方法在画布对象的边缘周围绘制一个边框。Rectangle() 方法也可以用于此目的。它绘制一个未经填充的矩形。

class CElement : public CElementBase
  {
protected:
   //--- 绘制边框
   virtual void      DrawBorder(void);
  };
//+------------------------------------------------------------------+
//| 绘制边框                                                         |
//+------------------------------------------------------------------+
void CElement::DrawBorder(void)
  {
//--- 坐标
   int x1=0,y1=0;
   int x2=m_canvas.X_Size()-1;
   int y2=m_canvas.Y_Size()-1;
//--- 绘制一个未经填充的矩形
   m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(m_border_color,m_alpha));
  }

上一篇文章中已经提到可以将任意数量的图片组分配给任何控件。所以, 绘制控件的方法必须能够输出用户设置的所有图像。CElement::DrawImage() 方法即用于此目的。程序按顺序 遍历所有的组其中的图片, 将它们逐像素输出到画布。在输出图像的循环开始之前, 检测组中当前所选的图片。参见此方法的代码:

class CElement : public CElementBase
  {
protected:
   //--- 绘制图片
   virtual void      DrawImage(void);
  };
//+------------------------------------------------------------------+
//| 绘制图片                                                          |
//+------------------------------------------------------------------+
void CElement::DrawImage(void)
  {
//--- 组的数量
   uint group_total=ImagesGroupTotal();
//--- 绘制图片
   for(uint g=0; g<group_total; g++)
     {
      //--- 所选图片的索引
      int i=SelectedImage(g);
      //--- 如果没有图片
      if(i==WRONG_VALUE)
         continue;
      //--- 坐标
      int x =m_images_group[g].m_x_gap;
      int y =m_images_group[g].m_y_gap;
      //--- 大小
      uint height =m_images_group[g].m_image[i].Height();
      uint width  =m_images_group[g].m_image[i].Width();
      //--- 绘制
      for(uint ly=0,p=0; ly<height; ly++)
        {
         for(uint lx=0; lx<width; lx++,p++)
           {
            //--- 如果没有颜色, 转至下一像素
            if(m_images_group[g].m_image[i].Data(p)<1)
               continue;
            //--- 获取下层 (单元格背景) 的颜色, 和图标指定像素的颜色
            uint background  =::ColorToARGB(m_canvas.PixelGet(x+lx,y+ly));
            uint pixel_color =m_images_group[g].m_image[i].Data(p);
            //--- 混合颜色
            uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color));
            //--- 绘制叠加图标的像素
            m_canvas.PixelSet(x+lx,y+ly,foreground);
           }
        }
     }
  }

许多控件都有一个文本描述。可以使用 CElement::DrawText() 方法显示它。此方法中的若干字段允许根据控件的状态自定义文本的显示。控件有三个状态可用:

  • 锁定;
  • 按下;
  • 聚焦 (鼠标悬停)。

此外, 方法考虑是否 启用中心文本对齐方式。其代码如此:

class CElement : public CElementBase
  {
protected:
   //--- 绘制文字
   virtual void      DrawText(void);
  };
//+------------------------------------------------------------------+
//| 绘制文字                                                         |
//+------------------------------------------------------------------+
void CElement::DrawText(void)
  {
//--- 坐标
   int x =m_label_x_gap;
   int y =m_label_y_gap;
//--- 定义文本标签的颜色
   color clr=clrBlack;
//--- 如果控件被锁定
   if(m_is_locked)
      clr=m_label_color_locked;
   else
     {
      //--- 如果控件按下
      if(!m_is_pressed)
         clr=(m_mouse_focus)? m_label_color_hover : m_label_color;
      else
        {
         if(m_class_name=="CButton")
            clr=m_label_color_pressed;
         else
            clr=(m_mouse_focus)? m_label_color_hover : m_label_color_pressed;
        }
     }
//--- 字号属性
   m_canvas.FontSet(m_font,-m_font_size*10,FW_NORMAL);
//--- 考虑中心对齐模式绘制文本
   if(m_is_center_text)
     {
      x =m_x_size>>1;
      y =m_y_size>>1;
      m_canvas.TextOut(x,y,m_label_text,::ColorToARGB(clr),TA_CENTER|TA_VCENTER);
     }
   else
      m_canvas.TextOut(x,y,m_label_text,::ColorToARGB(clr),TA_LEFT);
  }

所有上述方法将在公共虚拟方法 CElement::Draw() 里调用。它没有基础代码, 因为每个控件中要调用的绘图方法集合都是专有的。

class CElement : public CElementBase
  {
public:
   //--- 绘制控件
   virtual void      Draw(void) {}
  };

研究 CElement::Update() 方法。每次程序更改图形界面的控件时都会调用它。有两个调用选项可用: (1) 完全重绘控件 或 (2) 应用之前所做的更改 (见下面的代码清单)。这个方法也被声明为虚拟的, 因为某些控件类可能有其专有版本, 它们考虑了方法和渲染顺序的特殊性。

class CElement : public CElementBase
  {
public:
   //--- 更新控件以显示最新变化
   virtual void      Update(const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| 更新控件                                                         |
//+------------------------------------------------------------------+
void CElement::Update(const bool redraw=false)
  {
//--- 重绘控件
   if(redraw)
     {
      Draw();
      m_canvas.Update();
      return;
     }
//--- 应用
   m_canvas.Update();
  }


新设计的图形界面

由于函数库的所有控件都已体现, 所以可以实现图形界面的新设计。无需发明任何特殊的东西, 可使用现成的解决方案。以 Windows 10 操作系统的简约美学为基础。 

图标的图形, 诸如形成控件的按钮, 单选按钮, 复选框, 组合框, 菜单项, 树形列表项及其它均类似于 Windows 10。 

之前提到的透明度现在可以为任何控件设置。下面的屏幕截图显示了半透明窗口的示例 (CWindow)。此处的 alpha 通道值是 200。  

图例. 8. 演示控件窗体的透明度。 

图例. 8. 演示控件窗体的透明度。


若要令窗体的整个区域透明, 请使用 CWindow::TransparentOnlyCaption() 方法。省缺模式 仅对标题应用透明度。 

class CWindow : public CElement
  {
private:
   //--- 仅为标题启用透明度
   bool              m_transparent_only_caption;
   //---
public:
   //--- 仅为标题启用透明度模式
   void              TransparentOnlyCaption(const bool state) { m_transparent_only_caption=state; }
  };
//+------------------------------------------------------------------+
//| 构造函数                                                          |
//+------------------------------------------------------------------+
CWindow::CWindow(void) : m_transparent_only_caption(true)
  {
...
  }

以下是不同类型按钮的外观:

 图例. 9. 演示几种按钮类型的外观。

图例. 9. 演示几种按钮类型的外观。


下一个屏幕截图显示了复选框, 旋转编辑框, 带有下拉列表和滚动条的组合框以及数字滑块的当前外观。请注意, 现在可以制作动画图标了。状态栏的第三项模仿从服务器断开连接。其外观精确复制 MetaTrader 5 状态条里的类似元素。

图例. 10. 演示复选框, 组合框, 滑块和其它控件的外观。

图例. 10. 演示复选框, 组合框, 滑块和其它控件的外观。

来自图形界面函数库的其它控件的外观可以在本文附带的测试 MQL 应用程序中看到。


工具提示

其它管理控件的工具提示显示的方法也已添加到 CElement 类中。现在可以为任何控件设置标准工具提示, 其文本不可超过 63 个字符。使用 CElement::Tooltip() 方法设置和获取工具提示文本。

class CElement : public CElementBase
  {
protected:
   //--- 工具提示文字
   string            m_tooltip_text;
   //---
public:
   //--- 工具提示
   void              Tooltip(const string text)                      { m_tooltip_text=text;             }
   string            Tooltip(void)                             const { return(m_tooltip_text);          }
  };

使用 CElement::ShowTooltip() 方法启用或禁用工具提示显示。 

class CElement : public CElementBase
  {
public:
   //--- 工具提示显示模式
   void              ShowTooltip(const bool state);
  };
//+------------------------------------------------------------------+
//| 设置工具提示显示                                                  |
//+------------------------------------------------------------------+
void CElement::ShowTooltip(const bool state)
  {
   if(state)
      m_canvas.Tooltip(m_tooltip_text);
   else
      m_canvas.Tooltip("\n");
  }

每个控件类都拥有获取指向嵌套控件指针的方法。例如, 如果需要为窗体按钮创建工具提示, 则应将以下代码行添加到自定义类中的窗体创建方法中:

...
//--- 设置工具提示
   m_window.GetCloseButtonPointer().Tooltip("关闭");
   m_window.GetCollapseButtonPointer().Tooltip("折叠/展开");
   m_window.GetTooltipButtonPointer().Tooltip("工具提示");
...

参见下图看它如何工作。窗体按钮已启用标准工具提示。也许控件需要长于 63 个字符的描述, 请使用 CTooltip 控件。

 图例. 11. 演示两种工具提示 (标准和定制)。

图例. 11. 演示两种工具提示 (标准和定制)。



新的事件标识符

添加了新的事件标识符。这显著降低了 CPU 的资源消耗。这是如何实现的?

当创建大型带有图形界面和大量控件的 MQL 应用程序时, 重要的是最大限度地减少 CPU 占用。

如果您用鼠标悬停在控件上, 则会高亮显示。这表明该控件可用于交互。不过, 并非所有控件在同一时间均可用及可见。

  • 下拉列表和日历, 大多数时间都隐藏了上下文菜单。它们偶尔被打开, 只让用户选择必要的选项, 日期或模式。
  • 控件组可以分配到不同的选项卡, 但一次只能打开一个选项卡。 
  • 如果窗体最小化, 那么它的所有控件也被隐藏。
  • 如果一个对话框打开, 则只有这个窗体将会响应事件。

逻辑上, 当图形界面中只有一部分可供使用时, 没道理持续地处理整个控件列表。只需要为打开的控件列表生成一个事件处理数组。

还有带交互的控件只影响控件本身。所以, 这种控件是仅有的必须留待处理的控件。我们列出这些控件和状况:

  • 移动滚动条按钮 (CScroll)。有必要仅启用滚动条本身和它的部分控件 (列表视图, 表格, 多行文本框等)。 
  • 移动滑块按钮 (CSlider)。为此, 滑块控件和旋转编辑框, 足以反映数值的变化。
  • 更改表格的列宽度 (CTable)。只有表格必须留待处理。
  • 在树形视图控件中更改列表的宽度 (CTreeView)。只有拖动列表的相邻边框时, 控件必须要处理。
  • 移动窗体 (CWindow)。除了窗体被移动之外, 所有控件排除在处理之外。

在所有列举的情况下, 控件需要发送消息, 且必须在函数库核心中接收并处理。核心将会处理两个事件标识符, 来判断控件的可用性 (ON_SET_AVAILABLE), 并生成一个控件数组 (ON_CHANGE_GUI)。所有事件标识符都位于 Define.mqh 文件内:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                          版权所有 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
...
#define ON_CHANGE_GUI               (28) // 图形界面已更改
#define ON_SET_AVAILABLE            (39) // 设置可用项
...

使用 Show() 和 Hide() 方法隐藏或显示控件。为了设置可用性, 已将新的属性添加到 CElement 类中。使用虚拟公用方法 CElement::IsAvailable() 设置它的值。此处, 与定义控件状态的其它方法类似, 也要为设置嵌套控件传递数值。相对于传递的状态设置鼠标左键单击的优先级。如果控件不可用, 优先级将被重置

class CElement : public CElementBase
  {
protected:
   bool              m_is_available;   // 可用
   //---
public:
   //--- 可用控件标志
   virtual void      IsAvailable(const bool state)                   { m_is_available=state;                 }
   bool              IsAvailable(void)                         const { return(m_is_available);               }
  };
//+------------------------------------------------------------------+
//| 控件可用性                                                        |
//+------------------------------------------------------------------+
void CElement::IsAvailable(const bool state)
  {
//--- 若已设置, 离开
   if(state==CElementBase::IsAvailable())
      return;
//--- 设置
   CElementBase::IsAvailable(state);
//--- 其它控件
   int elements_total=ElementsTotal();
   for(int i=0; i<elements_total; i++)
      m_elements[i].IsAvailable(state);
//--- 设置鼠标左键点击的优先级
   if(state)
      SetZorders();
   else
      ResetZorders();
  }

举例说明, 这是 CComboBox::ChangeComboBoxListState() 方法的代码, 它决定了组合框控件中下拉列表的可见性。 

如果按下组合框按钮且需要显示列表视图, 则在显示列表视图后立即发送含有 ON_SET_AVAILABLE 标识符的事件。作为附加参数, 将会传递 (1) 控件的标识符和 (2) 事件处理程序所需操作的标志: 恢复所有可见控件仅令事件中指定了标识符的控件可用。标志为 1 的恢复, 而值为 0 的特殊控件设置可用性。 

含有 ON_SET_AVAILABLE 标识符的消息后面跟一条含有 ON_CHANGE_GUI 事件标识符的消息。处理它时将涉及生成当前可用控件的数组。

//+------------------------------------------------------------------+
//| 将组合框的当前状态改变为相反                                       |
//+------------------------------------------------------------------+
void CComboBox::ChangeComboBoxListState(void)
  {
//--- 如果按下按钮
   if(m_button.IsPressed())
     {
      //--- 显示列表视图
      m_listview.Show();
      //--- 发送消息来检测可用控件
      ::EventChartCustom(m_chart_id,ON_SET_AVAILABLE,CElementBase::Id(),0,"");
      //--- 发送有关图形界面变化的消息
      ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0,"");
     }
   else
     {
      //--- 隐藏列表视图
      m_listview.Hide();
      //--- 发送消息来恢复控件
      ::EventChartCustom(m_chart_id,ON_SET_AVAILABLE,CElementBase::Id(),1,"");
      //--- 发送有关图形界面变化的消息
      ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0,"");
     }
  }

但是对于 Tabs 控件, 例如, 仅发送上述事件之一 ON_CHANGE_GUI 标识符用于处理就足够了。无需令某些控件可用。切换选项卡时, 将分配到选项卡组的控件设置可见性状态。在 CTabs 类中, 控件组的可见性由新版函数库中修订的 CTabs::ShowTabElements() 方法进行管理。也许有时需要在选项卡中放置一组选项卡。所以, 即使显示所选选项卡的控件之一为 CTabs 的类型, 则 CTabs::ShowTabElements() 方法也将立即在此控件中调用。这种方法允许在任何嵌套层次上放置选项卡。

//+------------------------------------------------------------------+
//| 仅显示所选选项卡的控件                                            |
//+------------------------------------------------------------------+
void CTabs::ShowTabElements(void)
  {
//--- 如果选项卡被隐藏, 离开
   if(!CElementBase::IsVisible())
      return;
//--- 检查所选选项卡的索引
   CheckTabIndex();
//---
   uint tabs_total=TabsTotal();
   for(uint i=0; i<tabs_total; i++)
     {
      //--- 获取挂载到选项卡的控件数量
      int tab_elements_total=::ArraySize(m_tab[i].elements);
      //--- 如果选择此选项卡
      if(i==m_selected_tab)
        {
         //--- 显示选项卡的控件
         for(int j=0; j<tab_elements_total; j++)
           {
            //--- 显示控件
            CElement *el=m_tab[i].elements[j];
            el.Reset();
            //--- 如果这是 Tabs 控件, 则显示打开的控件
            CTabs *tb=dynamic_cast<CTabs*>(el);
            if(tb!=NULL)
               tb.ShowTabElements();
           }
        }
      //--- 隐藏非活动选项卡的控件
      else
        {
         for(int j=0; j<tab_elements_total; j++)
            m_tab[i].elements[j].Hide();
        }
     }
//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_CLICK_TAB,CElementBase::Id(),m_selected_tab,"");
  }

一旦 显示所选选项卡的控件, 该方法将发送一条消息, 指出图形界面已更改, 并且需要生成可用于处理的控件数组。

//+------------------------------------------------------------------+
//| 按下组中的选项卡                                                  |
//+------------------------------------------------------------------+
bool CTabs::OnClickTab(const int id,const int index)
  {
//--- 如果 (1) 标识符不匹配或 (2) 控件被锁定, 离开
   if(id!=CElementBase::Id() || CElementBase::IsLocked())
      return(false);
//--- 如果索引不匹配, 离开
   if(index!=m_tabs.SelectedButtonIndex())
      return(true);
//--- 恢复所选选项卡的索引
   SelectedTab(index);
//--- 重绘控件
   Reset();
   Update(true);
//--- 仅显示所选选项卡的控件
   ShowTabElements();
//--- 发送有关图形界面变化的消息
   ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0.0,"");
   return(true);
  }

事件产生的两个新标识符已添加到 Defines.mqh 文件中。

  • ON_MOUSE_FOCUS — 鼠标光标进入控件的区域;
  • ON_MOUSE_BLUR — 鼠标光标离开控件的区域。

...
#define ON_MOUSE_BLUR               (34) // 鼠标光标离开控件的区域
#define ON_MOUSE_FOCUS              (35) // 鼠标光标进入控件的区域
...

仅当跨越控件的边界时才会生成这些事件。控件基类 (CElementBase) 含有 CElementBase::CheckCrossingBorder() 方法, 检测鼠标光标跨越控件区域边界的时刻。我们来补充上述事件的产生:

//+------------------------------------------------------------------+
//| 检测控件边界的跨越点                                              |
//+------------------------------------------------------------------+
bool CElementBase::CheckCrossingBorder(void)
  {
//--- 如果此刻跨越控件边界
   if((MouseFocus() && !IsMouseFocus()) || (!MouseFocus() && IsMouseFocus()))
     {
      IsMouseFocus(MouseFocus());
      //--- 关于跨入控件的消息
      if(MouseFocus())
         ::EventChartCustom(m_chart_id,ON_MOUSE_FOCUS,m_id,m_index,m_class_name);
      //--- 关于跨出控件的消息
      else
         ::EventChartCustom(m_chart_id,ON_MOUSE_BLUR,m_id,m_index,m_class_name);
      //---
      return(true);
     }
//---
   return(false);
  }

在当前版本的函数库中, 这些事件仅在主菜单 (CMenuBar) 中处理。让我们看看它是如何工作的。

一旦主菜单创建并存储后, 其项目 (CMenuItem) 作为单独的控件落入存储列表。CMenuItem 类衍生自 CButton (按钮控件)。所以, 调用菜单项的事件处理器伊始, 会先调用 CButton 基类的事件处理器。

//+------------------------------------------------------------------+
//| 事件处理器                                                        |
//+------------------------------------------------------------------+
void CMenuItem::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 基类当中的事件处理器
   CButton::OnEvent(id,lparam,dparam,sparam);
...
  }

基本事件处理程序已包含跟踪按钮跨越, 它不需要在 CMenuItem 派生类中覆盖。

//+------------------------------------------------------------------+
//| 事件处理器                                                        |
//+------------------------------------------------------------------+
void CButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 处理鼠标移动事件
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- 如果跨越边界, 重绘控件
      if(CheckCrossingBorder())
         Update(true);
      //---
      return;
     }
...
  }

如果光标跨越按钮区域内的边框, 则会生成含有 ON_MOUSE_FOCUS 标识符的事件。现在, 当主菜单控件被激活时, CMenuBar 类的事件处理程序使用这个非常事件 切换上下文菜单。 

//+------------------------------------------------------------------+
//| 事件处理器                                                        |
//+------------------------------------------------------------------+
void CMenuBar::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 处理菜单项变更的焦点事件
   if(id==CHARTEVENT_CUSTOM+ON_MOUSE_FOCUS)
     {
      //--- 如果 (1) 主菜单尚未激活或 (2) 标识符不匹配, 离开
      if(!m_menubar_state || lparam!=CElementBase::Id())
         return;
      //--- 依据主菜单的激活项切换上下文菜单
      SwitchContextMenuByFocus();
      return;
     }
...
  }


优化函数库核心

我们来研究针对 CWndContainerCWndEvents 类所做的更改, 修正和添加, 可以将其称为函数库的核心。毕竟, 它们调配对其所有控件的访问, 并处理由图形界面的控件生成的事件流。

一个模板方法 CWndContainer::ResizeArray() 已添加到 CWndContainer 类中, 用于处理数组。传递给此方法的任何类型的数组将会逐一增加元素, 该方法将返回最后一个元素的索引。

//+------------------------------------------------------------------+
//| 用于存储所有接口对象的类                                           |
//+------------------------------------------------------------------+
class CWndContainer
  {
private:
   //--- 数组增加一个元素并返回最后的索引
   template<typename T>
   int               ResizeArray(T &array[]);
  };
//+------------------------------------------------------------------+
//| 数组增加一个元素并返回最后的索引                                   |
//+------------------------------------------------------------------+
template<typename T>
int CWndContainer::ResizeArray(T &array[])
  {
   int size=::ArraySize(array);
   ::ArrayResize(array,size+1,RESERVE_SIZE_ARRAY);
   return(size);
  }

我来提醒您一下, 在 WindowElements 结构中已经为 CWndContainer 类 (存储指向图形界面所有控件的指针) 的众多控件声明了私有数组。为了从该列表中获得某种类型的控件数量, 已实现了通用的 CWndContainer::ElementsTotal() 方法。为其传递窗口的索引和控件的类型, 以便在 MQL 应用程序的图形界面中获取它们的数量。新的 ENUM_ELEMENT_TYPE 枚举已添加到 Enums.mqh 文件中, 用来指定控件的类型:

//+------------------------------------------------------------------+
//| 控制类型的枚举                                                    |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_TYPE
  {
   E_CONTEXT_MENU    =0,
   E_COMBO_BOX       =1,
   E_SPLIT_BUTTON    =2,
   E_MENU_BAR        =3,
   E_MENU_ITEM       =4,
   E_DROP_LIST       =5,
   E_SCROLL          =6,
   E_TABLE           =7,
   E_TABS            =8,
   E_SLIDER          =9,
   E_CALENDAR        =10,
   E_DROP_CALENDAR   =11,
   E_SUB_CHART       =12,
   E_PICTURES_SLIDER =13,
   E_TIME_EDIT       =14,
   E_TEXT_BOX        =15,
   E_TREE_VIEW       =16,
   E_FILE_NAVIGATOR  =17,
   E_TOOLTIP         =18
  };

CWndContainer::ElementsTotal() 方法的代码如下列出:

//+------------------------------------------------------------------+
//| 指定索引的窗口内指定类型控件的数量                                 |
//+------------------------------------------------------------------+
int CWndContainer::ElementsTotal(const int window_index,const ENUM_ELEMENT_TYPE type)
  {
//--- 检查数组超界
   int index=CheckOutOfRange(window_index);
   if(index==WRONG_VALUE)
      return(WRONG_VALUE);
//---
   int elements_total=0;
//---
   switch(type)
     {
      case E_CONTEXT_MENU    : elements_total=::ArraySize(m_wnd[index].m_context_menus);   break;
      case E_COMBO_BOX       : elements_total=::ArraySize(m_wnd[index].m_combo_boxes);     break;
      case E_SPLIT_BUTTON    : elements_total=::ArraySize(m_wnd[index].m_split_buttons);   break;
      case E_MENU_BAR        : elements_total=::ArraySize(m_wnd[index].m_menu_bars);       break;
      case E_MENU_ITEM       : elements_total=::ArraySize(m_wnd[index].m_menu_items);      break;
      case E_DROP_LIST       : elements_total=::ArraySize(m_wnd[index].m_drop_lists);      break;
      case E_SCROLL          : elements_total=::ArraySize(m_wnd[index].m_scrolls);         break;
      case E_TABLE           : elements_total=::ArraySize(m_wnd[index].m_tables);          break;
      case E_TABS            : elements_total=::ArraySize(m_wnd[index].m_tabs);            break;
      case E_SLIDER          : elements_total=::ArraySize(m_wnd[index].m_sliders);         break;
      case E_CALENDAR        : elements_total=::ArraySize(m_wnd[index].m_calendars);       break;
      case E_DROP_CALENDAR   : elements_total=::ArraySize(m_wnd[index].m_drop_calendars);  break;
      case E_SUB_CHART       : elements_total=::ArraySize(m_wnd[index].m_sub_charts);      break;
      case E_PICTURES_SLIDER : elements_total=::ArraySize(m_wnd[index].m_pictures_slider); break;
      case E_TIME_EDIT       : elements_total=::ArraySize(m_wnd[index].m_time_edits);      break;
      case E_TEXT_BOX        : elements_total=::ArraySize(m_wnd[index].m_text_boxes);      break;
      case E_TREE_VIEW       : elements_total=::ArraySize(m_wnd[index].m_treeview_lists);  break;
      case E_FILE_NAVIGATOR  : elements_total=::ArraySize(m_wnd[index].m_file_navigators); break;
      case E_TOOLTIP         : elements_total=::ArraySize(m_wnd[index].m_tooltips);        break;
     }
//--- 返回指定类型的控件数量
   return(elements_total);
  }

为了降低 CPU 的负担, 有必要在 WindowElements 结构中添加更多的数组, 它将存储指向以下类别控件的指针。

  • 主控件数组
  • 带定时器的控件数组
  • 可见并可用于处理的控件数组
  • 沿 X 轴可自动调整大小的控件数组
  • 沿 Y 轴可自动调整大小的控件数组
class CWndContainer
  {
protected:
...
   //--- 控件数组的结构
   struct WindowElements
     {
      ...
      //--- 主要控件的数组
      CElement         *m_main_elements[];
      //--- 定时器控件
      CElement         *m_timer_elements[];
      //--- 当前可见和可用的控件
      CElement         *m_available_elements[];
      //--- 沿 X 轴自动调整大小的控件
      CElement         *m_auto_x_resize_elements[];
      //--- 沿 Y 轴自动调整大小的控件
      CElement         *m_auto_y_resize_elements[];
      ...
     };
   //--- 每个窗口的控件数组
   WindowElements    m_wnd[];
...
  };

通过相应的方法获得这些数组的大小:

class CWndContainer
  {
public:
   //--- 主要控件的数量
   int               MainElementsTotal(const int window_index);
   //--- 带定时器的控件数量
   int               TimerElementsTotal(const int window_index);
   //--- 沿 X 轴自动调整大小的控件数量
   int               AutoXResizeElementsTotal(const int window_index);
   //--- 沿 Y 轴自动调整大小的控件数量
   int               AutoYResizeElementsTotal(const int window_index);
   //--- 当前可用控件的数量
   int               AvailableElementsTotal(const int window_index);
  };

CWndContainer::AddToElementsArray() 方法将指针添加到主控件的数组。缩减版本的方法:

//+------------------------------------------------------------------+
//| 添加指向控件数组的指针                                             |
//+------------------------------------------------------------------+
void CWndContainer::AddToElementsArray(const int window_index,CElementBase &object)
  {
...
//--- 添加到主控件的数组
   last_index=ResizeArray(m_wnd[window_index].m_main_elements);
   m_wnd[window_index].m_main_elements[last_index]=::GetPointer(object);
...
  }

其它类别的数组在 CWndEvents 类中生成 (见下文)。在它们当中使用单独的方法来添加指针。

class CWndContainer
  {
protected:
   //--- 添加带计时器的控件数组指针
   void              AddTimerElement(const int window_index,CElement &object);
   //--- 添加沿 X 轴自动调整大小的控件数组指针
   void              AddAutoXResizeElement(const int window_index,CElement &object);
   //--- 添加沿 Y 轴自动调整大小的控件数组指针
   void              AddAutoYResizeElement(const int window_index,CElement &object);
   //--- 添加指向当前可用控件数组指针
   void              AddAvailableElement(const int window_index,CElement &object);
  };
//+------------------------------------------------------------------+
//| A添加带计时器的控件数组指针                                        |
//+------------------------------------------------------------------+
void CWndContainer::AddTimerElement(const int window_index,CElement &object)
  {
   int last_index=ResizeArray(m_wnd[window_index].m_timer_elements);
   m_wnd[window_index].m_timer_elements[last_index]=::GetPointer(object);
  }
//+------------------------------------------------------------------+
//| 添加自动调整大小的控件数组指针 (X)                                 |
//+------------------------------------------------------------------+
void CWndContainer::AddAutoXResizeElement(const int window_index,CElement &object)
  {
   int last_index=ResizeArray(m_wnd[window_index].m_auto_x_resize_elements);
   m_wnd[window_index].m_auto_x_resize_elements[last_index]=::GetPointer(object);
  }
//+------------------------------------------------------------------+
//| 添加自动调整大小的控件数组指针 (Y)                                 |
//+------------------------------------------------------------------+
void CWndContainer::AddAutoYResizeElement(const int window_index,CElement &object)
  {
   int last_index=ResizeArray(m_wnd[window_index].m_auto_y_resize_elements);
   m_wnd[window_index].m_auto_y_resize_elements[last_index]=::GetPointer(object);
  }
//+------------------------------------------------------------------+
//| 添加指向当前可用控件数组指针                                       |
//+------------------------------------------------------------------+
void CWndContainer::AddAvailableElement(const int window_index,CElement &object)
  {
   int last_index=ResizeArray(m_wnd[window_index].m_available_elements);
   m_wnd[window_index].m_available_elements[last_index]=::GetPointer(object);
  }

CWndEvents 类中也有内部使用的新方法。因此, 隐藏图形界面的所有控件都需要 CWndEvents::Hide() 方法。它使用双循环: 首先隐藏 窗体, 然后第二个循环隐藏挂载到窗体的控件。请注意, 在这个方法中, 第二次循环遍历由 主控件 指针组成的控件数组。Hide() 和 Show() 控件方法现在的这种安置方式, 可以在整个嵌套深度中影响嵌套控件链中的这些方法。

//+------------------------------------------------------------------+
//| 事件处理类                                                        |
//+------------------------------------------------------------------+
class CWndEvents : public CWndContainer
  {
protected:
   //--- 隐藏所有控件
   void              Hide();
  };
//+------------------------------------------------------------------+
//| 隐藏控件                                                          |
//+------------------------------------------------------------------+
void CWndEvents::Hide(void)
  {
   int windows_total=CWndContainer::WindowsTotal();
   for(int w=0; w<windows_total; w++)
     {
      m_windows[w].Hide();
      int main_total=MainElementsTotal(w);
      for(int e=0; e<main_total; e++)
        {
         CElement *el=m_wnd[w].m_main_elements[e];
         el.Hide();
        }
     }
  }

还有一个新的 CWndEvents::Show() 方法来显示指定格式的控件。在参数中指定的窗口 首先被显示。而后, 如果窗口没有最小化, 则挂载到此窗体的所有控件都可见。此循环会跳过的控件 (1) 下拉菜单或 (2) 设计为主控件的 Tabs 控件。选项卡中的控件稍后在循环外部 使用 CWndEvents::ShowTabElements() 方法显示

class CWndEvents : public CWndContainer
  {
protected:
   //--- 显示指定窗口的控件
   void              Show(const uint window_index);
  };
//+------------------------------------------------------------------+
//| 显示指定窗口的控件                                                |
//+------------------------------------------------------------------+
void CWndEvents::Show(const uint window_index)
  {
//--- 显示指定窗口的控件
   m_windows[window_index].Show();
//--- 如果窗口没有最小化
   if(!m_windows[window_index].IsMinimized())
     {
      int main_total=MainElementsTotal(window_index);
      for(int e=0; e<main_total; e++)
        {
         CElement *el=m_wnd[window_index].m_main_elements[e];
         //--- 显示控件, 如果 (1) 不是下拉列表, 和 (2) 其主控件不是选项卡
         if(!el.IsDropdown() && dynamic_cast<CTabs*>(el.MainPointer())==NULL)
            el.Show();
        }
      //--- 仅显示所选选项卡的控件
      ShowTabElements(window_index);
     }
  }

将需要调用 CWndEvents::Update() 方法重新绘制 MQL 应用程序图形界面的所有控件。该方法可以在两种模式下工作: (1) 全部重绘所有控件, 或 (2) 应用以前进行的更改。若要全部重绘和更新图形界面, 需要传递 true 值。

class CWndEvents : public CWndContainer
  {
protected:
   //--- 重绘控件
   void              Update(const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| 重绘控件                                                          |
//+------------------------------------------------------------------+
void CWndEvents::Update(const bool redraw=false)
  {
   int windows_total=CWndContainer::WindowsTotal();
   for(int w=0; w<windows_total; w++)
     {
      //--- 重绘控件
      int elements_total=CWndContainer::ElementsTotal(w);
      for(int e=0; e<elements_total; e++)
        {
         CElement *el=m_wnd[w].m_elements[e];
         el.Update(redraw);
        }
     }
  }

稍后我们会讨论这些方法。现在, 我们来研究为上述类别生成数组的一些方法。

以前的版本有一个功能, 当鼠标光标悬停在它们上面时, 控件颜色基于计时器的渐变。为了减少体量并降低资源消耗, 这个多余的功能已被删除。所以, 当前版本函数库的所有控件不使用定时器。它仅存在于 (1) 滚动条滑块的快速滚动中, (2) 轮转编辑框中的值和 (3) 日历中的日期。因此, 只有恰当的控件才会添加到 CWndEvents::FormTimerElementsArray() 方法的相应数组中 (见下面的代码清单)。 

由于指向控件的指针存储在类型为控件基类 (CElement) 的数组中, 所以在此使用 动态类型转换(dynamic_cast ), 以及用于检测控件派生类型的许多其它类方法。 

class CWndEvents : public CWndContainer
  {
protected:
   //--- 生成带有计时器的控件数组
   void              FormTimerElementsArray(void);
  };
//+------------------------------------------------------------------+
//| 生成带有计时器的控件数组                                           |
//+------------------------------------------------------------------+
void CWndEvents::FormTimerElementsArray(void)
  {
   int windows_total=CWndContainer::WindowsTotal();
   for(int w=0; w<windows_total; w++)
     {
      int elements_total=CWndContainer::ElementsTotal(w);
      for(int e=0; e<elements_total; e++)
        {
         CElement *el=m_wnd[w].m_elements[e];
         //---
         if(dynamic_cast<CCalendar    *>(el)!=NULL ||
            dynamic_cast<CColorPicker *>(el)!=NULL ||
            dynamic_cast<CListView    *>(el)!=NULL ||
            dynamic_cast<CTable       *>(el)!=NULL ||
            dynamic_cast<CTextBox     *>(el)!=NULL ||
            dynamic_cast<CTextEdit    *>(el)!=NULL ||
            dynamic_cast<CTreeView    *>(el)!=NULL)
           {
            CWndContainer::AddTimerElement(w,el);
           }
        }
     }
  }


现在, 定时器变得更加简单: 它不再需要检查整个控件列表, 仅有包含此函数的那些:

//+------------------------------------------------------------------+
//| 通过定时器检查所有控件的事件                                       |
//+------------------------------------------------------------------+
void CWndEvents::CheckElementsEventsTimer(void)
  {
   int awi=m_active_window_index;
   int timer_elements_total=CWndContainer::TimerElementsTotal(awi);
   for(int e=0; e<timer_elements_total; e++)
     {
      CElement *el=m_wnd[awi].m_timer_elements[e];
      if(el.IsVisible())
         el.OnEventTimer();
     }
  }

仅有一些图形界面的控件需要处理鼠标悬浮事件。排除这种事件处理的可用控件数组: 

  • CButtonsGroup — 按钮组;
  • CFileNavigator — 文件导航器;
  • CLineGraph — 线形图表;
  • CPicture — 图片;
  • CPicturesSlider — 图片滑块;
  • CProgressBar — 进度条;
  • CSeparateLine — 分隔线;
  • CStatusBar — 状态栏;
  • CTabs — 选项卡;
  • CTextLabel — 文字标签。

当鼠标悬浮时, 所有这些控件都不会高亮显示。不过, 它们当中含有嵌套控件的那些会高亮显示。但由于循环中通用数组来形成可用控件数组, 嵌套控件也将参与选择。数组将会捡取所有 可见, 可用以及未锁定的 控件。 

class CWndEvents : public CWndContainer
  {
protected:
   //--- 生成可用控件的数组
   void              FormAvailableElementsArray(void);
  };
//+------------------------------------------------------------------+
//| 生成可用控件的数组                                                |
//+------------------------------------------------------------------+
void CWndEvents::FormAvailableElementsArray(void)
  {
//--- 窗口索引
   int awi=m_active_window_index;
//--- 控件总数
   int elements_total=CWndContainer::ElementsTotal(awi);
//--- 清除数组
   ::ArrayFree(m_wnd[awi].m_available_elements);
//---
   for(int e=0; e<elements_total; e++)
     {
      CElement *el=m_wnd[awi].m_elements[e];
      //--- 只添加可见和可用于处理的控件
      if(!el.IsVisible() || !el.IsAvailable() || el.IsLocked())
         continue;
      //--- 排除不需要处理鼠标悬停事件的控件
      if(dynamic_cast<CButtonsGroup   *>(el)==NULL &&
         dynamic_cast<CFileNavigator  *>(el)==NULL &&
         dynamic_cast<CLineGraph      *>(el)==NULL &&
         dynamic_cast<CPicture        *>(el)==NULL &&
         dynamic_cast<CPicturesSlider *>(el)==NULL &&
         dynamic_cast<CProgressBar    *>(el)==NULL &&
         dynamic_cast<CSeparateLine   *>(el)==NULL &&
         dynamic_cast<CStatusBar      *>(el)==NULL &&
         dynamic_cast<CTabs           *>(el)==NULL &&
         dynamic_cast<CTextLabel      *>(el)==NULL)
        {
         AddAvailableElement(awi,el);
        }
     }
  }

其余要研究的是 CWndEvents::FormAutoXResizeElementsArray() 和 CWndEvents::FormAutoYResizeElementsArray() 方法, 它们生成启用了自动调整大小模式的控件的数组指针。此类控件遵循所附主控件的大小。并非所有控件都含有自动调整大小的方法代码。此处是含有代码的:

CElement::ChangeWidthByRightWindowSide() 虚拟方法中定义了自动调整宽度代码的控件:

  • CButton — 按钮。
  • CFileNavigator — 文件导航器。
  • CLineGraph —线形图表。
  • CListView — 列表视图。
  • CMenuBar — 主菜单。
  • CProgressBar — 进度条。
  • CStandardChart — 标准图表。
  • CStatusBar — 状态栏。
  • CTable — 表格。
  • CTabs — 选项卡。
  • CTextBox — 文本编辑框。
  • CTextEdit — 编辑框。
  • CTreeView — 树形视图。

CElement::ChangeHeightByBottomWindowSide() 虚拟方法中定义了自动调整高度代码的控件:

  • CLineGraph —线形图表。
  • CListView — 列表视图。
  • CStandardChart — 标准图表。
  • CTable — 表格。
  • CTabs — 选项卡。
  • CTextBox — 文本编辑框。

当创建这些类别的数组时, 检查这些控件中是否启用了自动调整大小模式; 如果启用, 控件将添加到数组中。这些代码无需研讨: 上面已经讨论过类似的方法。 

现在我们来看看上面所列类别的数组何时生成。在创建图形界面 (用户自行创建) 的主要方法中, 一旦所有指定的控件成功创建, 就必须调用唯一的方法 CWndEvents::CompletedGUI() 将它们显示在图表上。它表明程序已完成了 MQL 应用程序的图形界面的创建。 

我们来研究 CWndEvents::CompletedGUI() 方法的细节。它调用本节前面描述的所有方法。最初, 图形界面的所有控件都是隐藏的。它们当中尚无一个进行渲染。所以, 为了避免它们逐一出现, 需要在渲染之前隐藏它们。接下来, 进行渲染本身, 并将最后的变更应用于每个控件。之后, 只需显示主窗口的控件。然后按类别生成控件指针数组。在方法末尾, 更新图表。 

class CWndEvents : public CWndContainer
  {
protected:
   //--- 完成图形界面的创建
   void              CompletedGUI(void);
  };
//+------------------------------------------------------------------+
//| 完成图形界面的创建                                                |
//+------------------------------------------------------------------+
void CWndEvents::CompletedGUI(void)
  {
//--- 如果尚无窗口, 离开
   int windows_total=CWndContainer::WindowsTotal();
   if(windows_total<1)
      return;
//--- 显示通知用户的评论
   ::Comment("Update. 请等待...");
//--- 隐藏控件
   Hide();
//--- 绘制控件
   Update(true);
//--- 显示激活窗口的控件
   Show(m_active_window_index);
//--- 生成带有计时器控件的数组
   FormTimerElementsArray();
//--- 生成可见同时可用控件的数组
   FormAvailableElementsArray();
//--- 生成自动调整大小控件的数组
   FormAutoXResizeElementsArray();
   FormAutoYResizeElementsArray();
//--- 重绘图表
   m_chart.Redraw();
//--- 清除注释
   ::Comment("");
  }

检查并处理控件事件的 CWndEvents::CheckElementsEvents() 方法已被大大修改。我们来更详细地介绍一下。 

此方法现在有两个事件处理模块。一个模块专门用来处理鼠标光标移动 (CHARTEVENT_MOUSE_MOVE)。如前所述, 代替循环遍历激活窗口的所有控件列表, 现在循环内只能遍历可用于处理的控件。这就是为什么首先生成 含有指向可用控件 数组的原因。大型 MQL 应用程序的图形界面可能含有数百甚至数千个控件, 列表中只有少数同时可见和可用。这种方法极大地节省了 CPU 资源。 

另一个修改是现在检查 (1) 子窗口的位置 和 (2) 正在外部循环进行的 聚焦控件, 以及每一个不在处理程序中的控件。因此, 与每个控件相关的检查现在位于相同的位置。若将来需要更改事件处理算法, 这会很方便。

所有其它类型的事件都在单独的模块中处理。当前版本要遍历图形界面的整个控件列表。先前位于控件类中的所有检查也已被移至外部循环。 

在方法的末尾, 事件被发送至 MQL 应用程序的自定义类。

//+------------------------------------------------------------------+
//| 检查控件事件                                                      |
//+------------------------------------------------------------------+
void CWndEvents::CheckElementsEvents(void)
  {
//--- 处理移动鼠标光标的事件
   if(m_id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- 如果窗体位于图表的另一个子窗口中, 离开
      if(!m_windows[m_active_window_index].CheckSubwindowNumber())
         return;
      //--- 只检查可用的控件
      int available_elements_total=CWndContainer::AvailableElementsTotal(m_active_window_index);
      for(int e=0; e<available_elements_total; e++)
        {
         CElement *el=m_wnd[m_active_window_index].m_available_elements[e];
         //--- 检查聚焦控件
         el.CheckMouseFocus();
         //--- 处理事件
         el.OnEvent(m_id,m_lparam,m_dparam,m_sparam);
        }
     }
//--- 除鼠标光标移动外的所有事件
   else
     {
      int elements_total=CWndContainer::ElementsTotal(m_active_window_index);
      for(int e=0; e<elements_total; e++)
        {
         //--- 只检查可用的控件
         CElement *el=m_wnd[m_active_window_index].m_elements[e];
         if(!el.IsVisible() || !el.IsAvailable() || el.IsLocked())
            continue;
         //--- 在控件的事件处理程序中处理事件
         el.OnEvent(m_id,m_lparam,m_dparam,m_sparam);
        }
     }
//--- 将事件转发到应用程序文件
   OnEvent(m_id,m_lparam,m_dparam,m_sparam);
  }

用于生成同时可见并可处理控件数组的 CWndEvents::FormAvailableElementsArray() 方法在以下情况下调用:

  • 打开一个对话框。一旦打开一个对话框, 就会生成 ON_OPEN_DIALOG_BOX 事件, 它在 CWndEvents::OnOpenDialogBox() 方法中处理。处理此事件后, 必须为打开的窗口生成可用控件数组。

  • 图形界面的变化。任何由交互引起的图形界面变化都会生成 ON_CHANGE_GUI 事件。它由新的 私有 方法 CWndEvents::OnChangeGUI() 处理。此处, 当 ON_CHANGE_GUI 时间抵达, 首先 生成可用控件数组。然后 将所有工具提示移至顶层。在方法结束时, 重新绘制图表以显示最新的变更。

class CWndEvents : public CWndContainer
  {
private:
   //--- 在图形界面中的变化
   bool              OnChangeGUI(void);
  };
//+------------------------------------------------------------------+
//| 图形界面发生变化的事件                                             |
//+------------------------------------------------------------------+
bool CWndEvents::OnChangeGUI(void)
  {
//--- 如果信号是有关图形界面的变化
   if(m_id!=CHARTEVENT_CUSTOM+ON_CHANGE_GUI)
      return(false);
//--- 生成可见同时可用控件的数组
   FormAvailableElementsArray();
//--- 将工具提示移到顶层
   ResetTooltips();
//--- 重绘图表
   m_chart.Redraw();
   return(true);
  }

接下来, 考虑使用 ON_SET_AVAILABLE 标识符处理事件, 以判断可用于处理的控件。 

执行 CWndEvents::OnSetAvailable() 方法来处理 ON_SET_AVAILABLE 事件。但在处理其代码的描述之前, 有必要考虑一些辅助方法。这里有 10 种图形界面控件可生成这种标识符事件。它们都有判断自身激活状态的方法。我们来列出它们的名称:

  • 主菜单 — CMenuBar::State()。
  • 菜单项 — CMenuItem::GetContextMenuPointer().IsVisible()。
  • 拆分按钮 — CSplitButton::GetContextMenuPointer().IsVisible()。
  • 组合框 — CComboBox::GetListViewPointer().IsVisible()
  • 下拉式日历 — DropCalendar::GetCalendarPointer().IsVisible()。
  • 滚动条 — CScroll::State()。
  • 表格 — CTable::ColumnResizeControl()。
  • 数字滑块 — CSlider::State()。
  • 树形视图 — CTreeView::GetMousePointer().State()。
  • 标准图表 — CStandartChart::GetMousePointer().IsVisible()。

这些控件中的每一个都在 CWndContainer 类中含有私有数组。CWndEvents 类实现了判断哪个控件当前处于激活的方法。所有这些方法返回其私有数组中激活控件的索引。

class CWndEvents : public CWndContainer
  {
private:
   //--- 返回激活主菜单的索引
   int               ActivatedMenuBarIndex(void);
   //--- 返回激活菜单项的索引
   int               ActivatedMenuItemIndex(void);
   //--- 返回激活的拆分按钮索引
   int               ActivatedSplitButtonIndex(void);
   //--- 返回激活的组合框索引
   int               ActivatedComboBoxIndex(void);
   //--- 返回激活的下拉式日历索引
   int               ActivatedDropCalendarIndex(void);
   //--- 返回激活的滚动条索引
   int               ActivatedScrollIndex(void);
   //--- 返回激活的表格索引
   int               ActivatedTableIndex(void);
   //--- 返回激活的滑块索引
   int               ActivatedSliderIndex(void);
   //--- 返回激活的树形视图索引
   int               ActivatedTreeViewIndex(void);
   //--- 返回激活的子图表索引
   int               ActivatedSubChartIndex(void);
  };

由于大多数这些方法的唯一不同在于判断控件状态的条件, 所以只需研究其中之一的代码。下面的列表显示了 CWndEvents::ActivatedTreeViewIndex() 方法的代码, 该方法返回激活的树形视图的索引。如果此类型的控件已启用选项卡模式, 则该检查将被拒绝

//+------------------------------------------------------------------+
//| 返回激活的树形视图索引                                             |
//+------------------------------------------------------------------+
int CWndEvents::ActivatedTreeViewIndex(void)
  {
   int index=WRONG_VALUE;
//---
   int total=ElementsTotal(m_active_window_index,E_TREE_VIEW);
   for(int i=0; i<total; i++)
     {
      CTreeView *el=m_wnd[m_active_window_index].m_treeview_lists[i];
      //--- 如果选项卡模式已启用, 转到下一个
      if(el.TabItemsMode())
         continue;
      //--- 如果是在更改列表宽度的过程中 
      if(el.GetMousePointer().State())
        {
         index=i;
         break;
        }
     }
   return(index);
  }

CWndEvents::SetAvailable() 方法旨在设置控件的可用性状态。作为参数, 有必要传递 (1) 所需的窗体索引和 (2) 为控件设置的状态。 

如果需要令所有控件不可用, 只需在一个循环中迭代它们并设置 false 值。 

如果需要令控件可用, 那么对于树形视图, 则调用同名的重载方法 CTreeView::IsAvailable(), 其中包含两种模式设置状态: (1) 仅适用于主控件和 (2) 整个嵌套深度的所有控件。所以, 动态类型转换在这里用于 获取指向派生控件类的控件指针。 

class CWndEvents : public CWndContainer
  {
protected:
   //--- 设置控件的可用性状态
   void              SetAvailable(const uint window_index,const bool state);
  };
//+------------------------------------------------------------------+
//| 设置控件的可用性状态                                               |
//+------------------------------------------------------------------+
void CWndEvents::SetAvailable(const uint window_index,const bool state)
  {
//--- 获取主控件的数量
   int main_total=MainElementsTotal(window_index);
//--- 如有必要令控件不可用
   if(!state)
     {
      m_windows[window_index].IsAvailable(state);
      for(int e=0; e<main_total; e++)
        {
         CElement *el=m_wnd[window_index].m_main_elements[e];
         el.IsAvailable(state);
        }
     }
   else
     {
      m_windows[window_index].IsAvailable(state);
      for(int e=0; e<main_total; e++)
        {
         CElement *el=m_wnd[window_index].m_main_elements[e];
         //--- 如果是树形视图
         if(dynamic_cast<CTreeView*>(el)!=NULL)
           {
            CTreeView *tv=dynamic_cast<CTreeView*>(el);
            tv.IsAvailable(true);
            continue;
           }
         //--- 如果是文件导航器
         if(dynamic_cast<CFileNavigator*>(el)!=NULL)
           {
            CFileNavigator *fn =dynamic_cast<CFileNavigator*>(el);
            CTreeView      *tv =fn.GetTreeViewPointer();
            fn.IsAvailable(state);
            tv.IsAvailable(state);
            continue;
           }
         //--- 令控件可用
         el.IsAvailable(state);
        }
     }
  }

挂载上下文菜单的菜单项需要一种方法, 能够循环遍历所打开的上下文菜单的整个深度, 并访问它们。在这种情况下, 需要使上下文菜单可用于处理。这将用递归实现。 

下面是 CWndEvents::CheckContextMenu() 方法的代码。首先, 传递菜单项类型的对象, 并尝试获取指向上下文菜单的指针。如果指针正确, 检查 此上下文菜单是否已打开。如果是, 设置可用性标志。然后在一个循环内将此菜单的所有项目设置为可用状态。与此同时, 使用 CWndEvents::CheckContextMenu() 方法 检查每个项目是否含有上下文菜单。

class CWndEvents : public CWndContainer
  {
private:
   //--- 检查并令上下文菜单可用
   void              CheckContextMenu(CMenuItem &object);
  };
//+------------------------------------------------------------------+
//| 递归检查并令上下文菜单可用                                         |
//+------------------------------------------------------------------+
void CWndEvents::CheckContextMenu(CMenuItem &object)
  {
//--- 获取上下文菜单指针
   CContextMenu *cm=object.GetContextMenuPointer();
//--- 如果项目中没有上下文菜单, 离开
   if(::CheckPointer(cm)==POINTER_INVALID)
      return;
//--- 如果有上下文菜单, 但它是隐藏的, 离开
   if(!cm.IsVisible())
      return;
//--- 设置控件可用标志
   cm.IsAvailable(true);
//---
   int items_total=cm.ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- 设置控件可用标志
      CMenuItem *mi=cm.GetItemPointer(i);
      mi.IsAvailable(true);
      //--- 检查该项目是否有上下文菜单
      CheckContextMenu(mi);
     }
  }

现在我们来研究 CWndEvents::OnSetAvailable() 方法, 该方法处理事件以便判断可用的控件。 

如果接收到含有 ON_SET_AVAILABLE 标识符的自定义事件, 则首先需要判断当前是否存在激活的控件。局部变量存储激活控件的索引, 以便快速访问其私有数组。

如果接收到判断可用控件的信号, 则首先 禁止访问整个列表。如果信号是恢复, 则在检查不存在激活控件之后, 恢复整个列表的访问, 且程序离开方法

如果程序在此方法中到达下一个代码块, 这意味着它是 (1) 判断可用控件的信号, 或 (2) 恢复, 但是有一个激活的下拉日历。当使用激活的组合框打开下拉列表时, 可能会出现第二种情况, 下拉列表已关闭。

如果满足所描述的条件之一, 尝试获取指向激活控件的指针。如果没有收到指针, 程序将离开该方法

如果得到指针, 则可以使用该控件。对于一些控制, 这些是如何完成的细节。这些都是这种情况:

  • 主菜单 (CMenuBar)。如果它被激活, 则需要提供与其相关的所有打开的上下文菜单。为此目的, 在上面的代码清单中参考了递归方法 CWndEvents::CheckContextMenu()。

  • 菜单项 (CMenuItem)。菜单项可以是独立控件, 可以挂载那些上下文菜单。因此, 如果这样的控件被激活, 则控件本身 (菜单项) 也在此处可用, 以及其中所有打开的上下文菜单。

  • 滚动条 (CScroll)。如果滚动条被激活 (在移动过程中为滑块), 则从第一个开始就要令所有控件可用。例如, 如果滚动条挂载到列表视图, 则列表视图及其全部嵌套深度的所有控件将可用。

  • 树形视图 (CTreeView)。当其列表的宽度被更改时, 可以激活此控件。有必要排除鼠标悬停时列表视图的处理, 并使树形视图本身可用于处理。

以下是 CWndEvents::OnSetAvailable() 方法的代码。

class CWndEvents : public CWndContainer
  {
private:
   //--- 判断可用的控件
   bool              OnSetAvailable(void);
  };
//+------------------------------------------------------------------+
//| 判断可用控件的事件                                                |
//+------------------------------------------------------------------+
bool CWndEvents::OnSetAvailable(void)
  {
//--- 如果信号是有关控件可用性变化
   if(m_id!=CHARTEVENT_CUSTOM+ON_SET_AVAILABLE)
      return(false);
//--- 设置/恢复信号
   bool is_restore=(bool)m_dparam;
//--- 判断激活控件
   int mb_index =ActivatedMenuBarIndex();
   int mi_index =ActivatedMenuItemIndex();
   int sb_index =ActivatedSplitButtonIndex();
   int cb_index =ActivatedComboBoxIndex();
   int dc_index =ActivatedDropCalendarIndex();
   int sc_index =ActivatedScrollIndex();
   int tl_index =ActivatedTableIndex();
   int sd_index =ActivatedSliderIndex();
   int tv_index =ActivatedTreeViewIndex();
   int ch_index =ActivatedSubChartIndex();
//--- 如果信号是判断可用控件, 首先禁用访问 
   if(!is_restore)
      SetAvailable(m_active_window_index,false);
//--- 只在没有激活的项目时才能恢复
   else
     {
      if(mb_index==WRONG_VALUE && mi_index==WRONG_VALUE && sb_index==WRONG_VALUE &&
         dc_index==WRONG_VALUE && cb_index==WRONG_VALUE && sc_index==WRONG_VALUE &&
         tl_index==WRONG_VALUE && sd_index==WRONG_VALUE && tv_index==WRONG_VALUE && ch_index==WRONG_VALUE)
        {
         SetAvailable(m_active_window_index,true);
         return(true);
        }
     }
//--- 如果 (1) 信号是禁用访问或 (2) 恢复下拉日历
   if(!is_restore || (is_restore && dc_index!=WRONG_VALUE))
     {
      CElement *el=NULL;
      //--- 主菜单
      if(mb_index!=WRONG_VALUE)
        { el=m_wnd[m_active_window_index].m_menu_bars[mb_index];      }
      //--- 菜单项
      else if(mi_index!=WRONG_VALUE)
        { el=m_wnd[m_active_window_index].m_menu_items[mi_index];     }
      //--- 拆分按钮
      else if(sb_index!=WRONG_VALUE)
        { el=m_wnd[m_active_window_index].m_split_buttons[sb_index];  }
      //--- 没有下拉列表的下拉式日历
      else if(dc_index!=WRONG_VALUE && cb_index==WRONG_VALUE)
        { el=m_wnd[m_active_window_index].m_drop_calendars[dc_index]; }
      //--- 下拉列表
      else if(cb_index!=WRONG_VALUE)
        { el=m_wnd[m_active_window_index].m_combo_boxes[cb_index];    }
      //--- 滚动条
      else if(sc_index!=WRONG_VALUE)
        { el=m_wnd[m_active_window_index].m_scrolls[sc_index];        }
      //--- 表格
      else if(tl_index!=WRONG_VALUE)
        { el=m_wnd[m_active_window_index].m_tables[tl_index];         }
      //--- 滑块
      else if(sd_index!=WRONG_VALUE)
        { el=m_wnd[m_active_window_index].m_sliders[sd_index];        }
      //--- 树形视图
      else if(tv_index!=WRONG_VALUE)
        { el=m_wnd[m_active_window_index].m_treeview_lists[tv_index]; }
      //--- 子图表
      else if(ch_index!=WRONG_VALUE)
        { el=m_wnd[m_active_window_index].m_sub_charts[ch_index];     }
      //--- 如果没有得到控件指针, 离开
      if(::CheckPointer(el)==POINTER_INVALID)
         return(true);
      //--- 主菜单模块
      if(mb_index!=WRONG_VALUE)
        {
         //--- 令主菜单及其可见的上下文菜单可用
         el.IsAvailable(true);
         //--- 
         CMenuBar *mb=dynamic_cast<CMenuBar*>(el);
         int items_total=mb.ItemsTotal();
         for(int i=0; i<items_total; i++)
           {
            CMenuItem *mi=mb.GetItemPointer(i);
            mi.IsAvailable(true);
            //--- 检查并令上下文菜单可用
            CheckContextMenu(mi);
           }
        }
      //--- 菜单项模块
      if(mi_index!=WRONG_VALUE)
        {
         CMenuItem *mi=dynamic_cast<CMenuItem*>(el);
         mi.IsAvailable(true);
         //--- 检查并令上下文菜单可用
         CheckContextMenu(mi);
        }
      //--- 滚动条模块
      else if(sc_index!=WRONG_VALUE)
        {
         //--- 从主节点开始令其可用
         el.MainPointer().IsAvailable(true);
        }
      //--- 树形视图模块
      else if(tv_index!=WRONG_VALUE)
        {
         //--- 锁定除主控件之外的所有控件
         CTreeView *tv=dynamic_cast<CTreeView*>(el);
         tv.IsAvailable(true,true);
         int total=tv.ElementsTotal();
         for(int i=0; i<total; i++)
            tv.Element(i).IsAvailable(false);
        }
      else
        {
         //--- 令控件可用
         el.IsAvailable(true);
        }
     }
//---
   return(true);
  }



测试控件的应用程序

用于测试目的 MQL 应用程序已实现。其图形界面包含函数库的所有控件。它看上去如何: 

图例. 12. 测试 MQL 应用程序的图形界面。

图例. 12. 测试 MQL 应用程序的图形界面。


您可在文章末尾下载它以便更仔细地研究。



结束语

这个版本的函数库与图形界面 X: 在多行文本框内选择文本 (构建 13) 中的表述区别很明显。完成了很多工作, 而这些几乎影响了函数库的所有文件。现在函数库中的所有控件都是在单独的对象上绘制。代码的可读性有所改善, 代码量已减少了大约 30%, 且其功能业已扩展。用户报告的一些其它错误和缺陷已经修复。

如果您已经开始使用以前版本的函数库创建 MQL 应用程序, 建议您首先将新版本下载到单独安装的 MetaTrader 5 终端副本上, 以便学习和彻底测试函数库。

处于当前开发阶段的创建图形界面的函数库如下图所示。这并非函数的最终版本: 它将来依然会发展和改进。

 图例. 13. 当前开发阶段的函数库结构

图例. 13. 当前开发阶段的函数库结构



如果您在使用这些文件中提供的素材时有任何疑问, 可以参考函数库开发系列文章之一的详细描述, 或在本文的评论中提出您的问题。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/3366

附加的文件 |
最近评论 | 前往讨论 (1)
1556165156
1556165156 | 28 8月 2017 在 11:19
加分析师327836497  收益率达到百分之74.5
图形界面 XI: 重构函数库代码 (集成编译 14.1) 图形界面 XI: 重构函数库代码 (集成编译 14.1)

随着函数库的增长, 其代码必须重新优化以便减少其大小。本文中描述的函数库版本已变得更加面向对象。这令代码更容易学习。最新变化的详细描述将令读者能够根据自己的需求独立开发函数库。

交易货币篮子时可用的形态。第三部分 交易货币篮子时可用的形态。第三部分

本文是交易货币篮子时发生形态的终篇。它综合了趋势跟踪指标和标准图形结构的应用。

跨平台智能交易系统: 时间过滤器 跨平台智能交易系统: 时间过滤器

本文探讨如何实现跨平台智能交易系统的各种时间过滤方法。时间过滤器类负责检查给定时间是否处于特定时间配置设置的范围内。

交易货币对篮子时出现的测试形态。第 I 部 交易货币对篮子时出现的测试形态。第 I 部

我们开始测试形态, 并尝试有关交易货币对篮子的文章中所描述的方法。我们看看在实践中如何应用超卖/超买等级的突破形态。