图形界面III:简单与多功能按钮组(第二章)·进阶篇
🔘

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

(2/3)· 还在为每个按钮手写状态互斥逻辑?一组控件只需一个类就能管住按下与禁用

案例拆解新手友好 第 2/3 篇
很多自建面板的交易者把按钮当成孤立对象堆在图表上,结果选了 A 仍要手动把 B 置灰。本篇把相互关联的按钮收进一个组类,让组内永远只有一个处于按下态。

◍ 按钮组状态切换的索引容错逻辑

在 MT5 自定义面板里做按钮组(CButtonsGroup)时,SelectionButton(index) 这个方法是核心交互入口。它不要求调用方保证 index 合法,而是内部先做越界收敛:index 大于等于按钮总数时取末位,小于 0 时取 0,其余原样使用。 代码里用 correct_index 承接修正后的值,再翻转 m_buttons_state[correct_index] 的布尔状态。随后遍历全部按钮,根据 m_radio_buttons_mode 决定是单选(只让 correct_index 按下)还是多选(仅该索引且状态为真才按下)。 单选模式下,循环内会把非选中项强制 m_buttons_state[i]=false 并恢复普通配色;多选模式下则保留各按钮独立状态。每次调用后,若检测到有按下按钮,就把选中文字和索引分别写进 m_selected_button_text 与 m_selected_button_index,否则索引落为 WRONG_VALUE。 若组里按钮数小于 1,方法不会崩,只是用 Print 提示至少先 AddButton()。在实盘面板里这属于低频边界,但写 EA 界面时最好提前断言,避免点选无响应还查不出原因。

MQL5 / C++
class="type">void CButtonsGroup::SelectionButton(class="kw">const class="type">int index)
  {
class=class="str">"cmt">//--- 检查组中按下的按钮
   class="type">bool check_pressed_button=class="kw">false;
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=class="str">"cmt">//--- 如果超出数组范围则调整索引值
   class="type">int correct_index=(index>=buttons_total)? buttons_total-class="num">1 : (index<class="num">0)? class="num">0 : index;
class=class="str">"cmt">//--- 把按钮状态调整相反状态
   m_buttons_state[correct_index]=(m_buttons_state[correct_index])? class="kw">false : true;
class=class="str">"cmt">//--- 在一组按钮中迭代
   for(class="type">int i=class="num">0; i<buttons_total; i++)
     {
      class=class="str">"cmt">//--- 根据模式做相关检查
      class="type">bool condition=(m_radio_buttons_mode)? (i==correct_index) : (i==correct_index && m_buttons_state[i]);
      class=class="str">"cmt">//--- 如果满足条件,使按钮按下
      if(condition)
        {
         if(m_radio_buttons_mode)
            m_buttons_state[i]=true;
         class=class="str">"cmt">//--- 有按下的按钮
         check_pressed_button=true;
         class=class="str">"cmt">//--- 设置颜色
         m_buttons[i].Color(m_text_color_pressed);
         m_buttons[i].BackColor(m_buttons_color_pressed[i]);
         CElement::InitColorArray(m_buttons_color_pressed[i],m_buttons_color_pressed[i],m_buttons_total[i].m_buttons_color_array);
        }
      class=class="str">"cmt">//--- 如果条件满足,使按钮抬起
      else
        {
         class=class="str">"cmt">//--- 设置禁用状态和颜色
         m_buttons_state[i]=class="kw">false;
         m_buttons[i].Color(m_text_color);
         m_buttons[i].BackColor(m_buttons_color[i]);
         CElement::InitColorArray(m_buttons_color[i],m_buttons_color_hover[i],m_buttons_total[i].m_buttons_color_array);
        }
      class=class="str">"cmt">//--- 把按钮正常状态清零
      m_buttons[i].State(class="kw">false);
     }
class=class="str">"cmt">//--- 如果有按下的按钮,保存它的文字和索引
   m_selected_button_text   =(check_pressed_button) ? m_buttons[correct_index].Description() : "";
   m_selected_button_index =(check_pressed_button) ? correct_index : WRONG_VALUE;
   }

