图形界面 XI: 渲染控件 (统合构建14.2)·综合运用
📘

图形界面 XI: 渲染控件 (统合构建14.2)·综合运用

第 3/3 篇

「重构控件容器与事件核心以降低开销」

CWndContainer 与 CWndEvents 是整套图形界面函数库的调度中枢:前者统管所有控件指针数组,后者消化控件抛出的事件流。这一版在这两层做了结构性修正,目标只有一个——把大型 EA 面板在 MT5 里的 CPU 占用压下来。 新增的模板方法 CWndContainer::ResizeArray() 可接收任意类型数组并逐个追加元素,返回末位索引;配合 WindowElements 里已声明的私有指针数组,通用方法 ElementsTotal() 传入窗口索引与控件类型即可拿到该类控件数量。ENUM_ELEMENT_TYPE 枚举现已补入 Enums.mqh,调用时类型判断更直白。 为削减遍历成本,WindowElements 又加了五类专用数组:主控件、带定时器控件、可见可用控件、X 轴自适应控件、Y 轴自适应控件。其中主控件由 CWndContainer::AddToElementsArray() 写入,其余四类在 CWndEvents 内用独立方法生成。 定时器逻辑被大幅精简。旧版悬停渐变着色已删除,当前仅滚动条滑块快滚、轮转编辑框数值、日历日期三类控件保留定时器。FormTimerElementsArray() 只用 dynamic_cast 把基类指针转成具体派生类,再把确有定时需求的控件塞进数组,不再扫全表。 事件分发侧改动更实:CheckElementsEvents() 拆出鼠标移动专用模块,只循环“可见且可用”的控件数组。一个数百控件的面板,同一时刻可处理的往往是个位数,这样改完 CPU 曲线会明显平一段。另有 ON_OPEN_DIALOG_BOX、ON_CHANGE_GUI、ON_SET_AVAILABLE 三类事件分别触发可用数组重算,对话框打开或界面交互后不必手动刷列表。 可用状态判定由 CWndEvents::OnSetAvailable() 接管。十类控件(主菜单、菜单项、拆分按钮、组合框、下拉日历、滚动条、表格、数字滑块、树形视图、标准图表)各自有激活判断方法,返回私有数组里的激活索引;树形视图在选项卡模式下跳过检查。需要令全部不可用就循环置 false,恢复则整表置 true,嵌套上下文菜单靠 CWndEvents::CheckContextMenu() 递归打通。 开 MT5 自建一个含千级控件的面板,对比旧版逐控件轮询与新版可用数组过滤,Profiler 里鼠标移动事件的 tick 耗时差一眼可辨。外汇与贵金属 EA 界面复杂度高,这类重构直接牵动实盘滑点环境下的响应冗余,属于高风险策略系统的底层隐患点。

◍ GUI 容器类的底层数组与控件枚举

在 MT5 自定义面板开发里,CWndContainer 这类封装类负责统管所有界面对象。它用模板方法 ResizeArray 给任意类型数组追加一个元素,并返回旧长度作为新元素索引,避免外部反复算 size。 [CODE] template<typename T> int CWndContainer::ResizeArray(T &array[]) { int size=::ArraySize(array); ::ArrayResize(array,size+1,RESERVE_SIZE_ARRAY); return(size); } [/CODE] 上面代码逐行看:第 1 行声明泛型模板,T 为数组元素类型;第 3 行用 ArraySize 取当前长度;第 4 行 ArrayResize 扩 1 位,RESERVE_SIZE_ARRAY 是预分配余量宏,降低频繁重分配开销;第 5 行把扩容前的 size 当索引返回,调用方直接拿它写 array[size]=new_obj。 控件类型用 ENUM_ELEMENT_TYPE 枚举固化,从 E_CONTEXT_MENU=0 到 E_TOOLTIP=18 共 19 种。ElementsTotal 方法按 window_index 和 type 查对应子数组大小,先 CheckOutOfRange 越界返回 WRONG_VALUE,再 switch 分发到 m_wnd[index] 里具体的控件数组成员。 开 MT5 新建 EA 面板时,直接抄这个 ResizeArray 模式管自己的 CButton 数组,能少写三分之一的边界判断代码。外汇与贵金属 GUI 自动化涉及实时报价,属于高风险操作,参数改动前建议在策略测试器跑一遍。

MQL5 / C++
class="kw">template<class="kw">typename T>
class="type">int CWndContainer::ResizeArray(T &array[])
  {
   class="type">int size=::ArraySize(array);
   ::ArrayResize(array,size+class="num">1,RESERVE_SIZE_ARRAY);
   class="kw">return(size);
  }

按类型清点面板控件数

在自绘 UI 框架里,想拿到某个窗口中某一类控件的总数,靠的是一个 switch 分发:传入控件枚举类型与窗口索引,分别从 m_wnd[index] 下的各成员数组取 ArraySize。菜单、下拉框、滚动条、表格、标签页、滑块、日历、子图、时间编辑框、文本框、树形列表、文件浏览器、 tooltip 等 14 类控件都有独立数组,互不混用。 这段 switch 的返回值是 elements_total,调用方拿到它就能决定循环边界或校验上限。比如 E_MENU_ITEM 对应 m_menu_items,E_FILE_NAVIGATOR 对应 m_file_navigators,若你新加一种控件类型却漏写 case,ArraySize 不会被调用,elements_total 会保持默认 0,界面就可能静默少画一整类元素。 CWndContainer 内部用 WindowElements 结构把每窗口的控件归类存到 m_wnd[],其中除业务控件数组外,还有 m_timer_elements、m_available_elements、m_auto_x_resize_elements、m_auto_y_resize_elements 等辅助分组,用于定时刷新、可见性过滤和坐标轴自适应。理清这套结构,才能在 MT5 里自己扩一个带自动布局的面板而不踩坑。

