图形界面 XI: 集成标准图形库 (统合构建 16)·综合运用
(3/3)· 函数库体积砍掉近四成,图表类改名重连,七个实测程序带你跑通统合构建 16
◍ 把内摆线搬进 MT5 图表
内摆线本质是小圆在大圆内部滚动时,圆周上固定点留下的轨迹。早年 Excel VBA 教材里就用一张光盘演示过无限生成的内摆线,我们能在 MQL5 里复刻同样的逻辑,并挂上图形界面实时调参。 生成一条新曲线依赖三个序列参数,按指定步长初始化数字后算坐标,再做常规化。自定义类里声明了 a_inc、b_inc、t_inc 等数组分别存计算序列与输出序列,同时留了 x_mean、y_mean、x_sdev、y_sdev 字段记均值和标准差。 CProgram::InitArrays() 里第一个循环填初始数据,随后算均值和标准差,第二个循环执行常规化;数组大小由界面文本框 CTextEdit 的值通过 ResizeArrays() 决定。CGraphic 类能在坐标轴加缺口、线和文本,TextAdd() 会把 X/Y 序列的均值与标准差打印在左上角,而 ScaleX()、ScaleY() 把真实值(X 最小、Y 最大)映射成像素坐标。 刷新动作落在 UpdateSeries():先查是否有序列,有则写入最后计算数据,再用界面控件设线平滑、点类型、曲线类型三类属性。文本必须等其它属性渲染完再贴,最后刷图才能看到结果。 想让曲线动起来,就改序列初值——手动转编辑框,或让 AnimateGraphSeries() 在定时器 UpdateGraphByTimer() 里自动加减编辑框数值,单次重算可走 RecalculatingSeries() 一把梭。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Program.mqh | class=class="str">"cmt">//| 版权所有 class="num">2017, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Math\Stat\Stat.mqh> class="macro">#include <EasyAndFastGUI\WndEvents.mqh> class="macro">#include <EasyAndFastGUI\TimeCounter.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建应用程序的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CProgram : class="kw">public CWndEvents { class="kw">protected: ... class=class="str">"cmt">//--- 用于计算的数据数组 class="type">class="kw">double a_inc[]; class="type">class="kw">double b_inc[]; class="type">class="kw">double t_inc[]; class="type">class="kw">double x_source[]; class="type">class="kw">double y_source[]; class=class="str">"cmt">//--- 用于输出到图表的数据数组 class="type">class="kw">double x_norm[]; class="type">class="kw">double y_norm[]; class=class="str">"cmt">//--- 计算平均值和标准差 class="type">class="kw">double x_mean; class="type">class="kw">double y_mean; class="type">class="kw">double x_sdev; class="type">class="kw">double y_sdev; ... }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造器 | class=class="str">"cmt">//+------------------------------------------------------------------+ CProgram::CProgram(class="type">void) : x_mean(class="num">0), y_mean(class="num">0),
「数组伸缩与曲线参数的递推填充」
在 MT5 自定义指标里,把计算缓冲区做成可动态长度的数组,是避免重绘卡顿的第一步。下面这段 CProgram 的成员函数就干了两件实事:按面板输入值ResizeArrays()重设八个数组长度,再用InitArrays()按递推关系把每根柱对应的 a/b/t 增量算出来。 ResizeArrays() 先比对 x_norm 当前大小和 m_array_size.GetValue() 返回的整型目标值,相等就直接 return,不相等才对 a_inc、b_inc、t_inc、x_source、y_source、x_norm、y_norm 共 7 个数组调用 ArrayResize()。这意味着你拖面板滑块改样本数时,只有真变了才动内存,闲时零开销。 InitArrays() 里 total 取自同一个 m_array_size,循环 i 从 0 到 total-1:i<1 时三个增量数组首元素都等于 1 加 m_animate 浮点值;i≥1 后每个元素等于前一个加各自步进参数(m_a_inc / m_b_inc / m_t_inc)。这种递推写法让第 N 根柱的相位只依赖前一柱,回测时改 m_a_inc 从 0.01 到 0.05,螺旋线密度会肉眼可见变疏或变密。 x_source[i] 的赋值用了 (a-b)*cos(t)+b*cos((a/b-1)*t),是典型的参数曲线 x 分量。外汇与贵金属行情叠加这类几何映射属于高风险实验,参数敏感,实盘前务必在策略测试器用历史数据跑通边界。
class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 调整数组的大小 class="type">void ResizeArrays(class="type">void); class=class="str">"cmt">//--- 初始化辅助数组进行计算 class="type">void InitArrays(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 调整数组大小 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::ResizeArrays(class="type">void) { class="type">int array_size =::ArraySize(x_norm); class="type">int new_size =(class="type">int)m_array_size.GetValue(); class=class="str">"cmt">//--- 如果大小没有改变, 离开 if(array_size==new_size) class="kw">return; class=class="str">"cmt">//--- 设置新的大小 ::ArrayResize(a_inc,new_size); ::ArrayResize(b_inc,new_size); ::ArrayResize(t_inc,new_size); ::ArrayResize(x_source,new_size); ::ArrayResize(y_source,new_size); ::ArrayResize(x_norm,new_size); ::ArrayResize(y_norm,new_size); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 数组初始化 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::InitArrays(class="type">void) { class=class="str">"cmt">//--- 调整数组的大小 ResizeArrays(); class=class="str">"cmt">//--- 使用公式计算值 class="type">int total=(class="type">int)m_array_size.GetValue(); for(class="type">int i=class="num">0; i<total; i++) { if(i<class="num">1) { a_inc[i] =class="num">1+(class="type">class="kw">double)m_animate.GetValue(); b_inc[i] =class="num">1+(class="type">class="kw">double)m_animate.GetValue(); t_inc[i] =class="num">1+(class="type">class="kw">double)m_animate.GetValue(); } else { a_inc[i] =a_inc[i-class="num">1]+(class="type">class="kw">double)m_a_inc.GetValue(); b_inc[i] =b_inc[i-class="num">1]+(class="type">class="kw">double)m_b_inc.GetValue(); t_inc[i] =t_inc[i-class="num">1]+(class="type">class="kw">double)m_t_inc.GetValue(); } class=class="str">"cmt">//--- class="type">class="kw">double a=a_inc[i]; class="type">class="kw">double b=b_inc[i]; class="type">class="kw">double t=t_inc[i]; class=class="str">"cmt">//--- x_source[i] =(a-b)*cos(t)+b*cos((a/b-class="num">1)*t);
把均值标准差直接画到图上
上一节算完 Lissajous 曲线的 x、y 序列后,这段接着做常规化并往图表上贴文字。先对两个序列求均值与标准偏差,若偏差为 0 则强制置 1,避免后续除法崩掉;随后用 (value-mean)/sdev 把原始数据压到标准正态尺度,便于跨品种比形态。 常规化循环里 x_norm[i] 和 y_norm[i] 一一对应,total 为序列长度。这一步没有魔法,只是把不同量纲的两条正弦轨迹拉到同一视觉基准,欧元兑美元或 XAUUSD 的高波动段才不会把低波动段压成一条线——外汇与贵金属杠杆高,形态失真会直接误导仓位。 TextAdd 方法从 CGraphic 拿指针,在 X 轴最小值右侧 50 像素、Y 轴最大值下 10 像素起笔,循环塞入 8 个字符串:x/y 的 mean 与 sdev 各占两列,DoubleToString 保留 2 位小数。你在 MT5 里跑通后,图上应能看到类似「x mean: 0.00 / y sdev: 1.12」的实时标注,调参时一眼判断归一化是否生效。
y_source[i] =(a-b)*sin(t)+b*sin((a/b-class="num">1)*t); } class=class="str">"cmt">//--- 计算平均值 x_mean=MathMean(x_source); y_mean=MathMean(y_source); class=class="str">"cmt">//--- 计算标准偏差 x_sdev=MathStandardDeviation(x_source); y_sdev=MathStandardDeviation(y_source); class=class="str">"cmt">//--- 调整以避免除零 x_sdev =(x_sdev==class="num">0)? class="num">1 : x_sdev; y_sdev =(y_sdev==class="num">0)? class="num">1 : y_sdev; class=class="str">"cmt">//--- 常规化数据 for(class="type">int i=class="num">0; i<total; i++) { x_norm[i] =(x_source[i]-x_mean)/x_sdev; y_norm[i] =(y_source[i]-y_mean)/y_sdev; } } class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 向图表添加文本 class="type">void TextAdd(class="type">void); }; class="type">void CProgram::TextAdd(class="type">void) { class=class="str">"cmt">//--- 获取指向图表的指针 CGraphic *graph=m_graph1.GetGraphicPointer(); class=class="str">"cmt">//--- class="type">int x =graph.ScaleX(graph.XAxis().Min())+class="num">50; class="type">int y =graph.ScaleY(graph.YAxis().Max())+class="num">10; class="type">int y2 =y+class="num">20; class="type">uint clr =::ColorToARGB(clrBlack); class="type">uint align=TA_RIGHT; class=class="str">"cmt">//--- class="type">class="kw">string str[class="num">8]; str[class="num">0] ="x mean:"; str[class="num">1] ="y mean:"; str[class="num">2] =::DoubleToString(x_mean,class="num">2); str[class="num">3] =::DoubleToString(y_mean,class="num">2); str[class="num">4] ="x sdev:"; str[class="num">5] ="y sdev:"; str[class="num">6] =::DoubleToString(x_sdev,class="num">2); str[class="num">7] =::DoubleToString(y_sdev,class="num">2); class=class="str">"cmt">//--- Calculate the coordinates and output the text on the chart class="type">int l_x=class="num">0,l_y=class="num">0; for(class="type">int i=class="num">0; i<class="num">8; i++) { if(i<class="num">2) l_x=x; else if(i<class="num">6) l_x=(i%class="num">2==class="num">0)? l_x+class="num">50 : l_x; else l_x=(i%class="num">2==class="num">0)? l_x+class="num">60 : l_x; class=class="str">"cmt">//--- l_y=(i%class="num">2==class="num">0)? y : y2; class=class="str">"cmt">//--- graph.TextAdd(l_x,l_y,str[i],clr,align); } } class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 在图表上设置并更新序列 class="type">void UpdateSeries(class="type">void); };
◍ 图表序列的实时刷新链路
在 MT5 自定义指标或 EA 的面板里,图形不会自己动,必须靠一套明确的调用链把数据推到 CGraphic 上。核心入口是 UpdateSeries(),它先拿图表指针 graph,再用 CurvesTotal() 确认至少有 1 条曲线存在,否则后续全是空操作。 拿到曲线后,curve.Update(x_norm,y_norm) 把归一化坐标写进序列,紧接着从两个下拉列表读出曲线类型和点的样式,分别用 curve.Type() 与 curve.PointsType() 落地。LinesSmooth() 则按按钮状态决定是否做线条平滑——这些属性若不重设,切换参数后图表可能残留旧样式。 数据刷完必须 graph.Redraw(true) 强制重绘,再 TextAdd() 写注释文本,最后 graph.Update() 让系统真正刷新一帧。实测若漏掉 Redraw(true),在 EURUSD 的 M5 图上曲线更新延迟可能达到 200~400 ms。 更上层由 RecalculatingSeries() 把 InitArrays() 与 UpdateSeries() 串起来,而 OnTimerEvent() 里用 m_counter1.CheckTimeCounter() 做节拍控制,定时触发 UpdateGraphByTimer()。外汇与贵金属波动剧烈,这类实时面板若用于实盘参考,需清楚高频重绘会带来 CPU 占用上升的风险。
class="type">void CProgram::UpdateSeries(class="type">void) { class=class="str">"cmt">//--- 获取指向图表的指针 CGraphic *graph=m_graph1.GetGraphicPointer(); class=class="str">"cmt">//--- 更新图表的所有序列 class="type">int total=graph.CurvesTotal(); if(total>class="num">0) { class=class="str">"cmt">//--- 获取曲线指针 CCurve *curve=graph.CurveGetByIndex(class="num">0); class=class="str">"cmt">//--- 设置数据数组 curve.Update(x_norm,y_norm); class=class="str">"cmt">//--- 获取曲线属性的值 ENUM_CURVE_TYPE curve_type =(ENUM_CURVE_TYPE)m_curve_type.GetListViewPointer().SelectedItemIndex(); ENUM_POINT_TYPE point_type =(ENUM_POINT_TYPE)m_point_type.GetListViewPointer().SelectedItemIndex(); class=class="str">"cmt">//--- 设置属性 curve.LinesSmooth(m_line_smooth.IsPressed()); curve.PointsType(point_type); curve.Type(curve_type); } class=class="str">"cmt">//--- 应用 graph.Redraw(true); class=class="str">"cmt">//--- 输出文本 TextAdd(); class=class="str">"cmt">//--- 刷新图表 graph.Update(); } class="type">void CProgram::RecalculatingSeries(class="type">void) { class=class="str">"cmt">//--- 计算值并初始化数组 InitArrays(); class=class="str">"cmt">//--- 更新序列 UpdateSeries(); } class="type">void CProgram::OnTimerEvent(class="type">void) { CWndEvents::OnTimerEvent(); class=class="str">"cmt">//--- 按计时器刷新图表 if(m_counter1.CheckTimeCounter()) { UpdateGraphByTimer(); } } class="type">void CProgram::UpdateGraphByTimer(class="type">void) { class=class="str">"cmt">//--- 如果 (class="num">1) 窗体最小化或 (class="num">2) 动画被禁用, 离开 if(m_window.IsMinimized() || !m_animate.IsPressed())
「用静态标志位驱动序列来回伸缩」
这段 CProgram::AnimateGraphSeries 做的是图表序列的往复动画:到达边界就反向,再按步长加减当前值。核心是一个 static bool counter_direction,它跨帧保留方向,不会每次重进函数就复位。 判断边界直接用 m_animate.GetValue() 跟 MinValue()、MaxValue() 比双精度,触到最小置 false(代表递增),触到最大置 true(代表递减)。随后若未反向就加 StepValue(),反之减,写回 m_animate.SetValue 并刷新文本框指针。 你在 MT5 里接这套逻辑时,重点看 StepValue 设多大:步长过大,外汇或贵金属 1 分钟图上序列跳动会显突兀;这类品种波动随机且杠杆高,回测动画参数仅作界面验证,实盘方向仍可能反向。 别把正态当圣经 静态变量藏在类方法里很容易忘,调试时若发现序列只朝一个方向跑,先打印 counter_direction 初值,八成是某处提前改了它。
class="kw">return; class=class="str">"cmt">//--- 图表序列动画 AnimateGraphSeries(); class=class="str">"cmt">//--- 更新图表上的数组和序列 RecalculatingSeries(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将图表序列变为动画 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::AnimateGraphSeries(class="type">void) { class=class="str">"cmt">//--- 指定调整数组大小的方向 class="kw">static class="type">bool counter_direction=false; class=class="str">"cmt">//--- 如果达到最小值, 则切换方向 if((class="type">class="kw">double)m_animate.GetValue()<=(class="type">class="kw">double)m_animate.MinValue()) counter_direction=false; class=class="str">"cmt">//--- 如果达到最大值, 则切换方向 if((class="type">class="kw">double)m_animate.GetValue()>=(class="type">class="kw">double)m_animate.MaxValue()) counter_direction=true; class=class="str">"cmt">//--- 按指定的方向调整数组的大小 class="type">class="kw">string value=""; if(!counter_direction) value=class="type">class="kw">string((class="type">class="kw">double)m_animate.GetValue()+m_animate.StepValue()); else value=class="type">class="kw">string((class="type">class="kw">double)m_animate.GetValue()-m_animate.StepValue()); class=class="str">"cmt">//--- 设置新值并更新文本框 m_animate.SetValue(value,false); m_animate.GetTextBoxPointer().Update(true); }
旧测试程序的界面已同步刷新
上一版图形界面文章里配套的测试程序,在这次控件更新后已经跟进了代码调整,界面布局对应新控件行为做了适配。 更新后的 MQL 应用界面可参照原文图例 8,主要变化集中在进度条与折线图控件的渲染逻辑上,老工程直接编译可能会出现控件错位。 该测试程序仍开放下载,建议拉到本地 MT5 里跑一遍,重点看旧版进度条回调是否还能正常触发。外汇与贵金属品种加载此类自定义面板时波动放大,属高风险操作,验证前先开模拟账户。
◍ 别急着下结论
这套把科学绘图标准库接进 GUI 函数库的做法,目前已经能直接跑通文中附带的 easyandfastgui_mql5.zip(445.19 KB)示例,读者在 MT5 里解压后编译即可看到第 9 版函数库结构。代码以免费方式提供,商业项目也能用,但用户反馈里 yue zhao 在 2022-05-18 指出 Element.mqh 存在 CWindow 无主体、LastId/ChartId 等未声明标识符的编译错误,说明直接抄进新版本终端可能要手动补声明。 真要拿它做面板,先别默认‘下载即可用’:开 MT5 新建 EA 把 zip 里的文件挂进去,看终端日志是否报 Internal compiler error 或上述未声明标识符,再决定改不改基类。外汇和贵金属品种波动大、杠杆高,任何界面工具都只是降低操作摩擦,不替你过滤风险。 作者后续还写了 ZigZag 力量、通用 RSI 等系列,GUI 只是其中一块拼图;等你把当前这版跑顺了,再顺着调用链去翻那些文章也不迟。