按钮组怎么吞掉一次鼠标点击

在 MT5 自定义控件里,按钮组要响应图表上的左键点击,入口是 OnEvent。图表事件推过来时,先判 id 是否为 CHARTEVENT_OBJECT_CLICK,再交给 OnClickButton 用被点对象的名字去匹配,匹配不上直接 return,不污染其他控件逻辑。 OnClickButton 先用 StringFind 看 pressed_object 是否带 ProgramName()+"_buttons_" 前缀,不带就退出;随后用 IdFromObjectName 取 ID,与 CElement::Id() 不一致也退出。这两道过滤能保证只有本组按钮能触发后续动作。 组被禁用时(m_buttons_group_state 为 false),函数会把所有按钮 State(false) 再退出,界面上表现为整组不可点。若组可用,则遍历 ButtonsTotal() 个按钮,用 Name()==pressed_object 锁定 check_index,找不到就 WRONG_VALUE 退出。 命中后调用 SelectionButton(check_index) 切换选中态,并用 EventChartCustom 把 ON_CLICK_BUTTON、控件 ID、选中索引和文字推回图表。EA 或脚本侧挂个自定义事件监听,就能拿到用户点了哪个按钮——外汇与贵金属图表挂这类控件,仍属高风险环境,事件逻辑出错可能误发订单。 另有一私有方法 CheckPressedOverButton 专门处理鼠标悬停/按下的命中检测,配合 OnEvent 里的 CHARTEVENT_MOUSE_MOVE 分支做高亮,是按钮组交互完整性的另一半。

MQL5 / C++
class="type">void CButtonsGroup::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)
     {
      if(OnClickButton(sparam))
         class="kw">return;
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在组里按下按钮                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CButtonsGroup::OnClickButton(class="kw">const class="type">class="kw">string pressed_object)
  {
class=class="str">"cmt">//--- 如果按下的不是菜单项,就退出
   if(::StringFind(pressed_object,CElement::ProgramName()+"_buttons_",class="num">0)<class="num">0)
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 从对象名称中取得ID
   class="type">int id=IdFromObjectName(pressed_object);
class=class="str">"cmt">//--- 如果ID不匹配,就退出
   if(id!=CElement::Id())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 用于检查索引
   class="type">int check_index=WRONG_VALUE;
class=class="str">"cmt">//--- 检查是否按下了组内的一个按钮
   class="type">int buttons_total=ButtonsTotal();
class=class="str">"cmt">//--- 如果此按钮被禁用,就退出
   if(!m_buttons_group_state)
     {
      for(class="type">int i=class="num">0; i<buttons_total; i++)
         m_buttons[i].State(class="kw">false);
      class=class="str">"cmt">//---
      class="kw">return(class="kw">false);
     }
class=class="str">"cmt">//--- 如果按下了按钮,保存索引
   for(class="type">int i=class="num">0; i<buttons_total; i++)
     {
      if(m_buttons[i].Name()==pressed_object)
        {
         check_index=i;
         break;
        }
     }
class=class="str">"cmt">//--- 如果这个组的按钮没有被按下,就退出
   if(check_index==WRONG_VALUE)
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 切换按钮状态
   SelectionButton(check_index);
class=class="str">"cmt">//--- 发送相关信号
   ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElement::Id(),m_selected_button_index,m_selected_button_text);
   class="kw">return(true);
  }