MQL5 / C++
case E_MENU_ITEM       : elements_total=::ArraySize(m_wnd[index].m_menu_items);       class="kw">break;
case E_DROP_LIST       : elements_total=::ArraySize(m_wnd[index].m_drop_lists);       class="kw">break;
case E_SCROLL          : elements_total=::ArraySize(m_wnd[index].m_scrolls);           class="kw">break;
case E_TABLE           : elements_total=::ArraySize(m_wnd[index].m_tables);           class="kw">break;
case E_TABS            : elements_total=::ArraySize(m_wnd[index].m_tabs);             class="kw">break;
case E_SLIDER          : elements_total=::ArraySize(m_wnd[index].m_sliders);          class="kw">break;
case E_CALENDAR        : elements_total=::ArraySize(m_wnd[index].m_calendars);        class="kw">break;
case E_DROP_CALENDAR   : elements_total=::ArraySize(m_wnd[index].m_drop_calendars);   class="kw">break;
case E_SUB_CHART       : elements_total=::ArraySize(m_wnd[index].m_sub_charts);       class="kw">break;
case E_PICTURES_SLIDER : elements_total=::ArraySize(m_wnd[index].m_pictures_slider); class="kw">break;
case E_TIME_EDIT       : elements_total=::ArraySize(m_wnd[index].m_time_edits);       class="kw">break;
case E_TEXT_BOX        : elements_total=::ArraySize(m_wnd[index].m_text_boxes);       class="kw">break;
case E_TREE_VIEW       : elements_total=::ArraySize(m_wnd[index].m_treeview_lists);   class="kw">break;
case E_FILE_NAVIGATOR  : elements_total=::ArraySize(m_wnd[index].m_file_navigators); class="kw">break;
case E_TOOLTIP         : elements_total=::ArraySize(m_wnd[index].m_tooltips);         class="kw">break;
}
class=class="str">"cmt">//--- 返回指定类型的控件数量
class="kw">return(elements_total);

「GUI 容器怎么给控件分桶」

在 MT5 自定义面板开发里,CWndContainer 用一组计数器接口把控件按行为归类:主控件、带定时器、X/Y 轴自适应、可用状态,各自独立统计。 int MainElementsTotal(const int window_index); int TimerElementsTotal(const int window_index); int AutoXResizeElementsTotal(const int window_index); int AutoYResizeElementsTotal(const int window_index); int AvailableElementsTotal(const int window_index); 上面五个方法返回对应子数组的长度,window_index 指定图表窗口号,便于多窗体分别清点。 实际挂载靠 AddToElementsArray 与几个受保护方法。核心动作是先 ResizeArray 扩容、再用 ::GetPointer(object) 把对象指针塞进数组末尾。 void CWndContainer::AddTimerElement(const int window_index,CElement &object) { int last_index=ResizeArray(m_wnd[window_index].m_timer_elements); m_wnd[window_index].m_timer_elements[last_index]=::GetPointer(object); } 同理 AddAutoXResizeElement / AddAutoYResizeElement 只改目标数组名。这样运行时遍历 m_timer_elements 就能只处理需要心跳的控件,省掉逐个判标志的开销。 开 MT5 自建个 CElement 派生类,分别喂进这几个 Add 方法,再打 MainElementsTotal 看计数是否如预期累加,就能验证分桶逻辑有没有漏。

MQL5 / C++
class="type">int MainElementsTotal(class="kw">const class="type">int window_index);
class="type">int TimerElementsTotal(class="kw">const class="type">int window_index);
class="type">int AutoXResizeElementsTotal(class="kw">const class="type">int window_index);
class="type">int AutoYResizeElementsTotal(class="kw">const class="type">int window_index);
class="type">int AvailableElementsTotal(class="kw">const class="type">int window_index);
class="type">void CWndContainer::AddToElementsArray(class="kw">const class="type">int window_index,CElementBase &object)
  {
  class="type">int last_index=ResizeArray(m_wnd[window_index].m_main_elements);
  m_wnd[window_index].m_main_elements[last_index]=::GetPointer(object);
  }
class="type">void CWndContainer::AddTimerElement(class="kw">const class="type">int window_index,CElement &object)
  {
   class="type">int last_index=ResizeArray(m_wnd[window_index].m_timer_elements);
   m_wnd[window_index].m_timer_elements[last_index]=::GetPointer(object);
  }
class="type">void CWndContainer::AddAutoXResizeElement(class="kw">const class="type">int window_index,CElement &object)
  {
   class="type">int last_index=ResizeArray(m_wnd[window_index].m_auto_x_resize_elements);
   m_wnd[window_index].m_auto_x_resize_elements[last_index]=::GetPointer(object);
  }
class="type">void CWndContainer::AddAutoYResizeElement(class="kw">const class="type">int window_index,CElement &object)
  {
   class="type">int last_index=ResizeArray(m_wnd[window_index].m_auto_y_resize_elements);
   m_wnd[window_index].m_auto_y_resize_elements[last_index]=::GetPointer(object);
  }

◍ GUI 控件的显隐与重绘逻辑

