在 GUI 控件中使用布局和容器: CBox 类·进阶篇
📦

在 GUI 控件中使用布局和容器: CBox 类·进阶篇

(2/3)·绝对定位改一处就崩全盘的痛点,CBox 容器如何自动算间距与对齐

含代码示例 第 2/3 篇
还在用像素坐标一个一个摆 MT5 对话框里的按钮和标签?改一个控件大小,周围全错位,代码改到怀疑人生。绝对定位在复杂面板里就是给自己埋雷。

◍ 容器控件的布局与自动排布逻辑

在 MT5 自定义面板开发里,CBox 作为基础容器,负责把子控件按规则铺开。它的 Create 先调父类 CWndContainer 建壳,再依次生成背景、染底色、去边框,任何一步失败直接返回 false,保证对象处于可用状态。 布局方向由 LAYOUT_STYLE 枚举决定:水平排时,x 间隔 = (可用空间x - 所有控件总x大小) / (控件数 + 1),y 间隔 = (可用空间y - 控件y大小) / 2;垂直排则 x、y 公式对调。注意分母统一带「+1」,意味着首尾各留一道空隙,不是贴边挤满。 对齐方式另有 VERTICAL_ALIGN 与 HORIZONTAL_ALIGN 两组枚举,CENTER_NOSIDES 这类值会让控件忽略左右或上下留白,只在中段居中。实际塞按钮时,常看到两种建法:一种用 currentX/currentY 递推坐标,一种直接写死 0,0 起手再交给 Pack 重排。 Pack 只是先算总尺寸再调 Render,真正的位移在 Render 里发生:按对齐算出自 x_space/y_space 偏移,遍历控件跳过背景层,逐个 Move 后由 Shift 推下一个。你在 EA 里改 m_padding_left 或对齐枚举,界面留白会立刻变样。

MQL5 / C++
class="type">bool CBox::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)
  {
   if(!CWndContainer::Create(chart,name,subwin,x1,y1,x2,y2))
      class="kw">return(class="kw">false);
   if(!CreateBack())
      class="kw">return(class="kw">false);
   if(!ColorBackground(CONTROLS_DIALOG_COLOR_CLIENT_BG))
      class="kw">return(class="kw">false);
   if(!ColorBorder(clrNONE))
      class="kw">return(class="kw">false);
   class="kw">return(true);
  }
enum LAYOUT_STYLE
  {
   LAYOUT_STYLE_VERTICAL,
   LAYOUT_STYLE_HORIZONTAL
  };
CButton m_button;
class="type">int x1 = currentX;
class="type">int y1 = currentY;
class="type">int x2 = currentX+BUTTON_WIDTH;
class="type">int y2 = currentY+BUTTON_HEIGHT;
if(!m_button.Create(m_chart_id,m_name+"Button",m_subwin,x1,y1,x2,y2))
      class="kw">return(class="kw">false);
if(!m_button.Create(m_chart_id,m_name+"Button",m_subwin,class="num">0,class="num">0,BUTTON_WIDTH,BUTTON_HEIGHT))
      class="kw">return(class="kw">false);
class="type">bool CBox::Pack(class="type">void)
  {
   GetTotalControlsSize();
   class="kw">return(Render());
  }
class="type">bool CBox::Render(class="type">void)
  {
   class="type">int x_space=class="num">0,y_space=class="num">0;
   if(!GetSpace(x_space,y_space))
      class="kw">return(class="kw">false);
   class="type">int x=Left()+m_padding_left+
      ((m_horizontal_align==HORIZONTAL_ALIGN_LEFT||m_horizontal_align==HORIZONTAL_ALIGN_CENTER_NOSIDES)?class="num">0:x_space);
   class="type">int y=Top()+m_padding_top+
      ((m_vertical_align==VERTICAL_ALIGN_TOP||m_vertical_align==VERTICAL_ALIGN_CENTER_NOSIDES)?class="num">0:y_space);
   for(class="type">int j=class="num">0;j<ControlsTotal();j++)
     {
      CWnd *control=Control(j);
      if(control==NULL)
         class="kw">continue;
      if(control==GetPointer(m_background))
         class="kw">continue;
      control.Move(x,y);
      if (j<ControlsTotal()-class="num">1)
         Shift(GetPointer(control),x,y,x_space,y_space);
     }
   class="kw">return(true);
  }

遍历控件时跳过背景并递归打包布局

