OLAP 交易可视化基础:用自适应 GUI 摆脱硬编码坐标(基础篇)
📊

OLAP 交易可视化基础:用自适应 GUI 摆脱硬编码坐标(基础篇)

(1/3)·标准库对话框尺寸写死、标签重叠,大型超立方体根本没法看,这篇先铺 GUI 底层

进阶 第 1/3 篇
很多交易者把 OLAP 结果直接丢进专家日志当文本读,超立方体一大就完全没法肉眼比对维度。还有人硬改标准库控件坐标,窗口一拉伸标签全叠在一起。可视化没做对,多维分析等于白做。

给 OLAP 结果找个能自适应的 GUI 容器

上一篇我们用 MQL 类把 OLAP 多维数据跑通了,但结果只丢在专家日志里当纯文本,肉眼根本没法交互。要真正看清楚超立方体里的维度关系,得在 MT5 里画出能随窗口拉伸的界面,而不是写死坐标的弹窗。 翻过几个 MQL GUI 库就会发现一个通病:控件位置全是 x/y 硬编码常量,标准库对话框更是固定尺寸。面对大型 OLAP 超立方体,用户想把窗口最大化、让轴标签不重叠都做不到。mql5 社区里也有独立 GUI 方案,要么只管布局不管缩放,要么接入要读一大堆非标准文档,性价比偏低。 Enrico Lambino 写的两篇布局容器文章给了个轻量思路:用 CBox 做水平/垂直嵌套布局,用 CGrid 做表格布局,都能挂标准控件和自建 CWnd 子类。唯一缺口是容器和窗口不能动态改尺寸——这正好是我们补第一刀的地方。 你在 MT5 里验证很简单:建个 EA,把标准库 Controls 的窗口写死 400×300,再塞一组 20 行维度的标签,编译运行后拖窗口大小,标签立刻重叠;换成 Lambino 的 CGrid 嵌套结构,同样数据会按格重排。外汇与贵金属行情跳动快、维度组合多,这类界面若不能自适应,复盘效率会直接掉档,且杠杆品种高风险,错看一层维度可能放大亏损。

◍ 给对话框加上能拉伸的「橡胶」外壳

在标准 MT5 控件库里,CAppDialog 本身不支持窗口最大化与任意缩放。要做出「橡胶窗口」,得从 CAppDialog 派生一个新类 MaximizableAppDialog,并在 Include\Layouts\ 下建 MaximizableAppDialog.mqh。派生后原有对话框逻辑不动,只是挂上两个新按钮:标题栏里挨着最小化按钮的「最大化」、右下角的「拖拽缩放」三角形。 标准库的 CGrid 名字和本文容器类撞车,但两者无关,不用管。真正麻烦的是 CDialog 里的 m_caption 和 CAppDialog 里的 m_button_minmax 被声明为 private,派生类拿不到。唯一现实做法是给 include/controls 文件夹做一份副本,把相关成员从 private 改成 protected,否则编译直接报 private member access error。库一更新就得重打补丁,这是当前架构的硬伤。 最大化状态、缩放过程用独立 bool 变量记录,并配一个矩形持续监视图表尺寸变化,用 SetSizeLimit 设最小不可缩尺寸。测试样例沿用 Experts\Examples\Layouts\ 下的 Controls2 和 SlidingPuzzle2,复制改名后把头文件里的父类替换成 MaximizableAppDialog,再补一个 SelfAdjustment 虚方法让控件跟着新尺寸重排。 最小化状态下右下角缩放按钮会和关闭按钮重叠并抢鼠标事件,根因是 CWndContainer::OnMouseEvent 只倒序遍历子元素、完全不看 Z 轴顺序。没法定补丁时就用歪招:最小化时把缩放按钮的点击转派给关闭按钮。 下面这段是 CBox::GetTotalControlsSize 的修正版,关键点在于对嵌套容器递归调用自身,并按布局方向(水平/垂直)累加或取最大尺寸,最后把容器本身撑到新总计。 原方法只算平级控件,遇到嵌套 CBox 会漏算内部尺寸,导致自适应窗口缩到比内容还小。改完后在 SlidingPuzzle3 里用 m_main 容器的 pack 方法对客户端区重排,游戏盘面就能跟着窗口扯大扯小。外汇与贵金属 EA 改界面同样高风险,MT5 测试前务必先在策略测试器跑一遍无报错再上真图。

