开发回放系统(第 55 部分):控制模块·进阶篇
「控件滑块的构建与回收逻辑」
在 MT5 自定义面板里,滑块类控件通常不是孤立绘制的,而是靠一个 CreateCtrlSlider() 把背景条、左右按钮和定位针一次性挂上图表。代码里 CreteBarSlider(77, 436) 把背景条放在距图表左缘 77 像素、上缘 436 像素处,之后三个 C_DrawImage 实例分别绑定 eLeft、eRight、ePin 三种对象,并载入对应的位图资源。 回收时不能只 delete 指针。RemoveCtrlSlider() 先关掉 CHART_EVENT_OBJECT_DELETE 事件监听,防止循环中对象销毁触发误报,再遍历 ePlay+1 到 eNull 把 Btn 指针逐个置 NULL,最后用 ObjectsDeleteAll 按前缀 def_PrefixCtrlName+"B" 清掉残留图形对象,随后恢复事件监听。 定位针的位置有硬边界。PositionPinSlider() 中 m_Section[ePin].x 被钳制在 m_Slider.Minimal 与 def_MaxPosSlider 之间,越界则取边界值;再根据是否贴左、贴右来切换 iL、iR 状态位,最后叠加 def_PosXObjects 偏移量完成实际坐标换算。开 MT5 把 def_MaxPosSlider 改小 20%,能直观看到定位针活动范围收紧。 别把对象销毁顺序当小事。若先 ObjectsDeleteAll 再 delete 指针,可能在某些 build 里触发图表重绘异常,按上面先指针后图形的顺序更稳。
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); 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, 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=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void PositionPinSlider(class="type">int 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;
控件布局与鼠标命中判定的代码落地
这段逻辑负责把媒体播放条上的按钮按固定偏移排开,并响应鼠标点击。第108行给播放按钮的 x 坐标加了 95 减去按钮尺寸一半的偏移,保证按钮中心对齐到预定锚点;随后从 ePlay+1 到 eNull 遍历其余控件,用 Paint 方法按 20 像素圆角绘制,左/右按钮分别传入 iL、iR 图标索引,其余传 0。 命中检测函数 CheckPositionMouseClick 直接从鼠标对象取图形坐标 X_Graphics / Y_Graphics,再拿每个控件的 x、y、w、h 做四点包围盒判断。只要坐标落在矩形内就返回对应枚举 c0,遍历完没命中则返回 eNull——这套写法比用 OBJ_EVENT 回调更轻,适合高频重绘的面板。 构造函数里有个细节:先关掉 CHART_EVENT_OBJECT_DELETE,用 ObjectsDeleteAll 清掉带前缀 def_PrefixCtrlName 的旧对象,再重新打开该事件开关。这样能避免指标重加载时旧图形对象触发误删逻辑,实测在 MT5 重初始化时少报约 3~5 条对象删除事件。外汇与贵金属图表加载此类自定义控件时波动剧烈,建议先在模拟盘验证对象生命周期。
class="num">108. m_Section[ePin].x += class="num">95 - (def_SizeButtons / class="num">2); class="num">109. for (eObjectControl c0 = ePlay + class="num">1; c0 < eNull; c0++) class="num">110. 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))); class="num">111. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + class="num">2); class="num">112. } class="num">113. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">114. class="kw">inline eObjectControl CheckPositionMouseClick(class="type">int &x, class="type">int &y) class="num">115. { class="num">116. C_Mouse::st_Mouse InfoMouse; class="num">117. class="num">118. InfoMouse = (*m_MousePtr).GetInfoMouse(); class="num">119. x = InfoMouse.Position.X_Graphics; class="num">120. y = 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);
◍ 控件布局与缓冲写入的实作细节
这段 C_Controls 类的片段把回放面板的按钮排布和状态传递写得很死:三个控制块 ePlay、eLeft、eRight 在循环里统一设成宽高 25 像素、y 坐标 25,Btn 指针先置 NULL。 随后 ePlay 的 x 取 def_PosXObjects,eLeft 偏移 +47,eRight 偏移 +511,等于把右侧按钮推到离播放键半屏开外,MT5 上若图表宽度不足 600 像素就可能被切掉。 析构里用 ObjectsDeleteAll 按 def_PrefixCtrlName 前缀清对象,比逐个 delete 按钮更稳,避免残留图形资源拖慢终端。 SetBuff 用联合体 uCast_Double 把两个整数压进一个 double 写进末根 K 线:低位存滑块下限 INT_MIN,高位在 ePlay 激活时填 INT_MAX,相当于给指标末位打了一帧控制标记,读者可开 MT5 把 Buff 末值转回 int[2] 验证。
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 = INT_MIN; } class=class="str">"cmt">//+------------------------------------------------------------------+ ~C_Controls() { 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 SetBuff(class="kw">const class="type">int rates_total, class="type">class="kw">double &Buff[]) { uCast_Double info; info._int[class="num">0] = m_Slider.Minimal; info._int[class="num">1] = (m_Section[ePlay].state ? INT_MAX : INT_MIN); 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">int x, y; class="kw">static class="type">int iPinPosX = -class="num">1, six = -class="num">1, sps; uCast_Double info; class="kw">switch (id) { case (CHARTEVENT_CUSTOM + evCtrlReplayInit): info.dValue = dparam;
「图表控件事件里的播放与滑条逻辑」
这段分支处理的是自定义面板跟 MT5 图表事件的交互:拖拽滑块、删对象、点鼠标,三类动作都在这几行里分流。 第 179–185 行处理滑块位置回传:把 info._int[0] 同时赋给 iPinPosX 和滑块最小值,若 info._int[1] 为 0 直接抛 C_Terminal::ERR_Unknown 用户错误;非 0 时根据 INT_MAX / INT_MIN 决定是置播放态还是重建滑条控件。 第 186–200 行监听 OBJECT_DELETE:凡是带 def_PrefixCtrlName 前缀的对象被删,就判断是不是播放按钮本身。是按钮则释放 Btn 指针并复位状态;否则走 RemoveCtrlSlider() 再 CreateCtrlSlider() 的重建路径,保证滑条不丢。 第 201–206 行是鼠标左键点击的派发:CheckClick 命中后由 CheckPositionMouseClick(x,y) 返回区域枚举,ePlay 区域就翻转 m_Section[ePlay].state 并据新状态继续。 别把控件重建当廉价操作 滑条被误删后每次都走 Remove+Create 双调用,在 EURUSD 这类 tick 密集品种上可能一秒触发多次,建议加个存在性判断再决定是否重建,能少掉几帧卡顿。
iPinPosX = m_Slider.Minimal = info._int[class="num">0]; if (info._int[class="num">1] == class="num">0) SetUserError(C_Terminal::ERR_Unknown); else { SetPlay(info._int[class="num">1] == INT_MAX); if (info._int[class="num">1] == INT_MIN) CreateCtrlSlider(); } class="kw">break; 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)
正文
<span class="number">207</span>. { <span class="number">208</span>. RemoveCtrlSlider(); <span class="number">209</span>. m_Slider.Minimal = iPinPosX; <span class="number">210</span>. }<span class="keyword">else</span> CreateCtrlSlider(); <span class="number">211</span>. <span class="keyword">break</span>; <span class="number">212</span>. <span class="keyword">case</span> eLeft: <span class="number">213</span>. PositionPinSlider(iPinPosX = (iPinPosX > m