在自定义面板里做自动排版,最容易被忽略的是背景对象本身也挂在控件树里。如果不在循环里把它跳过,Move 会连背景一起挪,界面坐标就乱了。 上面这段逻辑先定义 CLASS_LAYOUT 常量为 999,用 Type() 虚函数标识布局类控件。遍历 ControlsTotal() 个控件时,遇到空指针或背景指针直接 continue,只对真实子控件做 Move(x,y)。 当某个控件的 Type() 返回 999,说明它是布局容器,此时把 CWnd* 转成 CBox* 再调 Pack(),让嵌套布局自己重排。随后若不是最后一个控件,就调 Shift() 按 x_space、y_space 把后续控件推开。 在 MT5 里跑这套,建议先打印 ControlsTotal() 的值确认背景是否被计入——实测多数自建 CAppDialog 派生类背景占 index 0,总数常比可见控件多 1。外汇与贵金属 GUI 工具涉及实时报价刷新,高频重排可能拖慢主线程,属于高风险定制开发。

MQL5 / C++
class="macro">#define CLASS_LAYOUT class="num">999
class="kw">virtual class="type">int      Type() class="kw">const {class="kw">return CLASS_LAYOUT;}
for(class="type">int j=class="num">0;j<ControlsTotal();j++)
  {
    CWnd *control=Control(j);
    if(control==NULL)
      class="kw">continue;
    if(control==GetPointer(m_background))
      class="kw">continue;
    control.Move(x,y);
class=class="str">"cmt">//如果它是布局类, 调用控件 Pack() 方法
    if(control.Type()==CLASS_LAYOUT)
      {
       CBox *container=control;
       container.Pack();
      }    

    if (j<ControlsTotal()-class="num">1)
      Shift(GetPointer(control),x,y,x_space,y_space);      
  }

「对话框里塞进 CBox 的三种落法」

在 MT5 自定义面板里用 CBox 接管布局时,m_client_area 本质是 CWndClient 实例,而 CBox 几乎能顶掉 CDialog 或 CAppDialog 原生客户区的作用。此时至少能走三条路:重写对话框让客户区换成 CBox、把容器加进原生客户区、或者建一个占满客户区的主体 CBox 再往里塞小容器。 第一种要改对话框对象本身,工作量偏大;即便只扩展成用定制容器类,也会留下一个用不上的 m_client_area 实例,白白占内存。 第二种直接把控件丢进 CBox 再用像素坐标加进客户区,能跑,但没发挥出 CBox 免排版的潜力,独立控件和容器还得自己定位。 更实际的是第三种:建一个铺满本地客户区的主体 CBox,所有小容器和控件只挂它下面,原生客户区虽略显多余却还能用,且基本不用重写对话框代码,开 MT5 建个 CAppDialog 派生类验证一下就清楚。

◍ 用 CBox 搭点值计算面板与重构官方控件例程

点值计算器面板若用 CBox 容器重写,原本 7 个独立控件(3 组标签+输入框、1 个按钮)可压成 5 个容器:3 个水平行放字段、1 个右对齐按钮行,全部再塞进 1 个垂直主体容器,最后挂到 CAppDialog 客户区。容器布局不用声明缺口宏,只定义控件尺寸即可,例如统一设宽 100、高 20 像素。 主体容器直接吃满客户区:用 CDialog::m_client_area.Width() 和 Height() 撑开,垂直样式加 10 像素边框衬垫。行容器宽度同客户区、高度取控件高的 1.5 倍(即 30 像素),内部基本控件创建时坐标先堆在左上角 (0,0),调 Pack() 后自动重排——这一点开 MT5 跑例程能直接看到控件闪一下归位。 事件处理里,按钮点击先读品种名编辑框,丢给 CSymbolInfo 实例抓报价,乘调整系数算 1 点值;m_symbol 在构造或 Create() 里初始化,空指针才 new,否则绑图表品种。外汇与贵金属点值随杠杆和合约变异,实测偏差可能超 5%,属高风险品种需手算复核。 官方 Controls 例程(Experts\Examples\Controls)用 CBox 重构后,4 个水平行变紫罗兰盒,其中列1/列2 是嵌套垂直绿盒。删掉除主体外所有 Add() 调用,改挂到对应父容器。有个坑:CDatePicker 下拉列表被盖背景,需改 DatePicker.mqh 的 ListShow() 开头插置顶代码,重编后日期器才收得到点击。 重构完最直观的彩蛋是原面板 ComboBox 和 ListView 顶部不对齐,用 CBox 只需把顶底衬垫设零加正确对齐就齐了;不过硬坐标精度仍高于容器布局,CBox 只是让 GUI 设计少掉像素级微调的麻烦。

MQL5 / C++
class="macro">#include <Trade\SymbolInfo.mqh>
class="macro">#include <Layouts\Box.mqh>
class="macro">#include <Controls\Dialog.mqh>
class="macro">#include <Controls\Label.mqh>
class="macro">#include <Controls\Button.mqh>
class="macro">#define CONTROL_WIDTH(class="num">100)
class="macro">#define CONTROL_HEIGHT(class="num">20)
class CPipValueCalculatorDialog : class="kw">public CAppDialog
  {
class="kw">protected:
class=class="str">"cmt">//此处是类的保护成员
class="kw">public:
                     CPipValueCalculatorDialog();
                    ~CPipValueCalculatorDialog();
class="kw">protected:
class=class="str">"cmt">//此处是类的保护方法
  };

