自定义图形控件。第 3 部分 表单·进阶篇
「往表单里塞控件的实操顺序」
在 MT5 里写自定义 GUI,控件不是随手 new 出来就能用,得按生命周期挂进表单类。若你要在表单外部调用控件方法,就把控件类放在 public 区;只在表单内折腾就放 protected 区,少暴露接口。 恢复值的变量是可选但实用的:用 Cancel/Apply 双按钮的表单,给每个控件配一个备份变量,存打开瞬间的值,用户乱改后点 Cancel 能一键还原。 初始化阶段有两件事必须在 OnInitEvent() 里做:对所有控件调 Init(),然后按需载入上次保存的数据(重启 EA、终端、电脑后还想保留状态才做,保存动作在后面第 12 步)。 首次打开表单时看到的样子由“设定默认值”决定;如果第 4 步载入了数据就以载入值为准,列表项、菜单项也在这步加。随后 OnShowEvent(int aLeft, int aTop) 里调带参 Show(),控件坐标跟着表单工作区左上角走。 隐藏走 OnHideEvent() 里的 Hide(),子窗口显示才需要 OnWindowChangeEvent() 里调 SetSubWindow(int aSubWindow)。事件分发统一在 EventsHandler() 调各控件 Event(),控件间联动也在这处理。 Apply 按钮的逻辑在 OnApplyEvent():先校验值,出错就提示并返回 false 让表单不关;全对返回 true 关闭,同时把数据写进文件/全局变量/图表物件。Cancel 对应的 OnCancelEvent() 返回 false 时表单关不掉,想关必须返回 true。 随附的 eIncGUI_v3_Test_Form.mq5 可直接开来看:启动弹主表单(图 7),无关闭按钮,关了就得重启 EA 才能再来。主菜单能开两种子表单——1 型带 Cancel/Apply 和一个输入框,2 型带 Close、CLabel 和几个 CButton,两者关闭都要 MessageBox() 确认。外汇/贵金属 EA 挂 GUI 前先认清这是高风险环境,参数误触可能直接发单。
两级菜单的底层改动与搭法
MT5 自定义 GUI 里菜单只建议做两级:水平条用 CHMenu、选项卡用 CVMenu。再往下叠层级理论上能写,但得自己搞树状结构,工作量够单独开一篇文章,常规面板没必要碰。 这一版对两个类都动了刀。CVMenu 修掉了点 Label 时返回坐标错乱的坑,LastClickedX/Y/Quarter 现在和文本框返回值一致;CHMenu 也同步改了这四个方法加 LastClickedW。新增的 SolvePosX()、SolvePosY() 直接吃显示对象宽高、吐出 X/Y 坐标,老那套 LastClicked* 可以退役了。 两个类都加了 LastClickedName1()、LastClickedName2(),分别拿文本框名和对勾标签名。CVMenu 另配 ToggleNameAdd() 与 ToggleNamesClear():往切换名单里塞了名,除菜单自身和名单内对象,任意图表事件都会自动收起菜单;清掉名单就回到普通模式。 实操逻辑是:水平项被点,用 LastClickedName1/2 取对象名喂给垂直菜单的 ToggleNameAdd,之后垂直菜单只在自身事件和那个水平项事件下存活,点别处就藏。选了垂直项就关菜单干正事,再点同一水平项则收起。示例里主菜单 3 项配 3 个垂直菜单,m_vm1 初始化宽度 70、行高 10,挂了 Type-1/Type-2 两项。 下面这段是从表单子类抠出来的核心声明与事件处理,OnInitEvent 里 Init 加 AddItem,OnHide 全 Hide,OnShow 只亮水平条,EventsHandler 里 m_event0==0 时靠 SolvePosLeft/Top 算位置弹 m_vm1。外汇贵金属面板开发属高风险定制,参数错了界面直接不渲染,建议开 MT5 跑通再改。
class CForm: class="kw">public CFormBase{ class="kw">public: CHMenu m_hm; CVMenu m_vm1; CVMenu m_vm2; CVMenu m_vm3; class=class="str">"cmt">// 水平菜单 m_hm.Init(m_Name+"_HM",m_Width,class="num">2); class=class="str">"cmt">// 加入水平菜单项目 m_hm.AddItem("Form types"); m_hm.AddItem("Item-class="num">2"); m_hm.AddItem("Item-class="num">3"); class=class="str">"cmt">// 垂直菜单 class="num">1 m_vm1.Init(m_Name+"_VM1",class="num">70,class="num">10); class=class="str">"cmt">// 加入项目至垂直菜单 class="num">1 m_vm1.AddItem("Type-class="num">1"); m_vm1.AddItem("Type-class="num">2"); class=class="str">"cmt">// 垂直菜单 class="num">2 m_vm2.Init(m_Name+"_VM2",class="num">70,class="num">3); class=class="str">"cmt">// 加入项目至垂直菜单 class="num">2 m_vm2.AddItem("Item-class="num">2-class="num">1"); m_vm2.AddItem("Item-class="num">2-class="num">2"); m_vm2.AddItem("Item-class="num">2-class="num">3"); m_vm2.AddItem("Item-class="num">2-class="num">4"); m_vm2.AddItem("Item-class="num">2-class="num">5"); class=class="str">"cmt">// 垂直菜单 class="num">3 m_vm3.Init(m_Name+"_VM3",class="num">70,class="num">3); class=class="str">"cmt">// 加入项目至垂直菜单 class="num">3 m_vm3.AddItem("Item-class="num">3-class="num">1"); m_vm3.AddItem("Item-class="num">3-class="num">2"); m_vm3.AddItem("Item-class="num">3-class="num">3"); m_vm3.AddItem("Item-class="num">3-class="num">4"); m_vm3.AddItem("Item-class="num">3-class="num">5"); class="type">void OnHideEvent() { m_hm.Hide(); m_vm1.Hide(); m_vm2.Hide(); m_vm3.Hide(); } class="type">void OnShowEvent(class="type">int aLeft,class="type">int aTop) { m_hm.Show(aLeft,aTop); } class="type">void EventsHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam) { class="type">int m_event0=m_hm.Event(id,lparam,dparam,sparam); class="type">int m_event1=m_vm1.Event(id,lparam,dparam,sparam); class="type">int m_event2=m_vm2.Event(id,lparam,dparam,sparam); class="type">int m_event3=m_vm3.Event(id,lparam,dparam,sparam); if(m_event0==class="num">0) { class=class="str">"cmt">// 点击项目 class="num">0 if(m_vm1.Visible()) { m_vm1.Hide(); class=class="str">"cmt">// 如果垂直菜单打开, 关闭它 } else{ class=class="str">"cmt">// 如果垂直菜单关闭 m_vm1.ToggleNamesClear(); class=class="str">"cmt">// 清除"切换"的名字列表 class=class="str">"cmt">// 为表单的项目加入图形对象名 class=class="str">"cmt">// 水平菜单的最后事件 class=class="str">"cmt">// 水平菜单 m_vm1.ToggleNameAdd(m_hm.LastClickedName1()); m_vm1.ToggleNameAdd(m_hm.LastClickedName2()); class=class="str">"cmt">// 显示垂直菜单 m_vm1.Show(m_hm.SolvePosLeft(m_vm1.Width()),m_hm.SolvePosTop(m_vm1.Height())); } } if(m_event1>=class="num">0) { class=class="str">"cmt">// 垂直菜单事件 class="num">1 m_vm1.Hide(); class=class="str">"cmt">// 隐藏菜单 if(m_event1==class="num">0) { class=class="str">"cmt">// 点击项目 class="num">0
◍ 事件对象点击的分支处理
在 MT5 的 GUI 事件回调里,区分不同控件的交互动作靠的是事件编号比对。上面这段残缺片段里,m_event1==1 代表用户点击了菜单或列表里的「项目 1」,进入对应分支执行后续逻辑。 实际写 EA 面板时,这种 if(m_event1==N) 的写法最直观,但控件一多就容易嵌套过深。建议把每个事件编号映射到枚举或字符串,方便后期维护。 打开 MT5 的 MetaEditor,新建一个 EA 并在 OnChartEvent 里打印 m_event1 的数值,你就能在图表上点不同对象时,从日志看到具体触发的是哪个编号。外汇与贵金属杠杆高,实盘前务必在策略测试器用历史数据验证逻辑。
class=class="str">"cmt">//... } if(m_event1==class="num">1) { class=class="str">"cmt">// 点击项目 class="num">1 class=class="str">"cmt">//... } }
「复制一份模板就能起一个表单」
系列走到这里,图形界面创建需要的基础方法已经攒得相当完整:说明式框架、可最小化最大化移动的表单、控件随表单显隐同步,这些现在都算标配。所有控件和表单共用一套配色与设计,换肤只改一处就行。 起一个新表单的实际步骤很固定。先 include 进 IncGUI_v3.mqh,再把 CFormTemplate 复制成带独立名字的子类并声明;表单属性丢进子类的 MainProperties() 里设。 EA 侧只接三个钩子:OnInit() 调 Init()、OnDeinit() 调 Hide()、OnChartEvent() 调 Event(),要默认显示就在 OnInit() 里补一句 Show()。控件则在子类里声明,由 OnInitEvent()、OnShowEvent()、OnHideEvent()、EventHandler() 分别驱动 Init/Show/Hide/Event。照这套抄,半小时能立起一个可拖动的面板。
把工具请下神坛
这套 GUI 库落地时只需三个文件就位:IncGUI_v3.mqh 丢进终端数据目录的 MQL5/Include,eIncGUI_v3_Test_Form.mq5 放 MQL5/Experts,随附的 CHM 文档(472.3 KB 压缩包内)早年曾被读者反馈丢失,后来作者补回了。 真正跑通的办法不是反复读文档,而是直接把测试 EA 挂上图表,看表单怎么被实例化。作者原话也承认,把全部类塞进一个窗体对象数组的写法“便利性值得怀疑”,但胜在简单直给。 外汇与贵金属图形面板的高频重绘本身就有卡顿风险,别神话任何第三方库。能改两行参数、让面板响应你的下单逻辑,比追新版本号实在。