如何创建任意复杂度的图形面板(基础篇)
📘

如何创建任意复杂度的图形面板(基础篇)

第 1/3 篇

「在 MT5 里搭出任意复杂度的图形面板」

MT5 的 GUI 不靠拖控件,而是用 CCanvas 类在图表上直接画。想做多按钮、多标签、带图表的面板,核心是把绘制逻辑和事件响应拆开:先画静态层,再处理鼠标命中区域。 官方示例工程在 2018-05-03 发布,至今累计阅读 10 628、评论 142,说明这套原生画法仍是多数自定义面板的基础方案。 实际动手时,先在 OnInit 里实例化 CCanvas 并绑定图表窗口,用 Resize 定画布尺寸;之后所有按钮坐标都按相对画布的百分比存,避免不同分辨率下错位。 高风险提示:外汇与贵金属杠杆交易可能迅速亏损本金,面板只是辅助观察工具,不构成任何方向建议。

先看清 CAppDialog 面板的骨架

在 MT5 里用 CAppDialog 搭自定义面板前,得先认全它的目录结构:从 AppWindow 的能力边界、CAppDialog 对象与继承关系,到主要常数去哪找、如何用 #undef 重定义,都是绕不开的入口。 实际开发中,面板往往要塞按钮、嵌套控件,并借助 CDialog 把 CAppDialog 纳入控件组统一管理。标准控件行为可通过重写改变,事件处理的内建宏定义也能直接读出来改。 这套结构理清后,增加一个带两个按钮的简易面板并不复杂,但涉及 EA 自动下单的外汇/贵金属操作属高风险,参数和事件逻辑建议在策略测试器里先跑通再上实盘。

◍ 为什么多数 MT5 开发者绕开图形界面

即便在 MT5 已普及的当下,多数指标与 EA 开发者仍不愿在程序里接入图形界面。核心障碍不在语法,而在标准库的面板与对话框类只给了方法的技术描述,语言参考里的示例注释繁杂,没摸清对象结构和设计思路前,很难直接动手写自己的面板。 我实际拆解过面板的处理机制,结论很直接:从 CAppDialog 派生一个最简应用,再逐步往里加按钮等控件,比硬读文档更快建立认知。本文聚焦 CAppDialog 的运行细节——最少要覆写哪几个函数、对象按什么顺序创建、用到的常量怎么改。 外汇与贵金属交易工具自带高杠杆风险,图形面板只是降低操作摩擦的手段,不改变策略本身的概率属性。开 MT5 新建一个继承 CAppDialog 的空类,先跑通窗口显示,再验证下文逐行代码。

「用 CAppDialog 搭一个最薄面板」

CAppDialog 是 MQL5 标准库里把功能相关控件归拢到一个应用程序对话框里的类,用它能在图表上快速拉出一块可视区域,把后续按钮、输入框等元件挂上去。 下面这段 EA 是能跑通的最小骨架:全局声明一个 CAppDialog 实例,OnInit 里 Create 出窗口并 Run,OnDeinit 里 Destroy 回收,OnChartEvent 把图表事件透传给面板。Create 的参数里 (0,"AppWindow",0,20,20,360,324) 表示绑定主图、左上角偏移 20 像素、宽 360 高 324。 漏掉 Destroy 这一步会出实打实的问题:每次切换周期或品种,旧元件不销毁,新元件会叠在原有之上,面板越堆越乱。外汇与贵金属交易本身高风险,这类界面 bug 虽不直接影响报价,但会干扰你盯盘和手动下单的判断。 把代码贴进 MT5 新建的 EA,编译挂到 EURUSD 图表,就能看到左上角浮出 360×324 的空面板;之后删 EA 前先确认 OnDeinit 有 Destroy,避免重挂时元件重叠。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     LearnCAppDialog.mq5 |
class=class="str">"cmt">//|     Copyright class="num">2018, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                  [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2018, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Controls\Dialog.mqh>
CAppDialog AppWindow;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA交易初始化函数                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 创建应用程序对话框
   if(!AppWindow.Create(class="num">0,"AppWindow",class="num">0,class="num">20,class="num">20,class="num">360,class="num">324))
      class="kw">return(INIT_FAILED);
class=class="str">"cmt">//--- 运行应用程序
   AppWindow.Run();
class=class="str">"cmt">//--- 成功
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 交易终止函数                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- 销毁对话框
   AppWindow.Destroy(reason);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 交易图表事件函数                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,        class=class="str">"cmt">// 事件 ID
                  const class="type">long& lparam,  class=class="str">"cmt">// 长整数类型的事件参数
                  const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度小数类型的事件参数
                  const class="type">class="kw">string& sparam) class=class="str">"cmt">// 字符串类型的事件参数
  {
   AppWindow.ChartEvent(id,lparam,dparam,sparam);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

把EA窗口挂进MT5图表事件

想让自定义对话框跟着MT5图表走,关键不在画界面,而在把窗口对象接进三个生命周期钩子。OnInit里先Create,失败直接返回INIT_FAILED,成功再Run,否则EA初始化会卡在黑屏。 下面这段是最小可跑骨架,坐标和尺寸先写死:左上角偏移(20,20),宽360高324。外汇和贵金属波动剧烈,这种独立窗口常用来脱离主图做轻量盯盘,但任何自行加载的EA都有冻结终端的概率风险,建议先在模拟盘验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 创建应用程序对话框
   if(!AppWindow.Create(class="num">0,"AppWindow",class="num">0,class="num">20,class="num">20,class="num">360,class="num">324))
      class="kw">return(INIT_FAILED);
class=class="str">"cmt">//--- 运行应用程序
   AppWindow.Run();
class=class="str">"cmt">//--- 成功
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 交易图表事件函数                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,        class=class="str">"cmt">// 事件 ID
                  const class="type">long& lparam,  class=class="str">"cmt">// 长整数类型的事件参数
                  const class="type">class="kw">double& dparam,class=class="str">"cmt">// 双精度小数类型的事件参数
                  const class="type">class="kw">string& sparam)class=class="str">"cmt">// 字符串类型的事件参数
  {
   AppWindow.ChartEvent(id,lparam,dparam,sparam);
  }
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- 销毁对话框
   AppWindow.Destroy(reason);
  }
