在交易中应用 OLAP(第 2 部分):可视化交互式多维数据分析的结果·进阶篇
📘

在交易中应用 OLAP(第 2 部分):可视化交互式多维数据分析的结果·进阶篇

第 2/3 篇

「让数值框和下拉框跟着窗口拉伸」

标准库里 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 挂接,逻辑比旧写法紧凑,但跨版本项目比对会费劲。

MQL5 / C++
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 链里接上即可,高度系数不改的话可能挤到第二排。 外汇与贵金属图表上跑这类面板属于高频重绘操作,实盘加载前建议在策略测试器用历史数据验证布局不溢出,相关品种波动剧烈、杠杆风险高,参数未稳前勿直接挂真实账户。

MQL5 / C++
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 环境兼容与脚本崩溃风险,建议在模拟账户先跑通再上实盘。

MQL5 / C++
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 对象崩溃可能拖累行情响应。

MQL5 / C++
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();
}

常见问题

在父容器 resize 事件里按比例重算子控件 left/right 与 width,绑定到窗口客户区尺寸,别写死像素值。
先建根 CAppDialog,再按「容器→分组框→具体控件」顺序挂,每层只管自己的布局锚点,避免跨层直接改子控件。
小布可以读取你当前品种页的控件树快照,标出未绑定 resize 的死控件和重叠区域,省去肉眼排查。
需重写数据映射把多维对映到单图坐标,并保留轴标签回调;原矩阵色阶逻辑要下沉为单图配色函数。
循环里对每条曲线调 Create 后立刻取 Handle,若返回 INVALID_HANDLE 即未初始化,应断点查父容器挂载顺序。