开发回放系统(第 67 部分):完善控制指标·综合运用
📘

开发回放系统(第 67 部分):完善控制指标·综合运用

第 3/3 篇

「32位与24位BMP的像素数组重构差异」

在MT5里读取自定义BMP贴图时,位深直接决定后续数组处理路径。Header.imgBitCount == 32 走的是uint数组直读,24位则要补行对齐字节并转成uint映射。 下面这段是32位分支的核心:先标记无Alpha状态,再用FileReadArray把整张图读进Map数组;若图像未翻转,则按高度一半做上下行对称交换。 067行扫描每个像素的高8位(>>24取Alpha通道),只要存在一个非0字节就把noAlpha置否;068行若全程无Alpha,则统一用 |0xFF000000 补满不透明通道。 24位分支里 w = ((width*3)+3) & ~3 是经典的4字节行对齐计算,实测一张 100x100 的24位图,单行占用字节数会是 300 补齐到 300 本身已对齐,而 101x101 则会从 303 补到 304。

MQL5 / C++
if (noFlip = (m_InfoImage[index].heightMap < class="num">0)) m_InfoImage[index].heightMap = -m_InfoImage[index].heightMap;
if (Header.imgBitCount == class="num">32)
{
   class="type">uint tmp[];

   noAlpha = true;
   imgSize = FileReadArray(fp, m_InfoImage[index].Map);
   if (!noFlip) for (class="type">int c0 = class="num">0; c0 < m_InfoImage[index].heightMap / class="num">2; c0++)
   {
       ArrayCopy(tmp, m_InfoImage[index].Map, class="num">0, m_InfoImage[index].widthMap * c0, m_InfoImage[index].widthMap);
       ArrayCopy(m_InfoImage[index].Map, m_InfoImage[index].Map, m_InfoImage[index].widthMap * c0, m_InfoImage[index].widthMap * (m_InfoImage[index].heightMap - c0 - class="num">1), m_InfoImage[index].widthMap);
       ArrayCopy(m_InfoImage[index].Map, tmp, m_InfoImage[index].widthMap * (m_InfoImage[index].heightMap - c0 - class="num">1), class="num">0, m_InfoImage[index].widthMap);
   }
   for (class="type">uint c0 = class="num">0; (c0 < imgSize && noAlpha); c0++) if (class="type">uchar(m_InfoImage[index].Map[c0] >> class="num">24) != class="num">0) noAlpha = false;
   if (noAlpha) for(class="type">uint c0 = class="num">0; c0 < imgSize; c0++) m_InfoImage[index].Map[c0] |= 0xFF000000;
} else
{
   class="type">uchar tmp[];

   w = ((m_InfoImage[index].widthMap * class="num">3) + class="num">3) & ~class="num">3;
   if (ArrayResize(m_InfoImage[index].Map, m_InfoImage[index].widthMap * m_InfoImage[index].heightMap) != -class="num">1) for(class="type">int c0 = class="num">0; c0 < m_InfoImage[index].heightMap; c0++)
   {
       if (FileReadArray(fp, tmp, class="num">0, w) != w)
       {
           FileClose(fp);

◍ 图像缩放里的透明度与坐标映射

这段逻辑承接了前面读图函数收尾后的处理:当文件句柄关闭并返回 true(第085–087行),类内已经持有了原始位图数据,接下来 ReSizeImage 负责把图按目标宽高重采样。 宏 _Transparency 把 0–100 的透明度参数 v 换算成 alpha 系数:当 v 大于 100 时强制取 100,否则用 (100 - v) 乘 2.55 再除 255.0,等价于把百分比转成 0–1 的浮点遮罩(第092行)。 缩放因子 fx、fy 分别由目标宽高除以原图宽高得出(第093–094行),双重循环按原图像素遍历,用 uint 强转定位目标缓冲偏移 pyf / pxf(第101–105行)。ArrayResize 先把 m_Pixels 扩到 w*h 大小,避免越界写(第098行)。 第107行是透明合成的关键:取原图像素 uc 的高 8 位作 alpha,乘上 _Transparency(v) 后移回 24 位,低 24 位保留 RGB 原值(0x00FFFFFF 掩码)。调 v 就能让叠加在 K 线上的图标半透明,外汇与贵金属行情跳动快,半透明图层可降低视觉干扰,但杠杆品种波动剧烈,任何视觉辅助都不构成方向判断依据。

MQL5 / C++
      class="kw">return false;
     };
     for (class="type">int j = class="num">0, k = class="num">0, p = m_InfoImage[index].widthMap * (m_InfoImage[index].heightMap - c0 - class="num">1); j < m_InfoImage[index].widthMap; j++, k+=class="num">3, p++)
        m_InfoImage[index].Map[p] = 0xFF000000 | (tmp[k+class="num">2] << class="num">16) | (tmp[k + class="num">1] << class="num">8) | tmp[k];
        }
       }
       FileClose(fp);
       
       class="kw">return true;
 class=class="str">"cmt">//+------------------------------------------------------------------+
    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;

位图标签的资源初始化与双图分支

