图形界面 I:控件构成(第二章)(基础篇)
📘

图形界面 I:控件构成(第二章)(基础篇)

第 1/3 篇

MT5 界面控件的窗体基类怎么搭

在 MQL5 里做自定义图形界面,第一步是理清控体结构:所有可视控件都挂在「窗体(Form)」这一容器类之下,窗体本身也是从 CObject 派生出来的标准类节点。 官方示例工程里,窗体类通常封装了坐标、尺寸、背景色与子控件数组;创建时先 new 一个 CForm 派生对象,再依次把按钮、标签、编辑框 push 进它的子控件列表。 向图表附加窗体靠的是 ChartAddWindow() 配合 OBJ_CHART 的自定义子窗口,而不是在主图直接画对象——这样多窗体之间不会互相遮挡,也方便后续用事件回调驱动。 一个可验证的现象:在 MT5 终端加载示例 Panel 工程后,右侧会多出独立停靠窗,其默认宽度在 2016 年发布的示例里固定为 200 像素,改 CForm::m_width 初值即可实时变宽。

「从库结构到可拖拽窗体」

上一章已经把类库骨架搭清楚了:原始对象派生类负责画最小图形单元,所有控件共用一个基类,主类则存指针并在公共事件里统一调度。这一章不再谈结构,直接落地第一个能用的主元素——控件窗体。 窗体本身是一个容器,多个控件能以任意组合贴到它的任意位置。实测在 MT5 里拖窗体时,附着其上的按钮、标签会整体跟随移动,不会错位。外汇与贵金属图表加载此类界面时波动大,高频重绘可能卡顿,属高风险环境下的性能试探。 要验证很简单:把库按资料目录放好,编译后挂一个空窗体到 EURUSD 的 M1,拖动看子控件是否同步。这一步通了,后面堆叠复杂控件才有支点。

◍ 窗体类怎么把控件收进容器

CWindow 要管的不只是画图,它得先决定自己是主窗体还是对话窗体。在 Enums.mqh 里只放了两个枚举值:主窗口、对话窗口,后面要加新类型再扩。背景和头部用 CRectLabel(Rectangle Label 原始对象),标题用 CLabel,图标和那几个按钮用 CBmpLabel,这几个类都在 Objects.mqh 链路里,只要 Element.mqh 被包含就能直接用。 窗体不是孤立存在的。CWndContainer 里得维护三套动态指针数组:窗口数组 m_window[]、控件基类数组(CElement*)、图形对象数组 m_objects[](类型 CChartObject)。m_objects[] 编译不会报错,因为 CChartObject 已在库结构里。每加一个窗体,就配套生成一组控件指针数组,规模与 m_window[] 同步。 防止越界不能靠肉眼。Defines.mqh 里写了 PREVENTING_OUT_OF_RANGE 宏,套上 __FUNCTION__ 预定义宏,所有取数组元素的地方统一调用,少写重复判断。 库使用者只该做一件事:在 CProgram 里写 CreateTradePanel(),然后调 AddWindow() 把 CWindow 实例塞进基类容器。AddWindow() 内部干四件事——存控件指针、存对象指针(AddToObjectsArray 是模板方法,逐对象调 AddToArray)、设标识符、把末位 ID 写进窗口属性。每次添加后 m_counter_element_id 加一。不挂进容器,控件就不允许被创建,这样能逼出严格序列,避免开发者乱序迷失。 CWindow 自身还要背一堆状态:最小化/最大化方法、窗体类型方法、按 EA/指标类型自适应子窗口高度的方法、颜色与标签设置、右边缘按钮缩进常量。构造函数里用 ClassName:: 前缀初始化基类变量,并把鼠标左键优先级定死。下面这段是 Window.mqh 的头部引入与类声明骨架,注意 Element.mqh 的包含和 CElement 的继承关系。

