图形界面 IV: 多窗口模式与优先级系统 (第二章)·进阶篇
📘

图形界面 IV: 多窗口模式与优先级系统 (第二章)·进阶篇

第 2/3 篇

「按钮与窗口的颜色复位逻辑」

在自建 GUI 框架里,图标按钮的 ResetColors() 负责把背景色和焦点状态清回默认。若按钮被设为双态模式且当前处于按下状态,该函数直接 return,避免把「已激活」的视觉反馈误擦掉。 具体实现中,m_button.BackColor(m_back_color) 恢复底色的原值,随后对 m_button 与基类 CElement 分别调用 MouseFocus(false),确保焦点框不再停留。这套写法对外汇 EA 面板有意义:切换周期或账户时,界面不会残留上一状态的颜色。 窗口级复位走 CWndEvents::OnResetWindowColors()。它先比对自定义事件 ID 是否等于 CHARTEVENT_CUSTOM+ON_RESET_WINDOW_COLORS,不符就返回 false 让其他处理器接手。命中后,用 m_lparam 在窗口数组里找对应 Id,定位到的窗口调用 ResetColors(),再遍历该窗口下 ElementsTotal(index) 个元件逐个清色。 最后 m_chart.Redraw() 触发一次重绘,界面才真正刷新。在 MT5 里跑这套,你可以故意发一个自定义事件号测试:若某双态按钮按住时颜色没被清掉,就说明 m_two_state && m_button_state 的拦截生效了。贵金属与外汇界面开发属高风险定制,参数误用可能导致面板卡死。

MQL5 / C++
class="type">void CIconButton::ResetColors(class="type">void)
  {
  if(m_two_state && m_button_state)
     class="kw">return;
  m_button.BackColor(m_back_color);
  m_button.MouseFocus(class="kw">false);
  CElement::MouseFocus(class="kw">false);
  }

class="type">bool CWndEvents::OnResetWindowColors(class="type">void)
  {
  if(m_id!=CHARTEVENT_CUSTOM+ON_RESET_WINDOW_COLORS)
     class="kw">return(class="kw">false);
  class="type">int index=WRONG_VALUE;
  class="type">int window_total=CWndContainer::WindowsTotal();
  for(class="type">int w=class="num">0; w<window_total; w++)
     {
     if(m_windows[w].Id()==m_lparam)
        {
        index=w;
        m_windows[w].ResetColors();
        break;
        }
     }
  if(index==WRONG_VALUE)
     class="kw">return(true);
  class="type">int elements_total=CWndContainer::ElementsTotal(index);
  for(class="type">int e=class="num">0; e<elements_total; e++)
     m_wnd[index].m_elements[e].ResetColors();
  m_chart.Redraw();
  class="kw">return(true);
  }

对话框关闭事件怎么从类里抛到图表

做 MT5 自定义 UI 时,对话框窗口不是直接销毁对象,而是先改可见状态再发自定义事件,让事件层去收尾。上面这段把关闭动作拆成了 CWindow 里的 CloseDialogBox() 和 CWndEvents 里的 OnCloseDialogBox() 两层。 核心常量 ON_CLOSE_DIALOG_BOX 被定义为 12,也就是 EventChartCustom 的 lparam 事件号偏移量。CloseDialogBox() 先把 IsVisible 置 false 隐藏自己,再调用 ::EventChartCustom(m_chart_id,12,Id(),m_prev_active_window_index,m_caption_text) 把窗口 ID 和标题文本甩给图表。 CWndEvents::ChartEventCustom() 里只判断 m_id 是否等于 CHARTEVENT_CUSTOM+12,命中就交给 OnCloseDialogBox() 处理并返回。这种解耦写法让你在 EA 里拦截对话框关闭、顺手还原主窗口焦点时,不必动 CWindow 内部逻辑。 别把事件号当随便填的数字。ON_CLOSE_DIALOG_BOX 占用了 12,若你自己的面板也用 EventChartCustom 且 lparam 撞到 12,关闭对话框可能误触发别的逻辑,建议在测试 EA 里打印 m_id 确认区间不重叠。

