开发回放系统(第 61 部分):玩转服务(二)·进阶篇
(2/3)· 服务跑起来却莫名变慢?本篇拆解图形对象误用如何拖垮回放系统
控件销毁与滑块定位的底层写法
面板退出或重建时,先关掉图表的对象删除事件回调,防止循环触发。代码里用 ChartSetInteger 把 CHART_EVENT_OBJECT_DELETE 设为 false,再逐个 delete 按钮指针并把数组槽位置 NULL,最后 ObjectsDeleteAll 按前缀清场,完事再把事件开关打开。 PositionPinSlider 负责把滑块位置夹在 Minimal 到 def_MaxPosSlider 之间,越界就吸边。注意第 110 行硬加了 95 - (def_SizeButtons/2) 的偏移量,按钮中心对齐逻辑全靠这组魔法数,改 def_SizeButtons 时这里必须同步调,否则控件会偏出可视区。 CheckPositionMouseClick 把鼠标图形坐标直接写回引用参数 x、y,遍历 ePlay 到 eNull 的控件枚举做命中判断。外汇与贵金属图表上挂这类自绘 UI,高频重绘可能引发卡顿,实盘前应在 MT5 策略测试器用真实tick跑一遍确认资源释放干净。
ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, class="kw">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="kw">inline class="type">void PositionPinSlider(class="type">class="kw">ushort p) { class="type">int iL, iR; m_Section[ePin].x = (class="type">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++) 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))); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + class="num">2); } class="kw">inline eObjectControl CheckPositionMouseClick(class="type">short &x, class="type">short &y) { C_Mouse::st_Mouse InfoMouse; InfoMouse = (*m_MousePtr).GetInfoMouse(); x = (class="type">short) InfoMouse.Position.X_Graphics; y = (class="type">short) InfoMouse.Position.Y_Graphics; for (eObjectControl c0 = ePlay; c0 < eNull; c0++) {
◍ 控件命中检测与构造期的坐标排布
这段逻辑先解决一个实操问题:鼠标点下去,怎么知道命中了哪个按钮。125 到 126 行用四个边界条件(x/y 下限、x+w/y+h 上限)判断坐标是否落在 m_Section[c0] 的矩形内,命中就直接返回该控件索引 c0;若遍历完都没中,129 行返回 eNull,调用方据此走“点空白”分支。
- 到 153 行是 C_Controls 的构造函数,进场先卡两道校验:指标名不通过或鼠标指针无效就 SetUserError,139 行发现 _LastError 非 ERR_SUCCESS 直接 return,避免后续在脏状态下建对象。
建按钮前有个细节容易踩坑:140 行先关掉 CHART_EVENT_OBJECT_DELETE,141 行用 ObjectsDeleteAll 清掉旧控件对象,142 行再重新打开该事件——防止清对象时触发删除回调造成递归。 按钮布局是硬编码的:145 到 147 行把每个控件的宽高设为 def_SizeButtons、y 统一为 25、Btn 指针置 NULL;149 到 151 行给出 x 坐标,ePlay 在 def_PosXObjects,eLeft 偏移 +47,eRight 偏移 +511。想加新按钮,改这三个偏移量比改循环体更稳。
- 到 159 行的析构里,157 行逐个 delete 按钮对象、158 行再清一次图表对象、159 行删鼠标指针——外汇/贵金属 EA 面板若频繁重加载,漏掉其中任一步都可能留下孤儿对象拖慢 MT5 终端。
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; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ 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, class="kw">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;
「回放控件的缓冲区与消息派发」
这段逻辑负责把滑块控件的状态写进指标缓冲区,并响应图表自定义事件。SetBuffer 函数在每根 K 线收尾时把当前帧的控制字写入 Buff[rates_total-1],也就是说你能在数据窗口最后一列看到实时回放位置。 代码里用了一个 uCast_Double 联合体做双精度与 16 位短整数数组的 reinterpret,_16b[eCtrlPosition] 存滑块最小位置,_16b[eCtrlStatus] 存播放或暂停标记。若 m_Slider.Minimal 超过 def_MaxPosSlider,状态位会强制夹在最大值内,避免越界写坏图形。 DispatchMessage 监听 CHARTEVENT_CUSTOM+evCtrlReplayInit,先校验 info._8b[7]=='D' 且 info._8b[6]=='M' 才认作合法回放指令,否则直接 break。通过这层校验后,iPinPosX 被更新为约束后的滑块位置,并调用 SetPlay 切换播放态。 想验证的话,在 MT5 里给图表发一笔自定义事件,dparam 低字节不是 'D''M' 时控件不会动;把 def_MaxPosSlider 改小到 100,滑块拖到 200 也会卡在 100,外汇与贵金属行情跳空频繁,这类回放边界条件最好先在模拟盘跑通。
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));class=class="str">"cmt">//SHORT_MAX : SHORT_MIN); 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">short x, y; class="kw">static class="type">class="kw">ushort iPinPosX = class="num">0; class="kw">static class="type">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; x = (class="type">short) info._16b[eCtrlPosition]; 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("")) {
播放键与滑杆的交互拆解
这段事件处理代码管两件事:对象被删除时怎么清场,以及鼠标左键点下去后播放键和滑杆怎么联动。外汇与贵金属图表上挂这类自定义控件,先想清楚内存和状态不同步带来的闪退风险。 当传入参数 sparam 等于播放控件名时,先 delete 掉 m_Section[ePlay].Btn 指针并把其置为 NULL,再调用 SetPlay 按当前 state 重画;否则走 RemoveCtrlSlider 加 CreateCtrlSlider 重建滑杆。191–200 行就是这套分支,注意 delete 后不置 NULL 在 MT5 后续访问中可能触发无效指针异常。 鼠标移动事件里,若检测到左键按下,按坐标命中区域分流:点中 ePlay 就翻转 state,播放态下移除滑杆并把 m_Slider.Minimal 锁到 iPinPosX,非播放态重建滑杆(204–212 行)。点 eLeft / eRight 则把 iPinPosX 减 1 或加 1,并夹在 Minimal 与 def_MaxPosSlider 边界内(214–218 行)。 开 MT5 把这段塞进你的面板类,故意不写 194 行置空那句,跑几次拖拽滑杆大概率能复现崩溃,验证指针清理的必要性。
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(); 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));
◍ 拖拽滑块时如何锁住图表滚动
这段收尾逻辑处理的是图表上『图钉滑块』的鼠标拖拽:按下图钉后先在 ePin 分支里记录起始坐标 six 与 sps,同时把 CHART_MOUSE_SCROLL 设为 false,等于临时冻住图表的鼠标滚轮平移,避免拖控件时画面乱跑。 代码第 227–228 行按位移重算 iPinPosX,并用三元裁剪把它限制在 m_Slider.Minimal 到 def_MaxPosSlider 之间,再送回 PositionPinSlider 刷新位置;越界部分直接被丢掉,滑块不会跑出轨道。 松开判定落在 six>0 的 else 分支:把 six 复位成 -1,并重新打开 CHART_MOUSE_SCROLL(true),图表滚动恢复。最后 ChartRedraw 强制重绘一次,确保 MT5 主图立刻反映新位置。 想验证就开 MT5 把这段嵌进你的面板类,按住图钉拖一下,若发现滚轮还能平移说明 six 初始值没进 ePin 分支;外汇与贵金属图表多周期切换频繁,这类锁定逻辑能明显降低误触风险。
class="num">219. class="kw">break; class="num">220. case ePin: class="num">221. if (six == -class="num">1) class="num">222. { class="num">223. six = x; class="num">224. sps = (class="type">short)iPinPosX; class="num">225. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, class="kw">false); class="num">226. } class="num">227. iPinPosX = sps + x - six; class="num">228. PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX))); class="num">229. class="kw">break; class="num">230. }else if (six > class="num">0) class="num">231. { class="num">232. six = -class="num">1; class="num">233. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); class="num">234. } class="num">235. class="kw">break; class="num">236. } class="num">237. ChartRedraw(GetInfoTerminal().ID); class="num">238. } class="num">239. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">240. }; class="num">241. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">242. class="macro">#undef def_PosXObjects class="num">243. class="macro">#undef def_ButtonPlay class="num">244. class="macro">#undef def_ButtonPause class="num">245. class="macro">#undef def_ButtonLeft class="num">246. class="macro">#undef def_ButtonRight class="num">247. class="macro">#undef def_ButtonPin class="num">248. class="macro">#undef def_PathBMP class="num">249. class=class="str">"cmt">//+------------------------------------------------------------------+