开发回放系统(第 64 部分):玩转服务(五)·进阶篇
📘

开发回放系统(第 64 部分):玩转服务(五)·进阶篇

第 2/3 篇

◍ 回放初始化时的内存标记与事件派发

在回放控制模块里,第53行把内存数组 _8b 的第6个字节写成字符 'M',这是一个内部状态标记,用于区分当前处于回放初始化阶段而非普通运行态。 第54行紧接着调用 EventChartCustom,向图表对象 m_Infos.IdReplay 发送自定义事件 evCtrlReplayInit,附带参数 0 与已写入标记的内存双精度值 m_IndControl.Memory.dValue,通知前端回放器完成握手。 这种写法把状态机推进和跨模块通信压缩在相邻两行,开 MT5 把这段贴进 EA 的 OnInit 末尾,改 IdReplay 为你自己的图表 ID,就能在日志里抓到 evCtrlReplayInit 的触发时机。

MQL5 / C++
m_IndControl.Memory._8b[class="num">6] = &class="macro">#x27;M&class="macro">#x27;;
EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, class="num">0, m_IndControl.Memory.dValue, "");
}

「图表关闭时的内存泄漏真凶」

在 MT5 自定义指标或 EA 运行中,图表被关闭的瞬间程序突然报错,很多人第一反应是平台 bug。其实多数情况是对象没被正确释放——MT5 只会按设计执行,垃圾回收的锅得开发者自己背。 问题集中在 C_Controls 类对 C_DrawImage 的间接引用上。C_DrawImage 通过指针声明(第47行),构造时初始化为 NULL(第148行),析构时 delete 并置 NULL(第95-96行)。逻辑看着没问题,但平台仍报警说 C_DrawImage 实例未删除。 根因在 new 的使用:CreateCtrlSlider 方法里第82行原本缺失了「指针已占用则跳过」的判断。第212行存在特殊调用路径,会在旧实例未释放时再次 new,重复分配内存。这些碎片累积到图表关闭时触发崩溃。补上第82行那一行检测,泄漏即消失。 MQL5 的指针和 C/C++ 不同,问题常在偶发交互中才暴露,可能测几百次不出事,某次关图就崩。凡是声明指针,构造函数里尽早初始化,delete 后立刻赋 NULL,这是保命习惯。 下面这段是修复后的 C_Controls.mqh 相关片段,注意第82行附近的守卫判断才是关键。

MQL5 / C++
<span class="number">class="num">001</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">002</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Daniel Jose"</span>
<span class="number">class="num">003</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">004</span>. <span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"..\Auxiliar\C_DrawImage.mqh"</span>
<span class="number">class="num">005</span>. <span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"..\Defines.mqh"</span>
<span class="number">class="num">006</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">007</span>. <span class="preprocessor">class="macro">#define </span>def_PathBMP&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"Images\\Market Replay\Control\\"</span>
<span class="number">class="num">008</span>. <span class="preprocessor">class="macro">#define </span>def_ButtonPlay&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; def_PathBMP + <span class="class="type">class="kw">string">"Play.bmp"</span>
<span class="number">class="num">009</span>. <span class="preprocessor">class="macro">#define </span>def_ButtonPause&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;def_PathBMP + <span class="class="type">class="kw">string">"Pause.bmp"</span>
<span class="number">class="num">010</span>. <span class="preprocessor">class="macro">#define </span>def_ButtonLeft&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; def_PathBMP + <span class="class="type">class="kw">string">"Left.bmp"</span>
<span class="number">class="num">011</span>. <span class="preprocessor">class="macro">#define </span>def_ButtonLeftBlock&nbsp;&nbsp;&nbsp;&nbsp;def_PathBMP + <span class="class="type">class="kw">string">"Left_Block.bmp"</span>
<span class="number">class="num">012</span>. <span class="preprocessor">class="macro">#define </span>def_ButtonRight&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;def_PathBMP + <span class="class="type">class="kw">string">"Right.bmp"</span>
<span class="number">class="num">013</span>. <span class="preprocessor">class="macro">#define </span>def_ButtonRightBlock&nbsp;&nbsp; def_PathBMP + <span class="class="type">class="kw">string">"Right_Block.bmp"</span>
<span class="number">class="num">014</span>. <span class="preprocessor">class="macro">#define </span>def_ButtonPin&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;def_PathBMP + <span class="class="type">class="kw">string">"Pin.bmp"</span>
<span class="number">class="num">015</span>. <span class="preprocessor">class="macro">#resource </span><span class="class="type">class="kw">string">"\\"</span> + def_ButtonPlay

回放控制面板的资源与对象布局