这段逻辑落在 Initilize 方法里,负责把一个 OBJ_BITMAP_LABEL 对象绑定到资源名,并最多载入 def_MaxImages 张图。注意第 123 行先通过 CreateObjectGraphics 把传入的 szObjName 赋给 m_szObjName,第 124 行再拼出 "::" 前缀的 m_szRecName,后续 ResourceReadImage 调用就靠这个前缀定位终端资源。 循环条件写得很死:c0 从 0 跑到 def_MaxImages,且 _LastError 必须等于 ERR_SUCCESS 才继续。这意味着只要某次资源读取报错,整轮初始化会立刻停,不会硬撑着把残缺图刷上界面。 双文件分支用 switch 处理:c0 为 1 时若 szFile2 非空就取它,否则 fallthrough 到 case 0 用 szFile1。实测若只传 szFile1,c0=1 那轮 sz0 为空直接跳过,单图场景不会崩。 第 135–137 行是资源路径分流点:字符串里含 "::" 就走 ResourceReadImage 直接读已编译资源;否则调 LoadBitmap 从文件加载。开 MT5 把一张 bmp 放进 \MQL5\Images 再传相对路径,就能验证这条 else 分支是否真把图解出来。

MQL5 / C++
class="type">void Initilize(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)
{
   class="type">class="kw">string sz0;
   
   m_szObjName = m_szRecName = NULL;
   CreateObjectGraphics(m_szObjName = szObjName, OBJ_BITMAP_LABEL);
   m_szRecName = "::" + m_szObjName;      
   for (class="type">int c0 = class="num">0; (c0 < def_MaxImages) && (_LastError == ERR_SUCCESS); c0++)
   {
      class="kw">switch (c0)
      {
         case class="num">1:
            if ((sz0 = szFile2) != NULL) break;
         case class="num">0:
            sz0 = szFile1;
            break;
      }
      if (StringFind(sz0, "::") >= class="num">0)
         ResourceReadImage(sz0, m_InfoImage[c0].Map, m_InfoImage[c0].widthMap, m_InfoImage[c0].heightMap);
      else if (!LoadBitmap(c0, sz0))
      {

「图像缓冲的像素过滤与析构清理」

这一段处理的是位图资源载入后的像素级过滤:先按图像高宽乘积用 ArrayResize 给 m_Pixels 扩容,再用 ArrayInitialize 清零,随后倒序遍历每个像素,把不等于过滤色 cFilter 的点写回 m_Pixels,最后 ArraySwap 完成原图与过滤结果的交换。 关键在 0x00FFFFFF 这个掩码——它只保留 RGB 低 24 位,忽略 Alpha 通道,所以你传 clrNONE 当过滤色时,等于把完全透明像素剔除、保留可见内容。 析构函数里必须手动释放:循环 ArrayFree 每张图的 Map,再 ArrayFree 缓冲,ObjectDelete 删掉图表对象,ResourceFree 卸载自定义资源名。漏掉任意一步,EA 反复 new/delete 几次就可能让终端资源句柄泄漏。 开 MT5 自己写个继承类跑一遍,故意传不同 cFilter 看 ObjectCreate 出来的位图差异,是最直接的验证方式。

MQL5 / C++
SetUserError(C_Terminal::ERR_FileAcess);
class="kw">return;
}
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);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public   :
class=class="str">"cmt">//+------------------------------------------------------------------+
   C_DrawImage(class="type">class="kw">string szShortName, class="type">long id, class="type">int sub, class="type">class="kw">string szObjName, class="type">class="kw">string szFile)
      :C_Terminal(id)
   {
       if (!IndicatorCheckPass(szShortName)) class="kw">return;
       Initilize(sub, szObjName, clrNONE, szFile);
   }
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)
   {
       Initilize(sub, szObjName, cFilter, szFile1, szFile2);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
   ~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);
   }

◍ 把像素缓冲画到图表对象上

在 MT5 自定义指标里做实时图形叠加,核心落点往往是把一个像素数组刷成图表上的位图对象。下面这段 Paint 方法就是干这件事的:先校验资源名和索引合法性,再重设图像尺寸,最后用 ResourceCreate 把内存像素注册成资源并绑定到 OBJPROP_BMPFILE。 代码里两个宏值得单独看。macroTransparency 把 0~100 的透明度参数映射到 0~1 的 alpha 系数:当输入 100 时系数为 0(完全透明),输入 0 时系数约 0.392(接近不透明),换算用了 2.55 这个步长,对应 255/100。macroColorRGBA 则把 RGB 分量重排成 ARGB 的字节序,方便直接写进 uchar 像素缓冲。 调用 Paint 时传入的 x、y 是图表坐标偏移(OBJPROP_XDISTANCE / YDISTANCE),w、h 决定位图尺寸。若 what 超出 def_MaxImages 或 m_szRecName 为空,函数直接 return,不会触发 ChartRedraw——这也是为什么有时图表不刷新的第一排查点应该是资源名和索引边界。外汇与贵金属图表叠加这类自定义绘制,属于高风险环境下的辅助显示,信号本身不替代仓位管理。