class CButtonsGroup : class="kw">public CElement
  {
class="kw">private:
   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">void CButtonsGroup::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())

「按钮组的鼠标焦点与按下着色逻辑」

在 MT5 自定义面板里,按钮组对鼠标事件的响应要先过几道闸:组被禁用、所属窗口被锁定、或者左键状态字不是 "1",函数都会直接 return,不往下走。这种早退写法能避免在无交互状态下反复跑循环,省一点主线程开销。 焦点判定靠的是鼠标坐标是否落进每个按钮的 X~X2、Y~Y2 包围盒。代码里用 lparam、dparam 强转成 int 得到 x、y,再对 ButtonsTotal() 个按钮逐个调 MouseFocus() 写入状态。 真正上色发生在 CheckPressedOverButton():有焦点的按钮刷成 m_buttons_color_pressed[i],没焦点且未被按下的刷回 m_buttons_color[i]。下面这段是核心循环。 创建组时坐标用宏偏移:BUTTONS_GROUP1_GAP_X=7、BUTTONS_GROUP1_GAP_Y=50,按钮水平间距数组是 {0,72,144,216},也就是每个按钮占 72 像素宽。开 MT5 把这组值抄进你的面板类,就能直接复现四连按钮的排布。

MQL5 / C++
class="type">void CButtonsGroup::CheckPressedOverButton(class="type">void)
  {
  class="type">int buttons_total=ButtonsTotal();
class=class="str">"cmt">//--- 根据鼠标左键按下的位置设置颜色
  for(class="type">int i=class="num">0; i<buttons_total; i++)
    {
    class=class="str">"cmt">//--- 如果有焦点,就是按下按钮的颜色
    if(m_buttons[i].MouseFocus())
      m_buttons[i].BackColor(m_buttons_color_pressed[i]);
    class=class="str">"cmt">//--- 如果没有焦点,则...
    else
      {
      class=class="str">"cmt">//--- ...如果没有按下组按钮,设置背景颜色
      if(!m_buttons_state[i])
        m_buttons[i].BackColor(m_buttons_color[i]);
      }
    }
  }

◍ 用数组批量铺一组按钮并默认选中第二个

在 MT5 自定义指标或 EA 的面板里,一次性塞进四个同宽按钮,最省事的办法就是先备好几组平行数组。下面这段代码把文字、宽度、常态/悬停/按下三态颜色都按索引对齐:前两个按钮用暗红系(C'195,0,0' 常态、C'255,51,51' 悬停、C'135,0,0' 按下),后两个用皇家蓝系(clrRoyalBlue 常态、C'85,170,255' 悬停、C'50,100,135' 按下),宽度统一 70 像素。 文字颜色单独用 TextColor(clrWhite) 设成白字,按下态再切到 clrGold 金色的 TextColorPressed,这样点下去时有明显反馈。循环里从 i=0 到 3 调 AddButton,把 x 间隙、文字、宽度和三态色一股脑交给按钮组对象 m_buttons_group1,比手写四个独立按钮少一大截重复代码。 建组动作靠 CreateButtonsGroup(m_chart_id,m_subwin,x,y) 完成,返回 false 就直接退出函数,避免后面悬空引用。注意被标黄的那行 SelectionButton(1) —— 它让组内索引为 1 的第二个按钮处于高亮选中态,用户一打开面板就能看到默认焦点在哪。 最后用 CWndContainer::AddToElementsArray(0,m_buttons_group1) 把这组按钮挂进对象容器的共用数组,函数 return true 交差。外汇与贵金属图表上挂这类面板属于高频交互改造,MT5 脚本异常可能导致图表卡顿,动手前先在模拟盘验证。

MQL5 / C++
class="type">class="kw">string buttons_text[] ={"BUTTON class="num">1","BUTTON class="num">2","BUTTON class="num">3","BUTTON class="num">4"};
class="type">int buttons_width[] ={class="num">70,class="num">70,class="num">70,class="num">70};
class="type">class="kw">color buttons_color[] ={C&class="macro">#x27;class="num">195,class="num">0,class="num">0&class="macro">#x27;,C&class="macro">#x27;class="num">195,class="num">0,class="num">0&class="macro">#x27;,clrRoyalBlue,clrRoyalBlue};
class="type">class="kw">color buttons_color_hover[] ={C&class="macro">#x27;class="num">255,class="num">51,class="num">51&class="macro">#x27;,C&class="macro">#x27;class="num">255,class="num">51,class="num">51&class="macro">#x27;,C&class="macro">#x27;class="num">85,class="num">170,class="num">255&class="macro">#x27;,C&class="macro">#x27;class="num">85,class="num">170,class="num">255&class="macro">#x27;};
class="type">class="kw">color buttons_color_pressed[] ={C&class="macro">#x27;class="num">135,class="num">0,class="num">0&class="macro">#x27;,C&class="macro">#x27;class="num">135,class="num">0,class="num">0&class="macro">#x27;,C&class="macro">#x27;class="num">50,class="num">100,class="num">135&class="macro">#x27;,C&class="macro">#x27;class="num">50,class="num">100,class="num">135&class="macro">#x27;};
class=class="str">"cmt">//--- 设置属性
 m_buttons_group1.TextColor(clrWhite);
 m_buttons_group1.TextColorPressed(clrGold);
class=class="str">"cmt">//--- 在组中加入四个按钮
 for(class="type">int i=class="num">0; i<class="num">4; i++)
    m_buttons_group1.AddButton(buttons_x_gap[i],class="num">0,buttons_text[i],buttons_width[i],
                               buttons_color[i],buttons_color_hover[i],buttons_color_pressed[i]);
class=class="str">"cmt">//--- 创建一组按钮
 if(!m_buttons_group1.CreateButtonsGroup(m_chart_id,m_subwin,x,y))
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 高亮显示组中的第二个按钮
 m_buttons_group1.SelectionButton(class="num">1);
class=class="str">"cmt">//--- 在对象组的共用数组中加入一个对象
 CWndContainer::AddToElementsArray(class="num">0,m_buttons_group1);
 class="kw">return(true);
}

单选按钮组的类该怎么搭

做单选按钮组控件,先建一个 RadioButtons.mqh,继承库里的 CElement,头里把 WndContainer.mqh 包含进来。和简单按钮组不同,单选组只有一种模式:同组按钮不能全弹起,永远有一个处于按下态,背景色通常跟挂载表单一致,用来抓焦点和跟踪按下事件。 每个单选项由三个基底对象拼成:背景、图标、文字标签。共有属性管背景边框色、文字色、高度、三种图标(活动/禁用/屏蔽)和左键优先级;特别属性是文字、宽度、状态(组内仅一可按下)、边缘排布和标签渐变色。 AddButton() 是公开方法,用来在创建控件前预分配属性数组大小并填值;一个按钮都不加,界面构建会直接终止。循环建对象时索引要传进名称构成,避免重名。 事件侧要在 Defines.mqh 加新 ID:ON_CLICK_LABEL,用于文字标签被按下的消息投递。处理时依次检查控件类型、ID、可用性,循环里用索引定位被按按钮,若未高亮就切状态并发消息。 实测可在简单按钮组 EA 里挂四个单选组(每组四个)加一个简单组;第二个简单组的两个按钮分别用于屏蔽/启用前两组,编译挂图后应看到类似原文图 4 的布局。外汇与贵金属 GUI 自动化测试同样面临平台重载风险,参数改动前建议在模拟环境验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建单选按钮组的类      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CRadioButtons : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 文字标签的渐变色
  class="kw">struct LabelsGradients
    {
      class="type">class="kw">color                m_labels_color_array[];
    };
  LabelsGradients    m_labels_total[];
  class=class="str">"cmt">//--- 按钮属性:
  class=class="str">"cmt">//    (class="num">1) 背景颜色 以及 (class="num">2) 鼠标左键点击优先级
  class="type">class="kw">color               m_area_color;
  class="type">int                 m_area_zorder;
  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">int                 m_buttons_width[];
  class="type">class="kw">string              m_buttons_text[];
  class=class="str">"cmt">//--- 按钮高度
  class="type">int                 m_button_y_size;
  class=class="str">"cmt">//--- 活动状态,禁用状态和屏蔽状态的按钮图标
  class="type">class="kw">string              m_icon_file_on;
  class="type">class="kw">string              m_icon_file_off;
  class="type">class="kw">string              m_icon_file_on_locked;
  class="type">class="kw">string              m_icon_file_off_locked;
  class=class="str">"cmt">//--- 文字颜色
  class="type">class="kw">color               m_text_color;
  class="type">class="kw">color               m_text_color_off;
  class="type">class="kw">color               m_text_color_hover;
  class=class="str">"cmt">//--- 鼠标左键点击优先级
  class="type">int                 m_buttons_zorder;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 设置按钮在活动状态,禁用状态和屏蔽状态的图标
  class="type">void                IconFileOn(class="kw">const class="type">class="kw">string file_path)        { m_icon_file_on=file_path;        }
  class="type">void                IconFileOff(class="kw">const class="type">class="kw">string file_path)       { m_icon_file_off=file_path;       }

「把单选按钮的样式与创建拆开写」

上面这段类定义把「外观配置」和「实体创建」分成了两层:先用一组 void 设值函数把图标路径、区域色、文字色存进成员变量,再在 AddButton / CreateRadioButtons 里真正往图表上画对象。 IconFileOnLocked 与 IconFileOffLocked 接收 string 路径,分别绑定锁定态的开、关图标;AreaColor 管背景,TextColor / TextColorOff / TextColorHover 覆盖常态、关闭态、悬停态三种文字色。注意这些函数都要求在按钮创建前调用,否则颜色不会生效。 CRadioButtons 内部用 m_area[]、m_icon[]、m_label[] 三个动态数组承载矩形底、位图图标和文字标签,CreateArea / CreateRadio / CreateLabel 按 index 逐个实例化。RadioButtonsTotal 直接返回 m_icon 数组大小,也就是说按钮数量由你 AddButton 的次数决定,不是写死的。 在 MT5 里验证时,先调样式函数再调 AddButton,若改了颜色却没变化,十有八九是调用顺序反了——这类 GUI bug 在回测里不会报错,只会在图表上静默失效。

MQL5 / C++
class="type">void IconFileOnLocked(class="kw">const class="type">class="kw">string file_path)  { m_icon_file_on_locked=file_path;  }
class="type">void IconFileOffLocked(class="kw">const class="type">class="kw">string file_path) { m_icon_file_off_locked=file_path; }
class=class="str">"cmt">//--- (class="num">1) 背景颜色, (class="num">2) 文字颜色
class="type">void AreaColor(class="kw">const class="type">class="kw">color clr)                { m_area_color=clr;                }
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 TextColorHover(class="kw">const class="type">class="kw">color clr)           { m_text_color_hover=clr;         }
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 CRadioButtons : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 用于创建按钮的对象
  CRectLabel       m_area[];
  CBmpLabel        m_icon[];
  CLabel           m_label[];
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 创建按钮的方法
  class="type">bool             CreateRadioButtons(class="kw">const class="type">long chart_id,class="kw">const class="type">int window,class="kw">const class="type">int x,class="kw">const class="type">int y);
  class=class="str">"cmt">//---
class="kw">private:
  class="type">bool             CreateArea(class="kw">const class="type">int index);
  class="type">bool             CreateRadio(class="kw">const class="type">int index);
  class="type">bool             CreateLabel(class="kw">const class="type">int index);
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 按钮数量
  class="type">int              RadioButtonsTotal(class="type">void)                 class="kw">const { class="kw">return(::ArraySize(m_icon));    }
  };
把组态校验交给小布盯盘
这些控件组的创建与日志报错诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到哪一组按钮初始化失败,你只管设计交互逻辑。

常见问题

通用属性如高度、禁用色、文字色等组内共享;独特属性如单按钮宽度、文字、渐变色、到边界距离,决定每个按钮个体差异与排列。
后者需为每个控件额外写取指针方法,前者直接复用基类虚方法,少一层封装代码,初始化更短平快。
可以,小布会扫描 WndContainer 包含链与 ButtonsGroup.mqh 位置,若缺失包含或路径错乱会在品种页标红提示。
创建过程中止,终端日志打印对应消息,图表上该组不会生成任何 OBJ_BUTTON 对象。
单选组强制互斥单选,图标组侧重位图资源与悬停态切换,两者共享组容器但独特属性集不同。