图形界面 XI: 集成标准图形库 (统合构建 16)·综合运用
📊

图形界面 XI: 集成标准图形库 (统合构建 16)·综合运用

(3/3)· 函数库体积砍掉近四成,图表类改名重连,七个实测程序带你跑通统合构建 16

实战向 第 3/3 篇
很多人还在用旧版 CLineChart 硬画折线,函数库重构后它已更名为 CGraph,底层直接挂上标准库 CGraphic。继续照老样例抄,只会把坐标轴管理和曲线颜色生成写重复代码。

◍ 把内摆线搬进 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() 一把梭。

MQL5 / C++
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 分量。外汇与贵金属行情叠加这类几何映射属于高风险实验,参数敏感,实盘前务必在策略测试器用历史数据跑通边界。

MQL5 / C++
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」的实时标注,调参时一眼判断归一化是否生效。

MQL5 / C++
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 占用上升的风险。

MQL5 / C++
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 初值,八成是某处提前改了它。

MQL5 / C++
  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 只是其中一块拼图;等你把当前这版跑顺了,再顺着调用链去翻那些文章也不迟。

把图表联调交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到标准库类交互是否缺文件,你专注决策而非查 include 路径。

常见问题

通过 CGraphic::GetGraphicPointer() 方法获取指向 CGraphic 类实例的指针,之后可调用其内含的 CAxis、CCurve 等附加类操作坐标轴与曲线。
可以,小布盯盘的品种页能提示 Graphic.mqh、Curve.mqh 等包含关系是否完整,减少手动翻 include 出错的概率。
因为移除了开发中函数库里 CCanvas 的副本,改为直接引用标准库原始版本,并统一由 Graphic.mqh 向内包含,冗余代码被清掉。
它主要验证 CGraph 配合 CGraphic 绘制参数化曲线的流畅度,以及 CColorGenerator 在多条曲线下的配色表现。
关于从副本替换到标准库统合的完整讨论见《图形界面 XI: 集成标准图形库 (统合构建 16)》·基础篇与实战篇。