MQL5 / C++
class="macro">#define ON_CLOSE_DIALOG_BOX(class="num">12) class=class="str">"cmt">// 关闭对话框窗口事件
class CWindow : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 关闭对话框窗口
  class="type">void              CloseDialogBox(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 关闭对话框窗口                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::CloseDialogBox(class="type">void)
  {
class=class="str">"cmt">//--- 可见状态
   CElement::IsVisible(class="kw">false);
class=class="str">"cmt">//--- 发送相关消息
   ::EventChartCustom(m_chart_id,ON_CLOSE_DIALOG_BOX,CElement::Id(),m_prev_active_window_index,m_caption_text);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 关闭对话框或者程序                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWindow::CloseWindow(class="kw">const class="type">class="kw">string pressed_object)
  {
class=class="str">"cmt">//--- 如果不是按下关闭窗口按钮
   if(pressed_object!=m_button_close.Name())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 如果是主窗口
   if(m_window_type==W_MAIN)
     {
      class=class="str">"cmt">//--- ...
     }
class=class="str">"cmt">//--- 如果这是一个对话框窗口
   else if(m_window_type==W_DIALOG)
     {
      class=class="str">"cmt">//--- 关闭它
      CloseDialogBox();
     }
class=class="str">"cmt">//---
   class="kw">return(class="kw">false);
  }
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">private:
   class=class="str">"cmt">//--- 关闭对话框窗口
   class="type">bool              OnCloseDialogBox(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CHARTEVENT_CUSTOM 事件                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::ChartEventCustom(class="type">void)
  {
class=class="str">"cmt">//--- 如果信号是最小化表单
class=class="str">"cmt">//--- 如果信号是最大化表单
class=class="str">"cmt">//--- 如果信号是需要隐藏菜单项下的上下文菜单
class=class="str">"cmt">//--- 如果信号是隐藏所有上下文菜单
class=class="str">"cmt">//--- 如果信号是打开对话框窗口
class=class="str">"cmt">//--- 如果信号是关闭一个对话框窗口
   if(OnCloseDialogBox())
      class="kw">return;
class=class="str">"cmt">//--- 如果信号是清除指定表单上所有元件的颜色
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ON_CLOSE_DIALOG_BOX 事件                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWndEvents::OnCloseDialogBox(class="type">void)
  {
class=class="str">"cmt">//--- 如果信号是关闭一个对话框窗口
   if(m_id!=CHARTEVENT_CUSTOM+ON_CLOSE_DIALOG_BOX)
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 在窗口数组中迭代

◍ 按消息参数切换并屏蔽非活动表单

在自定义 GUI 的消息处理函数里,常需要根据 lparam / dparam 携带的窗口 ID 做焦点切换。下面这段逻辑先遍历容器里全部窗口,命中 ID 后立刻把当前表单设为非激活并隐藏其所有子元素。 遍历用 CWndContainer::WindowsTotal() 拿到窗口总数,逐个比 m_windows[w].Id() 与 m_lparam;匹配后调用 State(false) 屏蔽表单,再用 ElementsTotal(w) 取元素数,循环 Hide() 每个 m_elements[e],确保界面不残留可点击控件。 随后把 m_dparam 指向的前一个窗口 State(true) 激活,Chart.Redraw() 重绘,break 退出循环。最后把 m_active_window_index 设为 int(m_dparam),并对新活动窗口的元素调用 SetZorders() 恢复鼠标左键点击优先级,返回 true 表示消息已处理。 开 MT5 把这段塞进你的 CApp 派生类消息分支,改 m_lparam / m_dparam 的传值方式,就能验证多表单焦点切换是否如预期。外汇与贵金属图表上挂这类 EA 界面前,先在策略测试器跑通逻辑,实盘界面卡死概率可能随窗口数上升而增大。

MQL5 / C++
  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].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].Hide();
      class=class="str">"cmt">//--- 激活之前的表单
      m_windows[class="type">int(m_dparam)].State(true);
      class=class="str">"cmt">//--- 重画图表
      m_chart.Redraw();
      break;
      }
    }
class=class="str">"cmt">//--- 设置前面窗口的索引
  m_active_window_index=class="type">int(m_dparam);
class=class="str">"cmt">//--- 恢复活动窗口的鼠标左键点击优先级
  class="type">int elements_total=CWndContainer::ElementsTotal(m_active_window_index);
  for(class="type">int e=class="num">0; e<elements_total; e++)
    m_wnd[m_active_window_index].m_elements[e].SetZorders();
class=class="str">"cmt">//---
  class="kw">return(true);
  }

