图形界面 V: 组合框控件 (第三章)·综合运用
🧩

图形界面 V: 组合框控件 (第三章)·综合运用

(3/3)· 当下拉选择挤爆面板空间,复合控件的类封装与事件联动才是 MT5 GUI 的最后一公里

含代码示例偏理论 第 3/3 篇
把十几个参数硬塞进静态按钮,界面只会越改越乱。组合框把列表收进下拉,既省空间又保留完整选项——本章把这类复合控件的封装逻辑一次讲透。

「组合框按钮与鼠标状态的事件分流」

做自定义组合框控件时,按钮点击和鼠标悬停不能混在一个事件里处理。拆成 OnClickButton() 和 CheckPressedOverButton() 两个私有方法,前者只管对象名比对后切换列表显隐,后者专门跟踪左键压在按钮上的持续状态。 CheckPressedOverButton() 的开头先判断表单是否被锁、激活元件 ID 是否匹配,任一不满足直接 return。若元件无鼠标焦点,再查列表视图是否抢了焦点或滚动条在动,都满足才继续隐藏列表、复原颜色,并在 ID 匹配且非下拉态时解锁表单——记住表单只能由锁住它的那个元件解锁。 有焦点分支里,列表已可见就退出;隐藏时则按光标是否还在按钮上切换 pressed / hover 两档背景色。OnClickButton() 需在 CHARTEVENT_OBJECT_CLICK 里调用,CheckPressedOverButton() 则前置在 CHARTEVENT_MOUSE_MOVE 中做可见性、可用性、左键状态三项检查。 列表项被点会发 ON_CLICK_LIST_ITEM 自定义事件,在组合框事件处理函数里收这个消息,ID 对得上就存下高亮项文字并收起列表。想在图表属性变动时也收列表,就挂 CHARTEVENT_CHART_CHANGE。下面这段是两类方法的骨架与核心逻辑,可直接贴进 MT5 看行为。

