开发回放系统(第 45 部分):Chart Trade 项目(四)·进阶篇
📘

开发回放系统(第 45 部分):Chart Trade 项目(四)·进阶篇

第 2/3 篇

◍ 在图表上挂一块可编辑子窗口

做图形化交易面板时,常需要在主图之外再开一块独立区域来放按钮、输入框和状态显示。下面这段逻辑用 OBJ_CHART 对象在终端里现场画出名为「Chart Trade IDE」的子图表,并把它的编辑副本命名为「Chart Trade IDE > Edit」。 关键坐标与尺寸全部来自 m_Info 结构体里的 x、y 以及传入的 w、h 参数:XDistance 与 YDistance 定位置,XSize 与 YSize 定宽高。注意第 46、47 行把日期轴和价格轴都关了(OBJPROP_DATE_SCALE / OBJPROP_PRICE_SCALE 设为 false),否则子窗口会自带坐标轴干扰布局。 创建完后用 ObjectGetInteger 取 OBJPROP_CHART_ID,把真实窗口句柄写回 m_Info.WinHandle,后续所有鼠标命中测试都靠这个句柄。开 MT5 把下面代码塞进 EA 的 OnInit 之前先确认 m_Info.x/y 有值,否则窗口会贴到 (0,0)。外汇与贵金属杠杆高,这类面板只负责交互,下单逻辑务必另做风控。

MQL5 / C++
}
         }Regions[MSG_NULL];
      }m_Info;
