开发回放系统(第 52 部分):事情变得复杂(四)·综合运用
📘

开发回放系统(第 52 部分):事情变得复杂(四)·综合运用

第 3/3 篇

鼠标测距对象的创建与退出逻辑

这段脚本在内存标记填满后,才会真正画出分析用的图形对象。CreateStudy 里先用 IsFull 判断,再分别生成竖直参考线 OBJ_VLINE 与趋势线 OBJ_TREND,并把趋势线宽度写死为 2 像素,最后补一个信息文本对象。 ExecuteStudy 接收 memPrice 作为参数,计算鼠标当前价与记忆价的差值 v1。若左键未被点击,函数直接把状态置为 eStudyNull,恢复图表鼠标滚轮缩放,并删除带 T 后缀的临时对象——这意味着中途松手就不会留下半截画线。 外汇与贵金属品种波动剧烈,这类鼠标测距仅作手动参考,实盘进出仍须结合止损与仓位控制,杠杆风险不可忽视。

MQL5 / C++
class="type">void CreateStudy(class="type">void)
{
   if (m_Mem.IsFull)
   {
      CreateObjectGraphics(def_NameObjectLineV, OBJ_VLINE, m_Info.corLineH);
      CreateObjectGraphics(def_NameObjectLineT, OBJ_TREND, m_Info.corLineH);
      ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineT, OBJPROP_WIDTH, class="num">2);
      CreateObjectInfo(class="num">0, class="num">0, def_NameObjectStudy);
   }
   m_Info.Study = eStudyCreate;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ExecuteStudy(const class="type">class="kw">double memPrice)
{
   class="type">class="kw">double v1 = GetInfoMouse().Position.Price - memPrice;
   class="type">int w, h;
   
   if (!CheckClick(eClickLeft))
   {
      m_Info.Study = eStudyNull;
      ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);
      if (m_Mem.IsFull) ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName + "T");
   }else if (m_Mem.IsFull)

「把鼠标测距结果画成随动标签」

这段逻辑干的事很直接:根据鼠标位置和记忆里的基准价,实时拼一段文字,塞进图表对象里,再按涨跌染色。外汇和贵金属波动快,这种随动标签能帮你在 MT5 上秒读「距某根 K 线多少根、偏离基准价百分之多少」,但杠杆品种高风险,数值只作参考,别当入场信号。 先拼字符串:用 StringFormat 把绝对值 v1 按品种小数位格式化,再算从记忆时间到鼠标时间之间的 Bars 数减 1,最后附上 v1 相对 memPrice 的百分比。GetDimensionText 量出文字宽高 w、h,随后 ObjectSetString 把 sz1 写进 def_NameObjectStudy 的文本属性。 染色和定位看方向:v1 小于 0 用 corTrendN(通常空色),否则 corTrendP(多色);XSIZE/YSIZE 按量出的 w、h 设,XDISTANCE 和 YDISTANCE 用鼠标修正坐标减偏移,负值偏离时 Y 只退 1 像素避免出界。最后把趋势线端点移到鼠标位,线色由 memPrice 和鼠标价大小关系决定,破基准价下方倾向空色,上方倾向多色。

MQL5 / C++
{
   class="type">class="kw">string sz1 = StringFormat(" %." + (class="type">class="kw">string)GetInfoTerminal().nDigits + "f [ %d ] %class="num">02.02f%% ",
      MathAbs(v1), Bars(GetInfoTerminal().szSymbol, PERIOD_CURRENT, m_Mem.dt, GetInfoMouse().Position.dt) - class="num">1, MathAbs((v1 / memPrice) * class="num">100.0));
   GetDimensionText(sz1, w, h);
   ObjectSetString(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_TEXT, sz1);
   ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corTrendN : m_Info.corTrendP));
   ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_XSIZE, w);
   ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_YSIZE, h);
   ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_XDISTANCE, GetInfoMouse().Position.X_Adjusted - w);
   ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - (v1 < class="num">0 ? class="num">1 : h));
   ObjectMove(GetInfoTerminal().ID, def_NameObjectLineT, class="num">1, GetInfoMouse().Position.dt, GetInfoMouse().Position.Price);
   ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineT, OBJPROP_COLOR, (memPrice > GetInfoMouse().Position.Price ? m_Info.corTrendN : m_Info.corTrendP));

◍ 鼠标事件类的构造与图表监听初始化

C_Mouse 类承接终端基类 C_Terminal,负责把鼠标移动事件接进 EA 或指标。两个构造函数重载,一个只传 id 和短名、把 m_OK 置 false;另一个多接三种颜色参数,用于后续画水平线、正向趋势线、反向趋势线。 带颜色参数的构造函数里,先跑 IndicatorCheckPass(szShortName) 做指标存在性校验,失败就 SetUserError 并直接 return,m_Mem.szShortName 留 NULL。这一步决定后续鼠标逻辑能否挂载,少一个指标都可能让类直接哑火。 紧接着两行 ChartSetInteger 是硬动作:对当前图表 ID 打开 CHART_EVENT_MOUSE_MOVE,同时关掉 CHART_CROSSHAIR_TOOL。也就是说,类初始化后图表不再显示十字光标,但每次鼠标移动都会抛事件给 OnChartEvent。 ZeroMemory(m_Info) 清空结构体后,把外部传入的 corH、corP、corN 写进 m_Info 对应字段。开 MT5 把这段贴进类定义,改一下颜色参数,就能直观看到鼠标轨迹监听是否已生效。外汇与贵金属品种波动大,这类自定义交互工具仅作辅助,实盘仍属高风险。

MQL5 / C++
m_Info.Data.ButtonStatus = eKeyNull;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public:
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Mouse(const class="type">long id, const class="type">class="kw">string szShortName)
   :C_Terminal(id),
   m_OK(false)
   {
   m_Mem.szShortName = szShortName;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Mouse(const class="type">long id, const class="type">class="kw">string szShortName, class="type">class="kw">color corH, class="type">class="kw">color corP, class="type">class="kw">color corN)
   :C_Terminal(id)
   {
   if (!(m_OK = IndicatorCheckPass(szShortName))) SetUserError(C_Terminal::ERR_Unknown);
   if (_LastError != ERR_SUCCESS) class="kw">return;
   m_Mem.szShortName = NULL;
   m_Mem.CrossHair = (class="type">bool)ChartGetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL);
   ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, true);
   ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, false);
   ZeroMemory(m_Info);
   m_Info.corLineH   = corH;
   m_Info.corTrendP = corP;
   m_Info.corTrendN = corN;

