开发回放系统(第 64 部分):玩转服务(五)·综合运用
📘

开发回放系统(第 64 部分):玩转服务(五)·综合运用

第 3/3 篇

「控件类的构造与析构细节」

在 MT5 自定义指标里做图形控件,第一步往往是写一个继承自终端基类的控件管理器。下面这段构造逻辑展示了如何在初始化阶段就接管图表的对象删除事件,并顺手清掉旧对象,避免残留按钮干扰新会话。 构造函数先校验指标名与鼠标指针有效性,任一不通过就抛自定义错误并直接 return,保证后续逻辑不会在空指针上跑飞。随后它临时关闭 CHART_EVENT_OBJECT_DELETE,用 ObjectsDeleteAll 按前缀清空同类控件,再重新打开事件——这一关一开能避免删除过程自身触发递归回调。 按钮布局写死在代码里:播放键 x 从 def_PosXObjects 起,左键偏移 +47 像素,右键偏移 +511 像素,所有按钮统一 25 像素高、def_SizeButtons 见方。这种硬编码间距在 1920 宽屏下可能显得稀疏,但在小窗回测里位置稳定可复现。 析构时逐个 delete 按钮对象并再次清空图表对象,最后释放鼠标指针。如果你在 MT5 里复用这套结构,注意 m_MousePtr 若由外部传入则析构中 delete 可能双重释放——改成仅当 CheckPointer 确认归属时再删更稳。

MQL5 / C++
C_Controls(class="kw">const class="type">long Arg0, class="kw">const class="type">class="kw">string szShortName, C_Mouse *MousePtr)
:C_Terminal(Arg0),
 m_MousePtr(MousePtr)
{
 if ((!IndicatorCheckPass(szShortName)) || (CheckPointer(m_MousePtr) == POINTER_INVALID)) SetUserError(C_Terminal::ERR_Unknown);
 if (_LastError != ERR_SUCCESS) class="kw">return;
 ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false);
 ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName(""));
 ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true);
 for (eObjectControl c0 = ePlay; c0 < eNull; c0++)
  {
  m_Section[c0].h = m_Section[c0].w = def_SizeButtons;
  m_Section[c0].y = class="num">25;
  m_Section[c0].Btn = NULL;
  }
 m_Section[ePlay].x = def_PosXObjects;
 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 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;

回放控件的事件分发逻辑

在 MT5 自定义回放面板里,图表事件必须靠 OnChartEvent 的 id 分流。下面这段 switch 处理了三类关键事件:自定义回放初始化、对象被手动删除、以及鼠标左键点击,缺一个都可能让滑块和播放键失联。 自定义事件 CHARTEVENT_CUSTOM+evCtrlReplayInit 进来时,先用联合体 uCast_Double 把 double 型的 dparam 重新解释成字节布局。第 184 行硬性校验第 7、6 字节必须是 'D' 和 'M',不符直接 break,这能拦掉非本面板发来的伪造事件。 对象删除事件里,若删的是播放按钮(sparam==def_ObjectCtrlName(ePlay)),代码会 delete 掉 Btn 指针并置 NULL,再调用 SetPlay 恢复状态;删的是滑块相关对象则走 RemoveCtrlSlider+CreateCtrlSlider 重建。手删图形后控件不消失,往往就是这里没接住。 鼠标移动事件 203 行起,先判左键按下再调 CheckPositionMouseClick(x,y) 决定点到了哪个子区域。下面逐行拆一下核心片段,方便你直接抄进 EA 改。

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

◍ 播放开关如何接管滑杆控件

在面板交互逻辑里,ePlay 这个分支负责切换行情回放状态。代码第 206 到 213 行展示了点击播放按钮后,程序如何反转当前状态并决定滑杆的创建或销毁。 当 m_Section[ePlay].state 变为 true,说明进入播放模式,此时先 RemoveCtrlSlider() 移除旧滑杆,再把 m_Slider.Minimal 设为 iPinPosX 以锁定左边界;若状态为 false,则直接 CreateCtrlSlider() 重建滑杆。这个写法能保证回放时滑杆不干扰锚点定位。 你在 MT5 自定义指标里接类似逻辑时,注意 iPinPosX 必须提前算好——它通常等于锚点柱的画布 X 坐标。若漏了这步,滑杆最小值会错位,回放进度条可能拖出可视区。外汇与贵金属图表加载此类控件时波动刷新频繁,实盘前请用历史数据验证控件重绘开销。

MQL5 / C++
case ePlay:
   SetPlay(!m_Section[ePlay].state);
   if (m_Section[ePlay].state)
   {
      RemoveCtrlSlider();
      m_Slider.Minimal = iPinPosX;
   }else CreateCtrlSlider();
   class="kw">break;

「收束」

MQL5 里指针并非不能重分配,只是语言层面不拦你——这反而比显式禁止更危险。作者自述多年经验仍栽在漏写第 82 行的已用指针检测、以及第 212 行鼠标事件里的重复触发调用上,说明哪怕逻辑简单的程序,少了那一行守卫代码就可能崩。 外汇与贵金属杠杆高、滑点猛,这类隐蔽 bug 在实盘里可能直接放大成穿仓级亏损,开 MT5 把这类守卫判断补进自己的 EA 前务必先跑一遍回测。 指针是利器也是暗雷,写完别急着上实盘,先对着历史 tick 把重分配路径全走一遍。

常见问题

先调用父级构造保证基类成员有效,再注册事件避免空指针;顺序反了滑杆可能点不动。
开关在析构时没释放对滑杆的引用,导致事件回环;退出前显式解绑即可解决。
可以,把控件代码贴给小布,它能标出事件漏注册或析构未解绑的位置,省去手动排查。
能在播放开关层拦,暂停态直接吞掉滑杆 CLICK 事件,不向下分发就不会误触。
漏调析构释放 GDI 对象和定时器;在收束函数里逐个 Delete 并置 NULL 最稳。