class=class="str">"cmt">//+------------------------------------------------------------------+
   C_Mouse *m_Mouse;
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void CreateWindowRAD(class="type">int w, class="type">int h)
      {
         m_Info.szObj_Chart = "Chart Trade IDE";
         m_Info.szObj_Editable = m_Info.szObj_Chart + " > Edit";
         ObjectCreate(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJ_CHART, class="num">0, class="num">0, class="num">0);
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, m_Info.x);
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, m_Info.y);
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XSIZE, w);
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YSIZE, h);
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_DATE_SCALE, false);
         ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_PRICE_SCALE, false);
         m_Info.WinHandle = ObjectGetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_CHART_ID);
      };
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void AdjustEditabled(C_AdjustTemplate &Template, class="type">bool bArg)
      {
         for (eObjectsIDE c0 = class="num">0; c0 <= MSG_STOP_VALUE; c0++)
            if (bArg)
            {

「区域样式与模板字段的空值兜底」

面板里每个区域(c0 枚举项)在初始化时先尝试从模板读取 bgcolor、fontsz、fontnm 三个键。若模板里没有对应键,代码走 NULL 分支,把这三个属性留空交给后续默认逻辑处理,避免强转报错。 当键存在时,bgcolor 和 fontsz 用 StringToInteger 转成整型再强转 color / int,FontName 直接吃字符串。实测若模板写错成非数字文本,StringToInteger 返回 0,背景色会静默变成黑(0),这是调试时容易忽略的坑。 AdjustTemplate 用宏 macro_AddAdjust 批量给模板对象补 size_x、size_y、pos_x、pos_y 四个空键。首次加载(bFirst)或动态重建模板时,先确保这些几何键存在,后续才能安全 Get 而不触发越界。 别把 NULL 当免死金牌:模板缺键时虽然不崩,但区域可能以默认黑底白字渲染,跟你的深色主题面板撞色。开 MT5 把这段塞进 EA 初始化,故意删掉一个 fontnm 键看渲染差异就明白了。

MQL5 / C++
Template.Add(EnumToString(c0), "bgcolor", NULL);
Template.Add(EnumToString(c0), "fontsz", NULL);
Template.Add(EnumToString(c0), "fontnm", NULL);
}
else
{
m_Info.Regions[c0].bgcolor = (class="type">color) StringToInteger(Template.Get(EnumToString(c0), "bgcolor"));
m_Info.Regions[c0].FontSize = (class="type">int) StringToInteger(Template.Get(EnumToString(c0), "fontsz"));
m_Info.Regions[c0].FontName = Template.Get(EnumToString(c0), "fontnm");
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void AdjustTemplate(const class="type">bool bFirst = false)
{
class="macro">#define macro_AddAdjust(A) {\
(*Template).Add(A, "size_x", NULL);\
(*Template).Add(A, "size_y", NULL);\
(*Template).Add(A, "pos_x", NULL);\
(*Template).Add(A, "pos_y", NULL);\
}

正文

<span class="number">076</span>. <span class="preprocessor">#define </span>macro_GetAdjust(A) {&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&

◍ 面板初始化时的杠杆与风控字段回填

这段逻辑跑在面板首次绘制或参数刷新时,先把杠杆做了兜底:若 m_Info.Leverage 读到 ≤0 就强制置 1,避免后续点值换算出现除零或负杠杆。 接着用 macro_PointsToFinance 与 FinanceToPoints 双向换算,把止盈、止损的金额按当前杠杆重算一遍,MathAbs 保证只取绝对值写入面板描述字段。 从 095 到 100 行是往 Template 里塞展示数据:交易品种、杠杆值、止盈止损金额、日内交易标记(IsDayTrade)、最大化标记(IsMaximized)都以字符串或 0/1 状态写入,101 行 Execute 一次性渲染。 首次绘制(bFirst 为真)会遍历 eObjectsIDE 枚举到 MSG_CLOSE_POSITION,调用 macro_GetAdjust 做控件坐标自适应,并把标题区宽度锁到最大化按钮的 x 位置;108、109 行根据 IsMaximized 切换图表对象高度(210 或标题高+6)与水平距离(m_Info.x 或 minx)。外汇与贵金属品种杠杆波动大,面板里看到杠杆被置 1 时,优先去 MT5 账户规格里核对真实杠杆。

MQL5 / C++
m_Info.Leverage = (m_Info.Leverage <= class="num">0 ? class="num">1 : m_Info.Leverage);
m_Info.FinanceTake = macro_PointsToFinance(FinanceToPoints(MathAbs(m_Info.FinanceTake), m_Info.Leverage));
m_Info.FinanceStop = macro_PointsToFinance(FinanceToPoints(MathAbs(m_Info.FinanceStop), m_Info.Leverage));
(*Template).Add("MSG_NAME_SYMBOL", "descr", GetInfoTerminal().szSymbol);
(*Template).Add("MSG_LEVERAGE_VALUE", "descr", (class="type">class="kw">string)m_Info.Leverage);
(*Template).Add("MSG_TAKE_VALUE", "descr", (class="type">class="kw">string)m_Info.FinanceTake);
(*Template).Add("MSG_STOP_VALUE", "descr", (class="type">class="kw">string)m_Info.FinanceStop);
(*Template).Add("MSG_DAY_TRADE", "state", (m_Info.IsDayTrade ? "class="num">1" : "class="num">0"));
(*Template).Add("MSG_MAX_MIN", "state", (m_Info.IsMaximized ? "class="num">1" : "class="num">0"));
(*Template).Execute();
if (bFirst)
{
   for (eObjectsIDE c0 = class="num">0; c0 <= MSG_CLOSE_POSITION; c0++) macro_GetAdjust(c0);
   m_Info.Regions[MSG_TITLE_IDE].w = m_Info.Regions[MSG_MAX_MIN].x;
   AdjustEditabled(Template, false);
};
ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YSIZE, (m_Info.IsMaximized ? class="num">210 : m_Info.Regions[MSG_TITLE_IDE].h + class="num">6));
ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, (m_Info.IsMaximized ? m_Info.x : m_Info.minx));

「用坐标命中图表上的交互热区」

面板贴好模板后,下一步是判断鼠标落在哪个控件区域。CheckMousePosition 接收屏幕坐标 x、y,遍历从 0 到 MSG_CLOSE_POSITION 的所有热区枚举,用 m_Info.Regions[c0] 里存好的相对偏移和宽高做命中测试。 最大化与最小化状态会影响基准坐标:代码里用三元表达式在 IsMaximized 为真时取 m_Info.x / m_Info.y,否则退回 m_Info.minx / m_Info.miny,再各自加上 Region 的 x、y 得到热区左上角,叠加 w、h 得到右下角。 判定逻辑很直白——若 x 在 xi 与 xf 之间且 y 在 yi 与 yf 之间,直接 return 当前枚举值;全部轮完没命中就返回 MSG_NULL。这套坐标映射在 MT5 里实测:Eu 和美黄金 XAUUSD 图表因为边框像素差,minx/miny 通常需要你跑一次 Print 校准,外汇与贵金属杠杆高,改坐标参数前先在模拟盘验证。 让小布替你跑这套 把 Regions 数组打印出来,对比你机器上实际图表客户区,能省掉一半误触。

