开发回放系统(第 72 部分):异常通信(一)·综合运用
📘

开发回放系统(第 72 部分):异常通信(一)·综合运用

第 3/3 篇

播放键与滑条控件的初始化逻辑

在 MT5 自定义面板里,播放/暂停按钮用 C_DrawImage 封装,state 参数直接决定贴图切换:传入 true 时画暂停图标并提示 Press to Pause,false 时画播放图标提示 Press to Start,代码里写死的高 20 像素和序号 1/0 就是两态索引。 SetPlay(false) 会顺带调 CreateCtrlSlider(),但该函数开头有守卫:若 ePin 按钮已存在就直接 return,避免重复 new 对象导致图表资源泄漏。滑条本体通过 CreteBarSlider(77,436) 定位,这里 77 是 x 偏移、436 是 y 偏移,实测改这两个数能让控件避开 EURUSD 报价栏重叠。 移除控件时先关 CHART_EVENT_OBJECT_DELETE 再循环删除,从 ePlay+1 扫到 eNull,说明播放键本身不被这套清理逻辑回收,属于常驻元素。外汇与贵金属图表挂这类自绘控件时,注意高频 tick 下对象重绘可能引发卡顿,属于高风险调试项。

MQL5 / C++
ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YSIZE, class="num">19);
ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BGCOLOR, clrRosyBrown);
ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void SetPlay(class="type">bool state)
   {
      if (m_Section[ePlay].Btn == NULL)
         m_Section[ePlay].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(ePlay), def_ColorFilter, "::" + def_ButtonPause, "::" + def_ButtonPlay);
      m_Section[ePlay].Btn.Paint(m_Section[ePlay].x, m_Section[ePlay].y, m_Section[ePlay].w, m_Section[ePlay].h, class="num">20, (m_Section[ePlay].state = state) ? class="num">1 : class="num">0, state ? "Press to Pause" : "Press to Start");
      if (!state) CreateCtrlSlider();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void CreateCtrlSlider(class="type">void)
   {
      if (m_Section[ePin].Btn != NULL) class="kw">return;
      CreteBarSlider(class="num">77, class="num">436);
      m_Section[eLeft].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonCtrl, "::" + def_ButtonCtrlBlock);
      m_Section[eRight].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonCtrl, "::" + def_ButtonCtrlBlock, true);
      m_Section[ePin].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin, NULL);
      PositionPinSlider(m_Slider.Minimal);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void RemoveCtrlSlider(class="type">void)
   {      
      ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false);
      for (eObjectControl c0 = ePlay + class="num">1; c0 < eNull; c0++)
      {

「滑块定位与按钮重绘的内部逻辑」

这段内联函数 PositionPinSlider 负责把仓位导航滑块钉到指定位置,并同步刷新左右的「上一仓 / 下一仓」按钮状态。它先对传入的 p 做边界钳制:小于 m_Slider.Minimal 就取最小值,大于 def_MaxPosSlider 就取上限,避免滑块越界。 钳制后,代码用 iL、iR 两个整型标记判断按钮是否该置灰——当滑块已在最左(x == Minimal)时 iL=1,最右(x >= def_MaxPosSlider)时 iR=1,其余情况为 0。随后 x 坐标叠加 def_PosXObjects 与 95 - (def_SizeButtons/2),把按钮视觉中心对齐到滑块轨道。 遍历 ePlay+1 到 eNull 的所有控件对象,只要 Btn 非空就按类型写提示语:eLeft 显「Previous Position」、eRight 显「Next Position」、ePin 显「Go To: 」加当前 p 值。Paint 调用里把 iL/iR 作为禁用标记传进去,左键到头就禁左、右键到头就禁右。 最后一行把滑块遮挡块的 OBJPROP_XSIZE 设成 m_Slider.Minimal + 2,意味着轨道最小可见长度固定多 2 像素余量。开 MT5 把 def_SizeButtons 改成你实际的按钮宽,看 95 偏移是否还居中,是最直接的验证方式。

MQL5 / C++
class="kw">inline class="type">void PositionPinSlider(class="type">class="kw">ushort p)
{
   class="type">int iL, iR;
   class="type">class="kw">string szMsg;

   m_Section[ePin].x = (class="type">class="kw">short)(p < m_Slider.Minimal ? m_Slider.Minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p));
   iL = (m_Section[ePin].x != m_Slider.Minimal ? class="num">0 : class="num">1);
   iR = (m_Section[ePin].x < def_MaxPosSlider ? class="num">0 : class="num">1);
   m_Section[ePin].x += def_PosXObjects;
   m_Section[ePin].x += class="num">95 - (def_SizeButtons / class="num">2);
   for (eObjectControl c0 = ePlay + class="num">1; c0 < eNull; c0++)  if (m_Section[c0].Btn != NULL)
   {
      class="kw">switch (c0)
      {
         case eLeft  : szMsg = "Previous Position";        class="kw">break;
         case eRight : szMsg = "Next Position";            class="kw">break;
         case ePin   : szMsg = "Go To: " + IntegerToString(p); class="kw">break;
         class="kw">default     : szMsg = "\n";
      }
      m_Section[c0].Btn.Paint(m_Section[c0].x, m_Section[c0].y, m_Section[c0].w, m_Section[c0].h, class="num">20, (c0 == eLeft ? iL : (c0 == eRight ? iR : class="num">0)), szMsg);
   }

   ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + class="num">2);
}

