![图形界面 XI: 渲染控件 (统合构建14.2)](https://c.mql5.com/i/articles/overlay.png)
图形界面 XI: 渲染控件 (统合构建14.2)
内容
概述
首篇文章 图形界面 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 通道值从 0 到 255 作为参数传递。零值意味着完全透明。在当前版本中, 透明度仅适用于背景填充和边框。文字和图像将保持完全不透明, 并清除所有 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. 演示控件窗体的透明度。
若要令窗体的整个区域透明, 请使用 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. 演示几种按钮类型的外观。
下一个屏幕截图显示了复选框, 旋转编辑框, 带有下拉列表和滚动条的组合框以及数字滑块的当前外观。请注意, 现在可以制作动画图标了。状态栏的第三项模仿从服务器断开连接。其外观精确复制 MetaTrader 5 状态条里的类似元素。
图例. 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. 演示两种工具提示 (标准和定制)。
新的事件标识符
添加了新的事件标识符。这显著降低了 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; } ... }
优化函数库核心
我们来研究针对 CWndContainer 和 CWndEvents 类所做的更改, 修正和添加, 可以将其称为函数库的核心。毕竟, 它们调配对其所有控件的访问, 并处理由图形界面的控件生成的事件流。
一个模板方法 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 应用程序的图形界面。
您可在文章末尾下载它以便更仔细地研究。
结束语
这个版本的函数库与图形界面 X: 在多行文本框内选择文本 (构建 13) 中的表述区别很明显。完成了很多工作, 而这些几乎影响了函数库的所有文件。现在函数库中的所有控件都是在单独的对象上绘制。代码的可读性有所改善, 代码量已减少了大约 30%, 且其功能业已扩展。用户报告的一些其它错误和缺陷已经修复。
如果您已经开始使用以前版本的函数库创建 MQL 应用程序, 建议您首先将新版本下载到单独安装的 MetaTrader 5 终端副本上, 以便学习和彻底测试函数库。
处于当前开发阶段的创建图形界面的函数库如下图所示。这并非函数的最终版本: 它将来依然会发展和改进。
图例. 13. 当前开发阶段的函数库结构
如果您在使用这些文件中提供的素材时有任何疑问, 可以参考函数库开发系列文章之一的详细描述, 或在本文的评论中提出您的问题。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/3366
![交易中的夹角. 需要进一步的研究](https://c.mql5.com/2/27/MQL5-avatar-corner-003.png)
![图形界面 XI: 重构函数库代码 (集成编译 14.1)](https://c.mql5.com/2/28/MQL5-avatar-XI-build14.png)
您好,很高兴能看到您的开发作品,我发现有一个问题,请您有时间给解释下,问题: 树形控件,如何增加成tab类型,我设置成tab模式后不能工作。无法获取到增加到tab的元件。 谢谢!期待您的留言。
在第10版本可以做出来为什么14-2 就不能显示了呢?
你好,新版本的控件编辑输出部分,键盘的右侧数字小键盘失灵,输入数字没有反应,只能在左侧数字输入,请处理一下