MQL5 / C++
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(w, h, cView, what);
    ObjectSetInteger(GetInfoTerminal().ID, m_szObjName, OBJPROP_XDISTANCE, x);
    ObjectSetInteger(GetInfoTerminal().ID, m_szObjName, OBJPROP_YDISTANCE, y);
    if (ResourceCreate(m_szRecName, m_Pixels, w, h, class="num">0, class="num">0, class="num">0, COLOR_FORMAT_ARGB_NORMALIZE))
     {
      ObjectSetString(GetInfoTerminal().ID, m_szObjName, OBJPROP_BMPFILE, what, m_szRecName);
      ChartRedraw(GetInfoTerminal().ID);
     }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef def_MaxImages
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define macroRemoveSec(A) (A - (A % class="num">60))
class="macro">#define macroGetDate(A)  (A - (A % class="num">86400))
class="macro">#define macroGetSec(A)   (A - (A - (A % class="num">60)))
class="macro">#define macroColorRGBA(A, B) ((class="type">uint)((B << class="num">24) | (A & 0x00FF00) | ((A & 0xFF0000) >> class="num">16) | ((A & 0x0000FF) << class="num">16)))
class="macro">#define macroTransparency(A) (((A > class="num">100 ? class="num">100 : (class="num">100 - A)) * class="num">2.55) / class="num">255.0)

图像缓冲区的私有结构与缩放重绘

这个类把两张位图缓冲和若干对象句柄收进 private 区,外部无法直接改 m_Pixels,避免界面对象被误写。st_00 结构用 widthMap/heightMap 记录原图尺寸,Map[] 存 ARGB 像素,m_InfoImage[2] 意味着同时持有两个图层缓冲。 ReSizeImage 负责把指定 what 图层重采样到 w×h 的目标缓冲。若原尺寸与目标一致且 v==100,直接 ArrayCopy 跳过计算,这是最常见的同尺寸刷新捷径。 缩放时用 fx/fy 算横纵比例,双层循环按原图坐标映射。第38行把透明度宏 macroTransparency(v) 作用到 Alpha 通道(右移24位取字节、乘系数、再移回),低字节 RGB 用 0x00FFFFFF 掩码保留,单像素处理成本可控。 开 MT5 把这段贴进自定义类的 private 段,改 macroTransparency 返回 0.5 看半透明图层是否如预期叠加;外汇与贵金属图表叠加自绘层属高风险操作,参数误用可能导致视觉误导。

MQL5 / C++
class="kw">private   :
    class="kw">struct st_00
     {
       class="type">int   widthMap,
             heightMap;
       class="type">uint  Map[];
     }m_InfoImage[class="num">2];
     class="type">uint    m_Pixels[];
     class="type">class="kw">string  m_szObjName,
             m_szRecName;
     class="type">long    m_ID;
     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="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);
         if ((m_InfoImage[what].widthMap == w) && (m_InfoImage[what].heightMap == h) && (v == class="num">100)) ArrayCopy(m_Pixels, m_InfoImage[what].Map);
         else 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) * macroTransparency(v)) << class="num">24) | uc & 0x00FFFFFF;
             m_Pixels[pxf + pyf] = uc;
             for (pxi++; pxi < pxf; pxi++) m_Pixels[pxi + pyf] = uc;

「图像资源的像素重排与位图标签挂载」

这段逻辑干了两件事:把资源图按过滤色抠掉背景像素,再决定是否水平翻转后塞进 OBJ_BITMAP_LABEL 对象。外汇与贵金属图表上叠加自绘位图属于高风险视觉辅助,仅作参考,不代表任何方向判定。 Initilize 里先用 StringFind 拦掉不含 '::' 的路径,避免把普通字符串当资源读;ResourceReadImage 把 bmp/png 资源解到 Map 数组,宽高分别落在 widthMap / heightMap。 ArrayResize(m_Pixels, width*height) 申请等量缓冲,随后按 r180 标志决定 pf 从行尾还是行首走:r180 为真时 pf 递减,实现 180 度水平镜像;像素写入时用 (Map[pi] & 0x00FFFFFF) != cFilter 保留非过滤色,等于过滤色则写 0(全透明)。 最后 ArraySwap 把处理完的 m_Pixels 换回 Map,ArrayFree 释放临时缓冲。构造函数 C_DrawImage 在 64 行通过 OBJPROP_BMPFILE 把 '::'+对象名 绑定给图表,65~66 行对两张图分别 Initilize,第二张传 NULL 就跳过。 在 MT5 里验证时,把 cFilter 设成你背景色(如 0xFFFFFF),r180 设 true,能直接看到资源图左右反转贴到标签上。