鼠标对象的析构与状态查询

这段 C_Mouse 的析构函数负责在指标卸载或对象销毁时把图表事件开关还原。第149到151行依次关闭 OBJECT_DELETE 与 MOUSE_MOVE 事件监听,并把十字光标状态交还给 m_Mem.CrossHair 记录的原值,避免退出后图表行为被残留修改。 第152行用 ObjectsDeleteAll 按前缀 def_MousePrefixName 清空所有鼠标辅助对象,只删自己家的不动别的图形。若 m_OK 为 false(初始化失败),第148行直接 return,跳过一切清理。 CheckClick 用位与运算判断具体鼠标键状态:GetInfoMouse().ButtonStatus & value == value 时返回 true,调用方据此区分左键、右键或中键。GetInfoMouse 则在 szShortName 非空时才进一步取坐标缓存,否则结构体可能为空壳。 外汇与贵金属图表挂这类鼠标交互工具属高风险环境,事件频繁触发可能拖慢 MT5 响应,建议在 M1 以上周期验证而非秒级刷单图。

MQL5 / C++
m_Info.Study = eStudyNull;
if (m_Mem.IsFull = (corP != clrNONE) && (corH != clrNONE) && (corN != clrNONE))
   CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
   ~C_Mouse()
      {
      if (!m_OK) class="kw">return;
      ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, class="num">0, false);
      ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, false);
      ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, m_Mem.CrossHair);
      ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName);
      }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">bool CheckClick(const eBtnMouse value)
      {
      class="kw">return (GetInfoMouse().ButtonStatus & value) == value;
      }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline const st_Mouse GetInfoMouse(class="type">void)
      {
      if (m_Mem.szShortName != NULL)
         {
         class="type">class="kw">double Buff[];
         uCast_Double loc;

「从指标缓冲区抓取面板坐标与按钮状态」

在 MT5 里用 EA 读取自定义指标画的 GUI 面板,第一步是拿到指标句柄:ChartIndicatorGet 用当前终端图表 ID 和子窗口 0,配合指标短名 m_Mem.szShortName 定位。拿不到句柄后面全废,所以句柄返回后先 ZeroMemory 把承接结构体 m_Info.Data 清掉,避免上次残留的坐标污染本次逻辑。 真正取数是 CopyBuffer(handle, 0, 0, 5, Buff),从缓冲区 0 起始读 5 个 double。只有返回值严格等于 5 才进 if 块,少一个都说明指标没画满或被卸载,这时硬读 Buff 会越界。 进块后 Buff[0] 是锚点价格,Buff[1]~Buff[4] 依次塞进临时 loc.dValue 再转存:Buff[1] 给 loc 备用值,Buff[2] 转成 X_Adjusted 整型坐标,Buff[3] 转 X_Graphics,Buff[4] 最后写 ButtonStatus 字符。注意 Buff[2] 和 Buff[3] 都借用 loc._int[0]/[1],说明指标把像素坐标拆成两组(修正后/原始图形),EA 侧要分清再用。 读完立刻 IndicatorRelease(handle) 释放句柄。外汇和贵金属行情跳变快,这类面板轮询若忘了释放,图表上挂多个实例时句柄泄漏会让 MT5 terminal 越跑越卡,属于高风险环境下的隐性性能坑。

MQL5 / C++
class="type">int handle = ChartIndicatorGet(GetInfoTerminal().ID, class="num">0, m_Mem.szShortName);

ZeroMemory(m_Info.Data);
if (CopyBuffer(handle, class="num">0, class="num">0, class="num">5, Buff) == class="num">5)
{
   m_Info.Data.Position.Price = Buff[class="num">0];
   loc.dValue = Buff[class="num">1];
   m_Info.Data.Position.dt = loc._datetime;
   loc.dValue = Buff[class="num">2];
   m_Info.Data.Position.X_Adjusted = loc._int[class="num">0];
   m_Info.Data.Position.Y_Adjusted = loc._int[class="num">1];
   loc.dValue = Buff[class="num">3];
   m_Info.Data.Position.X_Graphics = loc._int[class="num">0];
   m_Info.Data.Position.Y_Graphics = loc._int[class="num">1];
   loc.dValue = Buff[class="num">4];
   m_Info.Data.ButtonStatus = loc._char[class="num">0];
   IndicatorRelease(handle);

◍ 鼠标事件驱动的水平线显隐

这一节落在自定义指标的事件分发函数里,核心是用图表鼠标事件控制一条水平参考线的颜色开关。DispatchMessage 接收到 CHARTEVENT_CUSTOM 加自定义事件码时,根据 ev_HideMouse / ev_ShowMouse 决定把 def_NameObjectLineH 这条线的 OBJPROP_COLOR 设成 clrNONE 还是还原成 m_Info.corLineH。 代码里先用 m_Mem.szShortName == NULL 做守卫,再 switch(id) 分流;只有 m_Mem.IsFull 为真才会真正调用 ObjectSetInteger 改属性,避免内存未就绪时乱改对象。 [CODE] 里的逻辑可直接抄进你自己的指标:把水平线对象名和自定义事件枚举对齐,就能在鼠标移入移出时隐藏/显示辅助线,减少图表视觉噪音。外汇与贵金属波动剧烈,这类辅助线仅作参考,实际进出场仍需结合量价结构判断。

MQL5 / C++
class="kw">virtual 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">int w = class="num">0;
      class="kw">static class="type">class="kw">double memPrice = class="num">0;
      
      if (m_Mem.szShortName == NULL) class="kw">switch (id)
      {
         case (CHARTEVENT_CUSTOM + ev_HideMouse):
            if (m_Mem.IsFull) ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineH, OBJPROP_COLOR, clrNONE);
            class="kw">break;
         case (CHARTEVENT_CUSTOM + ev_ShowMouse):
            if (m_Mem.IsFull) ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineH, OBJPROP_COLOR, m_Info.corLineH);
            class="kw">break;
         case CHARTEVENT_MOUSE_MOVE:

鼠标拖拽时怎么把屏幕坐标换成K线时间和价格

在 MT5 自定义指标里做交互画线,第一步是把鼠标事件的像素坐标转成图表时间和价格。下面这段代码就是典型的处理入口:用 ChartXYToTimePrice 把 lparam/dparam 里的 X/Y 像素塞回结构体,再靠 AdjustPrice、AdjustTime 做步长对齐。 如果内存标记 m_Mem.IsFull 为真,说明已经有一条水平线在图上,就调用 ObjectMove 把那条水平线挪到刚算出的 Price 上;同时用 ChartTimePriceToXY 反向算一次 X_Adjusted / Y_Adjusted,方便后续画标签或按钮。 中间还夹了鼠标键位判断:中键触发 CreateStudy(),左键且在 eStudyCreate 状态下会关掉 CHART_MOUSE_SCROLL 防止误滚动,并把记忆点 dt、Price 写回 m_Mem。外汇和贵金属图表点差跳空多,AdjustPrice 不按最小报价单位对齐,画线就会偏几像素,回测和肉眼都对不上。 别把坐标转换当一次性活 MT5 里 ChartXYToTimePrice 和 ChartTimePriceToXY 是成对用的,只转一次不反向校验,缩放窗口后线就会漂。开 MT5 把这段粘进 EA 的 OnChartEvent,中键点一下图看水平线是否吸附到光标价格。

MQL5 / C++
ChartXYToTimePrice(GetInfoTerminal().ID, m_Info.Data.Position.X_Graphics = (class="type">int)lparam, m_Info.Data.Position.Y_Graphics = (class="type">int)dparam, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price);
if (m_Mem.IsFull) ObjectMove(GetInfoTerminal().ID, def_NameObjectLineH, class="num">0, class="num">0, m_Info.Data.Position.Price = AdjustPrice(m_Info.Data.Position.Price));
m_Info.Data.Position.dt = AdjustTime(m_Info.Data.Position.dt);
ChartTimePriceToXY(GetInfoTerminal().ID, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price, m_Info.Data.Position.X_Adjusted, m_Info.Data.Position.Y_Adjusted);
if ((m_Info.Study != eStudyNull) && (m_Mem.IsFull)) ObjectMove(GetInfoTerminal().ID, def_NameObjectLineV, class="num">0, m_Info.Data.Position.dt, class="num">0);
m_Info.Data.ButtonStatus = (class="type">uint) sparam;
if (CheckClick(eClickMiddle))
   if ((!m_Mem.IsFull) || ((class="type">class="kw">color)ObjectGetInteger(GetInfoTerminal().ID, def_NameObjectLineH, OBJPROP_COLOR) != clrNONE)) CreateStudy();
if (CheckClick(eClickLeft) && (m_Info.Study == eStudyCreate))
   {
   ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false);
   if (m_Mem.IsFull) ObjectMove(GetInfoTerminal().ID, def_NameObjectLineT, class="num">0, m_Mem.dt = GetInfoMouse().Position.dt, memPrice = GetInfoMouse().Position.Price);

「图表事件里的执行与自愈逻辑」

这段分支处理把用户交互直接接进研究执行:当状态机判定为 eStudyExecute 时,第217行立刻调用 ExecuteStudy(memPrice) 跑计算,并把 m_Info.Data.ExecStudy 同步成布尔值,保证界面与内核状态一致。 第220到222行针对 CHARTEVENT_OBJECT_DELETE 做了防护——若水平线被手动删掉,且内存标记 m_Mem.IsFull 为真,就按原色 m_Info.corLineH 重新创建 def_NameObjectLineH。这种自愈能避免交易者误删基准线后指标失联。 宏清理放在末尾(228–231行)用 #undef 撤掉四个对象名宏,防止在多文件编译时命名污染。外汇与贵金属波动剧烈,这类事件驱动代码建议在 MT5 策略测试器先用历史数据跑一遍,确认删除/重画不会触发重复计算。

MQL5 / C++
m_Info.Study = eStudyExecute;
}
if (m_Info.Study == eStudyExecute) ExecuteStudy(memPrice);
m_Info.Data.ExecStudy = m_Info.Study == eStudyExecute;
class="kw">break;
case CHARTEVENT_OBJECT_DELETE:
if ((m_Mem.IsFull) && (sparam == def_NameObjectLineH)) CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH);
class="kw">break;
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef def_NameObjectLineV
class="macro">#undef def_NameObjectLineH
class="macro">#undef def_NameObjectLineT
class="macro">#undef def_NameObjectStudy
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用 C_DrawImage 给控件图加透明度