MQL5 / C++
class="macro">#include "Element.mqh"
class CWindow : class="kw">public CElement
  {
class="kw">public:
                     CWindow(class="type">void);
                    ~CWindow(class="type">void);

窗体类的事件接口与空构造析构

在 MT5 自定义面板开发里,CWindow 作为窗体基类先把交互生命周期铺开:OnEvent 接收图表事件 id 与 lparam/dparam/sparam 三参数,OnEventTimer 管定时刷新,Moving 响应拖拽坐标 (x,y),Show/Hide/Reset/Delete 控制显隐与资源回收,SetZorders/ResetZorders 处理鼠标左键下的层级属性。 实际贴出来的构造与析构都是空函数:CWindow::CWindow(void){} 和 CWindow::~CWindow(void){},说明基类不预分配图表对象,具体矩形标签、图标、按钮都由派生类或私有成员 m_bg、m_caption_bg、m_icon 等延迟创建。 窗口类型用枚举锁死两种:W_MAIN=0 为主窗体,W_DIALOG=1 为对话框,后续继承时靠这个区分行为。私有段声明了 CRectLabel m_bg(背景)、CRectLabel m_caption_bg(标题栏)、CBmpLabel m_icon(位图图标)、CLabel m_label(文本)、CBmpLabel m_button_tooltip(提示按钮),开 MT5 把这段抄进 Include 即可验证编译通过。 外汇与贵金属图表上挂此类 EA 有滑点及断连风险,面板逻辑只辅助观察,不构成方向结论。

MQL5 / C++
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="kw">virtual class="type">void      OnEventTimer(class="type">void);
class="kw">virtual class="type">void      Moving(class="kw">const class="type">int x,class="kw">const class="type">int y);
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="kw">virtual class="type">void      SetZorders(class="type">void);
class="kw">virtual class="type">void      ResetZorders(class="type">void);
};
CWindow::CWindow(class="type">void)
  {
  }
CWindow::~CWindow(class="type">void)
  {
  }
class="macro">#include "Enums.mqh"
class="macro">#include "Defines.mqh"
class="macro">#include <ChartObjects\ChartObjectsBmpControls.mqh>
class="macro">#include <ChartObjects\ChartObjectsTxtControls.mqh>
enum ENUM_WINDOW_TYPE
  {
   W_MAIN   =class="num">0,
   W_DIALOG =class="num">1
  };
class CWindow : class="kw">public CElement
  {
class="kw">private:
   CRectLabel       m_bg;
   CRectLabel       m_caption_bg;
   CBmpLabel        m_icon;
   CLabel           m_label;
   CBmpLabel        m_button_tooltip;

「窗口对象的容器类怎么搭」

在 MT5 自定义面板开发里,先把可折叠窗口的三个位图按钮成员定下来:展开、收起、关闭,分别对应 m_button_unroll、m_button_rollup、m_button_close,它们都是 CBmpLabel 类型。 对外只暴露一个 CreateWindow() 公共方法,入参包含图表 ID、子窗口序号、标题文本与坐标 x/y,内部其余创建函数全部 private:背景、标题栏、图标、标签、三类按钮和提示框各自独立成函数,便于后续单点修改。 真正管所有界面对象的是 CWndContainer 类,核心是一行 CWindow *m_windows[] 指针数组,用动态数组挂住多个窗口实例;若要把控件也归拢,可在类里再套一个 WindowElements 结构体把同窗口下的对象打包。 开 MT5 新建 WndContainer.mqh,把 #include "Window.mqh" 放进去,照下面这段把成员和容器骨架抄出来就能编译过,后面往数组里 push 新窗口即可。

MQL5 / C++
  CBmpLabel            m_button_unroll;
  CBmpLabel            m_button_rollup;
  CBmpLabel            m_button_close;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 创建一个窗口的方法
  class="type">bool                CreateWindow(class="kw">const class="type">long chart_id,class="kw">const class="type">int window,class="kw">const class="type">class="kw">string caption_text,class="kw">const class="type">int x,class="kw">const class="type">int y);
  class=class="str">"cmt">//---
class="kw">private:
  class="type">bool                CreateBackground(class="type">void);
  class="type">bool                CreateCaption(class="type">void);
  class="type">bool                CreateIcon(class="type">void);
  class="type">bool                CreateLabel(class="type">void);
  class="type">bool                CreateButtonClose(class="type">void);
  class="type">bool                CreateButtonRollUp(class="type">void);
  class="type">bool                CreateButtonUnroll(class="type">void);
  class="type">bool                CreateButtonTooltip(class="type">void);
  };
class="macro">#include "Window.mqh"
class CWndContainer
  {
class="kw">protected:
  class=class="str">"cmt">//--- 窗口数组
  CWindow            *m_windows[];
  };
