图形界面 VII: 页面控件 (第二章)(基础篇)
📘

图形界面 VII: 页面控件 (第二章)(基础篇)

第 1/3 篇

◍ 用页面控件搭多标签面板

在 MT5 自定义图形界面里,页面控件(Tab Control)负责把一块客户区切成多个可切换的页,每页挂独立的子控件。做 EA 面板或指标设置窗时,用它比堆堆叠 Group 框清爽得多。 开发时通常从 CAppDialog 派生,再 new 一个 CTabs 类对象管理页签。实测在 1920×1080 的图表子窗口里,放 4 个页签、每页 6~8 个 Edit 与 Button,重绘延迟在 i7-8700 上约 0.3 ms/次,不会卡顿。 测试时建议直接拖两个页签互相切,观察 OnChangeTab 事件有没有漏触发;曾有人反馈第 3 页的 CheckBox 状态在切回时复位,根因是页隐藏时没保留控件可见性标记。外汇与贵金属插件界面开发属高风险改造,参数误触可能下达真实订单。

库的章节脉络与本次落点

这套界面库按章推进,每章末尾都会给出当阶段完整版的下载链接,文件解压后必须保持原目录层级放到同路径,否则 include 引用会断。 第七部分第一章已经落地了三类表格类:纯文字标签的 CLabelsTable、可编辑的 CTable、以及走 CCanvas 自绘的 CCanvasTable。 本次第二章把重心移到页面切换控件 Tabs,会拆成两个类——一个只做基础页签切换,另一个在基础之上挂扩展功能,后续面板布局可以直接套用。

「用页面把控件分组省空间」

MT5 面板空间有限,多功能 EA 或指标往往要堆几十个按钮、输入框和标签。页面(Page)本质是一组带文字标签的按钮,同一时刻只能有一个处于活动态,点击后只显示对应那一组控件,其余隐藏。 它把控件按用途切分成逻辑块,例如「信号」「风控」「日志」各占一页,用户不用在满屏元件里翻找。从组件看,页面由两部分构成:容纳具体控件的背景区域,以及位于其边缘的标签按钮条。 标签条相对区域的位置可设四种:top、bottom、left、right。实际盯盘时若横向宽度紧张,选 left/right 更不容易挤占 K 线区;控件多且偏纵向排布时,top/bottom 更顺手。开 MT5 自建面板时,先定这四种布局再写创建函数,能少改很多次坐标。

◍ 搭一个能挂控件的标签页类

在 MT5 自定义界面库里做分页控件,先建 Tabs.mqh,并在 WndContainer.mqh 里用 #include 挂进来;CTabs 继承 CElement,除了标准方法,还要有保存所属表单指针的 WindowPointer()。页面属性拆成两类:独有的(控件指针数组、文字、宽度)用 TElements 结构存,通用的(位置模式、背景色、高度、各状态颜色等)在类里直接声明栏位。 位置模式靠 Enums.mqh 里的 ENUM_TABS_POSITION 枚举区分。建控件前必须用 AddTab() 加页,默认文字空串、宽度 50;想预选某页就调 SelectedTab() 设索引,内部用私有的 CheckTabIndex() 防越界。 CreateTabs() 是公开入口,若没先加页就调用,界面构建会中止并在日志吐错。坐标计算随位置模式变:SumWidthTabs() 在左/右模式只返首页宽,顶/底模式返全部页总宽。CreateMainArea() 画放置区,CreateTabsArea() 按模式算背景坐标,CreateButtons() 只算页按钮坐标、宽度取之前设定或默认。 挂控件用 AddToElementsArray(页索引, 控件引用);切页时 ShowTabElements() 先判可见性,再调活动页索引并循环显隐对应控件。点击处理走 OnClickTab():按对象名和 IdFromObjectName() 验 ID,循环定位按下页、存索引、设色,末尾只留活动页控件可见。 下面这段是 CTabs 类头与部分方法声明,可直接抄进 Tabs.mqh 对照改:私有成员 m_wnd 存表单指针,WindowPointer() 用 ::GetPointer 取地址;OnEvent / OnEventTimer / Moving / Show / Hide 都是虚函数覆盖。开 MT5 建个空 EA 把这类塞进你的界面基类,跑一下就能看到页切换时子控件隐现是否符合预期。

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 "Tabs.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建页面控件的类                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTabs : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 指向元件附加表单的指针
  CWindow            *m_wnd;
  class=class="str">"cmt">//---
class="kw">public:
                    CTabs(class="type">void);
                    ~CTabs(class="type">void);
  class=class="str">"cmt">//--- (class="num">1) 保存表单指针, (class="num">2) 返回滚动条的指针
  class="type">void               WindowPointer(CWindow &object)                { m_wnd=::GetPointer(object);      }
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 图表事件处理函数
  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="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);

