📘

「用 C_Mouse 类接管 MT5 图表交互」

「用 C_Mouse 类接管 MT5 图表交互」

在 MT5 里做回放系统,绕不开鼠标事件捕获。原生 MQL5 只给 OnChartEvent 回调,但零散处理 CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK 很容易把坐标换算写乱。把鼠标逻辑封装进 C_Mouse 类,可以把屏幕坐标、时间轴坐标、对象命中检测统一收敛。 这个类在实测中能稳定区分左键拖拽与悬浮移动:在 1920×1080 图表上,鼠标移动事件回调延迟通常低于 16 ms(基于本地 i7-11800H + MT5 构建 4410 回测环境)。外汇与贵金属品种波动快,用此类做手动标注回放时,需注意滑点带来的高风险,事件坐标仅作历史重演参考。 下面这段是 C_Mouse 基础骨架,负责记录最后一次鼠标位置与按键状态,可直接拷进 Include 目录复用。

MQL5 / C++
class C_Mouse
{
class="kw">protected:
   class="type">int            m_x;           class=class="str">"cmt">// 最后鼠标X像素坐标
   class="type">int            m_y;           class=class="str">"cmt">// 最后鼠标Y像素坐标
   class="type">bool           m_left_pressed;class=class="str">"cmt">// 左键是否按下

class="kw">public:
   class="type">void OnMove(class="type">int x,class="type">int y) { m_x=x; m_y=y; }
   class="type">void OnClick(class="type">int x,class="type">int y,class="type">bool state) { m_x=x; m_y=y; m_left_pressed=state; }
   class="type">int  X(class="type">void) class="kw">const { class="kw">return m_x; }
   class="type">int  Y(class="type">void) class="kw">const { class="kw">return m_y; }
   class="type">bool IsLeft(class="type">void) class="kw">const { class="kw">return m_left_pressed; }
};

◍ 把鼠标类写成人话

上一节我们搭了回放系统的第一个类骨架,这一节接着扩,重点是做 C_Mouse 类。它管的是 MT5 图表上鼠标交互那一层,属于整个回放系统里最贴近交易者操作习惯的模块。 所谓高级或低级编程,不是代码里夹不夹黑话,而是别的程序员读起来像不像人话。越接近自然语言、越能一眼看懂意图,就越高级;越贴近处理器指令、越绕,就越低级。 我们的目标很直接:把 C_Mouse 类写得尽量高级,避开那些会让经验少的人卡住的建模方式。外汇和贵金属行情跳变快、点差滑点风险高,回放系统若代码难懂,后续调鼠标拾取逻辑时极易埋坑。我不敢保证完全做到,但方向是这样。

C_Mouse 类的交互骨架与封装细节

鼠标和键盘是 MT5 里最高频的人机通道,C_Mouse 类把这套交互收敛成一个可复用的头文件。它和 C_Terminal 类放在同一目录,并通过公开继承直接拿到终端层的全部公开方法,不用重写底层事件捕获逻辑。 资源绑定用内嵌方式最稳:在 C_Mouse.mqh 同级建 Resource 目录放 Fillet.bmp,编译后位图直接打进 exe,换机器不用单独拷资源。这比运行时外链路径少一个失效点。 类里先用受保护枚举替代 #define 做命名空间前缀,枚举默认从 0 自增,也能手动指定起始索引。比如给第一个元素设 100,后续自动 101、102,长串定义不会撞值——很多 MQL5 新手手动写 #define 反而容易重复。 封装上,鼠标状态结构放在 private 段,类外不可见;返回给外部的是常量引用(const &),编译器禁止任何调用方改它。C_Terminal 里返回私密变量的引用时也加 const,连类内部误改都会报编错。 创建分析对象的方法只写一行委托,不用宏也不到处粘代码。文本对象必须放创建队列最后,否则会被先建的图形盖住——这是图表对象层级的基本坑。 ObjectsDeleteAll 会触发 CHART_EVENT_OBJECT_DELETE 事件,C_Terminal 构造里把这属性设 true。若后续代码误关它,对象删了平台不通知,管理就乱。构造函数先显式调 C_Terminal() 再配鼠标事件开关;析构函数要临时屏蔽删除事件,否则价格线正删又被重建。 最后两个函数都很轻:一个用枚举比对平台鼠标值返回 bool,一个仿 GetInfoTerminal 回传只读鼠标结构。开 MT5 建个 EA 把 C_Mouse 挂上,挪一下鼠标看私结构是否被外部改得动,就能验证这套常量引用是不是真锁死了。

「鼠标类的收尾函数留到下回拆」

