开发回放系统(第 54 部分):第一个模块的诞生·进阶篇
📘

开发回放系统(第 54 部分):第一个模块的诞生·进阶篇

第 2/3 篇

◍ 按钮对象的初始化与属性注入

在 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 决定。想改按钮观感,调这几个常量比改逻辑更快。 外汇与贵金属波动剧烈,这类面板只做辅助显示,任何信号都只是概率倾向,实盘前请在策略测试器里跑一遍验证参数。

MQL5 / C++
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 五分钟图跑一遍最直观。

MQL5 / C++
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 先验证低分辨率屏下的可读性,再决定是否加粗。

MQL5 / C++
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 对应的品种看标签格式是否对得上。

MQL5 / C++
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)。做自定义鼠标交互面板时,这俩开关漏一个,拖拽逻辑可能完全不触发。

MQL5 / C++
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_DELETECHART_EVENT_MOUSE_MOVE 监听,并把十字光标状态 CHART_CROSSHAIR_TOOL 恢复为进入前记忆值 m_Mem.CrossHair,最后用 ObjectsDeleteAll 按前缀 def_MousePrefixName 清空所有自建图形对象。 CheckClick 用位与运算 (GetInfoMouse().ButtonStatus & value) == value 判定指定鼠标键是否按下,返回布尔值;GetInfoMouse 则对外暴露内部鼠标状态结构体。两者均为 inline,编译期展开,调用零开销。 在 MT5 里把这套类挂到自定义指标上,切周期或移除指标时应能看到自建水平线被清除、鼠标事件不再触发——若残留对象,多半是 def_MousePrefixName 前缀没对齐。外汇与贵金属图表的高波动下,这类事件监听若未正确关闭,可能拖慢 Tick 响应。

MQL5 / C++
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)

常见问题

检查结构体里样式字段是否先赋默认值再注入对象属性;建议把初始化拆成「建对象→注属性→绑事件」三步分开测。
在结构体里加一个状态枚举字段,每次鼠标移动打印当前状态;着色异常多半是构造器里颜色常量没传进去。
可以。把模块代码贴给小布,它能标出标签动态渲染缺失的边界判断,并提示拖拽趋势线构造器漏初始化的字段。
重点看标签锚点用的是起点还是鼠标实时坐标;偏移量建议用相对像素而非绝对坐标,避免不同分辨率下错位。
通常是对象未判空就调用释放,或点击判定在对象已销毁后仍触发;收尾时先解绑事件再走析构。