图形界面III:用三个类搭出可复用按钮控件库(基础篇)
(1/3)· 从简单按钮到分割按钮,MetaTrader 5 界面开发最常被低估的基底件怎么写才不返工
很多交易者把 MT5 自定义面板当成一次性脚本,按钮状态、分组、解锁逻辑每次都重写。控件库前两部分啃完菜单系统,这一篇回到最朴素的按钮,把复用骨架先立住,后面才不会越叠越乱。
「给 MT5 界面塞几个能用的按钮类」
在 MT5 自定义指标或 EA 的图形界面里,原生控件并不够顺手。作者 Anatoli Kazharski 在 2016-04-20 发布的系列第一章,提供了三类按钮的封装思路:简单按钮、图标按钮、分割按钮,原文示例被查看 5773 次、收藏 32 次,说明这套封装在社群里确实有人反复翻出来用。 简单按钮负责最基础的点按触发;图标按钮把位图资源压进控件,节省界面文字空间;分割按钮则在一个控件里同时给出主操作和下拉扩展项,适合参数切换密集的面板。 外汇与贵金属交易界面自动化属于高风险操作环境,任何界面封装只解决交互效率,不替代策略逻辑。开 MT5 用 MetaEditor 新建 include 文件,把这几类按钮类各自独立成文件,是后续章节拼装界面的前提。
◍ 从菜单控件切到按钮:本篇只做轻量扩展
前两部分已经把图形界面库的架构和菜单类啃完了,主菜单和上下文菜单属于最复杂的控件,这一篇调头做最简单的互操作元件——按钮。 本篇会新建六个类:CSimpleButton、CIconButton、CSplitButton 三个独立按钮,以及 CButtonsGroup、CIconButtonsGroup、CRadioButtons 三个关联按钮组。相比之前第五章在 MT4 里测整套界面库的复杂度,这次只补控件不重造轮子。 顺带给上下文菜单加一种模式,并给 CWindow 表单类多塞一个方法参数,用来标记「当前是哪个控件在激活时锁住了表单」——这样表单只能被当初封锁它的那个控件解锁,避免别的控件误触解窗。 前面文章已经完整讲过的基础方法不再逐条复述,相关声明只保留在类代码里,读者开 MT5 看源码即可对应。
自己写一个 MT5 简单按钮类
MT5 标准库 Controls 文件夹里已有 CButton,基类是 CChartObjectButton,对应 OBJ_BUTTON 图形对象。它天然带「按下 / 抬起」两态,且按下时颜色比未按下略深;也可从菜单「插入→对象→图形对象→按钮」手动挂到图表上改参数。 我们要在 Controls 下新建 SimpleButton.mqh,写 CSimpleButton 类。可设属性包括:按钮尺寸、随鼠标位置切换的背景色、两态边框色、文字色。按钮行为分两种模式——一种是松手自动弹起,另一种是像开关一样保持按下,再次点击才抬起,用 IsPressed() 判当前状态。 有时条件不满足需禁用按钮,条件满足再解锁,这种封锁 / 解锁逻辑要在类里实现。自定义事件标识符 ON_CLICK_BUTTON 加到 Defines.mqh,专门给按钮用;CSimpleButton::OnClickButton() 先核对对象名与当前状态,不符就退出,通过则恢复初始色或切到另一组色,末尾发带标识符、元件 ID、索引和名称的消息。 实测时复制前文测试 EA,只留主菜单和上下文菜单项,在 WndContainer.mqh 包含本类。CProgram 里声明三个按钮:两个自动弹起,第三个固定两态。下方代码高亮处就是启用两态模式的开关。 还差一步:让按钮在鼠标悬停按下时变颜色。在 CHARTEVENT_MOUSE_MOVE 里查左键是否按住,CSimpleButton::OnEvent() 中加判断——隐藏、表单禁用、左键未按、按钮禁用,任一成立就退出;否则按焦点和状态设色。编译后挂图表,Simple Button 3 按下时 Simple Button 1 被禁用,松开恢复可用。外汇与贵金属 MT5 开发属高风险环境,实盘前务必在策略测试器验证。
class="macro">#include "Element.mqh" class="macro">#include "Window.mqh" class CSimpleButton : class="kw">public CElement { class="kw">private: CWindow *m_wnd; class=class="str">"cmt">// 指向附加元件表单的指针 class="kw">public: CSimpleButton(class="type">void); ~CSimpleButton(class="type">void); class="type">void WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); } class=class="str">"cmt">// 保存表单指针 class="kw">public: class="kw">virtual class="type">void 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">// 图表事件处理 class="kw">virtual class="type">void OnEventTimer(class="type">void); class=class="str">"cmt">// 计时器 class=class="str">"cmt">//--- 移动元件
「封装简单按钮类的接口与状态字段」
在 MT5 自定义面板开发里,把按钮逻辑封成一个 CSimpleButton 类能省掉大量重复代码。它继承自 CElement,基类先声明了一组虚函数:Moving 负责按 x、y 偏移移动控件,Show/Hide/Reset/Delete 分别对应显示、隐藏、重置和删除四种生命周期操作,注释里标得很直白。 SetZorders 和 ResetZorders 管的是鼠标左键点击优先级,也就是多个控件叠在一起时谁先响应点击,这在密集面板里很容易踩坑。 CSimpleButton 的私有段暴露了按钮的全部可调配属性:m_button_text 存文字,m_button_x_size / m_button_y_size 定尺寸;背景色就给了五套——常规、禁用、悬停、按下以及一套颜色数组 m_back_color_array,边框和文字也各有 off / pressed 状态色。 两个 bool 字段值得单独看:m_two_state 控制按钮是否双态(按一次锁、再按弹),m_button_state 标记当前可用还是禁用。对外只开放了 CreateSimpleButton(传 chart_id、subwin、文字和坐标)和内部 CreateButton,其余状态设置方法留在 public 段待补。 开 MT5 新建 EA 把这段类骨架贴进 include,先只填 CreateSimpleButton 的参数,就能在副图看到按钮占位,验证继承结构是否跑通。
class="kw">virtual class="type">void Moving(class="kw">const class="type">int x,class="kw">const class="type">int y); class=class="str">"cmt">//--- (class="num">1) 显示, (class="num">2) 隐藏, (class="num">3) 重置, (class="num">4) 删除 class="kw">virtual class="type">void Show(class="type">void); class="kw">virtual class="type">void Hide(class="type">void); class="kw">virtual class="type">void Reset(class="type">void); class="kw">virtual class="type">void Delete(class="type">void); class=class="str">"cmt">//--- (class="num">1) 设置, (class="num">2) 重置 鼠标左键点击的优先级 class="kw">virtual class="type">void SetZorders(class="type">void); class="kw">virtual class="type">void ResetZorders(class="type">void); }; class CSimpleButton : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 用于创建按钮的对象 CButton m_button; class=class="str">"cmt">//--- 按钮属性: class=class="str">"cmt">// (class="num">1) Text, (class="num">2) size class="type">class="kw">string m_button_text; class="type">int m_button_x_size; class="type">int m_button_y_size; class=class="str">"cmt">//--- 背景颜色 class="type">color m_back_color; class="type">color m_back_color_off; class="type">color m_back_color_hover; class="type">color m_back_color_pressed; class="type">color m_back_color_array[]; class=class="str">"cmt">//--- 边框颜色 class="type">color m_border_color; class="type">color m_border_color_off; class=class="str">"cmt">//--- 文字颜色 class="type">color m_text_color; class="type">color m_text_color_off; class="type">color m_text_color_pressed; class=class="str">"cmt">//--- 鼠标左键点击的优先级 class="type">int m_button_zorder; class=class="str">"cmt">//--- 两种按钮状态的模式 class="type">bool m_two_state; class=class="str">"cmt">//--- 可用/禁用 class="type">bool m_button_state; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 用于创建简单按钮的方法 class="type">bool CreateSimpleButton(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">class="kw">string button_text,class="kw">const class="type">int x,class="kw">const class="type">int y); class=class="str">"cmt">//--- class="kw">private: class="type">bool CreateButton(class="type">void); class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- (class="num">1) 设置按钮模式, class=class="str">"cmt">// (class="num">2) 按钮的一般状态 (可用/封锁禁用)
◍ 把按钮状态与配色拆成可读接口
在 MT5 自定义控件里,把按钮的内部状态和外显属性分开暴露,是后期维护面板的关键一步。下面这段类方法定义,把「是否双态」「当前是否被按下」「按钮文本与配色」全部做成了独立 getter/setter。 TwoState() 接收 bool 写入 m_two_state,控制按钮是点按即弹还是锁定切换;IsPressed() 直接返回 m_button.State() 的真实控件状态,比自己缓存标志更不易出错。ButtonState() 有两个重载,一个读 m_button_state,一个声明写入口,负责把外部事件同步进对象。 配色部分细分得很碎:文字有常规、off、pressed 三色,背景有常规、off、hover、pressed 四色,边框单独一列。实际盯盘面板里,hover 和 pressed 用不同背景色(例如 m_back_color_hover 与 m_back_color_pressed 取异色),能让你在黄金 1 分钟图快速辨认当前触发的是哪一类订单按钮。外汇与贵金属波动剧烈,这类视觉反馈只降低误操作概率,不预示任何方向。 开 MT5 建个空 EA,把下面接口原样贴进你的 CButton 派生类,编译后拖两个按钮到图表,分别设 BackColorHover(C'30,90,30') 与 BackColorPressed(C'90,30,30'),鼠标划过和点击就能看出状态分流是否生效。
class="type">void TwoState(class="kw">const class="type">bool flag) { m_two_state=flag; } class="type">bool IsPressed(class="type">void) class="kw">const { class="kw">return(m_button.State()); } class="type">bool ButtonState(class="type">void) class="kw">const { class="kw">return(m_button_state); } class="type">void ButtonState(class="kw">const class="type">bool state); class=class="str">"cmt">//--- 按钮大小 class="type">void ButtonXSize(class="kw">const class="type">int x_size) { m_button_x_size=x_size; } 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="type">class="kw">string Text(class="type">void) class="kw">const { class="kw">return(m_button.Description()); } class="type">void TextColor(class="kw">const class="type">color clr) { m_text_color=clr; } class="type">void TextColorOff(class="kw">const class="type">color clr) { m_text_color_off=clr; } class="type">void TextColorPressed(class="kw">const class="type">color clr) { m_text_color_pressed=clr; } class=class="str">"cmt">//--- 设置按钮背景的颜色 class="type">void BackColor(class="kw">const class="type">color clr) { m_back_color=clr; } class="type">void BackColorOff(class="kw">const class="type">color clr) { m_back_color_off=clr; } class="type">void BackColorHover(class="kw">const class="type">color clr) { m_back_color_hover=clr; } class="type">void BackColorPressed(class="kw">const class="type">color clr) { m_back_color_pressed=clr; } class=class="str">"cmt">//--- 设置按钮边框的颜色 class="type">void BorderColor(class="kw">const class="type">color clr) { m_border_color=clr; }
按钮状态切换与点击响应的底层实现
在 MT5 自定义控件里,按钮的视觉状态完全由 ButtonState() 驱动:传入 true 时套用激活态的文字、背景、边框三色,传入 false 则切到 off 系列颜色,这一行赋值直接决定界面反馈。
点击事件走 CHARTEVENT_OBJECT_CLICK 分支,先比对象名再判 m_button_state,若按钮处于禁用态会强制 State(false) 并直接返回,避免无效触发。单状态模式每次点击只刷新颜色不锁状态,双状态模式则根据 m_button.State() 在按下/松开间切换,并写入 m_back_color_pressed 到颜色数组。
宏 ON_CLICK_BUTTON (8) 把点击事件编码成独立 ID,方便在 EA 里与图表其他对象区分。外汇与贵金属图表挂这类按钮属于高风险环境下的界面扩展,实盘前应在策略测试器里把状态切换跑满一遍。
把下面这段代码贴进 CSimpleButton 类定义之后,编译后拖一个按钮到 XAUUSD 的 M5 图,点几下就能看到双色态翻转是否跟手。
class="type">void CSimpleButton::ButtonState(class="kw">const class="type">bool state) { m_button_state=state; m_button.State(class="kw">false); m_button.Color((state)? m_text_color : m_text_color_off); m_button.BackColor((state)? m_back_color : m_back_color_off); m_button.BorderColor((state)? m_border_color : m_border_color_off); } class="macro">#define ON_CLICK_BUTTON(class="num">8) class=class="str">"cmt">// 按下按钮 class CSimpleButton : 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="type">void CSimpleButton::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) { if(id==CHARTEVENT_OBJECT_CLICK) { if(OnClickButton(sparam)) class="kw">return; } } class="type">bool CSimpleButton::OnClickButton(class="kw">const class="type">class="kw">string clicked_object) { if(m_button.Name()!=clicked_object) class="kw">return(class="kw">false); if(!m_button_state) { m_button.State(class="kw">false); class="kw">return(class="kw">false); } if(!m_two_state) { m_button.State(class="kw">false); m_button.Color(m_text_color); m_button.BackColor(m_back_color); } else { if(m_button.State()) { m_button.State(true); m_button.Color(m_text_color_pressed); m_button.BackColor(m_back_color_pressed); CElement::InitColorArray(m_back_color_pressed,m_back_color_pressed,m_back_color_array); } else { m_button.State(class="kw">false);