开发回放系统(第 56 部分):调整模块·进阶篇
「滑条与播放控件的对象属性落地」
在 MT5 自定义面板里,滑块背景条用 OBJ_RECTANGLE_LABEL 画出来后,边框和填充的细节全靠 ObjectSetInteger 逐个塞属性。上面这段把滑块外框设成黑色 3 像素宽、BORDER_FLAT 平边,又在内部叠了一个高 19、距上沿 6 像素的 ros棕色凸起块(BORDER_RAISED),视觉上就是可拖动的仓位指示条。 播放键走的是另一套逻辑:SetPlay(bool state) 里首次调用才 new 一个 C_DrawImage,平时只切 Paint 的第 6 参——state 为真传 0 显示播放图、为假传 1 显示暂停图,按钮尺寸写死 20。 CreateCtrlSlider 把整套控件坐标定死:滑块条 CreteBarSlider(77,436) 意味着起点 X=77、Y=436;左右箭头和图钉按钮也在这里用 EnumToString(eLeft/eRight/ePin) 拼出唯一对象名,避免多实例重名。开 MT5 把这几行抄进 EA 的面板初始化段,就能直接看到控件成型。
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 = def_PrefixCtrlName + "B2", OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, def_PosXObjects + x); ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Section[ePin].y + class="num">6); 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=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void SetPlay(class="type">bool state) { if (m_Section[ePlay].Btn == NULL) m_Section[ePlay].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_PrefixCtrlName + EnumToString(ePlay), def_ColorFilter, "::" + def_ButtonPlay, "::" + def_ButtonPause); 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">0 : class="num">1)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateCtrlSlider(class="type">void) { CreteBarSlider(class="num">77, class="num">436); m_Section[eLeft].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_PrefixCtrlName + EnumToString(eLeft), def_ColorFilter, "::" + def_ButtonLeft, "::" + def_ButtonLeftBlock); m_Section[eRight].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_PrefixCtrlName + EnumToString(eRight), def_ColorFilter, "::" + def_ButtonRight, "::" + def_ButtonRightBlock); m_Section[ePin].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_PrefixCtrlName + EnumToString(ePin), def_ColorFilter, "::" + def_ButtonPin);
滑块控件的回收与定位逻辑
这段内联函数负责把图表上的滑块控制区整套拆掉再重画。RemoveCtrlSlider 先关掉 CHART_EVENT_OBJECT_DELETE 事件监听,避免删除对象时触发回调死循环,再遍历 ePlay+1 到 eNull 的按钮指针逐个 delete 并置 NULL,最后用 ObjectsDeleteAll 按前缀 'B' 清掉残留图形对象,重新打开事件监听。 PositionPinSlider 接收 short 型位置参数 p,先夹取到 [m_Slider.Minimal, def_MaxPosSlider] 区间内,再叠加 def_PosXObjects 与 95-(def_SizeButtons/2) 的偏移量。第 108 行那个 95 是按钮中心相对滑块原点的硬偏移,改按钮尺寸时这里必须同步算,否则 pin 会偏出轨道。 iL 与 iR 根据是否顶到最小/最大边界置 1,传给 Btn.Paint 的最后一个参数做禁用态渲染;第 111 行把滑条色块宽度锁成 m_Slider.Minimal+2,保证背景块不随拖动拉伸。外汇与贵金属图表加载这类自定义控件存在客户端卡顿风险,建议在 MT5 策略测试器外的实盘图表先单图表验证。 CheckPositionMouseClick 通过鼠标对象指针取 X_Graphics 图形坐标,x 被强转 short 回传,调用方拿到的就是相对于图表区域的点击列位置,用于判断命中了哪个控制段。
class="kw">inline class="type">void RemoveCtrlSlider(class="type">void) { 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_PrefixCtrlName + "B"); ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); } class="kw">inline class="type">void PositionPinSlider(class="type">class="kw">short p) { class="type">int iL, iR; m_Section[ePin].x = (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">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 值,强制转成 short 避免浮点误差干扰后续比较。 接着用 for 循环从 ePlay 扫到 eNull,逐个判断鼠标坐标 (x,y) 是否落在该控件矩形内:左不小于 m_Section[c0].x、上不小于 m_Section[c0].y、右不超过 x+w、下不超过 y+h,命中即返回对应枚举。 若全部落空则返回 eNull,这套逻辑在 120–127 行完成,是 UI 事件分发的基础。 构造函数里先把图表对象删除事件关掉,清掉旧前缀控件再重新打开,防止初始化时误触发删除回调。 控件尺寸统一设为 def_SizeButtons,Y 坐标固定 25;ePlay 的 X 取 def_PosXObjects,eLeft 右移 47 像素,eRight 右移 511 像素,滑块下限直接压到 SHORT_MIN(见 141–151 行)。 实际在 MT5 里改 def_PosXObjects 或那两个偏移量,就能把按钮组整体挪位,验证布局是否符合你的看盘习惯。
class="num">120. y = (class="type">class="kw">short) InfoMouse.Position.Y_Graphics; class="num">121. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) class="num">122. { class="num">123. 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="num">124. class="kw">return c0; class="num">125. } class="num">126. class="num">127. class="kw">return eNull; class="num">128. } class="num">129. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">130. class="kw">public : class="num">131. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">132. C_Controls(class="kw">const class="type">long Arg0, class="kw">const class="type">class="kw">string szShortName, C_Mouse *MousePtr) class="num">133. :C_Terminal(Arg0), class="num">134. m_MousePtr(MousePtr) class="num">135. { class="num">136. if ((!IndicatorCheckPass(szShortName)) || (CheckPointer(m_MousePtr) == POINTER_INVALID)) SetUserError(C_Terminal::ERR_Unknown); class="num">137. if (_LastError != ERR_SUCCESS) class="kw">return; class="num">138. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, class="kw">false); class="num">139. ObjectsDeleteAll(GetInfoTerminal().ID, def_PrefixCtrlName); class="num">140. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); class="num">141. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) class="num">142. { class="num">143. m_Section[c0].h = m_Section[c0].w = def_SizeButtons; class="num">144. m_Section[c0].y = class="num">25; class="num">145. m_Section[c0].Btn = NULL; class="num">146. } class="num">147. m_Section[ePlay].x = def_PosXObjects; class="num">148. m_Section[eLeft].x = m_Section[ePlay].x + class="num">47; class="num">149. m_Section[eRight].x = m_Section[ePlay].x + class="num">511; class="num">150. m_Slider.Minimal = SHORT_MIN; class="num">151. } class="num">152. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">153. ~C_Controls() class="num">154. {
「回放控件的缓冲写入与消息分发」
这段实现把回放面板的状态塞进指标缓冲区的最后一根 K 线。uCast_Double 用联合体把 16 位短整型拼回双精度:低 16 位存滑块最小位置,高 16 位用 SHORT_MAX / SHORT_MIN 标记播放或暂停,写入时只动 Buff[rates_total-1],不重写历史。 DispatchMessage 接的是 CHARTEVENT_CUSTOM + evCtrlReplayInit 自定义事件。dparam 拆出两个 ushort:第一个复位滑块最小位 iPinPosX,第二个若为 0 直接 SetUserError 抛错,非 0 则按 SHORT_MAX 开播、SHORT_MIN 重建滑块控件。 在 MT5 里跑时,把这段贴进你的 CIndicator 派生类,用 CustomEvent 发 evCtrlReplayInit 就能看到缓冲末位被改写;外汇与贵金属波动剧烈,回放仅用于复盘验证,实盘信号失效概率不低。
for (eObjectControl c0 = ePlay; c0 < eNull; c0++) class="kw">delete m_Section[c0].Btn; ObjectsDeleteAll(GetInfoTerminal().ID, def_PrefixCtrlName); class="kw">delete m_MousePtr; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void SetBuffer(class="kw">const class="type">int rates_total, class="type">class="kw">double &Buff[]) { uCast_Double info; info._16b[class="num">0] = (class="type">class="kw">ushort) m_Slider.Minimal; info._16b[class="num">1] = (class="type">class="kw">ushort) (m_Section[ePlay].state ? 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">class="kw">short x, y; class="kw">static class="type">class="kw">short iPinPosX = -class="num">1, six = -class="num">1, sps; uCast_Double info; class="kw">switch (id) { case (CHARTEVENT_CUSTOM + evCtrlReplayInit): info.dValue = dparam; iPinPosX = m_Slider.Minimal = (class="type">class="kw">short) info._16b[class="num">0]; if (info._16b[class="num">1] == class="num">0) SetUserError(C_Terminal::ERR_Unknown); else { SetPlay((class="type">class="kw">short)(info._16b[class="num">1]) == SHORT_MAX); if ((class="type">class="kw">short)(info._16b[class="num">1]) == SHORT_MIN) CreateCtrlSlider(); } class="kw">break;
图表对象删除与鼠标点击的事件分支
在自定义控件面板里,用户手动删掉一个图形对象不会静默发生,MT5 会抛出 CHARTEVENT_OBJECT_DELETE。代码第187–201行处理该事件:先判断被删对象名是否带控件前缀 def_PrefixCtrlName(第188行),避免误伤普通画线。 若删的是播放按钮本身(sparam 等于前缀拼接 ePlay 枚举字符串,第190行),则释放 Btn 指针置 NULL(192–193行),并调用 SetPlay 按当前 state 重建状态(194行);否则视为滑块类控件被删,执行 RemoveCtrlSlider 后再 CreateCtrlSlider 重画(197–198行)。 鼠标移动事件从202行开始。仅当左键处于按下状态(CheckClick(eClickLeft))才进入坐标判定 CheckPositionMouseClick(x,y)(203行)。命中 ePlay 区域时翻转播放状态(206行):状态为真则移除滑块并把 m_Slider.Minimal 锁到 iPinPosX(209–210行),否则重建滑块(211行)。 命中 eLeft 时(213–214行)把 iPinPosX 左移1像素但不低于 Minimal 下限,直接驱动 PositionPinSlider 刷新滑块位置。外汇与贵金属图表上跑这类 EA 属高风险操作,参数越界可能卡死控件重绘。
case CHARTEVENT_OBJECT_DELETE: if (StringSubstr(sparam, class="num">0, StringLen(def_PrefixCtrlName)) == def_PrefixCtrlName) { if (sparam == (def_PrefixCtrlName + EnumToString(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));
◍ 拖拽定位时如何锁死图表滚动
这段收尾逻辑处理的是播放控件上「图钉」按钮被按下后的鼠标拖拽行为。当你在 ePin 分支里首次点下,six 为 -1 时,代码记录当前鼠标 x 与滑块位置 sps,并立刻用 ChartSetInteger 把 CHART_MOUSE_SCROLL 设为 false,相当于临时冻结图表随鼠标滚轮平移。 iPinPosX = sps + x - six 这行把滑块位置跟着鼠标实时偏移;下一行再用三目运算把它夹在 m_Slider.Minimal 与 def_MaxPosSlider 之间,越界就吸边。外汇与贵金属品种波动剧烈,冻结滚动能避免拖控件时图表乱跑,但操作完记得释放。 松开后 six 重置为 -1 并重新打开 CHART_MOUSE_SCROLL,最后 ChartRedraw 刷新一次。宏定义在 241–248 行统一 undef,避免污染其他 include 文件。 [CODE]
- break;
- case eRight:
- PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + 1 : def_MaxPosSlider));
- break;
- case ePin:
- if (six == -1)
- {
- six = x;
- sps = iPinPosX;
- ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false);
- }
- iPinPosX = sps + x - six;
- PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX)));
- break;
- }else if (six > 0)
- {
- six = -1;
- ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);
- }
- break;
- }
- ChartRedraw(GetInfoTerminal().ID);
- }
- //+------------------------------------------------------------------+
- };
- //+------------------------------------------------------------------+
- #undef def_PosXObjects
- #undef def_ButtonPlay
- #undef def_ButtonPause
- #undef def_ButtonLeft
- #undef def_ButtonRight
- #undef def_ButtonPin
- #undef def_PrefixCtrlName
- #undef def_PathBMP
[/CODE] 逐行拆解:215/218/228/234 的 break 只是退出对应 case;217 是右键步进滑块 +1 并封顶。220–225 记录拖拽起点并关滚动;226 算偏移;227 夹取边界。229–233 是鼠标抬起分支,six 归位并恢复滚动。236 重绘,241 往后清掉七个宏。开 MT5 把这段塞进你的面板类,点图钉拖一下就能验证滚动锁定是否生效。
class="num">215. class="kw">break; class="num">216. case eRight: class="num">217. PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + class="num">1 : def_MaxPosSlider)); class="num">218. class="kw">break; class="num">219. case ePin: class="num">220. if (six == -class="num">1) class="num">221. { class="num">222. six = x; class="num">223. sps = iPinPosX; class="num">224. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, class="kw">false); class="num">225. } class="num">226. iPinPosX = sps + x - six; class="num">227. PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX))); class="num">228. class="kw">break; class="num">229. }else if (six > class="num">0) class="num">230. { class="num">231. six = -class="num">1; class="num">232. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); class="num">233. } class="num">234. class="kw">break; class="num">235. } class="num">236. ChartRedraw(GetInfoTerminal().ID); class="num">237. } class="num">238. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">239. }; class="num">240. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">241. class="macro">#undef def_PosXObjects class="num">242. class="macro">#undef def_ButtonPlay class="num">243. class="macro">#undef def_ButtonPause class="num">244. class="macro">#undef def_ButtonLeft class="num">245. class="macro">#undef def_ButtonRight class="num">246. class="macro">#undef def_ButtonPin class="num">247. class="macro">#undef def_PrefixCtrlName class="num">248. class="macro">#undef def_PathBMP