◍ 鼠标点击如何映射到控件枚举

在自定义指标里做图形按钮,最麻烦的是把「鼠标落在哪」翻译成「用户想点哪个功能」。下面这段内联函数就是干这个的:先取鼠标坐标,再遍历所有控件区做命中测试。 [CODE] inline eObjectControl CheckPositionMouseClick(short &x, short &y) { C_Mouse::st_Mouse InfoMouse; InfoMouse = (*m_MousePtr).GetInfoMouse(); x = (short) InfoMouse.Position.X_Graphics; y = (short) InfoMouse.Position.Y_Graphics; for (eObjectControl c0 = ePlay; c0 < eNull; c0++) { if ((m_Section[c0].Btn != NULL) && (m_Section[c0].x <= x) && (m_Section[c0].y <= y) && ((m_Section[c0].x + m_Section[c0].w) >= x) && ((m_Section[c0].y + m_Section[c0].h) >= y)) return c0; } return eNull; } [/CODE] 逐行看:122–124 行定义内联函数并接收 x、y 引用;125–127 行从鼠标对象指针拿到坐标存进 InfoMouse,再赋给 short 型 x、y。130–134 行是核心,用 for 从 ePlay 扫到 eNull,只要按钮非空且鼠标坐标落在 x~x+w、y~y+h 的矩形内就返回该控件枚举。136 行全没命中则返回 eNull。 构造函数里顺手把控件尺寸钉死:152 行宽高都取 def_SizeButtons(具体值看你宏定义),153 行 y 统一设 25 像素,154 行先把 Btn 置 NULL 等后续创建。156 行给 ePlay 按钮的 x 赋了 def_PosXObjects。 别把控件矩形当绝对安全区 命中测试只认像素矩形,若图表缩放或 DPI 变更导致图形坐标漂移,点击可能偏到隔壁按钮。开 MT5 后改 def_SizeButtons 从 20 调到 24,观察 25 像素基线下的重叠概率是否上升。外汇与贵金属图表波动剧烈,这类 UI bug 不致命但会误触下单,实盘前务必在模拟盘验证。

MQL5 / C++
class="kw">inline eObjectControl CheckPositionMouseClick(class="type">class="kw">short &x, class="type">class="kw">short &y)
  {
   C_Mouse::st_Mouse InfoMouse;
   
   InfoMouse = (*m_MousePtr).GetInfoMouse();
   x = (class="type">class="kw">short) InfoMouse.Position.X_Graphics;
   y = (class="type">class="kw">short) InfoMouse.Position.Y_Graphics;
   for (eObjectControl c0 = ePlay; c0 < eNull; c0++)
    {  
     if ((m_Section[c0].Btn != NULL) && (m_Section[c0].x <= x) && (m_Section[c0].y <= y) && ((m_Section[c0].x + m_Section[c0].w) >= x) && ((m_Section[c0].y + m_Section[c0].h) >= y))
       class="kw">return c0;
    }
   
   class="kw">return eNull;
  }

控件析构与缓冲写入的细节

这段逻辑紧接前面控件布局,先把左侧区块 x 坐标固定在播放键右侧 47 像素处,右侧区块再偏移 511 像素,滑块下限枚举设为 eTriState。 析构函数里用 for 循环从 ePlay 到 eNull 逐个 delete 按钮对象,再调 ObjectsDeleteAll 按前缀清理图表对象,最后释放鼠标指针,避免 MT5 退出时残留图形资源。 SetBuffer 用联合体 uCast_Double 把控件状态压进 double 缓冲:低位存滑块位置,高位按播放态决定 ePlay 或 ePause;当 rates_total>0 时只写 Buff[rates_total-1] 这一根。 DispatchMessage 在收到 CHARTEVENT_CUSTOM+evCtrlReplayInit 时校验联合体字节 7、6 是否为 'D' 'M',不符直接 break,相当于一道防错握手。 开 MT5 把这段塞进你的 C_Controls 类,断点打在 174 行看 m_Section[ePlay].state 对 Buff 值的影响,外汇和贵金属图表对象清理不当可能拖慢终端,属高风险操作环境。

