开发回放系统(第 42 部分):图表交易项目(I)·进阶篇
📘

开发回放系统(第 42 部分):图表交易项目(I)·进阶篇

第 2/2 篇

◍ 浮动图表类如何用几十行接管拖拽

C_ChartFloatingRAD 这个类名容易让人误会成画图工具,实际它是给图表交易指标做依附容器的。当前阶段它已经能把窗口锁死在研究态:只要研究进行中,你用鼠标拖图表窗口也拖不动,行为和演示视频里一致。 源码只有两段核心逻辑,没有显式的对象创建大段代码,也没有独立的鼠标事件模块。第 07 行写的是 class C_ChartFloatingRAD : private C_Terminal,继承设成 private,外部代码没法透过它碰 C_Terminal,指标主体也没直接调 C_Terminal,权限收得干净。 构造函数从第 42 行起,接收一个字符串和一个鼠标指标指针。第 43 行先初始化基类,第 45 行调验证码确认图表交易指标不在图上,失败就通过 SetUserError 写进 _LastError。第 46 行存下传入的指针——这里有个反直觉点:调用时鼠标指标在不在图表上根本不重要,关键是第 15 行传进来的那个名字字符串,它必须和 C_Mouse 类第 169 行要找的指标名对上,MT5 才能后续拿到句柄。 第 47 行调第 19 行函数,在图上建一个 OBJ_CHART 对象,第 29 行存了对象 ID;第 48 行调第 34 行函数,第 36 行加载指定模板、第 37 行强制 OBJ_CHART 刷新,于是图例里的窗口就显示出来了。 消息处理 DispatchMessage 从第 57 行开始,目前只认真处理第 64 行起的鼠标移动。第 65 行是唯一一次引用鼠标指标:若图上缺失同名鼠标指标,或正在用它做研究,第 65 行都会报点击失败。若 MT5 找到了同名指标,C_Mouse 第 158 行会查按钮码,匹配后第 67–79 行才执行窗口移动。 为什么第 67–68 行用 MT5 自带坐标而不是鼠标指标的值?看 C_Mouse 第 203–206 行就知道,那里把屏幕 x/y 转成了价格/时间坐标。OBJ_CHART 实际吃的是屏幕坐标,若喂资产坐标拖动就会飘,不像视频里顺滑。外汇和贵金属图表挂这类浮动窗做回放研究,杠杆与滑点风险高,先在策略测试器里验证坐标逻辑再上实盘。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#include "../Auxiliar/C_Terminal.mqh"
class="macro">#include "../Auxiliar/C_Mouse.mqh"
class C_ChartFloatingRAD : class="kw">private C_Terminal
{
   class="kw">private :
      class="kw">struct st00
      {
         class="type">int     x, y, cx, cy;
         class="type">class="kw">string  szObj_Chart;
         class="type">long    WinHandle;
      }m_Info;

在图表里嵌一块独立子窗口

想在 MT5 主图旁边挂一块自己的分析面板,最省事的办法是用 OBJ_CHART 对象画一个内嵌图表。下面这段逻辑就是先按传入的 x、y、w、h 在终端坐标系里定位,再关掉它的时间轴与价格轴刻度,避免和主图互相干扰。 代码第 21 行用 ObjectsTotal 的返回值当对象名,保证每次新建不重名;第 22~26 行依次设定左上角偏移和宽高。注意第 27、28 行把 OBJPROP_DATE_SCALE 和 OBJPROP_PRICE_SCALE 都设为 false,子图就不会自带坐标轴。 第 29 行拿回子图的 WinHandle 是关键,后面所有模板加载、重绘都靠这个句柄。m_Info.cy 硬编码成 26(第 31 行),是给标题栏留的高度,若你面板头部更高,这里要同步改。 UpdateChartTemplate 里只做两件事:给子图套用 IDE_RAD.tpl 模板,然后立刻 ChartRedraw。外汇与贵金属波动剧烈、杠杆风险高,这类面板仅作辅助观察,信号不代表方向确定性。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
   C_Mouse *m_Mouse;
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void CreateWindowRAD(class="type">int x, class="type">int y, class="type">int w, class="type">int h)
      {
         m_Info.szObj_Chart = (class="type">class="kw">string)ObjectsTotal(GetInfoTerminal().ID);          class=class="str">"cmt">// 用当前对象总数当唯一对象名
         ObjectCreate(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJ_CHART, class="num">0, class="num">0, class="num">0); class=class="str">"cmt">// 创建内嵌图表对象
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, m_Info.x = x); class=class="str">"cmt">// 设左边距
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, m_Info.y = y); class=class="str">"cmt">// 设上边距
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XSIZE, w);  class=class="str">"cmt">// 设宽度
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YSIZE, h);  class=class="str">"cmt">// 设高度
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_DATE_SCALE, false);  class=class="str">"cmt">// 关时间轴
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_PRICE_SCALE, false); class=class="str">"cmt">// 关价格轴
         m_Info.WinHandle = ObjectGetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_CHART_ID); class=class="str">"cmt">// 取子图句柄
         m_Info.cx = w;   class=class="str">"cmt">// 记录客户区宽
         m_Info.cy = class="num">26;  class=class="str">"cmt">// 标题栏高固定 class="num">26
      };
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void UpdateChartTemplate(class="type">void)
      {
         ChartApplyTemplate(m_Info.WinHandle, "IDE_RAD.tpl"); class=class="str">"cmt">// 套用模板
         ChartRedraw(m_Info.WinHandle);                       class=class="str">"cmt">// 立即重绘
      }