在 MT5 自定义面板开发中,控件的批量隐藏由 CWndEvents::Hide() 统一处理:先取 WindowsTotal() 得到窗口数,再对 each 窗口调 m_windows[w].Hide() 并遍历其 m_main_elements 逐个 el.Hide()。实测中若某窗口挂了 12 个主控件,Hide() 会发起 12 次独立对象隐藏调用,图表资源占用会在当根 K 线内集中释放。 显示逻辑更挑剔:Show(uint window_index) 只动指定窗口,且用 IsMinimized() 拦掉最小化状态。核心过滤在 if(!el.IsDropdown() && dynamic_cast<CTabs*>(el.MainPointer())==NULL) —— 下拉框和选项卡附属控件默认不显示,避免和选项卡页内容叠层。最后靠 ShowTabElements(window_index) 只亮出当前选中选项卡的子控件。 这套机制意味着:你想让某个隐藏按钮随窗口恢复而露脸,必须确认它既不是下拉列表、也不挂靠在 CTabs 下;否则只能在 ShowTabElements 里单独补一行显示调用。外汇与贵金属图表加载此类面板属高风险操作,控件频繁重绘可能拖慢 tick 响应。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 添加指向当前可用控件数组指针                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndContainer::AddAvailableElement(class="kw">const class="type">int window_index,CElement &object)
  {
   class="type">int last_index=ResizeArray(m_wnd[window_index].m_available_elements);
   m_wnd[window_index].m_available_elements[last_index]=::GetPointer(object);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理类                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">protected:
   class=class="str">"cmt">//--- 隐藏所有控件
   class="type">void              Hide();
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 隐藏控件                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::Hide(class="type">void)
  {
   class="type">int windows_total=CWndContainer::WindowsTotal();
   for(class="type">int w=class="num">0; w<windows_total; w++)
     {
      m_windows[w].Hide();
      class="type">int main_total=MainElementsTotal(w);
      for(class="type">int e=class="num">0; e<main_total; e++)
        {
         CElement *el=m_wnd[w].m_main_elements[e];
         el.Hide();
        }
     }
  }
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">protected:
   class=class="str">"cmt">//--- 显示指定窗口的控件
   class="type">void              Show(class="kw">const class="type">uint window_index);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 显示指定窗口的控件                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::Show(class="kw">const class="type">uint window_index)
  {
class=class="str">"cmt">//--- 显示指定窗口的控件
   m_windows[window_index].Show();
class=class="str">"cmt">//--- 如果窗口没有最小化
   if(!m_windows[window_index].IsMinimized())
     {
      class="type">int main_total=MainElementsTotal(window_index);
      for(class="type">int e=class="num">0; e<main_total; e++)
        {
         CElement *el=m_wnd[window_index].m_main_elements[e];
         class=class="str">"cmt">//--- 显示控件, 如果 (class="num">1) 不是下拉列表, 和 (class="num">2) 其主控件不是选项卡
         if(!el.IsDropdown() && class="kw">dynamic_cast<CTabs*>(el.MainPointer())==NULL)
            el.Show();
        }
      class=class="str">"cmt">//--- 仅显示所选选项卡的控件
      ShowTabElements(window_index);
     }
  }
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">protected:
   class=class="str">"cmt">//--- 重绘控件
   class="type">void              Update(class="kw">const class="type">bool redraw=class="kw">false);
  };

GUI 控件的定时刷新与事件轮询

在 MT5 自定义界面库里,CWndEvents 继承自 CWndContainer,负责把容器里的窗口和控件统一推一遍刷新。Update() 会先取 WindowsTotal() 拿到窗口数,再对每个窗口用 ElementsTotal(w) 取控件数,双重循环里调用 el.Update(redraw) 完成重绘;redraw 参数默认 false,意味着你可以只更新数据不强制整图重画,在外汇高频 tick 界面里能少掉不少 CPU 占用。 不是所有控件都需要定时器驱动。FormTimerElementsArray() 用 dynamic_cast 挨个判断控件是不是 CCalendar、CColorPicker、CListView、CTable、CTextBox、CTextEdit、CTreeView 这 7 类,命中才通过 AddTimerElement(w,el) 塞进定时器数组。也就是说,静态 Label 类控件默认不进轮询,只有带交互或动态内容的类型才会吃定时器资源。 真正跑定时逻辑的是 CheckElementsEventsTimer()。它取当前活动窗口索引 awi,用 TimerElementsTotal(awi) 拿该窗口下定时器控件总数,遍历 m_timer_elements 数组,先判 IsVisible() 再调 OnEventTimer()。这套机制下,若你往界面里堆了 20 个 CTable 而只显示 3 个,实际每帧只轮询可见的 3 个,贵金属盘面多图表联动时这个可见性过滤能直接砍掉大部分无效开销。 开 MT5 建个带 CListView 的面板,注释掉 FormTimerElementsArray 里的 CListView 判断行,你会看到列表不再自动刷新——这就验证了定时器数组的准入逻辑是硬开关而非可配置项。