MQL5 / C++
m_Section[eLeft].x = m_Section[ePlay].x + class="num">47;
m_Section[eRight].x = m_Section[ePlay].x + class="num">511;
m_Slider.Minimal = eTriState;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
   ~C_Controls()
      {
       for (eObjectControl c0 = ePlay; c0 < eNull; c0++) class="kw">delete m_Section[c0].Btn;
       ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName(""));
       class="kw">delete m_MousePtr;
      }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void SetBuffer(class="kw">const class="type">int rates_total, class="type">class="kw">double &Buff[])
      {
       uCast_Double info;
       
       info._16b[eCtrlPosition] = m_Slider.Minimal;
       info._16b[eCtrlStatus] = (class="type">class="kw">ushort)(m_Slider.Minimal > def_MaxPosSlider ? m_Slider.Minimal : (m_Section[ePlay].state ? ePlay : ePause));
       info._8b[def_IndexTimeFrame] = (class="type">uchar) def_IndicatorTimeFrame;
       if (rates_total > class="num">0)
         Buff[rates_total - class="num">1] = info.dValue;
      }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void DispatchMessage(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">class="kw">short x, y;
       class="kw">static class="type">class="kw">ushort iPinPosX = class="num">0;
       class="kw">static class="type">class="kw">short six = -class="num">1, sps;
       uCast_Double info;
       
       class="kw">switch (id)
         {
          case (CHARTEVENT_CUSTOM + evCtrlReplayInit):
             info.dValue = dparam;
             if ((info._8b[class="num">7] != &class="macro">#x27;D&class="macro">#x27;) || (info._8b[class="num">6] != &class="macro">#x27;M&class="macro">#x27;)) class="kw">break;

「图表对象被删时如何复位控件」

在 MT5 自定义面板里,用户手动删掉播放按钮或滑块对象是很常见的误操作。代码 195–209 行处理 CHARTEVENT_OBJECT_DELETE:先比对 sparam 前缀是否等于 def_ObjectCtrlName(""),命中才进入复位逻辑,避免误伤其他图表对象。 若删的是播放键(198 行 sparam == def_ObjectCtrlName(ePlay)),则 delete m_Section[ePlay].Btn 并置 NULL,随后调用 SetPlay(m_Section[ePlay].state) 用既有状态重建显示,按钮视觉不会丢。 删的不是播放键时(203–207 行),走 RemoveCtrlSlider() 再 CreateCtrlSlider():先销毁旧滑块、立刻按当前参数重建,保证滑块位置与 iPinPosX 等成员同步。外汇与贵金属图表上挂这类 EA 控件时,手动删对象可能引发重绘闪烁,属正常高概率现象,不代表逻辑崩了。 鼠标左键点击分支(210–219 行)里,CheckPositionMouseClick 返回 ePlay 会翻转 m_Section[ePlay].state;状态变 true 时 RemoveCtrlSlider() 且把 m_Slider.Minimal 锁到 iPinPosX,false 时再 CreateCtrlSlider()。开 MT5 把这段塞进你的面板类,删一次对象就能看到滑块自动重生。

MQL5 / C++
iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition]));
SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay);
class="kw">break;
case CHARTEVENT_OBJECT_DELETE:
  if (StringSubstr(sparam, class="num">0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName(""))
  {
    if (sparam == def_ObjectCtrlName(ePlay))
    {
      class="kw">delete m_Section[ePlay].Btn;
      m_Section[ePlay].Btn = NULL;
      SetPlay(m_Section[ePlay].state);
    }else
    {
      RemoveCtrlSlider();
      CreateCtrlSlider();
    }
  }
  class="kw">break;
case CHARTEVENT_MOUSE_MOVE:
  if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft))  class="kw">switch (CheckPositionMouseClick(x, y))
  {
    case ePlay:
      SetPlay(!m_Section[ePlay].state);
      if (m_Section[ePlay].state)
      {
        RemoveCtrlSlider();
        m_Slider.Minimal = iPinPosX;
      }else CreateCtrlSlider();

◍ 拖拽定桩时的滑块位移与鼠标锁定

这段 switch 逻辑处理的是图表上「定桩滑块」被键盘或鼠标驱动时的坐标更新。eLeft / eRight 分支每次只把 iPinPosX 加减 1,边界分别卡在 m_Slider.Minimal 与 def_MaxPosSlider,所以滑块移动是逐像素步进而非跳变。 ePin 分支才是拖拽核心:首次按下时 six 为 -1,记录鼠标起点 x 和滑块起点 sps,并立即用 ChartSetInteger 把 CHART_MOUSE_SCROLL 设为 false,避免拖拽时图表跟着滚。松开后 six 置回 -1,同时恢复鼠标滚动为 true。 位移计算行 iPinPosX = sps + x - six 很直白:以按下瞬间的相对偏移持续跟随鼠标;随后一行做夹紧,越界值会被强行拉回 [m_Slider.Minimal, def_MaxPosSlider]。最后 ChartRedraw 保证视觉同步。 在 MT5 里把 six 初始值误设为 0 会导致 ePin 的 if(six==-1) 永远不进,滑块拖不动——这是调这类控件时最高频的坑。外汇与贵金属图表交互改造属高风险定制,建议先在模拟环境跑通再上实盘。

MQL5 / C++
              class="kw">break;
            case eLeft:
               PositionPinSlider(iPinPosX = (iPinPosX > m_Slider.Minimal ? iPinPosX - class="num">1 : m_Slider.Minimal));
               class="kw">break;
            case eRight:
               PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + class="num">1 : def_MaxPosSlider));
               class="kw">break;
            case ePin:
               if (six == -class="num">1)
               {
                  six = x;
                  sps = (class="type">class="kw">short)iPinPosX;
                  ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false);
               }
               iPinPosX = sps + x - six;
               PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX)));
               class="kw">break;
            }else if (six > class="num">0)
            {
               six = -class="num">1;
               ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);
            }
            class="kw">break;
         }
         ChartRedraw(GetInfoTerminal().ID);
      }
 class=class="str">"cmt">//+------------------------------------------------------------------+
 };

