图形界面多窗口与优先级系统:从单窗到多窗的库结构铺垫(基础篇)
(1/3)· 很多 MQL5 界面写到对话框就卡住,只因没给窗口优先级留后路
「多窗口模式与鼠标优先级怎么搭」
在 MT5 里跑图形界面,多窗口模式不是简单叠控件,而是每个窗口有自己的消息循环与绘制上下文。作者 Anatoli Kazharski 在 2016-04-25 发布的示例里,用 4423 次查看、13 条讨论验证了这套结构在实盘面板里的可行性。 左键优先级系统是关键补丁:当多个浮动窗口重叠时,必须让最上层且命中控件区域的窗口先吃消息,否则会出现点 A 窗却触发 B 窗事件。原示例里靠一个层级标记数组来排序命中测试。 想验证就开 MT5 把示例拖进指标测试窗,故意让两个面板交叠,用鼠标点交界区,看事件是不是只落在顶层。外汇与贵金属图表上叠多窗口面板属高风险操作环境,消息错乱可能误导下单判断。
◍ 多窗口界面与点击优先级从哪切入
上一章已经把状态栏、工具提示这类信息元件跑通了,这一章要做的是把库再扩一层:给 MQL 程序搭出多窗口界面。没有多窗口,复杂面板的空间分配就只能硬挤在一个框里,后期维护成本直线上升。 另一个绕不开的点是鼠标左键点击的优先级系统。图形对象在 MT5 里是平铺在图表上的,如果不给控件排好响应次序,点下去到底谁吃这个事件就会乱套,按钮和滑块可能完全不理你。 库的完整版本按章节末尾链接下载,解压后必须保持压缩包里的目录层级原样拷进终端对应文件夹,否则 Include 路径解析会直接失败。外汇与贵金属品种波动剧烈,这类自定义面板仅作辅助,实盘前请在策略测试器里先验证交互逻辑。
给界面库加上多窗口切换的底子
早先的 ENUM_WINDOW_TYPE 只定义了主窗口 W_MAIN 和对话框 W_DIALOG 两个标识,库里实际只跑单窗口。要让多个控件表单同时挂在一个图形界面里,得先解决“当前哪个窗口在接管鼠标”的问题。 在事件主类 CWndEvents 里加一个 m_active_window_index 栏位,专门存当前活动窗口的索引。用户点按钮触发 ON_CLICK_BUTTON 后,应当在 CProgram::OnEvent() 里跟踪并调用 CWindow::Show() 把对话框调出来,但旧版 Show() 不会通知系统“有窗口开了”,这就是要补的洞。 我们在 Defines.mqh 里新增 ON_OPEN_DIALOG_BOX 标识(值 11),并在 CWindow::Show() 末尾发一条自定义图表事件,把元件 ID 和程序名一起带出去。非活动表单的标题要变色、鼠标掠过不改色、失去焦点时清掉颜色——这些状态靠 CWindow 里另写的两个取存索引方法加一个状态管理方法撑住。 关闭方向也得对称处理:Defines.mqh 再加 ON_CLOSE_DIALOG_BOX 和 ON_RESET_WINDOW_COLORS,CWindow::CloseDialogBox() 发消息时一并带上“之前活动窗口索引”,CWndEvents 收到后把匹配窗口隐藏、按传回的索引恢复上一层窗口并复位其点击优先级。这样 MT5 里拖出两个表单互相盖栈,才有可能不串焦点。 外汇与贵金属图表挂这种多窗口 EA 界面前,先在小周期回测里点开合十次以上,确认工具提示不会卡在屏蔽表单上——这类图形 Bug 在高波动行情里最容易暴露。
class CWndEvents : class="kw">public CWndContainer { class="kw">protected: class=class="str">"cmt">//--- 活动窗口的索引 class="type">int m_active_window_index; }; 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="macro">#define ON_OPEN_DIALOG_BOX(class="num">11) class=class="str">"cmt">// 打开对话框窗口的事件 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 显示窗口 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWindow::Show(class="type">void) { class=class="str">"cmt">//--- 使得所有对象可见 class=class="str">"cmt">//--- 可见状态 class=class="str">"cmt">//--- 焦点清零 class=class="str">"cmt">//--- 发送相关消息 ::EventChartCustom(m_chart_id,ON_OPEN_DIALOG_BOX,(class="type">long)CElement::Id(),class="num">0,m_program_name); } class CWindow : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 之前活动窗口的索引 class="type">int m_prev_active_window_index; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- (class="num">1) 保存 和 (class="num">2) 获取 之前活动窗口的索引
「窗口状态锁定与自定义事件分发」
CWindow 的 State() 用布尔开关切换窗口锁定:flag 为 false 时置 m_is_locked=true、标题底色切到 m_caption_bg_color_off,并抛出 EventChartCustom(m_chart_id, ON_RESET_WINDOW_COLORS, (long)CElement::Id(), 0, "") 广播清空颜色信号;flag 为 true 时解锁、恢复标题色并清掉鼠标焦点。 ON_RESET_WINDOW_COLORS 在 Defines.mqh 里被定义为常量 13,语义是通知表单上所有元件重置颜色,属于图形库内部事件闭环的一环。 CWndEvents 继承自 CWndContainer,私有方法 OnOpenDialogBox() 负责弹出对话框;在 ChartEventCustom() 里一旦该方法返回 true 就直接 return,说明对话框打开事件优先级高于最小化、最大化等其它自定义信号。 在 MT5 里新建面板时,若想复用这套锁定逻辑,直接抄 State() 的两个分支即可,注意 ON_RESET_WINDOW_COLORS 的 13 不要和你自己定义的自定义事件 ID 撞车。
class="type">void PrevActiveWindowIndex(class="kw">const class="type">int index) { m_prev_active_window_index=index; } class="type">int PrevActiveWindowIndex(class="type">void) class="kw">const { class="kw">return(m_prev_active_window_index); }; class="macro">#define ON_RESET_WINDOW_COLORS(class="num">13) class=class="str">"cmt">// 清空表单上所有元件的颜色 class CWindow : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 设置窗口的状态 class="type">void State(class="kw">const class="type">bool flag); }; class="type">void CWindow::State(class="kw">const class="type">bool flag) { if(!flag) { m_is_locked=true; m_caption_bg.BackColor(m_caption_bg_color_off); ::EventChartCustom(m_chart_id,ON_RESET_WINDOW_COLORS,(class="type">long)CElement::Id(),class="num">0,""); } else { m_is_locked=class="kw">false; m_caption_bg.BackColor(m_caption_bg_color); CElement::MouseFocus(class="kw">false); } } class CWndEvents : class="kw">public CWndContainer { class="kw">private: class="type">bool OnOpenDialogBox(class="type">void); }; class="type">void CWndEvents::ChartEventCustom(class="type">void) { if(OnOpenDialogBox()) class="kw">return; }
◍ 对话框弹出时怎么接管窗口层级
在 MT5 自定义 GUI 里,对话框类窗口往往需要「模态」效果:弹出一个表单时,其余表单得暂时失活。OnOpenDialogBox 这段处理就是干这个的,它先比对 m_id 是否为 CHARTEVENT_CUSTOM+ON_OPEN_DIALOG_BOX,再核对 m_sparam 是不是本程序名,避免别的 EA/脚本发来的自定义事件误触发。 匹配到的目标窗口会被 State(true) 激活,同时调用 SetZorders() 把鼠标左键点击优先级拉回;未匹配的窗口走 else 分支,State(false) 屏蔽掉,并对内部元件 ResetZorders() 清零点击优先级,实现「活动窗口关掉前别处点不动」。 激活窗口后还有两件事容易漏:一是把该窗口所有非表单、非下拉类元件 Show() 并显示优先级复位;二是找到上一个活动窗口里的工具提示,逐个 FadeOutTooltip() 淡出,防止旧提示浮在新窗口上。外汇与贵金属图表上跑这类界面属于高风险环境,事件冲突可能导致下单面板卡死,建议你在策略测试器里用自定义事件手动推一次 ON_OPEN_DIALOG_BOX 验证层级切换。
class="type">bool CWndEvents::OnOpenDialogBox(class="type">void) { class=class="str">"cmt">//--- 如果信号是打开对话框窗口 if(m_id!=CHARTEVENT_CUSTOM+ON_OPEN_DIALOG_BOX) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果消息是来自其他程序 if(m_sparam!=m_program_name) class="kw">return(true); class=class="str">"cmt">//--- 在窗口数组中迭代 class="type">int window_total=CWndContainer::WindowsTotal(); for(class="type">int w=class="num">0; w<window_total; w++) { class=class="str">"cmt">//--- 如果id匹配 if(m_windows[w].Id()==m_lparam) { class=class="str">"cmt">//--- 在弹出表单的表单中保存表单索引 m_windows[w].PrevActiveWindowIndex(m_active_window_index); class=class="str">"cmt">//--- 激活表单 m_windows[w].State(true); class=class="str">"cmt">//--- 恢复表单对象的鼠标左键点击优先级 m_windows[w].SetZorders(); class=class="str">"cmt">//--- 保存活动窗口的索引 m_active_window_index=w; class=class="str">"cmt">//--- 使活动窗口中的所有元件可见 class="type">int elements_total=CWndContainer::ElementsTotal(w); for(class="type">int e=class="num">0; e<elements_total; e++) { class=class="str">"cmt">//--- 跳过表单和下拉元件 if(m_wnd[w].m_elements[e].ClassName()=="CWindow" || m_wnd[w].m_elements[e].IsDropdown()) class="kw">continue; class=class="str">"cmt">//--- 使元件可见 m_wnd[w].m_elements[e].Show(); class=class="str">"cmt">//--- 恢复鼠标左键点击元件的优先级 m_wnd[w].m_elements[e].SetZorders(); } class=class="str">"cmt">//--- 隐藏工具提示 class="type">int tooltips_total=CWndContainer::TooltipsTotal(m_windows[w].PrevActiveWindowIndex()); for(class="type">int t=class="num">0; t<tooltips_total; t++) m_wnd[m_windows[w].PrevActiveWindowIndex()].m_tooltips[t].FadeOutTooltip(); } class=class="str">"cmt">//--- 其他表单将被屏蔽,直到活动窗口被关闭 else { class=class="str">"cmt">//--- 屏蔽表单 m_windows[w].State(class="kw">false); class=class="str">"cmt">//--- 表单元件鼠标左键点击优先级清零 class="type">int elements_total=CWndContainer::ElementsTotal(w); for(class="type">int e=class="num">0; e<elements_total; e++) m_wnd[w].m_elements[e].ResetZorders(); } } class=class="str">"cmt">//--- class="kw">return(true); } class CElement { class="kw">public: class=class="str">"cmt">//--- 元件颜色清除 class="kw">virtual class="type">void ResetColors(class="type">void) {} }; class CIconButton : class="kw">public CElement { class="kw">public: