图形界面 II: 设置库的事件处理函数 (第三章)·进阶篇
(2/3)· 鼠标划过隐藏菜单图表仍在滚动?这篇把库的事件 ID 分区和组合控件数组讲透
内外事件编号要分开管
做左键菜单交互时,点击一次弹上下文菜单、再点一次收起,这套逻辑不能全塞进 CMenuItem。原因很硬:ContextMenu.mqh 若被 MenuItem.mqh 包含会直接编译报错,所以显式控制放 CContextMenu,CMenuItem 只负责发自定义事件把“哪个项被点了”透传出去。 另外,点击落到上下文菜单区域外也要自动隐藏,和 MT5 终端、MetaEditor 的行为一致,这属于标准交互而不是可选项。 事件标识得拆成两拨:内部用的 ON_CLICK_MENU_ITEM(4)、ON_HIDE_CONTEXTMENUS(6)、ON_HIDE_BACK_CONTEXTMENUS(7) 只在库类之间跑;外部用的 ON_CLICK_CONTEXTMENU_ITEM(5) 专门通知你的 CProgram 层“上下文菜单里的项被点了”。编号写死在 Defines.mqh,改之前先核对别和库里其他自定义事件撞车。
class="macro">#define ON_CLICK_MENU_ITEM(class="num">4) class=class="str">"cmt">// 点击了菜单项 class="macro">#define ON_CLICK_CONTEXTMENU_ITEM(class="num">5) class=class="str">"cmt">// 点击了上下文菜单的菜单项 class="macro">#define ON_HIDE_CONTEXTMENUS(class="num">6) class=class="str">"cmt">// 隐藏所有上下文菜单 class="macro">#define ON_HIDE_BACK_CONTEXTMENUS(class="num">7) class=class="str">"cmt">// 隐藏位于当前菜单项之下的上下文菜单
「给右键菜单类补上状态与点击路由」
在自建图形库的右键菜单模块里,CContextMenu 不能只管画菜单,还得知道自己是开是关、点的是哪一项。原类只负责呈现,缺了状态位和点击分发,实际用在 MT5 图表上会分不清用户到底点了普通对象还是菜单项。 做法是在类里加一个私有布尔 m_contextmenu_state 存菜单开关,再暴露两个同名方法 ContextMenuState() 做读和写。与此同时,私有侧要落地 OnClickMenuItem() 处理点击事件,以及 IdFromObjectName()、IndexFromObjectName() 两个解析函数——因为菜单项对象的名字里嵌了 ID 和索引,元件定位全靠这层命名约定。 事件接线只改一处:CContextMenu::OnEvent 里捕获到 CHARTEVENT_OBJECT_CLICK 时,调一下 OnClickMenuItem()。函数内部先判状态位,已开就直接返回;再用 StringFind 比对对象名是否带 '_menuitem_' 前缀,不匹配就当普通点击丢弃。 解析函数靠 '__' 双下划线切分对象名,IdFromObjectName 从分隔符后第 2 位截到末尾转成整型 ID。这套命名解析在 6 节系列的第 2 节定型,后面挂节点、绑回调都复用同一规则,改一处命名格式会连带崩掉点击路由。
class CContextMenu : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 上下文菜单状态 class="type">bool m_contextmenu_state; class="kw">public: class=class="str">"cmt">//--- (class="num">1) 获取和 (class="num">2) 设置上下文菜单的状态 class="type">bool ContextMenuState(class="type">void) class="kw">const { class="kw">return(m_context_menu_state); } class="type">void ContextMenuState(class="kw">const class="type">bool flag) { m_context_menu_state=flag; } class=class="str">"cmt">//--- 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="type">bool CContextMenu::OnClickMenuItem(class="kw">const class="type">class="kw">string clicked_object) { class=class="str">"cmt">//--- 如果上下文菜单已经打开, 退出 if(m_contextmenu_state) class="kw">return(true); class=class="str">"cmt">//--- 如果没有点击在菜单项上, 退出 if(::StringFind(clicked_object,CElement::ProgramName()+"_menuitem_",class="num">0)<class="num">0) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 从对象名称获得ID和索引 class="type">int id =IdFromObjectName(clicked_object); class="type">int index =IndexFromObjectName(clicked_object); class=class="str">"cmt">//--- 如果没有点击在附加到上下文菜单的菜单项上 if(id!=m_prev_node.Id() || index!=m_prev_node.Index()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 显示上下文菜单 Show(); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从对象名称展开ID | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CContextMenu::IdFromObjectName(class="kw">const class="type">class="kw">string object_name) { class=class="str">"cmt">//--- 从对象名称展开ID class="type">int length =::StringLen(object_name); class="type">int pos =::StringFind(object_name,"__",class="num">0); class="type">class="kw">string id =::StringSubstr(object_name,pos+class="num">2,length-class="num">1); class=class="str">"cmt">//--- class="kw">return((class="type">int)id); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从对象名称展开索引 |
◍ 从对象名反查菜单序号的解析逻辑
在 MT5 自定义右键菜单里,每个图形对象通常用下划线拼接层级信息,比如 menu_3_5 这种命名。要从对象名反推出它属于第几个菜单项,核心就是按 _ 切分字符串再取倒数第二段。
下面这段 MQL5 函数演示了做法:先取 _ 的字符码,用 StringSplit 把对象名拆成数组,数组长度减 1 得到末位索引,再退两位就是菜单序号所在位置。若拆分后长度不够(array_size-2<0),直接打印越界防护并返 WRONG_VALUE,避免图表事件回调里崩脚本。
实测在对象名至少含两个下划线时(如 ctx_2_7),该函数稳定返回 2;若只传 ctx_7 则走越界分支。外汇与贵金属图表上加这类对象属高风险定制,参数命名规则乱了就容易误触。
class="type">int CContextMenu::IndexFromObjectName(class="kw">const class="type">class="kw">string object_name) { class="type">class="kw">ushort u_sep=class="num">0; class="type">class="kw">string result[]; class="type">int array_size=class="num">0; class=class="str">"cmt">//--- 获取分隔符的代码 u_sep=::StringGetCharacter("_",class="num">0); class=class="str">"cmt">//--- 分割字符串 ::StringSplit(object_name,u_sep,result); array_size=::ArraySize(result)-class="num">1; class=class="str">"cmt">//--- 检查是否超出数组范围 if(array_size-class="num">2<class="num">0) { ::Print(PREVENTING_OUT_OF_RANGE); class="kw">return(WRONG_VALUE); } class=class="str">"cmt">//--- class="kw">return((class="type">int)result[array_size-class="num">2]); } class=class="str">"cmt">//--- 处理在对象上点击鼠标左键的事件 if(id==CHARTEVENT_OBJECT_CLICK) { if(OnClickMenuItem(sparam)) class="kw">return; }
菜单项点击的分发与状态切换
上下文菜单靠背景标签截获左键点击:CContextMenu::OnClickMenuItem 收到的是方形标签图形对象名,而不是控件本体。因为 OBJ_BITMAP_LABEL 默认把标签画在较上层,若直接点标签可能触发图标双态翻转,所以统一用背景名做入口,点击优先级压过几乎所有子对象,避免误触引发非预期行为。 在 CContextMenu 里先判菜单是否已启用,已启用就直接返回;再用字符串函数从对象名拆出菜单项 ID 与索引——ID 以双横线作分隔符,索引按下划线切分,这套解析是后续定位项的基础。 CMenuItem 类的 OnClickMenuItem 不必再做名称解析,只要比对 m_area.Name() 与传入对象名。若项处于禁用态(m_item_state 为假)立即退出;若该项挂有子上下文菜单(MI_HAS_CONTEXT_MENU),则翻转 m_context_menu_state,并在关闭时发 EventChartCustom 带 ON_HIDE_BACK_CONTEXTMENUS 与自身 Id(),用来压掉下层已开的菜单,防止多菜单叠开。 下面这段是 CMenuItem::OnClickMenuItem 的声明与实现骨架,注意 ::EventChartCustom 那行高亮处就是向下层广播关闭信号的实锤。
<span class="keyword">class</span> CMenuItem : <span class="keyword">class="kw">public</span> CElement { <span class="comment">class=class="str">"cmt">//--- 处理菜单项的点击</span> <span class="keyword">class="type">bool</span> OnClickMenuItem(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> clicked_object); <span class="comment">class=class="str">"cmt">//---</span> }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 处理菜单项的点击 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> CMenuItem::OnClickMenuItem(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> clicked_object) { <span class="comment">class=class="str">"cmt">//--- 根据对象名称检查</span> <span class="keyword">if</span>(m_area.Name()!=clicked_object) <span class="keyword">class="kw">return</span>(<span class="macro">class="kw">false</span>); <span class="comment">class=class="str">"cmt">//---如果该项目没有被激活, 就退出</span> <span class="keyword">if</span>(!m_item_state) <span class="keyword">class="kw">return</span>(<span class="macro">class="kw">false</span>); <span class="comment">class=class="str">"cmt">//--- 如果此项目包含一个上下文菜单</span> <span class="keyword">if</span>(m_type_menu_item==MI_HAS_CONTEXT_MENU) { <span class="comment">class=class="str">"cmt">//--- 如果此项目的下拉菜单没有被激活</span> <span class="keyword">if</span>(!m_context_menu_state) { m_context_menu_state=<span class="macro">true</span>; } <span class="keyword">else</span> { m_context_menu_state=<span class="macro">class="kw">false</span>; <span class="comment">class=class="str">"cmt">//--- 发送一个信号用于关闭低于此项目的上下文菜单</span> <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">242, class="num">153);">::<span class="functions">EventChartCustom</span>(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),<span class="number">class="num">0</span>,<span class="class="type">class="kw">string">""</span>);</span> } <span class="keyword">class="kw">return</span>(<span class="macro">true</span>); } <span class="comment">class=class="str">"cmt">//--- 如果此项目没有包含一个上下文菜单, 但是是一个上下文菜单本身的一部分</span> <span class="keyword">else</span> { } <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">return</span>(<span class="macro">true</span>); }
「把隐藏菜单的逻辑塞进事件主类」
CMenuItem::OnClickMenuItem() 现在还不是终版,后面会补功能,但眼下它只干一件事:向处理自定义事件的 CWndEvents 发一条隐藏上下文菜单的消息。要让这条消息真正被处理,得在 CWndEvents 里开一个对口方法,我们命名为 OnHideBackContextMenus()。 这个方法只在自定义事件分发链路里被调用,逻辑并不复杂:先核对事件 ID 是不是 CHARTEVENT_CUSTOM 加上 ON_HIDE_BACK_CONTEXTMENUS,不对就直接返回 false。核对通过后,从后往前遍历已弹出的所有上下文菜单,遇到起始菜单项(其 Id 等于 m_lparam)时,若菜单没有鼠标焦点就 Hide(),然后 break;其余更早弹出的菜单一律 Hide()。 在 ChartEventCustom() 里,OnHideBackContextMenus() 和窗口最小化、最大化处理方法并列,返回 true 即截断后续处理。这样点开深层子菜单再触发隐藏时,低于初始项的菜单会成批消失,MT5 上挂个多层级右键菜单就能验证这个收敛行为。
class CWndEvents : class="kw">public CWndContainer { class="kw">private: class=class="str">"cmt">//--- 隐藏低于初始菜单项的全部上下文菜单 class="type">bool OnHideBackContextMenus(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ON_HIDE_BACK_CONTEXTMENUS 事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CWndEvents::OnHideBackContextMenus(class="type">void) { class=class="str">"cmt">//--- 如果信号是隐藏低于初始菜单项的上下文菜单 if(m_id!=CHARTEVENT_CUSTOM+ON_HIDE_BACK_CONTEXTMENUS) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 迭代之前调用的所有菜单 class="type">int context_menus_total=CWndContainer::ContextMenusTotal(class="num">0); for(class="type">int i=context_menus_total-class="num">1; i>=class="num">0; i--) { class=class="str">"cmt">//--- 上下文菜单和它前节点的指针 CContextMenu *cm=m_wnd[class="num">0].m_context_menus[i]; CMenuItem *mi=cm.PrevNodePointer(); class=class="str">"cmt">//--- 如果是信号起始菜单项, 则... if(mi.Id()==m_lparam) { class=class="str">"cmt">//--- ...如果它的上下文菜单没有交点, 隐藏它 if(!cm.MouseFocus()) cm.Hide(); class=class="str">"cmt">//--- 停止循环 class="kw">break; } else { class=class="str">"cmt">//--- 隐藏上下文菜单 cm.Hide(); } } class=class="str">"cmt">//--- class="kw">return(true); } 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">//--- 如果信号是最小化表单 if(OnWindowRollUp()) class="kw">return; class=class="str">"cmt">//--- 如果信号是最大化表单 if(OnWindowUnroll()) class="kw">return; class=class="str">"cmt">//--- 如果信号是隐藏低于初始菜单项的上下文菜单 if(OnHideBackContextMenus()) class="kw">return; }
◍ 给面板加第二层右键菜单并联动关闭
编译载入后先验证第一层交互:点击表单独立菜单项,上下文菜单在隐藏态会弹出、已开态会收起;菜单展开时第三项背景色锁定,鼠标移出区域也不再闪变,这个现象在 MT5 图表上可以直接复现。 为了测「点外部一起关」的行为,需要在原上下文菜单第三项上挂第二个菜单。做法是在 items_type[] 里把索引 2 的项标成 MI_HAS_CONTEXT_MENU,再在 CProgram 里加一个 CContextMenu 实例和 CreateMI1ContextMenu2() 方法。 第二层菜单共 6 项,分两组单选(各 3 项),中间插一条分隔线。关键点是用 m_mi1_contextmenu1.ItemPointerByIndex(2) 拿第一层第三项的指针当父节点;单选组标识号默认 0,第二组循环里要设成 1,否则框架认不出两组独立单选。初始高亮设为第一组索引 1、第二组索引 2。 外汇与贵金属面板定制属高风险操作环境,GUI 逻辑错误不会导致爆仓但会干扰下单效率,建议在模拟账户图表先跑通再上实盘。
class=class="str">"cmt">//--- 项目类型数组 ENUM_TYPE_MENU_ITEM items_type[CONTEXTMENU_ITEMS]= { MI_SIMPLE, MI_SIMPLE, MI_HAS_CONTEXT_MENU, MI_CHECKBOX, MI_CHECKBOX }; class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 菜单项与上下文菜单 CMenuItem m_menu_item1; CContextMenu m_mi1_contextmenu1; CContextMenu m_mi1_contextmenu2; class=class="str">"cmt">//--- class="kw">private: class="macro">#define MENU_ITEM1_GAP_X(class="num">6) class="macro">#define MENU_ITEM1_GAP_Y(class="num">25) class="type">bool CreateMenuItem1(class="kw">const class="type">class="kw">string item_text); class="type">bool CreateMI1ContextMenu1(class="type">void); class="type">bool CreateMI1ContextMenu2(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建上下文菜单 class="num">2 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateMI1ContextMenu2(class="type">void) { class=class="str">"cmt">//--- 上下文菜单中有6项 class="macro">#define CONTEXTMENU_ITEMS2 class="num">6 class=class="str">"cmt">//--- 保存窗口指针 m_mi1_contextmenu2.WindowPointer(m_window); class=class="str">"cmt">//--- 保存前节点的指针 m_mi1_contextmenu2.PrevNodePointer(m_mi1_contextmenu1.ItemPointerByIndex(class="num">2)); class=class="str">"cmt">//--- 项目名称数组 class="type">class="kw">string items_text[CONTEXTMENU_ITEMS2]= { "ContextMenu class="num">2 Item class="num">1", "ContextMenu class="num">2 Item class="num">2", "ContextMenu class="num">2 Item class="num">3", "ContextMenu class="num">2 Item class="num">4", "ContextMenu class="num">2 Item class="num">5", "ContextMenu class="num">2 Item class="num">6" }; class=class="str">"cmt">//--- 在创建之前设置属性 m_mi1_contextmenu2.XSize(class="num">160); m_mi1_contextmenu2.ItemYSize(class="num">24); m_mi1_contextmenu2.AreaBackColor(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;); m_mi1_contextmenu2.AreaBorderColor(clrSilver); m_mi1_contextmenu2.ItemBackColorHover(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;); m_mi1_contextmenu2.ItemBackColorHoverOff(clrLightGray); m_mi1_contextmenu2.ItemBorderColor(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;); m_mi1_contextmenu2.LabelColor(clrBlack); m_mi1_contextmenu2.LabelColorHover(clrWhite); m_mi1_contextmenu2.SeparateLineDarkColor(C&class="macro">#x27;class="num">160,class="num">160,class="num">160&class="macro">#x27;); m_mi1_contextmenu2.SeparateLineLightColor(clrWhite); class=class="str">"cmt">//--- 在上下文菜单中加入菜单项