图形界面 I:库结构(第一章)·进阶篇
🧩

图形界面 I:库结构(第一章)·进阶篇

(2/3)· 从标准库与第三方库的短板出发,看清一套可扩展 GUI 基类该长什么样

含代码示例偏理论 第 2/3 篇
很多交易者装了带面板的 EA 却从没看过底层类图,改个按钮样式就得翻遍源码。把界面当黑盒用,等到要加自定义控件就只能从头写。理清库结构,二次开发成本能降一大截。

◍ 矩形标签类的空壳构造与析构

在 MT5 自定义控件库里,CRectLabel 的构造函数只做了两件极简的事:把成员变量 m_y_size 初始化为 0,将 m_mouse_focus 设为 false,随后立即用空函数体收尾。 这种写法意味着对象诞生时并不绑定任何图表资源,所有实际绘制参数都要等后续方法显式赋值,适合在面板初始化阶段先批量 new 出占位对象。 析构函数同样是空实现,说明该类的内存回收不负责释放 GDI 资源,若你往里面挂了位图或字体,得在外部自己清理,否则终端跑久了可能泄漏句柄。 类声明区的 CRectLabel、CEdit、CLabel、CBmpLabel、CButton 一排前置声明,是为了让 Alt+G 快速跳转和交叉引用不报未定义错误,实际编译时链接的是后面各自的实现文件。

MQL5 / C++
                                                                               m_y_size(class="num">0),
                                                                               m_mouse_focus(class="kw">false)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 析构函数                                                                             
class=class="str">"cmt">//+------------------------------------------------------------------+
CRectLabel::~CRectLabel(class="type">void)
  {
  }
class=class="str">"cmt">//--- 用于快速导航(Alt+G)的类列表
class CRectLabel;
class CEdit;
class CLabel;
class CBmpLabel;
class CButton;

搭 GUI 控件库从 CElement 基类开始

在 MT5 里自己写一套图表控件库,第一步是把所有图形元素的公共基底抽出来。CElement 类放在 Element.mqh 中,它通过 #include 把 Objects.mqh 拉进来;反过来 Objects.mqh 里的类都不是 CElement 的基类,而是以后作为指针数组挂在基类里当成员对象用。所以你只要包含 Element.mqh 一个文件,整条链(Defines.mqh -> Objects.mqh -> Element.mqh)就全有了,不用重复 include。 基类要管的公共属性很具体:控件名、程序类型(EA/指标)、是否隐藏、是否下拉一部分、是否贴在某组标签上、鼠标是否悬停,以及每个控件唯一 id。图形对象本身以 CChartObject* 数组存,因此基类必须提供插入指针、按索引取指针、取数组大小、清缓存这几个方法。 事件和绘制不能写死。CElement 里的事件句柄、私有计时器,以及移动/显示/隐藏/重置/删对象/设鼠标左键优先级这些,全部声明成 virtual——因为每个控件行为不一样,派生类各自重写。 悬停变色是直观交互的关键。颜色计算只在对象贴图时跑一次,之后只查已算好的渐变数组,省资源。渐变用现成的 CColors 类(Colors.mqh,放在 EasyAndFastGUI 上层目录,用 ../ 包含),我改过一处:类体内声明、类体外定义的方法支持 Alt+G 跳转。基类里挂一个 CColors 对象,加两个东西:渐变颜色数量的变量与方法、初始化渐变数组和改指定对象颜色的方法。 初始化调 CColors::Gradient(),参数是「起始颜色数组、接收序列的数组、颜色数量」三项。ChangeObjectColor() 里要确定对象名、鼠标是否悬停、改哪部分(背景/边框)、以及两个端色。渐变数组在 InitColorArray() 里成型。这套结构跑通后,库依赖就如图里黄箭头所示:被包含文件里的类不作为基类,只当对象成员。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                    Element.mqh |
class=class="str">"cmt">//|                 Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "Objects.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 控件基类                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CElement
  {
class="kw">public:
                        CElement(class="type">void);
                       ~CElement(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                  Defines.mqh |
class=class="str">"cmt">//|                 Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- 类名

「用宏和基类给面板控件打身份标签」

在 MT5 自定义控件库里,先靠一组预处理宏把运行环境信息固化下来,能省掉后续每个类里反复拼字符串的麻烦。下面这段把类名、程序名、程序类型、字体与防越界提示都定义成宏,其中 CLASS_NAME 利用 __FUNCTION__ 截取到命名空间前的类名,FONT_SIZE 直接锁成 8 点小字,适合高密度面板。 CElement 作为所有图形控件的基类,protected 区只存三样环境标识:m_class_name、m_program_name、m_program_type,外加可见性、下拉态、标签对象数和鼠标焦点标志。公共接口里 ClassName() 有读写两个重载,读返回 const 引用、写直接赋值,ProgramName() 和 ProgramType() 仅提供只读访问。 把环境信息收口到基类而不是每个派生类各自获取,好处是运行时用 MQLInfoInteger(MQL_PROGRAM_TYPE) 只调一次,EA、指标、脚本拿到的是同一个 ENUM_PROGRAM_TYPE。你在 MT5 里新建一个面板 EA,把这段基类和 Defines.mqh 一并 include,编译后能在观测窗口看到 m_class_name 自动等于截出来的类名,不用手填。 外汇与贵金属图表上加这类自定义控件,本质是在终端层叠 UI,仍受报价断流与品种点差跳变影响,任何交互逻辑都只是辅助,不替代仓位风控。

MQL5 / C++
class="macro">#define CLASS_NAME ::StringSubstr(__FUNCTION__,class="num">0,::StringFind(__FUNCTION__,"::"))
class=class="str">"cmt">//--- 程序名
class="macro">#define PROGRAM_NAME ::MQLInfoString(MQL_PROGRAM_NAME)
class=class="str">"cmt">//--- 程序类型
class="macro">#define PROGRAM_TYPE(ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)
class=class="str">"cmt">//---放置数组大小溢出
class="macro">#define PREVENTING_OUT_OF_RANGE __FUNCTION__," > Prevention of exceeding the array size."
class=class="str">"cmt">//--- 字体
class="macro">#define FONT("Calibri")
class="macro">#define FONT_SIZE(class="num">8)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     Objects.mqh |
class=class="str">"cmt">//|      Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "Defines.mqh"
class="macro">#include <ChartObjects\ChartObjectsBmpControls.mqh>
class="macro">#include <ChartObjects\ChartObjectsTxtControls.mqh>
class CElement
  {
class="kw">protected:
   class=class="str">"cmt">//--- (class="num">1) 类名 (class="num">2) 程序 (class="num">3) 程序类型
   class="type">class="kw">string            m_class_name;
   class="type">class="kw">string            m_program_name;
   ENUM_PROGRAM_TYPE m_program_type;
   class=class="str">"cmt">//--- 控件状态
   class="type">bool              m_is_visible;
   class="type">bool              m_is_dropdown;
   class="type">int               m_is_object_tabs;
   class=class="str">"cmt">//--- 焦点
   class="type">bool              m_mouse_focus;
   class=class="str">"cmt">//---
class="kw">public:
                     CElement(class="type">void);
                    ~CElement(class="type">void);
   class=class="str">"cmt">//--- (class="num">1) 获取并设置类名 (class="num">2) 获取程序名, 
   class=class="str">"cmt">//     (class="num">3) 获取程序类型
   class="type">class="kw">string            ClassName(class="type">void)                class="kw">const { class="kw">return(m_class_name);      }
   class="type">void              ClassName(class="kw">const class="type">class="kw">string class_name)    { m_class_name=class_name;      }
   class="type">class="kw">string            ProgramName(class="type">void)              class="kw">const { class="kw">return(m_program_name);     }
   ENUM_PROGRAM_TYPE ProgramType(class="type">void)              class="kw">const { class="kw">return(m_program_type);     }
   class=class="str">"cmt">//--- 控件状态

◍ GUI 控件的可见性与焦点封装

在 MT5 自定义面板开发里,控件的显示状态与鼠标交互权需要被独立封装,否则多控件叠加时容易出现「明明画了却看不见」或「点 A 却选中 B」的幽灵 bug。下面这组存取器把可见、下拉、对象页签、鼠标焦点全部做成 const 重载,读写分离且不可越权改内部变量。 void IsVisible(const bool flag) { m_is_visible=flag; } bool IsVisible(void) const { return(m_is_visible); } void IsDropdown(const bool flag) { m_is_dropdown=flag; } bool IsDropdown(void) const { return(m_is_dropdown); } void IsObjectTabs(const int index) { m_is_object_tabs=index; } int IsObjectTabs(void) const { return(m_is_object_tabs); } //--- 焦点 bool MouseFocus(void) const { return(m_mouse_focus); } void MouseFocus(const bool focus) { m_mouse_focus=focus; } 逐行看:IsVisible 写方法把外部 bool 写进 m_is_visible,读方法加 const 保证不篡改成员;IsDropdown 同理管下拉展开态;IsObjectTabs 用 int 而非 bool,意味着一个控件可能挂靠多个对象页签(index 标识具体哪一页)。MouseFocus 的读写分离最关键——只读版本供绘制线程查询,写版本由事件回调设置,避免鼠标悬停时状态被并发改坏。 CElement 基类另存了 m_id 与 m_index 两个 protected 字段,Id()/Index() 同样用 const 重载暴露。实际调试时,若面板控件不响应点击,先打印 Index() 确认事件映射的索引没错位;外汇与贵金属图表挂 EA 做 UI 高频刷新时,这类竞态可能导致点击失效,属高风险调试点,建议开 MT5 策略测试器单步验证。

MQL5 / C++
class="type">void IsVisible(class="kw">const class="type">bool flag) { m_is_visible=flag; }
class="type">bool IsVisible(class="type">void) class="kw">const { class="kw">return(m_is_visible); }
class="type">void IsDropdown(class="kw">const class="type">bool flag) { m_is_dropdown=flag; }
class="type">bool IsDropdown(class="type">void) class="kw">const { class="kw">return(m_is_dropdown); }
class="type">void IsObjectTabs(class="kw">const class="type">int index) { m_is_object_tabs=index; }
class="type">int IsObjectTabs(class="type">void) class="kw">const { class="kw">return(m_is_object_tabs); }
class=class="str">"cmt">//--- 焦点
class="type">bool MouseFocus(class="type">void) class="kw">const { class="kw">return(m_mouse_focus); }
class="type">void MouseFocus(class="kw">const class="type">bool focus) { m_mouse_focus=focus; }
};
class CElement
  {
class="kw">protected:
  class=class="str">"cmt">//--- 控件的标识和索引
  class="type">int m_id;
  class="type">int m_index;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 设置并或获取控件标识
  class="type">void Id(class="kw">const class="type">int id) { m_id=id; }
  class="type">int Id(class="type">void) class="kw">const { class="kw">return(m_id); }
  class=class="str">"cmt">//---设置并获取控件索引
  class="type">void Index(class="kw">const class="type">int index) { m_index=index; }
  class="type">int Index(class="type">void) class="kw">const { class="kw">return(m_index); }
  };
class CElement
  {
class="kw">protected:

控件基类的对象管理与事件虚函数

做 MT5 图形控件库时,先得有一个统一的基类把图表对象指针收拢起来。CElement 里用 m_objects[] 这个 CChartObject 指针数组装下该控件关联的所有图形对象,对外只暴露 Object(index) 取指针、ObjectsElementTotal() 看数量、FreeObjectsArray() 清空三个口子。 Object() 不是简单下标访问:先拿 ArraySize 测数组,若小于 1 就 Print 报错并返回 NULL;索引越界时自动夹到 [0, size-1],避免外部传错序号直接崩。AddToArray() 则先取当前总数,ArrayResize 扩 1,再把传入对象的指针塞进去——这套写法让子类加线、加标签都不用管内存细节。 基类还预留了一组虚函数:OnEvent 接图表事件、OnEventTimer 跑定时器、Moving 管位移,以及 Show/Hide/Reset/Delete 和 Z 轴排序的 SetZorders/ResetZorders。全部是空实现,留给按钮、面板等子类去重写。外汇和贵金属图表上挂这类自定义控件,需注意 EA 在实盘环境崩溃可能造成订单暴露,属高风险操作,建议先在策略测试器里跑通。

MQL5 / C++
  class=class="str">"cmt">//--- 此控件中所有对象的公共指针数组
  CChartObject      *m_objects[];
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 通过指定的索引获取对象指针
  CChartObject      *Object(class="kw">const class="type">int index);
  class=class="str">"cmt">//--- (class="num">1) 获取控件对象数量 (class="num">2) 清空对象数组
  class="type">int               ObjectsElementTotal(class="type">void)            class="kw">const { class="kw">return(::ArraySize(m_objects)); }
  class="type">void              FreeObjectsArray(class="type">void)                      { ::ArrayFree(m_objects);       }
  class=class="str">"cmt">//---
class="kw">protected:
  class=class="str">"cmt">//--- 向共有数组中添加原始对象的指针的方法
  class="type">void              AddToArray(CChartObject &object);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 通过索引返回控件对象的指针                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
CChartObject *CElement::Object(class="kw">const class="type">int index)
  {
   class="type">int array_size=::ArraySize(m_objects);
class=class="str">"cmt">//--- 效验对象数组的大小
   if(array_size<class="num">1)
     {
      ::Print(__FUNCTION__,"> No("+m_class_name+") objects in this element!");
      class="kw">return(NULL);
     }
class=class="str">"cmt">//--- 如果数组大小越界则修正
   class="type">int i=(index>=array_size)? array_size-class="num">1 : (index<class="num">0)? class="num">0 : index;
class=class="str">"cmt">//--- 返回对象指针
   class="kw">return(m_objects[i]);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 向数组中添加对象指针                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CElement::AddToArray(CChartObject &object)
  {
   class="type">int size=ObjectsElementTotal();
   ::ArrayResize(m_objects,size+class="num">1);
   m_objects[size]=::GetPointer(object);
  }
class CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 图表事件句柄
  class="kw">virtual class="type">void      OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam) {}
  class=class="str">"cmt">//--- 计时器
  class="kw">virtual class="type">void      OnEventTimer(class="type">void) {}
  class=class="str">"cmt">//--- 移动控件
  class="kw">virtual class="type">void      Moving(class="kw">const class="type">int x,class="kw">const class="type">int y) {}
  class=class="str">"cmt">//--- (class="num">1) 显示, (class="num">2) 隐藏, (class="num">3) 重置, (class="num">4) 删除
  class="kw">virtual class="type">void      Show(class="type">void) {}
  class="kw">virtual class="type">void      Hide(class="type">void) {}
  class="kw">virtual class="type">void      Reset(class="type">void) {}
  class="kw">virtual class="type">void      Delete(class="type">void) {}
  class=class="str">"cmt">//--- (class="num">1) 设置, (class="num">2) 按鼠标左键重置属性
  class="kw">virtual class="type">void      SetZorders(class="type">void) {}
  class="kw">virtual class="type">void      ResetZorders(class="type">void) {}
  };

「用渐变数组接管控件悬停着色」

在 MT5 自定义控件库里,CElement 基类把颜色处理拆给 CColors 实例,并靠 m_gradient_colors_total 控制渐变阶数。想让按钮或面板在鼠标移入时平滑变色,关键不在逐个改对象属性,而是先备好一条颜色梯度数组。 InitColorArray 只接收外框色与悬停色两个端点,塞进长度为 2 的 colors 数组,再交给 m_clr.Gradient 按 m_gradient_colors_total 扩成多阶数组。比如 total 设为 10,就能得到 10 个过渡色,悬停时不再生硬跳变。 ChangeObjectColor 先判 color_array 大小,若不足 1 直接 return,避免空数组写对象属性引发运行时报错。实际调试时把 mouse_focus 打日志,能看到悬停状态翻转瞬间数组索引的走向。 外汇与贵金属图表挂这类控件属高风险环境,EA 崩溃可能漏掉平仓信号,建议先在策略测试器里用脚本验证着色逻辑再上实盘。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Element.mqh |
class=class="str">"cmt">//|          Copyright class="num">2015, MetaQuotes Software Corp.                 |
class=class="str">"cmt">//|                                             [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "Objects.mqh"
class="macro">#include "..\Colors.mqh"
class CElement
  {
class="kw">protected:
   class=class="str">"cmt">//--- 处理颜色的类的实例
   CColors            m_clr;
   class=class="str">"cmt">//--- 渐变色数量
   class="type">int                m_gradient_colors_total;
   class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 设置渐变尺寸
   class="type">void               GradientColorsTotal(class="kw">const class="type">int total)      { m_gradient_colors_total=total;  }
   class=class="str">"cmt">//---
class="kw">protected:
   class=class="str">"cmt">//--- 初始化渐变色数组
   class="type">void               InitColorArray(class="kw">const class="type">color outer_color,class="kw">const class="type">color hover_color,class="type">color &color_array[]);
   class=class="str">"cmt">//--- 改变对象颜色
   class="type">void               ChangeObjectColor(class="kw">const class="type">class="kw">string name,class="kw">const class="type">bool mouse_focus,class="kw">const ENUM_OBJECT_PROPERTY_INTEGER class="kw">property,
                                        class="kw">const class="type">color outer_color,class="kw">const class="type">color hover_color,class="kw">const class="type">color &color_array[]);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 初始化渐变色数组                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CElement::InitColorArray(class="kw">const class="type">color outer_color,class="kw">const class="type">color hover_color,class="type">color &color_array[])
  {
class=class="str">"cmt">//--- 渐变色数组
   class="type">color colors[class="num">2];
   colors[class="num">0]=outer_color;
   colors[class="num">1]=hover_color;
class=class="str">"cmt">//--- 构造颜色数组
   m_clr.Gradient(colors,color_array,m_gradient_colors_total);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 当鼠标悬停在对象上时改变对象的颜色                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CElement::ChangeObjectColor(class="kw">const class="type">class="kw">string name,class="kw">const class="type">bool mouse_focus,class="kw">const ENUM_OBJECT_PROPERTY_INTEGER class="kw">property,
                                 class="kw">const class="type">color outer_color,class="kw">const class="type">color hover_color,class="kw">const class="type">color &color_array[])
  {
   if(::ArraySize(color_array)<class="num">1)
      class="kw">return;
  }
把类图梳理交给小布盯盘
这些控件继承关系的诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到库结构速览,你专注决策逻辑而非翻代码。

常见问题

标准库框架结构更清晰但控件少、缺详细说明;Dmitry 的库控件更丰富但以说明书形式给出,控制机制作者并不全认可。本篇库试图汇两者优点。
前期两篇按钮/滚动条文章用面向过程只为熟悉 MQL5;大型界面项目涉及大量控件复用与继承,面向对象能明显降低维护与扩展成本。
一般负责坐标、尺寸、可见性、绘制调度等最通用属性,具体控件再由其衍生并追加交互与样式方法。
可以,小布盯盘的品种页内置了常见 GUI 库的类关系速览与控件清单,省去你手动逆向 mqh 的时间。
验证程序库与程序类在鼠标、计时器等事件下的响应路径是否正确,避免后续实战篇接入逻辑时出现派发错乱。