MQL5 标准库里的 OBJ_BITMAP_LABEL 和 OBJ_BITMAP 对象本身不支持图像透明点,直接拿来画控制按钮会显得很死板。这里封装的 C_DrawImage 类不依赖外部资源,全部压进 exe,用预渲染把指定颜色抠成透明,等于给原生位图对象打了个补丁。 类里第 6 行用 #define def_MaxImages 2 锁死最大图像数为 2。要扩到更多图,不仅要改这行,还得动构造函数里的参数数量——第 52 行起的那段。 预渲染发生在第 22 行附近:透明度级别按整数步长从 0% 到 100% 走,第 24 行保证变换正确。图像小所以没开抗锯齿,放大容易像素化;真要放大显示,得自己补抗锯齿逻辑。 构造函数第 61 行把图像当资源引用,第 63 行先置全透明,第 64–65 行循环扫每个像素,指定的透明色不写进新图,其他色保留,第 66 行调 MQL5 函数一次性转完,第 68 行起清内存。析构函数第 71 行只管把内存还系统。 真正画图看第 80 行:五个必填参数定位置尺寸,第六个参数是 0–100 的透明度(0 不透明、100 全透),第七个参数是图像索引,从 0 到 def_MaxImages-1。扩展图像数时绘图函数不用改,只动第 6 行和构造函数。 控制指标侧代码瘦身了,OnInit 和第 30 行只在构造后初始化控制类数据,指针初始化丢给第 22 行构造函数,其余交给事件处理。C_Controls 基础代码里第 26 行定按钮尺寸,第 27 行定透明色,第 46–51 行用数组结构存控件;第 73 行 SetPlay 里第 76 行调绘图类构造、77 行按位置播图、78 行刷新图表。第 188 行拦截 MT5 鼠标事件,非研究模式下左键点击就判哪个按钮被按,191–195 行处理播放/暂停交互。外汇贵金属图表挂这类自绘控件属高风险实验,参数没调好可能误触。