MQL5 / C++
class="type">void CBox::GetTotalControlsSize(class="type">void)
{
m_total_x = class="num">0;
m_total_y = class="num">0;
m_controls_total = class="num">0;
m_min_size.cx = class="num">0;
m_min_size.cy = class="num">0;
class="type">int total = ControlsTotal();
for(class="type">int i = class="num">0; i < total; i++)
{
CWnd *control = Control(i);
if(control == NULL) class="kw">continue;
if(control == &m_background) class="kw">continue;
CheckControlSize(control);
class=class="str">"cmt">// 添加:为嵌套容器递归调用自身
if(control.Type() == CLASS_LAYOUT)
{
((CBox *)control).GetTotalControlsSize();
}
CSize control_size = control.Size();
if(m_min_size.cx < control_size.cx)
m_min_size.cx = control_size.cx;
if(m_min_size.cy < control_size.cy)
m_min_size.cy = control_size.cy;
class=class="str">"cmt">// 编辑:根据容器方向有条件地增加m_total_x和m_total_y
if(m_layout_style == LAYOUT_STYLE_HORIZONTAL) m_total_x += control_size.cx;
else m_total_x = MathMax(m_min_size.cx, m_total_x);
if(m_layout_style == LAYOUT_STYLE_VERTICAL) m_total_y += control_size.cy;
else m_total_y = MathMax(m_min_size.cy, m_total_y);
m_controls_total++;
}
class=class="str">"cmt">// 添加:根据新总计调整容器大小
CSize size = Size();
if(m_total_x > size.cx && m_layout_style == LAYOUT_STYLE_HORIZONTAL)
{
size.cx = m_total_x;
}
if(m_total_y > size.cy && m_layout_style == LAYOUT_STYLE_VERTICAL)
{
size.cy = m_total_y;
}
Size(size);
}
class="macro">#include <Controls\Dialog.mqh>
class="macro">#include <Controls\Button.mqh>
class MaximizableAppDialog: class="kw">public CAppDialog
{
class="kw">protected:
CBmpButton m_button_truemax;

「可最大化对话框的事件映射与尺寸修复」

在 MT5 自定义面板里做可最大化对话框,核心是把最小化、最大化、拖拽缩放三套虚函数重写掉,并用 EVENT_MAP 把按钮 ID 绑到具体方法上。下面这段映射把真全屏按钮、尺寸手柄拖拽起手、拖拽过程与结束分别挂到 OnClickButtonTrueMax / OnDialogSizeStart / OnDialogDragProcess / OnDialogDragEnd,继承链直接落到 CAppDialog。 ChartEvent 里只拦截 CHARTEVENT_CHART_CHANGE,图表尺寸一变就调 OnChartChange:用 ChartGetInteger 取 CHART_WIDTH_IN_PIXELS 和 CHART_HEIGHT_IN_PIXELS,减掉 1*CONTROLS_BORDER_WIDTH 得到 m_max_rect。若 m_maximized 为真且当前矩形宽高和最大矩形不一致,就 Rebound + SelfAdjustment + Redraw,面板会贴着图表重排。

有个隐蔽坑:示例里 OnClickButtonSizeFixMe 直接判断 m_minimized 后 Destroy,但原注释写明「缺了 !IS_ENABLED!IS_VISIBLE 的早返回」。不补这行,按钮在隐藏或禁用状态下被点仍会进销毁逻辑,可能在实盘挂 EA 时误杀面板。开 MT5 把那行补上再编译,能少踩一次闪退。

资源位 three 张 bmp(expand2 / size6 / size10)走 #resource 预编译进 ex5,缩放手柄用 CBmpButton m_button_size 承载,最小尺寸限制交给 CSize m_size_limit。外汇与贵金属图表多屏缩放频率高,这类面板误操作概率偏大,改完务必在策略测试器外的小周期图手动拖几次验证。

MQL5 / C++
CBmpButton m_button_size;
class="type">bool m_maximized;
class="type">bool m_sizing;
CRect m_max_rect;
CSize m_size_limit;
class="kw">virtual class="type">bool CreateButtonMinMax(class="type">void) class="kw">override;
class="kw">virtual class="type">void OnClickButtonMinMax(class="type">void) class="kw">override;
class="kw">virtual class="type">void Minimize(class="type">void) class="kw">override;
class="kw">virtual class="type">bool OnDialogDragStart(class="type">void) class="kw">override;
class="kw">virtual class="type">bool OnDialogDragProcess(class="type">void) class="kw">override;
class="kw">virtual class="type">bool OnDialogDragEnd(class="type">void) class="kw">override;
class="kw">virtual class="type">bool 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="kw">override;
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) class="kw">override;
class="macro">#resource "res\\expand2.bmp"
class="macro">#resource "res\\size6.bmp"
class="macro">#resource "res\\size10.bmp"
class="kw">virtual class="type">void OnClickButtonTrueMax(class="type">void);
class="kw">virtual class="type">void Expand(class="type">void);
class="kw">virtual class="type">void Restore(class="type">void);
class="type">bool CreateButtonSize(class="type">void);
class="type">bool OnDialogSizeStart(class="type">void);
EVENT_MAP_BEGIN(MaximizableAppDialog)
ON_EVENT(ON_CLICK, m_button_truemax, OnClickButtonTrueMax)
ON_EVENT(ON_DRAG_START, m_button_size, OnDialogSizeStart)
ON_EVENT_PTR(ON_DRAG_PROCESS, m_drag_object, OnDialogDragProcess)
ON_EVENT_PTR(ON_DRAG_END, m_drag_object, OnDialogDragEnd)
EVENT_MAP_END(CAppDialog)
class="type">void MaximizableAppDialog::ChartEvent(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)
{
if(id == CHARTEVENT_CHART_CHANGE)
{
if(OnChartChange(lparam, dparam, sparam)) class="kw">return;
}
CAppDialog::ChartEvent(id, lparam, dparam, sparam);
}
class="type">bool MaximizableAppDialog::OnChartChange(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)
{
m_max_rect.SetBound(class="num">0, class="num">0,
(class="type">int)ChartGetInteger(ChartID(), CHART_WIDTH_IN_PIXELS) - class="num">0 * CONTROLS_BORDER_WIDTH,
(class="type">int)ChartGetInteger(ChartID(), CHART_HEIGHT_IN_PIXELS) - class="num">1 * CONTROLS_BORDER_WIDTH);
if(m_maximized)
{
if(m_rect.Width() != m_max_rect.Width() || m_rect.Height() != m_max_rect.Height())
{
Rebound(m_max_rect);
SelfAdjustment();
m_chart.Redraw();
}
class="kw">return true;
}
class="kw">return class="kw">false;
}
class="kw">virtual class="type">void SelfAdjustment(class="kw">const class="type">bool minimized = class="kw">false) = class="num">0;
class=class="str">"cmt">// if(!IS_ENABLED || !IS_VISIBLE) class="kw">return class="kw">false; - 缺了这一行
class="type">void MaximizableAppDialog::OnClickButtonSizeFixMe(class="type">void)
{
if(m_minimized)
{
Destroy();
}
}
EVENT_MAP_BEGIN(MaximizableAppDialog)
...
ON_EVENT(ON_CLICK, m_button_size, OnClickButtonSizeFixMe)
...
EVENT_MAP_END(CAppDialog)
class="macro">#include <Controls\Dialog.mqh>
class="macro">#include <Layouts\MaximizableAppDialog.mqh>
class CSlidingPuzzleDialog: class="kw">public MaximizableAppDialog class=class="str">"cmt">// CAppDialog