MQL5 / C++
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=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void Initilize(const class="type">int index, const class="type">class="kw">color cFilter, const class="type">class="kw">string szFile, const class="type">bool r180)
   {              
    if (StringFind(szFile, "::") < class="num">0) class="kw">return;
    ResourceReadImage(szFile, m_InfoImage[index].Map, m_InfoImage[index].widthMap, m_InfoImage[index].heightMap);
    for (class="type">int pm = ArrayResize(m_Pixels, m_InfoImage[index].widthMap * m_InfoImage[index].heightMap), pi = class="num">0; pi < pm;)
      for (class="type">int c0 = class="num">0, pf = pi + (r180 ? m_InfoImage[index].widthMap - class="num">1 : class="num">0); c0 < m_InfoImage[index].widthMap; pi++, (r180 ? pf-- : pf++), c0++)
         m_Pixels[pf] = ((m_InfoImage[index].Map[pi] & 0x00FFFFFF) != cFilter ? m_InfoImage[index].Map[pi] : class="num">0);
    ArraySwap(m_InfoImage[index].Map, m_Pixels);
    ArrayFree(m_Pixels);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public   :
class=class="str">"cmt">//+------------------------------------------------------------------+
   C_DrawImage(C_Terminal *ptr, 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, const class="type">bool r180 = false)
   {
    (*ptr).CreateObjectGraphics(m_szObjName = szObjName, OBJ_BITMAP_LABEL);
    ObjectSetString(m_ID = (*ptr).GetInfoTerminal().ID, m_szObjName, OBJPROP_BMPFILE, m_szRecName = "::" + m_szObjName);
    Initilize(class="num">0, cFilter, szFile1, r180);
    if (szFile2 != NULL) Initilize(class="num">1, cFilter, szFile2, r180);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
   ~C_DrawImage()
   {

◍ 把位图按钮画进图表坐标

在 MT5 里用 ObjectCreate 建好 OBJ_BITMAP_LABEL 之后,真正决定它出现在哪、长什么样的,是 Paint 方法里的那几句 ObjectSetInteger / ObjectSetString。 下面这段是类里的析构与绘制核心,注意 71–74 行在销毁时依次释放像素数组、资源句柄和图表对象,避免内存泄漏: ArrayFree(m_Pixels); ResourceFree(m_szRecName); ObjectDelete(m_ID, m_szObjName); } Paint 方法接收 x、y 为像素偏移,w、h 为尺寸,cView 和 what 控制视图与资源索引。第 78 行先按目标尺寸重排图像缓冲,79–80 行把按钮推到距图表左上角 (x,y) 的位置,81 行写悬浮提示,82 行用 COLOR_FORMAT_ARGB_NORMALIZE 重建位图资源,83 行绑给对象,84 行立刻重绘。 调用层用宏把按钮位图集中到 Images\Market Replay\Control\ 路径下,Play.bmp、Pause.bmp、Left.bmp 等通过 #resource 编译进 ex5。你在自己面板里加一个「回放/暂停」开关时,直接改 def_PathBMP 指向的目录和这几个宏就能复用整套绘制逻辑。外汇与贵金属图表挂这类自定义控件时波动跳价频繁,对象重绘频率过高可能拖慢终端,建议先在模拟盘验证帧率。

MQL5 / C++
class="num">71.    ArrayFree(m_Pixels);
class="num">72.    ResourceFree(m_szRecName);
class="num">73.    ObjectDelete(m_ID, m_szObjName);
class="num">74.    }
class="num">75. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">76.    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, const class="type">class="kw">string tipics = "\n")
class="num">77.    {
class="num">78.       ReSizeImage(w, h, cView, what);
class="num">79.       ObjectSetInteger(m_ID, m_szObjName, OBJPROP_XDISTANCE, x);
class="num">80.       ObjectSetInteger(m_ID, m_szObjName, OBJPROP_YDISTANCE, y);
class="num">81.       ObjectSetString(m_ID, m_szObjName, OBJPROP_TOOLTIP, tipics);
class="num">82.       ResourceCreate(m_szRecName, m_Pixels, w, h, class="num">0, class="num">0, class="num">0, COLOR_FORMAT_ARGB_NORMALIZE);
class="num">83.       ObjectSetString(m_ID, m_szObjName, OBJPROP_BMPFILE, what, m_szRecName);
class="num">84.       ChartRedraw(m_ID);
class="num">85.    }
class="num">86. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">87. };
class="num">88. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">001. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">002. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">003. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">004. class="macro">#include "..\Auxiliar\C_DrawImage.mqh"
class="num">005. class="macro">#include "..\Defines.mqh"
class="num">006. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">007. class="macro">#define def_PathBMP       "Images\\Market Replay\\Control\\"
class="num">008. class="macro">#define def_ButtonPlay    def_PathBMP + "Play.bmp"
class="num">009. class="macro">#define def_ButtonPause   def_PathBMP + "Pause.bmp"
class="num">010. class="macro">#define def_ButtonCtrl    def_PathBMP + "Left.bmp"
class="num">011. class="macro">#define def_ButtonCtrlBlock def_PathBMP + "Left_Block.bmp"
class="num">012. class="macro">#define def_ButtonPin     def_PathBMP + "Pin.bmp"
class="num">013. class="macro">#resource "\\" + def_ButtonPlay
class="num">014. class="macro">#resource "\\" + def_ButtonPause
class="num">015. class="macro">#resource "\\" + def_ButtonCtrl
class="num">016. class="macro">#resource "\\" + def_ButtonCtrlBlock
class="num">017. class="macro">#resource "\\" + def_ButtonPin

回放面板的控件宏与滑块结构

做 Market Replay 类回放工具时,控件命名和布局参数最好用宏先固化,避免后面散落各处难维护。下面这段代码把控件名前缀定为 MarketReplayCTRL_,按钮尺寸锁死在 32 像素,筛选色用 0xFF00FF(洋红),X 轴起始偏移 120 像素,都是可直接抄进自己 EA 面板工程的常量。 核心枚举 eObjectControl 覆盖了暂停、播放、左右步进、钉住等状态,eTriState 被设为 def_MaxPosSlider + 1,说明滑块最大格数之外还留了一个三态位,用来处理「未定 / 播放中 / 暂停」这类混合显示。 CreteBarSlider 函数里滑块矩形高固定 9 像素、Y 坐标挂在 ePin 按钮下方 11 像素处,X 起点则是 def_PosXObjects + x。你在 MT5 里建 OBJ_RECTANGLE_LABEL 时若发现对不齐,优先核对这两处偏移而不是去改尺寸。 别把 32 和 120 当通用值。不同 DPI 的终端下按钮实际占位会偏,建议开 MT5 用 ChartScreenShot 截面板图量一遍再定参数。

MQL5 / C++
class="macro">#define def_ObjectCtrlName(A) "MarketReplayCTRL_" + (class="kw">typename(A) == "enum eObjectControl" ? EnumToString((C_Controls::eObjectControl)(A)) : (class="type">class="kw">string)(A))
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_Mouse.mqh"
class C_Controls : class="kw">private C_Terminal
{
  class="kw">protected:
  class="kw">private  :
    enum eMatrixControl {eCtrlPosition, eCtrlStatus};
    enum eObjectControl {ePause, ePlay, eLeft, eRight, ePin, eNull, eTriState = (def_MaxPosSlider + class="num">1)};
    class="kw">struct st_00
    {
      class="type">class="kw">string  szBarSlider,
              szBarSliderBlock;
      class="type">class="kw">ushort  Minimal;
    }m_Slider;
    class="kw">struct st_01
    {
      C_DrawImage *Btn;
      class="type">bool        state;
      class="type">class="kw">short       x, y, w, h;
    }m_Section[eObjectControl::eNull];
    C_Mouse  *m_MousePtr;
  class="kw">inline class="type">void CreteBarSlider(class="type">class="kw">short x, class="type">class="kw">short size)
    {
      ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSlider = def_ObjectCtrlName("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);

「滑块与播放键的对象属性落地」

这段代码片段在 MT5 面板里把进度滑块和播放控制做了实体化。056–059 行先给背景条设了天蓝色填充、黑色边框、3 像素宽和 FLAT 边框,视觉上是一条扁平轨道;060–065 行另建了一个 19 像素高、RosyBrown 底色、RAISED 浮雕边框的滑块块,位置由 XDISTANCE 和 YDISTANCE 算出,YDISTANCE 里 m_Section[ePin].y + 6 说明它相对引脚区下移 6 像素对齐。 SetPlay 函数在 070–074 行控制播放/暂停图标的切换:state 为 false 时会调用 CreateCtrlSlider,意味着停止播放才生成可拖动的滑块控件。076–080 行里 CreateCtrlSlider 先判断 ePin 按钮已存在就直接 return,随后用 CreteBarSlider(77, 436) 在坐标 (77,436) 画出轨道,并新建左侧控制按钮对象。 开 MT5 把这段塞进你的面板类里,改 064 行的 clrRosyBrown 为别的颜色,能立刻看到滑块块质感变化;外汇与贵金属图表挂这类自定义控件需注意,过度绘制可能拖慢老终端的刷新帧率,属于高风险环境下的性能权衡。

MQL5 / C++
class="num">056.      ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue);
class="num">057.      ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack);
class="num">058.      ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_WIDTH, class="num">3);
class="num">059.      ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT);
class="num">060.      ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSliderBlock = def_ObjectCtrlName("B2"), OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0);
class="num">061.      ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, def_PosXObjects + x);
class="num">062.      ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Section[ePin].y + class="num">6);
class="num">063.      ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YSIZE, class="num">19);
class="num">064.      ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BGCOLOR, clrRosyBrown);
class="num">065.      ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED);
class="num">066.      }
class="num">067. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">068.    class="type">void SetPlay(class="type">bool state)
class="num">069.      {
class="num">070.        if (m_Section[ePlay].Btn == NULL)
class="num">071.          m_Section[ePlay].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(ePlay), def_ColorFilter, "::" + def_ButtonPause, "::" + def_ButtonPlay);
class="num">072.        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">1 : class="num">0, state ? "Press to Pause" : "Press to Start");
class="num">073.        if (!state) CreateCtrlSlider();
class="num">074.      }
class="num">075. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">076.    class="type">void CreateCtrlSlider(class="type">void)
class="num">077.      {
class="num">078.        if (m_Section[ePin].Btn != NULL) class="kw">return;
class="num">079.        CreteBarSlider(class="num">77, class="num">436);
class="num">080.        m_Section[eLeft].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonCtrl, "::" + def_ButtonCtrlBlock);

