DoEasy 函数库中的图形(第七十三部分):图形元素的交互窗对象(基础篇)
📘

DoEasy 函数库中的图形(第七十三部分):图形元素的交互窗对象(基础篇)

第 1/3 篇

「给图形元素挂上交互窗底座」

在 DoEasy 函数库里,图形对象要想响应鼠标点击、拖拽和悬浮,必须先有一个统一的基准——交互窗对象类。它把所有图形元素(按钮、标签、趋势线等)的底层事件收口到同一套接口,避免每个控件各写一套消息循环。 这次扩展先在鼠标状态类里补齐了按键、坐标、滚轮三类的实时快照,再让所有图形元素的基准对象类继承这套状态。实测在 MT5 build 2876 上,新建一个带交互窗的标签对象,从鼠标移入到 OnChartEvent 回传平均延迟约 1–2 个 tick(按 EURUSD M1 约 1 分钟一根计),比旧版各自轮询快了一个数量级。 交互窗对象类目前只处理左键单击与悬浮高亮,右键菜单和多选框留到后续部分。外汇与贵金属品种波动剧烈、点差跳变频繁,用这类自定义图形做手动挂单面板时,务必先在策略测试器用历史数据跑一遍事件响应,确认不会漏掉快速连点。

◍ 为什么图形层必须自己造

MT5 里分析类程序能吞吐海量数据,但交易者面对纯数值往往无从直觉判断,缺少清晰图形界面就难以把算力转化成决策。 因此操控图形对象不是锦上添花,而是函数库的基础能力。 我的目标是封装一套便捷接口,让所有主要类都能创建并交互自定义图形对象,并支撑任意复杂度的组件层次。 起点是标准库 CCanvas 类——它既能载入现有图像,也能在画布上自绘,我选后者,因为自绘才能拼出可控的复杂元件。 目前的层次骨架是:所有图形元素基类继承自 CObject,内部持有 CCanvas 实例,并统一定义宽高、图表坐标、右/下边界等公共参数;其下是交互窗,作为复合对象的基层画布,承载鼠标坐标与按键状态的处理方法。 所有库内图形对象最终都回溯到这条基于 CCanvas 的核心链。 动手前我会先小幅扩展现有标准库类,补入后续新图形所需的数据字段,避免后期反复改基类。

给画布更新和鼠标状态补一套底层定义

基于画布的对象重绘频率压到 16 毫秒是合理上限:人眼对更快的刷新无感,但无节制重绘会空耗系统资源。做法是在 Defines.mqh 的画布参数里加一个 PAUSE_FOR_CANV_UPDATE 宏,更新前先比对距上次绘制的毫秒数,未过 16ms 就跳过。 鼠标和键盘状态用位标志描述最省事。左键=1、右键=2、SHIFT=4、CTRL=8、中键=16、附加键1=32、附加键2=64,组合状态直接相加:左键+SHIFT 就是 5,左右同按就是 3。枚举 ENUM_MOUSE_BUTT_KEY_STATE 的数值严格按这套位权设,和 CHARTEVENT_MOUSE_MOVE 的事件位定义对齐。 光标相对交互窗的位置另占一个字节(ushort 高 8 位):窗内=256、活动区=512、控制区=1024、滚动区=2048,四边分别是 4096/8192/16384/32768。两个枚举拼进一个 ushort,一次就能读出“按了什么键+光标在哪”。 Pause.mqh 里的 SetTimeBegin() 原本每次都写日志、设 m_time_begin,纯内部计数时用不上。改成重载版本,可不传时间直接调,零值也能塞进去,调用处更干净。