做 MT5 市场回放工具时,控制按钮不能直接画在图表上裸跑,得先把图片资源编译进 ex5。代码 016–021 行用 #resource 把暂停、左移、右移、钉住等 6 个 PNG 资源挂到程序体内,路径由 def_ButtonPause 这类宏拼出来,运行时用 C_DrawImage 贴图,避免依赖外部文件导致分发失败。 控件命名和坐标有固定约定。def_ObjectCtrlName(A) 宏(023 行)给每个控件拼出 MarketReplayCTRL_ 前缀,枚举类控件还会用 EnumToString 转成可读名;按钮统一尺寸 32 像素(def_SizeButtons),左侧控件起始 X 坐标锁在 120(def_PosXObjects),洋红 0xFF00FF 作透明过滤色。 内部用两个结构体管状态。st_00 存滑条对象名和最小可拖值,st_01eObjectControl 枚举为下标建数组,每个元素含图像指针、布尔状态及 x/y/w/h 四向坐标(049 行)。SetPlay(bool state) 这种接口在赋值前会判空 m_Section[ePlay].Btn == NULL,防止未初始化按钮被误触。 开 MT5 新建 EA 把这段资源宏抄进去,改 def_PosXObjects 到 200 看看按钮会不会和主图指标重叠——外汇与贵金属回测属高风险验证,参数冲突可能让控件整组失效。

MQL5 / C++
class="macro">#resource "\" + def_ButtonPause
class="macro">#resource "\" + def_ButtonLeft
class="macro">#resource "\" + def_ButtonLeftBlock
class="macro">#resource "\" + def_ButtonRight
class="macro">#resource "\" + def_ButtonRightBlock
class="macro">#resource "\" + def_ButtonPin
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_ObjectCtrlName(A) "MarketReplayCTRL_" + (class="kw">typename(A) == "enum eObjectControl" ? EnumToString((C_Controls::eObjectControl)(A)) : (class="type">class="kw">string)(A))
class="macro">#define def_PosXObjects           class="num">120
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_SizeButtons           class="num">32
class="macro">#define def_ColorFilter           0xFF00FF
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "..\Auxiliar\C_Mouse.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Controls : class="kw">private C_Terminal
{
   class="kw">protected:
   class="kw">private  :
class=class="str">"cmt">//+------------------------------------------------------------------+
      enum eMatrixControl {eCtrlPosition, eCtrlStatus};
      enum eObjectControl {ePause, ePlay, eLeft, eRight, ePin, eNull, eTriState = (def_MaxPosSlider + class="num">1)};
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="kw">struct st_00
      {
         class="type">class="kw">string  szBarSlider,
                 szBarSliderBlock;
         class="type">class="kw">ushort  Minimal;
      }m_Slider;
      class="kw">struct st_01
      {
         C_DrawImage *Btn;
         class="type">bool        state;
         class="type">class="kw">short       x, y, w, h;
      }m_Section[eObjectControl::eNull];
      C_Mouse  *m_MousePtr;
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="type">void SetPlay(class="type">bool state)
      {
         if (m_Section[ePlay].Btn == NULL)

◍ 播放条与滑块控件的创建和销毁

这段逻辑负责在面板处于暂停态时,把播放/暂停按钮以及时间轴滑块相关控件一次性画出来。第 075–076 行先 new 一个 C_DrawImage 作为播放按钮,Paint 时传入圆角参数 20,并根据 state 决定显示暂停图(1)还是播放图(0);若 state 为假,第 077 行才调用 CreateCtrlSlider() 生成其余控件。 CreateCtrlSlider 内部用 m_Section[ePin].Btn 是否为 NULL 做幂等保护,避免重复创建。第 083 行 CreteBarSlider(77, 436) 把滑条画在距左 77、距顶 436 像素的位置,随后依次 new 左移、右移、固定针三个图像按钮,并把 Pin 滑块初始对齐到 m_Slider.Minimal。 RemoveCtrlSlider 先关掉 CHART_EVENT_OBJECT_DELETE 事件开关,再 for 循环从 ePlay+1 到 eNull 逐个 delete 按钮指针并置 NULL,最后 ObjectsDeleteAll 按前缀 "B" 清掉残留对象,再恢复删除事件。外汇与贵金属图表上挂这类自绘面板,需注意 MT5 对象事件可能在切换品种时误触发,属于高风险操作环境。 你在 MT5 里验证时,可直接把 ePin 按钮创建注释掉,看 Pin 滑块是否仍停在 Minimal——若位置偏移,说明 PositionPinSlider 依赖该对象先存在,调参顺序得改。

MQL5 / C++
m_Section[ePlay].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, 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);
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(GetInfoTerminal().ID, class="num">0, def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonLeft, "::" + def_ButtonLeftBlock);
m_Section[eRight].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonRight, "::" + def_ButtonRightBlock);
m_Section[ePin].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin);
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++)
{
class="kw">delete m_Section[c0].Btn;
m_Section[c0].Btn = NULL;
}
ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("B"));
ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

常见问题

在回放启动前用独立标志位记录已分配的内存块,若标志已置位则跳过分配,直接复用,可减少约 30% 的初始化耗时。
真凶多是未释放的定时器与图形对象句柄,关闭图表时要显式调用资源销毁函数并取消定时事件,否则句柄泄漏会持续占内存。
可以,小布能扫描当前品种页的自定义对象与定时器状态,列出未销毁的句柄并提示一键清理,你只管看盘决策。
按行列坐标常量布局,先算基准 X/Y 再递增加密,控件宽高写死在枚举里,创建时统一用循环避免手抖写错位置。
常见是滑块创建后没挂上自定义事件 ID,或销毁时漏了反注册,确认 OnEvent 里能收到对应 ID 且退出时 Delete 控件即可。