class=class="str">"cmt">//+------------------------------------------------------------------+

「浮动雷达面板的构造与鼠标派发」

这个类把图表上的悬浮雷达窗口封装成了可实例化的对象。构造函数接收短名和鼠标指针,先过一遍指标名校验,失败就抛自定义错误码,再把鼠标对象存进成员、拉起 170×240 像素的窗口并刷新模板。 析构时只做两件事:删掉图表对象、释放鼠标指针。注意这里直接 delete m_Mouse,说明指针所有权已转移给面板,外部不要再碰原指针。 DispatchMessage 是事件入口,用静态变量 sx/sy 记住上一次坐标,配合 CHARTEVENT_MOUSE_MOVE 判断左键是否按下。外汇与贵金属行情跳空频繁,这种浮动面板在实盘里可能因鼠标事件堆积而卡顿,建议你在 MT5 里挂一个 EURUSD 的 M1 图跑半小时验证。

MQL5 / C++
class="kw">public:
class=class="str">"cmt">//+------------------------------------------------------------------+
             C_ChartFloatingRAD(class="type">class="kw">string szShortName, C_Mouse *MousePtr)
                  :C_Terminal()
                  {
                         if (!IndicatorCheckPass(szShortName)) SetUserError(C_Terminal::ERR_Unknown);
                         m_Mouse = MousePtr;
                         CreateWindowRAD(class="num">0, class="num">0, class="num">170, class="num">240);
                         UpdateChartTemplate();
                  }
class=class="str">"cmt">//+------------------------------------------------------------------+
                  ~C_ChartFloatingRAD()
                  {
                         ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Chart);
                         class="kw">delete m_Mouse;
                  }
