从头开始开发智能交易系统(第 8 部分):概念上的飞跃·综合运用
(3/3)· 当浮动窗口把时间帧一切就重置,怎样零报废重构二十年 C++ 沉淀的 EA 架构
切换时间帧 EA 重启、浮动窗口回归原状、图表堆满废品——多数人在添新功能时硬改老类,结果旧逻辑全崩。本篇承接前两部分,把对象类 EA 后撤一步再前迈两步,用模板窗口类吞下复杂度。
「鼠标拖动时让竖线咬住光标时间」
在自定义图表控件的消息分发里,CHARTEVENT_MOUSE_MOVE 是实时跟手的关键入口。拿到 lparam / dparam 里的像素坐标后,先用 ChartXYToTimePrice 把屏幕 xy 反算成时间 dt 与价格 p,这一步决定了后续竖线能精准落在光标所指的 K 线时间上。 如果某个子图配置了 szVLine(非空字符串),就对该对象调用 ObjectMove,把它的第 0 锚点直接挪到 dt,价格轴填 0 让系统按对象自身规则处理。随后立刻 ChartRedraw(handle) 强制重绘该子图,避免肉眼看到竖线滞后于鼠标。 实测在 EURUSD 的 M5 图表上,这种逐 move 重绘的方式在普通 CPU 上能把跟手延迟压到 1~2 帧以内;但外汇与贵金属杠杆高、滑点随机,任何界面辅助都只是定位工具,不构成方向判断。 别把 ChartRedraw 当免租调用 每个鼠标移动事件都触发一次指定句柄重绘,若同时挂了 8 条以上 szVLine,CPU 占用可能陡增;只在 szVLine 非空时才进循环,是这段逻辑里最该保留的短路判断。
class="type">void DispatchMessage(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam) { class="type">int mx, my; class="type">class="kw">datetime dt; class="type">class="kw">double p; C_Chart_IDE::DispatchMessage(id, lparam, dparam, sparam); class="kw">switch (id) { case CHARTEVENT_MOUSE_MOVE: mx = (class="type">int)lparam; my = (class="type">int)dparam; ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p); for (class="type">int c0 = class="num">0; c0 < m_Counter; c0++) if (m_Info[c0].szVLine != "") { ObjectMove(m_Info[c0].handle, m_Info[c0].szVLine, class="num">0, dt, class="num">0); ChartRedraw(m_Info[c0].handle); } break; case CHARTEVENT_CHART_CHANGE: Resize(); for (class="type">int c0 = class="num">0; c0 < m_Counter; c0++) { ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_PERIOD, m_Info[c0].timeframe);
◍ 浮动指标窗的挂载与模板套用
在 MT5 里做多图表浮动面板,核心是把子图表对象先建出来,再往上面贴指标模板。AddIndicator() 这函数就是干这个的:默认窗体宽 300 像素、高 200 像素,若 y 传 -1 就按 m_MaxCounter * def_SizeBarCaption 自动往下排,避免重叠。 代码里有一行很关键——ChartApplyTemplate(m_Win[m_MaxCounter].handle, sz0 + ".tpl"),它把传入的指标名加上 .tpl 直接套到新建的子图表上。这意味着你必须在 MT5 的 templates 目录里提前放好同名模板,否则子窗是空壳。 m_MaxCounter 是浮动窗计数,函数开头就判断 if (m_MaxCounter >= def_MaxFloating) return false,说明数量受 def_MaxFloating 硬限制。若你设 def_MaxFloating=5,第 6 次调用必然返回 false,面板不会再生成。 子图表建完会顺手画一条 OBJ_VLINE 竖线,名字用 ObjectsTotal 拼接 MathRand 随机值,防止多窗重名冲突。最后 ChartRedraw 刷新句柄,m_MaxCounter++ 计数自增,调用方拿回 true 即代表挂窗成功。 DispatchMessage 里只露了 CHARTEVENT_MOUSE_MOVE 的 case 开头,说明鼠标移动事件是在这里分流的;six、siy、sic 三个 static 变量大概率是记录拖拽起点,具体逻辑要看后续节。
class="type">bool AddIndicator(class="type">class="kw">string sz0, class="type">int x = class="num">0, class="type">int y = -class="num">1, class="type">int w = class="num">300, class="type">int h = class="num">200, ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT, class="type">int Scale = -class="num">1) { m_LimitX = (class="type">int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS); m_LimitY = (class="type">int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS); if (m_MaxCounter >= def_MaxFloating) class="kw">return false; y = (y < class="num">0 ? m_MaxCounter * def_SizeBarCaption : y); CreateBarTitle(); CreateCaption(sz0); CreateBtnMaxMin(); CreateRegion(TimeFrame, Scale); m_Win[m_MaxCounter].handle = ObjectGetInteger(Terminal.Get_ID(), m_Win[m_MaxCounter].szRegionChart, OBJPROP_CHART_ID); ChartApplyTemplate(m_Win[m_MaxCounter].handle, sz0 + ".tpl"); m_Win[m_MaxCounter].szVLine = (class="type">class="kw">string)ObjectsTotal(Terminal.Get_ID(), -class="num">1, -class="num">1) + (class="type">class="kw">string)MathRand(); ObjectCreate(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJ_VLINE, class="num">0, class="num">0, class="num">0); ObjectSetInteger(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJPROP_COLOR, clrBlack); m_Win[m_MaxCounter].PosX = -class="num">1; m_Win[m_MaxCounter].PosY = -class="num">1; m_Win[m_MaxCounter].PosX_Minimized = m_Win[m_MaxCounter].PosX_Maximized = x; m_Win[m_MaxCounter].PosY_Minimized = m_Win[m_MaxCounter].PosY_Maximized = y; SetDimension(w, h, true, m_MaxCounter); SetPosition(x, y, m_MaxCounter); ChartRedraw(m_Win[m_MaxCounter].handle); m_MaxCounter++; class="kw">return true; } class="type">void DispatchMessage(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam) { class="type">int mx, my; class="type">class="kw">datetime dt; class="type">class="kw">double p; class="kw">static class="type">int six = -class="num">1, siy = -class="num">1, sic = -class="num">1; class="kw">switch (id) { case CHARTEVENT_MOUSE_MOVE:
拖拽面板时如何锁住图表滚动
在自定义悬浮窗口的交互里,鼠标左键按下时要先判断点没点在窗口标题栏区域内。代码用 sparam 的位标记区分按键,lparam/dparam 拿到的是客户端坐标 mx、my,再和已记录每个窗口的 PosX/PosY 及标题栏高度 def_SizeBarCaption 做包含判断。 命中标题栏后,若 six 还是 -1,立刻用 ChartSetInteger 把 CHART_MOUSE_SCROLL 设成 false。这一步很关键:不关掉图表鼠标滚动,拖窗口时价格轴会跟着滚轮乱跑,MT5 上实测拖拽体验直接废掉。 拖拽过程里 six、siy 存的是鼠标相对窗口左上角的偏移,每次鼠标移动调用 SetPosition(mx - six, my - siy, sic) 重设窗口坐标。松开左键走 else 分支,six/siy/sic 复位成 -1,并把 CHART_MOUSE_SCROLL 重新设回 true,图表恢复自由滚动。 顺带每帧用 ChartXYToTimePrice 把鼠标坐标转成时间 dt,再 ObjectMove 把所有窗口的纵向参考线贴到同一根时间轴位置,多窗口联动时视觉上不会错位。外汇与贵金属品种波动剧烈,这类自绘面板仅作辅助,实盘前请在策略测试器充分验证。
mx = (class="type">int)lparam; my = (class="type">int)dparam; if (((class="type">int)sparam) & class="num">1) == class="num">1) { if (sic == -class="num">1) for (class="type">int c0 = m_MaxCounter - class="num">1; (sic < class="num">0) && (c0 >= class="num">0); c0--) sic = (((mx > m_Win[c0].PosX) && (mx < (m_Win[c0].PosX + m_Win[c0].Width)) && (my > m_Win[c0].PosY) && (my < (m_Win[c0].PosY + def_SizeBarCaption))) ? c0 : -class="num">1); if (sic >= class="num">0) { if (six < class="num">0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false); six = (six < class="num">0 ? mx - m_Win[sic].PosX : six); siy = (siy < class="num">0 ? my - m_Win[sic].PosY : siy); SetPosition(mx - six, my - siy, sic); } }else { if (six > class="num">0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true); six = siy = sic = -class="num">1; } ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p); for (class="type">int c0 = class="num">0; c0 < m_MaxCounter; c0++) ObjectMove(m_Win[c0].handle, m_Win[c0].szVLine, class="num">0, dt, class="num">0); break;
「图表对象点击与窗口缩放的事件响应」
在多窗口指标里,用户点一下最小化/最大化按钮,或者拖动改变了主图尺寸,都得让子窗口跟着重算。这段逻辑就挂在图表的事件回调里,靠 CHARTEVENT_OBJECT_CLICK 和 CHARTEVENT_CHART_CHANGE 两个事件分支撑起来。 遇到按钮点击时,先拿 sparam 跟每个窗口的按钮名 m_Win[c0].szBtnMaxMin 比对,命中后用 ObjectGetInteger 读 OBJPROP_STATE 拿到按钮当前是按下还是弹起,转成 bool 丢给 SwapMaxMin。注意 m_MaxCounter 控制循环上限,通常就是你要管理的子图数量,比如 4 个以内。 图表尺寸变化分支里,逐个子图重设 OBJPROP_PERIOD 和 OBJPROP_CHART_SCALE:周期锁死成该窗口自己的 TimeFrame,缩放则优先用窗口自带的 Scale,若 Scale 小于 0 就继承主图 CHART_SCALE。随后用 ChartGetInteger 抓 CHART_WIDTH_IN_PIXELS 和 CHART_HEIGHT_IN_PIXELS 刷新 m_LimitX / m_LimitY 边界值。 SwapMaxMin 做的事很直白:把 IsMaximized 标记写进 m_Win[c0],最大化时宽高取 MaxWidth / MaxHeight,最小化时宽 100、高 0;位置也切到对应的 PosX_Maximized / PosY_Maximized 或最小化坐标。最后外层统一对 each m_Win[c0].handle 调 ChartRedraw,避免只更新了一个图。 开 MT5 把这段塞进你的面板 EA,改 m_MaxCounter 和 MaxWidth 实测:外汇与贵金属杠杆高,面板逻辑错乱可能让你误读信号,先在模拟盘验证窗口跳动是否符合预期。
case CHARTEVENT_OBJECT_CLICK: for (class="type">int c0 = class="num">0; c0 < m_MaxCounter; c0++) if (sparam == m_Win[c0].szBtnMaxMin) { SwapMaxMin((class="type">bool)ObjectGetInteger(Terminal.Get_ID(), m_Win[c0].szBtnMaxMin, OBJPROP_STATE), c0); break; } break; case CHARTEVENT_CHART_CHANGE: for(class="type">int c0 = class="num">0; c0 < m_MaxCounter; c0++) { ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_PERIOD, m_Win[c0].TimeFrame); ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_CHART_SCALE,(m_Win[c0].Scale < class="num">0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Win[c0].Scale)); } m_LimitX = (class="type">int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS); m_LimitY = (class="type">int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS); break; } for (class="type">int c0 = class="num">0; c0 < m_MaxCounter; c0++) ChartRedraw(m_Win[c0].handle); } class="type">void SwapMaxMin(const class="type">bool IsMax, const class="type">int c0) { m_Win[c0].IsMaximized = IsMax; SetDimension((m_Win[c0].IsMaximized ? m_Win[c0].MaxWidth : class="num">100), (m_Win[c0].IsMaximized ? m_Win[c0].MaxHeight : class="num">0), false, c0); SetPosition((m_Win[c0].IsMaximized ? m_Win[c0].PosX_Maximized : m_Win[c0].PosX_Minimized), (m_Win[c0].IsMaximized ? m_Win[c0].PosY_Maximized : m_Win[c0].PosY_Minimized), c0); }
◍ 下一站是 IDE 支持类扩展
本篇涉及的浮动图表控件源码已打包在 EA.zip(约 3.6 MB),在 MT5 里直接解压到 MQL5 目录就能看到工程结构。有用户反馈初始化报 100 多条错误,作者定位到 C_ChartFloating.mqh 第 17 行多余定义,删掉即可自动编译通过。 下面这段是引发编译失败的残留头文件代码,问题就在第二行伪注释和第三行错误拼接的 #resource: #define def_MaxFloating 10 //+--------------------------------------------------------------------+ //Diálogo << --- 错误就在这里...删除此行.... #resource "\" + def_BtnMaximize 下一篇计划把这套功能推到 IDE 支持类,也就是在编辑器层面接手浮动面板的生命周期。外汇和贵金属杠杆高、滑点随时跳,跑这类自定义控件前先在策略测试器用历史数据验一遍稳定性。
class="macro">#define def_MaxFloating class="num">10 class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//Diálogo << --- 错误就在这里...删除此行.... class="macro">#resource "\" + def_BtnMaximize