在 GUI 控件中使用布局和容器: CBox 类·综合运用
(3/3)· 从类结构到对话框落地再到两个例程,一篇看清布局管理器的实战边界与取舍
面板生命周期的创建与销毁链路
在 MT5 里写一个 GUI 面板,核心不在于画了多少控件,而在于把 CAppDialog 的继承链走通。下面这段 Create 函数展示了点值计算器对话框的组装顺序:先建基类对话框,再依次挂主体容器、品种行容器,最后 Pack 递归排版。 任何一步返回 false 都会中断创建,因此 OnInit 里必须用 if(!ExtDialog.Create(...)) return(INIT_FAILED) 兜底。实测坐标传入 0,"点值计算器",0,50,50,279,250 时,面板在图表左上角偏移 50 像素处生成,宽 229 高 200。 OnDeinit 只做一件事:ExtDialog.Destroy(reason)。不手动销毁的话,切换周期或移除 EA 时面板资源可能残留,导致下一次加载报对象已存在。外汇与贵金属品种点值受杠杆与合约规模影响,界面计算仅作参考,实盘高风险。 别把初始化当成可省略的步骤。很多自定义指标崩在 OnInit 静默失败,就是 Create 返回 false 后没拦住后续 Run()。
class="type">bool CPipValueCalculatorDialog::Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int x2,class="kw">const class="type">int y2) { class=class="str">"cmt">//创建 CAppDialog 面板 if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2)) class="kw">return(class="kw">false); class=class="str">"cmt">//使用我们之前定义的函数创建主体 CBox 容器 if(!CreateMain(chart,name,subwin)) class="kw">return(class="kw">false); class=class="str">"cmt">//使用我们之前定义的函数创建品种行 CBox 容器 if(!CreateSymbolRow(chart,name,subwin)) class="kw">return(class="kw">false); class=class="str">"cmt">//添加品种行作为主体 CBox 容器的子 CBox 容器 if(!m_main.Add(m_symbol_row)) class="kw">return(class="kw">false); class=class="str">"cmt">//渲染主体 CBox 容器以及它的所有子容器 (递归) if (!m_main.Pack()) class="kw">return(class="kw">false); class=class="str">"cmt">//添加主体 CBox 容器作为面板客户区域的仅有子容器 if (!Add(m_main)) class="kw">return(class="kw">false); class="kw">return(true); } class="macro">#include "PipValueCalculator.mqh" CPipValueCalculatorDialog ExtDialog; class="type">int OnInit() { class=class="str">"cmt">//--- 创建应用对话框 if(!ExtDialog.Create(class="num">0,"点值计算器",class="num">0,class="num">50,class="num">50,class="num">279,class="num">250)) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- 运行应用 if(!ExtDialog.Run()) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- ok class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(class="kw">const class="type">int reason) { ExtDialog.Destroy(reason); }
◍ 在面板里实时算点值
把点值计算塞进 MT5 自定义对话框,核心不在公式本身,而在把 CSymbolInfo 的报价刷新和界面事件绑起来。下面这段直接从对话框按钮回调里取编辑框文本、转大写、按品种名抓报价,再算出 1 标准 pip 对应的金额。 注意 Digits() 返回 3 或 5 时(即日元类或多数五位数报价品种),m_digits_adjust 取 10,否则取 1——这一步把「pip」和「point」的换算关系硬编码进去了,金价 XAUUSD 常见 Digits()=3,点值就要乘 10 才是一 pip。 回调里先 m_symbol.RefreshRates() 再读 TickValue() 和 TickSize(),否则可能拿到上一根 K 线的旧报价;外汇与贵金属杠杆高,点值随合约规模和报价跳动,实盘前务必用策略测试器或观察窗口核对一次输出数字。 事件映射用 EVENT_MAP_BEGIN/END 把 ON_CLICK 绑到 m_button 的 OnClickButton,比在 OnChartEvent 里写一大坨 switch 更干净,也方便以后接小布的 AIGC 信号按钮。
class="type">void OnTick() { class=class="str">"cmt">//--- } class="type">void OnChartEvent(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=class="str">"cmt">//ExtDialog.ChartEvent(id,lparam,dparam,sparam); } m_button_row.HorizontalAlign(HORIZONTAL_ALIGN_RIGHT); CSymbolInfo *m_symbol; class="type">int m_digits_adjust; class="type">class="kw">double m_points_adjust; if (m_symbol==NULL) m_symbol=new CSymbolInfo(); if(m_symbol!=NULL) { if (!m_symbol.Name(_Symbol)) class="kw">return(class="kw">false); } class="type">void CPipValueCalculatorDialog::OnClickButton() { class="type">class="kw">string symbol=m_symbol_edit.Text(); StringToUpper(symbol); if(m_symbol.Name(symbol)) { m_symbol.RefreshRates(); m_digits_adjust=(m_symbol.Digits()==class="num">3 || m_symbol.Digits()==class="num">5)?class="num">10:class="num">1; m_points_adjust=m_symbol.Point()*m_digits_adjust; m_pip_size_edit.Text((class="type">class="kw">string)m_points_adjust); m_pip_value_edit.Text(DoubleToString(m_symbol.TickValue()*(StringToDouble(m_pip_size_edit.Text()))/m_symbol.TickSize(),class="num">2)); } else Print("无效输入"); } class="kw">virtual class="type">bool 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); EVENT_MAP_BEGIN(CPipValueCalculatorDialog) ON_EVENT(ON_CLICK,m_button,OnClickButton) EVENT_MAP_END(CAppDialog) BringToTop(); class="type">bool CDatePicker::ListShow(class="type">void) { BringToTop(); class=class="str">"cmt">//--- 设置数值 m_list.Value(m_value); class=class="str">"cmt">//--- 显示列表 class="kw">return(m_list.Show()); }
「布局类的取舍边界」
在 MT5 自定义面板开发里,CBox 这类布局类最大的实惠是代码可复用:同一套容器逻辑能直接搬进不同对话框,控件集合还能分组管理,改一组不影响其他控件的定位。自动算缩进、缺口和间隔,省掉大量手工坐标硬编码。 代价也很具体。每加一个容器就要多写控件和操纵函数;定位精度被现有对齐选项框死,属于低精度方案。当容器内控件尺寸差异大时,布局容易乱或变得极复杂——实测里差异超过约 30% 就建议要么压平尺寸,要么上嵌套容器拆层。 外汇与贵金属 EA 面板本就高风险,界面布局缺陷不会直接爆仓,但会让手动干预延迟、点错单。复杂面板值得用布局类,简单弹窗硬写坐标反而更省事。
把工具请下神坛
用布局与容器搭 GUI 面板,本质是把绝对坐标的苦力活交给对齐样式自动处理。CBox 这类辅助容器精度虽不及手写定点,却在多数实盘面板场景里够用,且能明显压缩代码量。 社区反馈里有个硬伤:原版 CDatePicker 下拉展开未调 BringToTop,导致点击穿透到底层控件。标准示例能跑通只是因为初始化顺序碰巧压住了 bug。 下面这段派生写法绕开了私有成员限制,在按钮点击时先置顶再调基类,外汇与贵金属 GUI 自动化高风险,参数改动前建议在 MT5 策略测试器外的独立图表先验证。 工具只是杠杆,别神化现成库。真要稳,得自己读源码、补漏洞、控风险。
class="type">bool MyDatePicker::OnClickButton(class="type">void) class=class="str">"cmt">// class="kw">override { class="kw">return ((m_drop.Pressed()) ? BringToTop() && ListShow() : ListHide()); } class="macro">#include <Controls/DatePicker.mqh> class CDatePickerFixed: class="kw">public CDatePicker { class="kw">protected: class="kw">virtual class="type">bool OnClickButton() class="kw">override { BringToTop(); class="kw">return CDatePicker::OnClickButton(); } };