逐行拆:第4行Create参数依次是图表ID(0=当前图)、窗口名、X、Y、宽、高;第7行Run启动消息循环;OnChartEvent里第22行把图表事件透传给AppWindow,缺这句按钮点击全无响应;OnDeinit第28行Destroy按reason清理,不写会留残留窗口句柄。 直接把这段塞进EA模板,把360和324改成你控件实际占地,编译挂上EURUSD的M5就能看到浮窗。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 创建应用程序对话框
   if(!AppWindow.Create(class="num">0,"AppWindow",class="num">0,class="num">20,class="num">20,class="num">360,class="num">324))
      class="kw">return(INIT_FAILED);
class=class="str">"cmt">//--- 运行应用程序
   AppWindow.Run();
class=class="str">"cmt">//--- 成功
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 交易图表事件函数                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,        class=class="str">"cmt">// 事件 ID
                  const class="type">long& lparam,  class=class="str">"cmt">// 长整数类型的事件参数
                  const class="type">class="kw">double& dparam,class=class="str">"cmt">// 双精度小数类型的事件参数
                  const class="type">class="kw">string& sparam)class=class="str">"cmt">// 字符串类型的事件参数
  {
   AppWindow.ChartEvent(id,lparam,dparam,sparam);
  }
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- 销毁对话框
   AppWindow.Destroy(reason);
  }

◍ 面板能接住哪些交互事件

基于 CAppDialog 搭出来的面板,理论上能处理的事件都定义在数据目录 MQL5\Include\Controls\Defines.mqh 的 Events 模块里。从单击、双击到编辑起止、焦点切换、拖曳三段(起/中/止),再到显示隐藏,基本覆盖了 GUI 能碰到的用户动作。 具体事件编号从 0 到 14 是常规控件交互,100 留给整个应用关闭。下面这段宏定义就是可直接抄进自己 include 里的清单。 官方示例里,CRadioGroup 演示了 ON_CHANGE 怎么用,CScrollV 则覆盖了 ON_SCROLL_INC 和 ON_SCROLL_DEC。想验证的话,开 MT5 进 Include\Controls 翻 Defines.mqh,对照跑一遍这两个例子最直观。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define ON_CLICK(class="num">0)   class=class="str">"cmt">// 点击控件的事件
class="macro">#define ON_DBL_CLICK(class="num">1)   class=class="str">"cmt">// 双击控件的事件
class="macro">#define ON_SHOW(class="num">2)   class=class="str">"cmt">// 显示控件的事件
class="macro">#define ON_HIDE(class="num">3)   class=class="str">"cmt">// 隐藏控件的事件
class="macro">#define ON_CHANGE(class="num">4)   class=class="str">"cmt">// 修改控件的事件
class="macro">#define ON_START_EDIT(class="num">5)   class=class="str">"cmt">// 开始编辑的事件
class="macro">#define ON_END_EDIT(class="num">6)   class=class="str">"cmt">// 编辑结束的事件
class="macro">#define ON_SCROLL_INC(class="num">7)   class=class="str">"cmt">// 滚动条增加的事件
class="macro">#define ON_SCROLL_DEC(class="num">8)   class=class="str">"cmt">// 滚动条减小的事件
class="macro">#define ON_MOUSE_FOCUS_SET(class="num">9)   class=class="str">"cmt">//  "鼠标光标进入控件" 的事件
class="macro">#define ON_MOUSE_FOCUS_KILL(class="num">10)  class=class="str">"cmt">// "鼠标光标退出控件"的事件
class="macro">#define ON_DRAG_START(class="num">11)  class=class="str">"cmt">// "开始拖曳控件"的事件
class="macro">#define ON_DRAG_PROCESS(class="num">12)  class=class="str">"cmt">// "正在拖曳控件"的事件
class="macro">#define ON_DRAG_END(class="num">13)  class=class="str">"cmt">// "结束拖曳控件"的事件
class="macro">#define ON_BRING_TO_TOP(class="num">14)  class=class="str">"cmt">// "鼠标事件优先级提高"的事件
class="macro">#define ON_APP_CLOSE(class="num">100) class=class="str">"cmt">// "关闭应用程序"的事件

常见问题

先用 CAppDialog 搭一个最薄面板,只放一个容器和关闭逻辑,跑通后再往里加控件,避免一上来就堆复杂布局。
多半是没把面板接到图表事件循环,需要在 OnChartEvent 里把事件转发给 CAppDialog 的 OnEvent 方法才能接住点击。
可以,小布能直接解析面板类的继承关系和事件绑定位置,标出哪段管渲染、哪段管交互,省去你手动翻源码。
能接住鼠标点击、滚轮、图表拖拽以及按键输入等事件,具体看你重写了哪些虚函数,没重写的默认不处理。
不用,多数开发者绕开图形界面是因为不熟悉控件类,直接用 CAppDialog 及其子控件就能拼出多层布局,不必手绘。