开发回放系统(第 76 部分):新 Chart Trade(三)·进阶篇
「用鼠标拖拽副图窗口的坐标重算」
在 MT5 自定义指标里接管 CHARTEVENT_MOUSE_MOVE 后,左键点击标题区就能把浮动子图拖到任意位置。下面这段逻辑只处理 MSG_TITLE_IDE 分支:先判 sx<0 说明刚进入拖拽,清掉旧编辑对象并关掉图表鼠标滚动,避免滚轮干扰位移。 坐标换算靠窗口是否最大化来区分:最大化时用 m_Info.x/y,最小化态用 m_Info.minx/miny。sx、sy 存的是点击点相对窗口左上角的偏移,后续 mx=x-sx、my=y-sy 才是对象真正要设的 XDISTANCE / YDISTANCE,且都做了 >0 的保护,负数不写回。 第 302、303 行直接 ObjectSetInteger 改 OBJPROP_XDISTANCE 和 OBJPROP_YDISTANCE,实时跟手。304–312 行把新坐标回写 m_Info 结构体,最大化与还原态各走各的赋值,下次重绘不会跳回原位。外汇与贵金属图表多屏布局下,这种拖拽逻辑能省掉反复手动摆窗口的时间,但 MT5 脚本误操作可能让对象飞出可视区,高风险环境里建议先在模拟盘验证。
if (b1 || b2 || b3 || b4) AdjustTemplate(); break; case CHARTEVENT_MOUSE_MOVE: if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) class="kw">switch (CheckMousePosition(x = (class="type">class="kw">short)lparam, y = (class="type">class="kw">short)dparam)) { case MSG_TITLE_IDE: if (sx < class="num">0) { DeleteObjectEdit(); ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, class="kw">false); sx = x - (m_Info.IsMaximized ? m_Info.x : m_Info.minx); sy = y - (m_Info.IsMaximized ? m_Info.y : m_Info.miny); } if ((mx = x - sx) > class="num">0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, mx); if ((my = y - sy) > class="num">0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, my); if (m_Info.IsMaximized) { m_Info.x = (mx > class="num">0 ? mx : m_Info.x); m_Info.y = (my > class="num">0 ? my : m_Info.y); }else { m_Info.minx = (mx > class="num">0 ? mx : m_Info.minx); m_Info.miny = (my > class="num">0 ? my : m_Info.miny); }
图表事件回传与平仓指令的边界处理
这段逻辑处在自定义面板和图表主程序通信的末端:当收到 MSG_CLOSE_POSITION 且面板处于最大化、sz 小于 0 时,才允许把平仓参数打包外发。 注意 316–317 行的写法,MSG_SELL_MARKET 没有独立 break,而是借 ev 变量判断:若前面已置为 evChartTradeBuy 则保持,否则才赋 evChartTradeSell,这种 fall-through 容易在改 case 顺序时引入隐性 bug。 321–322 行用 StringFormat 拼出 "事件?品种?周期标记?杠杆?止盈点?止损点" 的串,其中 FinanceToPoints 把资金量换算成点数,保留两位小数;323 行先 PrintFormat 打到日志,325 行才通过 EventChartCustom 推给图表 ID。
- 行 else 分支里,若 sx 大于 0 则重开 CHART_MOUSE_SCROLL 并复位坐标,说明非平仓场景下要把图表交互权交还用户。外汇与贵金属杠杆高,这类自动平仓触发一旦参数错配,可能瞬间放大回撤。
case MSG_BUY_MARKET: ev = evChartTradeBuy; case MSG_SELL_MARKET: ev = (ev != evChartTradeBuy ? evChartTradeSell : ev); case MSG_CLOSE_POSITION: if ((m_Info.IsMaximized) && (sz < class="num">0)) { class="type">class="kw">string szTmp = StringFormat("%d?%s?%c?%d?%.2f?%.2f", ev, _Symbol, (m_Info.IsDayTrade ? &class="macro">#x27;D&class="macro">#x27; : &class="macro">#x27;S&class="macro">#x27;), m_Info.Leverage, FinanceToPoints(m_Info.FinanceTake, m_Info.Leverage), FinanceToPoints(m_Info.FinanceStop, m_Info.Leverage)); PrintFormat("Send %s - Args( %s )", EnumToString((EnumEvents) ev), szTmp); sz = x; EventChartCustom(GetInfoTerminal().ID, ev, class="num">0, class="num">0, szTmp); DeleteObjectEdit(); } break; }else { sz = -class="num">1; if (sx > class="num">0) { ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); sx = sy = -class="num">1; }
◍ 图表上改参数如何落进内存
在 MT5 自定义面板里直接编辑文本框,本质是靠 CHARTEVENT_OBJECT_ENDEDIT 事件接管用户输入。代码 339 行起监听该事件,随后用内嵌 switch 区分三个可编辑字段:杠杆、止盈金额、止损金额,分别对应 MSG_LEVERAGE_VALUE、MSG_TAKE_VALUE、MSG_STOP_VALUE。
- 行先用 ObjectGetString 把文本框内容读成字符串,再经 StringToDouble 转成双精度数。这里有个硬约束:若用户输入小于等于 0,则保留原值不覆盖(346–351 行),杠杆还会被 MathFloor 向下取整并强转 short,避免非整数杠杆写进结构。
字段写回后立刻调 AdjustTemplate() 重绘模板,并删除临时编辑框、把 obj 置为 MSG_NULL(352–354 行)。这一套下来,你在图表上敲个数字,后台结构体 m_Info 里的 FinanceTake、FinanceStop、Leverage 就实时变了,不用重开面板。
- 行之后的 CHARTEVENT_OBJECT_CLICK 则处理左键点击图表区,由 CheckMousePosition 根据 x/y 坐标判定点到了哪个按钮(如 MSG_DAY_TRADE),把 obj 交回上层逻辑分发。外汇与贵金属杠杆调节会放大波动风险,面板改值前先确认账户风控阈值。
}
break;
case CHARTEVENT_OBJECT_ENDEDIT:
class="kw">switch (obj)
{
case MSG_LEVERAGE_VALUE:
case MSG_TAKE_VALUE:
case MSG_STOP_VALUE:
dvalue = StringToDouble(ObjectGetString(GetInfoTerminal().ID, m_Info.szObj_Editable, OBJPROP_TEXT));
if (obj == MSG_TAKE_VALUE)
m_Info.FinanceTake = (dvalue <= class="num">0 ? m_Info.FinanceTake : dvalue);
else if (obj == MSG_STOP_VALUE)
m_Info.FinanceStop = (dvalue <= class="num">0 ? m_Info.FinanceStop : dvalue);
else
m_Info.Leverage = (dvalue <= class="num">0 ? m_Info.Leverage : (class="type">class="kw">short)MathFloor(dvalue));
AdjustTemplate();
obj = MSG_NULL;
ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable);
break;
}
break;
case CHARTEVENT_OBJECT_CLICK:
if (sparam == m_Info.szObj_Chart) if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) class="kw">switch (obj = CheckMousePosition(x = (class="type">class="kw">short)lparam, y = (class="type">class="kw">short)dparam))
{
case MSG_DAY_TRADE:「面板交互里的消息分支处理」
这段 switch 逻辑集中在图表事件回调里,负责把用户在面板上的点击、数值修改、对象删除翻译成状态切换与重绘。日交易开关与最大化状态都用三元取反实现,每次切换先清掉可编辑对象再 break,避免旧控件残留。 MSG_LEVERAGE_VALUE、MSG_TAKE_VALUE、MSG_STOP_VALUE 三个分支直接拿对应成员(杠杆、盈利目标、止损额度)调 CreateObjectEditable 生成可编辑框,说明这些参数在运行时允许手动改写,写完后统一走 AdjustTemplate 做模板对齐。 对象删除事件只盯一个特定对象名 szObj_Chart,一旦被删就调 macro_CloseIndicator 带 ERR_Unknown 退出指标,相当于把面板主图对象当成了指标存活的硬依赖。 最后不管哪个分支,只要 obj 不是 MSG_NULL 就 AdjustTemplate,再 ChartRedraw 强制重绘一次。开 MT5 把这段塞进自己的面板类,改 szObj_Chart 名字就能验证删除即退出的绑定是否生效。
class="num">362. m_Info.IsDayTrade = (m_Info.IsDayTrade ? class="kw">false : true); class="num">363. DeleteObjectEdit(); class="num">364. break; class="num">365. case MSG_MAX_MIN: class="num">366. m_Info.IsMaximized = (m_Info.IsMaximized ? class="kw">false : true); class="num">367. DeleteObjectEdit(); class="num">368. break; class="num">369. case MSG_LEVERAGE_VALUE: class="num">370. CreateObjectEditable(obj, m_Info.Leverage); class="num">371. break; class="num">372. case MSG_TAKE_VALUE: class="num">373. CreateObjectEditable(obj, m_Info.FinanceTake); class="num">374. break; class="num">375. case MSG_STOP_VALUE: class="num">376. CreateObjectEditable(obj, m_Info.FinanceStop); class="num">377. break; class="num">378. } class="num">379. if (obj != MSG_NULL) AdjustTemplate(); class="num">380. break; class="num">381. case CHARTEVENT_OBJECT_DELETE: class="num">382. if (sparam == m_Info.szObj_Chart) macro_CloseIndicator(C_Terminal::ERR_Unknown); class="num">383. break; class="num">384. } class="num">385. ChartRedraw(); class="num">386. } class="num">387. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">388. }; class="num">389. class=class="str">"cmt">//+------------------------------------------------------------------+
程序间通信这层纸迟早要捅破
这篇写到这儿看似收住了,但有个坑必须点明:应用间的通信协议才是 Chart Trade 真正难啃的第二块骨头。原文第 321 行直接建好了要用的协议,可光贴一行代码,你依然不知道两个程序怎么对话,照搬也调不通。 现实是 MT5 出来这么多年,极少有人用文中这种轻量思路,多数人要么从零造轮子,要么堆出庞大难维护的程序。拿到这里分享的拆解,你反而能写出更小、更易调试改动的模块——外汇与贵金属自动化本就高风险,能少写一千行就少一分踩雷概率。 剩下那几个事件(CHARTEVENT_OBJECT_ENDEDIT、CHARTEVENT_OBJECT_CLICK、CHARTEVENT_OBJECT_DELETE)代码比前面简单,自行翻源码就能懂。下一篇会拆第 321 行为何是那个格式,以及它怎么让 EA 根据用户点按钮去发单。