DoEasy. 控件 (第 18 部分): TabControl 中滚动选项卡的功能·进阶篇
「控件库的头部引用与版本声明」
在 MT5 自建 WForms 风格控件库时,头文件顶部的 include 路径决定了编译能否找到依赖容器。上面这段实际工程里用了相对路径,例如 #include "..\Containers\Panel.mqh" 指向父级目录的 Containers 文件夹,若你把源码挪到不同层级,编译器会直接报 cannot open include file。
#property strict 出现在多个头文件中,注释里写明是为 mql4 兼容必要;在 MT5 下它强制类型检查更严,能提前拦掉隐式转换类 bug。
版权与链接属性被重复声明了三次,版本号统一为 "1.00",作者标识指向同一用户。复制这类多文件控件库时,保持各 .mqh 的 #property version 一致,可以避免后续维护时搞不清哪份是旧版。
别把相对路径当万能
工程根目录结构若与原始作者不同,相对 include 会失效;开 MT5 后先按 F4 看编译日志里的具体缺失路径,再决定改绝对路径还是调整文件夹层级。
class="macro">#include "..\Containers\Panel.mqh" class="macro">#include "..\Containers\Container.mqh" class="macro">#include "..\Helpers\ListBoxItem.mqh" class="macro">#include "Container.mqh" class="macro">#class="kw">property copyright "Copyright class="num">2022, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict
面板控件库的引用与对象实例化入口
在 MT5 自定义面板开发中,先通过 include 把分散在 Helpers、WForms 目录下的控件头文件挂进来,才能用一套统一的类去拼界面。上面这段代码一次性引入了 TabField、四种方向箭头按钮、上下/左右箭头盒、GroupBox、TabControl,以及位于 WForms\Common Controls 下的 ListBox、CheckedListBox、ButtonListBox——合计 14 个头文件,缺任意一个都会在编译期报 'cannot open include file'。 真正生成图形元件的落点在 CPanel::CreateNewGObject。它接收 11 个参数:元件类型枚举、序号、描述文本、左上角坐标 (x,y)、宽高 (w,h)、颜色、不透明度(uchar 0~255)、是否可拖动、是否激活。函数体开头先把 element 置为 NULL,再用 switch(type) 分流到具体控件构造,意味着同一种工厂方法能产出不同图形元素,省掉每类控件写独立 new 的重复代码。 开 MT5 验证时,把这段 include 路径按你本地 Indicators 或 Scripts 目录层级对齐;若把面板放在 MQL5/Experts 下,相对路径 '..\..\WForms' 要确认往上两层确实能找到 WForms 文件夹,否则编译直接失败。外汇与贵金属图表上挂这类自定义面板属高风险操作,参数坐标算错可能导致控件溢出图表可视区。
class="macro">#include "..\Helpers\TabField.mqh" class="macro">#include "..\Helpers\ArrowButton.mqh" class="macro">#include "..\Helpers\ArrowUpButton.mqh" class="macro">#include "..\Helpers\ArrowDownButton.mqh" class="macro">#include "..\Helpers\ArrowLeftButton.mqh" class="macro">#include "..\Helpers\ArrowRightButton.mqh" class="macro">#include "..\Helpers\ArrowUpDownBox.mqh" class="macro">#include "..\Helpers\ArrowLeftRightBox.mqh" class="macro">#include "GroupBox.mqh" class="macro">#include "TabControl.mqh" class="macro">#include "..\..\WForms\Common Controls\ListBox.mqh" class="macro">#include "..\..\WForms\Common Controls\CheckedListBox.mqh" class="macro">#include "..\..\WForms\Common Controls\ButtonListBox.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Panel object class of WForms controls | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a new graphical object | class=class="str">"cmt">//+------------------------------------------------------------------+ CGCnvElement *CPanel::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const class="type">int obj_num, const class="type">class="kw">string descript, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">color colour, const class="type">uchar opacity, const class="type">bool movable, const class="type">bool activity) { CGCnvElement *element=NULL; class="kw">switch(type) {
◍ 图形元素工厂的分发逻辑
在 MT5 自定义图形库里,通常用一处 switch 把枚举类型映射到具体控件类。下面这段就是典型的分发器:根据 GRAPH_ELEMENT_TYPE_xxx 枚举,new 出对应的 CGCnvElement、CForm、CContainer 等对象,并统一传入图表 ID、子窗口号、描述与坐标尺寸。 这种写法的好处是调用方只需持有一个基类指针 element,不必关心背后是按钮还是列表框。外汇与贵金属图表上挂 EA 面板时,高风险来自误把子窗口号传错导致控件画到主图,调试时往往半天找不到原因。 逐行看,GRAPH_ELEMENT_TYPE_ELEMENT 分支最重,它要接 colour、opacity、movable、activity 等 7 个参数;而 GRAPH_ELEMENT_TYPE_FORM 往后大多只吃 6 个基础参数。你在 MT5 里改这类工厂时,先确认枚举值和类构造签名一一对应,否则编译期就会报不匹配。
case GRAPH_ELEMENT_TYPE_ELEMENT : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity); class="kw">break; case GRAPH_ELEMENT_TYPE_FORM : element=new CForm(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CONTAINER : element=new CContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_GROUPBOX : element=new CGroupBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_PANEL : element=new CPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_LABEL : element=new CLabel(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CHECKBOX : element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON : element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_BUTTON : element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX : element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM : element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX : element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break;
「面板里批量造 GUI 控件的 switch 落点」
在 CPanel::CreateNewGObject 的 switch 分支中,工作面板按控件枚举类型直接 new 出对应的标准库对象。从列表框到上下/左右箭头组合框,一共覆盖了 12 种 WF 系列图形元素,default 分支什么都不做只 break。 所有 new 出来的对象都绑定到 this.ChartID() 与 this.SubWindow(),并传入 descript 和 x/y/w/h 四参定位。如果你要在 MT5 里加自定义控件,照这个 case 格式补一行即可,编译后面板会自动识别。 new 完之后有空指针校验:若 element==NULL,终端会打印失败信息并带上 TypeElementDescription(type) 的文本。外汇与贵金属图表加载这类面板属高风险操作环境,建议先在策略测试器跑通再上实盘图表。
case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX : element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER : element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD : element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL : element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON : element=new CArrowButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP : element=new CArrowUpButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN : element=new CArrowDownButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT : element=new CArrowLeftButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT : element=new CArrowRightButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX : element=new CArrowUpDownBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX : element=new CArrowLeftRightBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; class="kw">default : class="kw">break; } if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type)); class="kw">return element;
图形元素工厂的 switch 分发逻辑
在 MT5 自定义图形库里,新增一个画布元素通常不是直接 new 具体类,而是走一个统一的工厂函数,按 type 参数决定实例化哪个派生类。 下面这段是工厂函数的参数列表与核心 switch 分发,参数涵盖了描述文本、坐标(x,y)、尺寸(w,h)、颜色、透明度(opacity,uchar 0~255)、是否可拖动(movable)和是否激活(activity)。 可以看到 GRAPH_ELEMENT_TYPE_ELEMENT 会构造基础的 CGCnvElement,并透传全部参数;而 FORM、WF_CONTAINER、WF_GROUPBOX、WF_PANEL、WF_LABEL 等类型则只传图表 ID、子窗口、描述与坐标尺寸,说明这些复合控件内部自己管样式。 开 MT5 把这段粘进你的图形库基类,改 case 分支就能扩展自有控件,外汇与贵金属图表叠加 UI 属高风险操作,参数错误可能遮住价格区域。
const class="type">class="kw">string descript, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">color colour, const class="type">uchar opacity, const class="type">bool movable, const class="type">bool activity) { CGCnvElement *element=NULL; class="kw">switch(type) { case GRAPH_ELEMENT_TYPE_ELEMENT : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity); class="kw">break; case GRAPH_ELEMENT_TYPE_FORM : element=new CForm(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CONTAINER : element=new CContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_GROUPBOX : element=new CGroupBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_PANEL : element=new CPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_LABEL : element=new CLabel(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break;
◍ 控件枚举到实例的映射收口
在图形对象工厂的 switch 分支里,剩下这批控件类型直接走 new 派生类并返回基类指针,逻辑和前面几类一致:传入图表 ID、子窗口号、描述文本和坐标宽高即可。 下面这段是分支表的收尾部分,覆盖了勾选框、单选钮、按钮、列表框及其变体,还有标签页与箭头按钮系列。注意 CListBoxItem 和 CCheckedListBox 虽都挂在列表概念下,但事件回调和命中区域差异明显,调试时别混用。 case GRAPH_ELEMENT_TYPE_WF_CHECKBOX: element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON: element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_BUTTON: element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX: element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM: element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX: element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX: element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER: element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD: element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL: element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON: element=new CArrowButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP: element=new CArrowUpButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN: 开 MT5 把这段贴进你的 CGraphElement 工厂,缺了 ARROW_BUTTON_DOWN 的 break 会编译报错,补上 new CArrowDownButton(...) 那行就能闭环。外汇与贵金属面板开发属高频改动场景,实盘前先在策略测试器里点一遍所有控件回调。
case GRAPH_ELEMENT_TYPE_WF_CHECKBOX: element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON: element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_BUTTON: element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX: element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM: element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX: element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX: element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER: element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD: element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL: element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON: element=new CArrowButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP: element=new CArrowUpButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN: