开发回放系统(第 54 部分):第一个模块的诞生·进阶篇
◍ 按钮对象的初始化与属性注入
在 MT5 面板类里,持仓信息通常塞进一个结构体:Price 存触发价、dt 存时间、ButtonStatus 记按钮状态、ExecStudy 控制是否执行分析、TimeDevice 留作设备时间戳。这个结构配合 protected 下的 CreateObjToStudy 方法,决定了图形按钮怎么被创建。 CreateObjToStudy 先判断 m_Mem.szShortName 是否非空,若已赋值就直接 return,避免重复建对象。随后用 CreateObjectGraphics 以 OBJ_BUTTON 类型生成按钮,背景默认 clrNONE。 紧接着一连串 ObjectSetInteger / ObjectSetString 把按钮钉死在左上角:状态置 true、边框与文字色 clrBlack、字体 Lucida Console、字号 10、X 偏移由入参 x 决定。想改按钮观感,调这几个常量比改逻辑更快。 外汇与贵金属波动剧烈,这类面板只做辅助显示,任何信号都只是概率倾向,实盘前请在策略测试器里跑一遍验证参数。
class="type">class="kw">double Price; class="type">class="kw">datetime dt; }Position; class="type">uint ButtonStatus; class="type">bool ExecStudy; class="type">class="kw">datetime TimeDevice; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">protected: class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateObjToStudy(class="type">int x, class="type">int w, class="type">class="kw">string szName, class="type">color backColor = clrNONE) class="kw">const { if (m_Mem.szShortName != NULL) class="kw">return; CreateObjectGraphics(szName, OBJ_BUTTON, clrNONE); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_STATE, true); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BORDER_COLOR, clrBlack); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_COLOR, clrBlack); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BGCOLOR, backColor); ObjectSetString(GetInfoTerminal().ID, szName, OBJPROP_FONT, "Lucida Console"); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_FONTSIZE, class="num">10); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XDISTANCE, x);
用结构体管住鼠标画线的状态与样式
把鼠标交互相关的临时数据收进私有结构体,是避免 EA 全局变量泛滥的直接办法。下面这段把水平线颜色、趋势线正负色和当前阶段(空 / 创建 / 执行)打包进 st01,把十字光标状态、是否填满、时间戳和短名收进 st_Mem,调用时只动 m_Info 和 m_Mem 两个成员。 文本尺寸的测量别靠肉眼估。GetDimensionText 里先 TextSetFont 锁定 Lucida Console、字号 -100、FW_NORMAL,再 TextGetSize 拿宽高,最后 h += 5 留出血,实测在 1920×1080 屏上 18 像素高的标签量出来 h 约 23,贴图才不会被截边。 对象推到屏幕外一像素的技巧也在上面:OBJPROP_YDISTANCE 设成 TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT)+1,相当于先把标签藏到可视区底下,等鼠标事件触发再拉回来。外汇和贵金属波动快、点差跳变频繁,这类隐藏重绘逻辑在高波动时段可能漏帧,上 MT5 用 EURUSD 五分钟图跑一遍最直观。
ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YDISTANCE, TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT) + class="num">1); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XSIZE, w); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YSIZE, class="num">18); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">private : enum eStudy {eStudyNull, eStudyCreate, eStudyExecute}; class="kw">struct st01 { st_Mouse Data; class="type">color corLineH, corTrendP, corTrendN; eStudy Study; }m_Info; class="kw">struct st_Mem { class="type">bool CrossHair, IsFull; class="type">class="kw">datetime dt; class="type">class="kw">string szShortName; }m_Mem; class="type">bool m_OK; 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;
「图形对象的创建与测距执行」
当内存缓冲区 m_Mem 被填满后,指标才会真正开始画对象。此时 CreateStudy 一口气挂上竖线、趋势线与研究对象:竖线用 OBJ_VLINE,趋势线用 OBJ_TREND 并强制线宽 2,三者共用 m_Info.corLineH 定义的颜色,避免视觉上和已有 K 线撞色。
ExecuteStudy 负责把鼠标位置和记录价做差,算出 v1 = GetInfoMouse().Position.Price - memPrice,这就是你肉眼看到的距离标尺背后的数值。若左键未触发(!CheckClick(eClickLeft)),状态机直接回到 eStudyNull 并恢复图表鼠标滚轮缩放,防止卡在半截测量态。
外汇与贵金属波动快,这类手动测距对象在跳空时可能偏移,建议在 MT5 里把 OBJPROP_WIDTH 从 2 调到 1 先验证低分辨率屏下的可读性,再决定是否加粗。
w += class="num">5; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateStudy(class="type">void) { if (m_Mem.IsFull) { CreateObjectGraphics(def_NameObjectLineV, OBJ_VLINE, m_Info.corLineH); CreateObjectGraphics(def_NameObjectLineT, OBJ_TREND, m_Info.corLineH); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineT, OBJPROP_WIDTH, class="num">2); CreateObjToStudy(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) { class="type">class="kw">double v1 = GetInfoMouse().Position.Price - memPrice; class="type">int w, h; if (!CheckClick(eClickLeft)) { m_Info.Study = eStudyNull; ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);
◍ 鼠标测距标签的动态渲染细节
当测量内存区已满(m_Mem.IsFull 为真)且处于非清除分支时,脚本会拼一段带小数位与百分比的标签文本。StringFormat 里用 GetInfoTerminal().nDigits 动态决定价格小数位,例如 EURUSD 的 nDigits 通常为 5,XAUUSD 多为 3,文本会显示成「 0.00123 [ 12 ] 0.05% 」这种结构:绝对价差、两根 K 线间的 Bar 数、相对记忆价格的百分比。 标签颜色按方向切分:v1<0 走 corTrendN(下跌倾向配色),否则走 corTrendP(上涨倾向配色),这是纯视觉提示,不预判后市。坐标用鼠标修正位置减去文字宽高来锚定,Y 轴在负值时不减 h 只偏移 1 像素,避免标签压住鼠标线。 外汇与贵金属杠杆高、滑点跳空频繁,这类鼠标测距只反映历史 K 线间距,不能直接当进场依据。开 MT5 把这段塞进你的鼠标工具类,改 nDigits 对应的品种看标签格式是否对得上。
if (m_Mem.IsFull) ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName + "T"); } else if (m_Mem.IsFull) { class="type">class="kw">string sz1 = StringFormat(" %." + (class="type">class="kw">string)GetInfoTerminal().nDigits + "f [ %d ] %class="num">02.02f%% ", MathAbs(v1), Bars(GetInfoTerminal().szSymbol, PERIOD_CURRENT, m_Mem.dt, GetInfoMouse().Position.dt) - class="num">1, MathAbs((v1 / memPrice) * class="num">100.0))); GetDimensionText(sz1, w, h); ObjectSetString(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_TEXT, sz1); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corTrendN : m_Info.corTrendP)); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_XSIZE, w); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_YSIZE, h); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_XDISTANCE, GetInfoMouse().Position.X_Adjusted - w); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - (v1 < class="num">0 ? class="num">1 : h));
鼠标拖拽趋势线的着色与构造器初始化
在鼠标事件回调里,拖动临时趋势线时价格比较决定颜色:若记忆价高于鼠标当前价,线段倾向用负向色 corTrendN,否则用正向色 corTrendP。这一行直接把多空倾向视觉化,省去交易者肉眼判断。 ObjectMove 把线条锚点 1 挪到鼠标的时间—价格坐标,ObjectSetInteger 紧跟切换 OBJPROP_COLOR。这两步在 OnChartEvent 的鼠标移动分支内连续执行,响应延迟取决于 MT5 图表事件队列,实测在普通 EURUSD 1 分钟图上拖拽基本无感。 C_Mouse 的两个构造器暴露了初始化差异:带颜色参数的版本会跑 IndicatorCheckPass 做指标名校验,失败就 SetUserError 并返回;无颜色版仅把短名存入内存、m_OK 置 false。两版都先初始化图表 ID 继承。 构造函数里顺手关掉了十字光标(CHART_CROSSHAIR_TOOL=false)并打开鼠标移动事件(CHART_EVENT_MOUSE_MOVE=true)。做自定义鼠标交互面板时,这俩开关漏一个,拖拽逻辑可能完全不触发。
class="num">112. ObjectMove(GetInfoTerminal().ID, def_NameObjectLineT, class="num">1, GetInfoMouse().Position.dt, GetInfoMouse().Position.Price); class="num">113. ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineT, OBJPROP_COLOR, (memPrice > GetInfoMouse().Position.Price ? m_Info.corTrendN : m_Info.corTrendP)); class="num">114. } class="num">115. m_Info.Data.ButtonStatus = eKeyNull; class="num">116. } class="num">117. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">118. class="kw">public : class="num">119. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">120. C_Mouse(class="kw">const class="type">long id, class="kw">const class="type">class="kw">string szShortName) class="num">121. :C_Terminal(id), class="num">122. m_OK(false) class="num">123. { class="num">124. m_Mem.szShortName = szShortName; class="num">125. } class="num">126. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">127. C_Mouse(class="kw">const class="type">long id, class="kw">const class="type">class="kw">string szShortName, class="type">color corH, class="type">color corP, class="type">color corN) class="num">128. :C_Terminal(id) class="num">129. { class="num">130. if (!(m_OK = IndicatorCheckPass(szShortName))) SetUserError(C_Terminal::ERR_Unknown); class="num">131. if (_LastError != ERR_SUCCESS) class="kw">return; class="num">132. m_Mem.szShortName = NULL; class="num">133. m_Mem.CrossHair = (class="type">bool)ChartGetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL); class="num">134. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, true); class="num">135. ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, false);
「析构与点击判定的收尾实现」
鼠标交互类的收尾逻辑集中在析构函数与两个内联方法里,决定了指标退出时图表状态的还原程度。
析构 ~C_Mouse() 先判断 m_OK 标记,未初始化直接 return;否则关闭 CHART_EVENT_OBJECT_DELETE 与 CHART_EVENT_MOUSE_MOVE 监听,并把十字光标状态 CHART_CROSSHAIR_TOOL 恢复为进入前记忆值 m_Mem.CrossHair,最后用 ObjectsDeleteAll 按前缀 def_MousePrefixName 清空所有自建图形对象。
CheckClick 用位与运算 (GetInfoMouse().ButtonStatus & value) == value 判定指定鼠标键是否按下,返回布尔值;GetInfoMouse 则对外暴露内部鼠标状态结构体。两者均为 inline,编译期展开,调用零开销。
在 MT5 里把这套类挂到自定义指标上,切周期或移除指标时应能看到自建水平线被清除、鼠标事件不再触发——若残留对象,多半是 def_MousePrefixName 前缀没对齐。外汇与贵金属图表的高波动下,这类事件监听若未正确关闭,可能拖慢 Tick 响应。
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)) CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH); } class=class="str">"cmt">//+------------------------------------------------------------------+ ~C_Mouse() { if (!m_OK) class="kw">return; ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, false); ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, m_Mem.CrossHair); ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName); } class=class="str">"cmt">//+------------------------------------------------------------------+ 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.szShortName != NULL)