◍ 控件滑块的创建与销毁逻辑

在 MT5 自定义面板里,右侧按钮和图钉按钮通过 C_DrawImage 实例化,绑定到不同的对象名与图片资源。第 081–082 行中,eRight 按钮带两块图(常态/屏蔽态),ePin 只传单图且第二参数用 NULL,说明图钉不需要禁用态渲染。 销毁阶段先看 RemoveCtrlSlider:第 088 行先关掉 CHART_EVENT_OBJECT_DELETE 事件,避免循环 delete 时图表反复触发对象删除回调;遍历 ePlay+1 到 eNull 把 Btn 指针 delete 并置 NULL,再用 ObjectsDeleteAll 按前缀 'B' 清对象,最后第 095 行恢复事件开关。 PositionPinSlider 负责图钉横坐标夹紧。第 103 行把传入 p 限制在 m_Slider.Minimal 与 def_MaxPosSlider 之间,第 106–107 行叠加 def_PosXObjects 与 95-(def_SizeButtons/2) 的偏移量,保证图钉视觉居中对齐滑轨。iL/iR 仅作边界标记(是否顶到最小/最大),实际位移计算不依赖它们。 把这段直接贴进你的 EA 面板类,改 def_SizeButtons 数值就能看到图钉偏移量随按钮尺寸变化;外汇与贵金属图表加载此类对象面板时存在脚本冲突导致图表卡顿的高风险,建议先在模拟账户验证。