MQL5 / C++
class CListView : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 处理按钮被按下
   class="type">bool                OnClickButton(class="kw">const class="type">class="kw">string clicked_object);
   class=class="str">"cmt">//--- 检查组合框按钮上鼠标左键被按下
   class="type">void                CheckPressedOverButton(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 按下组合框的按钮                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CComboBox::OnClickButton(class="kw">const class="type">class="kw">string clicked_object)
  {
class=class="str">"cmt">//--- 如果对象名称不同,退出  
   if(clicked_object!=m_button.Name())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 改变列表视图的状态
   ChangeComboboxListState();
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查是否在按钮上按下了鼠标左键                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CComboBox::CheckPressedOverButton(class="type">void)
  {
class=class="str">"cmt">//--- 如果表单被阻挡并且标识符不匹配,退出
   if(m_wnd.IsLocked() && m_wnd.IdActivatedElement()!=CElement::Id())
      class="kw">return;
class=class="str">"cmt">//--- 如果没有焦点
   if(!CElement::MouseFocus())
     {
      class=class="str">"cmt">//--- 如果列表视图上没有焦点或者启用了滚动条,退出
      if(m_listview.MouseFocus() || m_listview.ScrollState())
         class="kw">return;
      class=class="str">"cmt">//--- 隐藏列表视图
      m_listview.Hide();
      class=class="str">"cmt">//--- 恢复颜色
      ResetColors();
      class=class="str">"cmt">//--- 如果标识符匹配并且元件不是下拉的
      if(m_wnd.IdActivatedElement()==CElement::Id() && !CElement::IsDropdown())
         class=class="str">"cmt">//--- 解除表单的阻挡
         m_wnd.IsLocked(class="kw">false);
     }
class=class="str">"cmt">//--- 如果有焦点
   else
     {
      class=class="str">"cmt">//--- 如果列表视图可见,退出
      if(m_listview.IsVisible())
         class="kw">return;
      class=class="str">"cmt">//--- 根据焦点设置颜色
      if(m_button.MouseFocus())
         m_button.BackColor(m_button_color_pressed);
      else
         m_button.BackColor(m_button_color_hover);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

组合框怎么吞掉图表鼠标事件

在 MT5 自定义控件里,CComboBox 靠重载 OnEvent 把图表事件拦截成自身逻辑。左键点击优先交给 OnClickButton,若命中下拉按钮直接 return,不再向上透传,这能避免图表其他脚本误判点击事件。 鼠标移动分支里先用 CElement::IsVisible() 判断隐藏状态,隐藏就 return,省掉后续坐标运算。可见时才取 lparam、dparam 强转成 x、y 整数坐标,再做四边界命中测试:x 在 X() 与 X2() 之间且 y 在 Y() 与 Y2() 之间才置 MouseFocus。 m_combobox_state 为 false 代表列表未展开,直接 return;sparam=="0" 表示左键已松开也 return。两者皆否才调 CheckPressedOverButton() 处理按钮区按下。 标准库里组合框选项选中事件被定义为 ON_CLICK_COMBOBOX_ITEM (17),即列表视图内选项被点选时向父窗口抛出的事件 ID。开 MT5 拖一个 CComboBox 实例,打印 id==17 的触发时机,就能验证上面这套事件吞咽顺序。

MQL5 / C++
class="type">void CComboBox::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">//--- 按下组合框按钮
      if(OnClickButton(sparam))
         class="kw">return;
     }
  }
class="type">void CComboBox::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_MOUSE_MOVE)
     {
class=class="str">"cmt">//--- 如果元件隐藏,退出
      if(!CElement::IsVisible())
         class="kw">return;
class=class="str">"cmt">//--- 坐标
      class="type">int  x=(class="type">int)lparam;
      class="type">int  y=(class="type">int)dparam;
class=class="str">"cmt">//--- 检查元件上的焦点
      CElement::MouseFocus(x>CElement::X() && x<CElement::X2() &&
                           y>CElement::Y() && y<CElement::Y2());
      m_button.MouseFocus(x>m_button.X() && x<m_button.X2() &&
                          y>m_button.Y() && y<m_button.Y2());
class=class="str">"cmt">//--- 如果元件被阻挡,退出
      if(!m_combobox_state)
         class="kw">return;
class=class="str">"cmt">//--- 如果鼠标左键松开,退出
      if(sparam=="class="num">0")
         class="kw">return;
class=class="str">"cmt">//--- 检查在分割按钮上按下鼠标左键
      CheckPressedOverButton();
      class="kw">return;
     }
  }
class="macro">#define ON_CLICK_COMBOBOX_ITEM(class="num">17) class=class="str">"cmt">// 在组合框列表视图中选择一个项目

◍ 下拉框点选与图表变更的两路事件处理

MT5 自定义控件里,CComboBox 要靠重写 OnEvent 才能把用户操作接住。下面这段逻辑分两条路:一条管列表项点击,一条管图表属性变化,二者都直接改控件内部状态,不绕弯。 列表项被按下时,事件号是 CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM。先比 lparam 和自身 Id(),对上了才把选中项的文字写进 m_button_text,并同步到按钮的 Description()。随后翻转下拉列表的显隐状态,再用 EventChartCustom 往外抛 ON_CLICK_COMBOBOX_ITEM,附带 Id 和 m_label_text——这一步是给父窗口或其他控件留回调入口。 另一路是 CHARTEVENT_CHART_CHANGE。若 m_combobox_state 为 false 直接 return,相当于控件被禁用时不响应。否则隐藏列表视图、ResetColors() 恢复配色、把所在窗口 IsLocked(false) 解锁,并将激活元素置为 WRONG_VALUE,避免残留焦点。 逐行拆这段源码,重点看三处高亮:条件判断锁定事件来源、按钮文字与列表选中项的双向绑定、以及自定义事件的发出参数顺序(图表ID、事件码、元素ID、0、文本)。复制进你的 EA 面板类里,把 ON_CLICK_COMBOBOX_ITEM 接上自己的处理逻辑,就能在实盘图表上验证下拉交互是否跟手。外汇与贵金属图表加载此控件存在重绘开销,高频拖拽可能卡顿,属高风险环境下的界面取舍。

