从头开始开发智能交易系统(第 9 部分):概念上的飞跃 (II)·进阶篇
(2/3)· 当 MetaTrader 5 原生不支持图表内嵌图表,如何用对象嵌套绕过事件拦截陷阱
很多交易者以为在 MT5 浮动窗口里塞进图表交易界面只是拖几个对象的事,结果点击内部按钮全算到了外层头上。平台本就不是为这种用法设计的,事件传不到内层,逻辑直接瘫痪。这篇接着上篇,把嵌套对象的坑和绕法一次讲透。
用代码把图形对象钉在图表上
在 MQL5 指标或 EA 初始化收尾阶段,把刚创建的对象设为选中并强制重绘,是避免图形残留、保证交互即时性的关键一步。下面这段片段展示了 handle 图表下选中 sz1 对象后立即 ChartRedraw 的写法,随后返回 INIT_SUCCEEDED 表示初始化完成。 OnCalculate 里直接 return rates_total,意味着每根 K 线都触发一次计算但不做额外处理,常用于纯图形类指标——回测时 CPU 占用倾向更低。 OnChartEvent 捕获 CHARTEVENT_OBJECT_CLICK 后仅 Print(sparam),说明点击对象名会输出到日志;接外汇或贵金属图表时,这种点击事件可能帮助你快速定位手动标注的支撑阻力位,但贵金属杠杆高、滑点大,实盘验证前务必在模拟盘跑通。
ObjectSetInteger(handle, sz1, OBJPROP_SELECTED, true); ChartRedraw(handle); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[]) { class="kw">return rates_total; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(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) { if (id == CHARTEVENT_OBJECT_CLICK) Print(sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 把 IDE 塞进浮动窗口的继承改造
做浮动窗口不要从零写事件系统。MQL5 没有多重继承,所以先调 C_TemplateChart 和 C_ChartFloating 的继承序列,让浮动窗口类能复用原先 IDE 里跑通的事件接收逻辑,只在高亮处插入新对象,其余代码原样不动。 改动后图表上能出 IDE 信息,但对象与浮动窗口脱节。先取窗口在图表上的锚点:用 ChartGetInteger 拿 CHART_WIDTH_IN_PIXELS 和 CHART_HEIGHT_IN_PIXELS 算边界,再给每个窗口存 PosX_Maximized / PosY_Maximized,初始 y 为负时按 m_MaxCounter * def_SizeBarCaption 自动排布,避免重叠。 最小化时会露馅——窗口缩了对象还留在屏上,因为对象没绑进窗口区域。补一段判定:若对象坐标超出窗口矩形就隐藏,处理时间能省掉后续移动时的大量重绘。移动修复同理,靠 SetPosition 与 ChartRedraw 联动。 下面这段是改造后的核心局部代码,def_MaxFloating 控制同屏浮动窗口上限,超了 StageLocal01 直接返 false,系统不会变成无法维护的拼凑体。外汇/贵金属图表加载这类模板前请先在模拟盘验证,实盘高杠杆下对象闪烁可能引发误触。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool StageLocal01(class="type">class="kw">string sz0, 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 class="kw">false; CreateBarTitle(); CreateCaption(sz0); CreateBtnMaxMin(); CreateRegion(TimeFrame, Scale); m_Win[m_MaxCounter].handle = ObjectGetInteger(Terminal.Get_ID(), m_Win[m_MaxCounter].szRegionChart, OBJPROP_CHART_ID); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void StageLocal02(class="type">int x, class="type">int y, class="type">int w, class="type">int h) { y = (y < class="num">0 ? m_MaxCounter * def_SizeBarCaption : y); 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=class="str">"cmt">//+------------------------------------------------------------------+ 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) { if (!StageLocal01(sz0, TimeFrame, Scale)) class="kw">return class="kw">false; 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();
「模板加载时分两种挂法」
在批量建图函数 AddTemplate 里,系统先判高度 h 是否为 0:若为 0,就按当前品种或默认名 SetBase 写底参,并试图给上一子窗口 ChartApplyTemplate 套用 .tpl;若套用失败且是品种级调用,则回退到 Default.tpl。这一分支决定了无高度参数时的裸图基准。 当传入模板名等于 def_NameTemplateRAD 时走 RAD 专用通道:若 w>0 且 h>0 就调 Add_RAD_IDE 建指标区,否则调 C_Chart_IDE::Create 建子窗口并清空 szVLine。非 RAD 模板则走通用通道,有尺寸就 AddIndicator,无尺寸就现场造一条垂直线——名字用 ObjectsTotal 总数拼 MathRand 随机数,避免重名。 下面这段是通用分支里造垂直线的原码,注意 OBJ_VLINE 的时间参数填 0 表示绑定当前时间轴: ObjectCreate(m_Info[m_Counter-1].handle, m_Info[m_Counter-1].szVLine, OBJ_VLINE, 0, 0, 0); ObjectSetInteger(m_Info[m_Counter-1].handle, m_Info[m_Counter-1].szVLine, OBJPROP_COLOR, clrBlack); 在 MT5 里跑这段,若终端对象总数超过 50 个,szVLine 前缀就会是 '50' 加随机尾,方便你在对象列表里排查是不是本类模板生成的线。外汇与贵金属图表多窗口管理属高风险操作,参数误写可能让 EA 反复建图拖慢终端。
ObjectCreate(m_Info[m_Counter - class="num">1].handle, m_Info[m_Counter - class="num">1].szVLine, OBJ_VLINE, class="num">0, class="num">0, class="num">0); ObjectSetInteger(m_Info[m_Counter - class="num">1].handle, m_Info[m_Counter - class="num">1].szVLine, OBJPROP_COLOR, clrBlack);
给自定义图表挂上 IDE 模板的两种写法
在 MT5 里用 EA 批量开子图时,想让某个窗口直接套用交易面板样式,可以走 Add_RAD_IDE 这条路。第一个版本只做了最基础的校验:若 StageLocal01 没通过就直接返回 false,随后对当前窗口句柄调用 ChartApplyTemplate 加载 \Files\Chart Trade\IDE.tpl,再把坐标和尺寸交给 StageLocal02。 第二个版本多了一道防御:w 或 h 任一小于等于 0 直接 return false,避免把非法尺寸传进绘图层。实测中若模板路径写错,ChartApplyTemplate 会静默失败,图表仍是空白面板,所以路径字符串必须和 Files 目录下的 tpl 文件名严格一致。 在 AddTemplate 的调度逻辑里,当模板名等于 def_NameTemplateRAD 时,会走 C_Chart_IDE::Create(Add_RAD_IDE(...)) 创建浮动 IDE 图;否则若 w、h 都大于 0 就挂指标,否则画一条 OBJ_VLINE 做占位。外汇与贵金属品种波动剧烈,这类自动建图逻辑若参数误设,可能在短时间内生成大量离线图表,占用终端资源,使用前建议在模拟账户验证。 下面这段是 Add_RAD_IDE 带尺寸校验的可用骨架,复制到你的面板类里就能跑: bool Add_RAD_IDE(string sz0, int x, int y, int w, int h) {
| if ((w <= 0) | (h <= 0)) return false; // 尺寸非法直接退出,防止建出不可见窗口 |
|---|
if (!StageLocal01(sz0, PERIOD_CURRENT, -1)) return false; // 初始化子图环境,失败则返回 ChartApplyTemplate(m_Win[m_MaxCounter].handle, "\\Files\\Chart Trade\\IDE.tpl"); // 套用 IDE 模板 StageLocal02(x, y, w, h); // 按传入坐标和尺寸布局 return true; // 建图成功 }
class="type">bool Add_RAD_IDE(class="type">class="kw">string sz0, class="type">int x, class="type">int y, class="type">int w, class="type">int h) { if ((w <= class="num">0) || (h <= class="num">0)) class="kw">return class="kw">false; if (!StageLocal01(sz0, PERIOD_CURRENT, -class="num">1)) class="kw">return class="kw">false; ChartApplyTemplate(m_Win[m_MaxCounter].handle, "\\Files\\Chart Trade\\IDE.tpl"); StageLocal02(x, y, w, h); class="kw">return true; }
◍ 从模板文件反序列化界面控件
这段逻辑做的是把 Chart Trade\IDE.tpl 这个二进制模板读进内存,再按 XML 风格标签逐个重建图表上的 Label、Button、Edit 等对象。外汇与贵金属图表挂 EA 做自定义面板时,这种从 tpl 动态生成控件的方式很常见,但 MT5 上若文件路径或对象类型不匹配,加载会直接返回 false,属于高风险自定制环节。
先以 FILE_BIN|FILE_READ 打开模板,句柄无效就退出;随后 FileReadInteger(...,SHORT_VALUE) 跳过文件头两字节,接着把 eRESULT 到 eEDIT_STOP 区间的对象名清空,准备复用数组。浮窗与否决定子窗口 ID:浮动时固定为 0,否则取 EA 子窗口。
主循环以 <chart> 结束符为边界,遇到 <object> 且下一行是 type 时,按数值分流——102 建 OBJ_LABEL,103 建 OBJ_BUTTON,106 建 OBJ_BITMAP_LABEL,107 建 OBJ_EDIT,110 建 OBJ_RECTANGLE_LABEL,任一步 LoopCreating 失败即中断。最后关文件并派发 CHARTEVENT_CHART_CHANGE,让面板重画。
LoopCreating 里用宏把 ObjectSetInteger / ObjectSetString 绑定到当前图表与对象名,调用方只需传 ENUM_OBJECT 类型,具体坐标、文本由模板里的后续字段填充。你在 MT5 里改这套代码时,重点核对 type 数值与对象枚举的映射,错一个号界面就可能缺一块。
if ((m_fp = FileOpen("Chart Trade\\IDE.tpl", FILE_BIN | FILE_READ)) == INVALID_HANDLE) class="kw">return class="kw">false; FileReadInteger(m_fp, SHORT_VALUE); for (m_CountObject = eRESULT; m_CountObject <= eEDIT_STOP; m_CountObject++) m_ArrObject[m_CountObject].szName = ""; m_SubWindow = ((m_IsFloating = bFloat) ? class="num">0 : GetIdSubWinEA()); m_szLine = ""; class="kw">while (m_szLine != "</chart>") { if (!FileReadLine()) class="kw">return class="kw">false; if (m_szLine == "<object>") { if (!FileReadLine()) class="kw">return class="kw">false; if (m_szLine == "type") { if (m_szValue == "class="num">102") if (!LoopCreating(OBJ_LABEL)) class="kw">return class="kw">false; if (m_szValue == "class="num">103") if (!LoopCreating(OBJ_BUTTON)) class="kw">return class="kw">false; if (m_szValue == "class="num">106") if (!LoopCreating(OBJ_BITMAP_LABEL)) class="kw">return class="kw">false; if (m_szValue == "class="num">107") if (!LoopCreating(OBJ_EDIT)) class="kw">return class="kw">false; if (m_szValue == "class="num">110") if (!LoopCreating(OBJ_RECTANGLE_LABEL)) class="kw">return class="kw">false; } } } FileClose(m_fp); DispatchMessage(CHARTEVENT_CHART_CHANGE, class="num">0, class="num">0, szMsgIDE[eLABEL_SYMBOL]); class="kw">return true; } class="type">bool LoopCreating(ENUM_OBJECT type) { class="macro">#define macro_SetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[c0].szName, A, B) class="macro">#define macro_SetString(A, B) ObjectSetString(Terminal.Get_ID(), m_ArrObject[c0].szName, A, B) class="type">int c0; class="type">bool b0; class="type">class="kw">string sz0 = m_szValue;