MQL5 / C++
m_Section[eRight].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonCtrl, "::" + def_ButtonCtrlBlock, true);
m_Section[ePin].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin, NULL);
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_ObjectCtrlName("B"));
   ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void PositionPinSlider(class="type">class="kw">ushort p)
  {
   class="type">int iL, iR;
   class="type">class="kw">string szMsg;
   
   m_Section[ePin].x = (class="type">class="kw">short)(p < m_Slider.Minimal ? m_Slider.Minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p));
   iL = (m_Section[ePin].x != m_Slider.Minimal ? class="num">0 : class="num">1);
   iR = (m_Section[ePin].x < def_MaxPosSlider ? class="num">0 : class="num">1);
   m_Section[ePin].x += def_PosXObjects;
      m_Section[ePin].x += class="num">95 - (def_SizeButtons / class="num">2);
      for (eObjectControl c0 = ePlay + class="num">1; c0 < eNull; c0++)  if (m_Section[c0].Btn != NULL)
      {
       class="kw">switch (c0)
         {

按钮文案与鼠标命中判定的底层写法

这段逻辑干两件事:给导航按钮填文案,以及判断鼠标点到了哪个控件。112–115 行用 switch 把枚举映射成字符串,eLeft 显示「Previous Position」、eRight 显示「Next Position」,ePin 则拼上目标序号 IntegerToString(p),default 直接给个换行符当占位。

  • 行调用 Btn.Paint 时,第三个参数 20 是圆角半径,图标选择靠三元表达式:c0==eLeft 用 iL、eRight 用 iR、其余传 0。改文案或图标,直接动这两处即可。
  • 行把滑条块宽度设为 Minimal+2 像素,Minimal 通常是滑条最小可点区域,+2 是为了避免图形边框吃掉点击热区。

123–137 的 CheckPositionMouseClick 是命中测试核心:取鼠标图形坐标 x/y 后,遍历 ePlay 到 eNull 的控件,用四点包围盒判断 (x,y) 是否落在 m_Section[c0] 的矩形内。注意 132 行条件是「<=」而非「<」,边界像素也算命中,回测或实盘里若发现边缘误触,先查这里。

  • 行构造函数把终端句柄和鼠标指针接进来,m_MousePtr 后续供所有点击逻辑共用,少一次全局变量污染。
MQL5 / C++
case eLeft  : szMsg = "Previous Position";   break;
case eRight : szMsg = "Next Position";       break;
case ePin   : szMsg = "Go To: " + IntegerToString(p); break;
class="kw">default     : szMsg = "\n";
}
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)), szMsg);
}

ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + class="num">2);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline eObjectControl CheckPositionMouseClick(class="type">class="kw">short &x, class="type">class="kw">short &y)
{
   C_Mouse::st_Mouse InfoMouse;

   InfoMouse = (*m_MousePtr).GetInfoMouse();
   x = (class="type">class="kw">short) InfoMouse.Position.X_Graphics;
   y = (class="type">class="kw">short) InfoMouse.Position.Y_Graphics;
   for (eObjectControl c0 = ePlay; c0 < eNull; c0++)
   {
      if ((m_Section[c0].Btn != NULL) && (m_Section[c0].x <= x) && (m_Section[c0].y <= y) && ((m_Section[c0].x + m_Section[c0].w) >= x) && ((m_Section[c0].y + m_Section[c0].h) >= y))
         class="kw">return c0;
   }

   class="kw">return eNull;
}
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)
   {

「控件析构与缓冲写入的实作细节」

这段逻辑出现在自定义指标的控制类里,负责在初始化失败或重绘前清理图表对象,并在析构时释放按钮指针。外汇与贵金属品种上挂这类指标,图表对象残留可能引发 OBJ_LABEL 堆积,建议直接在 MT5 的「对象列表」里核对数量。 第145至146行先做防御:指标句柄校验不通过或鼠标指针无效就抛用户错误,若 _LastError 非0直接返回,避免后续空指针操作。第147至149行临时关闭 CHART_EVENT_OBJECT_DELETE 事件,用 ObjectsDeleteAll 清掉前缀对象后再打开,防止删除触发连锁回调。 第150至160行是控件坐标的复位:循环把 ePlay 到 eNull 的区块高宽设为 def_SizeButtons、y 固定25,按钮指针置 NULL;随后 ePlay.x 取 def_PosXObjects,eLeft 向右偏移47像素,eRight 偏移511像素,m_Slider.Minimal 赋 eTriState。 析构函数第162至167行遍历删除各按钮对象并再清一次命名对象,最后 delete m_MousePtr,少了这步在切换周期时可能内存泄漏。SetBuffer 第169至177行把滑块最小态与播放态压进一个 double 的联合结构,写到 Buff[rates_total-1];当 rates_total>0 才写,避免越界。 打开 MT5 按 F4 把下面代码塞进你的控制类,编译后挂 XAUUSD 观察对象数是否随周期切换归零。

MQL5 / C++
if ((!IndicatorCheckPass(szShortName)) || (CheckPointer(m_MousePtr) == POINTER_INVALID)) SetUserError(C_Terminal::ERR_Unknown);
if (_LastError != ERR_SUCCESS) class="kw">return;
ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false);
ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName(""));
ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true);
for (eObjectControl c0 = ePlay; c0 < eNull; c0++)
{
   m_Section[c0].h = m_Section[c0].w = def_SizeButtons;
   m_Section[c0].y = class="num">25;
   m_Section[c0].Btn = NULL;
}
m_Section[ePlay].x = def_PosXObjects;
m_Section[eLeft].x = m_Section[ePlay].x + class="num">47;
m_Section[eRight].x = m_Section[ePlay].x + class="num">511;
m_Slider.Minimal = eTriState;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
~C_Controls()
{
   for (eObjectControl c0 = ePlay; c0 < eNull; c0++) class="kw">delete m_Section[c0].Btn;
   ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName(""));
   class="kw">delete m_MousePtr;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void SetBuffer(const class="type">int rates_total, class="type">class="kw">double &Buff[])
{
   uCast_Double info;
   info._16b[eCtrlPosition] = m_Slider.Minimal;
   info._16b[eCtrlStatus] = (class="type">class="kw">ushort)(m_Slider.Minimal > def_MaxPosSlider ? m_Slider.Minimal : (m_Section[ePlay].state ? ePlay : ePause));
   if (rates_total > class="num">0)
      Buff[rates_total - class="num">1] = info.dValue;
}

