在 GUI 控件中使用布局和容器: CBox 类·进阶篇
(2/3)·绝对定位改一处就崩全盘的痛点,CBox 容器如何自动算间距与对齐
◍ 容器控件的布局与自动排布逻辑
在 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 或对齐枚举,界面留白会立刻变样。
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 工具涉及实时报价刷新,高频重排可能拖慢主线程,属于高风险定制开发。
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 设计少掉像素级微调的麻烦。
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 这类常见值,就能在子窗口直接验证布局;外汇与贵金属杠杆高,面板只是辅助,实盘仍可能因滑点使点值偏离显示值。
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); }