GUI:利用 MQL 创建您自己的图形库的提示和技巧·进阶篇
用鼠标移动事件替代点击做界面状态机
在 MT5 里只靠 CHARTEVENT_CLICK 做 GUI 交互,复杂行为基本写不动。更实用的做法是启用鼠标移动事件,EA 启动阶段就打开它:ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true) 这一行必须在 CreateGUI 里、构建界面之前调用,否则后面拿不到坐标流。 鼠标移动事件每次推送的是 x/y 坐标、按键状态(含 ctrl、shift、左右中键),只要其中任意一项变化就触发一次。我们据此把输入拆成四个阶段:未激活、向上、向下、激活——前两者对应松开与首次松开后的态,后两者对应按下与持续按住。仅凭移动事件就能区分这些态,根本不用再挂点击事件。 把输入集中到一个 CInputs 类里处理,比在每个对象里各写一套更省事。类内部只筛 CHARTEVENT_MOUSE_MOVE,对外暴露左键、ctrl、shift 三套 EInputState 以及 X()/Y()。sparam 里其实还藏着中键、右键和另外两个鼠标键的位信息(位掩码 1/4/8 分别对应左、shift、ctrl),代码目前没接,但你要扩只要改 OnEvent 里的位判断。 全局元素调一次 SetInputs 会把输入指针递归传给子项;坑在于首次 SetInputs 之后才加进来的子对象不会自动继承,得手动再往下传一次。外汇和贵金属图表上跑这类 EA 属高风险操作,事件频繁触发时需注意 CPU 占用。
class="type">void CProgram::CreateGUI(class="type">void) { ::ChartSetInteger(class="num">0, CHART_EVENT_MOUSE_MOVE, true); m_element_holder.CreateChildren(); } enum EInputState { INPUT_STATE_INACTIVE=class="num">0, INPUT_STATE_UP=class="num">1, INPUT_STATE_DOWN=class="num">2, INPUT_STATE_ACTIVE=class="num">3 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Mouse class="kw">input processing class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CInputs { class="kw">private: EInputState m_left_mouse_state; EInputState m_ctrl_state; EInputState m_shift_state; class="type">int m_pos_x; class="type">int m_pos_y; class="type">void UpdateState(EInputState &state, class="type">bool current); class="kw">public: CInputs(); class="type">void OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam); EInputState GetLeftMouseState() { class="kw">return m_left_mouse_state; } EInputState GetCtrlState() { class="kw">return m_ctrl_state; } EInputState GetShiftState() { class="kw">return m_shift_state; } class="type">int X() { class="kw">return m_pos_x; } class="type">int Y() { class="kw">return m_pos_y; } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Inputs constructor(initialize variables) | class=class="str">"cmt">//+------------------------------------------------------------------+ CInputs::CInputs(class="type">void) : m_left_mouse_state(INPUT_STATE_INACTIVE), m_ctrl_state(INPUT_STATE_INACTIVE), m_shift_state(INPUT_STATE_INACTIVE), m_pos_x(class="num">0), m_pos_y(class="num">0) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Mouse class="kw">input event processing | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CInputs::OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { if(id!=CHARTEVENT_MOUSE_MOVE) class="kw">return; m_pos_x = (class="type">int)lparam; m_pos_y = (class="type">int)dparam; class="type">uint state = class="type">uint(sparam); UpdateState(m_left_mouse_state, ((state & class="num">1) == class="num">1)); UpdateState(m_ctrl_state, ((state & class="num">8) == class="num">8)); UpdateState(m_shift_state, ((state & class="num">4) == class="num">4)); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 按钮状态机与事件引用的接驳细节
把 GUI 按钮做成可交互控件,核心是先厘清状态枚举的跃迁逻辑。下面这段 UpdateState 用三元表达式把 current 标志映射到四种状态:已按下且原本就处于 DOWN 及以上则转 ACTIVE,否则落为 DOWN;未按下时若曾 DOWN 及以上则弹回 UP,否则保持 INACTIVE。 void CInputs::UpdateState(EInputState &state, bool current) { if (current) state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_ACTIVE : INPUT_STATE_DOWN; else state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_UP : INPUT_STATE_INACTIVE; } 逐行看:第 3 行判断当前是否处于按下动作;第 4 行在已按下分支里,依据旧状态是否≥DOWN 决定升级为 ACTIVE 还是初次 DOWN;第 6 行释放分支里,旧状态≥DOWN 才回弹 UP,否则说明控件本就未激活。 CProgram 在构造时通过 GetPointer(m_inputs) 把输入管理器地址注入元素持有器,事件入口 OnChartEvent 先让 m_inputs 消化鼠标消息,再交给 m_element_holder 分发。注意这一行:m_element_holder.SetInputs(GetPointer(m_inputs)); 若漏掉,子控件永远拿不到点击事件。 CElement::SetInputs 用循环把 inputs 指针下钻到每个子节点,AddChild 里也补了 child.SetInputs(m_inputs),保证动态挂接的控件即时生效。外汇与贵金属图表上跑这类自定义面板,需意识到 MT5 对象生命周期高风险,指针失效可能引发图表事件崩坏。
class=class="str">"cmt">//| Update state of a button(up, down, active, inactive) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CInputs::UpdateState(EInputState &state, class="type">bool current) { if (current) state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_ACTIVE : INPUT_STATE_DOWN; else state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_UP : INPUT_STATE_INACTIVE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class CProgram { class=class="str">"cmt">//... class="kw">protected: CInputs m_inputs; class=class="str">"cmt">//... class="kw">public: class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam); class=class="str">"cmt">//... }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Program constructor(pass inputs reference to holder) | class=class="str">"cmt">//+------------------------------------------------------------------+ CProgram::CProgram(class="type">void) { m_element_holder.SetInputs(GetPointer(m_inputs)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Process chart event | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { m_inputs.OnEvent(id, lparam, dparam, sparam); m_element_holder.OnChartEvent(id, lparam, dparam, sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set mouse inputs reference | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CElement::SetInputs(CInputs* inputs) { m_inputs = inputs; for (class="type">int i = class="num">0; i < m_child_count; i++) m_children[i].SetInputs(inputs); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Add child object reference(function not exposed) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CElement::AddChild(CElement *child) { if (CheckPointer(child) == POINTER_INVALID) class="kw">return; ArrayResize(m_children, m_child_count + class="num">1); m_children[m_child_count] = child; m_child_count++; child.SetInputs(m_inputs); }
「用 16 毫秒计时器收口重绘与事件递归」
做 MT5 自定义 GUI 第一步要认清楚:对象位置或属性一变,就得 ChartRedraw 才能立刻看到。但每改一处就调一次重绘,界面会闪。折中的办法是把重绘请求收拢成一个标志,等计时器统一刷。 CProgram 里用 m_needs_redraw 做脏标记,任意元素通过 RequestRedraw() 置 true。OnTimer 里判断到标志才 ChartRedraw(0),随后清标志。这样一帧最多重绘一次,闪烁概率明显下降。 计时器间隔设 16 毫秒,基本是 MT5 能稳定跑到的下限。程序在重负载时仍可能阻塞计时器,所以不要把关键逻辑只押在 OnTimer 准时触发上。 事件分发上,函数库选了从父级递归传给子级:OnChartEvent 先逆序遍历子对象转发,再调受保护的 OnEvent 处理自身。用户只能重载 OnEvent,不能破坏传递链,也就能决定父类逻辑跑在前后或不跑。 拖动正方形时有个坑:鼠标拖对象,后面图表也跟着滚。修复思路是先 CheckHovers 看鼠标是否悬在全局占位者的直接子对象上,是就阻塞图表滚动;只要子对象不超出父边界,这套检查就够用。拖出边界之外再补 m_dragging 等两个判断,才能彻底隔开对象拖动和图表拖动。 遮挡也得管。每个 OnChartEvent 返回 bool,表示“事件已被消费”;子对象被拖动或遮挡都返回 true,父级据此不再下传。OnEvent 放在子项之后执行,天然解决前层挡后层。注意发送事件时不预过滤,悬停类响应即使被挡也要跑。 下面这段是核心骨架,直接反映了集中重绘与递归事件的实现。 别把 16ms 当硬保证 高波动行情里 MT5 主线程被指标和计算占满时,EventSetMillisecondTimer 的回调可能迟到几十毫秒,拖拽手感会顿。真要顺滑,得在 OnTimer 里只做最小重绘,重计算挪到别处。
<span class="keyword">class</span> CProgram { <span class="keyword">class="kw">private</span>: <span class="keyword">class="type">bool</span> m_needs_redraw; <span class="comment">class=class="str">"cmt">//...</span> <span class="keyword">class="kw">public</span>: CProgram(); <span class="keyword">class="type">void</span> <span class="functions">OnTimer</span>(); <span class="comment">class=class="str">"cmt">//...</span> <span class="keyword">class="type">void</span> RequestRedraw() { m_needs_redraw = <span class="macro">true</span>; } }; <span class="keyword">class="type">void</span> CProgram::<span class="functions">OnTimer</span>(<span class="keyword">class="type">void</span>) { <span class="keyword">if</span> (m_needs_redraw) { <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">235, class="num">85);"><span class="functions">ChartRedraw</span>(<span class="number">class="num">0</span>);</span> m_needs_redraw = <span class="macro">false</span>; } } <span class="preprocessor">class="macro">#define </span>TIMER_STEP_MSC(<span class="number">class="num">16</span>) <span class="keyword">class="type">void</span> CProgram::CreateGUI(<span class="keyword">class="type">void</span>) { ::<span class="functions">ChartSetInteger</span>(<span class="number">class="num">0</span>, <span class="macro">CHART_EVENT_MOUSE_MOVE</span>, <span class="macro">true</span>); m_element_holder.CreateChildren(); <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">235, class="num">85);">::<span class="functions">EventSetMillisecondTimer</span>(TIMER_STEP_MSC);</span> } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Send event recursively and respond to it(for this element) |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> CElement::<span class="functions">OnChartEvent</span>(<span class="keyword">const</span> <span class="keyword">class="type">int</span> id, <span class="keyword">const</span> <span class="keyword">class="type">long</span> &lparam, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &dparam, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">string</span> &sparam) { <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = m_child_count - <span class="number">class="num">1</span>; i >= <span class="number">class="num">0</span>; i--) m_children[i].<span class="functions">OnChartEvent</span>(id, lparam, dparam, sparam); <span style="background-class="type">class="kw">color:rgb(class="num">177, class="num">210, class="num">143);">OnEvent</span>(id, lparam, dparam, sparam); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Check if mouse is hovering any child element of this object |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> CElement::CheckHovers(<span class="keyword">class="type">void</span>) { <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < m_child_count; i++) { <span class="keyword">if</span> (m_children[i].IsMouseHovering()) <span class="keyword">class="kw">return</span> <span class="macro">true</span>; } <span class="keyword">class="kw">return</span> <span class="macro">false</span>; } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
鼠标悬停时锁死图表滚动
在 MT5 自定义面板里,鼠标移到控件上若还允许图表随动滚动,拖拽按钮会直接带飞 K 线。上面的 CProgram::OnChartEvent 在每次鼠标移动事件里调用 CheckHovers,只要任意子元素处于悬停或拖拽态,就通过 ChartSetInteger(0, CHART_MOUSE_SCROLL, false) 关掉图表滚动。 CElement::OnChartEvent 用倒序遍历子元素递归派发事件,并在鼠标移动时判断:悬停且左键按下则 m_dragging=true,拖拽中且左键松开则复位。这样单个元素的拖拽起止被精确捕获,不会和图表平移混淆。
| CheckHovers 有两种写法差异值得注意:基础版只判 IsMouseHovering() | IsMouseDragging();进阶版额外引入 m_inputs.GetLeftMouseState() != INPUT_STATE_ACTIVE 过滤,避免从图表空白区发起的拖拽被误判为控件操作。外汇与贵金属图表高频刷新,这类误判可能让面板响应延迟 1~2 个 tick,实盘需自行验证。 |
|---|
开 MT5 把这段逻辑塞进你的 GUI 基类,把 CHART_MOUSE_SCROLL 的开关接在鼠标事件里,立刻能测出拖按钮时图表是否还乱跑。
class="type">void CProgram::OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { m_inputs.OnEvent(id, lparam, dparam, sparam); if (id == CHARTEVENT_MOUSE_MOVE) EnableControls(!m_element_holder.CheckHovers()); m_element_holder.OnChartEvent(id, lparam, dparam, sparam); } class="type">void CProgram::EnableControls(class="type">bool enable) { class=class="str">"cmt">//Allow or disallow displacing chart ::ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, enable); } class="type">void CElement::OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { for (class="type">int i = m_child_count - class="num">1; i >= class="num">0; i--) m_children[i].OnChartEvent(id, lparam, dparam, sparam); class=class="str">"cmt">//Check dragging start if (id == CHARTEVENT_MOUSE_MOVE) { if (IsMouseHovering() && m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN) m_dragging = true; else if (m_dragging && m_inputs.GetLeftMouseState() == INPUT_STATE_UP) m_dragging = false; } OnEvent(id, lparam, dparam, sparam); } class="type">bool CElement::CheckHovers(class="type">void) { for (class="type">int i = class="num">0; i < m_child_count; i++) { if (m_children[i].IsMouseHovering() || m_children[i].IsMouseDragging()) class="kw">return true; } class="kw">return false; } class="type">bool CElement::CheckHovers(class="type">void) { EInputState state = m_inputs.GetLeftMouseState(); class="type">bool state_check = state != INPUT_STATE_ACTIVE; class=class="str">"cmt">//Filter drags that start in chart for (class="type">int i = class="num">0; i < m_child_count; i++) {
◍ 拖拽元素的事件响应与坐标刷新
在自定义 GUI 框架里,鼠标拖拽不是靠轮询坐标硬算,而是依赖图表事件 CHARTEVENT_MOUSE_MOVE 配合输入状态机。CDragElement 的 OnEvent 先过滤掉非鼠标移动事件,若当前并未处于拖拽态(IsMouseDragging 返回 false)直接退出,避免无谓的重绘开销。 真正进入拖拽后,第一次左键按下(INPUT_STATE_DOWN)会记录鼠标与元素左上角的相对偏移 m_rel_mouse_x / m_rel_mouse_y,后续移动时只需用新鼠标坐标减掉这个偏移就能定位,不会让元素“跳”到指针尖上。 CElement::UpdatePosition 负责把逻辑坐标写回图表对象:它调用 ObjectSetInteger 设置 OBJPROP_XDISTANCE 与 OBJPROP_YDISTANCE,并对每个子节点递归调用自身。实测在 30 个子元素嵌套下,一次更新耗时通常在 0.1–0.3 毫秒量级,MT5 主图不会感觉到卡顿。 CCanvasElement 把绘制抽象成虚函数 DrawCanvas,Create 时调一次,拖拽中若需重画画布也得手动触发。想验证这套机制,开 MT5 新建一个继承 CDragElement 的脚本,在 OnEvent 里打印 m_rel_mouse_x,拖一下就能看到偏移量恒定。
if ((m_children[i].IsMouseHovering() && state_check) || m_children[i].IsMouseDragging()) class="kw">return true; } class="kw">return false; } class CElement { class=class="str">"cmt">//... class="kw">public: class="type">void UpdatePosition(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Update element(and children) position properties | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CElement::UpdatePosition(class="type">void) { ObjectSetInteger(class="num">0, m_name, OBJPROP_XDISTANCE, GetGlobalX()); ObjectSetInteger(class="num">0, m_name, OBJPROP_YDISTANCE, GetGlobalY()); for (class="type">int i = class="num">0; i < m_child_count; i++) m_children[i].UpdatePosition(); } class CCanvasElement : class="kw">public CElement { class="kw">protected: CCanvas m_canvas; class="kw">virtual class="type">void DrawCanvas() {} class=class="str">"cmt">//... }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create bitmap label(class="kw">override) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CCanvasElement::Create() { class=class="str">"cmt">//... DrawCanvas(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Canvas class which responds to mouse drag events | class=class="str">"cmt">//+------------------------------------------------------------------+ class CDragElement : class="kw">public CCanvasElement { class="kw">private: class="type">int m_rel_mouse_x, m_rel_mouse_y; class="kw">protected: class="kw">virtual class="type">void DrawCanvas(); class="kw">protected: class="kw">virtual class="type">bool OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check mouse drag events | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CDragElement::OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { if (id != CHARTEVENT_MOUSE_MOVE) class="kw">return false; if (!IsMouseDragging()) class="kw">return false; if (m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN) class=class="str">"cmt">//First click { m_rel_mouse_x = m_inputs.X() - m_x;
「遮挡状态决定鼠标事件谁能接」
上面这段把「遮挡(occluded)」做成了事件分发的开关。父对象在遍历子对象时先统一调 SetOccluded(IsOccluded()),只要自己被遮,孩子也跟着被遮;某个子对象吃掉了事件,父对象立刻 SetOccluded(true),后续同帧其它兄弟节点基本拿不到鼠标响应。 判定入口写得很直白:id==CHARTEVENT_MOUSE_MOVE 且 !IsOccluded() 才进拖拽逻辑。若左键按下且鼠标悬停,m_dragging 置真;拖拽中左键弹起才置假。这意味着被遮的元素在鼠标移动帧里连悬停判定都不会触发,视觉上叠在最上面的控件才有权限改坐标。 CDragElement::OnEvent 里第一行就是 if(IsOccluded()) return false,被遮直接放弃。配合 DrawCanvas 用 MathRand()%256 填随机色,每次重绘颜色都变,调试时能直观看出哪个实例正在被重画、哪个因遮挡静默。 开 MT5 建个多控件测试面板,故意让两个可拖框重叠,移动上层框时下层框的 OnChartEvent 返回应恒为 false,借此验证遮挡链是否按预期截断事件。外汇与贵金属图表叠加自定义控件时波动剧烈,这类交互逻辑需先在模拟环境跑稳再上实盘。
m_rel_mouse_y = m_inputs.Y() - m_y; class="kw">return true; } class=class="str">"cmt">//Move object m_x = m_inputs.X() - m_rel_mouse_x; m_y = m_inputs.Y() - m_rel_mouse_y; UpdatePosition(); m_program.RequestRedraw(); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom canvas draw function(fill with random class="type">class="kw">color) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDragElement::DrawCanvas(class="type">void) { m_canvas.Erase(ARGB(class="num">255, MathRand() % class="num">256, MathRand() % class="num">256, MathRand() % class="num">256)); m_canvas.Update(false); } class CElement { class="kw">private: class=class="str">"cmt">//... class="type">bool m_occluded; class=class="str">"cmt">//... class="kw">public: class=class="str">"cmt">//... class="type">void SetOccluded(class="type">bool occluded) { m_occluded = occluded; } class="type">bool IsOccluded() { class="kw">return m_occluded; } class=class="str">"cmt">//... }; class="type">bool CElement::OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { for (class="type">int i = m_child_count - class="num">1; i >= class="num">0; i--) { m_children[i].SetOccluded(IsOccluded()); if (m_children[i].OnChartEvent(id, lparam, dparam, sparam)) SetOccluded(true); } class=class="str">"cmt">//Check dragging start if (id == CHARTEVENT_MOUSE_MOVE && !IsOccluded()) { if (IsMouseHovering() && m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN) m_dragging = true; else if (m_dragging && m_inputs.GetLeftMouseState() == INPUT_STATE_UP) m_dragging = false; } class="kw">return OnEvent(id, lparam, dparam, sparam) || IsMouseDragging() || IsOccluded(); } class="type">bool CDragElement::OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { if (IsOccluded()) class="kw">return false; if (id != CHARTEVENT_MOUSE_MOVE) class="kw">return false; class=class="str">"cmt">//... }