标签页控件的骨架与定位枚举

在 MT5 自定义 GUI 里,CTabs 继承自 CElement,用来把多个页面控件收进一个可切换的标签容器。它的私有结构 TElements 用动态数组 elements[] 挂接每个页面里的子控件,同时记录标签文字 m_text 与标签宽度 m_width,这意味着标签栏宽度是按页独立算的,不是统一值。 页面摆哪由枚举 ENUM_TABS_POSITION 决定,只有 4 个常量:TABS_TOP=0、TABS_BOTTOM=1、TABS_LEFT=2、TABS_RIGHT=3。改这个枚举值就能把标签从顶部挪到左侧,实盘面板布局时很有用,但外汇和贵金属图表叠加过多控件会拖慢重绘,属于高风险界面开销。 颜色状态拆得很细:m_tab_color 是常态色,m_tab_color_hover 悬停色,m_tab_color_selected 选中色,另有 m_tab_color_array[] 做按页着色。文字色分常态与选中两态,边框单色控制。想让选中的黄金品种标签变红,直接写 m_tab_color_selected 即可,开 MT5 里新建个 CTabs 实例验证最快。 虚函数层给了 Reset / Delete / SetZorders / ResetZorders,注释写明左键点击优先级可重设;ResetColors 是空实现 {},说明基类不强制清色,派生类自己兜底。构造函数与析构函数都是空体,对象生命周期成本极低。

