图形界面 IV: 多窗口模式与优先级系统 (第二章)·进阶篇
「按钮与窗口的颜色复位逻辑」
在自建 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 的拦截生效了。贵金属与外汇界面开发属高风险定制,参数误用可能导致面板卡死。
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 确认区间不重叠。
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 界面前,先在策略测试器跑通逻辑,实盘界面卡死概率可能随窗口数上升而增大。
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 属高风险自定义工具,实盘前请在模拟环境反复切周期验证。
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 和窗体库的解耦方式决定了多按钮扩展时不用改派发主干,只增元素数组条目即可。
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); }