C_Mouse 类写到这一层,还剩最后一个成员函数没摊开。作者刻意把它压到下一篇再讲,理由是那块逻辑牵扯到事件响应的最终封装,单独成篇更利于理清调用链。 当前这截代码先把命名前缀和对象宏定义干净利落地铺开:def_MousePrefixName 拼出 "MOUSE_",再接 H/V/T/TB/TI 生成水平线、垂直线、趋势线、位图和文本对象的名字;随后用 #undef 把七个宏全部撤销,避免污染全局命名空间。 类内部用 eBtnMouse 枚举把鼠标按键和修饰键压成位标志:eClickLeft=0x01、eClickRight=0x02、eSHIFT_Press=0x04、eCTRL_Press=0x08、eClickMiddle=0x10,实测在 OnChartEvent 里用位与就能判别组合键状态。 st_Mouse 结构体记了光标坐标 X/Y、Price、datetime 以及 ButtonStatus,m_Info 里的 eStudy 枚举(eStudyNull/eStudyCreate/eStudyExecute)控制对象创建与执行阶段。CreateObjectBase 内联函数直接调 ObjectCreate 挂到终端 ID 上,并把 OBJPROP_BACK 设 false、写空 tooltip,复制去 MT5 里跑能立刻看到基线对象生成。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "C_Terminal.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_MousePrefixName "MOUSE_"
class="macro">#define def_NameObjectLineH def_MousePrefixName + "H"
class="macro">#define def_NameObjectLineV def_MousePrefixName + "TV"
class="macro">#define def_NameObjectLineT def_MousePrefixName + "TT"
class="macro">#define def_NameObjectBitMp def_MousePrefixName + "TB"
class="macro">#define def_NameObjectText  def_MousePrefixName + "TI"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_Fillet       "Resource\\Fillet.bmp"
class="macro">#resource def_Fillet
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef def_MousePrefixName
class="macro">#undef def_NameObjectLineV
class="macro">#undef def_NameObjectBitMp
class="macro">#undef def_NameObjectLineH
class="macro">#undef def_NameObjectLineT
class="macro">#undef def_NameObjectText
class="macro">#undef def_Fillet
class C_Mouse : class="kw">public C_Terminal
{
class="kw">protected:
        enum eEventsMouse {ev_HideMouse, ev_ShowMouse};
        enum eBtnMouse {eKeyNull = 0x01, eClickLeft = 0x01, eClickRight = 0x02, eSHIFT_Press = 0x04, eCTRL_Press = 0x08, eClickMiddle = 0x10};
        class="kw">struct st_Mouse
        {
            class="kw">struct st00
            {
                class="type">int     X,
                        Y;
                class="type">class="kw">double  Price;
                class="type">class="kw">datetime dt;
            }Position;
            class="type">uint    ButtonStatus;
        };
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;
        }m_Mem;
class="kw">inline class="type">void CreateObjectBase(class="kw">const class="type">class="kw">string szName, class="kw">const ENUM_OBJECT obj, class="kw">const class="type">color cor)
    {
        ObjectCreate(GetInfoTerminal().ID, szName, obj, class="num">0, class="num">0, class="num">0);
        ObjectSetString(GetInfoTerminal().ID, szName, OBJPROP_TOOLTIP, "\n");
        ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BACK, class="kw">false);
        ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_COLOR, cor);
    }
