图形界面多窗口与优先级系统:从单窗到多窗的库结构铺垫(基础篇)
🪟

图形界面多窗口与优先级系统:从单窗到多窗的库结构铺垫(基础篇)

(1/3)· 很多 MQL5 界面写到对话框就卡住,只因没给窗口优先级留后路

新手友好 第 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 在高波动行情里最容易暴露。

MQL5 / C++
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 撞车。

MQL5 / C++
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 验证层级切换。

MQL5 / C++
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:
把窗口状态诊断交给小布
这些多窗口活动态与控件屏蔽逻辑,小布盯盘的 AIGC 已内置到品种页的诊断面板,打开即可看当前表单层级是否异常,你只管调架构。

常见问题

是的,基础库只给主窗口和对话框两个标识,未启用多窗口时同一时间仅一个表单可交互,需要扩展才能叠加多个控件表单。
可以,小布盯盘的 AIGC 诊断模块能读取当前 EA 界面的表单状态与事件拦截情况,提示哪些控件因失去活动态被屏蔽。
多窗口可能同时开启,关闭后需知道回退到哪个窗口为活动态,否则界面会丢焦点或控件无响应。
应放到库的 Defines.mqh 中与其他标识符并列,便于事件系统统一识别和处理窗口打开消息。