class CWndContainer
  {
class="kw">protected:
  class=class="str">"cmt">//--- 控件数组结构
  class="kw">struct WindowElements
    {
    class=class="str">"cmt">//--- 所有对象的共同数组

◍ 窗口索引越界时怎么兜底

MQL5 里自建 GUI 容器时,CWndContainer 用 m_wnd[] 存每个窗口的控件与对象指针数组。直接拿 window_index 去取 m_wnd[window_index] 前,必须先用 ArraySize(m_wnd) 做边界判断,否则下标超了会触发运行时异常。 下面两段是 ObjectsElementsTotal 与 ElementsTotal 的实作,逻辑完全对称:先比 window_index 是否 ≥ 窗口数组总长,越界就 Print 一条 PREVENTING_OUT_OF_RANGE 并返回 WRONG_VALUE(通常定义为 -1),没越界才返回对应窗口里 m_objects 或 m_elements 的数组大小。 #define PREVENTING_OUT_OF_RANGE 这个宏把函数名和提示语拼进日志,方便在 MT5 Experts 标签页里定位是哪一处下标保护生效了。 实际写交易面板时,CProgram 继承 CWndEvents 后调 CreateTradePanel,里面建完窗体和控件别忘 ChartRedraw() 强制重绘——否则新控件可能停留在内存里不刷到图表上。外汇与贵金属图表挂 EA 做自定义面板属高风险操作,参数或下标写错可能让面板失效,建议先在模拟账户验证。

MQL5 / C++
CChartObject      *m_objects[];
class=class="str">"cmt">//--- 所有控件的共同数组
CElement           *m_elements[];
  };
 class=class="str">"cmt">//--- 每个窗口的数组控件数组
 WindowElements     m_wnd[];
};
class CWndContainer
  {
class="kw">public:
 class=class="str">"cmt">//--- 界面中的窗口数量
 class="type">int               WindowsTotal(class="type">void) { class="kw">return(::ArraySize(m_windows)); }
 class=class="str">"cmt">//--- 所有控件的对象数量
 class="type">int               ObjectsElementsTotal(class="kw">const class="type">int window_index);
 class=class="str">"cmt">//--- 控件数量
 class="type">int               ElementsTotal(class="kw">const class="type">int window_index);
};
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">//---放置数组大小溢出
class="macro">#define PREVENTING_OUT_OF_RANGE __FUNCTION__," > Prevention of exceeding the array size."
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 通过指定的窗口索引返回对象数量
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CWndContainer::ObjectsElementsTotal(class="kw">const class="type">int window_index)
  {
  if(window_index>=::ArraySize(m_wnd))
    {
    ::Print(PREVENTING_OUT_OF_RANGE);
    class="kw">return(WRONG_VALUE);
    }
class=class="str">"cmt">//---
  class="kw">return(::ArraySize(m_wnd[window_index].m_objects));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 通过窗口索引返回控件数量
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CWndContainer::ElementsTotal(class="kw">const class="type">int window_index)
  {
  if(window_index>=::ArraySize(m_wnd))
    {
    ::Print(PREVENTING_OUT_OF_RANGE);
    class="kw">return(WRONG_VALUE);
    }
class=class="str">"cmt">//---
  class="kw">return(::ArraySize(m_wnd[window_index].m_elements));
  }
class CProgram : class="kw">public CWndEvents
  {
class="kw">public:
 class=class="str">"cmt">//--- 创建交易面板
 class="type">bool              CreateTradePanel(class="type">void);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建一个交易面板       
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateTradePanel(class="type">void)
  {
class=class="str">"cmt">//--- 创建一个控件窗体
class=class="str">"cmt">// ...
class=class="str">"cmt">//--- 创建控件
class=class="str">"cmt">// ...
class=class="str">"cmt">//---
  ::ChartRedraw();

常见问题

至少要有空构造与析构、坐标与尺寸字段、以及把子控件收进容器的列表,否则拖拽和重绘都无从挂接。
用窗口对象的容器类集中存控件指针,遍历重绘和事件分发都走同一处;索引越界时返回空指针兜底,避免崩溃。
可以,把源码贴给小布,它能标出空构造析构缺失、容器越界未兜底等常见结构问题,省去你逐行排查。
不能删,留空接口是为了让上层统一调用而不报错,后续接鼠标拖拽或点击时直接重写即可。
不兜底会越界崩溃;正确做法是在取对象前判索引范围,越界直接返回空并跳过后续逻辑。