◍ 回放控件的消息分发与自愈

在 MT5 自定义回放工具里,DispatchMessage 是图表事件的总入口。它接收来自滑块、按钮等图形对象的操作信号,再分流到对应的状态机处理,避免在主循环里堆砌判断逻辑。 看 CHARTEVENT_CUSTOM + evCtrlReplayInit 这个分支:先用联合体 uCast_Double 把 double 型的 dparam 拆成字节流,校验第 7、6 字节必须是 'D' 和 'M' 才认作合法回放帧。这一道校验能拦掉大部分非本工具发出的自定义事件,减少误触发。 滑块位置用了静态变量 iPinPosX 做下限锁存——新到的位置若小于已记录的最小值,就维持旧值,保证回放进度条只能往前或停在已到过的最左端。实测中若外部传入 eCtrlPosition 超过 def_MaxPosSlider(通常设为 1000),会被硬截断,防止越界绘制。 对象被手动删除时的分支更有意思:当用户在图表上删掉播放按钮,代码不是报错退出,而是 delete 指针后置 NULL,再调 SetPlay 用残留状态重建;删掉滑块则走 RemoveCtrlSlider + CreateCtrlSlider 原地复活。这种自愈让面板经得起手误。 别把『删除即崩溃』当默认假设 很多第三方指标在对象被删后直接 ArrayOutOfRange,而这段的防御式重建值得抄。开 MT5 把这段塞进你的回放 EA,故意删一下控件,看是否自动还原。

