开发回放系统(第 64 部分):玩转服务(五)·进阶篇
◍ 回放初始化时的内存标记与事件派发
在回放控制模块里,第53行把内存数组 _8b 的第6个字节写成字符 'M',这是一个内部状态标记,用于区分当前处于回放初始化阶段而非普通运行态。 第54行紧接着调用 EventChartCustom,向图表对象 m_Infos.IdReplay 发送自定义事件 evCtrlReplayInit,附带参数 0 与已写入标记的内存双精度值 m_IndControl.Memory.dValue,通知前端回放器完成握手。 这种写法把状态机推进和跨模块通信压缩在相邻两行,开 MT5 把这段贴进 EA 的 OnInit 末尾,改 IdReplay 为你自己的图表 ID,就能在日志里抓到 evCtrlReplayInit 的触发时机。
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行附近的守卫判断才是关键。
<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 <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 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 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 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 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 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 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 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_01 以 eObjectControl 枚举为下标建数组,每个元素含图像指针、布尔状态及 x/y/w/h 四向坐标(049 行)。SetPlay(bool state) 这种接口在赋值前会判空 m_Section[ePlay].Btn == NULL,防止未初始化按钮被误触。
开 MT5 新建 EA 把这段资源宏抄进去,改 def_PosXObjects 到 200 看看按钮会不会和主图指标重叠——外汇与贵金属回测属高风险验证,参数冲突可能让控件整组失效。
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 依赖该对象先存在,调参顺序得改。
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">//+------------------------------------------------------------------+