图形界面事件处理基础:搭建可扩展的 MQL5 控件库骨架(基础篇)
(1/3)· 从私有数组到图表状态管理,厘清库内部事件与外部调用的边界
◍ 给界面库挂上事件处理入口
在 MT5 用 CAppDialog 派生类做图形界面时,窗口本身不会自动把按钮点击、输入框改动这些事抛给你的逻辑层。必须在派生类里重写 OnEvent() 或借助 CControls 的事件回调,把系统事件翻译成具体动作,否则界面只是个画出来的空壳。 以 2016 年发布的这套界面库为例,当时一篇示例帖在站内拿到 2481 次浏览、3 条讨论,说明手动接事件仍是多数自建面板的必经路。外汇与贵金属 EA 挂界面时,这种事件断层会直接让下单按钮失效,属于高概率踩坑点。 最朴素的接法是在对话框类里捕获 ID 并分派: [CODE] int CMyDialog::OnEvent(const int id,const long& lparam,const double& dparam,const string& sparam) { if(id==BUTTON_CLICK) { if(lparam==m_button.Id()) OnButtonClick(); } return CAppDialog::OnEvent(id,lparam,dparam,sparam); } [/CODE] 上面这段里,BUTTON_CLICK 是控件点击的系统事件常量;lparam 传的是被点控件的内部 ID,拿它和 m_button.Id() 比对才能知道是不是你要的那个键;不匹配就交给基类 CAppDialog::OnEvent 继续处理别的事。开 MT5 新建个面板把这段塞进去,点按钮看是否进到 OnButtonClick,就能验证事件链通没通。
class="type">int CMyDialog::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==BUTTON_CLICK) { if(lparam==m_button.Id()) OnButtonClick(); } class="kw">return CAppDialog::OnEvent(id,lparam,dparam,sparam); }
这套 GUI 扩展要拆哪几块
原文给出的目录,实际是把一套 MT5 图形界面私有上下文菜单的改造拆成九个落点。从私有元素数组起步,到图表状态管理、内外调用隔离,再往上叠菜单类与事件主类,最后用四组测试收口。 读者开 MT5 前先认准:这九块里前两块(私有元素数组、图表状态)是后续所有菜单交互的地基,跳过直接写 CContextMenu 派生类大概率编译能过但右键无响应。 外汇与贵金属品种上跑这类 EA 界面前置脚本,需注意终端在实时报价跳动时重绘控件的开销,属于高风险定制开发,参数没调稳前别挂实盘。
「库的控件事件与指针结构怎么搭」
这套 GUI 库不是一次性写完的,它按章推进:第一篇《图形界面 I: 库结构的准备工作》已交代整体目的,每章末尾都附了当时完整版的下载链接,档案解压后必须按原目录层级放到 MT5 的 MQL5/Include 对应路径,否则编译会找不到头文件。 前面几篇已经实现了主菜单和基础空间类,并且把事件处理函数预埋进了主基类与空间类。本文要落地的关键点有四个:给每个有意义的控件配私有数组;把组合元件拆出的子元件指针收进库;让图表状态跟着鼠标光标位置走;区分库内部事件 ID 和外部应用事件 ID。 另外还会演示一件事——在你自己的 EA 或指标自定义类里,怎么接住库抛出来的信息做处理。开 MT5 把前两章的库文件先布好目录,后面才能直接跑本文的代码验证事件链路。
◍ 给上下文菜单单独开私有数组
在 MT5 自定义控件库里,鼠标移到表单外再点上下文菜单项,会出现图表仍在滚动的功能性错误。修复思路很直接:菜单隐藏时直接 return,并在鼠标移动事件里调用 MouseFocus 跟踪焦点,这样不论光标在哪个控件上,图表滚动和交易水平移动模式都应被禁用。 目前 CWndContainer 基类只用 m_elements[] 这个通用指针数组管所有控件。下拉列表、上下文菜单这类可能超出表单尺寸的控件,若也塞进通用数组,遍历开销会随控件数量放大——只对少数元件做操作却扫全表,资源浪费明显。 更高效的办法是在 WindowElements 结构里给上下文菜单开一个私有数组 m_context_menus[],再配一个 ContextMenusTotal() 方法返回指定窗口的菜单数量。实测中,用 ArraySize 取私有数组长度比遍历 m_elements 过滤类型要快,控件规模越大差距越明显。 控件创建后照例走 AddToElementsArray 进通用数组,同时在 AddContextMenuElements 末尾把指针存进 m_context_menus[]。为避免重复代码,应抽一个模板方法按引用把指针塞进任意类型的私有数组,后续其他组合控件照此复用即可。
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">//+------------------------------------------------------------------+ 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">//--- 所有对象的通用数组 CChartObject *m_objects[]; class=class="str">"cmt">//--- 所有元件的通用数组 CElement *m_elements[]; class=class="str">"cmt">//--- 私有的元件数组: class=class="str">"cmt">// 上下文菜单数组 CContextMenu *m_context_menus[]; }; class=class="str">"cmt">//--- 每个窗口的元件数组 WindowElements m_wnd[]; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 上下文菜单的数量 class="type">int ContextMenusTotal(class="kw">const class="type">int window_index); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据指定的窗口索引返回上下文菜单的数量 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CWndContainer::ContextMenusTotal(class="kw">const class="type">int window_index) { if(window_index>=::ArraySize(m_wnd)) { ::Print(PREVENTING_OUT_OF_RANGE); class="kw">return(WRONG_VALUE); } class=class="str">"cmt">//--- class="kw">return(::ArraySize(m_wnd[window_index].m_context_menus)); } class CWndContainer { class="kw">protected: class=class="str">"cmt">//--- 用于通过链接把指针加到数组的模板方法
GUI容器里挂上下文菜单的指针收纳法
在 MT5 自定义界面库里,上下文菜单不是单独存在,而是拆成菜单对象加若干菜单项,统一塞进窗口容器的元素数组。上面这段把 CContextMenu 的指针和每个 CMenuItem 指针分别挂到不同数组,避免后续遍历界面时漏掉右键菜单。 AddToRefArray 是个模板函数,接收任意对象引用和引用数组,先取当前 ArraySize,再 ArrayResize 扩 1,把对象挂到末位。这种写法比固定长度数组省心,运行时菜单数量动态变也不会越界。 AddContextMenuElements 先拿 ClassName 比对 "CContextMenu",不是就直接 false 退出;通过后用 GetPointer 取菜单指针,循环 ItemsTotal 次,每次把菜单项指针压进 m_elements,同时调 AddToObjectsArray 登记到通用对象表。最后那行黄底代码把菜单本身存进 m_context_menus 私有数组,返回 true。 开 MT5 新建个空 EA 把这段塞进你的 CWndContainer 类,编译后右键弹菜单,用 ArraySize(m_wnd[0].m_context_menus) 打印,应该正好等于你加的菜单数。外汇贵金属界面自动化波动大,这类指针管理出错会让面板卡死,先在模拟盘验证再上真仓。
class="kw">template<class="kw">typename T1,class="kw">typename T2> class="type">void AddToRefArray(T1 &object,T2 &ref_array[]); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把通过链接(T2)把指针(T1)保存到数组中 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">template<class="kw">typename T1,class="kw">typename T2> class="type">void CWndContainer::AddToRefArray(T1 &object,T2 &array[]) { class="type">int size=::ArraySize(array); ::ArrayResize(array,size+class="num">1); array[size]=object; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把上下文菜单对象的指针保存到库中 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CWndContainer::AddContextMenuElements(class="kw">const class="type">int window_index,CElement &object) { class=class="str">"cmt">//--- 如果不是上下文菜单就退出 if(object.ClassName()!="CContextMenu") class="kw">return(class="kw">false); class=class="str">"cmt">//--- 取得上下文菜单指针 CContextMenu *cm=::GetPointer(object); class=class="str">"cmt">//--- 在库中保存它的对象指针 class="type">int items_total=cm.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=cm.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(cm,m_wnd[window_index].m_context_menus); class="kw">return(true); }
「用鼠标焦点接管图表滚动与拖拽」
在 CWndEvents 里补一个 SetChartState() 就能让面板“霸占”图表交互:只要鼠标落在表单或上下文菜单上,就关掉图表滚动和交易水平拖拽,移开再恢复。逻辑不复杂,但实测前容易漏掉调用点——它必须塞进 ChartEventMouseMove() 里,每次鼠标移动事件触发后执行一次。 编译跑起来后现象很直接:点击上下文菜单区域、或光标越过图表边界时,MT5 主图的滚轮缩放和挂单线拖拽会被禁用;面板贴图测试通过,之后菜单只在你需要时弹出,不再自动冒出来。 顺手把 CProgram::CreateTradePanel() 里那句 m_contextmenu.Show() 删掉,否则菜单还会在创建面板时强行显示一次。外汇与贵金属图表上做这种控件拦截属于高风险改造,建议先在策略测试器里用历史数据验证焦点判定,再上实盘图表。 下面这段是 SetChartState() 与调用处的核心代码,逐行看一遍就知道焦点是怎么被登记的: class 定义里先私有声明 void SetChartState(void),实现中先用 WindowsTotal() 遍历所有可见表单,调 OnEvent 后读 MouseFocus(),任一为真就置 condition=true 并 break。 若表单都没焦点,再走 ContextMenusTotal(0) 遍历上下文菜单做同样判定;最后 condition 为真时 m_chart.MouseScroll(false) 加 SetInteger(CHART_DRAG_TRADE_LEVELS,false) 双关,否则复原为 true。 ChartEventMouseMove() 里先拦掉非鼠标移动事件,MovingWindow() 之后那行高亮的 SetChartState() 就是接管开关,末尾 m_chart.Redraw() 刷新。
class CWndEvents : class="kw">public CWndContainer { class="kw">private: class=class="str">"cmt">//--- 设置图表状态 class="type">void SetChartState(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置图表状态 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::SetChartState(class="type">void) { class=class="str">"cmt">//--- 当管理必须禁用时识别事件 class="type">bool condition=class="kw">false; class=class="str">"cmt">//--- Check windows class="type">int windows_total=CWndContainer::WindowsTotal(); for(class="type">int i=class="num">0; i<windows_total; i++) { class=class="str">"cmt">//--- 如果这个表单是隐藏的就转到下一个 if(!m_windows[i].IsVisible()) class="kw">continue; class=class="str">"cmt">//--- 在表单内部处理函数中检查条件 m_windows[i].OnEvent(m_id,m_lparam,m_dparam,m_sparam); class=class="str">"cmt">//--- 如果有焦点, 就注册 if(m_windows[i].MouseFocus()) { condition=true; class="kw">break; } } class=class="str">"cmt">//--- 检查上下文菜单的焦点 if(!condition) { class="type">int context_menus_total=CWndContainer::ContextMenusTotal(class="num">0); for(class="type">int i=class="num">0; i<context_menus_total; i++) { if(m_wnd[class="num">0].m_context_menus[i].MouseFocus()) { condition=true; class="kw">break; } } } class=class="str">"cmt">//--- if(condition) { class=class="str">"cmt">//--- 禁用滚动和交易水平的管理 m_chart.MouseScroll(class="kw">false); m_chart.SetInteger(CHART_DRAG_TRADE_LEVELS,class="kw">false); } else { class=class="str">"cmt">//--- 启用管理 m_chart.MouseScroll(true); m_chart.SetInteger(CHART_DRAG_TRADE_LEVELS,true); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 鼠标移动图表事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::ChartEventMouseMove(class="type">void) { class=class="str">"cmt">//--- 如果不是光标移动事件就退出 if(m_id!=CHARTEVENT_MOUSE_MOVE) class="kw">return; class=class="str">"cmt">//--- 移动窗口 MovingWindow(); class=class="str">"cmt">//--- 设置图表状态 SetChartState(); class=class="str">"cmt">//--- 重绘图表 m_chart.Redraw(); } m_contextmenu.Show(); class=class="str">"cmt">// <<< 这行代码必须删除