OLAP 交易可视化基础:用自适应 GUI 摆脱硬编码坐标(基础篇)
(1/3)·标准库对话框尺寸写死、标签重叠,大型超立方体根本没法看,这篇先铺 GUI 底层
给 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 测试前务必先在策略测试器跑一遍无报错再上真图。
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。外汇与贵金属图表多屏缩放频率高,这类面板误操作概率偏大,改完务必在策略测试器外的小周期图手动拖几次验证。
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 面板时照这个结构套能省掉一堆消息分发代码。
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)