图形界面III:简单与多功能按钮组(第二章)(基础篇)
📘

图形界面III:简单与多功能按钮组(第二章)(基础篇)

第 1/3 篇

◍ 按钮组类的三种基础形态

在 MT5 自定义图形界面开发里,按钮组是交互控件的第一道坎。原文把按钮组拆成三类:简单按钮组、单选按钮组、图标按钮组,各自对应不同的选取逻辑与渲染需求。 简单按钮组适合触发独立动作,单选按钮组强制组内互斥(类似指标周期切换),图标按钮组则用位图替代文字降低界面噪声。三类都封装成独立类,便于在 EA 或指标面板里直接实例化。 该系列示例文发布于 2016-04-25,历史浏览量 3477、评论 8 条,说明这类底层控件封装长期有实操需求。开 MT5 用 MetaEditor 新建类继承 Control 基类,先跑通简单按钮组再扩展单选与图标,是验证这套结构的最低成本路径。

「库的章节脉络与按钮组定位」

这套界面库按章节递进,每一章末尾都附了当阶段完整版压缩包与文章索引,解压后目录层级必须和归档里完全一致,否则 MT5 里 include 路径会报找不到文件。 系列第三部分首篇只做了基础按钮和通用按钮两种控件,能点、能回调,但彼此独立。本篇往前推一步,做的是互斥关联的按钮组——同一组里只能选中一个,典型用途是周期切换、信号源单选这类单选项场景。 从验证角度,你可以直接下载第三部分第二章的库文件,拖进 MQL5/Include 对应子目录,新建一个 EA 挂 EURUSD 图表,调用 CButtonGroup 派生类加三个按钮,跑起来点一下就能看到高亮态自动排他。外汇与贵金属品种波动剧烈,这类界面只解决交互,不涉及任何方向判断。

用 CButton 派生做互斥按钮组

简单按钮组本质是 OBJ_BUTTON 图形对象的数组,核心约束是组内同一时刻只能有一个处于按下态。相比基于 CSimpleButton 再包一层指针库,直接继承 CButton 更省事——不必为每个控件单独写取指针的方法,所以我们选这条路线,在 Controls 目录下建 ButtonsGroup.mqh 并让 WndContainer.mqh 包含它。 组内属性分两类:通用项含高度、禁用色、边框色(可用/禁用两态)、文字色(多态)、左键点击优先级;独项含按下/抬起状态、到表单边界的 XY 间距、文字、宽度、各态颜色与渐变色。每个按钮的边界距离独立存储,意味着你能任意排布顺序,不必拘泥水平或垂直。 动态数组 m_buttons[] 在构造时大小未定,每次调 AddButton() 才扩一个元素并填参。若建组前没加过任何按钮,CreateButtons() 会中止界面创建并在日志报错;按钮在循环里逐个挂到图表。下面这段是类声明的精简骨架,能看到独特属性全拆成了平行数组。 单选模式由类内一个布尔栏位控制,默认 true 表示组内恒有一个按下,设 false 则允许全抬起。切换状态时先查数组越界:无按钮就打日志但不退出,继续跑迟早撞上 m_buttons_state[] 越界,所以开发者必须至少 AddButton 一次。SelectionButton() 里按模式把其余按钮复位颜色,末尾把高亮文字与索引存进栏位,非按下态写空串与 WRONG_VALUE。 事件侧靠 OnClickButton() 做名称、ID、可用态三重检查,ID 用 IdFromObjectName() 从对象名解出来;通过后转交 SelectionButton() 并往事件流推一条带按钮数据的消息,自定义类里能接。鼠标掠过与左键按下的回应放在 CheckPressedOverButton(),它在 CHARTEVENT_MOUSE_MOVE 里被调,前置可见/可用/表单可用/左键按下四个判断。 实测时把测试 EA 里除主菜单外的控件清掉,建一个 CButtonsGroup 实例并设好边距。下面代码里水平排了四个按钮,两红两蓝;想要初始就高亮某一个,直接调公有的 SelectionButton() 即可。编译载图后界面应出现如图的互斥组。外汇与贵金属图表上挂这类 EA 属高风险操作,参数误用可能导致界面逻辑异常。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于创建简单按钮组的类                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CButtonsGroup : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 用于创建按钮的对象
   CButton            m_buttons[];
   class=class="str">"cmt">//--- 按钮渐变色
   class="kw">struct ButtonsGradients
     {
      class="type">class="kw">color           m_buttons_color_array[];
     };
   ButtonsGradients   m_buttons_total[];
   class=class="str">"cmt">//--- 按钮属性:
   class=class="str">"cmt">//    按钮独特属性的数组
   class="type">bool               m_buttons_state[];
   class="type">int                m_buttons_x_gap[];
   class="type">int                m_buttons_y_gap[];
   class="type">class="kw">string             m_buttons_text[];
   class="type">int                m_buttons_width[];
   class="type">class="kw">color              m_buttons_color[];
   class="type">class="kw">color              m_buttons_color_hover[];
   class="type">class="kw">color              m_buttons_color_pressed[];
   class=class="str">"cmt">//--- 按钮高度
   class="type">int                m_button_y_size;
   class=class="str">"cmt">//--- 被屏蔽禁用按钮的颜色
   class="type">class="kw">color              m_back_color_off;
   class=class="str">"cmt">//--- 激活和屏蔽模式下的边框颜色
   class="type">class="kw">color              m_border_color;
   class="type">class="kw">color              m_border_color_off;
   class=class="str">"cmt">//--- 文字颜色
   class="type">class="kw">color              m_text_color;
   class="type">class="kw">color              m_text_color_off;

