图形界面 I: 图形界面的动画 (第三章)·综合运用
🖱️

图形界面 I: 图形界面的动画 (第三章)·综合运用

(3/3)· 从光标追踪到控件响应,把静态面板变成会呼吸的交易界面

案例拆解 第 3/3 篇
很多自行封装的 MQL5 面板在图表上纹丝不动,只因没开启光标追踪属性,控件永远收不到鼠标坐标。更隐蔽的坑是指标挂在子窗口时 Y 轴不校正,点上去全是偏移。这两处不处理,再好看的界面也只是摆设。

「让自定义表单在MT5图表上跟手移动」

做图表UI库时,所有交互事件都先汇聚到 CWndEvents::ChartEvent()。这个方法第一步必须判空:若窗口指针数组大小为0(即 CWndContainer::WindowsTotal()<1),直接 return,避免后续对空容器操作崩在运行时。 事件进来后先 InitChartEventsParams() 初始化类内成员,再分两条线走:CheckElementsEvents() 轮询各控件事件,ChartEventMouseMove() 专门跟鼠标。当前测试阶段只有一个窗口,代码里临时写死用 m_wnd[0],多窗口架构 later 必须改索引逻辑。 移动表单的核心是自己写的 MovingWindow():先取窗口 X()/Y() 坐标,调窗口自身 Moving(x,y),再循环把 m_wnd[0].m_elements[] 里每个控件同步 Moving(x,y)。每次坐标变了记得重绘图表,否则视觉上不动。 载入EA后发现窗口拖不动,根因在 CWindow 构造函数里 m_movable 默认 false。开发者得在 CProgram::CreateWindow() 里显式开权限,否则用户侧永远锁死。外汇/贵金属图表挂EA改界面属高风险操作,建议先在模拟盘图表验完再上实盘。 图表尺寸变化会触发 CHARTEVENT_CHART_CHANGE,滚动也会发。所以在 CWindow::OnEvent() 末尾要加判断:仅当鼠标左键按住时才取图表尺寸、做越界修正,松手状态跳过以省CPU。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理程序                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::ChartEvent(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">//--- 如果数组为空, 退出
   if(CWndContainer::WindowsTotal()<class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- 初始化事件参数成员变量
   InitChartEventsParams(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- 验证界面控件事件
   CheckElementsEvents();
class=class="str">"cmt">//--- 鼠标移动事件
   ChartEventMouseMove();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 验证控件事件                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::CheckElementsEvents(class="type">void)
  {
   class="type">int elements_total=CWndContainer::ElementsTotal(class="num">0);
   for(class="type">int e=class="num">0; e<elements_total; e++)
      m_wnd[class="num">0].m_elements[e].OnEvent(m_id,m_lparam,m_dparam,m_sparam);
  }
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">private:
   class=class="str">"cmt">//--- 移动窗口
   class="type">void                MovingWindow(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 移动窗口                                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::MovingWindow(class="type">void)
  {
class=class="str">"cmt">//--- 移动窗口
   class="type">int x=m_windows[class="num">0].X();
   class="type">int y=m_windows[class="num">0].Y();
   m_windows[class="num">0].Moving(x,y);
class=class="str">"cmt">//--- 移动控件
   class="type">int elements_total=CWndContainer::ElementsTotal(class="num">0);
   for(class="type">int e=class="num">0; e<elements_total; e++)
      m_wnd[class="num">0].m_elements[e].Moving(x,y);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 鼠标移动图表事件                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+

拖拽窗口时的坐标与重绘逻辑

在 MT5 自定义控件里,鼠标移动事件首先要过滤噪声。CWndEvents::ChartEventMouseMove 只在 m_id 等于 CHARTEVENT_MOUSE_MOVE 时才进入 MovingWindow 与重绘,其余事件直接 return,避免每帧无谓计算。 窗口创建时给的可视参数很直接:左上角锚点 (1,1),宽高各 200 像素,Movable(true) 打开后用户才能拖动。下面这段是核心骨架,逐行拆开看更清楚。 图表属性变更(比如手动缩放或拖动主图)触发 CHARTEVENT_CHART_CHANGE 时,若鼠标处于未按下状态(NOT_PRESSED),才调用 SetWindowProperties 重算尺寸并 UpdateWindowXY 修正坐标,防止浮窗错位。这意味着按住拖拽过程中不会频繁回贴,松手才校准。 把 Comment 里打印的 x/y/rel_y/w.x/w.y/subwin 等字段挂到图表上,能实时看到浮窗相对坐标。外汇与贵金属图表波动剧烈,这类自绘控件在高波动时可能掉帧,建议在模拟盘先验证重绘频率。

MQL5 / C++
class="type">void CWndEvents::ChartEventMouseMove(class="type">void)
  {
class=class="str">"cmt">//--- 如果不是光标移动事件, 退出处理
   if(m_id!=CHARTEVENT_MOUSE_MOVE)
      class="kw">return;
class=class="str">"cmt">//--- 移动窗口
   MovingWindow();
class=class="str">"cmt">//--- 重绘图表
   m_chart.Redraw();
  }
::Comment("x: ",x,"\n",
         "y: ",y,"\n",
         "rel_y: ",rel_y,"\n",
         "w.x: ",m_x,"\n",
         "w.y: ",m_y,"\n",
         "subwin: ",subwin,"\n",
         "m_subwin: ",m_subwin,"\n",
         "clamping mode: ",m_clamping_area_mouse);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建用于控件的表单                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateWindow(const class="type">class="kw">string caption_text)
  {
class=class="str">"cmt">//--- 在窗口数组中增加一个窗口指针
   CWndContainer::AddWindow(m_window);
class=class="str">"cmt">//--- 坐标
   class="type">int x=class="num">1;
   class="type">int y=class="num">1;
class=class="str">"cmt">//--- 属性
   m_window.Movable(true);
   m_window.XSize(class="num">200);
   m_window.YSize(class="num">200);
class=class="str">"cmt">//--- 创建一个表单
   if(!m_window.CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
      class="kw">return(false);
class=class="str">"cmt">//---
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 图表事件处理函数                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::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">//--- 图表属性改变的事件
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      class=class="str">"cmt">//--- 如果是松开按键
      if(m_clamping_area_mouse==NOT_PRESSED)
        {
         class=class="str">"cmt">//--- 取得窗口大小
         SetWindowProperties();
         class=class="str">"cmt">//--- 调整坐标
         UpdateWindowXY(m_x,m_y);
        }
      class="kw">return;
     }
  }

◍ 用毫秒级计时器捕捉鼠标掠过

控件在光标掠过瞬间变色,靠的是轮询而不是图表事件。MQL5 默认禁用计时器,得由界面库主动开启;秒级 EventSetTimer 太慢,一秒延迟在 hover 交互里根本不可用,所以只能上 EventSetMillisecondTimer。 参考资料写明,毫秒计时器最小间隔约 10–16 毫秒。我们在 Defines.mqh 里把步长定为 16 毫秒(#define TIMER_STEP_MSC (16)),这个粒度足够让颜色切换看起来无延迟。 计时器只在 CWndEvents 构造函数里、且非策略测试器环境下启动。测试器强制最小间隔为一秒,即便调用了毫秒函数也无效,而且图形对象操作在测试器里有限制,所以直接 ::EventSetMillisecondTimer(TIMER_STEP_MSC) 并配合 ::MQLInfoInteger(MQL_TESTER) 判断。析构时用 ::EventKillTimer() 收掉。 每个控件靠 CElement 的虚方法 OnEventTimer() 响应,CWndEvents::CheckElementsEventsTimer() 负责遍历所有控件并触发各自实现。CProgram::OnTimerEvent() 只需调基类同名方法,就能把 hover 检查塞进程序计时器事件流。 CWindow 里落地了 ChangeObjectsColor():MouseFocus() 返回当前有焦点的对象,CheckMouseFocus() 在光标移动时更新焦点,早先载入的两张图标用 State() 切换,CElement::ChangeObjectColor() 改指定部位颜色。光标进窗口区域时表头背景色变,传对象名给 ChangeObjectColor() 还能顺带改边框。编译后挂到图表,hover 表单即见变色。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  Defines.mqh |
class=class="str">"cmt">//|                     Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                            [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- 时间步长 (毫秒)
class="macro">#define TIMER_STEP_MSC(class="num">16)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWndEvents::CWndEvents(class="type">void)
  {
class=class="str">"cmt">//--- 启用计时器
   if(!::MQLInfoInteger(MQL_TESTER))
      ::EventSetMillisecondTimer(TIMER_STEP_MSC);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 析构函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWndEvents::~CWndEvents(class="type">void)
  {
class=class="str">"cmt">//--- 删除计时器
   ::EventKillTimer();
  }
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">private:
   class=class="str">"cmt">//--- 根据计时器检查所有控件的事件
   class="type">void                CheckElementsEventsTimer(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据计时器检查所有控件的事件                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::CheckElementsEventsTimer(class="type">void)
  {
   class="type">int elements_total=CWndContainer::ElementsTotal(class="num">0);

「窗口对象靠计时器刷新焦点色」

自定义 GUI 里,鼠标悬停改色不能等点击事件,得靠定时器轮询每个元素的焦点状态。下面这段就是窗口类在计时器触发时统一重绘控件颜色的实现。 CWindow::OnEventTimer() 被上层事件管理器逐个元素调用,内部只做一件事:执行 ChangeObjectsColor()。后者先读三个按钮的 MouseFocus() 布尔值,通过 State() 切换图标;再用 CElement::ChangeObjectColor() 改表头背景色,传入普通色、悬停色和颜色数组三个参数。 事件总入口 CWndEvents::OnTimerEvent() 先判断 WindowsTotal() 小于 1 就直接 return,避免空数组访问报错;随后 CheckElementsEventsTimer() 派发到各元素,最后 m_chart.Redraw() 强制重绘。CProgram::OnTimerEvent() 只是透传,方便主程序挂接。 在 MT5 里跑这套,把 OnTimerEvent 挂到 ChartSetInteger(0,CHART_EVENT_TIMER,true) 后,悬停响应延迟通常在一帧内;若元素超过 200 个,Redraw 频次可能拖累主图帧率,建议只在焦点态变化时才重绘。外汇与贵金属图表加载自定义面板属高风险操作,误写对象名可能清掉已有图形。

MQL5 / C++
for(class="type">int e=class="num">0; e<elements_total; e++)
      m_wnd[class="num">0].m_elements[e].OnEventTimer();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计时器                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::OnTimerEvent(class="type">void)
  {
class=class="str">"cmt">//--- 如果数组为空, 退出
   if(CWndContainer::WindowsTotal()<class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- 根据计时器
   CheckElementsEventsTimer();
class=class="str">"cmt">//--- 重绘图表
   m_chart.Redraw();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计时器                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnTimerEvent(class="type">void)
  {
   CWndEvents::OnTimerEvent();
  }
class CWindow: class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 修改表单对象的颜色
  class="type">void                ChangeObjectsColor(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 当光标掠过对象时改变它的颜色                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::ChangeObjectsColor(class="type">void)
  {
class=class="str">"cmt">//--- 修改按钮的图标
   m_button_rollup.State(m_button_rollup.MouseFocus());
   m_button_unroll.State(m_button_unroll.MouseFocus());
   m_button_close.State(m_button_close.MouseFocus());
class=class="str">"cmt">//--- 修改表头的颜色
   CElement::ChangeObjectColor(m_caption_bg.Name(),CElement::MouseFocus(),OBJPROP_BGCOLOR,
                               m_caption_bg_color,m_caption_bg_color_hover,m_caption_color_bg_array);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计时器                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::OnEventTimer(class="type">void)
  {
class=class="str">"cmt">//--- 修改表单对象的颜色
   ChangeObjectsColor();
  }

窗口拖拽与焦点事件的落地衔接

这一节把表单可移动和控件响应鼠标光标的能力补进了 CWindow 类骨架,下一步是继续扩方法,用点击控件直接驱动表单管理。从用户反馈看,MT5 1340 版在 Win10 下曾出现面板设了 Movable=true 仍拖不动的情况,回退到前章源码才正常,说明库版本错配会直接废掉移动特性。 切换时间框架时图块飞回 (x=0,y=0) 是另一处坑,社区里提到可用全局变量或写文件存坐标,但作者原版没给现成持久化方案,只能自己在 ChartEvent 里接 ON_MOUSE_FOCUS_SET 后落盘。 下面这段是从列表控件里摘的焦点处理片段,重点在 ON_MOUSE_FOCUS_SET 映射到 OnItemFocusSet,选中行后发自定义事件通知界面重绘: 虚拟方法 OnItemFocusSet 接收行索引,内部先 Select 再 EventChartCustom 抛 ON_CHANGE,返回 true 表示已消化事件;EVENT_MAP 里把索引事件绑到 m_rows 容器,和标准库只调 ChartEvent 漏掉焦点事件的区别就在这里。

MQL5 / C++
  class="kw">virtual class="type">bool        OnItemFocusSet(const class="type">int index);
  class=class="str">"cmt">//--- 重新绘制
  class="type">bool                Redraw(class="type">void);
  class="type">bool                RowState(const class="type">int index,const class="type">bool select);
  class="type">bool                CheckView(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 图表事件的通用处理程序                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CListView)
ON_INDEXED_EVENT(ON_CLICK,m_rows,OnItemClick)
ON_INDEXED_EVENT(ON_MOUSE_FOCUS_SET,m_rows,OnItemFocusSet)
EVENT_MAP_END(CWndClient)
class="type">bool CListView::OnItemFocusSet(const class="type">int index)
  {
class=class="str">"cmt">//--- 选择 "行"
   Select(index+m_offset);
class=class="str">"cmt">//--- 发送通知
   EventChartCustom(CONTROLS_SELF_MESSAGE,ON_CHANGE,m_id,class="num">0.0,m_name);
class=class="str">"cmt">//--- 已处理
   class="kw">return(true);
  }
把界面响应诊断交给小布
小布盯盘已内置常见 MQL5 面板交互异常的检测逻辑,打开对应品种页即可看到控件事件是否漏接、坐标是否错位,你只管调策略。

常见问题

大概率未在 CChart 实例上启用光标位置追踪,且未处理鼠标按键的四种状态枚举,导致 OnEvent 里拿不到有效坐标与按压信号。
需要在管理图形界面时判断应用程序所在窗口,若非主窗口则对 Y 坐标做偏移校正,否则系统返回的是主图坐标系。
可以,小布盯盘的品种页会标记控件事件漏接与坐标错位类异常,省去你逐行打点查 CWindow::OnEvent 的时间。
不必,只需在悬停状态切换时触发局部颜色属性更新并调用对应重绘方法,避免每帧全量刷新拖慢图表。
本篇第三章专门覆盖测试表单在图表上的移动,关于库结构准备见《图形界面 I: 库结构的准备工作》·基础篇。