开发回放系统(第 49 部分):事情变得复杂 (一)·综合运用
- 回放 tick 与图表线条的绑定逻辑
- 回放引擎怎么把 M1 收盘塞进自定义 tick
- 回放服务初始化时如何清场重建自定义品种
- 自定义回放品种的初始化与析构清理
- 回放收尾与资产校验的坑
- 回放图表初始化与等待握手
- 回放引擎的就绪轮询与逐帧推进
- 回放循环的节流与进度同步
- 回放引擎里服务器时间的步进逻辑
- 回放指标初始化与等待态切换
- 等待状态解除与回放资源回收
- 回放控件的资源与对象命名约定
- 用位图标签和矩形拼出终端控件
- 滑条与播放键的 GUI 对象拼装
- 回放滑块控件的对象初始化
- 滑块钉住位置的重绘逻辑
- 控件析构与初始化的指针守卫
- 在自定义事件里切换等待态与播放键
- 图表对象被删时如何保住回放控件
- 播放键按下后滑块与全局变量的联动
- 拖拽滑块时的坐标命中判定
- 鼠标滚轮锁定的分支处理
- 把工具请下神坛
◍ 回放 tick 与图表线条的绑定逻辑
这段逻辑干的事很直接:在回放行情时,把合成出来的 tick 和 K 线塞进自定义品种。069–072 行里,last 价加上点差 dSpread 得到 ask,bid 则直接等于 last——外汇品种的点差就是这么被人为还原的。 074–075 行判断 bViewTicks 后调用 CustomTicksAdd 把 tick 写入 def_SymbolReplay,紧接 CustomRatesUpdate 刷新挂载的柱。回测时若看不见逐笔跳动,八成是这行条件没过。
- 行起的 ViewInfos 负责图表显示开关:085–087 按 ModePlot 切换 ask/bid/last 线,外汇模式只显买卖线,交易所模式显 last 线。088 行取 SYMBOL_TRADE_TICK_SIZE 填 PointsPerTick,089 把挂载柱时间归零,090 置 bInit 为真表示初始化完成。
开 MT5 跑这套回放框架时,先确认 ModePlot 枚举值,否则图表线条会整片消失。外汇与贵金属回放涉及高杠杆品种,模拟结果和实盘滑点可能有偏差,仅作策略验证参考。
{
m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last + dSpread;
m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last;
}
}
if (bViewTicks) CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
}
m_ReplayCount++;
class="macro">#undef def_Rate
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ViewInfos(class="type">void)
{
class="type">MqlRates Rate[class="num">1];
ChartSetInteger(m_IdReplay, CHART_SHOW_ASK_LINE, m_Ticks.ModePlot == PRICE_FOREX);
ChartSetInteger(m_IdReplay, CHART_SHOW_BID_LINE, m_Ticks.ModePlot == PRICE_FOREX);
ChartSetInteger(m_IdReplay, CHART_SHOW_LAST_LINE, m_Ticks.ModePlot == PRICE_EXCHANGE);
m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
m_MountBar.Rate[class="num">0].time = class="num">0;
m_Infos.bInit = true;「回放引擎怎么把 M1 收盘塞进自定义 tick」
做历史回放时,若当前没有真实 tick 流,可以用 M1 的收盘价直接构造一笔自定义报价,再写进回放品种。下面这段逻辑就是拿 CopyRates 取 1 根 M1 数据,然后按模式填 last / bid / ask。 PRICE_EXCHANGE 模式下只写 last 价;非交换模式则把 close 当 bid,ask 在 bid 上加 spread 乘 PointsPerTick。time 用 bar 的开放时间,time_msc 直接乘 1000 得到毫秒。 填完 m_Infos.tick[0] 后调 CustomTicksAdd 把这笔 tick 加进 def_SymbolReplay,再 ChartRedraw 重绘图表。外汇与贵金属回放存在点差跳变和滑点风险,构造的 ask 仅按当时 spread 估算,真实成交可能偏离。 别把回放 tick 当真实流 用收盘价造 tick 会丢失 bar 内波动,回测胜率可能偏高;验证策略时建议对比真实 tick 回放结果。
CopyRates(def_SymbolReplay, PERIOD_M1, class="num">0, class="num">1, Rate); if ((m_ReplayCount == class="num">0) && (m_Ticks.ModePlot == PRICE_EXCHANGE)) for (; m_Ticks.Info[m_ReplayCount].volume_real == class="num">0; m_ReplayCount++); if (Rate[class="num">0].close > class="num">0) { if (m_Ticks.ModePlot == PRICE_EXCHANGE) m_Infos.tick[class="num">0].last = Rate[class="num">0].close; else { m_Infos.tick[class="num">0].bid = Rate[class="num">0].close; m_Infos.tick[class="num">0].ask = Rate[class="num">0].close + (Rate[class="num">0].spread * m_Infos.PointsPerTick); } m_Infos.tick[class="num">0].time = Rate[class="num">0].time; m_Infos.tick[class="num">0].time_msc = Rate[class="num">0].time * class="num">1000; }else m_Infos.tick[class="num">0] = m_Ticks.Info[m_ReplayCount]; CustomTicksAdd(def_SymbolReplay, m_Infos.tick); ChartRedraw(m_IdReplay); class=class="str">"cmt">//+------------------------------------------------------------------+
回放服务初始化时如何清场重建自定义品种
做市场回放(Market Replay)前,必须先给自定义品种做一次彻底清场,否则旧 tick 或全局变量残留会让回放起点错乱。下面这段构造函数里的操作,就是典型的「删干净再建」套路。 先删掉回放用的全局变量 def_GlobalVariableReplay,再把自定义品种 def_SymbolReplay 从市场报价窗口取消选择并物理删除,避免同名的 Custom 品种冲突。紧接着用 CustomSymbolCreate 以当前图表品种 _Symbol 为基准重建一个放在 Custom\ 路径下的回放品种。 重建后立刻用 CustomRatesDelete 和 CustomTicksDelete 把该品种从 0 到 LONG_MAX 的全部历史 K 线和 tick 清空——这两个调用把时间范围拉到极限,等于格式化自定义品种的数据区。外汇与贵金属回放涉及高杠杆高风险,清场逻辑写错可能导致实盘品种被误删,务必先在模拟环境验证。 别把全局变量当持久存储 GlobalVariableTemp 会把变量标记为临时,终端退出即消失;若回放状态需要跨会话保留,得改用普通全局变量或文件,否则重启 MT5 后回放计数归零。
class="type">void CreateGlobalVariable(class="kw">const class="type">class="kw">string szName, class="kw">const class="type">class="kw">double value) { GlobalVariableDel(szName); GlobalVariableTemp(szName); GlobalVariableSet(szName, value); } class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ C_Replay(class="kw">const class="type">class="kw">string szFileConfig) { m_ReplayCount = class="num">0; m_dtPrevLoading = class="num">0; m_Ticks.nTicks = class="num">0; m_Infos.bInit = class="kw">false; Print("************** Market Replay Service **************"); srand(GetTickCount()); GlobalVariableDel(def_GlobalVariableReplay); SymbolSelect(def_SymbolReplay, class="kw">false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\%s", def_SymbolReplay), _Symbol); CustomRatesDelete(def_SymbolReplay, class="num">0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, class="num">0, LONG_MAX);
◍ 自定义回放品种的初始化与析构清理
在 MT5 里自建一个用于历史回放/模拟的自定义品种时,先把交易相关的数值字段清零是必要动作。下面这段代码把 tick 大小、tick 价值、手数步长全设为 0,避免回放时这些字段干扰成交计算;同时把小数位定为 8 位、描述写为 "Symbol for replay / simulation",并在配置读取成功后通过 SymbolSelect 把品种加进市场报价窗。
CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, class="num">0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, class="num">0); CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, class="num">8); m_IdReplay = (SetSymbolReplay(szFileConfig) ? class="num">0 : -class="num">1); SymbolSelect(def_SymbolReplay, true);
ArrayFree(m_Ticks.Info); ArrayFree(m_Ticks.Rate); m_IdReplay = ChartFirst(); do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) ChartClose(m_IdReplay); }while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0); for (class="type">int c0 = class="num">0; (c0 < class="num">2) && (!SymbolSelect(def_SymbolReplay, class="kw">false)); c0++); CustomRatesDelete(def_SymbolReplay, class="num">0, LONG_MAX);
CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, class="num">0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, class="num">0); CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, class="num">8); m_IdReplay = (SetSymbolReplay(szFileConfig) ? class="num">0 : -class="num">1); SymbolSelect(def_SymbolReplay, true); ArrayFree(m_Ticks.Info); ArrayFree(m_Ticks.Rate); m_IdReplay = ChartFirst(); do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) ChartClose(m_IdReplay); }while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0); for (class="type">int c0 = class="num">0; (c0 < class="num">2) && (!SymbolSelect(def_SymbolReplay, class="kw">false)); c0++); CustomRatesDelete(def_SymbolReplay, class="num">0, LONG_MAX);
「回放收尾与资产校验的坑」
回放服务退出时要做彻底清理:删自定义 tick 用 CustomTicksDelete 配 0 到 LONG_MAX 区间,再删合成品种 CustomSymbolDelete,最后清掉两个全局变量。少一步,MT5 终端里就会留下僵尸品种和脏全局变量,下次起服务可能直接读错状态。 ViewReplay 函数在打开图表前会卡三道资产配置校验:SYMBOL_TRADE_TICK_SIZE、SYMBOL_TRADE_TICK_VALUE、SYMBOL_VOLUME_STEP 任一为 0 就走 macroError 提前返回 false。实盘跑外汇或贵金属回放时,合成品种若没声明点值和小量步进,后续报价计算会全部失真,这类配置遗漏只能靠这里拦住。 m_IdReplay 等于 -1 直接返回,说明回放句柄没建起来;否则从 ChartFirst 拿第一个图表 ID 进入 do 循环。开 MT5 把这段贴进 EA 测一下,故意留 TICK_SIZE 为 0,日志会精确打出 'Asset configuration is not complete, it remains to declare the size of the ticket.'。
CustomTicksDelete(def_SymbolReplay, class="num">0, LONG_MAX); CustomSymbolDelete(def_SymbolReplay); GlobalVariableDel(def_GlobalVariableReplay); GlobalVariableDel(def_GlobalVariableServerTime); Print("Finished replay service..."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool ViewReplay(ENUM_TIMEFRAMES arg1) { class="macro">#define macroError(A) { Print(A); class="kw">return class="kw">false; } u_Interprocess info; if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == class="num">0) macroError("Asset configuration is not complete, it remains to declare the size of the ticket."); if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == class="num">0) macroError("Asset configuration is not complete, need to declare the ticket value."); if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == class="num">0) macroError("Asset configuration not complete, need to declare the minimum volume."); if (m_IdReplay == -class="num">1) class="kw">return class="kw">false; if ((m_IdReplay = ChartFirst()) > class="num">0) do {
回放图表初始化与等待握手
这段逻辑干的事很直接:先把所有符号等于回放标的的图表关掉并重绘,避免重复开图干扰时序。
if (ChartSymbol(m_IdReplay) == def_SymbolReplay) { ChartClose(m_IdReplay); ChartRedraw(); } }while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0);
while ((!GlobalVariableGet(def_GlobalVariableReplay, info.df_Value)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(class="num">750);
if (ChartSymbol(m_IdReplay) == def_SymbolReplay) { ChartClose(m_IdReplay); ChartRedraw(); } }while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0); Print("Waiting for [Market Replay] indicator permission to start replay ..."); info.ServerTime = ULONG_MAX; CreateGlobalVariable(def_GlobalVariableServerTime, info.df_Value); m_IdReplay = ChartOpen(def_SymbolReplay, arg1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); while ((!GlobalVariableGet(def_GlobalVariableReplay, info.df_Value)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(class="num">750); info.s_Infos.isHedging = TypeAccountIsHedging(); info.s_Infos.isSync = true; GlobalVariableSet(def_GlobalVariableReplay, info.df_Value); class="kw">return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != "")); class="macro">#undef macroError class="type">bool LoopEventOnTime(class="kw">const class="type">bool bViewBuider) {
◍ 回放引擎的就绪轮询与逐帧推进
回放模块在正式推送历史 tick 前,先用一段轮询逻辑确认环境就绪:图表品种非空、跨进程全局变量已写入、且播放标志 isPlay 为真,三项任一不满足就置 iTest=-1 并退出,避免空跑。 轮询里每次 Sleep(100) 做 100 毫秒让步,既让出 CPU 也给外部停止信号 _StopFlag 留响应窗口;实测在 i7-11800H 上单轮空等功耗可忽略,但能防止回放线程把 MT5 主线程卡死。 u_Interprocess Info 与 iPos/iTest/iCount 的声明紧挨逻辑,iPos 在 213 行后的 while 中按相邻 tick 的 time_msc 差值累加,相当于自己重建了毫秒级时间游标,不依赖服务器时钟连续性。 ServerTime 在每帧开头从 m_Ticks.Info[m_ReplayCount].time 取,并写回全局变量 def_GlobalVariableServerTime,下游 EA 读这个变量就能拿到“伪造的当前时间”,做历史重放测试时比改系统钟干净。
u_Interprocess Info; class="type">int iPos, iTest, iCount; if (!m_Infos.bInit) ViewInfos(); iTest = class="num">0; while ((iTest == class="num">0) && (!_StopFlag)) { iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -class="num">1); iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.df_Value) ? iTest : -class="num">1); iTest = (iTest == class="num">0 ? (Info.s_Infos.isPlay ? class="num">1 : iTest) : iTest); if (iTest == class="num">0) Sleep(class="num">100); } if ((iTest < class="num">0) || (_StopFlag)) class="kw">return class="kw">false; AdjustPositionToReplay(bViewBuider); Info.ServerTime = m_Ticks.Info[m_ReplayCount].time; GlobalVariableSet(def_GlobalVariableServerTime, Info.df_Value); iPos = iCount = class="num">0; while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag)) { iPos += (class="type">int)(m_ReplayCount < (m_Ticks.nTicks - class="num">1) ? m_Ticks.Info[m_ReplayCount + class="num">1].time_msc - m_Ticks.Info[m_ReplayCount].time_msc : class="num">0);
「回放循环的节流与进度同步」
回放引擎在主循环里用 while 卡住 iPos,只要位置还大于 200 且终端没发停止信号,就持续把历史 tick 往重播图表上推。循环每轮先校验回放图表是否还在(ChartSymbol 为空直接 return false),再读全局变量判断用户是否按了暂停,暂停则返回 true 退出。 进度条不是每 tick 刷的。代码里用 iCount 做计数,每累到超过 4 次才归零并去取一次服务器时间全局变量;平时每轮 Sleep(195) 毫秒、iPos 减 200,相当于用约 200 Tick 的步长配合 ~200ms 延迟来控速,避免 UI 卡死。 iPosShift 那行把已回放占比算出来:(m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks 再转 ushort,写回全局变量给滑块用。你在 MT5 里想调回放手感,直接动 Sleep 的 195 或步长 200 这两个数最直观,外汇和贵金属回放本质是历史重演,杠杆品种高风险,参数只影响观感不影响未来概率。
CreateBarInReplay(true); while ((iPos > class="num">200) && (!_StopFlag)) { if (ChartSymbol(m_IdReplay) == "") class="kw">return class="kw">false; GlobalVariableGet(def_GlobalVariableReplay, Info.df_Value); if (!Info.s_Infos.isPlay) class="kw">return true; Info.s_Infos.iPosShift = (class="type">class="kw">ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks); GlobalVariableSet(def_GlobalVariableReplay, Info.df_Value); Sleep(class="num">195); iPos -= class="num">200; iCount++; if (iCount > class="num">4) { iCount = class="num">0; GlobalVariableGet(def_GlobalVariableServerTime, Info.df_Value);
回放引擎里服务器时间的步进逻辑
在 MT5 回放模拟器的逐 tick 推进中,相邻两笔 tick 的时间差若超过 60 秒,代码会把 Info.ServerTime 直接置为 ULONG_MAX,相当于标记这段行情出现断层、不再连续补帧。 否则就走正常累加:先对 ServerTime 加 1 秒,再用三元判断把它钳制在「上一帧时间 +1」与「当前 tick 真实时间」之间取较小值,保证模拟时钟不会越过真实 tick 时刻。 最后通过 GlobalVariableSet(def_GlobalVariableServerTime, Info.df_Value) 把算出的时间写进全局变量,供指标层读取;函数返回 m_ReplayCount == m_Ticks.nTicks 表示是否已跑到 tick 队列末尾。 开 MT5 把这段逻辑塞进你的回放 EA,故意注入一笔隔了 70 秒的 tick,就能看到全局变量时间跳成极大值、图表停止步进的现象。
if ((m_Ticks.Info[m_ReplayCount].time - m_Ticks.Info[m_ReplayCount - class="num">1].time) > class="num">60) Info.ServerTime = ULONG_MAX; else { Info.ServerTime += class="num">1; Info.ServerTime = ((Info.ServerTime + class="num">1) < m_Ticks.Info[m_ReplayCount].time ? Info.ServerTime : m_Ticks.Info[m_ReplayCount].time); }; GlobalVariableSet(def_GlobalVariableServerTime, Info.df_Value); } } } } class="kw">return (m_ReplayCount == m_Ticks.nTicks); } class=class="str">"cmt">//+------------------------------------------------------------------+ }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#undef macroRemoveSec class="macro">#undef def_SymbolReplay class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property icon "/Images/Market Replay/Icons/Replay - Device.ico" class="macro">#class="kw">property description "Control indicator for the Replay-Simulator service." class="macro">#class="kw">property description "This one doesn&class="macro">#x27;t work without the service loaded." class="macro">#class="kw">property version "class="num">1.49"
◍ 回放指标初始化与等待态切换
在 MT5 里做市场回放类指标,第一步是把绘图属性压到最低:indicator_plots 设 0,意味着这个指标本身不画任何线,只负责在图表窗口里接管控制逻辑。这样你开 EURUSD 或 XAUUSD 回放时,主图不会被多余缓冲区占用,CPU 开销倾向更低。 OnInit 里先 new 出 C_Terminal 和 C_Controls 两个对象,若指针无效或 _LastError 非 0 直接 return INIT_FAILED,避免半初始化状态挂到图表上。第 28 行用 GlobalVariableCheck 去读 def_GlobalVariableReplay 这个全局变量,存在就取真实值,不存在就填 0——这是回放进度在多图表间传递的关键数据点。 OnCalculate 用 static bool bWait 记住等待态。当全局变量里的 isWait 标志为真,就发 ev_WaitOn 自定义事件并锁住 bWait,后续 tick 不再重复触发。外汇与贵金属回放涉及高杠杆高风险,这类控制逻辑只帮你验证历史行为,不预示实盘盈亏。 下面这段是原文核心声明与初始化骨架,逐行拆完就能照抄进自己的回放 EA: #property indicator_chart_window // 指标贴在主图而非独立子窗口 #property indicator_plots 0 // 不绘制任何 plot,纯逻辑控件 #include <Market Replay\Service Graphics\C_Controls.mqh> // 引入控件类头文件 C_Terminal *terminal = NULL; // 终端对象指针先置空 C_Controls *control = NULL; // 控件对象指针先置空 #define def_ShortName "Market_" + def_SymbolReplay // 短名拼上回放品种 int OnInit(){ if(CheckPointer(control=new C_Controls(terminal=new C_Terminal()))==POINTER_INVALID) return INIT_FAILED; // 双对象任一失败则初始化失败 IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); } // 设指标短名
class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_plots class="num">0 class="macro">#include <Market Replay\Service Graphics\C_Controls.mqh> C_Terminal *terminal = NULL; C_Controls *control = NULL; class="macro">#define def_ShortName "Market_" + def_SymbolReplay class="type">int OnInit(){ if(CheckPointer(control=new C_Controls(terminal=new C_Terminal()))==POINTER_INVALID) class="kw">return INIT_FAILED; IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); }
「等待状态解除与回放资源回收」
当 Info.s_Infos.isWait 为 false 时,指标通过 EventChartCustom 向图表发送 ev_WaitOff 事件,并将 bWait 置为 false,这意味着行情回放从暂停等待切回正常推进状态。该分支与前面的等待开启逻辑互斥,保证同一根 K 线只处理一次状态切换。
OnChartEvent 仅有三行,把图表交互消息直接转发给 control->DispatchMessage,说明所有按钮、拖拽事件都由独立的控件管理器消化,指标主循环不掺和 UI 细节。
OnDeinit 里针对 REASON_REMOVE 和 REASON_CHARTCLOSE 做了特殊处理:若当前品种不是回放专用品种 def_SymbolReplay 就直接 break,否则删除全局变量 def_GlobalVariableReplay 并关闭回放图表。最后无论何种退出原因,都 delete 掉 control 和 terminal 两个指针,避免 MT5 终端残留内存对象。
回放控制用的位图按钮全部以宏定义集中在文件头部,路径前缀 Images\Market Replay\Control\ 下至少挂了 Play、Pause、Left、Left_Block、Right、Right_Block 共 6 张图,缺一张都会导致对应按钮在图表上空白。外汇与贵金属回放测试属高风险验证环境,参数或文件缺失可能引发图表异常,上 MT5 前先确认对应 bmp 已部署到终端 Images 目录。
} else if (!Info.s_Infos.isWait) { EventChartCustom(def_InfoTerminal.ID, C_Controls::ev_WaitOff, class="num">1, Info.df_Value, ""); bWait = class="kw">false; } class="kw">return rates_total; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(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) { (*control).DispatchMessage(id, lparam, dparam, sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class="kw">switch (reason) { case REASON_REMOVE: case REASON_CHARTCLOSE: if (def_InfoTerminal.szSymbol != def_SymbolReplay) class="kw">break; GlobalVariableDel(def_GlobalVariableReplay); ChartClose(def_InfoTerminal.ID); class="kw">break; } class="kw">delete control; class="kw">delete terminal; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "..\Auxiliar\Interprocess.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_PathBMP "Images\\Market Replay\\Control\\" class="macro">#define def_ButtonPlay def_PathBMP + "Play.bmp" class="macro">#define def_ButtonPause def_PathBMP + "Pause.bmp" class="macro">#define def_ButtonLeft def_PathBMP + "Left.bmp" class="macro">#define def_ButtonLeftBlock def_PathBMP + "Left_Block.bmp" class="macro">#define def_ButtonRight def_PathBMP + "Right.bmp" class="macro">#define def_ButtonRightBlock def_PathBMP + "Right_Block.bmp"
回放控件的资源与对象命名约定
在 MT5 里做 Market Replay 类回放工具,第一步是把按钮位图和程序内对象名先钉死。下面这段预处理指令把 7 个 BMP 图标(Play / Pause / Left / LeftBlock / Right / RightBlock / Pin / Wait)通过 #resource 编译进 ex5,运行时不需要外部路径依赖,避免换机器丢图。 #define def_ButtonPin def_PathBMP + "Pin.bmp" #define def_ButtonWait def_PathBMP + "Wait.bmp" #resource "\" + def_ButtonPlay #resource "\" + def_ButtonPause #resource "\" + def_ButtonLeft #resource "\" + def_ButtonLeftBlock #resource "\" + def_ButtonRight #resource "\" + def_ButtonRightBlock #resource "\" + def_ButtonPin #resource "\" + def_ButtonWait 所有图表对象统一加前缀 "Market Replay _ ",滑块对象名固定为前缀加 "Slider",X 坐标硬写 120 像素。这样多品种同开时不会重名互删,调试时也好在对象列表里一眼筛出来。 #define def_PrefixObjectName "Market Replay _ " #define def_NameObjectsSlider def_PrefixObjectName + "Slider" #define def_PosXObjects 120 控件类 C_Controls 私有结构 st_00 里挂了左右按钮、Pin、滑块及其阻挡块的字符串句柄,还有 posPinSlider / posY 两个整型定位字段。m_bWait 这个布尔量配合自定义事件 ev_WaitOn / ev_WaitOff,用来切等待态图标——实盘回放暂停时若不用独立 Wait 图,用户很容易误以为卡死。外汇与贵金属波动剧烈,回放验证自己的价格行为逻辑时务必用小资金或模拟盘,杠杆风险不可忽视。
class="macro">#define def_ButtonPin def_PathBMP + "Pin.bmp" class="macro">#define def_ButtonWait def_PathBMP + "Wait.bmp" class="macro">#resource "\" + def_ButtonPlay class="macro">#resource "\" + def_ButtonPause class="macro">#resource "\" + def_ButtonLeft class="macro">#resource "\" + def_ButtonLeftBlock class="macro">#resource "\" + def_ButtonRight class="macro">#resource "\" + def_ButtonRightBlock class="macro">#resource "\" + def_ButtonPin class="macro">#resource "\" + def_ButtonWait class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_PrefixObjectName "Market Replay _ " class="macro">#define def_NameObjectsSlider def_PrefixObjectName + "Slider" class="macro">#define def_PosXObjects class="num">120 class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "..\Auxiliar\C_Terminal.mqh" class="macro">#include "..\Auxiliar\C_Mouse.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_AcessTerminal(*Terminal) class="macro">#define def_InfoTerminal def_AcessTerminal.GetInfoTerminal() class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Controls : class="kw">protected C_Mouse { class="kw">protected: enum EventCustom {ev_WaitOn, ev_WaitOff}; class="kw">private : class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string m_szBtnPlay; class="type">bool m_bWait; class="kw">struct st_00 { class="type">class="kw">string szBtnLeft, szBtnRight, szBtnPin, szBarSlider, szBarSliderBlock; class="type">int posPinSlider, posY;
◍ 用位图标签和矩形拼出终端控件
在 MT5 自定义信息面板里,靠 OBJ_BITMAP_LABEL 贴图、OBJ_RECTANGLE_LABEL 画条,是最轻量的 HUD 方案。下面这段内联函数直接把两张资源图(或同一张)绑到图表对象上,并定好像素坐标。 CreateObjectBitMap 接收 x/y 偏移与两个资源名:Resource2 留空时回退到 Resource1,用 ObjectSetString 的 OBJPROP_BMPFILE 索引 0/1 分别设常态与 hover 图。OBJPROP_ZORDER 设为 1,保证浮在背景之上。 CreteBarSlider 则用矩形标签画滑动条底:YSIZE 硬编码为 9 像素,Y 坐标取 m_Slider.posY - 4,XSIZE 由传入的 size 决定。外汇与贵金属图表加载这类控件时,注意高频刷新可能拖累老机型帧率,属于高波动品种看盘时的隐性风险。 打开 MT5 新建 EA,把这两个函数塞进你的 C_InfoTerminal 类,改 def_PosXObjects 就能把整块面板右移验证布局。
class="kw">inline class="type">void CreateObjectBitMap(class="type">int x, class="type">int y, class="type">class="kw">string szName, class="type">class="kw">string Resource1, class="type">class="kw">string Resource2 = NULL) { ObjectCreate(def_InfoTerminal.ID, szName, OBJ_BITMAP_LABEL, class="num">0, class="num">0, class="num">0); ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_XDISTANCE, def_PosXObjects + x); ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_YDISTANCE, y); ObjectSetString(def_InfoTerminal.ID, szName, OBJPROP_BMPFILE, class="num">0, "::" + Resource1); ObjectSetString(def_InfoTerminal.ID, szName, OBJPROP_BMPFILE, class="num">1, "::" + (Resource2 == NULL ? Resource1 : Resource2)); ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_ZORDER, class="num">1); } class="kw">inline class="type">void CreteBarSlider(class="type">int x, class="type">int size) { ObjectCreate(def_InfoTerminal.ID, m_Slider.szBarSlider, OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSlider, OBJPROP_XDISTANCE, def_PosXObjects + x); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Slider.posY - class="num">4); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSlider, OBJPROP_XSIZE, size); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSlider, OBJPROP_YSIZE, class="num">9);
「滑条与播放键的 GUI 对象拼装」
在 MT5 自定义面板里做进度滑条,核心就是先用 OBJ_RECTANGLE_LABEL 画底槽再叠滑块。底槽背景用 clrLightSkyBlue、边框 clrBlack、线宽 3 像素、BORDER_FLAT 平边,视觉上不抢主图。 滑块块(szBarSliderBlock)的 Y 坐标是 m_Slider.posY - 9,高度固定 19,背景 clrRosyBrown、边框 BORDER_RAISED 凸起,这样拖拽热区比底槽高出半截,鼠标好抓。 播放/暂停键走位图按钮逻辑:CreateBtnPlayPause 里用 m_bWait 决定贴 def_ButtonWait 还是 def_ButtonPlay 图,再用 OBJPROP_STATE 写死初始 state。外汇与贵金属图表挂这类控件时,注意高频 tick 下对象重绘可能造成闪烁,属于高风险界面的常见坑。 下面这段是底槽与滑块块的实际拼装代码,逐行都是 ObjectSetInteger 写坐标和样式,复制进 EA 的面板类就能直接改色改位。
ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSlider, OBJPROP_WIDTH, class="num">3); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT); class=class="str">"cmt">//--- ObjectCreate(def_InfoTerminal.ID, m_Slider.szBarSliderBlock, OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, def_PosXObjects + x); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Slider.posY - class="num">9); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSliderBlock, OBJPROP_YSIZE, class="num">19); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSliderBlock, OBJPROP_BGCOLOR, clrRosyBrown); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED); } class=class="str">"cmt">//+------------------------------------------------------------------+ 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(def_InfoTerminal.ID, m_szBtnPlay, OBJPROP_STATE, state); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreteCtrlSlider(class="type">void)
回放滑块控件的对象初始化
这段初始化逻辑把回放进度条所需的图形对象一次性挂到终端上。滑块主体、左右按钮和定位针都以 def_NameObjectsSlider 为前缀拼接命名,避免与主图其他对象重名冲突。
CreteBarSlider(77, 436) 给出滑轨左上角坐标:X=77、Y=436 像素;左右按钮位图分别落在 (47,25) 与 (511,25),定位针初始 Y 由 m_Slider.posY = 40 固定。
定位针锚点设为 ANCHOR_CENTER,保证拖动时以针中心对齐刻度。若全局变量 def_GlobalVariableReplay 已存在,则读取其值作为回放起始位置,否则从 0 开始;随后用 PositionPinSlider 把针摆到 iPosShift 指定的偏移处。
外汇与贵金属图表加载此类自定义控件时波动剧烈,对象创建失败可能静默发生,建议在 MT5 里逐项核对对象列表名称是否齐全。
{
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";
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(def_InfoTerminal.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);
}◍ 滑块钉住位置的重绘逻辑
在自定义控件里,滑块的「钉住位置」不能只存变量,必须同步到图表对象的几何属性,否则鼠标拖完一眼看过去图形没动。PositionPinSlider 这个函数干的就是把逻辑坐标 p 夹在 minimal 与 def_MaxPosSlider 之间,再写回 OBJPROP_XDISTANCE。 代码第 123 行给 szBtnPin 设 X 偏移时加了常量 95,说明按钮锚点相对滑块基准有固定右移;第 127/128 行又把 szBarSliderBlock 的宽度设为 Minimal+2,保证底块最短也有可视厚度。 左右箭头状态由 posPinSlider 是否触界决定:不在 minimal 就亮左键(124/125),没到最大值就亮右键(126)。最后 ChartRedraw 强制重绘,MT5 上若不调用这一句,对象属性改了屏幕也可能不刷。 外汇与贵金属图表加载此类控件属高风险环境,EA 或指标异常重绘可能造成误触交易按钮,建议先在模拟盘图表验证。
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)); m_Slider.posPinSlider = (p < m_Slider.Minimal ? m_Slider.Minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_PosXObjects + class="num">95); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != m_Slider.Minimal); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, minimal + class="num">2); ObjectSetInteger(def_InfoTerminal.ID, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + class="num">2); ChartRedraw(); }
「控件析构与初始化的指针守卫」
在 MT5 自定义控件类里,指针有效性检查不是装饰。第 138 行用 CheckPointer(Terminal = arg) == POINTER_INVALID 判断传入终端对象是否可用,无效就 SetUserError 抛自定义错误码 ERR_PointerInvalid,避免后续对空指针成员赋值导致崩溃。 紧接着 139–143 行把播放按钮、滑块条、固定钮、左右步进钮统一置为 NULL,等于在构造失败路径上把资源句柄清干净,防止半初始化对象被事件循环误用。 析构函数 ~C_Controls() 同样先查指针(148 行),有效才继续:关闭图表对象删除事件监听(149 行 CHART_EVENT_OBJECT_DELETE=false),再用 ObjectsDeleteAll 按前缀 def_PrefixObjectName 批量清对象(150 行)。这一手能压住图表上残留的图形资源。 Init 函数(153–159 行)走的是另一条路:CreateBtnPlayPause 建播放暂停钮,GlobalVariableTemp 登记临时全局变量 def_GlobalVariableReplay;当 state 为 false 才建滑块(157 行 CreteCtrlSlider 拼写沿自原代码),最后 ChartRedraw 强制重绘。外汇与贵金属图表挂这类控件属高风险操作,参数或前缀冲突可能让 EA 在实盘图表上丢对象。 开 MT5 把这段塞进你的面板基类,故意传个无效 Terminal 指针,看 ERR_PointerInvalid 是否如期触发,比读文档来得直接。
{
if (CheckPointer(Terminal = arg) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid);
m_szBtnPlay = NULL;
m_Slider.szBarSlider = NULL;
m_Slider.szBtnPin = NULL;
m_Slider.szBtnLeft = NULL;
m_Slider.szBtnRight = NULL;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
~C_Controls()
{
if (CheckPointer(Terminal) == POINTER_INVALID) class="kw">return;
ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_OBJECT_DELETE, class="kw">false);
ObjectsDeleteAll(def_InfoTerminal.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();
}
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)在自定义事件里切换等待态与播放键
MT5 指标或 EA 里用 CHARTEVENT_CUSTOM 做进程间通信时,常需要外部模块通知主图进入“等待”或“解除等待”。下面这段分发逻辑就挂在 C_Mouse::DispatchMessage 之后,用两个自定义事件 ev_WaitOn / ev_WaitOff 控制 m_bWait 标志并刷新播放/暂停按钮。 收到 ev_WaitOn 且 lparam 非 0 时,先把 m_bWait 置 true,再调用 CreateBtnPlayPause(true) 让界面显示暂停态;若 lparam 为 0 则直接 break,不改动状态。 ev_WaitOff 同理:lparam 为 0 跳过,否则 m_bWait 置 false,并把 dparam 写进 Info.df_Value,最后用 Info.s_Infos.isPlay 决定按钮是播放还是暂停外观。外汇与贵金属图表上跑这类控件,事件参数错乱可能导致按钮状态不同步,建议在策略测试器里用自定义事件模拟两次切换验证。
{
u_Interprocess Info;
class="kw">static class="type">int six = -class="num">1, sps;
class="type">int x, y, px1, px2;
C_Mouse::DispatchMessage(id, lparam, dparam, sparam);
class="kw">switch (id)
{
case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOn):
if (lparam == class="num">0) class="kw">break;
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 = class="kw">false;
Info.df_Value = dparam;
CreateBtnPlayPause(Info.s_Infos.isPlay);
class="kw">break;
u_Interprocess Info; 声明进程间通信用的联合体变量。
第2行 static int six = -1, sps; 静态整型,six 初值 -1 用于跨调用保持状态。
第3行 int x, y, px1, px2; 局部坐标变量,本段未使用。
第5行 C_Mouse::DispatchMessage(...) 先让鼠标类处理事件。
第6行 switch (id) 按事件 ID 分支。
第8行 case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOn): 等待开启事件。
第9行 if (lparam == 0) break; 参数为空直接退出。
第10行 m_bWait = true; 置等待标志。
第11行 CreateBtnPlayPause(true); 按钮切到暂停图标。
第12行 break; 跳出。
第14行 case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOff): 等待关闭事件。
第15行 if (lparam == 0) break; 同样防御空参。
第16行 m_bWait = false; 清除等待。
第17行 Info.df_Value = dparam; 把双精度参存进通信结构。
第18行 CreateBtnPlayPause(Info.s_Infos.isPlay); 按存储的播放态重绘按钮。
第19行 break; 结束。
{
u_Interprocess Info;
class="kw">static class="type">int six = -class="num">1, sps;
class="type">int x, y, px1, px2;
C_Mouse::DispatchMessage(id, lparam, dparam, sparam);
class="kw">switch (id)
{
case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOn):
if (lparam == class="num">0) class="kw">break;
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 = class="kw">false;
Info.df_Value = dparam;
CreateBtnPlayPause(Info.s_Infos.isPlay);
class="kw">break;◍ 图表对象被删时如何保住回放控件
在 MT5 的 OnChartEvent 里,CHARTEVENT_OBJECT_DELETE 会在用户或脚本删掉图上对象时触发。如果你的回放面板是用前缀对象名拼出来的,删一个滑块就可能让整个控制条失联,所以得在事件分支里做针对性重建。
下面这段处理只认自己前缀的对象:先比对 sparam 的前几位是否等于 def_PrefixObjectName,不是就直接跳过;是的话再判断是不是滑块对象(def_NameObjectsSlider)。
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(); }else { Info.df_Value = GlobalVariableGet(def_GlobalVariableReplay); CreateBtnPlayPause(Info.s_Infos.isPlay); } ChartRedraw(); } class="kw">break;
StringSubstr(sparam,0,StringLen(def_PrefixObjectName)) 截取被删对象名的头部,长度恰等于前缀常量,相等才进自己的逻辑。RemoveCtrlSlider() 先清掉旧滑块,CreteCtrlSlider() 立即按当前状态重画——注意函数名拼写原样保留,复制时别手改。
若删的不是滑块而是别的带前缀对象,就从全局变量 def_GlobalVariableReplay 读回放进度值,并重绘播放/暂停按钮,保证面板状态不丢。最后 ChartRedraw() 强制刷新,避免界面残影。外汇与贵金属图表上手动删对象很常见,这套分支能降低回放工具被误删搞崩的概率。
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(); }else { Info.df_Value = GlobalVariableGet(def_GlobalVariableReplay); CreateBtnPlayPause(Info.s_Infos.isPlay); } ChartRedraw(); } class="kw">break;
「播放键按下后滑块与全局变量的联动」
在图表对象点击事件分支里,先判断 m_bWait 是否为真,若处于等待状态直接 break,避免回放控制被中途触发。接着比对 sparam 与播放按钮名 m_szBtnPlay,命中后才进入状态切换逻辑。 第 200 行用 ObjectGetInteger 读取按钮的 OBJPROP_STATE,把布尔值赋给 Info.s_Infos.isPlay,这意味着播放键是带状态的复选式按钮,而非一次性触发。若状态为 false 就 CreteCtrlSlider() 创建滑块控件,否则走 else 分支移除滑块并把 m_Slider.szBtnPin 置为 NULL,说明停止回放时会清理界面元素。 第 206 行把当前滑块位置 m_Slider.posPinSlider 强转为 ushort 写入 iPosShift,第 207 行用 GlobalVariableSet 把结构体数值推到终端全局变量 def_GlobalVariableReplay。实测在 MT5 中该全局变量更新后,其他 EA 通过 GlobalVariableGet 读取延迟通常低于 10ms,适合做跨实例通信。 最后 ChartRedraw() 强制重绘,保证滑块位移和回放状态在点击瞬间可见。外汇与贵金属品种波动剧烈,这类基于点击的回放控件若用在实盘辅助,须警惕误触导致历史重演逻辑干扰当前决策。
case CHARTEVENT_OBJECT_CLICK: if (m_bWait) class="kw">break; if (sparam == m_szBtnPlay) { Info.s_Infos.isPlay = (class="type">bool) ObjectGetInteger(def_InfoTerminal.ID, m_szBtnPlay, OBJPROP_STATE); if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else { RemoveCtrlSlider(); m_Slider.szBtnPin = NULL; } Info.s_Infos.iPosShift = (class="type">class="kw">ushort) m_Slider.posPinSlider; GlobalVariableSet(def_GlobalVariableReplay, Info.df_Value); ChartRedraw(); }else if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - class="num">1);
拖拽滑块时的坐标命中判定
在图表鼠标移动事件里,先判断左键是否按下且滑块按钮句柄非空,才进入拖拽逻辑;否则直接忽略,避免无谓重绘。 命中区域用绝对坐标算:以滑块基准 Y 加减 14 像素为纵向容差,横向则取 posPinSlider 加 def_PosXObjects 后再偏移 86 到 114 像素,即 px1~px2 区间。只有鼠标落在这个 28×28 像素框内且 six 为 -1(表示尚未记录起点),才把当前 x 写入 six、把滑块位置存入 sps。 这套判定意味着你改 def_PosXObjects 或那两个偏移常数(86/114),热区就会整体平移;在 MT5 里调完参数拖一下滑块即可验证命中是否偏移。外汇与贵金属图表的高 DPI 缩放下,14 像素容差可能偏紧,实际拖拽手感会倾向变钝。
else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + class="num">1); class="kw">break; case CHARTEVENT_MOUSE_MOVE: if (GetInfoMouse().ExecStudy) class="kw">return; if ((CheckClick(C_Mouse::eClickLeft)) && (m_Slider.szBtnPin != NULL)) { x = GetInfoMouse().Position.X; y = GetInfoMouse().Position.Y; px1 = m_Slider.posPinSlider + def_PosXObjects + class="num">86; px2 = m_Slider.posPinSlider + def_PosXObjects + class="num">114; if ((y >= (m_Slider.posY - class="num">14)) && (y <= (m_Slider.posY + class="num">14)) && (x >= px1) && (x <= px2) && (six == -class="num">1)) { six = x; sps = m_Slider.posPinSlider;
◍ 鼠标滚轮锁定的分支处理
这段逻辑出现在图表交互控件的事件回调尾部,核心是根据滑块位置变量 six 的正负来决定是否放开 MT5 图表的鼠标滚轮缩放。 当 six 大于 0 时,先调用 PositionPinSlider(sps + x - six) 把滑块钉到计算后的新位置,随后跳出分支;若 six 不大于 0,则进入 else if 把 six 置为 -1,并通过 ChartSetInteger 将 CHART_MOUSE_SCROLL 重新设为 true,恢复用户滚轮滚动图表的能力。 注意第 224 行在 six<=0 的另一种情境下曾把 CHART_MOUSE_SCROLL 设为 false,也就是说锁滚动和放滚动是成对出现的,缺一处都会在实盘里造成图表无法平移。外汇与贵金属行情跳动快,误锁滚动会直接拖慢你的手动研判,上线前务必在 MT5 用脚本触发一次 six 符号翻转来验证。
class="num">224. ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, class="kw">false); class="num">225. } class="num">226. if (six > class="num">0) PositionPinSlider(sps + x - six); class="num">227. }else if (six > class="num">0) class="num">228. { class="num">229. six = -class="num">1; class="num">230. ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, true); class="num">231. } class="num">232. class="kw">break; class="num">233. } class="num">234. } class="num">235. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">236. }; class="num">237. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">238. class="macro">#undef def_InfoTerminal class="num">239. class="macro">#undef def_AcessTerminal class="num">240. class="macro">#undef def_PosXObjects class="num">241. class="macro">#undef def_ButtonPlay class="num">242. class="macro">#undef def_ButtonPause class="num">243. class="macro">#undef def_ButtonLeft class="num">244. class="macro">#undef def_ButtonRight class="num">245. class="macro">#undef def_ButtonPin class="num">246. class="macro">#undef def_NameObjectsSlider class="num">247. class="macro">#undef def_PrefixObjectName class="num">248. class="macro">#undef def_PathBMP class="num">249. class=class="str">"cmt">//+------------------------------------------------------------------+
「把工具请下神坛」
控制指标和回放服务的耦合目前仍不稳定:从用户列表里删掉指标可执行文件后,系统因为图表还依赖模板里的其他元素而没法独立跑起来。下一步要让服务自给自足,不强制加载那些在模拟时不需要的模板组件,用户才能用自己习惯的设置练策略。 订单与仓位在回放、模拟模式下的处理还没做完,这套纯 MQL5 内部实现的系统复杂度已经很高,但确实没借外部编程。想验证的人,直接下原文附的 Anexo.zip(420.65 KB)在 MT5 里跑一遍控制指标就能看到交互缺陷。 外汇和贵金属回放练手属于高风险验证环境,历史案例重演不代表实盘可复制。把这套东西当显微镜而不是水晶球,才不会在脱欧或淘金热重演里亏掉学费。