图形界面III:简单与多功能按钮组(第二章)·进阶篇
(2/3)· 还在为每个按钮手写状态互斥逻辑?一组控件只需一个类就能管住按下与禁用
◍ 按钮组状态切换的索引容错逻辑
在 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 界面时最好提前断言,避免点选无响应还查不出原因。
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 分支做高亮,是按钮组交互完整性的另一半。
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 把这组值抄进你的面板类,就能直接复现四连按钮的排布。
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 脚本异常可能导致图表卡顿,动手前先在模拟盘验证。
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 自动化测试同样面临平台重载风险,参数改动前建议在模拟环境验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建单选按钮组的类 | 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 在回测里不会报错,只会在图表上静默失效。
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)); } };