MQL5 / C++
class="type">void DispatchMessage(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
{
   class="type">class="kw">short x, y;
   class="kw">static class="type">class="kw">ushort iPinPosX = class="num">0;
   class="kw">static class="type">class="kw">short six = -class="num">1, sps;
   uCast_Double info;

   class="kw">switch (id)
   {
      case (CHARTEVENT_CUSTOM + evCtrlReplayInit):
         info.dValue = dparam;
         if ((info._8b[class="num">7] != &class="macro">#x27;D&class="macro">#x27;) || (info._8b[class="num">6] != &class="macro">#x27;M&class="macro">#x27;)) break;
         iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition]));
         SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay);
         break;
      case CHARTEVENT_OBJECT_DELETE:
         if (StringSubstr(sparam, class="num">0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName(""))
         {
            if (sparam == def_ObjectCtrlName(ePlay))
            {
               class="kw">delete m_Section[ePlay].Btn;
               m_Section[ePlay].Btn = NULL;
               SetPlay(m_Section[ePlay].state);
            }else
            {
               RemoveCtrlSlider();
               CreateCtrlSlider();
            }
         }

图表鼠标事件里的播放与滑杆联动

在 MT5 自定义指标或 EA 的图表事件回调里,CHARTEVENT_MOUSE_MOVE 是捕捉拖拽与点击的核心入口。上面这段逻辑把鼠标左键行为分流到播放开关、左右步进和图钉拖拽三种操作,直接决定了面板交互是否跟手。 左侧 ePlay 分支用 SetPlay 翻转状态:开启时移除旧滑杆、把 Minimal 设为当前 iPinPosX,关闭时重建滑杆。这意味着你每点一次播放键,滑杆下限就被钉在当时的指针位置,回测起点可能因此偏移 1~N 根 K 线。 eLeft / eRight 各自把 iPinPosX 减 1 或加 1,并钳制在 Minimal 与 def_MaxPosSlider 之间。若 def_MaxPosSlider 设得比图表可见柱数大,滑杆会走出价格区,属典型越界 bug。 ePin 分支在 six==-1 时记录鼠标起点并关闭 CHART_MOUSE_SCROLL,随后用 iPinPosX = sps + x - six 做相对位移。外汇与贵金属波动剧烈,这类手动定位在高杠杆下极易误触,实盘前务必在策略测试器用历史数据跑一遍。

MQL5 / C++
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();
       break;
    case eLeft:
       PositionPinSlider(iPinPosX = (iPinPosX > m_Slider.Minimal ? iPinPosX - class="num">1 : m_Slider.Minimal));
       break;
    case eRight:
       PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + class="num">1 : def_MaxPosSlider));
       break;
    case ePin:
       if (six == -class="num">1)
       {
        six = x;
        sps = (class="type">class="kw">short)iPinPosX;
        ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false);
       }
       iPinPosX = sps + x - six;

「滑块越界钳制与鼠标滚动复位」

这段逻辑处理的是面板滑块拖拽后的坐标修正与状态切换。当传入的水平坐标 iPinPosX 小于滑块下限 m_Slider.Minimal 时,直接取下限;大于上限常量 def_MaxPosSlider 时取上限,否则用原值,调用 PositionPinSlider 完成重绘定位。 若 six 大于 0 进入另一分支,先把 six 置为 -1,再对当前图表 ID 执行 ChartSetInteger 打开 CHART_MOUSE_SCROLL,意味着退出某种锁定态后恢复鼠标滚轮平移。最后无论走哪条 case,都在 ChartRedraw 强制刷新界面。 宏清理部分用 #undef 逐一撤销 def_PosXObjects、def_ButtonPlay 到 def_PathBMP 等编译期符号,避免跨文件命名污染。开 MT5 把这段贴进 EA 的鼠标事件回调,拖一下滑块就能看到坐标被钳在 [Minimal, def_MaxPosSlider] 区间内。

MQL5 / C++
 PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX)));
 break;
 }else if (six > class="num">0)
 {
 six = -class="num">1;
 ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);
 }
 break;
 }
 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_ButtonCtrl
class="macro">#undef def_ButtonCtrlBlock
class="macro">#undef def_ButtonPin
class="macro">#undef def_PathBMP
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 代码同步与数据获取的路径

前面各节拆出的 MQL5 片段都不是孤立示例,而是完整功能体的一部分。你只要把文中标出的片段直接覆盖进对应函数,就能让本地工程与说明保持同版本,不必等外部源文件。 作者停更完整源文件压缩包,原因很直接:文中那类运行时构造,熟手改起来能兜住异常,新手复制整套却容易在调用链上写出难以排查的错。 演示用的分时报价和柱形数据分在三个 ZIP 里,前面文章可下;本地编译出的可执行文件跑起来应接近文内视频效果。外汇与贵金属仿真涉及高杠杆,回测数据不等于实盘表现,接入前先在小布盯盘里跑一遍验证。

常见问题

32位每行按4字节对齐且含透明通道,24位无透明且对齐填充不同;重构时要先按位深算行字节数再逐像素拷贝,否则图像会错位或偏色。
缩放映射要按目标尺寸反算源坐标并做最近邻或线性采样;透明像素单独判断跳过绘制,黑边多是没处理Alpha或坐标越界导致的。
小布可以基于你的品种页直接加载历史tick流,自动生成位图标签并套用缩放重绘,你只需核对坐标映射参数即可。
双图分支能把静态底图与动态覆盖层分开缓冲,重绘时只刷变动层,降低CPU占用并避免闪烁。
常见是像素过滤后没释放私有结构里的临时数组,且标签对象未显式删除;退出前遍历缓冲调用清理并销毁标签即可。