开发回放系统(第 51 部分):事情变得复杂(三)·进阶篇
◍ 用矩形标签拼出滑块轨道
在 MT5 里做自定义控件,滑块的底轨通常用 OBJ_RECTANGLE_LABEL 画出来,而不是去碰图表上的价格对象。上面这段 CreteBarSlider 函数就是典型做法:先建一个长条矩形当轨道,再建一个短矩形当滑块块。 轨道的几何参数很直白——XDISTANCE 用传入的 x 加左偏移,YDISTANCE 取滑块基准位减 4 像素,XSIZE 由 size 参数决定,YSIZE 写死 9。颜色用 clrLightSkyBlue 做底、clrBlack 做边,边框宽度 3、类型 BORDER_FLAT,视觉上就是一条细扁的浅蓝槽。 注意第 073 行紧接着又 ObjectCreate 了一个 szBarSliderBlock,说明轨道和滑块块是分开两个对象管理的。你在 EA 里复刻时,若 size 传 200、x 传 0,就能在图表左上区域看到一条 200×9 的浅蓝轨道,外汇和贵金属图表都适用,但这类 overlay 控件密集时可能拖慢老机器渲染,属高风险环境下的性能隐患。
ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XDISTANCE, def_PosXObjects + x); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Slider.posY - class="num">4); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XSIZE, size); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YSIZE, class="num">9); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_WIDTH, class="num">3); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT); ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0);
滑块控件与播放键的底层摆位
在 MT5 面板里做自定义滑块,核心是用 ObjectSetInteger 逐个钉死坐标与外观属性。下面这段把滑块挡块放在距左边界 def_PosXObjects + x 像素、距顶 m_Slider.posY - 9 的位置,高度锁死 19 像素,底色用 clrRosyBrown,边框类型 BORDER_RAISED 做出凸起感。 播放/暂停按钮则靠 CreateObjectBitMap 画位图,再用 OBJPROP_STATE 切状态;当 m_bWait 为真时贴等待图,否则贴暂停或播放图。这种写法让你在 EA 面板里实时控回放进度,不依赖系统控件。 CreteCtrlSlider 里先给四个子对象拼名字:Bar、Bar Block、BtnL、BtnR,全部挂在 def_NameObjectsSlider 前缀下。外汇与贵金属图表加载这类面板时波动剧烈,对象重绘可能掉帧,建议在模拟盘先验证坐标偏移量。
ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, def_PosXObjects + x); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Slider.posY - class="num">9); 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="type">void CreateBtnPlayPause(class="type">bool state) { m_szBtnPlay = def_PrefixObjectName + "Play"; CreateObjectBitMap(class="num">0, class="num">25, m_szBtnPlay, (m_bWait ? def_ButtonWait : def_ButtonPause), (m_bWait ? def_ButtonWait : def_ButtonPlay)); ObjectSetInteger(GetInfoTerminal().ID, m_szBtnPlay, OBJPROP_STATE, state); } class="type">void CreteCtrlSlider(class="type">void) { u_Interprocess Info; m_Slider.szBarSlider = def_NameObjectsSlider + " Bar"; m_Slider.szBarSliderBlock = def_NameObjectsSlider + " Bar Block"; m_Slider.szBtnLeft = def_NameObjectsSlider + " BtnL"; m_Slider.szBtnRight = def_NameObjectsSlider + " BtnR";
「滑杆控件的创建与回收细节」
这段初始化逻辑在 MT5 自定义回放面板里搭出一个水平滑杆:滑块锚点固定在 Y=40,轨道与两侧按钮分别落在 X=77~436、左键位 X=47、右键位 X=511 的坐标上,拖拽钮位图尺寸 25×25 且锚点设为居中。 第 103 行先查全局变量 def_GlobalVariableReplay,若存在就取回放进度值,否则归零;第 104~105 行把滑杆下限锁到 Info.s_Infos.iPosShift 并立刻把拖钮摆到该位置,避免首次绘制时钮跑偏。 RemoveCtrlSlider 在删除前先把 CHART_EVENT_OBJECT_DELETE 关掉,ObjectsDeleteAll 清掉 def_NameObjectsSlider 前缀的所有图元,再恢复事件开关——不这样做,批量删对象会触发一堆误删回调。 PositionPinSlider 里那句三元把传入位置夹在 minimal 与 def_MaxPosSlider 之间,越界值直接被削平,实盘里改 def_MaxPosSlider 就能拉长回放窗口。
m_Slider.szBtnPin = def_NameObjectsSlider + " BtnP"; m_Slider.posY = class="num">40; CreteBarSlider(class="num">77, class="num">436); CreateObjectBitMap(class="num">47, class="num">25, m_Slider.szBtnLeft, def_ButtonLeft, def_ButtonLeftBlock); CreateObjectBitMap(class="num">511, class="num">25, m_Slider.szBtnRight, def_ButtonRight, def_ButtonRightBlock); CreateObjectBitMap(class="num">0, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER); if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.df_Value = class="num">0; m_Slider.Minimal = Info.s_Infos.iPosShift; PositionPinSlider(Info.s_Infos.iPosShift); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void RemoveCtrlSlider(class="type">void) { ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); ObjectsDeleteAll(GetInfoTerminal().ID, def_NameObjectsSlider); ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void PositionPinSlider(class="type">int p, class="kw">const class="type">int minimal = class="num">0) { m_Slider.posPinSlider = (p < minimal ? minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p));
◍ 滑块位置钳制与按钮状态同步的坑
在自定义滑条控件里,传入参数 p 必须先做边界钳制,否则拖出范围会令界面对象越界。第118行用三元表达式把 p 限制在 m_Slider.Minimal 与 def_MaxPosSlider 之间,写进 m_Slider.posPinSlider,这一步决定后面所有 ObjectSetInteger 的基准坐标。 第119行把图钉按钮的水平距离设为 posPinSlider + def_PosXObjects + 95,这里的 95 是图钉图标相对滑条块的固定偏移量,调 UI 时若发现图钉和滑条脱节,先核这个数。 左右箭头按钮的可用状态靠 OBJPROP_STATE 控制:左键在 posPinSlider 不等于 Minimal 时才亮(120、121行存在重复赋值,后者覆盖前者,实际以 m_Slider.Minimal 为准);右键在 posPinSlider 小于 def_MaxPosSlider 时亮(122行)。 滑条遮罩块宽度在123、124行也被重复设为 minimal+2 与 m_Slider.Minimal+2,同样后者生效。每轮改完调 ChartRedraw 重绘(125行),缺这行拖动会卡帧。 构造函数里把按钮与滑条对象指针先置 NULL(136–139行),是防止 IndicatorCheckPass 失败后仍误访问野指针。外汇贵金属面板高频重绘,这类控件若状态机写错,可能拖累 MT5 主图响应,实盘前务必在策略测试器里拖满边界验证。
m_Slider.posPinSlider = (p < m_Slider.Minimal ? m_Slider.Minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_PosXObjects + class="num">95); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != m_Slider.Minimal); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, minimal + class="num">2); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + class="num">2); ChartRedraw(GetInfoTerminal().ID); } 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_bWait(false), m_MousePtr(MousePtr) { if (!IndicatorCheckPass(szShortName)) SetUserError(C_Terminal::ERR_Unknown); m_szBtnPlay = NULL; m_Slider.szBarSlider = NULL; m_Slider.szBtnPin = NULL; m_Slider.szBtnLeft = NULL;
控件析构与消息派发的底层接法
回放面板的 C_Controls 类在析构时做了两件关键事:先关掉图表的对象删除事件监听,再按统一前缀清掉所有自建图形对象。这样切图表或移除 EA 时不会留下孤儿按钮,也不会误删用户手动画的线。 具体看析构函数,ChartSetInteger 把 CHART_EVENT_OBJECT_DELETE 设回 false,ObjectsDeleteAll 只删 def_PrefixObjectName 前缀的物件,第 146 行这个前缀隔离是避免误伤的核心。 Init 方法根据 state 决定建播放键还是建滑条:state 为真只建按钮并注册临时全局变量 def_GlobalVariableReplay,为假才 CreteCtrlSlider(注意原文拼写缺了 a),最后 ChartRedraw 强制重绘一次。 DispatchMessage 用 switch 接 CHARTEVENT_CUSTOM+ev_WaitOn,第 163–164 行对 lparam==0 直接 break,说明子进程没传有效句柄时主线程不往下走,避免空指针刷图表。 开 MT5 把这段粘进类里,故意把 def_PrefixObjectName 改成你自己 EA 的缩写,切周期看物件是否残留,就能验证前缀隔离是否生效。外汇与贵金属图表物件交互属高风险操作,误删可能打断你的手动止损线。
m_Slider.szBtnRight = NULL; } class=class="str">"cmt">//+------------------------------------------------------------------+ ~C_Controls() { ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); ObjectsDeleteAll(GetInfoTerminal().ID, def_PrefixObjectName); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Init(class="kw">const class="type">bool state) { CreateBtnPlayPause(state); GlobalVariableTemp(def_GlobalVariableReplay); if (!state) CreteCtrlSlider(); ChartRedraw(GetInfoTerminal().ID); } 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) { u_Interprocess Info; class="kw">switch (id) { case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOn): if (lparam == class="num">0) class="kw">break;
「自定义事件里的暂停与滑块回收」
在图表事件分支里,自定义事件 CHARTEVENT_CUSTOM 加上控件偏移量 ev_WaitOn / ev_WaitOff 用来切换回放暂停状态。收到 WaitOn 时把 m_bWait 置 true 并刷新播放按钮为暂停态,WaitOff 则要求 lparam 非 0 才生效,同时把 dparam 写回 Info.df_Value 作为当前帧值。 对象删除事件要防误删:只用前缀匹配 def_PrefixObjectName 的才归本控件管,进一步判断是否是 def_NameObjectsSlider 前缀的滑块对象。命中后先 RemoveCtrlSlider() 解绑旧滑块,紧接 CreteCtrlSlider() 重建一套,保证拖拽控件被手动删掉也能自愈。 这段逻辑在 MT5 上可直接粘进 OnChartEvent 的 switch 里验证:手动删掉滑块图形,控件会在下一帧重新画出来,回放不会卡死。外汇与贵金属图表加载此类自绘控件时波动剧烈,重建过程可能带来 1~2 帧闪烁,属正常概率现象。
m_bWait = true; CreateBtnPlayPause(true); class="kw">break; case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOff): if (lparam == class="num">0) class="kw">break; m_bWait = false; Info.df_Value = dparam; CreateBtnPlayPause(Info.s_Infos.isPlay); class="kw">break; case CHARTEVENT_OBJECT_DELETE: if (StringSubstr(sparam, class="num">0, StringLen(def_PrefixObjectName)) == def_PrefixObjectName) { if (StringSubstr(sparam, class="num">0, StringLen(def_NameObjectsSlider)) == def_NameObjectsSlider) { RemoveCtrlSlider(); CreteCtrlSlider();