图形界面 I: 图形界面的动画 (第三章)·综合运用
(3/3)· 从光标追踪到控件响应,把静态面板变成会呼吸的交易界面
「让自定义表单在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。
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 等字段挂到图表上,能实时看到浮窗相对坐标。外汇与贵金属图表波动剧烈,这类自绘控件在高波动时可能掉帧,建议在模拟盘先验证重绘频率。
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 表单即见变色。
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 频次可能拖累主图帧率,建议只在焦点态变化时才重绘。外汇与贵金属图表加载自定义面板属高风险操作,误写对象名可能清掉已有图形。
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 漏掉焦点事件的区别就在这里。
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); }