◍ 按钮组的属性接口与添加逻辑

在 MT5 自定义面板开发里,按钮组类先把可配置项全收进私有成员,再用一组 setter 暴露给外部。比如 m_button_y_size 控制按钮高度,m_back_color_off 管被禁用时的背景色,m_text_color_pressed 则是鼠标按下去那一瞬的文字色,这些成员赋值后才会真正进绘图队列。 ButtonsTotal() 直接返回 m_buttons 数组长度,也就是当前组里按钮的个数;它用 ArraySize 取尺寸而非单独计数,意味着你每调一次 AddButton 往数组塞对象,总数自动跟着变,不用手动维护计数器。 AddButton 的入参已经把布局与配色拆开:x_gap / y_gap 是相对偏移,text 是标签,width 定宽,后面三个 color 分别管常态、悬停、按下的按钮底色。实际写 EA 界面时,先设好 ButtonYSize、BackColorOff 这些属性,再循环调 AddButton,按钮才会按你预期的间距和禁用语义渲染出来。 外汇与贵金属图表上挂这类面板属于高频交互组件,MT5 端逻辑错误可能让按钮不响应或重叠,实盘前务必在策略测试器外的普通图表手动点一遍验证。

MQL5 / C++
class="type">class="kw">color                m_text_color_pressed;
class=class="str">"cmt">//--- 鼠标左键点击优先级
class="type">int                 m_buttons_zorder;
class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 按钮数量
  class="type">int               ButtonsTotal(class="type">void)                    class="kw">const { class="kw">return(::ArraySize(m_buttons));    }
  class=class="str">"cmt">//--- (class="num">1) 按钮的高度
  class="type">void              ButtonYSize(class="kw">const class="type">int y_size)                { m_button_y_size=y_size;             }
  class=class="str">"cmt">//--- (class="num">1) 被屏蔽按钮的背景色以及边框色 ((class="num">2) 可用/(class="num">3) 被屏蔽禁用)
  class="type">void              BackColorOff(class="kw">const class="type">class="kw">color clr)                { m_back_color_off=clr;               }
  class="type">void              BorderColor(class="kw">const class="type">class="kw">color clr)                 { m_border_color=clr;                 }
  class="type">void              BorderColorOff(class="kw">const class="type">class="kw">color clr)              { m_border_color_off=clr;             }
  class=class="str">"cmt">//--- 文字颜色
  class="type">void              TextColor(class="kw">const class="type">class="kw">color clr)                   { m_text_color=clr;                   }
  class="type">void              TextColorOff(class="kw">const class="type">class="kw">color clr)                { m_text_color_off=clr;               }
  class="type">void              TextColorPressed(class="kw">const class="type">class="kw">color clr)            { m_text_color_pressed=clr;           }
};
class CButtonsGroup : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 在创建按钮前设置指定的属性
  class="type">void              AddButton(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap,class="kw">const class="type">class="kw">string text,class="kw">const class="type">int width,
                   class="kw">const class="type">class="kw">color button_color,class="kw">const class="type">class="kw">color button_color_hover,class="kw">const class="type">class="kw">color button_color_pressed);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 添加一个按钮                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CButtonsGroup::AddButton(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap,class="kw">const class="type">class="kw">string text,class="kw">const class="type">int width,

「按钮组动态扩容与空组拦截」

在 MT5 自定义面板里维护一组按钮,核心动作是向多个并行数组追加一项。AddButton 内部先取当前 m_buttons 大小,再对 11 个相关数组统一 ArrayResize 到 array_size+1,避免只扩主数组而漏掉颜色或状态数组导致越界。 扩容后把传入的 x_gap、y_gap、text、width 以及三种 color 按索引落库,并把 m_buttons_state 置为 false,表示这个新按钮初始未被按下。这样每加一个按钮,结构体保持一致,后续循环创建时可以直接按索引读参。 CreateButtons 开头先取 ButtonsTotal(),若小于 1 就 Print 报错并返回 false。实测若忘了先 AddButton 直接调创建,日志会输出“一个组至少要包含一个按钮”,按钮组不会生成——这是写 GUI 时最容易漏的一步。 按钮数组的扩容成本随数量线性增长,若计划挂 50 个以上按钮,建议在初始化时一次性 ArrayResize 到预估上限,减少运行时反复分配。外汇与贵金属图表加载此类 EA 控件仍属高风险环境,参数异常可能引发图表卡顿。

MQL5 / C++
class="kw">const class="type">class="kw">color button_color,class="kw">const class="type">class="kw">color button_color_hover,class="kw">const class="type">class="kw">color pressed_button_color)
  {
class=class="str">"cmt">//--- 把数组大小增加一个元件
   class="type">int array_size=::ArraySize(m_buttons);
   ::ArrayResize(m_buttons,array_size+class="num">1);
   ::ArrayResize(m_buttons_total,array_size+class="num">1);
   ::ArrayResize(m_buttons_state,array_size+class="num">1);
   ::ArrayResize(m_buttons_x_gap,array_size+class="num">1);
   ::ArrayResize(m_buttons_y_gap,array_size+class="num">1);
   ::ArrayResize(m_buttons_text,array_size+class="num">1);
   ::ArrayResize(m_buttons_width,array_size+class="num">1);
   ::ArrayResize(m_buttons_color,array_size+class="num">1);
   ::ArrayResize(m_buttons_color_hover,array_size+class="num">1);
   ::ArrayResize(m_buttons_color_pressed,array_size+class="num">1);
class=class="str">"cmt">//--- 保存传入参数的值
   m_buttons_x_gap[array_size]        =x_gap;
   m_buttons_y_gap[array_size]        =y_gap;
   m_buttons_text[array_size]         =text;
   m_buttons_width[array_size]        =width;
   m_buttons_color[array_size]        =button_color;
   m_buttons_color_hover[array_size] =button_color_hover;
   m_buttons_color_pressed[array_size] =pressed_button_color;
   m_buttons_state[array_size]        =class="kw">false;
   }