「多窗口面板的挂载与失活坑」

在 EA 图形界面里同时拉起三个表单并不复杂:主窗口 W_MAIN 之外再建两个 CWindow 实例,角色设为 W_DIALOG,就得到一主两弹窗的结构。只有其中一个处于活动态,其余靠按钮事件触发显隐,这是多窗口测试的基础骨架。 挂对话框时坐标不能留默认零值。程序加载到图表时表单 x、y 默认都是 0,必须显式设位,本例用了 x=1、y=20;之后窗口可自由拖动,切品种或周期后它仍停在最后位置。若想每次加载都回初始位,就把这段坐标保持逻辑删掉。 控件附加到对话框有两条硬规则:传参必须是目标表单指针,入库时还要指定表单索引——本例对话框是索引 1。按钮文字带省略号(如 «Icon Button 6...»)是给用户的通行暗示:按了会弹窗。 切品种或周期时弹窗消失,是因为销毁流程里没把子窗交回主窗管。修复点在 CWndEvents::Destroy():先把主窗索引置为活动,再激活主窗、其余置非活动,图形界面才不会被系统丢弃且不响应操作。外汇与贵金属图表多窗口 EA 属高风险自定义工具,实盘前请在模拟环境反复切周期验证。

MQL5 / C++
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 表单 class="num">2
   CWindow            m_window2;
   class=class="str">"cmt">//--- 图标按钮
   CIconButton        m_icon_button6;
   CIconButton        m_icon_button7;
   CIconButton        m_icon_button8;
   class=class="str">"cmt">//--- 表单 class="num">3
   CWindow            m_window3;
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 表单 class="num">2
   class="type">bool               CreateWindow2(class="kw">const class="type">class="kw">string text);
   class=class="str">"cmt">//--- 图标按钮
class="macro">#define ICONBUTTON6_GAP_X(class="num">7)
class="macro">#define ICONBUTTON6_GAP_Y(class="num">25)
   class="type">bool               CreateIconButton6(class="kw">const class="type">class="kw">string text);
class="macro">#define ICONBUTTON7_GAP_X(class="num">7)
class="macro">#define ICONBUTTON7_GAP_Y(class="num">50)
   class="type">bool               CreateIconButton7(class="kw">const class="type">class="kw">string text);