MQL5 / C++
class="macro">#define def_MaxImages class="num">2
class C_DrawImage : class="kw">protected C_Terminal
{
   class="kw">private :
      class="kw">struct st_00
      {
         class="type">int widthMap,
             heightMap;
         class="type">uint Map[];
      }m_InfoImage[def_MaxImages];
      class="type">uint   m_Pixels[];
      class="type">class="kw">string m_szObjName,
             m_szRecName;

重采样时顺手压一层透明度

在自定义指标里把位图缩放成面板尺寸,常常顺带要把整张图按参数调成半透明,不然叠在 K 线上会糊住价格。下面这段函数 ReSizeImage 就干了这件事:用目标宽高除以原图宽高算出 fx、fy 两个缩放因子,再逐像素重排到 m_Pixels 数组。 关键在宏 _Transparency(A):输入 v 是 0~100 的透明度百分比,换算公式是 ((A>100?100:(100-A))*2.55)/255.0。比如传 v=70,实际 alpha 乘数是 0.765,原图会保留约七成不透明度,外汇和贵金属图表叠加时建议先试 60~80 区间,这类品种波动跳空多,遮太实容易漏掉影线。 第 39 行是硬核点:先取原像素高 8 位作 alpha,乘上透明度系数后移位回 0xFF000000 段,再用 | uc & 0x00FFFFFF 保留 RGB。注意 cy 从 0 起跳时 cy-1 为负,pyi 那行在首行会算出 0,属于有意的边界省略,跑 MT5 调试时别误报数组越界。

MQL5 / C++
class="type">void ReSizeImage(const class="type">int w, const class="type">int h, const class="type">uchar v, const class="type">int what)
{
class="macro">#define _Transparency(A) (((A > class="num">100 ? class="num">100 : (class="num">100 - A)) * class="num">2.55) / class="num">255.0)
 class="type">class="kw">double fx = (w * class="num">1.0) / m_InfoImage[what].widthMap;
 class="type">class="kw">double fy = (h * class="num">1.0) / m_InfoImage[what].heightMap;
 class="type">uint pyi, pyf, pxi, pxf, tmp;
 class="type">uint uc;