把拼图面板钉进客户区

CSlidingPuzzleDialog 的 Create 方法先调 MaximizableAppDialog::Create 建出基础对话框,失败直接返 false,这一步决定后续控件能不能挂上去。 SelfAdjustment 里取 ClientAreaWidth 和 ClientAreaHeight 算出客户区尺寸,再喂给 m_main.Size 和 m_main.Pack,窗口缩放时面板会自动重排,不用手算坐标。 EVENT_MAP_END 收尾标明这是 CAppDialog 派生链的终点,ON_INDEXED_EVENT(ON_CLICK, m_buttons, OnClickButton) 把按钮数组的点击统一路由到 OnClickButton,写 MT5 面板时照这个结构套能省掉一堆消息分发代码。

MQL5 / C++
EVENT_MAP_END(MaximizableAppDialog) class=class="str">"cmt">// CAppDialog
class="type">bool CSlidingPuzzleDialog::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(!MaximizableAppDialog::Create(chart, name, subwin, x1, y1, x2, y2)) class=class="str">"cmt">// CAppDialog
class="kw">return (class="kw">false);
...
class="type">void CSlidingPuzzleDialog::SelfAdjustment(class="kw">const class="type">bool minimized = class="kw">false)
{
CSize size;
size.cx = ClientAreaWidth();
size.cy = ClientAreaHeight();
m_main.Size(size);
m_main.Pack();
}
ON_INDEXED_EVENT(ON_CLICK, m_buttons, OnClickButton)
把界面适配交给小布盯盘
这些自适应布局的诊断与品种页联动,小布盯盘的 AIGC 已内置,打开对应品种页即可看到缩放后的多维概览,你只管读结论。

常见问题

标准库对话框窗口大小固定,元素用 x/y 硬编码,拉伸后单元格标签会重叠,无法自适应轴长。
CBox 提供水平或垂直布局容器可嵌套出任意结构,CGrid 提供表格布局,两者均兼容标准及自研 CWnd 控件。
Enrico Lambino 的 CBox/CGrid 缺动态窗口与容器尺寸调整,需自行扩展 resize 逻辑才能随窗体变化重排。
小布盯盘内置了适配多品种的多维概览面板,不需你自己写 Box.mqh 那套,打开页就能看缩放后的结果。
屏幕表单编辑器开发成本很高,多数库选静态坐标或简易自适应,本文为聚焦 OLAP 也避开了编辑器。