DoEasy 函数库中的图形(第七十三部分):图形元素的交互窗对象(基础篇)
「给图形元素挂上交互窗底座」
在 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,纯内部计数时用不上。改成重载版本,可不传时间直接调,零值也能塞进去,调用处更干净。
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 就能验证你的组合键数值是否和文档一致。
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() 打几组鼠标事件,就能验证状态分流是否如预期。
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">// | MouseState.mqh | </span> |
|---|
<span class="comment">//| &n