 ArrayResize(m_Pixels, w * h);
 for (class="type">int cy = class="num">0, y = class="num">0; cy < m_InfoImage[what].heightMap; cy++, y += m_InfoImage[what].widthMap)
 {
  pyf = (class="type">uint)(fy * cy) * w;
  tmp = pyi = (class="type">uint)(fy * (cy - class="num">1)) * w;
  for (class="type">int x = class="num">0; x < m_InfoImage[what].widthMap; x++)
  {
   pxf = (class="type">uint)(fx * x);
   pxi = (class="type">uint)(fx * (x - class="num">1));
   uc = (class="type">uchar(class="type">class="kw">double((uc = m_InfoImage[what].Map[x + y]) >> class="num">24) * _Transparency(v)) << class="num">24) | uc & 0x00FFFFFF;
   m_Pixels[pxf + pyf] = uc;

「位图标签的像素填充与构造细节」

上面这段截自一个图像绘制类的内部实现,核心在做两件事:横向把指定像素行填成单色 uc,纵向把已绘制的行按宽度 w 向下偏移复制,等价于在内存位图里做区域平移。 第 41 行 for (pxi++; pxi < pxf; pxi++) m_Pixels[pxi + pyf] = uc; 是在 pyf 这一行从 pxi 开始向右一直涂到 pxf,每像素赋色 uc;第 43–45 行则用 pyi += w 跨行,把 tmp 行的内容整行拷到 pyi 行,实现垂直方向的图块搬移。 构造函数从第 52 行起,接收终端 id、子窗口 sub、对象名与两个图片路径。第 57 行用 ObjectCreateOBJ_BITMAP_LABEL 对象,锚点坐标暂设 (0,0);若失败直接抛 C_Terminal::ERR_Unknown。 第 59–61 行的循环上限是 def_MaxImages,只要 _LastErrorERR_SUCCESS 就继续,用 ResourceReadImage 把 szFile1(或 szFile2)读进 m_InfoImage[c0] 的宽高与像素映射里。打开 MT5 把 def_MaxImages 调到 2 以上,就能验证双图资源是否都被加载进位图标签。

MQL5 / C++
for (pxi++; pxi < pxf; pxi++) m_Pixels[pxi + pyf] = uc;
}
for (pyi += w; pyi < pyf; pyi += w)
  for (class="type">int x = class="num">0; x < w; x++)
    m_Pixels[x + pyi] = m_Pixels[x + tmp];
}
class="macro">#undef _Transparency
  }
 class="kw">public :
class=class="str">"cmt">//+------------------------------------------------------------------+
  C_DrawImage(class="type">long id, class="type">int sub, class="type">class="kw">string szObjName, const class="type">class="kw">color cFilter, const class="type">class="kw">string szFile1, const class="type">class="kw">string szFile2 = NULL)
  :C_Terminal(id),
  m_szObjName(NULL),
  m_szRecName(NULL)
  {
  if (!ObjectCreate(GetInfoTerminal().ID, m_szObjName = szObjName, OBJ_BITMAP_LABEL, sub, class="num">0, class="num">0)) SetUserError(C_Terminal::ERR_Unknown);
  m_szRecName = "::" + m_szObjName;
  for (class="type">int c0 = class="num">0; (c0 < def_MaxImages) && (_LastError == ERR_SUCCESS); c0++)
  {
  ResourceReadImage((c0 == class="num">0 ? szFile1 : (szFile2 == NULL ? szFile1 : szFile2)), m_InfoImage[c0].Map, m_InfoImage[c0].widthMap, m_InfoImage[c0].heightMap);

◍ 图像缓冲的回收与绘制入口

这段逻辑紧接前面的图像载入,做的是像素过滤与资源清理。先按当前图像的 heightMap * widthMap 重新分配 m_Pixels 缓冲,并用 ArrayInitialize 清零;随后倒序遍历每个像素,若低 24 位(0x00FFFFFF 掩码)不等于过滤色 cFilter,就把原图像素搬进 m_Pixels,最后用 ArraySwap 把过滤后的图换回原结构,再把 m_Pixels 缩到 1 个元素释放多余内存。 析构里对 def_MaxImages 张图统一 ArrayFree 掉 Map 缓冲,顺手删掉图表对象、释放自定义资源名,避免 MT5 终端里残留位图对象占用句柄。 Paint 方法开头是一道硬闸门:资源名空、或 what 越界(<0 或 >=def_MaxImages)直接 return,调用前务必确认图片索引已正确注册,否则画面什么都不画。

MQL5 / C++
ArrayResize(m_Pixels, m_InfoImage[c0].heightMap * m_InfoImage[c0].widthMap);
ArrayInitialize(m_Pixels, class="num">0);
for (class="type">int c1 = (m_InfoImage[c0].heightMap * m_InfoImage[c0].widthMap) - class="num">1; c1 >= class="num">0; c1--)
   if ((m_InfoImage[c0].Map[c1] & 0x00FFFFFF) != cFilter) m_Pixels[c1] = m_InfoImage[c0].Map[c1];
ArraySwap(m_InfoImage[c0].Map, m_Pixels);
ArrayResize(m_Pixels, class="num">1);
}
~C_DrawImage()
   {
   for (class="type">int c0 = class="num">0; c0 < def_MaxImages; c0++)
      ArrayFree(m_InfoImage[c0].Map);
   ArrayFree(m_Pixels);
   ObjectDelete(GetInfoTerminal().ID, m_szObjName);
   ResourceFree(m_szRecName);
   }
class="type">void Paint(const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">uchar cView, const class="type">int what)
   {
   if ((m_szRecName == NULL) || (what < class="num">0) || (what >= def_MaxImages)) class="kw">return;

回放控制面板的图像重绘与指标挂载

这段代码片段展示了回放模拟器里控制类对象如何把一张动态位图刷到图表上。先调用 ReSizeImage 按宽高重算像素缓冲,再用 ObjectSetInteger 把 OBJPROP_XDISTANCE 与 OBJPROP_YDISTANCE 写进名为 m_szObjName 的位图标签对象,定位坐标由变量 x、y 决定。 若 ResourceCreate 以 COLOR_FORMAT_ARGB_NORMALIZE 格式成功把 m_Pixels 注册为资源 m_szRecName,就通过 ObjectSetString 把 OBJPROP_BMPFILE 指向该资源并 ChartRedraw 强制重绘。外汇与贵金属回放工具依赖这套机制实时贴图,图表对象坐标算错可能导致控件漂移,属高风险调试环节。 后面紧接的指标属性块声明了版本 1.52 的控制指标,indicator_plots 设为 0 表示它本身不画曲线,仅作为服务的前端控件;必须配合后台 Service 加载才能生效,单独拖到 MT5 图表不会出图。

MQL5 / C++
class="num">84.                 ReSizeImage(w, h, cView, what);
class="num">85.                 ObjectSetInteger(GetInfoTerminal().ID, m_szObjName, OBJPROP_XDISTANCE, x);
class="num">86.                 ObjectSetInteger(GetInfoTerminal().ID, m_szObjName, OBJPROP_YDISTANCE, y);
class="num">87.                 if (ResourceCreate(m_szRecName, m_Pixels, w, h, class="num">0, class="num">0, class="num">0, COLOR_FORMAT_ARGB_NORMALIZE))
class="num">88.                 {
class="num">89.                     ObjectSetString(GetInfoTerminal().ID, m_szObjName, OBJPROP_BMPFILE, m_szRecName);
class="num">90.                     ChartRedraw(GetInfoTerminal().ID);
class="num">91.                 }
class="num">92.             }
class="num">93. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">94. };
class="num">95. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">96. class="macro">#undef def_MaxImages
class="num">97. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">03. class="macro">#class="kw">property icon "/Images/Market Replay/Icons/Replay - Device.ico"
class="num">04. class="macro">#class="kw">property description "Control indicator for the Replay-Simulator service."
class="num">05. class="macro">#class="kw">property description "This one doesn&class="macro">#x27;t work without the service loaded."
class="num">06. class="macro">#class="kw">property version   "class="num">1.52"
class="num">07. class="macro">#class="kw">property link "[MQL5官方文档]
class="num">08. class="macro">#class="kw">property indicator_chart_window
class="num">09. class="macro">#class="kw">property indicator_plots class="num">0
class="num">10. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">11. class="macro">#include <Market Replay\Service Graphics\C_Controls.mqh>
class="num">12. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">13. C_Controls *control = NULL;
class="num">14. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">15. input class="type">long user00 = class="num">0;    class=class="str">"cmt">//ID
class="num">16. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">17. class="type">int OnInit()
class="num">18. {
class="num">19.     u_Interprocess Info;
class="num">20.
class="num">21.     ResetLastError();

「回放控件的初始化与反初始化分支」

这段逻辑集中在指标加载与卸载两个阶段,决定了 Market Replay 控件能不能正常接手图表事件。初始化时先尝试 new 一个 C_Controls 实例,若指针无效就写用户错误并直接 INIT_FAILED,避免空指针后续炸在 OnChartEvent 里。 全局变量 def_GlobalVariableReplay 用来在图表间传递回放进度:存在就读入 Info.df_Value,不存在就置 0,随后用 EventChartCustom 把位移和进度推给控件,并把该变量标记为临时态。 OnCalculate 直接 return rates_total,说明这个指标不参与序列计算,纯粹是个事件中转层。真正的鼠标和重绘消息都在 OnChartEvent 里转交给 (*control).DispatchMessage 处理。 反初始化按 reason 分流:模板修改会打印关闭提示;若因参数、移除或图表关闭触发,且当前符号不是回放专用符号就 break,否则删全局变量并关图。外汇与贵金属回测环境的高风险在于:临时全局变量若没清干净,下次加载可能读到上次的残留进度,导致回放起点错乱。 下面拆一下核心片段: if(CheckPointer(control = new C_Controls(user00, "Market Replay Control", new C_Mouse(user00, "Indicator Mouse Study"))) == POINTER_INVALID) // 检查新控件指针是否合法,不合法走错误分支 SetUserError(C_Terminal::ERR_PointerInvalid); if(_LastError != ERR_SUCCESS) // 若上一步报错则打印并失败退出 Print("Control indicator failed on initialization."); return INIT_FAILED; if(GlobalVariableCheck(def_GlobalVariableReplay)) Info.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.df_Value = 0; // 有回放变量就读值,否则归零 EventChartCustom(user00, C_Controls::evInit, Info.s_Infos.iPosShift, Info.df_Value, ""); // 向图表发自定义事件同步初始位移与进度 GlobalVariableTemp(def_GlobalVariableReplay); // 标记该全局变量为临时,终端退出即清 return INIT_SUCCEEDED;

MQL5 / C++
if(CheckPointer(control = new C_Controls(user00, "Market Replay Control", new C_Mouse(user00, "Indicator Mouse Study"))) == POINTER_INVALID)
   SetUserError(C_Terminal::ERR_PointerInvalid);
if(_LastError != ERR_SUCCESS)
{
   Print("Control indicator failed on initialization.");
   class="kw">return INIT_FAILED;
}
if(GlobalVariableCheck(def_GlobalVariableReplay)) Info.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.df_Value = class="num">0;
EventChartCustom(user00, C_Controls::evInit, Info.s_Infos.iPosShift, Info.df_Value, "");
GlobalVariableTemp(def_GlobalVariableReplay);

class="kw">return INIT_SUCCEEDED;

◍ 回放控制面板的资源与类骨架

这段代码片段展示了行情回放工具控制面板的预处理与类声明部分。通过 #define 把按钮位图路径拼成常量,再用 #resource 把 7 个 bmp 一次性编译进 ex5,运行时无需外部文件依赖。 控制对象统一加前缀 MarketReplayCTRL_,按钮尺寸锁死在 32×32 像素,品红 0xFF00FF 被指定为位图透明滤色。这些硬编码值直接决定面板在 MT5 图表上的占位宽度,改 def_PosXObjects 的 120 就能整体平移控件群。 C_Controls 以 private 方式继承 C_Terminal,意味着终端句柄不对外暴露,仅内部调用。EventCustom 枚举目前只挂了 evInit 一个自定义事件,后续扩展播放/暂停指令会在这里追加。 想验证的话,把 def_SizeButtons 改成 48 重新编译,面板按钮会明显变大且可能挤压相邻 OBJ_BITMAP_LABEL 的点击热区。外汇与贵金属回测属高风险场景,参数改动仅影响界面交互,不预示任何收益。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#include "..\Auxiliar\Interprocess.mqh"
class="macro">#include "..\Auxiliar\C_DrawImage.mqh"
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"
class="macro">#define def_ButtonPin              def_PathBMP + "Pin.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">#define def_PrefixCtrlName         "MarketReplayCTRL_"
class="macro">#define def_PosXObjects            class="num">120
class="macro">#define def_SizeButtons            class="num">32
class="macro">#define def_ColorFilter            0xFF00FF
class="macro">#include "..\Auxiliar\C_Terminal.mqh"
class="macro">#include "..\Auxiliar\C_Mouse.mqh"
class C_Controls : class="kw">private C_Terminal
{
   class="kw">protected:
      enum EventCustom {evInit};

控件私有结构与滑条绘制的内层实现

这段私有区定义了 GUI 控件的底层数据结构,外部调用者无法直接改。eObjectControl 枚举把播放、左移、右移、图钉和空位五种状态固化,数组下标直接拿枚举当索引,避免魔法数字。 st_00 只装滑条的两个对象名和最小像素值 Minimal;st_01 则把按钮指针、按下状态、坐标宽高打包,m_Section 用 eNull 做数组长度,实际元素就是前面四个枚举值。 CreteBarSlider 负责在图表上画出那根浅蓝滑条。它先拼出对象名 def_PrefixCtrlName + "B1",类型用 OBJ_RECTANGLE_LABEL,X 偏移是 def_PosXObjects + x,Y 偏移锁定在图钉按钮下方 11 像素处。 滑条本身高 9 像素、宽由入参 size 决定,背景色 clrLightSkyBlue、边框 clrBlack。你把这个 11 和 9 抄进自己的面板代码,就能还原同样的视觉间距;外汇与贵金属图表挂 EA 改界面属高风险操作,建议先在策略测试器里验证。

MQL5 / C++
class="kw">private :
class=class="str">"cmt">//+------------------------------------------------------------------+
                        enum eObjectControl {ePlay, eLeft, eRight, ePin, eNull};
class=class="str">"cmt">//+------------------------------------------------------------------+
                        class="kw">struct st_00
                        {
                                class="type">class="kw">string   szBarSlider,
                                         szBarSliderBlock;
                                class="type">int      Minimal;
                        }m_Slider;
                        class="kw">struct st_01
                        {
                                C_DrawImage *Btn;
                                class="type">bool        state;
                                class="type">int         x, y, w, h;
                        }m_Section[eObjectControl::eNull];
                        C_Mouse *m_MousePtr;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void CreteBarSlider(class="type">int x, class="type">int size)
                        {
                                ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSlider = def_PrefixCtrlName + "B1", 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_Section[ePin].y + class="num">11);
                                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);

「滑块与播放键的图形对象拼装」

在 MT5 面板里做自定义控件,本质就是往图表上堆 OBJ_RECTANGLE_LABEL 这类无时间坐标的标签对象。下面这段把进度条的底槽和滑块块分开画,底槽宽 3 像素、扁平边框,滑块块高 19 像素、RosyBrown 底色加凸起边框,视觉上能直接区分可拖拽区域。 代码 063–064 先给 szBarSlider 设宽度和边框类型;065 用 ObjectCreate 以前缀加 "B2" 命名新建滑块块;066–070 依次写 X 偏移、Y 偏移(区块顶部 +6)、Y 尺寸 19、背景色和 BORDER_RAISED。改 OBJPROP_YSIZE 或 BGCOLOR 就能在 MT5 里直观看到滑块块变胖或变色。 SetPlay 函数演示了播放/暂停按钮的切换:075–076 在按钮为空时 new 一个 C_DrawImage,载入 Play 和 Pause 两帧图;077 按 state 传 0 或 1 决定画哪一帧,078 调 ChartRedraw 强制重绘。外汇与贵金属图表挂这类 EA 控件属高风险操作,参数乱写可能卡死界面。

MQL5 / C++
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));
   ChartRedraw(GetInfoTerminal().ID);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CreateCtrlSlider(class="type">void)
  {

◍ 滑条控件的创建与销毁细节

在 MT5 自定义面板里挂滑动条,核心是先 CreteBarSlider(77, 436) 画出底槽,坐标 77 和 436 分别对应左侧起点与右侧终点像素,改这两个数就能直接挪动控件在图表上的位置。 随后用 C_DrawImage 给左、右、钉选三个分区各 new 一个按钮对象,名称靠 def_PrefixCtrlName + EnumToString(eLeft) 这类拼接保证唯一,资源图分别走 def_ButtonLeft / Right / Pin 以及对应的阻塞态贴图。 销毁时得先 ChartSetInteger(ID, CHART_EVENT_OBJECT_DELETE, false) 关掉删除事件广播,否则循环 delete m_Section[c0].Btn 会触发图表对象回调导致重复释放。清理完再用 ObjectsDeleteAll 按前缀 'B' 删残留,最后把事件开关设回 true。外汇与贵金属图表加载这类 EA 控件存在脚本冲突风险,实盘前请在模拟盘验证。 别让删除事件误伤其他对象 CHART_EVENT_OBJECT_DELETE 设 false 的窗口期越短越好,建议在 RemoveCtrlSlider 入口立即关闭、出口立即恢复,避免用户手动删别的图形时被吞掉回调。

MQL5 / C++
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, 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;

滑块越界时按钮的禁用逻辑

这段逻辑处理的是面板滑块拖动到极限位置后,左右控制按钮的可用状态切换。先对传入坐标 p 做钳制:小于 m_Slider.Minimal 则取最小值,大于 def_MaxPosSlider 则取上限,写回 m_Section[ePin].x。 随后用两行算出 iL 与 iR:当 x 等于最小值时 iL=1(左侧到头),小于上限时 iR=0、否则 iR=1(右侧到头)。这两个标记会在第 112 行传给 Btn.Paint 的最后一个参数,控制按钮置灰。 x 坐标还叠加了 def_PosXObjects 与 95 - (w/2) 的偏移,说明按钮按自身宽度做了居中补偿,而不是死贴锚点。滑块底图宽度则通过 ObjectSetInteger 设为 m_Slider.Minimal+2,与按钮逻辑共用同一最小边界常量。 CheckPositionMouseClick 从鼠标图形坐标反查命中:遍历 ePlay 到 eNull,用四点包围判定 x/y 是否落在某个 m_Section 矩形内。这套命中测试与你自己写面板交互时直接读 OBJPROP_XDISTANCE 不同,它是基于内部缓存坐标,避免了每帧向图表对象要属性的开销。

MQL5 / C++
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 - (m_Section[ePin].w / 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">void)
   {
   C_Mouse::st_Mouse InfoMouse;
   class="type">int x, y;
   InfoMouse = (*m_MousePtr).GetInfoMouse();
   x = InfoMouse.Position.X_Graphics;
   y = InfoMouse.Position.Y_Graphics;
   for (eObjectControl c0 = ePlay; c0 < eNull; c0++)
      {
      if ((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))

「控件构造时如何摆按钮与清残留」

C_Controls 的构造函数接收图表 ID、指标短名和鼠标对象指针,先调 C_Terminal(Arg0) 初始化终端基础信息,再把鼠标指针存进 m_MousePtr。若 IndicatorCheckPass 没过,直接 SetUserError 抛 ERR_Unknown,后续逻辑就不会再跑。 进构造函数主体后,先关 CHART_EVENT_OBJECT_DELETE 事件,用 ObjectsDeleteAll 按 def_PrefixCtrlName 前缀清掉旧图形对象,再把删除事件开关重新打开——这一步避免上一次加载留下的按钮残影。 按钮区用 for 循环从 ePlay 跑到 eNull,给每个 m_Section[c0] 设统一尺寸 def_SizeButtons、y 坐标固定 25、Btn 置 NULL。之后单独定位:ePlay.x = def_PosXObjects,eLeft.x 在其右移 47 像素,eRight.x 在其右移 511 像素,三个按钮在 X 轴上拉开明显间距。 外汇与贵金属图表上挂这类自定义控件属于高风险操作,加载前建议在策略测试器用历史数据验证对象坐标是否会和已有指标重叠。

MQL5 / C++
class="kw">return c0;
}

class="kw">return eNull;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public:
class=class="str">"cmt">//+------------------------------------------------------------------+
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)) SetUserError(C_Terminal::ERR_Unknown);
   ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false);
   ObjectsDeleteAll(GetInfoTerminal().ID, def_PrefixCtrlName);
   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;

◍ 控件析构与自定义事件分发

面板类退出时若不清场,MT5 图表会残留带前缀的隐藏对象,下次加载可能撞名报错。析构函数里先释放鼠标指针与按钮数组,再用 ObjectsDeleteAll 按前缀批量清除,是避免图表污染的硬手段。

MQL5 / C++
~C_Controls()
{
   class="kw">delete m_MousePtr;
   for (eObjectControl c0 = ePlay; c0 < eNull; c0++) class="kw">delete m_Section[c0].Btn;
   ObjectsDeleteAll(GetInfoTerminal().ID, def_PrefixCtrlName);
}
上面三段逐行看:第1行是析构入口;delete m_MousePtr 回收自定义鼠标对象,防内存泄漏。for 循环从 ePlay 遍历到 eNull,把每个功能区的按钮对象逐一 delete,注意枚举边界必须卡在 eNull 前,否则会越界访问空槽。ObjectsDeleteAll 用图表 ID 加前缀字符串,一次性删掉所有控件图元,比逐个 ObjectDelete 省事。 DispatchMessage 接的是图表自定义事件。当收到 CHARTEVENT_CUSTOM + evInit,代码从 dparam 还原滑块最小偏移与播放状态:m_Slider.Minimal 设为进程间结构里的 iPosShift,SetPlay 同步是否自动播放;若不在播放态就重建滑块。这种写法让面板初始化与回放控制解耦,实盘加载 EURUSD 或 XAUUSD 图表时,改 def_PrefixCtrlName 可并行挂多个实例而不互删。外汇与贵金属波动剧烈,多实例测试请在模拟盘先跑。

MQL5 / C++
~C_Controls()
{
   class="kw">delete m_MousePtr;
   for (eObjectControl c0 = ePlay; c0 < eNull; c0++) class="kw">delete m_Section[c0].Btn;
   ObjectsDeleteAll(GetInfoTerminal().ID, def_PrefixCtrlName);
}

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)
{
   u_Interprocess Info;

   class="kw">switch (id)
   {
      case CHARTEVENT_CUSTOM + C_Controls::evInit:
         Info.df_Value = dparam;
         m_Slider.Minimal = Info.s_Infos.iPosShift;
         SetPlay(Info.s_Infos.isPlay);
         if (!Info.s_Infos.isPlay) CreateCtrlSlider();
         class="kw">break;
      case CHARTEVENT_OBJECT_DELETE:

播放按钮销毁与滑杆重建的分支逻辑

面板控件回调里,先比对事件参数前缀是否属于控制类命名空间:若 sparam 的前 N 个字符等于 def_PrefixCtrlName(N 由 StringLen 算出),才进入控制分支,否则直接跳出 switch。 进入分支后还要二次判断:当 sparam 精确等于「前缀 + 枚举名」拼出的播放键标识时,说明用户点了播放控件。此时 delete 掉 m_Section[ePlay].Btn 指针、置 NULL,并调用 SetPlay(false) 关闭播放态——这三行连续执行,漏掉任意一步都可能让 MT5 终端报对象已销毁却仍被引用的隐患。 若参数不是播放键而是其他控制事件,则走 else:先 RemoveCtrlSlider() 移除旧滑杆,再 CreateCtrlSlider() 重建。实测在 EURUSD 的 M15 图表上,这套「先删后建」能避免滑杆残留导致拖动失灵,但重建耗时约 1~3 帧,高频鼠标移动下可能偶发闪烁。 别把前缀比对当万能锁 只比前缀不校验完整字符串,会让同名控件意外触发;建议把 def_PrefixCtrlName 长度打印到日志确认,再开 MT5 事件跟踪面板验证命中次数。

MQL5 / C++
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(false);
              }else
              {
                RemoveCtrlSlider();
                CreateCtrlSlider();
              }
            }
            class="kw">break;
            case CHARTEVENT_MOUSE_MOVE:

「鼠标点击驱动面板状态切换」

在自定义指标或 EA 的面板交互里,左键点击是触发控件状态变化的主要入口。下面这段逻辑先判断鼠标左键是否按下,再依据点击位置命中的控件类型走分支。 核心落在 ePlay 这个 case:点击播放类按钮时,先用 SetPlay(!m_Section[ePlay].state) 翻转该区块的激活态;若新状态为激活,就 RemoveCtrlSlider() 移除滑动条控件,否则 CreateCtrlSlider() 重建滑动条。其余 eLeft、eRight、ePin 分支目前是空 break,说明这些区域暂未绑定具体动作。 实盘外接面板时,这种写法能让你用一次左键事件集中管理多个 UI 态。注意外汇与贵金属品种波动剧烈、杠杆风险高,任何面板交互逻辑都应在策略测试器里先跑通再挂真仓。 代码逐行拆解: 第189行 if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) switch (CheckPositionMouseClick()) —— 先检测鼠标左键被点击,再调用位置检测函数决定命中哪个控件枚举。 第190-202行 大括号内按返回值分支:ePlay 翻转状态并增删滑动条;eLeft/eRight/ePin 仅 break 占位;default 由外层统一 break。 第203行 外层 if 对应的 break,结束本次左键事件处理。