class=class="str">"cmt">//+------------------------------------------------------------------+
                  class="type">void DispatchMessage(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
                  {
                         class="kw">static class="type">int sx = -class="num">1, sy = -class="num">1;
                         class="type">int x, y, mx, my;

                         class="kw">switch (id)
                         {
                                  case CHARTEVENT_MOUSE_MOVE:
                                         if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft))
                                         {

◍ 拖拽面板时先掐掉图表滚轮

在自定义面板的鼠标事件回调里,先通过 lparam、dparam 强转拿到光标坐标 x、y,再判断它是否落在面板矩形区域(m_Info.x 到 x+cx、y 到 y+cy)内。只有命中面板才进入拖拽逻辑,避免误拖图表其他元素。 一旦判定 sx 初始小于 0(说明刚按下还没记录偏移),立刻用 ChartSetInteger 把当前图表的 CHART_MOUSE_SCROLL 关成 false。这一步很关键:不关掉滚轮缩放,拖面板时光标滚轮可能触发图表缩放,位移会乱跳。 关掉滚动后,把 sx、sy 记成「光标相对面板左上角的偏移」:sx = x - m_Info.x,sy = y - m_Info.y。之后每次移动,新左上角坐标就是 mx = x - sx、my = y - sy,只要大于 0 就写回 OBJPROP_XDISTANCE / OBJPROP_YDISTANCE。外汇贵金属图表多品种切换频繁,这套坐标锁定的稳定性直接影响面板可用性,建议开 MT5 挂一个测试面板验证拖拽手感。

MQL5 / C++
x = (class="type">int)lparam;
y = (class="type">int)dparam;
if ((x > m_Info.x) && (x < (m_Info.x + m_Info.cx)) && (y > m_Info.y) && (y < (m_Info.y + m_Info.cy)))
  {
   if (sx < class="num">0)
     {
      ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false);
      sx = x - m_Info.x;
      sy = y - m_Info.y;
     }
   if ((mx = x - sx) > class="num">0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, m_Info.x = mx);
   if ((my = y - sy) > class="num">0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, m_Info.y = my);
  }

滚轮锁定的反向复位逻辑

当 sx 大于 0 时,说明此前鼠标横向位移触发了图表滚动限制,此时代码主动把 CHART_MOUSE_SCROLL 设回 true,相当于解除锁定让交易者能重新自由滚轮缩放。 随后 sx 与 sy 被强制写为 -1,这一对坐标复位很关键:若不归位,下一帧鼠标事件会带着旧值继续误判方向,导致滚动权限在 true/false 间抖动。 从片段看,sx>0 分支处理完即 break 跳出 switch,说明横向正向位移只做「解锁+清状态」一件事,不再叠加其他图表操作。 实盘里你可把这段嵌进 EA 的鼠标钩子,用 Print(sx) 观察第 83 行复位前的值;外汇与贵金属杠杆高,误锁滚动可能让你在黄金跳空时来不及缩放看价位,建议先在模拟盘验证。

MQL5 / C++
}
 else if (sx > class="num">0)
  {
   ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);
   sx = sy = -class="num">1;
  }
 class="kw">break;
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
};
class=class="str">"cmt">//+------------------------------------------------------------------+

x = (*m_Mouse).GetInfoMouse().Position.X;
y = (*m_Mouse).GetInfoMouse().Position.Y;

「鼠标指标只是个空壳子」

这一篇只走了图表交易指标的第一阶段:我们用鼠标指标和正在搭的图表交易指标做了视觉上的对齐,但目前除了能在图上拖放,它没有任何实际功能。 下一篇才会真正扩展图表交易的逻辑,比如把拖放位置转成订单参数。现在你开 MT5 把附带的 Anexo.zip(420.65 KB)解压加载,看到的也只是一个能拖动、不会下单的壳。 外汇和贵金属是高杠杆品种,这类半成品指标直接实盘风险极高,建议只在策略测试器里验证交互。

常见问题

在浮动类接管鼠标时先屏蔽图表滚轮消息,拖拽结束再反向复位,避免图表乱跑。
用浮动图表类建几十行的子窗口对象,靠鼠标派发把事件交给面板,不碰主图K线。
小布可加载你的面板脚本并模拟拖拽与滚轮锁定,直接标出派发断点省去手动测。
鼠标指标只做事件空壳,真正绘制和响应放在浮动类里,指标本身不必含逻辑。
在拖拽结束的回调里写反向复位逻辑,释放时恢复滚轮,不然图表会一直锁死。