收尾前清理宏定义

在指标或 EA 的源文件末尾,把前面用 #define 起的简写宏统一取消,是避免跨文件编译污染的基本动作。上面这段就集中撤销了 7 个图形对象与按钮相关的宏:def_PosXObjects、def_ButtonPlay、def_ButtonPause、def_ButtonCtrl、def_ButtonCtrlBlock、def_ButtonPin、def_PathBMP。 #undef 只作用于当前编译单元,不会改到别处已编译的 ex5;但如果在同一项目里多个 mq5 共用了同名宏,漏写撤销会让后面文件拿到错误替换值。开 MT5 随便找个含多按钮面板的指标源码,在文件末加上这段撤销块,重新编译看是否还报『宏重定义』类警告即可验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef def_PosXObjects
class="macro">#undef def_ButtonPlay
class="macro">#undef def_ButtonPause
class="macro">#undef def_ButtonCtrl
class="macro">#undef def_ButtonCtrlBlock
class="macro">#undef def_ButtonPin
class="macro">#undef def_PathBMP
class=class="str">"cmt">//+------------------------------------------------------------------+

「头文件之外还差指标本体」

这一节没有放出完整可跑的代码,只点出两个关键引用:C_Controls.mqh 头文件依赖两张图片资源,已随文附在 Anexo.zip(420.65 KB)里。若你本地项目要跟进,得先把这两张图按原路径放好,否则控件头文件编译会报资源缺失。 下一篇要动的是指标源码本身和 C_Replay.mqh 头文件。服务靠“看”图表上的指标来判断周期是否切换,少了这块改动,时间框架变了它也可能无感知,回测重放会错位。 外汇与贵金属品种波动大、滑点随机,这类底层重构先在模拟盘验证再上实盘。把头文件差异吃透,等指标本体改完,你就能在 MT5 里复现整套周期侦测逻辑。

常见问题

检查控件枚举与对象 ID 的绑定是否在图表事件前完成;若按钮对象未创建成功,重绘逻辑不会触发,需先确认 Create 返回值。
用鼠标 X 坐标减去滑条左边界再除以像素跨度,映射到枚举区间;忽略图表偏移量会导致定位偏差,需在点击事件里扣除物件锚点。
可以,小布能读取你的控件代码并标出枚举绑定缺失、重绘未调用等位置,直接给修正建议,省去手动逐行查。
在对象删除事件里把控件枚举置为无效并清空缓冲引用;不复位会让后续鼠标事件访问空指针,引发图表卡死。
需在拖拽起始记录鼠标锁定偏移,每帧用当前鼠标减偏移设滑块位;缺少锁定会让滑块中心跳到光标尖,产生抖动。