MQL5 / C++
class="type">void CComboBox::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_CUSTOM+ON_CLICK_LIST_ITEM)
     {
      class=class="str">"cmt">//--- 如果标识符匹配
      if(lparam==CElement::Id())
        {
         class=class="str">"cmt">//--- 保存或者设置按钮的文字
         m_button_text=m_listview.SelectedItemText();
         m_button.Description(m_listview.SelectedItemText());
         class=class="str">"cmt">//--- 改变列表视图的状态
         ChangeComboBoxListState();
         class=class="str">"cmt">//--- 发送相关消息
         ::EventChartCustom(m_chart_id,ON_CLICK_COMBOBOX_ITEM,CElement::Id(),class="num">0,m_label_text);
        }
      class=class="str">"cmt">//---
      class="kw">return;
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理函数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CComboBox::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_CHART_CHANGE)
     {
      class=class="str">"cmt">//--- 如果元件被阻挡,退出
      if(!m_combobox_state)
        class="kw">return;
      class=class="str">"cmt">//--- 隐藏列表视图
      m_listview.Hide();
      class=class="str">"cmt">//--- 恢复颜色
      ResetColors();
      class=class="str">"cmt">//--- 解除表单的屏蔽
      m_wnd.IsLocked(class="kw">false);
      m_wnd.IdActivatedElement(WRONG_VALUE);
      class="kw">return;
     }
  }

「把组合框控件挂进窗口容器引擎」

要让一个控件被库引擎接管,核心动作就四步:先在 WndContainer.mqh 里 include 控件头文件;若控件内部还嵌了子对象(如下拉列表视图),就给它建一个私有数组加一个返回数组大小的公开方法;再写一个私有方法把子对象指针塞进私有数组,同时把列表视图及其滚动条也丢进通用对象数组;最后在主流程 AddToElementsArray() 里补一句调用,否则新控件不会被遍历到。 组合框的展开层是悬在表单之上的,若它贴着表单下沿,下拉后可能溢出边界。此时必须全程监控鼠标位置,并在左键按下下拉项的瞬间禁止图表滚动,否则图表会跟着滚轮跑。CWndEvents::SetChartState() 原本就管这事,现在要扩一段逻辑去识别下拉列表视图的存在。 下面这段是组合框接入时的关键骨架,黄色高亮部分是下拉列表视图专属的私有数组与处理方法,绿色是取数组长度的接口。注意 DropListsTotal() 里做了越界保护:window_index 超出 m_wnd 尺寸就打印 PREVENTING_OUT_OF_RANGE 并返回 WRONG_VALUE(通常定义为 -1),这是调试时最直接的报错信号。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 WndContainer.mqh |
class=class="str">"cmt">//|                                            Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                                     [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "ComboBox.mqh"
class CWndContainer
  {
class="kw">protected:
   class=class="str">"cmt">//--- 元件数组结构
   class="kw">struct WindowElements
     {
      class=class="str">"cmt">//--- 所有对象的通用数组
      class=class="str">"cmt">//--- 所有元件的通用数组
      class=class="str">"cmt">//--- 元件的私有数组:
      class=class="str">"cmt">//   上下文菜单数组
      class=class="str">"cmt">//--- 主菜单数组
      class=class="str">"cmt">//--- 工具提示
      class=class="str">"cmt">//--- 不同类型的下拉列表视图数组
      CElement        *m_drop_lists[];
     };
   class=class="str">"cmt">//--- 每个窗口的元件数组
   WindowElements   m_wnd[];
   class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 下拉列表视图的数量
   class="type">int              DropListsTotal(class="kw">const class="type">int window_index);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据指定的窗口索引返回下拉列表视图的数量                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CWndContainer::DropListsTotal(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_drop_lists));
  }
