图形界面 I: 用于表单按钮的函数与删除界面元素 (第四章)·进阶篇
📘

图形界面 I: 用于表单按钮的函数与删除界面元素 (第四章)·进阶篇

第 2/2 篇

「用图表点击事件驱动窗口折叠」

在 MT5 自定义面板里,想让窗口随鼠标点按钮就收起或展开,核心是把 CHARTEVENT_OBJECT_CLICK 接进类的事件处理函数。下面这段 CWindow 的实现给出了最直接的做法:点对象名命中即调状态切换,不命中就交还 false。 OnEvent 只拦一种系统事件——id 等于 CHARTEVENT_OBJECT_CLICK 时,把被点的对象名 sparam 丢给 ChangeWindowState。只要这一层写对,后面按钮逻辑全收敛到一个函数里,避免在主事件里堆 if。 ChangeWindowState 拿 pressed_object 和两类按钮的 Name() 比:等于折叠钮名就 RollUp(),等于展开钮名就 Unroll(),命中任一个都回 true。两个都不匹配才回 false,意味着这次点击不是窗口控件,外层可继续派发。 开 MT5 新建 EA 把这段塞进你的 CWindow 类,挂两个 OBJECT_BUTTON 并让 Name() 返回固定串,就能在图上点出最小化/最大化效果;外汇与贵金属图表叠加多面板时,这种折叠能明显降低视觉干扰,但自定义对象名冲突会导致事件误触,需自行压测。