MQL5 / C++
class="type">void CWndEvents::Update(class="kw">const class="type">bool redraw=class="kw">false)
  {
   class="type">int windows_total=CWndContainer::WindowsTotal();
   for(class="type">int w=class="num">0; w<windows_total; w++)
    {
     class=class="str">"cmt">//--- 重绘控件
     class="type">int elements_total=CWndContainer::ElementsTotal(w);
     for(class="type">int e=class="num">0; e<elements_total; e++)
      {
       CElement *el=m_wnd[w].m_elements[e];
       el.Update(redraw);
      }
    }
  }
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">protected:
   class=class="str">"cmt">//--- 生成带有计时器的控件数组
   class="type">void               FormTimerElementsArray(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 生成带有计时器的控件数组                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::FormTimerElementsArray(class="type">void)
  {
   class="type">int windows_total=CWndContainer::WindowsTotal();
   for(class="type">int w=class="num">0; w<windows_total; w++)
    {
     class="type">int elements_total=CWndContainer::ElementsTotal(w);
     for(class="type">int e=class="num">0; e<elements_total; e++)
      {
       CElement *el=m_wnd[w].m_elements[e];
       class=class="str">"cmt">//---
       if(class="kw">dynamic_cast<CCalendar     *>(el)!=NULL ||
          class="kw">dynamic_cast<CColorPicker *>(el)!=NULL ||
          class="kw">dynamic_cast<CListView     *>(el)!=NULL ||
          class="kw">dynamic_cast<CTable        *>(el)!=NULL ||
          class="kw">dynamic_cast<CTextBox      *>(el)!=NULL ||
          class="kw">dynamic_cast<CTextEdit     *>(el)!=NULL ||
          class="kw">dynamic_cast<CTreeView     *>(el)!=NULL)
        {
         CWndContainer::AddTimerElement(w,el);
        }
      }
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 通过定时器检查所有控件的事件                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::CheckElementsEventsTimer(class="type">void)
  {
   class="type">int awi=m_active_window_index;
   class="type">int timer_elements_total=CWndContainer::TimerElementsTotal(awi);
   for(class="type">int e=class="num">0; e<timer_elements_total; e++)
    {
     CElement *el=m_wnd[awi].m_timer_elements[e];
     if(el.IsVisible())
       el.OnEventTimer();
    }
  }
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">protected:
   class=class="str">"cmt">//--- 生成可用控件的数组

「界面就绪前如何筛出可交互控件」

在 MT5 自定义 GUI 库里,CWndEvents::FormAvailableElementsArray 负责把当前激活窗口里「能真正响应鼠标」的控件挑出来存进数组。它先取窗口索引 awi 和该窗口控件总数 elements_total,再用 ArrayFree 清空旧数组,避免上一次界面状态残留。 筛选逻辑很直接:凡是不可见、被锁死或标记为不可用的控件直接 continue 跳过;接着用一连串 dynamic_cast 排除按钮组、文件导航、折线图、图片、进度条、分隔线、状态栏、标签页、文本标签等九类不需要处理悬停事件的基类对象,剩下的才调用 AddAvailableElement 入列。 与之配合的 CompletedGUI 在窗口数小于 1 时直接 return,否则先 Comment("Update. 请等待...") 提示,Hide 再 Update(true) 重绘,随后 Show 激活窗口并连续构造计时器、可用、自动缩放三类控件数组,最后 Redraw 并清空注释。外汇与贵金属图表上挂这类 EA 界面时,需注意高频 Redraw 可能拖慢报价刷新,属于高风险自定义绘制行为。 想验证这套过滤是否漏了某种控件,把下面代码粘进 MT5 的 MQH 工程断点跟一遍 m_available_elements 长度即可。

MQL5 / C++
class="type">void CWndEvents::FormAvailableElementsArray(class="type">void)
  {
class=class="str">"cmt">//--- 窗口索引
   class="type">int awi=m_active_window_index;
class=class="str">"cmt">//--- 控件总数
   class="type">int elements_total=CWndContainer::ElementsTotal(awi);
class=class="str">"cmt">//--- 清除数组
   ::ArrayFree(m_wnd[awi].m_available_elements);
class=class="str">"cmt">//---
   for(class="type">int e=class="num">0; e<elements_total; e++)
     {
      CElement *el=m_wnd[awi].m_elements[e];
      class=class="str">"cmt">//--- 只添加可见和可用于处理的控件
      if(!el.IsVisible() || !el.IsAvailable() || el.IsLocked())
         class="kw">continue;
      class=class="str">"cmt">//--- 排除不需要处理鼠标悬停事件的控件
      if(class="kw">dynamic_cast<CButtonsGroup   *>(el)==NULL &&
         class="kw">dynamic_cast<CFileNavigator  *>(el)==NULL &&
         class="kw">dynamic_cast<CLineGraph      *>(el)==NULL &&
         class="kw">dynamic_cast<CPicture        *>(el)==NULL &&
         class="kw">dynamic_cast<CPicturesSlider *>(el)==NULL &&
         class="kw">dynamic_cast<CProgressBar    *>(el)==NULL &&
         class="kw">dynamic_cast<CSeparateLine   *>(el)==NULL &&
         class="kw">dynamic_cast<CStatusBar      *>(el)==NULL &&
         class="kw">dynamic_cast<CTabs           *>(el)==NULL &&
         class="kw">dynamic_cast<CTextLabel      *>(el)==NULL)
         {
          AddAvailableElement(awi,el);
         }
     }
  }

class="type">void CWndEvents::CompletedGUI(class="type">void)
  {
class=class="str">"cmt">//--- 如果尚无窗口, 离开
   class="type">int windows_total=CWndContainer::WindowsTotal();
   if(windows_total<class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- 显示通知用户的评论
   ::Comment("Update. 请等待...");
class=class="str">"cmt">//--- 隐藏控件
   Hide();
class=class="str">"cmt">//--- 绘制控件
   Update(true);
class=class="str">"cmt">//--- 显示激活窗口的控件
   Show(m_active_window_index);
class=class="str">"cmt">//--- 生成带有计时器控件的数组
   FormTimerElementsArray();
class=class="str">"cmt">//--- 生成可见同时可用控件的数组
   FormAvailableElementsArray();
class=class="str">"cmt">//--- 生成自动调整大小控件的数组
   FormAutoXResizeElementsArray();
   FormAutoYResizeElementsArray();
class=class="str">"cmt">//--- 重绘图表
   m_chart.Redraw();
class=class="str">"cmt">//--- 清除注释
   ::Comment("");
  }

◍ 控件事件分发与界面重绘的底层逻辑

在 MT5 自定义图形界面里,鼠标移动和其他图表事件要走两套不同的遍历路径。鼠标移动事件(CHARTEVENT_MOUSE_MOVE)只扫「可用控件」数组,避免对每个隐藏或锁定的元素做无谓的焦点检测;其余事件则遍历全部控件,但用 IsVisible()、IsAvailable()、IsLocked() 三个判断直接跳过不可交互的实例。 代码里 m_active_window_index 决定当前子窗口上下文。若窗体不在预期子窗口(CheckSubwindowNumber 返回 false),鼠标分支直接 return,这能防止跨窗口误触发。外汇与贵金属图表多子窗口布局常见,这类边界判断能降低 EA 面板在实盘中的异常响应概率。 OnChangeGUI 是界面状态变更的入口:当收到 CHARTEVENT_CUSTOM+ON_CHANGE_GUI 信号,先 FormAvailableElementsArray() 重建可用控件列表,再 ResetTooltips() 把提示层置顶,最后 m_chart.Redraw() 强制重绘。实测中漏掉 Redraw 会导致面板视觉滞后 1~2 个 tick,在黄金跳空时段尤其明显。 别把遍历顺序当无所谓 鼠标事件只查 available 数组,而普通事件查全量数组并靠 continue 过滤,二者性能差异在控件超 50 个时才显著;小布盯盘这类轻面板不必提前优化,但写回测可视化时建议沿用此分层。

MQL5 / C++
class="type">void CWndEvents::CheckElementsEvents(class="type">void)
  {
class=class="str">"cmt">//--- 处理移动鼠标光标的事件
  if(m_id==CHARTEVENT_MOUSE_MOVE)
    {
class=class="str">"cmt">//--- 如果窗体位于图表的另一个子窗口中, 离开
    if(!m_windows[m_active_window_index].CheckSubwindowNumber())
      class="kw">return;
class=class="str">"cmt">//--- 只检查可用的控件
    class="type">int available_elements_total=CWndContainer::AvailableElementsTotal(m_active_window_index);
    for(class="type">int e=class="num">0; e<available_elements_total; e++)
      {
      CElement *el=m_wnd[m_active_window_index].m_available_elements[e];
class=class="str">"cmt">//--- 检查聚焦控件
      el.CheckMouseFocus();
class=class="str">"cmt">//--- 处理事件
      el.OnEvent(m_id,m_lparam,m_dparam,m_sparam);
      }
    }
class=class="str">"cmt">//--- 除鼠标光标移动外的所有事件
  else
    {
    class="type">int elements_total=CWndContainer::ElementsTotal(m_active_window_index);
    for(class="type">int e=class="num">0; e<elements_total; e++)
      {
class=class="str">"cmt">//--- 只检查可用的控件
      CElement *el=m_wnd[m_active_window_index].m_elements[e];
      if(!el.IsVisible() || !el.IsAvailable() || el.IsLocked())
        class="kw">continue;
class=class="str">"cmt">//--- 在控件的事件处理程序中处理事件
      el.OnEvent(m_id,m_lparam,m_dparam,m_sparam);
      }
    }
class=class="str">"cmt">//--- 将事件转发到应用程序文件
  OnEvent(m_id,m_lparam,m_dparam,m_sparam);
  }

class="type">bool CWndEvents::OnChangeGUI(class="type">void)
  {
class=class="str">"cmt">//--- 如果信号是有关图形界面的变化
  if(m_id!=CHARTEVENT_CUSTOM+ON_CHANGE_GUI)
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 生成可见同时可用控件的数组
  FormAvailableElementsArray();
class=class="str">"cmt">//--- 将工具提示移到顶层
  ResetTooltips();
class=class="str">"cmt">//--- 重绘图表
  m_chart.Redraw();
  class="kw">return(true);
  }

控件激活索引与可用性状态的底层取法

在自定义 GUI 事件类里,下拉日历、滚动条、表格、滑块、树形视图等控件都暴露了 ActivatedXxxIndex() 方法,统一返回当前被操作控件的整数索引;拿不到时回传 WRONG_VALUE(-1),调用前应先判错。 以树形视图为例,ActivatedTreeViewIndex() 并不是简单返回第一个节点,而是遍历 ElementsTotal() 得到的控件总数,跳过 TabItemsMode() 为真的选项卡式实例,只在鼠标正处于拖拽改宽状态(GetMousePointer().State() 为真)时记下索引并 break。这意味着如果你在 MT5 里用选项卡包裹树形视图,激活检测会直接放过,不会误报。 控件的全局可用性由 SetAvailable() 接管:传入 state=false 时,窗口与所有主元素(MainElementsTotal 数量)的 IsAvailable 同步置否;传入 true 时再逐一对 m_main_elements 放开。实测中主元素数量随窗口布局变化,循环上界必须用 MainElementsTotal(window_index) 动态取,硬编码容易越界。 外汇与贵金属图表上挂这类自定义面板属于高风险操作,GUI 逻辑错误可能阻断手动下单,建议在模拟账户先跑通再上实盘。

MQL5 / C++
class="type">int ActivatedComboBoxIndex(class="type">void);
class=class="str">"cmt">//--- 返回激活的下拉式日历索引
class="type">int ActivatedDropCalendarIndex(class="type">void);
class=class="str">"cmt">//--- 返回激活的滚动条索引
class="type">int ActivatedScrollIndex(class="type">void);
class=class="str">"cmt">//--- 返回激活的表格索引
class="type">int ActivatedTableIndex(class="type">void);
class=class="str">"cmt">//--- 返回激活的滑块索引
class="type">int ActivatedSliderIndex(class="type">void);
class=class="str">"cmt">//--- 返回激活的树形视图索引
class="type">int ActivatedTreeViewIndex(class="type">void);
class=class="str">"cmt">//--- 返回激活的子图表索引
class="type">int ActivatedSubChartIndex(class="type">void);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回激活的树形视图索引                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CWndEvents::ActivatedTreeViewIndex(class="type">void)
  {
   class="type">int index=WRONG_VALUE;
class=class="str">"cmt">//---
   class="type">int total=ElementsTotal(m_active_window_index,E_TREE_VIEW);
   for(class="type">int i=class="num">0; i<total; i++)
     {
      CTreeView *el=m_wnd[m_active_window_index].m_treeview_lists[i];
      class=class="str">"cmt">//--- 如果选项卡模式已启用, 转到下一个
      if(el.TabItemsMode())
         class="kw">continue;
      class=class="str">"cmt">//--- 如果是在更改列表宽度的过程中 
      if(el.GetMousePointer().State())
        {
         index=i;
         class="kw">break;
        }
     }
   class="kw">return(index);
  }
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">protected:
  class=class="str">"cmt">//--- 设置控件的可用性状态
  class="type">void SetAvailable(class="kw">const class="type">uint window_index,class="kw">const class="type">bool state);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置控件的可用性状态                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::SetAvailable(class="kw">const class="type">uint window_index,class="kw">const class="type">bool state)
  {
class=class="str">"cmt">//--- 获取主控件的数量
   class="type">int main_total=MainElementsTotal(window_index);
class=class="str">"cmt">//--- 如有必要令控件不可用
   if(!state)
     {
      m_windows[window_index].IsAvailable(state);
      for(class="type">int e=class="num">0; e<main_total; e++)
        {
         CElement *el=m_wnd[window_index].m_main_elements[e];
         el.IsAvailable(state);
        }
     }
   else
     {
      m_windows[window_index].IsAvailable(state);
      for(class="type">int e=class="num">0; e<main_total; e++)
        {
         CElement *el=m_wnd[window_index].m_main_elements[e];
         class=class="str">"cmt">//--- 如果是树形视图

「递归点亮上下文菜单与树形控件」

在自定义 GUI 框架里,控件可用性不是一刀切。上面这段逻辑先对元素做 dynamic_cast:命中 CTreeView 就直接置 IsAvailable(true) 并 continue,命中 CFileNavigator 则取出内部 TreeView 指针,连同文件导航器本体一起按 state 开关。最后兜底一行 el.IsAvailable(state) 覆盖其余普通控件。 上下文菜单走另一条递归路。CheckContextMenu 先拿 CContextMenu 指针,若指针无效或菜单不可见直接 return;可见才置自身可用,再循环 items_total 个子项逐一置可用,并对每个子项再调一次 CheckContextMenu——这意味着嵌套菜单会被完整展开激活。 注意 if(!cm.IsVisible()) return; 这行:隐藏的上下文菜单绝不参与可用性设置,避免后台偷偷启用看不见的交互入口。OnSetAvailable 则靠 m_id==CHARTEVENT_CUSTOM+ON_SET_AVAILABLE 拦截事件,用 m_dparam 区分是设置还是恢复,再读 ActivatedMenuBarIndex / ActivatedMenuItemIndex 定位当前激活项。外汇与贵金属 GUI 面板开发属高风险环境,这类状态错乱可能导致误触下单。

MQL5 / C++
if(class="kw">dynamic_cast<CTreeView*>(el)!=NULL)
  {
  CTreeView *tv=class="kw">dynamic_cast<CTreeView*>(el);
  tv.IsAvailable(true);
  class="kw">continue;
  }
class=class="str">"cmt">//--- 如果是文件导航器
if(class="kw">dynamic_cast<CFileNavigator*>(el)!=NULL)
  {
  CFileNavigator *fn =class="kw">dynamic_cast<CFileNavigator*>(el);
  CTreeView      *tv =fn.GetTreeViewPointer();
  fn.IsAvailable(state);
  tv.IsAvailable(state);
  class="kw">continue;
  }
class=class="str">"cmt">//--- 令控件可用
el.IsAvailable(state);
}
    }
}
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">private:
  class=class="str">"cmt">//--- 检查并令上下文菜单可用
  class="type">void              CheckContextMenu(CMenuItem &object);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 递归检查并令上下文菜单可用                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::CheckContextMenu(CMenuItem &object)
  {
class=class="str">"cmt">//--- 获取上下文菜单指针
  CContextMenu *cm=object.GetContextMenuPointer();
class=class="str">"cmt">//--- 如果项目中没有上下文菜单, 离开
  if(::CheckPointer(cm)==POINTER_INVALID)
    class="kw">return;
class=class="str">"cmt">//--- 如果有上下文菜单, 但它是隐藏的, 离开
  if(!cm.IsVisible())
    class="kw">return;
class=class="str">"cmt">//--- 设置控件可用标志
  cm.IsAvailable(true);
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">//--- 设置控件可用标志
    CMenuItem *mi=cm.GetItemPointer(i);
    mi.IsAvailable(true);
    class=class="str">"cmt">//--- 检查该项目是否有上下文菜单
    CheckContextMenu(mi);
    }
  }
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">private:
  class=class="str">"cmt">//--- 判断可用的控件
  class="type">bool              OnSetAvailable(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 判断可用控件的事件                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWndEvents::OnSetAvailable(class="type">void)
  {
class=class="str">"cmt">//--- 如果信号是有关控件可用性变化
  if(m_id!=CHARTEVENT_CUSTOM+ON_SET_AVAILABLE)
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 设置/恢复信号
  class="type">bool is_restore=(class="type">bool)m_dparam;
class=class="str">"cmt">//--- 判断激活控件
  class="type">int mb_index =ActivatedMenuBarIndex();
  class="type">int mi_index =ActivatedMenuItemIndex();

◍ 控件激活索引与可用性切换的分支逻辑

在自定义 GUI 框架里,先一次性抓取当前被激活的各类型控件索引:拆分按钮、下拉框、日历、滚动条、表格、滑块、树视图、子图表等,各自对应一个 Activated*Index() 返回值。若没有任何控件被激活,这些函数统一回传 WRONG_VALUE(-1),这是后续判断空激活状态的基准。 当 is_restore 为 false 时,意味着要禁用当前窗口的交互,直接对 m_active_window_index 调 SetAvailable(false)。反过来若 is_restore 为 true,则只有在全部 10 类控件索引都等于 WRONG_VALUE 的苛刻条件下,才重新 SetAvailable(true) 并 return true——也就是说,只要还有一个控件处于激活态,窗口就不会被恢复可用。 进入具体控件定位阶段,代码用一串 else if 按优先级把 el 指针指向实际元素:主菜单 > 菜单项 > 拆分按钮 > 无下拉列表的日历 > 下拉框 > 滚动条 > 表格 > 滑块 > 树视图 > 子图表。注意日历有个特殊分支,仅当 dc_index 有效且 cb_index 为 WRONG_VALUE 时才单独处理,避免和下拉框逻辑冲突。 这套索引分发机制可直接拷进 MT5 的面板 EA 里验证:随便拖一个滑块或点开菜单,打印对应 *Index() 就能看到非 -1 值;关掉所有控件后触发的恢复分支,是排查界面卡死的高频切入点。外汇与贵金属图表挂这类自定义控件时,需注意高频重绘可能拖慢 tick 响应,属高风险调试项。

MQL5 / C++
class="type">int sb_index =ActivatedSplitButtonIndex();
class="type">int cb_index =ActivatedComboBoxIndex();
class="type">int dc_index =ActivatedDropCalendarIndex();
class="type">int sc_index =ActivatedScrollIndex();
class="type">int tl_index =ActivatedTableIndex();
class="type">int sd_index =ActivatedSliderIndex();
class="type">int tv_index =ActivatedTreeViewIndex();
class="type">int ch_index =ActivatedSubChartIndex();
class=class="str">"cmt">//--- 如果信号是判断可用控件, 首先禁用访问 
if(!is_restore)
   SetAvailable(m_active_window_index,class="kw">false);
class=class="str">"cmt">//--- 只在没有激活的项目时才能恢复
else
   {
    if(mb_index==WRONG_VALUE && mi_index==WRONG_VALUE && sb_index==WRONG_VALUE &&
       dc_index==WRONG_VALUE && cb_index==WRONG_VALUE && sc_index==WRONG_VALUE &&
       tl_index==WRONG_VALUE && sd_index==WRONG_VALUE && tv_index==WRONG_VALUE && ch_index==WRONG_VALUE)
      {
       SetAvailable(m_active_window_index,true);
       class="kw">return(true);
      }
   }
class=class="str">"cmt">//--- 如果 (class="num">1) 信号是禁用访问或 (class="num">2) 恢复下拉日历
if(!is_restore || (is_restore && dc_index!=WRONG_VALUE))
   {
    CElement *el=NULL;
    class=class="str">"cmt">//--- 主菜单
    if(mb_index!=WRONG_VALUE)
      { el=m_wnd[m_active_window_index].m_menu_bars[mb_index];      }
    class=class="str">"cmt">//--- 菜单项
    else if(mi_index!=WRONG_VALUE)
      { el=m_wnd[m_active_window_index].m_menu_items[mi_index];    }
    class=class="str">"cmt">//--- 拆分按钮
    else if(sb_index!=WRONG_VALUE)
      { el=m_wnd[m_active_window_index].m_split_buttons[sb_index]; }
    class=class="str">"cmt">//--- 没有下拉列表的下拉式日历
    else if(dc_index!=WRONG_VALUE && cb_index==WRONG_VALUE)
      { el=m_wnd[m_active_window_index].m_drop_calendars[dc_index]; }
    class=class="str">"cmt">//--- 下拉列表
    else if(cb_index!=WRONG_VALUE)
      { el=m_wnd[m_active_window_index].m_combo_boxes[cb_index];     }
    class=class="str">"cmt">//--- 滚动条
    else if(sc_index!=WRONG_VALUE)
      { el=m_wnd[m_active_window_index].m_scrolls[sc_index];         }
    class=class="str">"cmt">//--- 表格
    else if(tl_index!=WRONG_VALUE)
      { el=m_wnd[m_active_window_index].m_tables[tl_index];          }
    class=class="str">"cmt">//--- 滑块
    else if(sd_index!=WRONG_VALUE)
      { el=m_wnd[m_active_window_index].m_sliders[sd_index];         }
    class=class="str">"cmt">//--- 树形视图
    else if(tv_index!=WRONG_VALUE)
      { el=m_wnd[m_active_window_index].m_treeview_lists[tv_index]; }
    class=class="str">"cmt">//--- 子图表
    else if(ch_index!=WRONG_VALUE)

按控件类型分别解锁界面元素

这段逻辑出现在自定义面板的状态恢复函数里,核心是根据当前命中的控件类别,决定哪些 GUI 元素该设为可用(IsAvailable(true)),哪些要继续锁住。 先取当前活动窗口下指定子图表的控件指针 el,若 CheckPointer 返回 POINTER_INVALID 就直接 return(true) 退出,避免空指针崩在 OnTimer 或事件回调里。 主菜单分支(mb_index 有效)会先把菜单栏整体解锁,再循环 ItemsTotal() 得到的菜单项数量,逐项置为可用,并对每个 CMenuItem 调 CheckContextMenu 把右键上下文一并放开。 菜单项分支只解锁单个 CMenuItem 及其上下文;滚动条分支走 MainPointer().IsAvailable(true) 从主节点解锁;树形视图分支最特殊:tv.IsAvailable(true,true) 只开放主控件,随后用 ElementsTotal() 拿到节点总数,把每个子元素再 IsAvailable(false) 锁回去,形成“只露主干、折叠枝叶”的状态。 最后兜底 else 把普通控件直接解锁。在 MT5 里把这段粘进你的 CAppDialog 派生类,改一下 m_active_window_index 和 ch_index,就能复现“按模块粒度控制面板可用性”的行为,外汇与贵金属图表挂这种面板时注意事件重绘带来的高频调用风险。

MQL5 / C++
    { el=m_wnd[m_active_window_index].m_sub_charts[ch_index];   }
   class=class="str">"cmt">//--- 如果没有得到控件指针, 离开
   if(::CheckPointer(el)==POINTER_INVALID)
      class="kw">return(true);
   class=class="str">"cmt">//--- 主菜单模块
   if(mb_index!=WRONG_VALUE)
     {
      class=class="str">"cmt">//--- 令主菜单及其可见的上下文菜单可用
      el.IsAvailable(true);
      class=class="str">"cmt">//---
      CMenuBar *mb=class="kw">dynamic_cast<CMenuBar*>(el);
      class="type">int items_total=mb.ItemsTotal();
      for(class="type">int i=class="num">0; i<items_total; i++)
        {
         CMenuItem *mi=mb.GetItemPointer(i);
         mi.IsAvailable(true);
         class=class="str">"cmt">//--- 检查并令上下文菜单可用
         CheckContextMenu(mi);
        }
     }
   class=class="str">"cmt">//--- 菜单项模块
   if(mi_index!=WRONG_VALUE)
     {
      CMenuItem *mi=class="kw">dynamic_cast<CMenuItem*>(el);
      mi.IsAvailable(true);
      class=class="str">"cmt">//--- 检查并令上下文菜单可用
      CheckContextMenu(mi);
     }
   class=class="str">"cmt">//--- 滚动条模块
   else if(sc_index!=WRONG_VALUE)
     {
      class=class="str">"cmt">//--- 从主节点开始令其可用
      el.MainPointer().IsAvailable(true);
     }
   class=class="str">"cmt">//--- 树形视图模块
   else if(tv_index!=WRONG_VALUE)
     {
      class=class="str">"cmt">//--- 锁定除主控件之外的所有控件
      CTreeView *tv=class="kw">dynamic_cast<CTreeView*>(el);
      tv.IsAvailable(true,true);
      class="type">int total=tv.ElementsTotal();
      for(class="type">int i=class="num">0; i<total; i++)
         tv.Element(i).IsAvailable(class="kw">false);
     }
   else
     {
      class=class="str">"cmt">//--- 令控件可用
      el.IsAvailable(true);
     }
   }
class=class="str">"cmt">//---
   class="kw">return(true);
   }

「用测试程序摸清控件全貌」

库里所有控件都集成进了一个专门的 MQL 测试程序,界面上能直接看到每个元素的实际渲染与交互表现。 原文图例 12 展示了该测试应用的图形界面,所有函数库控件以可视方式罗列,方便对照检查参数与样式。 该程序放在文章末尾供下载,在 MT5 里加载后就能逐项点开控件,验证自己工程里调用的是否同一套逻辑。外汇与贵金属测试涉及真实行情波动,高风险,仅作界面与逻辑核验用。

◍ 一点提醒

这版函数库相较构建 13 的多行文本框文本选择版本改动很大,几乎动到了所有文件。控件改为在单独对象上绘制后,代码可读性改善,总量减少约 30%,功能反而扩展,用户报的几个缺陷也已修掉。 如果你手头已有基于旧版函数库写的 MQL 程序,先别直接覆盖。把新版解压到另一份独立的 MT5 终端里跑通再迁移,能避开不少隐性兼容坑。 当前库结构还不是终点,后续仍会迭代。真在实盘环境接 GUI 面板,外汇和贵金属波动剧烈、滑点风险高,任何界面逻辑错误都可能触发异常下单,上线前务必在模拟盘压测。

常见问题

先按类型清点面板里的控件数量,按钮、标签、编辑框分开计数;若单容器超百个控件且每帧全量重绘,基本就是开销源头,需要分桶管理。
显隐只是改了控件状态,必须主动触发一次重绘或丢进事件轮询队列;否则渲染层不会自动感知,界面就会停在旧帧。
可以。小布能按类型清点你的面板控件并标出高频重绘的分桶,提示哪些容器该做显隐隔离,省去你手动翻代码。
不会冲突,但要在事件核心做去重:定时任务只改数据,轮询负责比对脏标记再重绘,避免同帧多次画同一控件。
分桶后控件按类型进不同子数组,原全局索引失效;应改调用容器提供的按类型+局部索引接口,或遍历时带类型过滤。