class CWndContainer
  {
class="kw">private:
   class=class="str">"cmt">//--- 把上下文菜单元件指针保存到库中
   class=class="str">"cmt">//--- 把主菜单元件指针保存到库中
   class=class="str">"cmt">//--- 把分割按钮元件指针保存到库中
   class=class="str">"cmt">//--- 把工具提示元件指针保存到库中
   class=class="str">"cmt">//--- 把列表视图对象指针保存到库中
   class=class="str">"cmt">//--- 把下拉列表视图元件指针保存到库中
   class="type">bool            AddComboBoxElements(class="kw">const class="type">int window_index,CElement &object);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 把下拉列表视图元件指针保存到私有数组中                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWndContainer::AddComboBoxElements(class="kw">const class="type">int window_index,CElement &object)
  {
class=class="str">"cmt">//--- 如果不是组合框,退出
   if(object.ClassName()!="CComboBox")

组合框子控件如何挂进容器库

在自定义 GUI 框架里,组合框(CComboBox)不是孤立对象。它的下拉列表和垂直滚动条必须作为独立元件登记进窗口的 m_elements 数组,否则事件分发和重绘会漏掉这两块。 AddComboBoxElements() 里先通过 GetPointer(object) 拿回组合框实例,再用一个 i<2 的循环处理两个子件:i==0 时取列表视图指针 lv,塞进 m_elements 并额外写入 m_drop_lists 私有数组;i==1 时取垂直滚动条 sv,只进 m_elements。循环每次都先 ArraySize 再 ArrayResize(size+1),保证数组只扩不溢。 如果这段返回 true,说明组合框的子元件已全部入库;返回 false 则代表 object 根本不是组合框,调用方会继续走普通元件流程。 SetChartState() 展示了入库后的实际收益:它遍历 WindowsTotal() 个窗口,对可见表单调 OnEvent(),一旦某窗口 MouseFocus() 为真就置 condition=true 并 break。下拉列表和滚动条因为已在库内,才能跟着父窗口正确响应鼠标。 开 MT5 自建一个带组合框的面板,在 AddToElementsArray 里断点看 m_elements 长度——挂成功后 size 会比挂之前多 2,这是验证子控件入库最直接的数。

MQL5 / C++
class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 取得组合框指针
  CComboBox *cb=::GetPointer(object);
class=class="str">"cmt">//---
  for(class="type">int i=class="num">0; i<class="num">2; 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">//--- 把列表加到库中
      if(i==class="num">0)
        {
         CListView *lv=cb.GetListViewPointer();
         m_wnd[window_index].m_elements[size]=lv;
         AddToObjectsArray(window_index,lv);
         class=class="str">"cmt">//--- 把指针加到私有数组中
         AddToRefArray(lv,m_wnd[window_index].m_drop_lists);
        }
      class=class="str">"cmt">//--- 把滚动条加到库中
      else if(i==class="num">1)
        {
         CScrollV *sv=cb.GetScrollVPointer();
         m_wnd[window_index].m_elements[size]=sv;
         AddToObjectsArray(window_index,sv);
        }
    }
class=class="str">"cmt">//---
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 把指针加到元件数组中                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndContainer::AddToElementsArray(class="kw">const class="type">int window_index,CElement &object)
  {
class=class="str">"cmt">//--- 如果库中没有包含控件表单
class=class="str">"cmt">//--- 如果请求来自不存在的表单
class=class="str">"cmt">//--- 加到通用元件数组中
class=class="str">"cmt">//--- 把元件对象加到通用对象数组中
class=class="str">"cmt">//--- 保存所有表单中最后一个元件的id
class=class="str">"cmt">//--- 增加元件id计数器
class=class="str">"cmt">//--- 在库中保存上下文菜单对象的指针
class=class="str">"cmt">//--- 在库中保存主菜单对象的指针
class=class="str">"cmt">//--- 在库中保存分割按钮对象的指针
class=class="str">"cmt">//--- 在库中保存工具提示对象的指针
class=class="str">"cmt">//--- 在库中保存列表视图对象的指针
class=class="str">"cmt">//--- 在库中保存组合框控件对象的指针
  if(AddComboBoxElements(window_index,object))
      class="kw">return;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置图表状态                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::SetChartState(class="type">void)
  {
  class="type">int awi=m_active_window_index;
class=class="str">"cmt">//--- 当禁止管理时识别事件
  class="type">bool condition=class="kw">false;
class=class="str">"cmt">//--- 检查窗口
  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">//--- 检查下拉列表视图

◍ 下拉列表与右键菜单的焦点拦截

在自定义 GUI 的图表事件循环里,若不下探子控件焦点状态,图表本身会误判用户仍在操作主图,导致自定义面板和原生图表抢事件。下面这段逻辑就是专门在每帧先排查下拉列表与上下文菜单是否持有鼠标焦点。 先通过 CWndContainer::DropListsTotal(awi) 拿到当前活动窗口下的下拉列表视图总数,再逐个取 CListView 指针。只要某个列表可见且自身 MouseFocus() 为真,或滚动条 ScrollState() 被触发,就把 condition 置 true 并 break,省去后续空转。 下拉列表查完还没命中,就轮到上下文菜单:ContextMenusTotal(awi) 取总数,遍历 m_context_menus[i].MouseFocus(),任一为真同样置 condition 并跳出。这两道排查全空,condition 保持 false。 最后无论 condition 结果如何,都用 for 循环对 windows_total 个表单调用 CustomEventChartState(condition),把「是否正被子控件占用」广播出去。实盘里若发现拖拽面板时 K 线仍在缩放,八成是这里漏了 ScrollState() 判断——MT5 里滚动条拖动不会触发 MouseFocus()。

MQL5 / C++
  if(!condition)
    {
      class=class="str">"cmt">//--- 取得下拉列表视图总数
      class="type">int drop_lists_total=CWndContainer::DropListsTotal(awi);
      for(class="type">int i=class="num">0;i<drop_lists_total; i++)
        {
         class=class="str">"cmt">//--- 取得下拉列表视图指针
         CListView *lv=m_wnd[awi].m_drop_lists[i];
         class=class="str">"cmt">//--- 如果列表视图被激活 (可见)
         if(lv.IsVisible())
           {
            class=class="str">"cmt">//--- 检查列表视图上的焦点以及滚动条的状态
            if(m_wnd[awi].m_drop_lists[i].MouseFocus() || lv.ScrollState())
              {
               condition=true;
               class="kw">break;
              }
           }
        }
    }
class=class="str">"cmt">//--- 检查上下文菜单的焦点
  if(!condition)
    {
      class=class="str">"cmt">//--- 检查下拉上下文菜单总数
      class="type">int context_menus_total=CWndContainer::ContextMenusTotal(awi);
      for(class="type">int i=class="num">0;i<context_menus_total; i++)
        {
         class=class="str">"cmt">//--- 如果上下文菜单上有焦点
         if(m_wnd[awi].m_context_menus[i].MouseFocus())
           {
            condition=true;
            class="kw">break;
           }
        }
    }
class=class="str">"cmt">//--- 设置所有表单的图表状态
  for(class="type">int i=class="num">0;i<windows_total; i++)
     m_windows[i].CustomEventChartState(condition);
  }

「在面板里塞四个组合框看越界表现」

保留之前的三个列表视图,再往自定义面板左上角塞四个 CComboBox,其中两个专门用来观察下拉列表视图盖住下方静态列表时的渲染冲突。另两个放到表单边界之外,用来验证 CWndEvents::SetChartState() 在控件超出窗口时的行为——外汇和贵金属图表上做这种 UI 越界测试属于纯技术验证,账户仍面临高杠杆高风险。 组合框实例在 CProgram 里通过基类就能直接用。每个框写个独立方法设距表单左上角的偏移,四个方法逻辑几乎一样,差别只在用户属性。以第四个框为例,创建完直接调屏蔽方法,步骤依次是:存表单指针、算坐标、初始化文字数组、设属性(多数走默认)、写列表项、配滚动条、高亮第 0 项、创建控件、按需屏蔽、把指针回传基类。 顺序可以调,但有三步不能乱:控件类里必须先挂表单指针,否则 GUI 构建直接中断,日志会报原因;然后创建控件;最后把指针存进对象库。这些方法要在 CProgram::CreateTradePanel() 主方法里调用。 事件处理里加 ON_CLICK_COMBOBOX_ITEM 分支:收到第三个框的消息时,若选了非第 0 项的条目,第四个框解除屏蔽;选回第 0 项则重新屏蔽。编译挂到图表上,能看到下拉视图溢出表单、且框间联动如预期。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于创建应用程序的类                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
  class=class="str">"cmt">//--- 组合框
  CComboBox            m_combobox1;
  CComboBox            m_combobox2;
  CComboBox            m_combobox3;
  CComboBox            m_combobox4;
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 组合框 class="num">1
class="macro">#define COMBOBOX1_GAP_X(class="num">7)
class="macro">#define COMBOBOX1_GAP_Y(class="num">50)
  class="type">bool                 CreateComboBox1(class="kw">const class="type">class="kw">string text);
  class=class="str">"cmt">//--- 组合框 class="num">2
class="macro">#define COMBOBOX2_GAP_X(class="num">160)
class="macro">#define COMBOBOX2_GAP_Y(class="num">50)
  class="type">bool                 CreateComboBox2(class="kw">const class="type">class="kw">string text);
  class=class="str">"cmt">//--- 组合框 class="num">3
class="macro">#define COMBOBOX3_GAP_X(class="num">7)
class="macro">#define COMBOBOX3_GAP_Y(class="num">202)
  class="type">bool                 CreateComboBox3(class="kw">const class="type">class="kw">string text);
  class=class="str">"cmt">//--- 组合框 class="num">4
class="macro">#define COMBOBOX4_GAP_X(class="num">160)
class="macro">#define COMBOBOX4_GAP_Y(class="num">202)
  class="type">bool                 CreateComboBox4(class="kw">const class="type">class="kw">string text);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建组合框 class="num">1                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateComboBox1(class="kw">const class="type">class="kw">string text)
  {
class=class="str">"cmt">//--- 列表视图项目总数
class="macro">#define ITEMS_TOTAL1 class="num">8
class=class="str">"cmt">//--- 传入表单对象
  m_combobox1.WindowPointer(m_window1);
class=class="str">"cmt">//--- 坐标
  class="type">int x=m_window1.X()+COMBOBOX1_GAP_X;
  class="type">int y=m_window1.Y()+COMBOBOX1_GAP_Y;
class=class="str">"cmt">//--- 列表视图中项目内容的数组
  class="type">class="kw">string items_text[ITEMS_TOTAL1]={"FALSE","item class="num">1","item class="num">2","item class="num">3","item class="num">4","item class="num">5","item class="num">6","item class="num">7"};
class=class="str">"cmt">//--- 在创建之前设置属性
  m_combobox1.XSize(class="num">140);
  m_combobox1.YSize(class="num">18);
  m_combobox1.LabelText(text);
  m_combobox1.ButtonXSize(class="num">70);
  m_combobox1.AreaColor(clrWhiteSmoke);

组合框视觉属性与面板装配的细节

在 MT5 自定义控件库里,组合框的下拉按钮和标签配色完全可以分离控制。上面这段把标签常态设为 clrBlack,悬停转 clrCornflowerBlue;按钮背景用 C'206,206,206' 灰底,悬停切到 C'193,218,255' 浅蓝,边框也分常态与失焦两色,视觉反馈层级很清楚。 下拉列表本身通过 GetListViewPointer() 拿到指针后再调 LightsHover(true),开启行悬停高亮;若当前没选中项(SelectedItemIndex 返回 WRONG_VALUE),默认落到第 2 项索引——注意索引从 0 起算,实际是列表第三项被预选。VisibleItemsTotal(5) 限制一次展开只露 5 行,超出部分靠滚动。 CreateTradePanel() 里连续调了四个 CreateComboBoxX,任一返回 false 就整体中止,最后 m_chart.Redraw() 才把图层推到终端。事件侧用 CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM 拦截选项点击,按 LabelText() 比对来源控件再 Print 出 id/lparam/dparam/sparam,方便你接自己的逻辑。 开 MT5 新建 EA 把这几段拼进 CProgram 类,改 ITEMS_TOTAL1 和 items_text 数组,就能直接看下拉交互效果。外汇与贵金属插件开发涉及实时事件,回测与实盘表现可能偏离,属高风险验证。

MQL5 / C++
  m_combobox1.LabelColor(clrBlack);
  m_combobox1.LabelColorHover(clrCornflowerBlue);
  m_combobox1.ButtonBackColor(C&class="macro">#x27;class="num">206,class="num">206,class="num">206&class="macro">#x27;);
  m_combobox1.ButtonBackColorHover(C&class="macro">#x27;class="num">193,class="num">218,class="num">255&class="macro">#x27;);
  m_combobox1.ButtonBorderColor(C&class="macro">#x27;class="num">150,class="num">170,class="num">180&class="macro">#x27;);
  m_combobox1.ButtonBorderColorOff(C&class="macro">#x27;class="num">178,class="num">195,class="num">207&class="macro">#x27;);
  m_combobox1.ItemsTotal(ITEMS_TOTAL1);
  m_combobox1.VisibleItemsTotal(class="num">5);
class=class="str">"cmt">//--- 在组合框列表视图中保存项目内容
  for(class="type">int i=class="num">0;i<ITEMS_TOTAL1;i++)
    m_combobox1.ValueToList(i,items_text[i]);
class=class="str">"cmt">//--- 取得列表视图指针
  CListView *lv=m_combobox1.GetListViewPointer();
class=class="str">"cmt">//--- 设置列表视图属性
  lv.LightsHover(true);
  lv.SelectedItemByIndex(lv.SelectedItemIndex()==WRONG_VALUE ?class="num">2 : lv.SelectedItemIndex());
class=class="str">"cmt">//--- 创建控件
  if(!m_combobox1.CreateComboBox(m_chart_id,m_subwin,x,y))
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 把对象加到对象组的通用数组中
  CWndContainer::AddToElementsArray(class="num">0,m_combobox1);
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建交易面板                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateTradePanel(class="type">void)
  {
class=class="str">"cmt">//--- 创建控件表单1
class=class="str">"cmt">//--- 创建控件:
class=class="str">"cmt">//    主菜单
class=class="str">"cmt">//--- 上下文菜单
class=class="str">"cmt">//--- 创建状态栏
class=class="str">"cmt">//--- 组合框
  if(!CreateComboBox1("Combobox class="num">1:"))
    class="kw">return(class="kw">false);
  if(!CreateComboBox2("Combobox class="num">2:"))
    class="kw">return(class="kw">false);
  if(!CreateComboBox3("Combobox class="num">3:"))
    class="kw">return(class="kw">false);
  if(!CreateComboBox4("Combobox class="num">4:"))
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 列表视图
class=class="str">"cmt">//--- 重绘图表
  m_chart.Redraw();
  class="kw">return(true);
  }
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)
  {
class=class="str">"cmt">//--- 选择了组合框项目的事件
  if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM)
    {
    if(sparam==m_combobox1.LabelText())
      ::Print(__FUNCTION__," > This message is from combobox class="num">1 > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
    else if(sparam==m_combobox2.LabelText())
      ::Print(__FUNCTION__," > This message is from combobox class="num">2 > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
    class=class="str">"cmt">//--- 处理来自第三个组合框的事件
    else if(sparam==m_combobox3.LabelText())
      {

◍ 用组合框联动锁死无效参数

在 MT5 自定义指标或 EA 的面板里,组合框之间可以做状态联动:当第三个组合框文本等于 "FALSE" 时,直接把第四个组合框置灰,避免用户继续选无意义配置。 这种写法依赖 m_combobox3.ButtonText() 的实时返回值,而不是缓存变量。若返回非 "FALSE",第四个组合框立刻恢复可用,逻辑对称、无残留状态。 外汇与贵金属行情跳变频繁,面板参数误触可能让策略在极端波动中发出错误信号,高风险环境下用状态锁减少人为干扰值得做。

MQL5 / C++
  ::Print(__FUNCTION__," > This message is from combobox class="num">3 > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
  class=class="str">"cmt">//--- 如果选择了指定的值,禁用组合框 class="num">4
  if(m_combobox3.ButtonText()=="FALSE")
     m_combobox4.ComboBoxState(class="kw">false);
  class=class="str">"cmt">//--- 如果选择了其他值,启用组合框 class="num">4
  else
     m_combobox4.ComboBoxState(true);
  }
  else if(sparam==m_combobox4.LabelText())
   ::Print(__FUNCTION__," > This message is from combobox class="num">4 > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
  }
}

「画得少,看得清」

这一阶段的图形界面库已经把组合框(ComboBox)收进结构里,当前库形态如图 3 所示,下一阶段才去写复选框和编辑框的组合类。想验证的人直接下第五部分压缩包:MQL4 版 196.34 KB,MQL5 版 196.3 KB,解压丢进 MT5 的 MQL5/Include 就能跑。 有个坑得提前说:有用户在 2019-11-22 反馈,CreateComboBox 第一个参数若传数组元素,界面会失交互或挂起;传普通声明变量则正常。所以抄代码时别图省事用 m_currency_box[0] 这种写法硬塞。 下面这段是从原文抠出来的创建逻辑,注意宏定义被立刻 undef 了,实际项目里别留这种废宏: //--- 列表中的项目总数 #define ITEMS_TOTAL1 8 #undef ITEMS_TOTAL1 bool CProgram::CreateComboBox(CComboBox &combobox,const int x_gap,const int y_gap) if(!CreateComboBox(m_currency_box_1,25,25)) return(false); if(!CreateComboBox(m_currency_box[0],25,25)) return(false); 外汇与贵金属 GUI 自动化高风险,库只是工具,参数错一处就可能让面板卡死。

MQL5 / C++
class=class="str">"cmt">//--- 列表中的项目总数
class="macro">#define ITEMS_TOTAL1 class="num">8
class="macro">#undef ITEMS_TOTAL1
class="type">bool CProgram::CreateComboBox(CComboBox &combobox,class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap)
    if(!CreateComboBox(m_currency_box_1,class="num">25,class="num">25))
        class="kw">return(class="kw">false);
    if(!CreateComboBox(m_currency_box[class="num">0],class="num">25,class="num">25))
        class="kw">return(class="kw">false);
交给小布盯盘看盘口
这些诊断与界面原型思路小布盯盘的 AIGC 已内置,打开对应品种页即可看到,把重复劳动交给小布,你专注决策。

常见问题

组合框需要自己控制下拉显隐与选中回写,公开列表视图会破坏封装边界,导致外部直接改数据而界面状态不同步。
选项超过四个或文字较长时,组合框用下拉收起冗余,界面紧凑;单选按钮会横向纵向都占空间,参数多时可读性下降。
目前小布内置的是诊断与模板示意,完整 mqh 类仍需按本文结构手写,但可借其输出减少重复样板。
常漏的是把组合框的自定义事件 ID 注册进引擎分发表,否则按钮点击不会触发列表显隐。
应用程序能模拟真实窗体生命周期与多控件共存,脚本环境往往缺表单上下文,测不出边界事件。