开发回放系统(第 77 部分):新 Chart Trade(四)·进阶篇
(2/3)·鼠标指标与控件点击偶发失灵、字符串协议如何切分字段,这篇把进阶坑位一次讲清
拖拽面板时怎么锁住图表滚动
在自定义悬浮面板里处理 MSG_TITLE_IDE 消息时,若鼠标横坐标 sx 小于 0,说明拖拽起点在标题区左侧,需要先清掉正在编辑的对象并关闭图表鼠标滚动,否则图表会跟着滚轮乱跑。 对应代码第315行调用 ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false),把当前图表 ID 的鼠标滚动直接禁用;随后用 sx = x - (m_Info.IsMaximized ? m_Info.x : m_Info.minx) 重算拖拽偏移,sy 同理取 y 减最大/最小化状态下的基准坐标。 真正落位置靠第319、320行:只有当 mx = x - sx 和 my = y - sy 都大于 0 时才写 OBJPROP_XDISTANCE / OBJPROP_YDISTANCE,负值时沿用旧坐标,避免面板被拖到图表外丢失锚点。 最大化与最小化两套坐标分开存(m_Info.x/y 与 m_Info.minx/miny),重启后面板位置才可能连续;外汇贵金属图表多品种切换频繁,这套隔离能减少悬浮窗跳变。
if ((m_Info.IsMaximized) && (sz < class="num">0)) CreateObjectEditable(obj = MSG_STOP_VALUE, m_Info.FinanceStop); class="kw">break; 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); } class="kw">break;
「图表事件到自定义信号的映射细节」
这段 switch 分支把面板发来的市场操作指令转成图表自定义事件。MSG_BUY_MARKET 直接映射到 evChartTradeBuy;MSG_SELL_MARKET 则做了防覆盖判断——若 ev 已是买入事件就保留,否则才赋值为 evChartTradeSell,避免同帧重复信号互相冲掉。 平仓分支只在窗口最大化且 sz<0 时拼装参数字符串:用 StringFormat 把事件号、品种、日/波段标记('D'/'S')、杠杆、止盈止损的点数化结果压成一条 "%d?%s?%c?%d?%.2f?%.2f" 格式串,随后 PrintFormat 打印、EventChartCustom 推到指定终端 ID。 分支收尾处若 sz 仍小于 0,会回写 sz=x 并调 AdjustTemplate 重排模板,MSG_NULL 时顺手清掉编辑对象。外汇与贵金属杠杆波动剧烈,这类自定义事件链路任一步错配都可能发不出单,建议在 MT5 策略测试器里把 PrintFormat 的输出抄出来比对。
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); } class="kw">break; } if (sz < class="num">0) { sz = x; AdjustTemplate(); if (obj == MSG_NULL) DeleteObjectEdit(); } }else {
◍ 图表事件里的参数回填逻辑
这段分支处理的是 MT5 图表对象编辑结束后的取值动作。当用户在面板里改完杠杆、止盈或止损的可编辑文本,程序通过 CHARTEVENT_OBJECT_ENDEDIT 拿到对象名,再用 StringToDouble 把文本内容转成数值。 关键的防呆写在 368–374 行:dvalue 小于等于 0 时直接沿用旧值,不会把仓位参数洗成无效数。止盈、止损走各自分支写进 m_Info.FinanceTake / FinanceStop,杠杆则额外用 MathFloor 向下取整再强转 short,避免小数杠杆进结构体。 AdjustTemplate() 在 375 行被调用,意味着每次改完参数都会重刷模板;随后立刻 ObjectDelete 删掉临时编辑框,obj 复位成 MSG_NULL。外汇和贵金属杠杆调整属于高风险操作,实际跟单前建议在策略测试器里先跑一遍这套事件链。 [CODE] sz = -1; if (sx > 0) { ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); sx = sy = -1; } break; case CHARTEVENT_OBJECT_ENDEDIT: 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 <= 0 ? m_Info.FinanceTake : dvalue); else if (obj == MSG_STOP_VALUE) m_Info.FinanceStop = (dvalue <= 0 ? m_Info.FinanceStop : dvalue); else m_Info.Leverage = (dvalue <= 0 ? m_Info.Leverage : (short)MathFloor(dvalue)); AdjustTemplate(); obj = MSG_NULL; ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); break; }[/CODE]
sz = -class="num">1; if (sx > class="num">0) { ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); sx = sy = -class="num">1; } class="kw">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">short)MathFloor(dvalue)); AdjustTemplate(); obj = MSG_NULL; ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); class="kw">break; }
图表对象点击后的状态分支处理
这段事件处理代码挂在 CHARTEVENT_OBJECT_CLICK 下,核心逻辑是先判断被点的对象是不是面板主图对象(m_Info.szObj_Chart),再确认左键命中,最后用 CheckMousePosition 反查鼠标落在哪个功能区块。 命中后走 switch 分发:MSG_DAY_TRADE 翻转日内交易开关 IsDayTrade,MSG_MAX_MIN 翻转窗口最大化标记 IsMaximized,两者都会顺手调 DeleteObjectEdit 清掉旧编辑框;MSG_LEVERAGE_VALUE、MSG_TAKE_VALUE、MSG_STOP_VALUE 则分别拉起杠杆、止盈金额、止损金额的可编辑框。
- 行有个收口判断:只要 obj 不是 MSG_NULL,就跑 AdjustTemplate 重排模板。这意味着任何有效点击都会触发一次界面重绘,外汇和贵金属品种在高杠杆下误触可能瞬间改掉风控参数,属于高风险操作,建议先在模拟盘验证。
另一个容易忽略的坑在 404 行:CHARTEVENT_OBJECT_DELETE 里若删掉的是面板主对象,直接 macro_CloseIndicator 关指标。复制下面代码到 MT5 里断点跟一遍,能看清对象生命周期怎么绑死指标退出。
class="kw">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">short)lparam, y = (class="type">short)dparam)) { case MSG_DAY_TRADE: m_Info.IsDayTrade = (m_Info.IsDayTrade ? class="kw">false : true); DeleteObjectEdit(); class="kw">break; case MSG_MAX_MIN: m_Info.IsMaximized = (m_Info.IsMaximized ? class="kw">false : true); DeleteObjectEdit(); class="kw">break; case MSG_LEVERAGE_VALUE: CreateObjectEditable(obj, m_Info.Leverage); class="kw">break; case MSG_TAKE_VALUE: CreateObjectEditable(obj, m_Info.FinanceTake); class="kw">break; case MSG_STOP_VALUE: CreateObjectEditable(obj, m_Info.FinanceStop); class="kw">break; } if (obj != MSG_NULL) AdjustTemplate(); class="kw">break; case CHARTEVENT_OBJECT_DELETE: if (sparam == m_Info.szObj_Chart) macro_CloseIndicator(C_Terminal::ERR_Unknown); class="kw">break; }