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

Anatoli Kazharski | 22 八月, 2017


内容

概述

首篇文章 图形界面 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. 当前开发阶段的函数库结构



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