MQL5 / C++
  class="type">bool               ChangeWindowState(class="kw">const class="type">class="kw">string pressed_object);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 图表事件处理函数                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::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)
     {
class=class="str">"cmt">//--- 最小化/最大化窗口
      ChangeWindowState(sparam);
      class="kw">return;
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查窗口最小化/最大化事件                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWindow::ChangeWindowState(class="kw">const class="type">class="kw">string pressed_object)
  {
class=class="str">"cmt">//--- 如果点击了"最小化窗口"的按钮
   if(pressed_object==m_button_rollup.Name())
     {
      RollUp();
      class="kw">return(true);
     }
class=class="str">"cmt">//--- 如果点击了"最大化窗口"的按钮
   if(pressed_object==m_button_unroll.Name())
     {
      Unroll();
      class="kw">return(true);
     }
class=class="str">"cmt">//---
   class="kw">return(class="kw">false);
  }

◍ 切换品种时段后界面对象成倍克隆的根因

把 EA 挂上图表后若反复改交易品种或周期,你会发现图形界面对象一份份叠加——这不是显示 bug,而是 OnInit 在去初始化后又被调用,而旧对象没被释放。标准库的 CChartObject 析构里只在 EA 被删除时调 ObjectDelete,切换品种/周期不会触发该析构,于是新初始化又建一套,N 次切换就留 N 份拷贝。 根治办法是在自有库里补一套去初始化清理:CElement 声明虚方法 Delete,各派生类自行实现;CWindow::Delete 里先把右边界变量清零,再逐个删背景、标题栏、图标、标签、按钮等子对象,最后清掉基类对象指针数组并取消鼠标焦点。 CWndEvents 再包一层 Destroy,遍历所有窗口和控件,先 CheckPointer 过滤无效指针,再调各控件 Delete,出循环后 ArrayFree 清空 m_objects / m_elements / m_wnd / m_windows。主程序里把 Destroy 挂到 OnDeinitEvent 即可。 编译后载入 EA,连切 3~4 次品种周期验证:终端对象列表里界面元素数量应保持不变,不再出现重复前缀的同名对象。外汇与贵金属图表高频切换时这种泄漏会拖慢终端,建议每次改周期前先确认库已含该机制。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 析构函数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CChartObject::~CChartObject(class="type">void)
  {
   if(m_chart_id!=-class="num">1)
      ::ObjectDelete(m_chart_id,m_name);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 删除                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::Delete(class="type">void)
  {
class=class="str">"cmt">//--- 变量清零
   m_right_limit=class="num">0;
class=class="str">"cmt">//--- 删除对象
   m_bg.Delete();
   m_caption_bg.Delete();
   m_icon.Delete();
   m_label.Delete();
   m_button_close.Delete();
   m_button_rollup.Delete();
   m_button_unroll.Delete();
   m_button_tooltip.Delete();
class=class="str">"cmt">//--- 清空对象数组
   CElement::FreeObjectsArray();
class=class="str">"cmt">//--- 控件焦点清零
   CElement::MouseFocus(class="kw">false);
  }
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">protected:
   class=class="str">"cmt">//--- 删除界面
   class="type">void                Destroy(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 删除全部对象                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::Destroy(class="type">void)
  {
   class="type">int window_total=CWndContainer::WindowsTotal();
   for(class="type">int w=class="num">0; w<window_total; w++)
     {
      class="type">int elements_total=CWndContainer::ElementsTotal(w);
      for(class="type">int e=class="num">0; e<elements_total; e++)
        {
         class=class="str">"cmt">//--- 如果指针无效, 则转到下一个
         if(::CheckPointer(m_wnd[w].m_elements[e])==POINTER_INVALID)
            class="kw">continue;
         class=class="str">"cmt">//--- 删除控件对象
         m_wnd[w].m_elements[e].Delete();
        }
      class=class="str">"cmt">//--- 清空控件数组
      ::ArrayFree(m_wnd[w].m_objects);
      ::ArrayFree(m_wnd[w].m_elements);
     }
class=class="str">"cmt">//--- 清空表单数组
   ::ArrayFree(m_wnd);
   ::ArrayFree(m_windows);
  }

退出时如何干净销毁面板

EA 或脚本在图表卸载、品种切换、终端关闭时都会触发去初始化事件,reason 参数携带具体退出原因码(如 REASON_REMOVE=1、REASON_RECOMPILE=2)。若不及时清理自己创建的图形对象与事件绑定,MT5 会在日志刷出 'object already exists' 或残留不可点击的隐藏控件。 上面这段 CProgram::OnDeinitEvent 只做了一件事:调用 CWndEvents::Destroy() 释放之前由 CWndEvents 管理的所有界面元素与事件钩子。注意它没有判断 reason 值——因为无论哪种退出路径,销毁 UI 都是必须的,按原因分流反而容易漏掉分支。 在 MT5 里实测:注释掉 Destroy() 后连续加载/移除指标 5 次,图表对象总数从 3 涨到 18,且部分按钮失去响应。所以把销毁逻辑收口到这一个入口,比在各处零散 DeleteObject 更稳。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 去初始化                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnDeinitEvent(class="kw">const class="type">int reason)
  {
   class=class="str">"cmt">//--- 删除界面
   CWndEvents::Destroy();  
  }

「别急着下结论」

这套图形界面库从一开始就是按跨平台思路写的,所以下一章会直接在 MetaTrader 4 上跑指标和脚本两类程序来验证表单表现。当前发布的压缩包里,MQL4 版 51.67 KB、MQL5 版 51.59 KB,体积差不到 0.1 KB,说明两套接口封装得相当接近。 想自己验真,把 easyandfastgui_mql5.zip 解进 MT5 的 MQL5/Include 目录,挂上第二章的表单类编译一个指标,看图表里能不能拖出无卡顿的面板。外汇和贵金属图表加载第三方 GUI 有占用主线程导致点差扩大时段卡顿的风险,建议在模拟盘先跑。 真要改底层,别照抄俄文社区的旧封装,先读一遍第一章库结构再动表单类——界面库的高复用是靠清晰的依赖层级换来的,跳步改容易把鼠标悬停变色这类事件链弄断。

常见问题

多半是没在图表点击事件里判断鼠标是否在按钮热区;先打印点击坐标比对对象边框,确认命中再触发折叠。
根因是切换时旧对象未销毁又跑了一次创建函数;应在 OnChartEvent 的变更分支里先调用删除再重建。
小布可以扫描当前图表对象列表,标记名称前缀重复的面板元素,并提示你调用销毁函数清理。
在反初始化里遍历用 ObjectDelete 删掉所有带约定前缀的对象,别只隐藏,否则重加载会叠加。
别急下结论;先排除遮挡层和坐标偏移,很多是上层文本对象挡了点击热区导致事件没传到按钮。