开发回放系统(第 42 部分):图表交易项目(I)·进阶篇
◍ 浮动图表类如何用几十行接管拖拽
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 实际吃的是屏幕坐标,若喂资产坐标拖动就会飘,不像视频里顺滑。外汇和贵金属图表挂这类浮动窗做回放研究,杠杆与滑点风险高,先在策略测试器里验证坐标逻辑再上实盘。
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。外汇与贵金属波动剧烈、杠杆风险高,这类面板仅作辅助观察,信号不代表方向确定性。
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 图跑半小时验证。
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 挂一个测试面板验证拖拽手感。
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 行复位前的值;外汇与贵金属杠杆高,误锁滚动可能让你在黄金跳空时来不及缩放看价位,建议先在模拟盘验证。
} 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)解压加载,看到的也只是一个能拖动、不会下单的壳。 外汇和贵金属是高杠杆品种,这类半成品指标直接实盘风险极高,建议只在策略测试器里验证交互。