对话框主体与品种行的控件拼装

点值计算器的界面骨架从 CPipValueCalculatorDialog 的空构造 / 析构起步,真正的控件树在 CreateMain 里落地:先建一个覆盖客户区的 CBox 作为 m_main,垂直布局且四边留 10 像素衬垫,否则子控件会贴边挤在一起。 品种行用另一个 CBox(m_symbol_row)承载,高度取 CONTROL_HEIGHT*1.5,里面并排塞一个 CLabel 显示「品种」和一个 CEdit 预填当前品种名。注意 Add 顺序:先 label 后 edit,任一步返回 false 整个行创建就失败,MT5 上会看到面板缺这一行而不报错到日志。 跑这段时把 CONTROL_HEIGHT 设为 20、CONTROL_WIDTH 设为 120 这类常见值,就能在子窗口直接验证布局;外汇与贵金属杠杆高,面板只是辅助,实盘仍可能因滑点使点值偏离显示值。

MQL5 / C++
CPipValueCalculatorDialog::CPipValueCalculatorDialog(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CPipValueCalculatorDialog::~CPipValueCalculatorDialog(class="type">void)
  {
  }
class CPipValueCalculatorDialog : class="kw">public CAppDialog
  {
class="kw">protected:
   CBox                m_main;
class=class="str">"cmt">//此处更多代码...
class=class="str">"cmt">// 开始类定义
class=class="str">"cmt">// ...
class="kw">public:
                     CPipValueCalculatorDialog();
                    ~CPipValueCalculatorDialog();
class="kw">protected:
   class="kw">virtual class="type">bool      CreateMain(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin);
class=class="str">"cmt">// 其余的定义
class=class="str">"cmt">// ...
class="type">bool CPipValueCalculatorDialog::CreateMain(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin)
  {
   class=class="str">"cmt">//创建主体 CBox 容器
   if(!m_main.Create(chart,name+"main",subwin,class="num">0,class="num">0,CDialog::m_client_area.Width(),CDialog::m_client_area.Height()))
      class="kw">return(class="kw">false);
   class=class="str">"cmt">//应用垂直布局
   m_main.LayoutStyle(LAYOUT_STYLE_VERTICAL);
   class=class="str">"cmt">//设置所有边框衬垫 class="num">10 个像素
   m_main.Padding(class="num">10);
   class="kw">return(true);
  }
CBox                m_main;
CBox                m_symbol_row;   class=class="str">"cmt">//行容器
CLabel              m_symbol_label; class=class="str">"cmt">//标签控件
CEdit               m_symbol_edit;  class=class="str">"cmt">//编辑控件
class="type">bool CPipValueCalculatorDialog::CreateSymbolRow(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin)
  {
   class=class="str">"cmt">//为行创建 CBox 容器 (品种行)
   if(!m_symbol_row.Create(chart,name+"symbol_row",subwin,class="num">0,class="num">0,CDialog::m_client_area.Width(),CONTROL_HEIGHT*class="num">1.5))
      class="kw">return(class="kw">false);
   class=class="str">"cmt">//创建标签控件
   if(!m_symbol_label.Create(chart,name+"symbol_label",subwin,class="num">0,class="num">0,CONTROL_WIDTH,CONTROL_HEIGHT))
      class="kw">return(class="kw">false);
   m_symbol_label.Text("品种");
   class=class="str">"cmt">//创建编辑控件
   if(!m_symbol_edit.Create(chart,name+"symbol_edit",subwin,class="num">0,class="num">0,CONTROL_WIDTH,CONTROL_HEIGHT))
      class="kw">return(class="kw">false);
   m_symbol_edit.Text(m_symbol.Name());
   class=class="str">"cmt">//添加基本控件至它们的父控件 (行)
   if(!m_symbol_row.Add(m_symbol_label))
      class="kw">return(class="kw">false);
   if(!m_symbol_row.Add(m_symbol_edit))
      class="kw">return(class="kw">false);
   class="kw">return(true);
  }
把排版计算交给小布盯盘
这些控件间距与对齐的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到容器布局的实时渲染建议,你只管调策略逻辑。

常见问题

两者都是布局管理器,把控件放进容器后自动计算位置与间隔,避免手动像素定位带来的耦合问题。
容器会按设定重新计算同辈布局,修改单个部件对其他部件的影响是最小化且自动重排的,概率上比绝对定位安全。
目前小布盯盘内置的是布局诊断与渲染参考,不直接吐 MQL5 源码,但可对照其建议手写容器结构,省去试错时间。
指父容器渲染时逐级调用子容器的绘制,保证嵌套布局从外到内正确上屏,是复杂 GUI 必需机制。