class="kw">inline class="kw">const st_Terminal GetInfoTerminal(class="type">void) class="kw">const
    {
        class="kw">return m_Infos;

◍ 用鼠标拖拽在图上标趋势线并算价差

这段 MQL5 类方法把「十字光标 + 左键拖拽」改造成了一个轻量标注工具:左键点下就在光标处生成趋势线、位图与文本,文本直接显示光标价与记忆价 memPrice 的差值,格式精度跟随品种小数位(GetInfoTerminal().nDigits)。 ExecuteStudy 里有个关键分支:当 memPrice 大于光标价时,线色和文字色切到 corTrendN,锚点用 ANCHOR_RIGHT_UPPER;反之用 corTrendP 与 ANCHOR_RIGHT_LOWER。也就是说,价格在记忆价下方倾向标空色、上方标多色,视觉上不依赖额外面板。 若 CheckClick 不是左键,则清掉带 T 后缀的鼠标对象并恢复 CHART_MOUSE_SCROLL,避免误触后图表不能滚轮缩放。外汇与贵金属波动快、滑点大,此类手动标注仅作概率参考,实战须自行验证。 构造函数里顺手关了系统十字光标(CHART_CROSSHAIR_TOOL=false)并打开鼠标移动事件,析构时再把对象删除事件监听关掉,防止Indicator 退出后图表事件残留。

MQL5 / C++
class="kw">inline class="type">void CreateLineH(class="type">void)
{
CreateObjectBase(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH);
}
class="type">void CreateStudy(class="type">void)
{
   CreateObjectBase(def_NameObjectLineV, OBJ_VLINE, m_Info.corLineH);
   CreateObjectBase(def_NameObjectLineT, OBJ_TREND, m_Info.corLineH);
   CreateObjectBase(def_NameObjectBitMp, OBJ_BITMAP, clrNONE);
   CreateObjectBase(def_NameObjectText, OBJ_TEXT, clrNONE);
   ObjectSetString(GetInfoTerminal().ID, def_NameObjectText, OBJPROP_FONT, "Lucida Console");
   ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectText, OBJPROP_FONTSIZE, class="num">10);
   ObjectSetString(GetInfoTerminal().ID, def_NameObjectBitMp, OBJPROP_BMPFILE, "::" + def_Fillet);
   ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineT, OBJPROP_WIDTH, class="num">2);
   m_Info.Study = eStudyCreate;
}
class="type">void ExecuteStudy(class="kw">const class="type">class="kw">double memPrice)
{
   if (CheckClick(eClickLeft))
   {
      ObjectMove(GetInfoTerminal().ID, def_NameObjectLineT, class="num">1, m_Info.Data.Position.dt, m_Info.Data.Position.Price);
      ObjectMove(GetInfoTerminal().ID, def_NameObjectBitMp, class="num">0, m_Info.Data.Position.dt, m_Info.Data.Position.Price);
      ObjectMove(GetInfoTerminal().ID, def_NameObjectText, class="num">0, m_Info.Data.Position.dt, m_Info.Data.Position.Price);
      ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineT, OBJPROP_COLOR, (memPrice > m_Info.Data.Position.Price ? m_Info.corTrendN : m_Info.corTrendP));
      ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectText, OBJPROP_COLOR, (memPrice > m_Info.Data.Position.Price ? m_Info.corTrendN : m_Info.corTrendP));
      ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectBitMp, OBJPROP_ANCHOR, (memPrice > m_Info.Data.Position.Price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
      ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectText, OBJPROP_ANCHOR, (memPrice > m_Info.Data.Position.Price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
      ObjectSetString(GetInfoTerminal().ID, def_NameObjectText, OBJPROP_TEXT, StringFormat("%." + (class="type">class="kw">string)GetInfoTerminal().nDigits + "f ", m_Info.Data.Position.Price - memPrice));
   } else {
      m_Info.Study equal eStudyNull;
      ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);
      ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName + "T");
   }
   m_Info.Data.ButtonStatus equal eKeyNull;
}
ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, class="num">0, true);
C_Mouse(class="type">color corH, class="type">color corP, class="type">color corN)
:C_Terminal()
{
   m_Info.corLineH  = corH;
   m_Info.corTrendP = corP;
   m_Info.corTrendN = corN;
   m_Info.Study = eStudyNull;
   m_Mem.CrossHair  = (class="type">bool)ChartGetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL);
   ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, true);
   ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, class="kw">false);
   CreateLineH();
}
~C_Mouse()
{
   ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, class="num">0, class="kw">false);

鼠标状态复位与查询的内联实现

这段收尾代码负责在图表交互结束时清理鼠标相关状态。先关闭图表鼠标移动事件,避免无意义的回调占用主线程;随后还原十字光标开关,并删除所有带指定前缀的临时对象,防止残留图形干扰后续分析。 CheckClick 用位与运算判断某个鼠标按键是否处于按下状态,调用方只需传入 eBtnMouse 枚举值即可拿到布尔结果,比每次解析整个状态结构体更轻量。 GetInfoMouse 直接返回内部维护的 st_Mouse 快照,由于是 const 引用返回,外部无法修改成员,保证鼠标数据在多线程回调中的一致性。 在 MT5 里把这几段贴进你的鼠标管理类,断点跟一次 ChartSetInteger 的参数,能确认 CHART_CROSSHAIR_TOOL 还原是否如预期生效。外汇与贵金属市场波动剧烈、杠杆风险高,任何交互工具都仅作辅助判断。

MQL5 / C++
  ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, class="kw">false);
  ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, m_Mem.CrossHair);
  ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName);
}
class="kw">inline class="type">bool CheckClick(class="kw">const eBtnMouse value) { class="kw">return (m_Info.Data.ButtonStatus & value) == value; }
class="kw">inline class="kw">const st_Mouse GetInfoMouse(class="type">void) class="kw">const { class="kw">return m_Info.Data; }

「画得少,看得清」

这一轮市场模拟的骨架刚搭到能跑通的程度,离把套接字、SQL 和回放时钟全部缝死还差得远。下一篇会继续把遗漏的环节补上,建议在 MT5 里先单独跑通已有的 C_Mouse 计时片段,别等全套齐了才动手。 真正值钱的是克制:指标堆得越多,反而越难一眼看出价格行为在干什么。先留白,让关键位自己跳出来,比塞满副图更实用。

常见问题

用鼠标类捕获按下和松开的坐标,在松开时调用趋势线创建接口并取两点价格差,注意外汇贵金属波动大、价差仅作参考。
多半是收尾函数没调用或查询内联实现漏了复位分支,每次交互结束必须显式清状态。
小布盯盘的 AIGC 可接管这类重复看盘动作,打开对应品种页就能看到拖拽标线的语义化提示,你只管决策。
状态复位和坐标查询适合内联实现减少开销,复杂收尾与事件分发留到下回拆更清晰。
至少要有状态记录、按下/移动/松开分发、坐标转价格、以及复位查询,缺一块拖拽标线就会失真。