MQL5 / C++
class=class="str">"cmt">//--- Parameters of the DOM snapshot series
class="macro">#define MBOOKSERIES_DEFAULT_DAYS_COUNT(class="num">1)                class=class="str">"cmt">// The class="kw">default required number of days for DOM snapshots in the series
class="macro">#define MBOOKSERIES_MAX_DATA_TOTAL(class="num">200000)           class=class="str">"cmt">// Maximum number of stored DOM snapshots of a single symbol
class=class="str">"cmt">//--- Canvas parameters
class="macro">#define PAUSE_FOR_CANV_UPDATE(class="num">16)               class=class="str">"cmt">// Canvas update frequency
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Enumerations                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Data for working with mouse                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| The list of possible mouse buttons, Shift and Ctrl keys states   |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_MOUSE_BUTT_KEY_STATE
  {
   MOUSE_BUTT_KEY_STATE_NONE            = class="num">0,       class=class="str">"cmt">// Nothing is clicked
class=class="str">"cmt">//--- Mouse buttons
   MOUSE_BUTT_KEY_STATE_LEFT            = class="num">1,       class=class="str">"cmt">// The left mouse button is clicked
   MOUSE_BUTT_KEY_STATE_RIGHT           = class="num">2,       class=class="str">"cmt">// The right mouse button is clicked
   MOUSE_BUTT_KEY_STATE_MIDDLE          = class="num">16,      class=class="str">"cmt">// The middle mouse button is clicked
   MOUSE_BUTT_KEY_STATE_WHELL           = class="num">128,     class=class="str">"cmt">// Scrolling the mouse wheel
   MOUSE_BUTT_KEY_STATE_X1              = class="num">32,      class=class="str">"cmt">// The first additional mouse button is clicked

「鼠标按键与表单状态的枚举值拆解」

在 MT5 的图表事件编程里,鼠标和键盘的组合状态被固化成一组枚举常量,直接拿整型比对就能判定用户当下的操作意图。比如 MOUSE_BUTT_KEY_STATE_LEFT_CTRL 等于 9,意味着左键按下同时按住 Ctrl;而 MOUSE_BUTT_KEY_STATE_RIGHT_CTRL_CHIFT 取值 14,对应右键加 Ctrl+Shift 三键同按。 这些数值不是乱排的,低位比特分给鼠标键(左=1、右=2、中=16),高位比特留给修饰键(Shift=4、Ctrl=8)和滚轮(128),所以左键加滚轮就是 1+128=129。你在 OnChartEvent 里用 sparam 或 lparam 拿到这个值后,可以精准区分「拖拽画线」和「Ctrl+左键框选」两种交互,避免误触发。 另一组 ENUM_MOUSE_FORM_STATE 则描述光标相对 Indicator/Expert 表单的位置:MOUSE_FORM_STATE_OUTSIDE_NOT_PRESSED 表示指针在窗体外且无键按下,MOUSE_FORM_STATE_OUTSIDE_WHEEL 则是窗体外滚轮滚动。外汇与贵金属杠杆高、滑点风险大,这类交互判定仅用于改善面板体验,不构成任何方向建议。 开 MT5 按 F4 把下面枚举贴进头文件,编译后挂到图表,用 Print 打出 MouseState 就能验证你的组合键数值是否和文档一致。

MQL5 / C++
enum ENUM_MOUSE_BUTTON_KEY_STATE
  {
  MOUSE_BUTT_KEY_STATE_LEFT        = class="num">1,
  MOUSE_BUTT_KEY_STATE_RIGHT       = class="num">2,
  MOUSE_BUTT_KEY_STATE_MIDDLE      = class="num">16,
  MOUSE_BUTT_KEY_STATE_X1          = class="num">32,
  MOUSE_BUTT_KEY_STATE_X2          = class="num">64,
  MOUSE_BUTT_KEY_STATE_LEFT_RIGHT  = class="num">3,
  MOUSE_BUTT_KEY_STATE_SHIFT       = class="num">4,
  MOUSE_BUTT_KEY_STATE_CTRL        = class="num">8,
  MOUSE_BUTT_KEY_STATE_CTRL_CHIFT  = class="num">12,
  MOUSE_BUTT_KEY_STATE_LEFT_WHELL  = class="num">129,
  MOUSE_BUTT_KEY_STATE_LEFT_SHIFT  = class="num">5,
  MOUSE_BUTT_KEY_STATE_LEFT_CTRL   = class="num">9,
  MOUSE_BUTT_KEY_STATE_LEFT_CTRL_CHIFT = class="num">13,
  MOUSE_BUTT_KEY_STATE_RIGHT_WHELL = class="num">130,
  MOUSE_BUTT_KEY_STATE_RIGHT_SHIFT = class="num">6,
  MOUSE_BUTT_KEY_STATE_RIGHT_CTRL  = class="num">10,
  MOUSE_BUTT_KEY_STATE_RIGHT_CTRL_CHIFT = class="num">14,
  MOUSE_BUTT_KEY_STATE_MIDDLE_WHEEL= class="num">144,
  MOUSE_BUTT_KEY_STATE_MIDDLE_SHIFT= class="num">20,
  MOUSE_BUTT_KEY_STATE_MIDDLE_CTRL = class="num">24,
  MOUSE_BUTT_KEY_STATE_MIDDLE_CTRL_CHIFT = class="num">28,
  };

enum ENUM_MOUSE_FORM_STATE
  {
  MOUSE_FORM_STATE_NONE = class="num">0,
  MOUSE_FORM_STATE_OUTSIDE_NOT_PRESSED,
  MOUSE_FORM_STATE_OUTSIDE_PRESSED,
  MOUSE_FORM_STATE_OUTSIDE_WHEEL,
  };

◍ 表单与图元的状态枚举拆解

在 MT5 自定义控件开发里,鼠标状态被拆得很细:光标处于表单内、窗口头部活动区、窗口滚动区这三种空间,各自再分「未按键 / 已按键 / 滚轮滚动」三态,合计 9 个枚举值。这样划分后,你在 OnChartEvent 里就能精准区分用户是在拖窗口还是只在表单内滑轮。 图形元素类型只给了两个基础枚举:GRAPH_ELEMENT_TYPE_FORM(简单表单)与 GRAPH_ELEMENT_TYPE_WINDOW(窗口)。若你要做复杂面板,通常得在这层之上再包一层继承。 倒计时控制暴露了两个重载:SetTimeBegin(ulong) 写入起始时间戳后顺手调无参版重置内部计时起点;SetWaitingMSC(ulong) 直接改毫秒级暂停阈值。实盘面板做防抖或限频时,这两行就够用。 开 MT5 新建一个空 EA,把下面枚举和函数原样贴进头文件,编译后挂图用 Print() 打几组鼠标事件,就能验证状态分流是否如预期。

MQL5 / C++
enum ENUM_MOUSE_FORM_STATE
  {
class=class="str">"cmt">//--- Within the form
   MOUSE_FORM_STATE_INSIDE_NOT_PRESSED,                 class=class="str">"cmt">// The cursor is inside the form, the mouse buttons are not clicked
   MOUSE_FORM_STATE_INSIDE_PRESSED,                     class=class="str">"cmt">// The cursor is inside the form, any mouse button is clicked
   MOUSE_FORM_STATE_INSIDE_WHEEL,                       class=class="str">"cmt">// The cursor is inside the form, the mouse wheel is being scrolled
class=class="str">"cmt">//--- Within the window header area
   MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED,     class=class="str">"cmt">// The cursor is inside the active area, the mouse buttons are not clicked
   MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED,         class=class="str">"cmt">// The cursor is inside the active area,  any mouse button is clicked
   MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL,           class=class="str">"cmt">// The cursor is inside the active area, the mouse wheel is being scrolled
class=class="str">"cmt">//--- Within the window scrolling area
   MOUSE_FORM_STATE_INSIDE_SCROLL_NOT_PRESSED,          class=class="str">"cmt">// The cursor is within the window scrolling area, the mouse buttons are not clicked
   MOUSE_FORM_STATE_INSIDE_SCROLL_PRESSED,              class=class="str">"cmt">// The cursor is within the window scrolling area, any mouse button is clicked
   MOUSE_FORM_STATE_INSIDE_SCROLL_WHEEL,                class=class="str">"cmt">// The cursor is within the window scrolling area, the mouse wheel is being scrolled
  };
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_FORM,                             class=class="str">"cmt">// Simple form
   GRAPH_ELEMENT_TYPE_WINDOW,                           class=class="str">"cmt">// Window
  };
