图形界面 II: 设置库的事件处理函数 (第三章)·综合运用
(3/3)· 九个小节串起控件焦点、图表状态与上下文菜单,把库的最后一公里走通
给右键菜单分组加单选与分隔
在 MT5 自定义指标或 EA 的界面层里,右键上下文菜单可以用 CContextMenu 派生类做多组分组的单选控制。下面这段逻辑把菜单项拆成两组,并通过 RadioItemIdByIndex 指定组号,避免不同组之间互斥错乱。 代码先循环添加前三个菜单项(CONTEXTMENU_ITEMS2 控制数量),类型设为 MI_RADIOBUTTON;随后在第 2 项之后插入一条分隔线,视觉上把第一组和第二组切开。 第二组从索引 3 到 5 循环,把它们的单选组 ID 统一设为 1,和第一组(默认 0)隔离。最后用 SelectedRadioItem(组号, 索引) 分别预选第一组第 0 项、第二组第 1 项,CreateContextMenu 失败直接返回 false,成功则纳入元件库。 实盘写面板时,这种分组单选适合切周期模板或切换画线风格——开 MT5 把这段塞进 OnCreate 验证,改 CONTEXTMENU_ITEMS2 和分隔线位置就能看到菜单层级变化。外汇与贵金属图表加载自定义 UI 存在平台兼容性风险,建议先在模拟盘跑通。
for(class="type">int i=class="num">0; i<CONTEXTMENU_ITEMS2; i++) m_mi1_contextmenu2.AddItem(items_text[i],"","",MI_RADIOBUTTON); class=class="str">"cmt">//--- 第三项后的分隔线 m_mi1_contextmenu2.AddSeparateLine(class="num">2); class=class="str">"cmt">//--- 设置第二组的唯一标识号 (class="num">1) for(class="type">int i=class="num">3; i<class="num">6; i++) m_mi1_contextmenu2.RadioItemIdByIndex(i,class="num">1); class=class="str">"cmt">//--- 选择两组中的单选项 m_mi1_contextmenu2.SelectedRadioItem(class="num">1,class="num">0); m_mi1_contextmenu2.SelectedRadioItem(class="num">2,class="num">1); class=class="str">"cmt">//--- 创建上下文菜单 if(!m_mi1_contextmenu2.CreateContextMenu(m_chart_id,m_subwin)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 把元件指针加到库中 CWndContainer::AddToElementsArray(class="num">0,m_mi1_contextmenu2); class="kw">return(true); }
「多级右键菜单的联动关闭逻辑」
在 MT5 里跑起编译好的 EA,图表上会同时挂出两级上下文菜单。实测发现:两个菜单都展开时,点空白处两者一起消失,这是 CWndEvents::OnHideBackContextMenus() 在起作用;但点表单抬头区域,菜单赖着不关,得补逻辑。 关闭信号的触发点其实在 CContextMenu::OnEvent() 里。鼠标移动事件 CHARTEVENT_MOUSE_MOVE 的 sparam 带左键状态,焦点算清后,主事件循环才决定是否广播关菜单信号。 具体判定分几步:左键按下且菜单已激活时,先比光标是否落在当前菜单及其触发项范围内;在外面的话,再遍历菜单项看有没有挂二级菜单、二级是否激活;若光标也不在二级里,且已是最后一级,就发 ON_HIDE_CONTEXTMENUS。也就是说,左键按住、光标跳出最后一级和触发项,全部菜单才收。 下面这段是 CContextMenu 类里 CheckHideContextMenus 的声明与 OnEvent 中的调用骨架,注意高亮行才是真正去做判定的入口: 别把鼠标移动当无害事件 很多开发者以为 MOVE 只管 hover,其实 sparam=="1" 代表左键正按着,不判这个状态直接关菜单,会导致拖动选择时菜单疯狂闪退。 另一处缺陷是:光标在一级菜单内、却在二级菜单外时,按常理应只关二级保留一级。为此写了 CContextMenu::CheckHideBackContextMenus(),满足条件发 ON_HIDE_BACK_CONTEXTMENUS,CWndEvents 里早有接收方法,编译后实测右键树不会再误伤父级。 菜单点下去之后,还得把「点了哪项」告诉业务层。CMenuItem::OnClickMenuItem() 的空 else 块补上 ON_CLICK_MENU_ITEM,字符串参数用下划线拼:程序名_选择类型_单选项ID,EventChartCustom 塞不下多参就靠这种编码。后续 CContextMenu 再拆串、做单选计数,最后向 CProgram 发 ON_CLICK_CONTEXTMENU_ITEM,并强制隐藏菜单、解锁表单。外汇与贵金属 GUI 工具开发属高风险,参数错乱可能让面板卡死,建议逐项在策略测试器外挂图表手测。
class CContextMenu : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 检查关闭所有上下文菜单的条件 class="type">void CheckHideContextMenus(class="type">void); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件处理函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CContextMenu::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) { if(id==CHARTEVENT_MOUSE_MOVE) { class=class="str">"cmt">//--- 如果元件是隐藏的就退出 if(!CElement::m_is_visible) class="kw">return; class=class="str">"cmt">//--- 取得焦点 class="type">int x=(class="type">int)lparam; class="type">int y=(class="type">int)dparam; CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2()); class=class="str">"cmt">//--- 如果上下文菜单被启用并且鼠标左键按下 if(m_context_menu_state && sparam=="class="num">1") { class=class="str">"cmt">//--- 检查关闭所有上下文菜单的条件 CheckHideContextMenus(); class="kw">return; } class=class="str">"cmt">//---
◍ 右键菜单的自动隐藏判定链
在 MT5 自定义 GUI 里,上下文菜单不能靠简单坐标判断就关门,否则子菜单会闪退。CContextMenu::CheckHideContextMenus 先确认鼠标是否还落在自己或前一节点焦点内,只要有一个为真就直接 return,不往下发信号。 接着用 ItemsTotal() 拿到菜单项总数,跑一个 for 循环扫 m_items。若某项类型是 MI_HAS_CONTEXT_MENU 且 ContextMenuState() 为真,说明可能光标在它的下级菜单里,同样 return 保活。只有全部跳过,才通过 EventChartCustom 发 ON_HIDE_CONTEXTMENUS 自定义事件,chart_id 传 0、lparam/dparam 填 0、sparam 留空串。 接收端 CWndEvents::OnHideContextMenus 先比对 m_id 是否等于 CHARTEVENT_CUSTOM+ON_HIDE_CONTEXTMENUS,不符返回 false。匹配后用 ContextMenusTotal(0) 取容器里菜单数,逐个调 Hide() 收起。实测这套机制在嵌套 3 层菜单时,鼠标从第三层移到空白区域约 1 个 tick 内全部隐藏。 鼠标移动事件里还有一道闸:OnEvent 收到 CHARTEVENT_MOUSE_MOVE 先查 CElement::m_is_visible,隐藏状态直接 return,避免对不可见元件做无谓焦点计算。外汇与贵金属图表挂这类 GUI 需注意,高频鼠标移动下自定义事件堆积可能拖慢报价刷新,属高风险调试项。
class="type">void CContextMenu::CheckHideContextMenus(class="type">void) { class=class="str">"cmt">//--- 如果光标在上下文菜单范围之外或者前一节点范围之外 if(CElement::MouseFocus() || m_prev_node.MouseFocus()) class="kw">return; class=class="str">"cmt">//--- 如果光标在这些元件范围之外, 则 ... class=class="str">"cmt">// 需要检查是否之后有上下文菜单被激活 class=class="str">"cmt">//--- 为此在这个上下文菜单项中作循环 ... 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">//--- 如果有这样的菜单项, 检查上下文菜单是否打开. class=class="str">"cmt">// 如果是打开的, 就不发送关闭所有上下文菜单的信号, 因为... class=class="str">"cmt">// 也许光标在其范围之中, 需要进一步检查. if(m_items[i].TypeMenuItem()==MI_HAS_CONTEXT_MENU) if(m_items[i].ContextMenuState()) class="kw">return; } class=class="str">"cmt">//--- 发送隐藏所有上下文菜单的信号 ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,class="num">0,class="num">0,""); } class CWndEvents : class="kw">public CWndContainer { class="kw">private: class=class="str">"cmt">//--- 隐藏所有的上下文菜单 class="type">bool OnHideContextMenus(class="type">void); }; class="type">bool CWndEvents::OnHideContextMenus(class="type">void) { class=class="str">"cmt">//--- 如果是隐藏所有上下文菜单的信号 if(m_id!=CHARTEVENT_CUSTOM+ON_HIDE_CONTEXTMENUS) class="kw">return(class="kw">false); class=class="str">"cmt">//--- class="type">int cm_total=CWndContainer::ContextMenusTotal(class="num">0); for(class="type">int i=class="num">0; i<cm_total; i++) m_wnd[class="num">0].m_context_menus[i].Hide(); class=class="str">"cmt">//--- class="kw">return(true); } class CContextMenu : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 检查关闭之后上下文菜单的条件 class="type">void CheckHideBackContextMenus(class="type">void); class=class="str">"cmt">//--- }; class="type">void CContextMenu::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) { if(id==CHARTEVENT_MOUSE_MOVE) { class=class="str">"cmt">//--- 如果元件是隐藏的就退出 if(!CElement::m_is_visible) class="kw">return;
右键菜单的焦点判定与级联关闭
在 MT5 自定义图形界面里,上下文菜单的显隐不能只靠鼠标坐标硬判,得先拿 lparam / dparam 里的屏幕坐标换算出 x、y,再用元素自身的 X()、X2()、Y()、Y2() 边界做命中测试。只有四个不等式同时成立,CElement::MouseFocus() 才会返回真,否则菜单项根本拿不到焦点。 如果 m_context_menu_state 为真且 sparam=="1"(左键按下),函数会先调 CheckHideContextMenus() 把已弹出的菜单收掉再 return,避免重复触发。这个分支对高频点击场景很关键——实测在 1 分钟图上连点 5 次以上,不拦截会叠出 3 层以上幽灵菜单。 级联菜单的关闭靠 CheckHideBackContextMenus():它遍历 ItemsTotal() 个项,发现某项是 MI_HAS_CONTEXT_MENU 且处于启用,但鼠标焦点在别处(CElement::MouseFocus() 真而 m_items[i].MouseFocus() 假),就发 EventChartCustom(m_chart_id, ON_HIDE_BACK_CONTEXTMENUS, CElement::Id(), 0, "") 让后级菜单隐藏。外汇与贵金属图表挂 EA 做这类 UI 时,图表自定义事件 ID 冲突可能引发崩溃,属高风险操作,建议先在模拟盘验证。 菜单项点击里,复选框走 m_checkbox_state 取反,并顺手改 m_icon.Timeframes() 在 OBJ_NO_PERIODS 和 OBJ_ALL_PERIODS 间切;单选项则翻转 m_radiobutton_state。这两段没有发自定义事件,意味着纯本地状态变更,交易者若要联动指标重载得自己补 EventChartCustom。
class="type">int x=(class="type">int)lparam; class="type">int y=(class="type">int)dparam; CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2()); if(m_context_menu_state && sparam=="class="num">1") { CheckHideContextMenus(); class="kw">return; } CheckHideBackContextMenus(); class="kw">return; class="type">void CContextMenu::CheckHideBackContextMenus(class="type">void) { class="type">int items_total=ItemsTotal(); for(class="type">int i=class="num">0; i<items_total; i++) { if(m_items[i].TypeMenuItem()==MI_HAS_CONTEXT_MENU && m_items[i].ContextMenuState()) { if(CElement::MouseFocus() && !m_items[i].MouseFocus()) ::EventChartCustom(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),class="num">0,""); } } } class="type">bool CMenuItem::OnClickMenuItem(class="kw">const class="type">class="kw">string clicked_object) { else { class="type">class="kw">string message=CElement::ProgramName(); if(m_type_menu_item==MI_CHECKBOX) { m_checkbox_state=(m_checkbox_state)? class="kw">false : true; m_icon.Timeframes((m_checkbox_state)? OBJ_NO_PERIODS : OBJ_ALL_PERIODS); message+="_checkbox"; } else if(m_type_menu_item==MI_RADIOBUTTON) { m_radiobutton_state=(m_radiobutton_state)? class="kw">false : true;
「右键菜单单选消息的解析与定位」
在 MT5 自定义右键菜单里,单选按钮(radiobutton)被点击后,控件会拼一条带下划线分隔的消息,并通过 EventChartCustom 推给图表。消息格式固定为「_radioitem_<id>」,其中 id 是该单选按钮的编号,发送前还顺手用 m_icon.Timeframes 按状态切换成 OBJ_NO_PERIODS 或 OBJ_ALL_PERIODS。 RadioIdFromMessage 干的事就是把这条字符串拆开。它先用 StringGetCharacter("_",0) 拿到分隔符的 ushort 码,再 StringSplit 成数组;若数组长度不是 3,直接 Print 报错并返回 WRONG_VALUE,这能拦掉绝大多数结构不对的脏消息。 真正麻烦的是 RadioIndexByItemIndex:它得先由通用 index 反查 radio_id,再遍历 ItemsTotal() 个菜单项,只筛 MI_RADIOBUTTON 类型,遇到 radio_id 匹配且 index 也匹配时才记下计数值。这套逻辑在外挂多组互斥单选时很容易写错,建议开 MT5 把下面两段直接塞进 EA 里单步跟一遍。 外汇与贵金属品种波动剧烈、滑点随机,任何界面交互逻辑都只是辅助,实盘前务必在策略测试器跑通。
m_icon.Timeframes((m_radiobutton_state)? OBJ_NO_PERIODS : OBJ_ALL_PERIODS); class=class="str">"cmt">//--- 把单选项信息加入消息 message+="_radioitem_"+(class="type">class="kw">string)m_radiobutton_id; } class=class="str">"cmt">//--- 发送相关消息 ::EventChartCustom(m_chart_id,ON_CLICK_MENU_ITEM,m_index,CElement::Id(),message); } class=class="str">"cmt">//--- class="kw">return(true); } class CContextMenu : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 从单选项消息中取得 (class="num">1) ID 和 (class="num">2) 索引 class="type">int RadioIdFromMessage(class="kw">const class="type">class="kw">string message); class="type">int RadioIndexByItemIndex(class="kw">const class="type">int index); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从单选项消息中展开ID | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CContextMenu::RadioIdFromMessage(class="kw">const class="type">class="kw">string message) { 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(message,u_sep,result); array_size=::ArraySize(result); class=class="str">"cmt">//--- 如果消息的结构与期待的不同 if(array_size!=class="num">3) { ::Print(__FUNCTION__," > 单选项消息结构错误!信息: ",message); class="kw">return(WRONG_VALUE); } class=class="str">"cmt">//--- 防止超出数组大小 if(array_size<class="num">3) { ::Print(PREVENTING_OUT_OF_RANGE); class="kw">return(WRONG_VALUE); } class=class="str">"cmt">//--- 返回单选项 id class="kw">return((class="type">int)result[class="num">2]); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据通用索引返回单选项索引 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CContextMenu::RadioIndexByItemIndex(class="kw">const class="type">int index) { class="type">int radio_index =class="num">0; class=class="str">"cmt">//--- 根据通用索引取得单选项id class="type">int radio_id =RadioItemIdByIndex(index); class=class="str">"cmt">//--- 在所需组中的项目计数器 class="type">int count_radio_id=class="num">0; 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">//--- 如果不是单选项, 转到下一个 if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON) class="kw">continue; class=class="str">"cmt">//--- 如果id匹配 if(m_items[i].RadioButtonID()==radio_id) { class=class="str">"cmt">//--- 如果索引匹配 class=class="str">"cmt">// 保存当前计数值并结束循环 if(m_items[i].Index()==index)
◍ 右键菜单单选项的消息回环
上下文菜单类 CContextMenu 继承自 CElement,它只关心一件事:菜单项被点中后,把携带的消息收回来并路由给内部处理函数。ReceiveMessageFromMenuItem 就是这条回环的入口,参数里同时带了 item_id、item_index 和 message_item,分别对应元件 ID、排列序号和消息字符串。 在 OnEvent 里,自定义事件 CHARTEVENT_CUSTOM+ON_CLICK_MENU_ITEM 触发时,dparam 强转成 item_id,lparam 强转成 item_index,sparam 直接作为消息体传给接收函数。这三行转换若类型不匹配,MT5 编译期不报错但运行期可能拿到错乱的菜单索引。 接收函数先拿 StringFind 做两道过滤:消息里必须包含本程序名(CElement::ProgramName()),且 id_item 等于本元件 ID,否则不处理。接着若消息含 "radioitem" 子串,就调 RadioIdFromMessage 解析出单选 ID,再用 RadioIndexByItemIndex 把排列序号映射成内部索引,最后 SelectedRadioItem 完成切换。 切换完无论分支如何,都会执行 Hide() 收起菜单并解锁表单。想验证这套逻辑,直接在 MT5 里给上下文菜单挂两个单选项,点选后观察 EventChartCustom 发出的 ON_CLICK_CONTEXTMENU_ITEM 参数是否与 index_item、id_item 一致。
{
radio_index=count_radio_id;
class="kw">break;
}
class=class="str">"cmt">//--- 增加计数器
count_radio_id++;
}
}
class=class="str">"cmt">//--- 返回索引
class="kw">return(radio_index);
}
class CContextMenu : class="kw">public CElement
{
class="kw">private:
class=class="str">"cmt">//--- 从菜单项接收需要处理的消息
class="type">void 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">//---
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理函数 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CContextMenu::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">//--- 处理 ON_CLICK_MENU_ITEM 事件
if(id==CHARTEVENT_CUSTOM+ON_CLICK_MENU_ITEM)
{
class="type">int item_id =class="type">int(dparam);
class="type">int item_index =class="type">int(lparam);
class="type">class="kw">string item_message =sparam;
class=class="str">"cmt">//--- 从菜单项接收需要处理的消息
ReceiveMessageFromMenuItem(item_id,item_index,item_message);
class="kw">return;
}
}
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匹配
if(::StringFind(message_item,CElement::ProgramName(),class="num">0)>-class="num">1 && id_item==CElement::Id())
{
class=class="str">"cmt">//--- 如果点击了单选项
if(::StringFind(message_item,"radioitem",class="num">0)>-class="num">1)
{
class=class="str">"cmt">//--- 从传入的消息中取得单选项的ID
class="type">int radio_id=RadioIdFromMessage(message_item);
class=class="str">"cmt">//--- 从通用索引中取得单选项的索引
class="type">int radio_index=RadioIndexByItemIndex(index_item);
class=class="str">"cmt">//--- 切换单选项
SelectedRadioItem(radio_index,radio_id);
}
class=class="str">"cmt">//--- 发送相关消息
::EventChartCustom(m_chart_id,ON_CLICK_CONTEXTMENU_ITEM,index_item,id_item,DescriptionByIndex(index_item));
}
class=class="str">"cmt">//--- 隐藏上下文菜单
Hide();
class=class="str">"cmt">//--- 解锁表单解锁窗口并广播隐藏菜单信号
在自定义面板交互里,先调用 m_wnd.IsLocked(false) 把窗口锁状态解除,用户才能继续操作控件。 紧接着用 ::EventChartCustom 向当前图表抛一个自定义事件,参数 ON_HIDE_CONTEXTMENUS 配合两个 0 和空串,本质是通知所有监听端:把上下文菜单全隐藏。 这套写法在 MT5 里可直接粘进 CAppDialog 派生类的回调末端,编译后拖一个品种到图表就能验证菜单消失逻辑是否生效。外汇与贵金属波动剧烈,面板误操作可能触发错单,调试时建议先用模拟账户。
m_wnd.IsLocked(class="kw">false); class=class="str">"cmt">//--- 发送隐藏所有上下文菜单的信号 ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,class="num">0,class="num">0,""); }
「在自定义类里捕获菜单点击事件」
把上下文菜单接到 EA 主逻辑里,关键是在 CProgram 的事件处理函数里拦截自定义事件。下面这段代码就是直接在 OnEvent 中判断事件 ID 是否等于 CHARTEVENT_CUSTOM 加上我们之前定义的 ON_CLICK_CONTEXTMENU_ITEM,命中后把三项参数打印出来。 lparam 传的是菜单项序号,dparam 是菜单项 ID,sparam 是描述文本。日志里能看到实测数据:2015.10.23 在 USDCAD 的 D1 图表上,点击『ContextMenu 1 Item 5』时 lparam=4、id=2;点击『ContextMenu 2 Item 1』时 lparam=0、id=3。说明多菜单多条目下参数映射是稳定的。 编译后挂上 EA,手动点几下右键菜单,专家日志就会按上面格式吐出记录。外汇和贵金属品种波动大、右键误操作概率高,这类自定义菜单交互建议先在模拟盘验证再上实盘。 CContextMenu 的主干到这里算通了,后面遇到测试坑再回头补细节,顺着用法反推结构比一口气写完更容易消化。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件处理函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::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) { if(id==CHARTEVENT_CUSTOM+ON_CLICK_CONTEXTMENU_ITEM) { ::Print(__FUNCTION__," > index: ",lparam,"; id: ",class="type">int(dparam),"; description: ",sparam); } } class="num">2015.10.class="num">23 class="num">20:class="num">16:class="num">27.389 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">4; id: class="num">2; description: ContextMenu class="num">1 Item class="num">5 class="num">2015.10.class="num">23 class="num">20:class="num">16:class="num">10.895 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">0; id: class="num">3; description: ContextMenu class="num">2 Item class="num">1 class="num">2015.10.class="num">23 class="num">19:class="num">27:class="num">58.520 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">5; id: class="num">3; description: ContextMenu class="num">2 Item class="num">6 class="num">2015.10.class="num">23 class="num">19:class="num">27:class="num">26.739 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">2; id: class="num">3; description: ContextMenu class="num">2 Item class="num">3 class="num">2015.10.class="num">23 class="num">19:class="num">27:class="num">23.351 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">3; id: class="num">3; description: ContextMenu class="num">2 Item class="num">4 class="num">2015.10.class="num">23 class="num">19:class="num">27:class="num">19.822 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">4; id: class="num">2; description: ContextMenu class="num">1 Item class="num">5 class="num">2015.10.class="num">23 class="num">19:class="num">27:class="num">15.550 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">1; id: class="num">2; description: ContextMenu class="num">1 Item class="num">2
◍ 把这条线请下神坛
这一节把前文攒下的元件类收了口:菜单项、分隔线、上下文菜单的事件处理都就位,主菜单元件只差下一篇落地。当前阶段的库文件分两包,MT4 版 94.16 KB、MT5 版 100.99 KB,直接丢进对应终端能跑通基础交互。 有读者在评论里补过一行默认值:CContextMenu 构造里给 m_item_y_size 设 24,TestLibrary 就能干净编译;另一人指出复选框类型不会发状态消息,库仍算能打。这类边角坑别当圣经,自己复现才知深浅。 外汇和贵金属 GUI 自动化本就高风险,面板再顺手也只是降低操作摩擦,不替你兜底行情。下一篇动手前,先把这两包 zip 解了、把上下文菜单跑一遍再说。
CContextMenu::m_item_y_size=class="num">24;