MQL5 / C++
class="type">int xi, yi, xf, yf;

for (eObjectsIDE c0 = class="num">0; c0 <= MSG_CLOSE_POSITION; c0++)
{
   xi = (m_Info.IsMaximized ? m_Info.x : m_Info.minx) + m_Info.Regions[c0].x;
   yi = (m_Info.IsMaximized ? m_Info.y : m_Info.miny) + m_Info.Regions[c0].y;
   xf = xi + m_Info.Regions[c0].w;
   yf = yi + m_Info.Regions[c0].h;
   if ((x > xi) && (y > yi) && (x < xf) && (y < yf)) class="kw">return c0;
}
class="kw">return MSG_NULL;

在图表区域里生成可编辑文本控件

想在 MT5 自定义面板的某个区域里放一个能改字的输入框,用模板函数 CreateObjectEditable 比反复手敲 ObjectCreate 省事得多。它先按终端 ID 删掉旧对象,再建一个 OBJ_EDIT 类型的图形,坐标直接吃区域结构体的偏移量。 代码里 XDISTANCE、YDISTANCE 都加了 3 像素内缩,YSIZE 和 XSIZE 直接取区域宽高,FONT_SIZE 比区域设定小 1,避免字撑破边框。最后把传入的 value 强转成 string 写进 OBJPROP_TEXT 并 ChartRedraw,界面才即时刷新。 RestoreState 这类配套函数通常负责把序列化后的 double 联合体读回结构体,布尔返回值标记还原是否成功。外汇与贵金属图表挂 EA 做这种控件时,终端卡顿可能让 ChartRedraw 延迟,属正常概率现象,别误判成逻辑崩了。

MQL5 / C++
class="kw">template <class="kw">typename T>
class="type">void CreateObjectEditable(eObjectsIDE arg, T value)
{
   class="type">long id = GetInfoTerminal().ID;
   ObjectDelete(id, m_Info.szObj_Editable);
   CreateObjectGraphics(m_Info.szObj_Editable, OBJ_EDIT, clrBlack, class="num">0);
   ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_XDISTANCE, m_Info.Regions[arg].x + m_Info.x + class="num">3);
   ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_YDISTANCE, m_Info.Regions[arg].y + m_Info.y + class="num">3);
   ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_XSIZE, m_Info.Regions[arg].w);
   ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_YSIZE, m_Info.Regions[arg].h);
   ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_BGCOLOR, m_Info.Regions[arg].bgcolor);
   ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_ALIGN, ALIGN_CENTER);
   ObjectSetInteger(id, m_Info.szObj_Editable, OBJPROP_FONTSIZE, m_Info.Regions[arg].FontSize - class="num">1);
   ObjectSetString(id, m_Info.szObj_Editable, OBJPROP_FONT, m_Info.Regions[arg].FontName);
   ObjectSetString(id, m_Info.szObj_Editable, OBJPROP_TEXT, (class="type">class="kw">string)value);
   ChartRedraw();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool RestoreState(class="type">void)
{
   uCast_Double info;
   class="type">bool bRet;

常见问题

给模板字段设默认值,空值时不读配置直接套用基础样式,避免子窗口渲染崩坏。
在面板构造阶段读取账户与品种风控参数,直接写入对应文本控件内容,省去手动输入。
可以,小布能自动扫描坐标命中逻辑与控件生成代码,标出热区重叠和空值风险,你只看结果。
多是坐标换算没扣除子窗口偏移,热区判定应基于相对图表客户区的局部坐标而非绝对坐标。
热区未覆盖控件整块区域或事件优先级被面板吞掉,需把命中判断放到前置消息钩子里。