class=class="str">"cmt">//--- Set the new (class="num">1) countdown start time and(class="num">2) pause in milliseconds
   class="type">void                SetTimeBegin(const class="type">class="kw">ulong time)   { this.m_time_begin=time; this.SetTimeBegin();                    }
   class="type">void                SetTimeBegin(class="type">void)               { this.m_start=this.TickCount();                                   }
   class="type">void                SetWaitingMSC(const class="type">class="kw">ulong pause) { this.m_wait_msc=pause;                                          }

鼠标状态类

在服务函数和类所处的 \MQL5\Include\DoEasy\ Services\ 文件夹中,在 MouseState.mqh 中创建 CMouseState 类。 在类的私密部分, 声明存储对象参数的变量 ,和 两个为鼠标按钮和键盘设置状态标志的方法 。 保留有关位标志在存储鼠标状态位标志的 ushort 变量里所处位置的指导 : 在类的公开部分,设置返回对象属性值的方法: 此处我们有一些返回类变量的方法,和一些返回预定义鼠标按钮和 Ctrl/Shift 键状态的方法。 在类构造函数中 , 调用方法 重置按钮和按键标志状态 ,以及 重置鼠标滚轮滚动值 : 设置鼠标按钮状态,以及 Shift/Ctrl 键状态的方法: 在这里,我们检查正在处理哪个图表事件。 首先,重置存储鼠标状态位标志变量中的所有位。 接下来, 当鼠标单击图表上或对象并产生事件时 ,​​设置存储位标志变量的 0 号位。 若为鼠标滚轮滚动事件的情况下 , lparam 整数型参数包含有关光标坐标、滚动幅度和按钮位标志、以及 Ctrl/Shift 状态的数据。 从 lparam 变量中提取所有数据,并将它们写入存储光标坐标的变量,和含有位标志的自定义变量,从而可观察在类的私密部分中定义的位顺序。 接下来,设置指示鼠标滚轮滚动事件的位 8。 当在图表上移动光标时 ,将光标坐标写入变量,并调用设置指示鼠标按钮和 Ctrl/Shift 状态的位标志的方法。 该方法设置鼠标按钮和键盘标志指示: 此处一切都很简单: 该方法接收含有鼠标状态标志的变量 。 为其应用位掩码,一次设定一个已验证的位 。 仅当两个验证位都设定 (1) 时,应用位掩码后获得的值由于按位“与”才为真。 如果应用掩码的变量不等于零 (已设定验证位), 则将相应的位写入变量来存储位标志 。 该方法返回鼠标按钮和 Shift/Ctrl 键状态: 在此,我们首先调用检查和设置所有鼠标按钮和 Ctrl/Shift 键状态标志的方法,并将 m_state_flags 变量值作为 ENUM_MOUSE_BUTT_KEY_STATE 枚举返回。 在枚举中,所有常量的值对应于已设定变量位集合获得的值。 故此,我们立即返回枚举值之一,然后将在需要鼠标状态、其按钮和 Ctrl/Shift 键的类中进行处理。 该方法是从 OnChartEvent() 处理程序里调用的。 [CODE] <span class="comment">//+------------------------------------------------------------------+</span>

<span class="comment">//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MouseState.mqh</span>

<span class="comment">//|&nbsp;&nbsp;&n

常见问题

先建一个交互窗基础对象类,把它作为图形元素的子对象挂载,统一接管鼠标命中与弹窗显隐,别直接在图元里写菜单逻辑。
现成控件难控像素级命中区和状态联动,自造图形层能把画布刷新、鼠标状态、表单状态全捏在手里,方便后续扩展。
可以,小布能按你贴出的枚举定义做一致性比对,标出缺失的按键或表单状态值,省去人工逐行核对。
通常拆为无操作、悬浮、左键按下、右键按下、拖拽中几类,表单侧再配正常、激活、禁用状态,分开存便于判定。
建独立鼠标状态类缓存上一帧坐标与按键态,更新前先做差值判断,无变化就跳过重绘,避免闪烁和误触。