class CButtonsGroup : class="kw">public CElement
  {
class="kw">public:
   class=class="str">"cmt">//--- 创建按钮的方法
   class="type">bool             CreateButtonsGroup(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">int x,class="kw">const class="type">int y);
   class=class="str">"cmt">//---
class="kw">private:
   class="type">bool             CreateButtons(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建按钮                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CButtonsGroup::CreateButtons(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
   class="type">int l_x =m_x;
   class="type">int l_y =m_y;
class=class="str">"cmt">//--- 取得按钮数量
   class="type">int buttons_total=ButtonsTotal();
class=class="str">"cmt">//--- 如果组中没有按钮,报告
   if(buttons_total<class="num">1)
     {
      ::Print(__FUNCTION__," > 本方法将被调用 "
                           "一个组至少要包含一个按钮!使用 CButtonsGroup::AddButton() 方法");
      class="kw">return(class="kw">false);
     }
class=class="str">"cmt">//--- 创建指定数量的按钮
   for(class="type">int i=class="num">0; i<buttons_total; i++)
     {
      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">//--- 保存对象指针
     }
class=class="str">"cmt">//---
   class="kw">return(true);
   }
class CButtonsGroup : class="kw">public CElement

按钮组状态切换与选中追踪的实现细节

在 MT5 自定义面板里做按钮组,核心是两个类成员配合:CButtonsGroup 用 m_buttons_group_state 控制整组可用/禁用,用 m_selected_button_text 和 m_selected_button_index 记住当前高亮项。禁用整组时,循环把每个按钮的 State 置 false,并按 state 切换文字色、背景色、边框色——启用用 m_text_color / m_buttons_color[i] / m_border_color,禁用用对应的 _off 后缀变量。 重新启用时若之前有选中项(m_selected_button_index != WRONG_VALUE),代码会把该按钮状态恢复为 true,并套用按下态配色 m_text_color_pressed 与 m_buttons_color_pressed[index]。这意味着你的 EA 面板在暂停交易时段灰掉按钮,恢复后不会丢用户上次选的周期或品种。 SelectionButton(const int index) 负责按索引切换单选状态,配合 RadioButtonsMode 标记的互斥逻辑,能在 0~ButtonsTotal()-1 范围内只亮一个。开 MT5 把这段塞进你的 CElement 派生类,调一下 ButtonsGroupState(false) 再看颜色回写,就能验证禁用态是否如预期不残留高亮。外汇与贵金属界面自动化涉及实盘操作,灰化误用可能导致下单受阻,属高风险环节,验证请在模拟盘完成。

MQL5 / C++
{
class="kw">public:
 class=class="str">"cmt">//--- 单选按钮模式
 class="type">bool                m_radio_buttons_mode;
 class=class="str">"cmt">//---
class="kw">public:
 class=class="str">"cmt">//--- 设置单选按钮模式
 class="type">void                RadioButtonsMode(class="kw">const class="type">bool flag)                { m_radio_buttons_mode=flag;    }
};
class CButtonsGroup : class="kw">public CElement
 {
class="kw">private:
 class=class="str">"cmt">//--- 可用/禁用
 class="type">bool                m_buttons_group_state;
 class=class="str">"cmt">//---
class="kw">public:
 class=class="str">"cmt">//--- 按钮组的通用状态 (可用/禁用)
 class="type">bool                ButtonsGroupState(class="type">void)                          class="kw">const { class="kw">return(m_buttons_group_state);  }
 class="type">void                ButtonsGroupState(class="kw">const class="type">bool state);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 改变按钮状态                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CButtonsGroup::ButtonsGroupState(class="kw">const class="type">bool state)
 {
 m_buttons_group_state=state;
class=class="str">"cmt">//---
 class="type">int buttons_total=ButtonsTotal();
 for(class="type">int i=class="num">0; i<buttons_total; i++)
   {
    m_buttons[i].State(class="kw">false);
    m_buttons[i].Color((state)? m_text_color : m_text_color_off);
    m_buttons[i].BackColor((state)? m_buttons_color[i]: m_back_color_off);
    m_buttons[i].BorderColor((state)? m_border_color : m_border_color_off);
   }
class=class="str">"cmt">//--- 如果在禁用之前是按下的状态
 if(m_buttons_group_state)
   {
    if(m_selected_button_index!=WRONG_VALUE)
     {
      m_buttons_state[m_selected_button_index]=true;
      m_buttons[m_selected_button_index].Color(m_text_color_pressed);
      m_buttons[m_selected_button_index].BackColor(m_buttons_color_pressed[m_selected_button_index]);
     }
   }
}
class CButtonsGroup : class="kw">public CElement
 {
class="kw">private:
 class=class="str">"cmt">//--- 高亮按钮的(class="num">1) 文字 和 (class="num">2) 索引
 class="type">class="kw">string              m_selected_button_text;
 class="type">int                 m_selected_button_index;
 class=class="str">"cmt">//---
class="kw">public:
 class=class="str">"cmt">//--- 返回高亮按钮的 (class="num">1) 文字 和 (class="num">2) 索引
 class="type">class="kw">string              SelectedButtonText(class="type">void)                         class="kw">const { class="kw">return(m_selected_button_text);  }
 class="type">int                 SelectedButtonIndex(class="type">void)                        class="kw">const { class="kw">return(m_selected_button_index); }
 class=class="str">"cmt">//--- 根据指定的索引切换按钮的状态
 class="type">void                SelectionButton(class="kw">const class="type">int index);
};

常见问题

从基础按钮类派生一个组管理类,在点击回调里先清掉同组其他按钮的选中态,再置当前按钮为选中,即可实现互斥。
在添加按钮和状态切换前加数量判断,若组内数量为0直接返回并写日志,可避免空组调用导致的崩溃。
可以,把按钮组代码贴给小布,它能帮你跑通选中追踪与互斥逻辑,标出空组未拦截等隐患,省去手动逐行查。
扩容时把原选中索引暂存,新按钮追加后再按索引恢复选中态,并在添加接口里限制最大数量防溢出。
在组类里封装SetColor、SetFont接口,循环遍历组内按钮批量赋值,改一处即可全组生效,降低硬编码风险。