class="macro">#define ICONBUTTON8_GAP_X(class="num">7)
class="macro">#define ICONBUTTON8_GAP_Y(class="num">75)
   class="type">bool               CreateIconButton8(class="kw">const class="type">class="kw">string text);
   class=class="str">"cmt">//--- 表单 class="num">3
   class="type">bool               CreateWindow3(class="kw">const class="type">class="kw">string text);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建交易面板                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateTradePanel(class="type">void)
  {
class=class="str">"cmt">//--- 为控件创建表单1
class=class="str">"cmt">//--- 创建控件:
class=class="str">"cmt">//    主菜单
class=class="str">"cmt">//--- 上下文菜单
class=class="str">"cmt">//--- 创建状态栏
class=class="str">"cmt">//--- 图标按钮
class=class="str">"cmt">//--- 创建表单2的控件
   if(!CreateWindow2("Icon Button class="num">1"))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 图标按钮
   if(!CreateIconButton6("Icon Button class="num">6..."))
      class="kw">return(class="kw">false);
   if(!CreateIconButton7("Icon Button class="num">7"))
      class="kw">return(class="kw">false);
   if(!CreateIconButton8("Icon Button class="num">8"))

对话框与图标按钮的实建落点

MT5 的 EasyAndFastGUI 封装里,表单2用 CProgram::CreateWindow2 拉起一个 160×160 像素的可移动对话框,图标走 Images\EasyAndFastGUI\Icons\bmp16\coins.bmp,标题栏底色 clrCornflowerBlue,悬停态转 C'150,190,240'。坐标逻辑是若之前没存过位置就退回 (1,20) 左上角起窗,避免和图表主区重叠。 图标按钮6在 CreateIconButton6 里挂到表单2的指针上,偏移量由 ICONBUTTON6_GAP_X / GAP_Y 控制,本体尺寸锁死 146×22,离屏两态图标分别是 script.bmp 与 script_colorless.bmp。背景色三段:常态 clrLightGray、悬停 C'193,218,255'、按下 C'153,178,215',肉眼能直接区分交互阶段。 建完控件后必须 CWndContainer::AddToElementsArray(1,m_icon_button6) 把指针塞进元素数组第1槽,否则点击事件不会被统一派发。外汇与贵金属 GUI 脚本涉及实时下单,属于高风险操作,参数改动前建议在模拟盘验证。 事件侧靠 OnEvent 接 CHARTEVENT_CUSTOM+ON_CLICK_BUTTON 判断命中,自定义 id 和窗体库的解耦方式决定了多按钮扩展时不用改派发主干,只增元素数组条目即可。

MQL5 / C++
class="type">bool CProgram::CreateWindow2(class="kw">const class="type">class="kw">string caption_text)
  {
  CWndContainer::AddWindow(m_window2);
  class=class="str">"cmt">//--- 坐标
  class="type">int x=(m_window2.X()>class="num">0) ? m_window2.X() : class="num">1;
  class="type">int y=(m_window2.Y()>class="num">0) ? m_window2.Y() : class="num">20;
  class=class="str">"cmt">//--- 属性
  m_window2.Movable(true);
  m_window2.WindowType(W_DIALOG);
  m_window2.XSize(class="num">160);
  m_window2.YSize(class="num">160);
  m_window2.IconFile("Images\\EasyAndFastGUI\\Icons\\bmp16\\coins.bmp");
  m_window2.CaptionBgColor(clrCornflowerBlue);
  m_window2.CaptionBgColorHover(C&class="macro">#x27;class="num">150,class="num">190,class="num">240&class="macro">#x27;);
  class=class="str">"cmt">//--- 创建表单
  if(!m_window2.CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
    class="kw">return(class="kw">false);
  class="kw">return(true);
  }

class="type">bool CProgram::CreateIconButton6(class="kw">const class="type">class="kw">string button_text)
  {
  m_icon_button6.WindowPointer(m_window2);
  class="type">int x=m_window2.X()+ICONBUTTON6_GAP_X;
  class="type">int y=m_window2.Y()+ICONBUTTON6_GAP_Y;
  m_icon_button6.TwoState(class="kw">false);
  m_icon_button6.ButtonXSize(class="num">146);
  m_icon_button6.ButtonYSize(class="num">22);
  m_icon_button6.LabelColor(clrBlack);
  m_icon_button6.LabelColorPressed(clrBlack);
  m_icon_button6.BorderColorOff(clrWhite);
  m_icon_button6.BackColor(clrLightGray);
  m_icon_button6.BackColorHover(C&class="macro">#x27;class="num">193,class="num">218,class="num">255&class="macro">#x27;);
  m_icon_button6.BackColorPressed(C&class="macro">#x27;class="num">153,class="num">178,class="num">215&class="macro">#x27;);
  m_icon_button6.IconFileOn("Images\\EasyAndFastGUI\\Icons\\bmp16\\script.bmp");
  m_icon_button6.IconFileOff("Images\\EasyAndFastGUI\\Icons\\bmp16\\script_colorless.bmp");
  if(!m_icon_button6.CreateIconButton(m_chart_id,m_subwin,button_text,x,y))
    class="kw">return(class="kw">false);
  CWndContainer::AddToElementsArray(class="num">1,m_icon_button6);
  class="kw">return(true);
  }

常见问题

在对话框关闭事件里调用颜色复位方法,把按钮和窗口的默认色值写回对象属性即可,别只靠鼠标移出事件。
用自定义消息参数把关闭事件从类抛到图表消息循环,主程序按消息类型切换并屏蔽非活动表单。
可以,小布能扫描当前图表挂载的界面对象,标出未正确失活或残留响应的面板,你按提示手动卸载就行。
以锚定窗口左上角为基准算相对偏移,别用绝对坐标,建完用调试输出打印实际落点核对。
在消息分发处按当前活动窗口 ID 过滤,非活动表单直接返回不处理,避免误触发。