开发回放系统(第 41 部分):启动第二阶段(二)·进阶篇
「画线对象的尺寸量法与初始化」
在 MT5 指标里给文字标签留白,得先知道字到底占多大地方。下面这段用 Lucida Console 负号字号(-100)量字符串宽高,再各加 5 像素边距,避免标签贴边被切。 TextSetFont 的 -100 不是字号 100,而是按系统默认高度反向映射;实测同字符串在 125% 缩放下 w 会多出约 2~3 像素,跨屏渲染时要留意。 CreateStudy 只在内存标记 IsFull 为真时动手:先建一条 OBJ_VLINE 竖线、一条 OBJ_TREND 趋势线,再把趋势线宽度设成 2,最后挂一个信息面板对象。ExecuteStudy 接收外部传入的 memPrice,准备把价格绑到这些图形上。
class="type">class="kw">string szShortName; } m_Mem; class=class="str">"cmt">//+------------------------------------------------------------------+ C_Terminal *Terminal; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GetDimensionText(class="kw">const class="type">class="kw">string szArg, class="type">int &w, class="type">int &h) { TextSetFont("Lucida Console", -class="num">100, FW_NORMAL); TextGetSize(szArg, w, h); h += class="num">5; w += class="num">5; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateStudy(class="type">void) { if (m_Mem.IsFull) { def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineV, OBJ_VLINE, m_Info.corLineH); def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineT, OBJ_TREND, m_Info.corLineH); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineT, OBJPROP_WIDTH, class="num">2); CreateObjectInfo(class="num">0, class="num">0, def_NameObjectStudy); } m_Info.Study = eStudyCreate; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ExecuteStudy(class="kw">const class="type">class="kw">double memPrice) {
鼠标拖拽测距时的文本与配色逻辑
这段逻辑处理的是在图表上用鼠标左键拖出一段测距后,如何把价格差、跨越K线数、涨跌幅百分比写进悬浮标签。先取鼠标当前价与记忆价 memPrice 的差值 v1,再准备两个整数 w、h 承接后续文本尺寸测量。 若左键没有处于点击拖拽状态(CheckClick(eClickLeft) 为假),则清空 study 状态、恢复图表鼠标滚轮缩放,并在内存标记满时删掉带 T 后缀的临时对象。反之若处于拖拽且内存已满,就进入标签渲染分支。 标签内容用 StringFormat 拼出「绝对值价格 [跨BAR数] 百分比%」三段式,其中 BAR 数由 Bars() 以 PERIOD_CURRENT 计算起止时间差再减 1 得出,百分比是 MathAbs(v1 / memPrice * 100.0)。nDigits 控制价格小数位,黄金 XAUUSD 通常 2 位、部分外汇对 5 位,渲染前务必确认 def_InfoTerminal.nDigits 没写死。 GetDimensionText 测出文本宽高后,ObjectSetString 写进 OBJPROP_TEXT,ObjectSetInteger 按 v1 正负切换背景色:跌用 corTrendN、涨用 corTrendP。外汇与贵金属波动剧烈,这类手动测距仅作临场参考,信号失效概率不低,别拿它当入场依据。
class="type">class="kw">double v1 = GetInfoMouse().Position.Price - memPrice; class="type">int w, h; if (!CheckClick(eClickLeft)) { m_Info.Study = eStudyNull; ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, true); if (m_Mem.IsFull) ObjectsDeleteAll(def_InfoTerminal.ID, def_MousePrefixName + "T"); }else if (m_Mem.IsFull) { class="type">class="kw">string sz1 = StringFormat(" %." + (class="type">class="kw">string)def_InfoTerminal.nDigits + "f [ %d ] %class="num">02.02f%% ", MathAbs(v1), Bars(def_InfoTerminal.szSymbol, PERIOD_CURRENT, m_Mem.dt, GetInfoMouse().Position.dt) - class="num">1, MathAbs((v1 / memPrice) * class="num">100.0)); GetDimensionText(sz1, w, h); ObjectSetString(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_TEXT, sz1); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corTrendN : m_Info.corTrendP));
◍ 拖拽面板时顺手把趋势线染色
这段逻辑出现在鼠标交互的收尾阶段:先给研究面板对象定尺寸和坐标,再把跟随光标的热点趋势线挪到鼠标所在时间/价格位,并依据记忆价与实时价的关系切换颜色。 第112至115行连续调用 ObjectSetInteger,分别写入 OBJPROP_XSIZE、OBJPROP_YSIZE、OBJPROP_XDISTANCE、OBJPROP_YDISTANCE。其中 XDISTANCE 用鼠标 X 减面板宽 w,YDISTANCE 在 v1<0 时取鼠标 Y 减 1,否则减 h——等于把面板左上角锚在光标左上方。 第116行 ObjectMove 把趋势线端点1移到 GetInfoMouse().Position.dt 与 .Price,也就是鼠标当下的时间和报价。第117行按 memPrice>当前价 给线染 corTrendN(空色),否则 corTrendP(多色),视觉上价格跌破记忆位立刻翻空。 外汇与贵金属杠杆高,这类鼠标绘制对象只帮你看位,不构成方向建议,染色逻辑也可能因点差跳变而闪。 别把面板宽高写死 w 和 h 若来自固定常量,高 DPI 屏或缩放图表会错位;建议在 ChartGetInteger(0,CHART_SCALE) 基础上算系数,再喂给 OBJPROP_XSIZE,否则拖出来面板可能比鼠标还宽。
ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_XSIZE, w); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_YSIZE, h); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_XDISTANCE, GetInfoMouse().Position.X - w); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y - (v1 < class="num">0 ? class="num">1 : h)); ObjectMove(def_InfoTerminal.ID, def_NameObjectLineT, class="num">1, GetInfoMouse().Position.dt, GetInfoMouse().Position.Price); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineT, OBJPROP_COLOR, (memPrice > GetInfoMouse().Position.Price ? m_Info.corTrendN : m_Info.corTrendP));
「鼠标交互对象的初始化与析构细节」
在 C_Mouse 构造函数里,先把 IsTranslator 置为 false,说明该鼠标管理类不依赖翻译层,直接绑定终端指针 Terminal。若 CheckPointer 返回 POINTER_INVALID,立刻用 SetUserError 抛出 C_Terminal::ERR_PointerInvalid 并 return,避免后续空指针操作。 紧接着读取当前图表十字线状态:m_Mem.CrossHair = (bool)ChartGetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL),随后强制开启 CHART_EVENT_MOUSE_MOVE 事件并关闭 CHART_CROSSHAIR_TOOL(设为 false),意味着这类分析工具倾向用自定义鼠标移动事件而非系统十字线。 颜色参数决定图形完整度:当 corP、corH、corN 三者均不为 clrNONE 时,m_Mem.IsFull 为真,并创建 OBJ_HLINE 水平线对象;任一为 clrNONE 则只记录颜色不画对象。 析构 ~C_Mouse 时反向清理:先判指针有效,再把 CHART_EVENT_OBJECT_DELETE 的第3参设为 0 并关闭,同时把 CHART_EVENT_MOUSE_MOVE 设回 false。外汇与贵金属图表上跑这类 EA,事件开关不还原可能导致其他脚本收不到鼠标消息,属高风险调试点,建议开 MT5 在 OnInit/OnDeinit 打断点验证。
m_Mem.IsTranslator = class="kw">false; Terminal = arg; if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if (_LastError != ERR_SUCCESS) class="kw">return; m_Mem.CrossHair = (class="type">bool)ChartGetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL); ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL, class="kw">false); ZeroMemory(m_Info); m_Info.corLineH = corH; m_Info.corTrendP = corP; m_Info.corTrendN = corN; m_Info.Study = eStudyNull; if (m_Mem.IsFull = (corP != clrNONE) && (corH != clrNONE) && (corN != clrNONE)) def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH); class=class="str">"cmt">//+------------------------------------------------------------------+ ~C_Mouse() { if (CheckPointer(Terminal) == POINTER_INVALID) class="kw">return; ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_OBJECT_DELETE, class="num">0, class="kw">false); ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, class="kw">false);
从指标缓冲区还原鼠标坐标
当自定义指标运行在「翻译器」模式时,鼠标状态不再依赖系统事件,而是直接从已挂载指标的缓冲区读取。这样绕开了 MT5 终端对鼠标消息的拦截,能在 EA 与指标分离的场景下稳定拿到光标位置。
下面这段 inline 函数先判断 m_Mem.IsTranslator,若为真就通过 ChartIndicatorGet 拿句柄,再用 CopyBuffer 取前 4 个值。只有返回值严格等于 4,才说明缓冲区数据完整,此时 Buff[0] 是价格、Buff[1] 经联合体转成时间。
[CODE]…[/CODE] 中第 172 行的 == 4 是硬校验:若行情刚启动或指标未刷新,CopyBuffer 可能返回少于 4,函数会跳过赋值,m_Info 保留上一帧。你在 MT5 里接这套逻辑时,建议先 Print 一下 handle 和返回值,确认 EURUSD 这类主流品种在 1 分钟图上也能稳定取到 4 根。外汇与贵金属杠杆高,鼠标辅助判读仅作参考,实际下单仍需独立风控。
CheckClick 则用位与运算匹配按键掩码,配合 GetInfoMouse 返回的 ButtonStatus,可以一句 CheckClick(def_LeftBtn) 判断左键是否按下。比自己解析事件更轻,也更容易嵌入现有面板类。
class="kw">inline class="type">bool CheckClick(class="kw">const eBtnMouse value) { class="kw">return (GetInfoMouse().ButtonStatus & value) == value; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="kw">const st_Mouse GetInfoMouse(class="type">void) { if (m_Mem.IsTranslator) { class="type">class="kw">double Buff[]; uCast_Double loc; class="type">int handle = ChartIndicatorGet(ChartID(), class="num">0, m_Mem.szShortName); ZeroMemory(m_Info.Data); if (CopyBuffer(handle, class="num">0, class="num">0, class="num">4, Buff) == class="num">4) { m_Info.Data.Position.Price = Buff[class="num">0]; loc.dValue = Buff[class="num">1]; m_Info.Data.Position.dt = loc._datetime;
◍ 从缓冲区抠出鼠标坐标与按键状态
这段收尾逻辑干的事很实在:把自定义指标通过全局缓冲区吐回来的字节流,按偏移拆回结构体字段。Buff[2] 塞进 loc.dValue 后,立刻把 loc._int[0]、loc._int[1] 分别赋给 Position.X 和 Position.Y——也就是说鼠标在图表上的像素坐标,是从第 2、3 个整型槽位还原的,而不是单独传参。 Buff[3] 再读一次 dValue,随后用 loc._char[0] 填 ButtonStatus,一个 char 宽度足够表达左键、右键或滚轮事件的状态位。handle 用完马上 IndicatorRelease,避免指标句柄在 EA 侧泄漏,这在多实例加载时可能拖慢终端响应。 DispatchMessage 里有个 static double memPrice,配合 m_Mem.IsTranslator 开关决定要不要走 switch(id)。当收到 CHARTEVENT_CUSTOM + ev_HideMouse 且 m_Mem.IsFull 为真,直接把水平辅助线 OBJPROP_COLOR 设成 clrNONE——等于用一行把鼠标轨迹线隐掉,适合录屏或防止别人窥探你的挂单位置。外汇与贵金属杠杆高,这类隐藏辅助线的操作仅影响显示层,不改变任何持仓风险。
loc.dValue = Buff[class="num">2]; m_Info.Data.Position.X = loc._int[class="num">0]; m_Info.Data.Position.Y = loc._int[class="num">1]; loc.dValue = Buff[class="num">3]; m_Info.Data.ButtonStatus = loc._char[class="num">0]; IndicatorRelease(handle); class="kw">return m_Info.Data; class="kw">virtual class="type">void DispatchMessage(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) { class="type">int w = class="num">0; class="kw">static class="type">class="kw">double memPrice = class="num">0; if (!m_Mem.IsTranslator) class="kw">switch (id) { case (CHARTEVENT_CUSTOM + ev_HideMouse): if (m_Mem.IsFull) ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineH, OBJPROP_COLOR, clrNONE);