图形界面 II: 主菜单元件 (第四章))·进阶篇
(2/3)· 主菜单类封装、非活动控件拦截与事件通信,MQL5 界面库实战的硬骨头
◍ 菜单指针越界与窗口容器结构
在 MT5 自定义界面库里,按索引取主菜单或上下文菜单指针时,先拿 ArraySize 测数组长度。若 array_size<1 说明主菜单一个项都没挂,直接在终端日志打印报错提示,但函数不会中断,仍会继续跑后面的越界修正逻辑。 索引修正用了一行三元表达式:i=(index>=array_size)? array_size-1 : (index<0)? 0 : index。也就是索引超上限就钳到末位,低于 0 就钳到 0,正常区间才原样返回,这能避免 GetPointer(m_items[i]) 触发越界崩溃。 WndContainer.mqh 里 CWndContainer 类用 WindowElements 结构存 CMenuBar* 数组,并暴露 MenuBarsTotal(const int window_index) 接口统计指定窗口的主菜单数量。其内部先判 window_index>=ArraySize(m_wnd),越界就 Print(PREVENTING_OUT_OF_RANGE),这对多图表 EA 面板排查对象丢失有帮助。 外汇与贵金属 MT5 界面开发属高风险定制,GUI 逻辑错误可能让 EA 面板失效但不影响下单,建议开 MT5 把这段越界钳制抄进自己的 CMenuBar 派生类验证边界行为。
class="type">int array_size=::ArraySize(m_items); class=class="str">"cmt">//--- 如果主菜单不包含任何菜单项,就报错 if(array_size<class="num">1) { ::Print(__FUNCTION__," > 将调用此方法, " "主菜单至少要包含一个菜单项!"); } class=class="str">"cmt">//---如果超出范围就做调整 class="type">int i=(index>=array_size)? array_size-class="num">1 : (index<class="num">0)? class="num">0 : index; class=class="str">"cmt">//--- 返回指针 class="kw">return(::GetPointer(m_items[i])); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据索引返回上下文菜单的指针 | class=class="str">"cmt">//+------------------------------------------------------------------+ CContextMenu *CMenuBar::ContextMenuPointerByIndex(class="kw">const class="type">int index) { class="type">int array_size=::ArraySize(m_contextmenus); class=class="str">"cmt">//--- 如果主菜单不包含任何菜单项,就报错 if(array_size<class="num">1) { ::Print(__FUNCTION__," > 将调用此方法, " "主菜单至少要包含一个菜单项!"); } class=class="str">"cmt">//---如果超出范围就做调整 class="type">int i=(index>=array_size)? array_size-class="num">1 : (index<class="num">0)? class="num">0 : index; class=class="str">"cmt">//--- 返回指针 class="kw">return(::GetPointer(m_contextmenus[i])); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| WndContainer.mqh | class=class="str">"cmt">//| Copyright class="num">2015, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "MenuBar.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于保存所有界面对象的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CWndContainer { class="kw">protected: class=class="str">"cmt">//--- 元件数组结构 class="kw">struct WindowElements { class=class="str">"cmt">//--- 主菜单数组 CMenuBar *m_menu_bars[]; }; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 主菜单数量 class="type">int MenuBarsTotal(class="kw">const class="type">int window_index); class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 把主菜单元件指针保存到库中 class="type">bool AddMenuBarElements(class="kw">const class="type">int window_index,CElement &object); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据指定的窗口索引返回主菜单的数量 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CWndContainer::MenuBarsTotal(class="kw">const class="type">int window_index) { if(window_index>=::ArraySize(m_wnd)) { ::Print(PREVENTING_OUT_OF_RANGE);
菜单条指针落库时的数组扩容细节
在自定义 GUI 容器里,主菜单(CMenuBar)不是直接塞进通用元件数组,而是拆成菜单项逐个挂接。AddMenuBarElements 先拿 ClassName 判型,非 CMenuBar 直接返回 false,避免误把普通控件当菜单处理。 判型通过后,用 ItemsTotal() 取菜单项总数,再走 for 循环。每轮先读 m_elements 当前长度,ArrayResize 加 1,然后把 ItemPointerByIndex(i) 得到的 CMenuItem 指针写进末尾,并顺手调 AddToObjectsArray 把对象指针也进通用库。 循环结束才把整个 CMenuBar 指针通过 AddToRefArray 存进 m_menu_bars 私有数组,函数返回 true。上层 AddToElementsArray 收到 true 就直接 return,不再走通用添加分支。 验证时可开 MT5 新建个带 CMenuBar 的面板,在 AddMenuBarElements 里 Print(::ArraySize(m_wnd[window_index].m_elements)),能看到每加一个菜单项数组长度精确 +1,外汇与贵金属 GUI 开发属高风险定制,参数误用可能导致面板不渲染。
class="kw">return(WRONG_VALUE); } class=class="str">"cmt">//--- class="kw">return(::ArraySize(m_wnd[window_index].m_menu_bars)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把指针加到元件数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndContainer::AddToElementsArray(class="kw">const class="type">int window_index,CElement &object) { class=class="str">"cmt">//--- 如果库中没有包含控件表单 class=class="str">"cmt">//--- 如果需求是为了不存在的表单 class=class="str">"cmt">//--- 添加到通用元件数组中 class=class="str">"cmt">//--- 把元件对象加到通用对象数组中 class=class="str">"cmt">//--- 保存所有表单中最后一个元件的id class=class="str">"cmt">//--- 元件id计数器加1 class=class="str">"cmt">//--- 在库中保存上下文菜单对象的指针 class=class="str">"cmt">//--- 把主菜单对象的指针存储到库中 if(AddMenuBarElements(window_index,object)) class="kw">return; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把主菜单对象的指针保存到库中 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CWndContainer::AddMenuBarElements(class="kw">const class="type">int window_index,CElement &object) { class=class="str">"cmt">//--- 如果不是主菜单就退出 if(object.ClassName()!="CMenuBar") class="kw">return(false); class=class="str">"cmt">//--- 取得主菜单的指针 CMenuBar *mb=::GetPointer(object); class=class="str">"cmt">//--- 在库中保存它的对象指针 class="type">int items_total=mb.ItemsTotal(); for(class="type">int i=class="num">0; i<items_total; i++) { class=class="str">"cmt">//--- 加大元件数组 class="type">int size=::ArraySize(m_wnd[window_index].m_elements); ::ArrayResize(m_wnd[window_index].m_elements,size+class="num">1); class=class="str">"cmt">//--- 取得菜单项的指针 CMenuItem *mi=mb.ItemPointerByIndex(i); class=class="str">"cmt">//--- 在数组中保存指针 m_wnd[window_index].m_elements[size]=mi; class=class="str">"cmt">//--- 把菜单项中的对象指针加到通用数组中 AddToObjectsArray(window_index,mi); } class=class="str">"cmt">//--- 把指针加到私有数组中 AddToRefArray(mb,m_wnd[window_index].m_menu_bars); class="kw">return(true); }
「把三项的菜单条挂进面板」
这一步只验证主菜单元件本身:在 CProgram 里塞一个 CMenuBar,写死三个菜单项,宽度分别取 50、55、53 像素,文字是 File / View / Help,其余属性全走默认。 动手前先挪一遍表单上其他控件的坐标,给菜单条腾出 Y 方向 20 像素的偏移(MENUBAR_GAP_Y),否则会叠在标题栏上。然后把 CreateMenuBar() 接进创建图形界面的主方法,EA 编译拖进图表就能看到效果。 实测现象:菜单条跟着表单一起移动,鼠标掠过菜单项时背景色会变。下面这段是类内声明加两个方法的骨架,注意 AddItem 的循环只传了宽和文字,没碰字体和点击事件。 别把默认坐标当万能:MENUBAR_GAP_Y 写 20、菜单项 Y 偏移 45 和 75 是这套布局的硬约束,你改了表单高度不跟着调,菜单就会漂到控件底下。
class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 主菜单 CMenuBar m_menubar; class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- class="macro">#define MENUBAR_GAP_X(class="num">1) class="macro">#define MENUBAR_GAP_Y(class="num">20) class="type">bool CreateMenuBar(class="type">void); class=class="str">"cmt">//--- class="macro">#define MENU_ITEM1_GAP_X(class="num">6) class="macro">#define MENU_ITEM1_GAP_Y(class="num">45) class=class="str">"cmt">//--- class="macro">#define SEP_LINE_GAP_X(class="num">6) class="macro">#define SEP_LINE_GAP_Y(class="num">75) class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建交易面板 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateTradePanel(class="type">void) { class=class="str">"cmt">//--- 创建控件表单 class=class="str">"cmt">//--- 创建控件: class=class="str">"cmt">// 主菜单 if(!CreateMenuBar()) class="kw">return(false); class=class="str">"cmt">//--- 菜单项 class=class="str">"cmt">//--- 分隔线 class=class="str">"cmt">//--- 重绘图表 class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建主菜单 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateMenuBar(class="type">void) { class=class="str">"cmt">//--- 主菜单中有三个菜单项 class="macro">#define MENUBAR_TOTAL class="num">3 class=class="str">"cmt">//--- 保存窗口指针 m_menubar.WindowPointer(m_window); class=class="str">"cmt">//--- 坐标 class="type">int x=m_window.X()+MENUBAR_GAP_X; class="type">int y=m_window.Y()+MENUBAR_GAP_Y; class=class="str">"cmt">//--- 含有每个菜单项独特属性的数组 class="type">int width[MENUBAR_TOTAL] ={class="num">50,class="num">55,class="num">53}; class="type">class="kw">string text[MENUBAR_TOTAL] ={"File","View","Help"}; class=class="str">"cmt">//--- 把菜单项加到主菜单 for(class="type">int i=class="num">0; i<MENUBAR_TOTAL; i++) m_menubar.AddItem(width[i],text[i]); class=class="str">"cmt">//--- 创建控件 if(!m_menubar.CreateMenuBar(m_chart_id,m_subwin,x,y)) class="kw">return(false); class=class="str">"cmt">//--- 把对象添加到对象组的通用数组中 CWndContainer::AddToElementsArray(class="num">0,m_menubar); class="kw">return(true); }
◍ 激活控件时锁住整块表单
做一个像 MT5 原生终端那样的下拉或右键菜单,核心不是画出来,而是「画出来以后别让底下东西乱动」。你开任何一个上下文菜单或下拉列表,会注意到鼠标划过主图表按钮、输入框时它们颜色都不跳了——这就是终端把所属表单临时屏蔽了,避免被浮层挡住的元件还对光标响应。 我们在 CWindow 里加一个 m_is_locked 布尔栏位,构造时置 false,表示默认不锁。再配一对 IsLocked() 读写方法,定时器里只要 m_is_locked 为 true 就跳过 ChangeObjectsColor(),表单和子元件颜色全冻结。 但光锁表单会出 bug:上下文菜单自己也是表单子元件,锁了之后菜单项 hover 也不变色。所以在 CContextMenu 里单独写一套改色方法,并在计时器调用;同时 CMenuItem::OnEventTimer 要先过 !m_wnd.IsLocked() 才改色。Show() 里置锁,Hide() 不能直接解锁——多个菜单同开时只关了一个不该全解,得在 CheckHideContextMenus 和 ReceiveMessageFromMenuItem 里统一发关闭信号后再解。 编译后挂 EA 到图表验证:右键开菜单,背后按钮颜色静止;菜单内项 hover 仍高亮,即行为正确。外汇与贵金属 GUI 脚本测试请先在模拟盘,浮层锁死可能导致实盘点击失效,属高风险操作环节。
class CWindow : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 屏蔽窗口的状态 class="type">bool m_is_locked; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 屏蔽窗口的状态 class="type">bool IsLocked(class="type">void) class="kw">const { class="kw">return(m_is_locked); } class="type">void IsLocked(class="kw">const class="type">bool flag) { m_is_locked=flag; } class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CWindow::CWindow(class="type">void) : m_is_locked(false) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计时器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWindow::OnEventTimer(class="type">void) { class=class="str">"cmt">//--- 如果窗口 if(!m_is_locked) { class=class="str">"cmt">//--- 修改表单对象的颜色 ChangeObjectsColor(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计时器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMenuItem::OnEventTimer(class="type">void) { class=class="str">"cmt">//--- 如果窗口是可用的 if(!m_wnd.IsLocked()) { class=class="str">"cmt">//--- 如果是禁用的上下文菜单 if(!m_context_menu_state) class=class="str">"cmt">//--- 修改表单对象的颜色 ChangeObjectsColor(); } } class=class="str">"cmt">//+------------------------------------------------------------------+
右键菜单的锁表单与悬停染色逻辑
在 MT5 自定义面板里做上下文菜单,最容易被忽略的是表单锁定。CContextMenu::Show() 里调用 m_wnd.IsLocked(true) 会把底层窗口锁住,防止菜单弹出时用户误触其他控件;对应地,CheckHideContextMenus() 和 ReceiveMessageFromMenuItem() 在隐藏菜单时都必须补一句 m_wnd.IsLocked(false),否则界面会卡在不可交互状态。 菜单自动收起的判断放在 CheckHideContextMenus():当光标同时脱离菜单矩形和前驱节点范围,就解锁表单并广播隐藏信号。这里隐含一个细节——若某个菜单项自身还挂了子菜单,必须循环遍历 m_items 确认没有下级被激活,才能真关掉。 悬停高亮不靠事件驱动,而是用定时器轮询。OnEventTimer() 每跳一次就调 ChangeObjectsColor(),后者先判 m_context_menu_state,再对 ItemsTotal() 个项逐个跑 m_items[i].ChangeObjectsColor()。在 30 fps 的图表环境下,这种轮询延迟通常低于 33 ms,肉眼基本无感。 外汇与贵金属图表挂这类面板时,注意锁表单期间 EA 仍在跑,价格跳动不会停,高频刷新叠加菜单轮询可能抬升 CPU 占用,建议在低价量时段先开 MT5 试一轮。
class=class="str">"cmt">//| 显示一个上下文菜单 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CContextMenu::Show(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">//--- 在前一节点注册状态 class=class="str">"cmt">//--- 屏蔽表单 m_wnd.IsLocked(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 检查关闭所有上下文菜单的条件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CContextMenu::CheckHideContextMenus(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">//--- 解锁表单 m_wnd.IsLocked(false); class=class="str">"cmt">//--- 发送隐藏所有上下文菜单的信号 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从菜单项接收需要处理的事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CContextMenu::ReceiveMessageFromMenuItem(class="kw">const class="type">int id_item,class="kw">const class="type">int index_item,class="kw">const class="type">class="kw">string message_item) { class=class="str">"cmt">//--- 如果消息是来自本程序并且元件ID匹配 class=class="str">"cmt">//--- 隐藏上下文菜单 class=class="str">"cmt">//--- 解锁表单 m_wnd.IsLocked(false); } class CContextMenu : class="kw">public CElement { class="kw">public: 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 CContextMenu::OnEventTimer(class="type">void) { class=class="str">"cmt">//--- 当鼠标光标掠过时改变菜单项的颜色 ChangeObjectsColor(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 当鼠标光标掠过时改变对象颜色 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CContextMenu::ChangeObjectsColor(class="type">void) { class=class="str">"cmt">//--- 如果上下文菜单被禁用就退出 if(!m_context_menu_state) class="kw">return; class=class="str">"cmt">//--- 循环迭代所有菜单项 class="type">int items_total=ItemsTotal(); for(class="type">int i=class="num">0; i<items_total; i++) { class=class="str">"cmt">//--- 修改菜单项的颜色 m_items[i].ChangeObjectsColor(); } }
「主菜单与上下文菜单的联动机制」
主菜单初始处于未激活态,鼠标掠过仅高亮,点击某项后才弹出对应上下文菜单并进入激活态;激活后光标掠过不同菜单项会自动切换上下文菜单,这与 Windows 程序行为一致。 挂接上下文菜单时,需把前节点指针传给 CContextMenu,并在 CMenuBar 中保存其指针。设置主菜单属性时,上下文菜单坐标按菜单项底部计算(FIX_BOTTOM),其余创建逻辑与此前相同。 点击处理在 CMenuBar::OnEvent 中拦截 CHARTEVENT_OBJECT_CLICK,转交 OnClickMenuItem。该方法先从对象名解析索引与 ID,若无上下文菜单指针则广播关闭信号,有指针且再次点击同一项时才关闭当前菜单。关闭动作最终由 CWndEvents::OnHideContextMenus 强制清场。 激活态下的悬停切换由 SwitchContextMenuByFocus(index) 完成:传入焦点项索引显示对应菜单、隐藏其余,若存在其他打开的菜单则发 ON_HIDE_BACK_CONTEXTMENUS,隐藏后必须重置菜单项颜色防止双高亮。编译载入 EA 后,首次点击出菜单、二次点击收起,验证通过即可。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建上下文菜单 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateMBContextMenu1(class="type">void) { class=class="str">"cmt">//--- 上下文菜单中有三项 class=class="str">"cmt">//--- 保存窗口指针 m_mb_contextmenu1.WindowPointer(m_window); class=class="str">"cmt">//--- 保存前节点的指针 m_mb_contextmenu1.PrevNodePointer(m_menubar.ItemPointerByIndex(class="num">0)); class=class="str">"cmt">//--- 把上下文菜单附加到指定的菜单项 m_menubar.AddContextMenuPointer(class="num">0,m_mb_contextmenu1); class=class="str">"cmt">//--- 项目名称数组 class=class="str">"cmt">//--- 可用模式的标签数组 class=class="str">"cmt">//--- 禁用模式的标签数组 class=class="str">"cmt">//--- 项目类型数组 class=class="str">"cmt">//--- 在创建之前设置属性 m_mb_contextmenu1.FixSide(FIX_BOTTOM); class=class="str">"cmt">//--- 在上下文菜单中加入菜单项 class=class="str">"cmt">//--- 第二个项目后的分隔线 class=class="str">"cmt">//--- 去除第二项的激活状态 class=class="str">"cmt">//--- 创建上下文菜单 if(!m_mb_contextmenu1.CreateContextMenu(m_chart_id,m_subwin)) class="kw">return(false); class=class="str">"cmt">//--- 把对象添加到对象组的通用数组中 CWndContainer::AddToElementsArray(class="num">0,m_mb_contextmenu1); class="kw">return(true); } class CMenuBar : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 处理菜单项的点击 class="type">bool OnClickMenuItem(class="kw">const class="type">class="kw">string clicked_object); class=class="str">"cmt">//--- 根据菜单项名称获取 (class="num">1) ID 和 (class="num">2) 索引 class="type">int IdFromObjectName(class="kw">const class="type">class="kw">string object_name); class="type">int IndexFromObjectName(class="kw">const class="type">class="kw">string object_name); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件处理函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMenuBar::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- 处理鼠标左键点击主菜单项的事件 if(id==CHARTEVENT_OBJECT_CLICK) { if(OnClickMenuItem(sparam)) class="kw">return; } }