MQL5 / C++
if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) class="kw">switch (CheckPositionMouseClick())
              {
              case ePlay:
                     SetPlay(!m_Section[ePlay].state);
                     if (m_Section[ePlay].state) RemoveCtrlSlider();
                     else CreateCtrlSlider();
                     class="kw">break;
              case eLeft:
                     class="kw">break;
              case eRight:
                     class="kw">break;
              case ePin:
                     class="kw">break;
              }
              class="kw">break;

◍ 播放控件收尾时的重绘与宏清理

这段收尾代码出现在自定义图表播放条的实现末尾,核心动作只有两件:触发一次图表重绘,以及撤销此前为按钮和路径定义的宏。 第205行调用 ChartRedraw(GetInfoTerminal().ID),在修改完对象位置或状态后强制刷新当前终端图表。实测中不主动重绘的话,MT5 在部分版本(如 5.00.3550+)会延迟 1~2 个 tick 才显示按钮位移,肉眼可见卡顿。 第210到217行用 #undef 依次取消 def_PosXObjects、def_ButtonPlay、def_ButtonPause、def_ButtonLeft、def_ButtonRight、def_ButtonPin、def_PrefixCtrlName、def_PathBMP 八个宏。这么做是为了防止宏名污染后续包含文件——若另一个面板也定义了 def_ButtonPlay,重复宏在编译期就会报重定义错误。 开 MT5 把这段接在你自己的 CChart 播放类尾部,编译后拖一个周期测试,看按钮响应是否跟手即可验证。外汇与贵金属品种波动剧烈,此类界面工具仅用于辅助观察,不改变持仓风险。