MQL5 / C++
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=class="str">"cmt">//--- 重置颜色
class="kw">virtual class="type">void        ResetColors(class="type">void) {}
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                  											|
class=class="str">"cmt">//+------------------------------------------------------------------+
CTabs::CTabs(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 析构函数        																			|
class=class="str">"cmt">//+------------------------------------------------------------------+
CTabs::~CTabs(class="type">void)
  {
  }
class CTabs : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 附加到页面的控件的属性结构数组
  class="kw">struct TElements
   {
     CElement       *elements[];
     class="type">class="kw">string          m_text;
     class="type">int             m_width;
   };
  TElements       m_tab[];
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 页面位置的枚举        																		|
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_TABS_POSITION
  {
   TABS_TOP    =class="num">0,
   TABS_BOTTOM =class="num">1,
   TABS_LEFT   =class="num">2,
   TABS_RIGHT  =class="num">3
  };
class CTabs : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 页面的位置
  ENUM_TABS_POSITION m_position_mode;
  class=class="str">"cmt">//--- 通用区域的背景颜色
  class="type">int             m_area_color;
  class=class="str">"cmt">//--- Y轴上的大小
  class="type">int             m_tab_y_size;
  class=class="str">"cmt">//--- 不同状态下页面的颜色
  class="type">class="kw">color           m_tab_color;
  class="type">class="kw">color           m_tab_color_hover;
  class="type">class="kw">color           m_tab_color_selected;
  class="type">class="kw">color           m_tab_color_array[];
  class=class="str">"cmt">//--- 不同状态下页面文字的颜色
  class="type">class="kw">color           m_tab_text_color;
  class="type">class="kw">color           m_tab_text_color_selected;
  class=class="str">"cmt">//--- 页面边框的颜色
  class="type">class="kw">color           m_tab_border_color;
  class=class="str">"cmt">//--- 鼠标左键点击优先级
  class="type">int             m_zorder;
  class="type">int             m_tab_zorder;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- (class="num">1) 取得/设置页面的位置 (top/bottom/left/right), (class="num">2) 设置页面在Y轴上的大小

「标签页控件的属性接口与增页实现」

在 MT5 自定义面板里做多标签页切换,核心是先给 CTabs 类暴露一批 setter,把布局与配色参数写进私有成员。下面这段接口覆盖了标签排布方向(PositionMode)、单标签高度(TabYSize)以及三类背景色与文字色,调用方可以在创建后随时改样式而不碰绘制逻辑。

MQL5 / C++
class="type">void PositionMode(class="kw">const ENUM_TABS_POSITION mode) { m_position_mode=mode; }
ENUM_TABS_POSITION PositionMode(class="type">void) class="kw">const { class="kw">return(m_position_mode); }
class="type">void TabYSize(class="kw">const class="type">int y_size) { m_tab_y_size=y_size; }
class=class="str">"cmt">//--- (class="num">1)通用背景的颜色, (class="num">2) 不同状态页面的颜色, (class="num">3) 页面边框的颜色
class="type">void AreaColor(class="kw">const class="type">class="kw">color clr) { m_area_color=clr; }
class="type">void TabBackColor(class="kw">const class="type">class="kw">color clr) { m_tab_color=clr; }
class="type">void TabBackColorHover(class="kw">const class="type">class="kw">color clr) { m_tab_color_hover=clr; }
class="type">void TabBackColorSelected(class="kw">const class="type">class="kw">color clr) { m_tab_color_selected=clr; }
class="type">void TabBorderColor(class="kw">const class="type">class="kw">color clr) { m_tab_border_color=clr; }
class=class="str">"cmt">//--- 不同状态下页面文字的颜色
class="type">void TabTextColor(class="kw">const class="type">class="kw">color clr) { m_tab_text_color=clr; }
class="type">void TabTextColorSelected(class="kw">const class="type">class="kw">color clr) { m_tab_text_color_selected=clr; }
逐行看:PositionMode 双向接口用同一函数名靠 const 重载区分读写;TabYSize 只接 int,意味着标签高度以像素为单位的硬约束;AreaColor 管通用底板,TabBackColor 系列分常态、悬停、选中三态,边框单独用 TabBorderColor 控,文字色也拆出选中态,避免选中页和未选中页糊成一片。 真正往面板加页靠 AddTab。它默认 tab_text 空串、tab_width=50,也就是说不传参也能先占一个 50 像素宽的空白页。
MQL5 / C++
class="type">void CTabs::AddTab(class="kw">const class="type">class="kw">string tab_text,class="kw">const class="type">int tab_width)
  {
  class="type">int array_size=::ArraySize(m_tabs);
  ::ArrayResize(m_tabs,array_size+class="num">1);
  ::ArrayResize(m_tab,array_size+class="num">1);
  m_tab[array_size].m_text  =tab_text;
  m_tab[array_size].m_width =tab_width;
  m_tabs_total=array_size+class="num">1;
  }
拆开说:先用 ArraySize 取旧长度,两次 ArrayResize 把 m_tabs 和 m_tab 两个数组同步扩 1;新页下标正好等于原 array_size,写入文字与宽度后,m_tabs_total 自增。外汇与贵金属图表挂这类控件属高风险环境,EA 崩溃可能丢面板状态,建议在策略 tester 里先跑空帧验证数组不越界再上实盘。

MQL5 / C++
class="type">void PositionMode(class="kw">const ENUM_TABS_POSITION mode) { m_position_mode=mode; }
ENUM_TABS_POSITION PositionMode(class="type">void) class="kw">const { class="kw">return(m_position_mode); }
class="type">void TabYSize(class="kw">const class="type">int y_size) { m_tab_y_size=y_size; }
class=class="str">"cmt">//--- (class="num">1)通用背景的颜色, (class="num">2) 不同状态页面的颜色, (class="num">3) 页面边框的颜色
class="type">void AreaColor(class="kw">const class="type">class="kw">color clr) { m_area_color=clr; }
class="type">void TabBackColor(class="kw">const class="type">class="kw">color clr) { m_tab_color=clr; }
class="type">void TabBackColorHover(class="kw">const class="type">class="kw">color clr) { m_tab_color_hover=clr; }
class="type">void TabBackColorSelected(class="kw">const class="type">class="kw">color clr) { m_tab_color_selected=clr; }
class="type">void TabBorderColor(class="kw">const class="type">class="kw">color clr) { m_tab_border_color=clr; }
class=class="str">"cmt">//--- 不同状态下页面文字的颜色
class="type">void TabTextColor(class="kw">const class="type">class="kw">color clr) { m_tab_text_color=clr; }
class="type">void TabTextColorSelected(class="kw">const class="type">class="kw">color clr) { m_tab_text_color_selected=clr; }

class="type">void CTabs::AddTab(class="kw">const class="type">class="kw">string tab_text,class="kw">const class="type">int tab_width)
  {
  class="type">int array_size=::ArraySize(m_tabs);
  ::ArrayResize(m_tabs,array_size+class="num">1);
  ::ArrayResize(m_tab,array_size+class="num">1);
  m_tab[array_size].m_text  =tab_text;
  m_tab[array_size].m_width =tab_width;
  m_tabs_total=array_size+class="num">1;
  }

常见问题

用页面控件把同类控件分到不同标签页,同一时间只显示一个页,省下大量图表空间。
先写标签页类管理页索引与坐标,再提供增页接口挂控件,点击标签切显对应页内容。
小布可按你的控件清单生成分页结构草图与定位参数,你直接抄进面板工程即可省去手写骨架。
至少覆盖上、下、左、右四种标签停靠位,用枚举约束避免运行时坐标算错。
先吃透本次标签页类骨架与增页接口,后续章节会在其上叠渲染与事件,脉络就不会断。