在 GUI 控件中使用布局和容器: CBox 类·综合运用
📦

在 GUI 控件中使用布局和容器: CBox 类·综合运用

(3/3)· 从类结构到对话框落地再到两个例程,一篇看清布局管理器的实战边界与取舍

实战向进阶 第 3/3 篇
还在用像素偏移一个一个摆 MT5 面板控件?改一个按钮宽度,半个界面跟着错位重排。绝对定位的代码改起来像拆炸弹,动一处崩一片。

面板生命周期的创建与销毁链路

在 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()。

MQL5 / C++
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 信号按钮。

MQL5 / C++
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 策略测试器外的独立图表先验证。 工具只是杠杆,别神化现成库。真要稳,得自己读源码、补漏洞、控风险。

MQL5 / C++
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();
    }
};
把重复排版交给小布盯盘
这些布局诊断与控件依赖梳理,小布盯盘的 AIGC 已内置,打开对应品种页即可看到自动归类的面板结构,你只管决策要不要重构。

常见问题

倾向会有边际开销,层级越深遍历次数越多,但常规交易面板嵌套三五层通常无明显卡顿,超深结构建议拆分父容器。
不会自动重置,CBox 按既定布局样式重算间隔与位置,前提是你没有在外部硬写绝对坐标覆盖容器计算。
不能直接塞,需要去掉像素定位逻辑,改为向容器 add 控件并声明布局样式,否则两套定位会互相打架。
可以,小布盯盘的 AIGC 会扫描控件创建代码特征,标记依赖偏移量的部件,并给出改为容器布局的参考点。
优先看衬垫与字体度量差异,CBox 按分配空间算位置,不同品种标签长度变化可能撑破同辈容器,需要设最小宽或换行策略。