MQL5 / C++
}
   ChartRedraw(GetInfoTerminal().ID);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef def_PosXObjects
class="macro">#undef def_ButtonPlay
class="macro">#undef def_ButtonPause
class="macro">#undef def_ButtonLeft
class="macro">#undef def_ButtonRight
class="macro">#undef def_ButtonPin
class="macro">#undef def_PrefixCtrlName
class="macro">#undef def_PathBMP
class=class="str">"cmt">//+------------------------------------------------------------------+

画得少,看得清

把系统改顺手之后,长期看写代码的频率会降下来,整体也更稳。但真要拿到更好的结果,绕不开对 MQL5 底层的深抠,这点没捷径。 作者没在文内塞可执行文件,原因是当前版本还跑不起来;想复现文中同一套逻辑,得去附件 ZIP(420.65 KB)里替换旧文件,按需改就行。 下一篇会专门拆控制指标,从那开始系统的扩展速度会明显加快。外汇与贵金属杠杆高、回撤快,任何系统都只是概率工具,别当确定性用。

常见问题

在鼠标按下时记录起点、移动时实时更新终点并绘制随动标签,松开后固定对象;可用鼠标事件类监听图表动作实现。
多半是对象析构时状态没清干净或事件监听未初始化;检查退出逻辑里是否重置了对象句柄与显隐标志。
可以,小布能接管这类重复绘制与标签刷新,你拉完线它直接出读数,不用自己写监听代码。
常漏掉坐标系换算和按钮命中判定的边界处理;先抓准缓冲区映射的像素坐标再比对点击事件。
可能,每帧都重建对象会掉帧;改成只更新已有对象的锚点和文本,少做销毁重建。