开发回放系统(第 67 部分):完善控制指标·进阶篇
(2/3)·低流动性模型崩了只是表象,真正的坑在控制指标代码僵化难扩展
「滑块拖动时按钮区的重绘逻辑」
在自定义面板里,滑块位置 p 被夹在 m_Slider.Minimal 与 def_MaxPosSlider 之间,越界值会被直接截断,不会抛异常。夹完之后给 m_Section[ePin].x 叠加 def_PosXObjects 与 95 - (def_SizeButtons/2),等于把图钉按钮的锚点向右平移了近 95 像素量级,具体偏移取决于按钮尺寸常量。 PositionPinSlider 里还顺手算了 iL、iR 两个边界标志:当图钉贴到 Minimal 时 iL=1,贴到 def_MaxPosSlider 时 iR=1,其余情况为 0。随后遍历 ePlay+1 到 eNull 的控件,把左、右箭头按钮按 iL / iR 置灰,滑块条本体宽度也被重设为 m_Slider.Minimal+2。 CheckPositionMouseClick 负责把鼠标图形坐标写回 x、y,并逐个比对 m_Section[c0] 的矩形包围盒。命中条件很直白:x、y 同时落在区间 [x, x+w] 与 [y, y+h] 内才返回对应控件枚举,否则回 eNull。开 MT5 把这段塞进你的 C++ 面板类,拖一下滑块就能看到按钮禁用态随边界翻转。
class="kw">inline class="type">void PositionPinSlider(class="type">class="kw">ushort p) { class="type">int iL, iR; m_Section[ePin].x = (class="type">class="kw">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=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline eObjectControl CheckPositionMouseClick(class="type">class="kw">short &x, class="type">class="kw">short &y) { C_Mouse::st_Mouse InfoMouse; InfoMouse = (*m_MousePtr).GetInfoMouse(); x = (class="type">class="kw">short) InfoMouse.Position.X_Graphics; y = (class="type">class="kw">short) InfoMouse.Position.Y_Graphics; for(eObjectControl c0 = ePlay; c0 < eNull; c0++) { 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; }
控件类的构造与缓冲写入细节
在 MT5 自定义指标里封装图形控件,构造函数先卡两道关:指标名校验不通过,或传入的鼠标指针对象无效,就直接抛用户错误并 return,避免后续在空对象上操作崩在 OnInit。 构造体内对图表事件做了「先关后开」的处理:先把 CHART_EVENT_OBJECT_DELETE 设 false,清掉旧控件对象,再设回 true。这样能防止删除历史对象时误触发删除事件,导致状态错乱。 按钮布局用枚举循环初始化,基础尺寸走 def_SizeButtons,y 坐标统一 25;x 方向硬编码偏移——播放键在 def_PosXObjects,左键 +47,右键 +511。改键位间距时直接调这几个数即可。 析构里逐按钮 delete 并清对象、释放鼠标指针,防止 IndicatorRelease 后残留 GDI 句柄。SetBuffer 把滑块最小态与播放态压进 double 的 16 位字段,eCtrlStatus 行显示:当 Minimal 超 def_MaxPosSlider 时记 Minimal,否则看播放键 state 落 ePlay 或 ePause。外汇贵金属图表加载此类控件须留意:图表对象交互频繁,误删事件可能引发重绘卡顿,属高风险操作环境。
C_Controls(const class="type">long Arg0, 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 SetBuffer(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)); if (rates_total > class="num">0)
◍ 回放控件的消息分发逻辑
指标里的 DispatchMessage 是回放控制面板与图表交互的入口,它接收来自滑块、按钮等图形对象的事件并做状态同步。 代码第 173 行定义了函数签名,参数含 id、lparam、dparam、sparam,分别对应事件类型、长整型、双精度与字符串载荷;第 176–177 行用 static 变量 iPinPosX、six、sps 在多次调用间保留滑块位置与状态,避免每次重绘归零。 第 182–187 行处理自定义回放初始化事件:先把 dparam 塞进 uCast_Double 联合体,再校验第 7、6 字节是否为 'D' 'M' 标记,不符直接 break;通过后才把滑块最小值钳制在 [iPinPosX, def_MaxPosSlider] 区间,并依状态字节切换播放态。 第 188–198 行监听对象删除事件:若删的是播放按钮(def_ObjectCtrlName(ePlay)),就释放 m_Section[ePlay].Btn 指针并复位状态;否则走 RemoveCtrlSlider() 清理滑块。外汇与贵金属图表加载此类自定义控件时波动剧烈,误删对象可能让回放态错乱,建议在 MT5 策略测试器里先跑一遍再上实盘。
class="type">void DispatchMessage(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, 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; 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;)) 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); 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();
「播放键与滑块的鼠标事件分支」
在 MT5 自定义指标里,图表鼠标移动事件(CHARTEVENT_MOUSE_MOVE)是交互控件的核心入口。上面这段逻辑先判断左键是否按下,再按点击位置分流到播放、左移、右移、钉住等子状态。 点击 ePlay 区域会翻转播放状态:开启时移除滑块控件并把滑块下限锁到当前钉住位置 iPinPosX(211 行 m_Slider.Minimal = iPinPosX),关闭时重建滑块(212 行 CreateCtrlSlider)。这意味着播放模式下不允许手动拖滑块,回放区间起点被固化。 eLeft / eRight 用 iPinPosX 加减 1 来步进,边界分别是 m_Slider.Minimal 和 def_MaxPosSlider(215、218 行),单像素步进在 1920 宽屏上约对应 2~3 根 M1 棒,手感偏细。 ePin 分支在 six == -1 时记录鼠标 x 坐标与滑块位置,并关闭图表鼠标滚动(225 行 CHART_MOUSE_SCROLL=false),避免钉选时画面乱飘。外汇与贵金属波动快,这类控件若没锁滚动,复盘时容易误触平移,建议实盘前在策略测试器里先跑一遍鼠标逻辑。
class="num">199. CreateCtrlSlider(); class="num">200. } class="num">201. } class="num">202. break; class="num">203. case CHARTEVENT_MOUSE_MOVE: class="num">204. if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) class="kw">switch (CheckPositionMouseClick(x, y)) class="num">205. { class="num">206. case ePlay: class="num">207. SetPlay(!m_Section[ePlay].state); class="num">208. if (m_Section[ePlay].state) class="num">209. { class="num">210. RemoveCtrlSlider(); class="num">211. m_Slider.Minimal = iPinPosX; class="num">212. }else CreateCtrlSlider(); class="num">213. break; class="num">214. case eLeft: class="num">215. PositionPinSlider(iPinPosX = (iPinPosX > m_Slider.Minimal ? iPinPosX - class="num">1 : m_Slider.Minimal)); class="num">216. break; class="num">217. case eRight: class="num">218. PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + class="num">1 : def_MaxPosSlider)); class="num">219. 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">class="kw">short)iPinPosX; class="num">225. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false);
拖拽定位与图像缓冲的类封装
上面这段鼠标事件处理,核心是把滑块像素位置 iPinPosX 夹在 m_Slider.Minimal 与 def_MaxPosSlider 之间,再调用 PositionPinSlider 写回。注意 233 行:当 six>0 时把 six 置 -1 并重新打开 CHART_MOUSE_SCROLL,意味着拖拽结束后立刻归还图表滚轮控制权,否则用户会卡在无法滚动的状态。 下面新开的 C_DrawImage 类继承自 C_Terminal,用 #define def_MaxImages 2 把图像缓冲上限锁死为 2 张。m_InfoImage 数组里每个元素带 widthMap、heightMap 和 uint 型 Map[],说明每张图以裸像素数组方式驻留内存,而不是依赖 OBJ_BITMAP 标签反复向图表抛对象。 你可以直接把 006 行的 def_MaxImages 改成 4 或 8,看 MT5 回放多图序列时内存占用和重绘帧率怎么变——外汇与贵金属行情跳空频繁,这类自管像素缓冲在高波动时段可能比系统对象更稳,但仍有图表资源争用风险。
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. 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. 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">//+------------------------------------------------------------------+ class="num">001. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">002. class="macro">#class="kw">property copyright "Daniel Jose" class="num">003. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">004. class="macro">#include "C_Terminal.mqh" class="num">005. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">006. class="macro">#define def_MaxImages class="num">2 class="num">007. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">008. class C_DrawImage : class="kw">public C_Terminal class="num">009. { class="num">010. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">011. class="kw">private : class="num">012. class="kw">struct st_00 class="num">013. { class="num">014. class="type">int widthMap, class="num">015. heightMap; class="num">016. class="type">uint Map[]; class="num">017. }m_InfoImage[def_MaxImages]; class="num">018. class="type">uint m_Pixels[]; class="num">019. class="type">class="kw">string m_szObjName,
◍ 用结构体直接啃 BMP 头
把位图塞进指标或 EA 做背景,第一步是把文件头读准。下面这段用 MQL5 结构体映射 BMP 头部字段,一次性把宽高和像素密度读出来,省掉手动偏移的麻烦。 结构体 BitmapHeader 从 type 到 imgClrImportant 共 21 个字段,覆盖了 14 字节文件头加 40 字节信息头。FileReadStruct 按内存布局直接填,Header.imgWidth 和 imgHeight 就是后续贴图的坐标基准。 若 FileOpen 返回 INVALID_HANDLE,或者读到的字节数不等于 sizeof(Header),直接关句柄返回 false,说明文件不是合法 BMP 或路径错。读成功后把宽高写进 m_InfoImage[index],后续绘制时就不会把 1920×1080 的图错位成别的尺寸。 开 MT5 随便丢一张 24 位 BMP 进 Files 目录,改 szFileName 跑一遍,能在调试里看到 widthMap 等于实际像素宽,这套读取就通了。外汇和贵金属图表叠加自绘位图属高风险视觉辅助,别把它当信号依据。
class="type">bool LoadBitmap(class="type">int index, class="type">class="kw">string szFileName) { class="kw">struct BitmapHeader { class="type">class="kw">ushort type; class="type">uint size, reserv, offbits, imgSSize, imgWidth, imgHeight; class="type">class="kw">ushort imgPlanes, imgBitCount; class="type">uint imgCompression, imgSizeImage, imgXPelsPerMeter, imgYPelsPerMeter, imgClrUsed, imgClrImportant; } Header; class="type">int fp, w; class="type">bool noAlpha, noFlip; class="type">uint imgSize; if ((fp = FileOpen(szFileName, FILE_READ | FILE_BIN)) == INVALID_HANDLE) class="kw">return false; if (FileReadStruct(fp, Header) != class="kw">sizeof(Header)) { FileClose(fp); class="kw">return false; }; m_InfoImage[index].widthMap = (class="type">int)Header.imgWidth; m_InfoImage[index].heightMap = (class="type">int)Header.imgHeight;