在交易中应用 OLAP(第 2 部分):可视化交互式多维数据分析的结果·进阶篇
「让数值框和下拉框跟着窗口拉伸」
标准库里 CButton 这类控件调一下 Width 就能自适应,CListView 设个 Alignment 也会自动贴边。但 CSpinEdit 和 CComboBox 原生不支持任何伸缩方法,想让它们变成“橡胶”控件,只能写子类重写。 CSpinEdit 实际由输入框 m_edit 加两个按钮 m_inc、m_dec 拼成,可它的下属元素在私有区,得先把标准库里 private 改成 protected 才能碰。重写 OnResize 时,把输入框宽高铺满,再把两个按钮按 CONTROLS_BUTTON_SIZE 和偏移量挪到右边缘即可。 ComboBoxResizable 更麻烦:除了 OnResize,还要重写 OnClickButton、Enable、Disable 并加事件映射,统一管理 m_edit、m_list、m_drop。演示项目 Control3 里用这两个子类替换原控件,在 SelfAdjustment 中改宽度——该方法由 MaximableAppDialog 在窗体缩放后自动调,初始化时也在 CreateMain 里手调一次。 调试用的红色边框靠 LAYOUT_BOX_DEBUG 宏关掉。控件初始化现在改成每块进专用方法(CreateMain、CreateEditRow 等),成功返回 CWnd*,父容器用 Add 挂接,逻辑比旧写法紧凑,但跨版本项目比对会费劲。
class="macro">#include <Controls/SpinEdit.mqh> class=class="str">"cmt">// 需要补丁: class="kw">private: -> class="kw">protected: class SpinEditResizable: class="kw">public CSpinEdit { class="kw">public: class="kw">virtual class="type">bool OnResize(class="type">void) class="kw">override { m_edit.Width(Width()); m_edit.Height(Height()); class="type">int x1 = Width() - (CONTROLS_BUTTON_SIZE + CONTROLS_SPIN_BUTTON_X_OFF); class="type">int y1 = (Height() - class="num">2 * CONTROLS_SPIN_BUTTON_SIZE) / class="num">2; m_inc.Move(Left() + x1, Top() + y1); x1 = Width() - (CONTROLS_BUTTON_SIZE + CONTROLS_SPIN_BUTTON_X_OFF); y1 = (Height() - class="num">2 * CONTROLS_SPIN_BUTTON_SIZE) / class="num">2 + CONTROLS_SPIN_BUTTON_SIZE; m_dec.Move(Left() + x1, Top() + y1); class="kw">return CWndContainer::OnResize(); } }; class="macro">#include <Controls/ComboBox.mqh> class=class="str">"cmt">// 需要打补丁: class="kw">private: -> class="kw">protected: class ComboBoxResizable: class="kw">public CComboBox { class="kw">public: 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="kw">virtual class="type">bool OnResize(class="type">void) class="kw">override { m_edit.Width(Width()); class="type">int x1 = Width() - (CONTROLS_BUTTON_SIZE + CONTROLS_COMBO_BUTTON_X_OFF); class="type">int y1 = (Height() - CONTROLS_BUTTON_SIZE) / class="num">2; m_drop.Move(Left() + x1, Top() + y1); m_list.Width(Width()); class="kw">return CWndContainer::OnResize(); } class="kw">virtual class="type">bool OnClickButton(class="type">void) class="kw">override { class=class="str">"cmt">// 这是触发列表中元素大小调整的技巧 class=class="str">"cmt">// 我们需要它,因为标准ListView的编码方式不正确 class=class="str">"cmt">// 只有当存在垂直滚动时才调整元素的大小 class="type">bool vs = m_list.VScrolled(); if(m_drop.Pressed()) { m_list.VScrolled(true); } class="type">bool b = CComboBox::OnClickButton(); m_list.VScrolled(vs); class="kw">return b; } class="kw">virtual class="type">bool Enable(class="type">void) class="kw">override { m_edit.Show(); m_drop.Show(); class="kw">return CComboBox::Enable(); } class="kw">virtual class="type">bool Disable(class="type">void) class="kw">override { m_edit.Hide(); m_drop.Hide(); class="kw">return CComboBox::Disable(); } }; class="macro">#define EXIT_ON_DISABLED \ if(!IsEnabled()) \ { \ class="kw">return class="kw">false; \ } EVENT_MAP_BEGIN(ComboBoxResizable) EXIT_ON_DISABLED ON_EVENT(ON_CLICK, m_drop, OnClickButton) EVENT_MAP_END(CComboBox) class="type">void CControlsDialog::SelfAdjustment(class="kw">const class="type">bool minimized = class="kw">false) { CSize min = m_main.GetMinSize(); CSize size; size.cx = ClientAreaWidth(); size.cy = ClientAreaHeight(); if(minimized) { if(min.cx > size.cx) size.cx = min.cx; if(min.cy > size.cy) size.cy = min.cy; } m_main.Size(size); class="type">int w = (m_button_row.Width() - class="num">2 * class="num">2 * class="num">2 * class="num">3) / class="num">3; m_button1.Width(w); m_button2.Width(w); m_button3.Width(w); m_edit.Width(w); m_spin_edit.Width(w); m_combo_box.Width(m_lists_row.Width() / class="num">2); m_main.Pack(); }
◍ 对话框主面板的逐层拼装逻辑
在 MT5 自定义控件库里,一个可最大化对话框的落地靠的是分层 Create:先由 CControlsDialog::Create 调父类 MaximizableAppDialog::Create 占好坐标,再挂上 CreateMain 返回的容器指针,两步都成功才返回 true,否则直接 false。 CreateMain 内部把 m_main 设成垂直布局,用 ClientAreaWidth() 和 ClientAreaHeight() 铺满客户区,然后依次 Add 编辑框行、按钮行、日期微调行、列表行,最后 Pack() 收紧尺寸并跑一次 SelfAdjustment()。任何一环失败就返回 NULL,界面不会半残显示。 按钮行这段值得单独看:m_button_row 高度写死为 BUTTON_HEIGHT * 1.5,横向塞进三个按钮后用 Alignment(WND_ALIGN_LEFT|WND_ALIGN_RIGHT, 2, 0, 2, 0) 做左右贴边、间距 2 像素。想加第四个控件,直接补一个 CreateButton4() 并在 Add 链里接上即可,高度系数不改的话可能挤到第二排。 外汇与贵金属图表上跑这类面板属于高频重绘操作,实盘加载前建议在策略测试器用历史数据验证布局不溢出,相关品种波动剧烈、杠杆风险高,参数未稳前勿直接挂真实账户。
class="type">bool CControlsDialog::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) && Add(CreateMain(chart, name, subwin))) { class="kw">return true; } class="kw">return class="kw">false; } CWnd *CControlsDialog::CreateMain(class="kw">const class="type">long chart, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int subwin) { m_main.LayoutStyle(LAYOUT_STYLE_VERTICAL); if(m_main.Create(chart, name + "main", subwin, class="num">0, class="num">0, ClientAreaWidth(), ClientAreaHeight()) && m_main.Add(CreateEditRow(chart, name, subwin)) && m_main.Add(CreateButtonRow(chart, name, subwin)) && m_main.Add(CreateSpinDateRow(chart, name, subwin)) && m_main.Add(CreateListsRow(chart, name, subwin)) && m_main.Pack()) { SelfAdjustment(); class="kw">return &m_main; } class="kw">return NULL; } CWnd *CControlsDialog::CreateButtonRow(class="kw">const class="type">long chart, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int subwin) { if(m_button_row.Create(chart, name + "buttonrow", subwin, class="num">0, class="num">0, ClientAreaWidth(), BUTTON_HEIGHT * class="num">1.5) && m_button_row.Add(CreateButton1()) && m_button_row.Add(CreateButton2()) && m_button_row.Add(CreateButton3())) { m_button_row.Alignment(WND_ALIGN_LEFT|WND_ALIGN_RIGHT, class="num">2, class="num">0, class="num">2, class="num">0); class="kw">return &m_button_row; } class="kw">return NULL; }
把 PairPlot 改造成单图控件 CPlot
MQL5 标准库里 CCanvas、CGraphic 以及 CChartObjectBitmap 这类图形原型,本身都不带窗口控件能力,要塞进界面就得包一层能绘图的子类。现成的 PairPlot 文章给过一套基于 CGraphic 的相关性分析控件,类文件落在 Include\PairPlot\PairPlot.mqh,我们直接以它为底子派生 Plot.mqh,砍掉 CTimeserie 和多品种逻辑,只留单图。 具体改动是把原 CPairPlot(继承自 CWndClient)重写成 CPlot,再把内部的 CHistogram / CScatter 那条线废掉,转而让新类 CGraphicInPlot 直接从 CGraphic 派生,复用 CCurve 自带的 CURVE_POINTS、CURVE_LINES、CURVE_POINTS_AND_LINES、CURVE_STEPS、CURVE_HISTOGRAM 五种样式。这样控件不绑定任何特殊图表类型,调参就能换线形。 测试 EA 叫 PlotDemo,输入参数只有一个 PlotType 决定图形样式;对话框描述在 PlotDemo.mqh,初始化和事件绑定在 PlotDemo.mq5。下面这段是 EA 主体与控件类的核心代码,注意 Create() 里会用 ChartGetInteger 取 CHART_WIDTH_IN_PIXELS / CHART_HEIGHT_IN_PIXELS,超界时强制把窗口拉成全图区域(宽高上限即图表像素尺寸),SelfAdjustment() 则在非最小化时把 m_plot 尺寸同步到 ClientAreaWidth/Height。 外汇与贵金属图表加载这类自定义控件存在 MT5 环境兼容与脚本崩溃风险,建议在模拟账户先跑通再上实盘。
class="macro">#include "PlotDemo.mqh" class="kw">input ENUM_CURVE_TYPE PlotType = CURVE_POINTS; CPlotDemo *pPlotDemo; class="type">int OnInit() { pPlotDemo = new CPlotDemo; if(CheckPointer(pPlotDemo) == POINTER_INVALID) class="kw">return INIT_FAILED; if(!pPlotDemo.Create(class="num">0, "Plot Demo", class="num">0, class="num">20, class="num">20, class="num">800, class="num">600, PlotType)) class="kw">return INIT_FAILED; if(!pPlotDemo.Run()) class="kw">return INIT_FAILED; pPlotDemo.Refresh(); class="kw">return INIT_SUCCEEDED; } 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) { pPlotDemo.ChartEvent(id, lparam, dparam, sparam); } ... class="macro">#include <Controls\Dialog.mqh> class="macro">#include <PairPlot/Plot.mqh> class="macro">#include <Layouts/MaximizableAppDialog.mqh> class CPlotDemo: class="kw">public MaximizableAppDialog class=class="str">"cmt">// CAppDialog { class="kw">private: CPlot m_plot; class="kw">public: CPlotDemo() {} ~CPlotDemo() {} 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">const ENUM_CURVE_TYPE curveType = CURVE_POINTS); 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="type">bool Refresh(class="type">void); class="kw">virtual class="type">void SelfAdjustment(class="kw">const class="type">bool minimized = class="kw">false) class="kw">override { if(!minimized) { m_plot.Size(ClientAreaWidth(), ClientAreaHeight()); m_plot.Resize(class="num">0, class="num">0, ClientAreaWidth(), ClientAreaHeight()); } m_plot.Refresh(); } }; EVENT_MAP_BEGIN(CPlotDemo) EVENT_MAP_END(MaximizableAppDialog) class="type">bool CPlotDemo::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">const ENUM_CURVE_TYPE curveType = CURVE_POINTS) { class="kw">const class="type">int maxw = (class="type">int)ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS); class="kw">const class="type">int maxh = (class="type">int)ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS); class="type">int _x1 = x1; class="type">int _y1 = y1; class="type">int _x2 = x2; class="type">int _y2 = y2; if(x2 - x1 > maxw || x2 > maxw) { _x1 = class="num">0; _x2 = _x1 + maxw - class="num">0; } if(y2 - y1 > maxh || y2 > maxh) { _y1 = class="num">0; _y2 = _y1 + maxh - class="num">1; } if(!MaximizableAppDialog::Create(chart, name, subwin, _x1, _y1, _x2, _y2))
「用曲线数组验证绘图对象初始化」
这段片段演示了在自定义图形类里,如何先判错再喂数据:绘图对象建不出来就直接回 false,保证上层调用不会拿到空壳。 可以看到 x 与 y 是两个长度均为 10 的 double 数组,取值从 -10 到 8、y 从 -10 到 23,并非均匀采样;CurveAdd 被调了两次,分别把 (x,y) 和 (y,x) 各画一条,命名 Example 1 / Example 2,相当于把同一组数做了坐标互换对照。 最后 Refresh 只包了一层 m_plot.Refresh(),说明图形刷新权仍交还给封装好的绘图成员。开 MT5 把这段塞进 EA 的 OnInit,能在副图直接看到两条交叉散点曲线,验证绘图句柄是否真的绑上了子窗口。外汇与贵金属图表加载自定义绘图属高频改动,实盘前务必在模拟环境跑通,这类 UI 对象崩溃可能拖累行情响应。
class="kw">return class="kw">false; if(!m_plot.Create(m_chart_id, m_name + "Plot", m_subwin, class="num">0, class="num">0, ClientAreaWidth(), ClientAreaHeight(), curveType)) class="kw">return class="kw">false; if(!Add(m_plot)) class="kw">return class="kw">false; class="type">class="kw">double x[] = {-class="num">10, -class="num">4, -class="num">1, class="num">2, class="num">3, class="num">4, class="num">5, class="num">6, class="num">7, class="num">8}; class="type">class="kw">double y[] = {-class="num">5, class="num">4, -class="num">10, class="num">23, class="num">17, class="num">18, -class="num">9, class="num">13, class="num">17, class="num">4}; m_plot.CurveAdd(x, y, "Example class="num">1"); m_plot.CurveAdd(y, x, "Example class="num">2"); class="kw">return true; } class="type">bool CPlotDemo::Refresh(class="type">void) { class="kw">return m_plot.Refresh(); }