GUI:利用 MQL 创建您自己的图形库的提示和技巧·进阶篇
📘

GUI:利用 MQL 创建您自己的图形库的提示和技巧·进阶篇

第 2/3 篇

用鼠标移动事件替代点击做界面状态机

在 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 占用。

MQL5 / C++
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 对象生命周期高风险,指针失效可能引发图表事件崩坏。

MQL5 / C++
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 里只做最小重绘,重计算挪到别处。

MQL5 / C++
<span class="keyword">class</span> CProgram
{
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp; <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_needs_redraw;
<span class="comment">class=class="str">"cmt">//...</span>
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; CProgram();
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">OnTimer</span>();
<span class="comment">class=class="str">"cmt">//...</span>
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RequestRedraw()
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_needs_redraw = <span class="macro">true</span>;
&nbsp;&nbsp; }
};
<span class="keyword">class="type">void</span> CProgram::<span class="functions">OnTimer</span>(<span class="keyword">class="type">void</span>)
{
&nbsp;&nbsp; <span class="keyword">if</span> (m_needs_redraw)
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_needs_redraw = <span class="macro">false</span>;
&nbsp;&nbsp; }
}
<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>)
{
&nbsp;&nbsp; ::<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>);
&nbsp;&nbsp; m_element_holder.CreateChildren();
&nbsp;&nbsp; <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">//|&nbsp;Send event recursively and respond to it(for this element)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|</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> &amp;lparam, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;dparam, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">string</span> &amp;sparam)
{
&nbsp;&nbsp; <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = m_child_count - <span class="number">class="num">1</span>; i &gt;= <span class="number">class="num">0</span>; i--)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_children[i].<span class="functions">OnChartEvent</span>(id, lparam, dparam, sparam);
&nbsp;&nbsp; <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">//|&nbsp;Check if mouse is hovering any child element of this object&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|</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>)
{
&nbsp;&nbsp; <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; m_child_count; i++)
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (m_children[i].IsMouseHovering())
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">return</span> <span class="macro">true</span>;
&nbsp;&nbsp; }
&nbsp;&nbsp; <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 的开关接在鼠标事件里,立刻能测出拖按钮时图表是否还乱跑。

MQL5 / C++
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,拖一下就能看到偏移量恒定。

MQL5 / C++
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,借此验证遮挡链是否按预期截断事件。外汇与贵金属图表叠加自定义控件时波动剧烈,这类交互逻辑需先在模拟环境跑稳再上实盘。

MQL5 / C++
   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">//...
}

常见问题

用鼠标移动事件(OnMouseMove)驱动界面状态机,在事件里判断坐标落在哪个控件区,直接改按钮的悬停/按下标志并触发重绘,不必等点击。
给每个控件建独立事件引用ID,状态机只认ID不认顺序;调试时打印事件返回的控件ID和当前状态,看哪一步引用错配。
小布可根据你描述的控件和交互逻辑,直接产出事件接驳与状态切换的代码骨架,你只补具体样式和交易指令。
在悬停事件里置一个遮挡标志,图表收到鼠标滚轮前先判遮挡态,若面板区激活就吞掉滚动事件,只处理面板内逻辑。
用约16毫秒计时器在拖拽里收尾重绘,每次鼠标移动只记目标坐标,计时器统一刷位置并清递归,避免事件堆叠卡顿。