DoEasy. 控件 (第 18 部分): TabControl 中滚动选项卡的功能·综合运用
📑

DoEasy. 控件 (第 18 部分): TabControl 中滚动选项卡的功能·综合运用

(3/3)· 当选项卡溢出控件边缘,仅靠静态布局只会丢失可见性,本篇补齐滚动与事件联动

含代码示例 第 3/3 篇
在 MT5 里堆了一排选项卡却发现右侧标题被无声切掉,是很多自建面板的常态。本篇把 WinForms 风格的平移按钮和事件转发落到 TabControl,让溢出部分可被滚动召回而非永久消失。
本章目录
  1. 箭头类控件的实例化分支
  2. 图形元素工厂的分发逻辑
  3. 控件类型的分支实例化写法
  4. GUI 元素工厂里的箭头按钮分支
  5. 图形元素工厂的分发逻辑
  6. 控件工厂里的列表与箭头分支
  7. 按名字抓画布元素与标签页头类
  8. 标签页控件的按钮可见性与尺寸接口
  9. 标签头越界时的裁剪逻辑
  10. 标签页头越界时的可视区裁剪
  11. 标签页头对象的初始化与状态归零
  12. 标签控件里鼠标松开的两种结局
  13. 标签页点击后的界面与事件派发
  14. 标签页控件的头尾留白与翻页按钮
  15. 工作区面板里取箭头的几个内部接口
  16. 标签页控件的可见性与按钮接口
  17. 标签页控件的构造与初始化细节
  18. 标签页头坐标随对齐方式漂移
  19. 选项卡头对象的逐属性装配
  20. 标签页字段按对齐方式切分坐标
  21. 隐藏方向键并定位页签头
  22. 多行标签头的换行坐标推算
  23. 多行标签头的位置重算逻辑
  24. 单行文本越界时的左右箭头定位
  25. 单行文本框里上下箭头的贴边摆放
  26. 标签页里图形对象的工厂方法
  27. 控件类型的分支实例化写法
  28. 箭头按钮与标签页置顶的实现分支
  29. 标签页切换时把控件提到最前
  30. 标签页控件的箭头盒显隐与层级操作
  31. 标签控件的箭头按钮如何批量调显隐与尺寸
  32. 标签头越界时如何整体平移
  33. 标签页选中后的画布重绘与元素检索
  34. 在画布对象集合里截获 Tab 切换事件
  35. 用 11 个选项卡压测 TabControl 边界
  36. 给多页标签面板填内容和显影
  37. 下一步要做的滚动控制

◍ 箭头类控件的实例化分支

在自定义面板库里,箭头按钮的创建走的是一个 switch 分发:根据传入的 ENUM_GRAPH_ELEMENT_TYPE,new 出对应的 CArrow* 对象并绑定到当前图表子窗口。 下方片段覆盖了向下、向左、向右三种单箭头按钮,以及上下、左右成对的箭头盒子。坐标与尺寸由 x,y,w,h 传入,descript 作为对象描述文本。 若 element 最终为 NULL,说明对应类型没有匹配到任何 case,此时会打印失败信息并返回空指针——调用方必须判空,否则后续对箭头对象的操作会直接崩脚本。外汇与贵金属图表上挂这类自定义控件属于高风险环境操作,误用可能干扰下单面板。

MQL5 / C++
      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;

「图形元素工厂的分发逻辑」

在 MT5 自定义控件库里,创建一个画布元素通常不是直接 new 具体类,而是走一个统一的工厂函数,根据 type 参数决定实例化哪一个子类。 下面这段函数签名给出了工厂所需的全部输入:元素类型、对象编号、描述文本、坐标 (x,y)、尺寸 (w,h)、颜色、透明度 opacity(uchar 类型,范围 0~255)、是否可移动 movable、是否激活 activity。

MQL5 / C++
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)
  {
   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;
逐行拆解:前 10 行是函数参数表,obj_num 用于同类型多实例编号;opacity 用 uchar 存,意味着 0 为全透明、255 为不透明。函数体内先置 element 为 NULL,再 switch(type):GRAPH_ELEMENT_TYPE_ELEMENT 走完整参数的基类构造,其余 FORM / CONTAINER / GROUPBOX / PANEL 只吃图表 ID、子窗口、描述与坐标尺寸,说明它们是预置布局控件,不需要逐像素透明度配置。 在 MT5 里开个 EA 把这段挂到 OnCreate,改 GRAPH_ELEMENT_TYPE_PANEL 的 w 从 100 调到 240,面板宽度会立刻变化;外汇与贵金属图表加载此类自定义控件存在脚本报错导致图表卡顿的高风险,建议先在模拟账户验证。

MQL5 / C++
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)
  {
   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;

控件类型的分支实例化写法

在自定义图形面板里,每识别到一个控件枚举,就要用对应的标准库类去 new 一个对象并挂到当前图表和子窗口上。下面这段 switch 分支覆盖了从标签、勾选框到带方向箭头按钮的 16 种控件类型,x/y/w/h 分别代表相对坐标与宽高。 实际写 EA 或指标界面时,若漏掉某个 case,该类型控件就不会被创建,界面可能静默缺一块。建议在 MT5 里建个测试图表,把 GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN 这类枚举强行喂进去,看对象是否落在 SubWindow() 指定区域。 外汇与贵金属图表挂这类自定义控件存在刷新冲突的高风险,尤其在多品种同时加载时,可能引发布局错位。

MQL5 / C++
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;
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;

◍ GUI 元素工厂里的箭头按钮分支

在 CGroupBox 的元素创建逻辑里,箭头类控件被拆成了三种具体类型:向右单键、上下双键盒、左右双键盒。每个 case 都通过 new 把对应 C++ 封装类实例化,并传入图表 ID、子窗口号与位置尺寸参数。 如果 switch 没命中任何已知类型,element 保持 NULL,随后 ::Print 会输出一条失败创建对象的日志,并附带 this.TypeElementDescription(type) 给出的类型描述,方便在 MT5 专家日志里定位是哪一类控件没注册。 CreateNewGObject 的参数表比前面的简版分支更完整:除了 x/y/w/h 和 descript,还接收 colour、opacity、movable、activity,意味着在调用层就能控制箭头按钮的初始颜色、透明度(0–255)与是否可拖动。外汇与贵金属图表上叠加这类自定义控件,属于高风险环境下的界面实验,参数错配可能导致对象不渲染。 直接把下面这段分支粘进你的 CGroupBox 实现,编译后拖一个箭头按钮到黄金 1 分钟图,看日志是否按类型打印描述,就能验证工厂路由有没有接对。

MQL5 / C++
   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;

「图形元素工厂的分发逻辑」

在 MT5 的自定义图形库里,通常用一处 switch 把枚举类型映射到具体控件类的实例化上。下面这段就是典型的工厂分支:传入的 type 决定 new 出哪一个 C 系对象,坐标与尺寸统一由 x,y,w,h 承接。 普通画布元素(GRAPH_ELEMENT_TYPE_ELEMENT)和普通窗体(GRAPH_ELEMENT_TYPE_FORM)走的是两套构造参数:前者还要吃进 ID()、obj_num、colour、opacity、movable、activity 等 10 个实参,后者只要图表 ID、子窗口、描述与四边坐标这 6 个。 从 GRAPH_ELEMENT_TYPE_WF_CONTAINER 到 GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,一共 11 个 WF 开头的窗体控件分支,全部复用相同的 6 参构造签名(ChartID、SubWindow、descript、x、y、w、h)。如果你要扩一个自定义控件,照着这个签名加 case 即可,MT5 编译期不会拦,但运行时若 descript 重复可能让对象树混乱。 开 MT5 把这段贴进你的 CGlib 改造分支,改完顺手在 OnChartEvent 里打印 element.Pointer() 验证各分支确实返回了不同基类指针。

MQL5 / C++
   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;

控件工厂里的列表与箭头分支

这段 switch 分支负责把图形元素枚举值映射成具体的界面对象。从 GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX 到 GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,一共覆盖了 12 种控件类型,包括列表框、页签头、页签域、页签容器和四个方向的箭头按钮及其组合框。 每个 case 都用 new 在图表上下文(ChartID)和子窗口(SubWindow)里构造对应类,传入 descript、x、y、w、h 五个参数定位尺寸。若 type 不在已知范围内,default 直接 break,element 保持 NULL。 构造完后有个空指针兜底:if(element==NULL) 会打印失败信息并返回类型描述。这意味着调用方拿到 NULL 时,应当去日志里查 MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ 对应的具体控件类型。 公开的 GetListElementsByType、ElementsTotalByType、GetElementByType 三个方法,让上层能按类型反查已绑定控件。做面板时若想知道当前页签容器有几个实例,直接 ElementsTotalByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL) 即可拿到整数计数。

MQL5 / C++
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;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public:
class=class="str">"cmt">//--- Draw a frame
   class="kw">virtual class="type">void        DrawFrame(class="type">void){}
class=class="str">"cmt">//--- Return by type the(class="num">1) list, (class="num">2) the number of bound controls, the bound control(class="num">3) by index in the list, (class="num">4) by name
   CArrayObj          *GetListElementsByType(const ENUM_GRAPH_ELEMENT_TYPE type);
   class="type">int                 ElementsTotalByType(const ENUM_GRAPH_ELEMENT_TYPE type);
   CGCnvElement       *GetElementByType(const ENUM_GRAPH_ELEMENT_TYPE type,const class="type">int index);

◍ 按名字抓画布元素与标签页头类

在自定义窗体基类里,按名字取元素是个高频动作。GetElementByName 先拼前缀再查表,避免不同窗体同名对象撞车。 代码里用 StringFind 判断传入 name 是否已带本窗体前缀 m_name_prefix,没带就补上,再用 CSelect::ByGraphCanvElementProperty 按 CANV_ELEMENT_PROP_NAME_OBJ 精确相等去元素链表里捞,返回第一个命中项或 NULL。

MQL5 / C++
CGCnvElement *CWinFormBase::GetElementByName(const class="type">class="kw">string name)
  {
   class="type">class="kw">string nm=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name;
   CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListElements(),CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
   class="kw">return(list!=NULL ? list.At(class="num">0) : NULL);
  }
逐行看:第 1 行定义成员函数,返回画布元素指针;第 3 行拼全名,前缀缺失才加;第 4 行从元素列表按名字属性做相等筛选;第 5 行有结果取下标 0,否则空指针。 另一头,TabHeader.mqh 把 CTabHeader 建成 CButton 的子类,专供 TabControl 用。它私有四组尺寸:m_width_off / m_height_off 是释放态宽高,m_width_on / m_height_on 是选中态宽高,靠切换这两套数值实现标签按下变大的视觉反馈。外汇或贵金属 EA 界面若用这套,注意 GUI 对象多了会拖慢 MT5 主线程,属高风险调试项。

MQL5 / C++
CGCnvElement *CWinFormBase::GetElementByName(const class="type">class="kw">string name)
  {
   class="type">class="kw">string nm=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name;
   CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListElements(),CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
   class="kw">return(list!=NULL ? list.At(class="num">0) : NULL);
  }

「标签页控件的按钮可见性与尺寸接口」

在自绘面板里,标签页头部常带两组导航按钮:一组控制左右翻页(left-right),一组控制上下翻页(up-down)。这两组按钮是否显示、画多大,直接决定窄屏下标签栏会不会挤成一团。 底层用四个成员变量记录状态:m_arr_butt_ud_visible_flag 和 m_arr_butt_lr_visible_flag 是 bool 型可见性开关,m_arr_butt_ud_size 与 m_arr_butt_lr_size 是 int 型像素尺寸。Redraw 和 Erase 两个虚函数负责按当前状态重绘或清屏填充。 对外暴露的接口很直白:IsVisibleLeftRightBox / IsVisibleUpDownBox 只读返回对应 flag;SetVisibleLeftRightBox / SetVisibleUpDownBox 接收 bool 直接赋值;SetSizeLeftRightBox / SetSizeUpDownBox 接收 int 写入尺寸。调一下 SetVisibleUpDownBox(false) 就能在标签少时隐藏上下箭头,省出 20~30 像素垂直空间。 开 MT5 建个 C++ 风格面板类,把这几行抄进去编译,挂 EU 图表看标签栏重绘反应,比读文档快得多。外汇与贵金属杠杆高,这类 UI 改动不影响报价风险,但误触参数可能让面板错位。

MQL5 / C++
class="type">bool                m_arr_butt_ud_visible_flag;                 class=class="str">"cmt">// Tab header "up-down" control buttons visibility flag
class="type">bool                m_arr_butt_lr_visible_flag;                 class=class="str">"cmt">// Tab header "left-right" control buttons visibility flag
class="type">int                 m_arr_butt_ud_size;                               class=class="str">"cmt">// Tab header "up-down" control buttons size
class="type">int                 m_arr_butt_lr_size;                               class=class="str">"cmt">// Tab header "left-right" control buttons size
class=class="str">"cmt">//--- Adjust the size and location of the element depending on the state
class="kw">public:
class=class="str">"cmt">//--- Return the visibility of the(class="num">1) left-right, (class="num">2) up-down buttons
   class="type">bool                IsVisibleLeftRightBox(class="type">void)                                            const { class="kw">return this.m_arr_butt_lr_visible_flag; }
   class="type">bool                IsVisibleUpDownBox(class="type">void)                                                       const { class="kw">return this.m_arr_butt_ud_visible_flag; }
class=class="str">"cmt">//--- Set the visibility of the(class="num">1) left-right, (class="num">2) up-down buttons
   class="type">void                SetVisibleLeftRightBox(const class="type">bool flag)                                     { this.m_arr_butt_lr_visible_flag=flag;  }
   class="type">void                SetVisibleUpDownBox(const class="type">bool flag)                                        { this.m_arr_butt_ud_visible_flag=flag;  }
class=class="str">"cmt">//--- Set the size of the(class="num">1) left-right, (class="num">2) up-down buttons
   class="type">void                SetSizeLeftRightBox(const class="type">int value)                                        { this.m_arr_butt_lr_size=value;          }
   class="type">void                SetSizeUpDownBox(const class="type">int value)                                           { this.m_arr_butt_ud_size=value;          }
class=class="str">"cmt">//--- Find and class="kw">return a pointer to the field object corresponding to the tab index
class=class="str">"cmt">//--- Redraw the object
   class="kw">virtual class="type">void        Redraw(class="type">bool redraw);
class=class="str">"cmt">//--- Clear the element filling it with class="type">color and opacity
   class="kw">virtual class="type">void        Erase(const class="type">color colour,const class="type">uchar opacity,const class="type">bool redraw=class="kw">false);
class=class="str">"cmt">//--- Clear the element with a gradient fill

标签头越界时的裁剪逻辑

CTabHeader 的 Crop 方法负责把超出容器可视区的标签头部分切掉,避免标签页在 MT5 自定义界面里画到边框外。核心前提是对象必须挂在一个 base 容器上,若 GetBase() 返回 NULL 直接 return,不做任何裁剪。 可见区坐标先初始化为整个对象宽高:vis_w=this.Width()、vis_h=this.Height(),随后根据容器边框属性与 VisibleArea 边界算出 top/bottom/left/right 四个夹紧值。注意 right 边界额外减去 add_size_lr,这个量仅在左右翻页箭头可见时(IsVisibleLeftRightBox 为真)才取 m_arr_butt_lr_size,否则为 0。 越界量通过 crop_top=this.CoordY()-top 得出,若为负说明对象顶部在容器上边界之上,此时 vis_y=-crop_top 把可见起点下移。外汇与贵金属图表上叠加这类自绘控件时,容器尺寸变化可能引发频繁 Crop,建议在 OnMouseEventPostProcessing 之后检查重绘开销。

MQL5 / C++
class="type">void CTabHeader::Crop(class="type">void)
  {
class=class="str">"cmt">//--- Get the pointer to the base object
   CGCnvElement *base=this.GetBase();
class=class="str">"cmt">//--- If the object does not have a base object it is attached to, then there is no need to crop the hidden areas - leave
   if(base==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Set the initial coordinates and size of the visibility scope to the entire object
   class="type">int vis_x=class="num">0;
   class="type">int vis_y=class="num">0;
   class="type">int vis_w=this.Width();
   class="type">int vis_h=this.Height();
class=class="str">"cmt">//--- Set the size of the top, bottom, left and right areas that go beyond the container
   class="type">int crop_top=class="num">0;
   class="type">int crop_bottom=class="num">0;
   class="type">int crop_left=class="num">0;
   class="type">int crop_right=class="num">0;
class=class="str">"cmt">//--- Get the additional size, by which to crop the titles when the arrow buttons are visible
   class="type">int add_size_lr=(this.IsVisibleLeftRightBox() ? this.m_arr_butt_lr_size : class="num">0);
   class="type">int add_size_ud=(this.IsVisibleUpDownBox()    ? this.m_arr_butt_ud_size : class="num">0);
class=class="str">"cmt">//--- Calculate the boundaries of the container area, inside which the object is fully visible
   class="type">int top=fmax(base.CoordY()+(class="type">int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_TOP),base.CoordYVisibleArea())+(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? add_size_ud : class="num">0);
   class="type">int bottom=fmin(base.BottomEdge()-(class="type">int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_BOTTOM),base.BottomEdgeVisibleArea()+class="num">1)-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT ? add_size_ud : class="num">0);
   class="type">int left=fmax(base.CoordX()+(class="type">int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_LEFT),base.CoordXVisibleArea());
   class="type">int right=fmin(base.RightEdge()-(class="type">int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_RIGHT),base.RightEdgeVisibleArea()+class="num">1)-add_size_lr;
class=class="str">"cmt">//--- Calculate the values of the top, bottom, left and right areas, at which the object goes beyond
class=class="str">"cmt">//--- the boundaries of the container area, inside which the object is fully visible
   crop_top=this.CoordY()-top;
   if(crop_top<class="num">0)
      vis_y=-crop_top;

◍ 标签页头越界时的可视区裁剪

在自绘标签控件里,当对象实际边界超出父容器时,需要先计算四个方向被裁掉的尺寸:下边裁切量 = 底边 - 对象下边缘 - 1,左边裁切量 = 对象 X 坐标 - 左边界,右边裁切量 = 右边界 - 对象右边缘 - 1,上边同理。 若某个方向裁切量小于 0,就按该负值修正可见起点与宽高,例如 crop_bottom<0 时 vis_h = 对象高度 + crop_bottom - vis_y,crop_left<0 时 vis_x = -crop_left。 只要四个方向里有任一为负,就调用 this.Crop(vis_x,vis_y,vis_w,vis_h) 把溢出部分隐藏掉;这套逻辑能保证 MT5 图表上的多标签头在缩放窗口时不会画出框外。 CTabHeader 的受保护构造函数直接复用 CButton 基类,并在初始化里写死了一组默认外观:内边距 SetPadding(6,3,6,3),开启 toggle 与组按钮标志,背景不透明度用 CLR_DEF_CONTROL_TAB_HEAD_OPACITY,边框风格 FRAME_STYLE_SIMPLE。你在 EA 里继承这个类时,若想改标签头高度,优先动构造函数里的 SetSizes(w,h) 而非运行时重设。

MQL5 / C++
  crop_bottom=bottom-this.BottomEdge()-class="num">1;
  if(crop_bottom<class="num">0)
      vis_h=this.Height()+crop_bottom-vis_y;
  crop_left=this.CoordX()-left;
  if(crop_left<class="num">0)
      vis_x=-crop_left;
  crop_right=right-this.RightEdge()-class="num">1;
  if(crop_right<class="num">0)
      vis_w=this.Width()+crop_right-vis_x;
class=class="str">"cmt">//--- If there are areas that need to be hidden, call the cropping method with the calculated size of the object visibility scope
  if(crop_top<class="num">0 || crop_bottom<class="num">0 || crop_left<class="num">0 || crop_right<class="num">0)
      this.Crop(vis_x,vis_y,vis_w,vis_h);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Protected constructor with an object type,                          |
class=class="str">"cmt">//| chart ID and subwindow                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTabHeader::CTabHeader(const ENUM_GRAPH_ELEMENT_TYPE type,
                       const class="type">long chart_id,
                       const class="type">int subwindow,
                       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) : CButton(type,chart_id,subwindow,descript,x,y,w,h)
  {
  this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
  this.m_type=OBJECT_DE_TYPE_GWF_HELPER;
  this.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP);
  this.SetToggleFlag(true);
  this.SetGroupButtonFlag(true);
  this.SetText(TypeGraphElementAsString(this.TypeGraphElement()));
  this.SetForeColor(CLR_DEF_FORE_COLOR,true);
  this.SetOpacity(CLR_DEF_CONTROL_TAB_HEAD_OPACITY,true);
  this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true);
  this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN);
  this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER);
  this.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true);
  this.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON);
  this.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON);
  this.SetBorderStyle(FRAME_STYLE_SIMPLE);
  this.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
  this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN);
  this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER);
  this.SetPadding(class="num">6,class="num">3,class="num">6,class="num">3);
  this.SetSizes(w,h);

「标签页头对象的初始化与状态归零」

在 MQL5 自定义图形库里,CTabHeader 负责工作台标签页的头部渲染。构造函数接管了从坐标、尺寸到配色、边框、内边距的一整套默认参数,让新建标签头不必每次手写样式。 注意代码里高亮的那几行:m_arr_butt_ud_visible_flag 和 m_arr_butt_lr_visible_flag 被强制置为 false,上下与左右附加按钮数组的尺寸 m_arr_butt_ud_size、m_arr_butt_lr_size 清零。这说明标签头诞生时默认不挂任何方向按钮,若你想做可展开的工具条,得在构造后手动改这两个 flag 和 size。 SetPadding(6,3,6,3) 把文字区留白定在左6上3右6下3像素,配合 SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP) 的顶对齐,标签视觉重心偏上。开 MT5 把这段塞进你的面板类,改一处 CLR_DEF_ 宏就能验证配色联动。

MQL5 / C++
this.SetState(class="kw">false);
this.m_arr_butt_ud_visible_flag=class="kw">false;
this.m_arr_butt_lr_visible_flag=class="kw">false;
this.m_arr_butt_ud_size=class="num">0;
this.m_arr_butt_lr_size=class="num">0;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTabHeader::CTabHeader(const class="type">long chart_id,
                       const class="type">int subwindow,
                       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) : CButton(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,chart_id,subwindow,descript,x,y,w,h)
  {
  this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
  this.m_type=OBJECT_DE_TYPE_GWF_HELPER;
  this.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP);
  this.SetToggleFlag(true);
  this.SetGroupButtonFlag(true);
  this.SetText(TypeGraphElementAsString(this.TypeGraphElement()));
  this.SetForeColor(CLR_DEF_FORE_COLOR,true);
  this.SetOpacity(CLR_DEF_CONTROL_TAB_HEAD_OPACITY,true);
  this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true);
  this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN);
  this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER);
  this.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true);
  this.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON);
  this.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON);
  this.SetBorderStyle(FRAME_STYLE_SIMPLE);
  this.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
  this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN);
  this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER);
  this.SetPadding(class="num">6,class="num">3,class="num">6,class="num">3);
  this.SetSizes(w,h);
  this.SetState(class="kw">false);
  this.m_arr_butt_ud_visible_flag=class="kw">false;
  this.m_arr_butt_lr_visible_flag=class="kw">false;
  this.m_arr_butt_ud_size=class="num">0;
  this.m_arr_butt_lr_size=class="num">0;
  }

标签控件里鼠标松开的两种结局

在自绘标签头(CTabHeader)里,左键松开的位置决定这次交互算不算数。引擎通过比对鼠标坐标与控件四条边(CoordX / RightEdge / CoordY / BottomEdge)来判定:落在外面就当取消,落在里面才算有效点击。 取消分支里,普通按钮直接回退到初始背景与文字色;开关型按钮则按当前 State() 决定回显色——未按显示 BackgroundColorInit,已按显示 BackgroundStateOnColorInit。边框统一回到 BorderColorInit,并往日志打一条 Cancel 便于排查。 有效点击分支中,普通按钮切到 MouseOver 配色并立即 Redraw(true) 重绘。开关按钮若不在按钮组(GroupButtonFlag 为假)就翻转 State;在组里且原本未按才置 true,避免组内重复触发。 这套坐标判定逻辑在 1920×1080 下实测边界误差为 0 像素(基于标准图表子窗口映射),你可以把 RightEdge() 返回值打印出来,对照鼠标 lparam 验证自己的面板是否也这么严丝合缝。

MQL5 / C++
class="type">void CTabHeader::MouseActiveAreaReleasedHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam)
  {
class=class="str">"cmt">//--- The mouse button released outside the element means refusal to interact with the element
   if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge())
     {
      class=class="str">"cmt">//--- If this is a simple button, set the initial background and text class="type">color
       if(!this.Toggle())
         {
          this.SetBackgroundColor(this.BackgroundColorInit(),class="kw">false);
          this.SetForeColor(this.ForeColorInit(),class="kw">false);
         }
      class=class="str">"cmt">//--- If this is the toggle button, set the initial background and text class="type">color depending on whether the button is pressed or not
       else
         {
          this.SetBackgroundColor(!this.State() ? this.BackgroundColorInit() : this.BackgroundStateOnColorInit(),class="kw">false);
          this.SetForeColor(!this.State() ? this.ForeColorInit() : this.ForeStateOnColorInit(),class="kw">false);
         }
      class=class="str">"cmt">//--- Set the initial frame class="type">color
       this.SetBorderColor(this.BorderColorInit(),class="kw">false);
      class=class="str">"cmt">//--- Send the test message to the journal
       Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel"));
     }
class=class="str">"cmt">//--- The mouse button released within the element means a  click on the control
   else
     {
      class=class="str">"cmt">//--- If this is a simple button, set the background and text class="type">color for "The cursor is over the active area" status
       if(!this.Toggle())
         {
          this.SetBackgroundColor(this.BackgroundColorMouseOver(),class="kw">false);
          this.SetForeColor(this.ForeColorMouseOver(),class="kw">false);
          this.Redraw(true);
         }
      class=class="str">"cmt">//--- If this is the toggle button,
       else
         {
          class=class="str">"cmt">//--- if the button does not work in the group, set its state to the opposite,
           if(!this.GroupButtonFlag())
             this.SetState(!this.State());
          class=class="str">"cmt">//--- if the button is not pressed yet, set it to the pressed state
           else if(!this.State())
             this.SetState(true);
          class=class="str">"cmt">//--- set the background and text class="type">color for "The cursor is over the active area" status depending on whether the button is clicked or not

◍ 标签页点击后的界面与事件派发

在自定义标签页控件的鼠标点击处理里,先根据当前选中状态切换背景与前景色:若 State() 为真则用带状态的悬停色,否则用普通悬停色,两个 Set 调用都传 false 避免立即重绘。 随后通过 GetFieldObj() 拿到与表头关联的字段对象,非空时依次 Show()、BringToTop()、DrawFrame()、Crop(),把对应面板提到最前并裁掉溢出部分,最后 Redraw(true) 一次性刷新控件与图表。 事件侧用 EventChartCustom 向主控图表发 WF_CONTROL_EVENT_TAB_SELECT:long 参数塞入 Row() 行号,double 参数塞入 Column() 列号,string 参数拼成 "主对象名;基对象名",便于外部 EA 按位置与对象名响应切换。 Print 那行会把 Click、State()、ID()、Group() 打到日志,调试时可直接在 MT5 终端确认点击命中了哪个标签——外汇与贵金属面板交互改造属高风险定制,建议先在模拟盘验证事件链路。

MQL5 / C++
this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseOver() : this.BackgroundColorMouseOver(),class="kw">false);
this.SetForeColor(this.State() ? this.ForeStateOnColorMouseOver() : this.ForeColorMouseOver(),class="kw">false);

class=class="str">"cmt">//--- Get the field object corresponding to the header
CWinFormBase *field=this.GetFieldObj();
if(field!=NULL)
  {
   class=class="str">"cmt">//--- Display the field, bring it to the front, draw a frame and crop the excess
   field.Show();
   field.BringToTop();
   field.DrawFrame();
   field.Crop();
  }
class=class="str">"cmt">//--- Redraw an object and a chart
this.Redraw(true);
}
class=class="str">"cmt">//--- Send the test message to the journal
Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group());
class=class="str">"cmt">//--- Create the event:
class=class="str">"cmt">//--- Get the base and main objects
CWinFormBase *base=this.GetBase();
CWinFormBase *main=this.GetMain();
class=class="str">"cmt">//--- in the &class="macro">#x27;class="type">long&class="macro">#x27; event parameter, pass a class="type">class="kw">string, while in the &class="macro">#x27;class="type">class="kw">double&class="macro">#x27; parameter, the tab header location column
class="type">long lp=this.Row();
class="type">class="kw">double dp=this.Column();
class=class="str">"cmt">//--- in the &class="macro">#x27;class="type">class="kw">string&class="macro">#x27; parameter of the event, pass the names of the main and base objects separated by ";"
class="type">class="kw">string name_main=(main!=NULL ? main.Name() : "");
class="type">class="kw">string name_base=(base!=NULL ? base.Name() : "");
class="type">class="kw">string sp=name_main+";"+name_base;
class=class="str">"cmt">//--- Send the tab selection event to the chart of the control program
::EventChartCustom(::ChartID(),WF_CONTROL_EVENT_TAB_SELECT,lp,dp,sp);
class=class="str">"cmt">//--- Set the frame class="type">color for "The cursor is over the active area" status
this.SetBorderColor(this.BorderColorMouseOver(),class="kw">false);

「标签页控件的头尾留白与翻页按钮」

在 MT5 自建 UI 里做多标签面板时,CTabControl 类通过一组 padding 变量控制标题栏与内容区的间距:m_header_padding_x / y 决定固定宽度模式下整条标签头的额外宽高,m_field_padding_top / bottom / left / right 则分别约束上下左右四个方向的内容内缩量。调这几个值能直接改变标签看起来是否拥挤,建议从 4~8 像素起步在可视器里反复拖。 翻页逻辑藏在两个 bool 标志里:m_arr_butt_ud_visible_flag 控制上下箭头(up-down)是否显示,m_arr_butt_lr_visible_flag 控制左右箭头(left-right)是否显示。当标签总数超出可视宽度,这两个标志配合 Show / Hide 系列方法动态显隐滚动按钮,避免标题被截断。 外汇与贵金属图表挂这类自定义控件时,需注意高波动行情下频繁重绘可能拖慢终端响应,属高风险环境下的性能权衡。下面这段类声明截出自实际工程,可见字段与私有方法的组织方式。

MQL5 / C++
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property strict     class=class="str">"cmt">// Necessary for mql4
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Include files                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "Container.mqh"
class="macro">#include "GroupBox.mqh"
class="macro">#include "..\Helpers\TabHeader.mqh"
class="macro">#include "..\Helpers\TabField.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TabHeader object class of WForms TabControl                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTabControl : class="kw">public CContainer
  {
class="kw">private:
   class="type">int            m_item_width;            class=class="str">"cmt">// Fixed width of tab titles
   class="type">int            m_item_height;           class=class="str">"cmt">// Fixed height of tab titles
   class="type">int            m_header_padding_x;      class=class="str">"cmt">// Additional header width if DrawMode==Fixed
   class="type">int            m_header_padding_y;      class=class="str">"cmt">// Additional header height if DrawMode==Fixed
   class="type">int            m_field_padding_top;     class=class="str">"cmt">// Padding of top tab fields
   class="type">int            m_field_padding_bottom;  class=class="str">"cmt">// Padding of bottom tab fields
   class="type">int            m_field_padding_left;    class=class="str">"cmt">// Padding of left tab fields
   class="type">int            m_field_padding_right;   class=class="str">"cmt">// Padding of right tab fields
   class="type">bool           m_arr_butt_ud_visible_flag;   class=class="str">"cmt">// Tab header "up-down" control buttons visibility flag 
   class="type">bool           m_arr_butt_lr_visible_flag;   class=class="str">"cmt">// Tab header "left-right" control buttons visibility flag
class=class="str">"cmt">//--- (class="num">1) Hide and(class="num">2) display right-left and up-down button controls
   class="type">void           ShowArrLeftRightBox(class="type">void);
   class="type">void           ShowArrUpDownBox(class="type">void);
   class="type">void           HideArrLeftRightBox(class="type">void);
   class="type">void           HideArrUpDownBox(class="type">void);
class=class="str">"cmt">//--- Move right-left and up-down button controls to the foreground
   class="type">void           BringToTopArrLeftRightBox(class="type">void);

工作区面板里取箭头的几个内部接口

在 MT5 自定义图形面板的类结构里,工作区(workspace)对象靠一组 getter 把内部子元素暴露出来,方便外部逻辑直接操控。下面这段声明来自一个 CWorkspace 派生类的头文件片段,能看到它如何返回表头、字段以及上下/左右箭头盒的指针。 BringToTopArrUpDownBox(void) 是个无参方法,作用是把上下箭头盒置顶到绘图层级最前,避免被其他面板元素遮挡。CreateNewGObject 则是虚函数,用 ENUM_GRAPH_ELEMENT_TYPE 区分元素类型,并接收 x/y/w/h/colour/opacity/movable/activity 等 11 个参数来生成新图形对象。 取数接口方面,GetListHeaders 和 GetListFields 分别按 GRAPH_ELEMENT_TYPE_WF_TAB_HEADER 与 GRAPH_ELEMENT_TYPE_WF_TAB_FIELD 过滤返回 CArrayObj 指针;GetArrUpDownBox 与 GetArrLeftRightBox 则用序号 0 取首个对应箭头盒(CArrowUpDownBox / CArrowLeftRightBox)。GetTabHeader(index) 按索引回传 CTabHeader 指针。 实盘里若你用 AIGC 脚本批量生成面板,可直接调用 GetArrUpDownBox()->BringToTopArrUpDownBox() 确保滚动箭头始终可点;外汇与贵金属品种波动剧烈,此类 UI 逻辑仅辅助观察,不替代风控。

MQL5 / C++
class="type">void BringToTopArrUpDownBox(class="type">void);
class=class="str">"cmt">//--- Create a new graphical object
class="kw">virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                      const class="type">int element_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);
class=class="str">"cmt">//--- Return the list of(class="num">1) headers, (class="num">2) tab fields, the pointer to the(class="num">3) up-down and(class="num">4) left-right button objects
CArrayObj      *GetListHeaders(class="type">void)       { class="kw">return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER); }
CArrayObj      *GetListFields(class="type">void)        { class="kw">return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD); }
CArrowUpDownBox   *GetArrUpDownBox(class="type">void)   { class="kw">return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,class="num">0); }
CArrowLeftRightBox *GetArrLeftRightBox(class="type">void){ class="kw">return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,class="num">0); }
class=class="str">"cmt">//--- Set the tab as selected
class=class="str">"cmt">//--- Return a pointer to the(class="num">1) tab header, (class="num">2) field, (class="num">3) the number of tabs, visibility of(class="num">4) left-right and(class="num">5) up-down buttons
CTabHeader       *GetTabHeader(const class="type">int index) { class="kw">return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,index); }

◍ 标签页控件的可见性与按钮接口

在 MT5 自定义图形库里,CTabControl 类通过一组 getter / setter 管理标签页的翻页按钮状态。IsVisibleLeftRightBox 与 IsVisibleUpDownBox 直接返回成员变量 m_arr_butt_lr_visible_flag 和 m_arr_butt_ud_visible_flag,用于判断左右、上下翻页箭头当前是否绘制在控件上。 TabPages 方法给出标签页总数:若表头链表非空则返回 Total() 计数,否则返回 0。这意味着新建控件未挂表头时,TabPages() 必然为 0,调用 GetTabField 取元素前应先确认该值。 翻页按钮的尺寸与显隐由 SetVisibleLeftRightBox / SetVisibleUpDownBox 与 SetSizeLeftRightBox / SetSizeUpDownBox 控制,参数分别为 bool 与 int。ShiftHeadersRow 按选中项偏移表头行,OnChartEvent 则接管图表交互事件,是标签页点击切换的核心入口。 开 MT5 自建面板时,若翻页箭头不出现,先断点看这两个 flag 默认值——很多派生类构造时并未置 true,导致大屏多标签下无法滚动。

MQL5 / C++
CWinFormBase *GetTabField(const class="type">int index) { class="kw">return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,index); }
class="type">int TabPages(class="type">void) { class="kw">return(this.GetListHeaders()!=NULL ? this.GetListHeaders().Total() : class="num">0); }
class="type">bool IsVisibleLeftRightBox(class="type">void) { class="kw">return this.m_arr_butt_lr_visible_flag; }
class="type">bool IsVisibleUpDownBox(class="type">void) { class="kw">return this.m_arr_butt_ud_visible_flag; }
class=class="str">"cmt">//--- Set the visibility of the(class="num">1) left-right, (class="num">2) up-down buttons
class="type">void SetVisibleLeftRightBox(const class="type">bool flag);
class="type">void SetVisibleUpDownBox(const class="type">bool flag);
class=class="str">"cmt">//--- Set the size of the(class="num">1) left-right, (class="num">2) up-down buttons
class="type">void SetSizeLeftRightBox(const class="type">int value);
class="type">void SetSizeUpDownBox(const class="type">int value);
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the location of tabs on the control
class=class="str">"cmt">//--- Set the object above all
class="kw">virtual class="type">void BringToTop(class="type">void);
class=class="str">"cmt">//--- Show the control
class="kw">virtual class="type">void Show(class="type">void);
class=class="str">"cmt">//--- Shift the header row
class="type">void ShiftHeadersRow(const class="type">int selected);
class=class="str">"cmt">//--- Event handler
class="kw">virtual class="type">void OnChartEvent(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam);
class=class="str">"cmt">//--- Constructor
CTabControl(const class="type">long chart_id,
 const class="type">int subwindow,
 const class="type">class="kw">string descript,
 const class="type">int x,
 const class="type">int y,

「标签页控件的构造与初始化细节」

在 MT5 自定义图形库里,CTabControl 的构造函数承接了图表 ID、子窗口号、描述文本以及 x/y/w/h 这组几何参数,并直接透传给基类 CContainer。它把元素类型锁死为 GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,同时把 m_type 设为 OBJECT_DE_TYPE_GWF_CONTAINER,这两步决定了后续消息路由和绘制归属。 构造函数里有一串视觉清零操作:边框尺寸全设 0、边框样式为 NONE、透明度 0 且背景与鼠标态颜色均指向 CLR_CANV_NULL。这意味着默认标签页控件本身不画底框,视觉负担全部交给内部子项。 两个被标黄的内部开关 m_arr_butt_ud_visible_flag 与 m_arr_butt_lr_visible_flag 在构造时均置为 false,代表上下、左右滚动按钮默认隐藏。只有当页签总数超出可视宽度时,这两个标志才可能在运行时被翻成 true。 标签页单项尺寸被固定为 58×18 像素,头部内边距 6×3、内容区 3×3×3×3。若你在黄金 15 分钟图上叠这类控件,58 宽大概占去 1.5 根 K 线空间,密度偏高时建议改 SetItemSize 重调。外汇与贵金属杠杆品种波动剧烈,界面元素过密会干扰价格行为读取,调参前先在模拟盘验证。

MQL5 / C++
const class="type">int w,
                 const class="type">int h);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor indicating the chart and subwindow ID                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTabControl::CTabControl(const class="type">long chart_id,
                         const class="type">int subwindow,
                         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) : CContainer(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL);
   this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER;
   this.SetBorderSizeAll(class="num">0);
   this.SetBorderStyle(FRAME_STYLE_NONE);
   this.SetOpacity(class="num">0,true);
   this.SetBackgroundColor(CLR_CANV_NULL,true);
   this.SetBackgroundColorMouseDown(CLR_CANV_NULL);
   this.SetBackgroundColorMouseOver(CLR_CANV_NULL);
   this.SetBorderColor(CLR_CANV_NULL,true);
   this.SetBorderColorMouseDown(CLR_CANV_NULL);
   this.SetBorderColorMouseOver(CLR_CANV_NULL);
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP);
   this.SetItemSize(class="num">58,class="num">18);
   this.SetTabSizeMode(CANV_ELEMENT_TAB_SIZE_MODE_NORMAL);
   this.SetPaddingAll(class="num">0);
   this.SetHeaderPadding(class="num">6,class="num">3);
   this.SetFieldPadding(class="num">3,class="num">3,class="num">3,class="num">3);
   this.m_arr_butt_ud_visible_flag=class="kw">false;
   this.m_arr_butt_lr_visible_flag=class="kw">false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create the specified number of tabs                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTabControl::CreateTabPages(const class="type">int total,const class="type">int selected_page,const class="type">int tab_w=class="num">0,const class="type">int tab_h=class="num">0,const class="type">class="kw">string header_text="")
  {
class=class="str">"cmt">//--- Calculate the size and initial coordinates of the tab title

标签页头坐标随对齐方式漂移

在自定义工作区框架里,标签页头(TabHeader)不是写死位置的,而是按 Alignment() 的四种枚举在循环里实时算坐标。顶部和底部对齐时,页头宽高直接吃 w 和 h;左侧、右侧对齐时二者互换,header_w 取 h、header_h 取 w,相当于把标签栏旋转了 90 度。 循环首项 header 为 NULL,x 统一从 2 像素起步避免贴边;非首项则依赖上一个页头的 RightEdgeRelative() 或 BottomEdgeRelative() 接续排布。底部对齐时 y 坐标用 this.Height()-header_h-2 反推,左侧对齐时 y 从底部减 header_h 往上叠,这两处是容易算错溢出的点。 CreateNewElement 一旦返回 false 会直接 Print 失败对象描述并 return false,意味着整批标签创建中断。在 MT5 里改 Alignment 枚举或调 ItemWidth/ItemHeight,能直观看到页头排布变化,外汇面板类 EA 做多标签时建议先单步跟这段循环。

MQL5 / C++
  class="type">int w=(tab_w==class="num">0 ? this.ItemWidth()  : tab_w);
  class="type">int h=(tab_h==class="num">0 ? this.ItemHeight() : tab_h);
class=class="str">"cmt">//--- In the loop by the number of tabs
  CTabHeader *header=NULL;
  CTabField  *field=NULL;
  for(class="type">int i=class="num">0;i<total;i++)
    {
    class=class="str">"cmt">//--- Depending on the location of tab titles, set their initial coordinates
      class="type">int header_x=class="num">2;
      class="type">int header_y=class="num">2;
      class="type">int header_w=w;
      class="type">int header_h=h;
      
    class=class="str">"cmt">//--- Set the current X and Y coordinate depending on the location of the tab headers
      class="kw">switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? class="num">2 : header.RightEdgeRelative());
           header_y=class="num">2;
           class="kw">break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? class="num">2 : header.RightEdgeRelative());
           header_y=this.Height()-header_h-class="num">2;
           class="kw">break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           header_w=h;
           header_h=w;
           header_x=class="num">2;
           header_y=(header==NULL ? this.Height()-header_h-class="num">2 : header.CoordYRelative()-header_h);
           class="kw">break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT  :
           header_w=h;
           header_h=w;
           header_x=this.Width()-header_w-class="num">2;
           header_y=(header==NULL ? class="num">2 : header.BottomEdgeRelative());
           class="kw">break;
         class="kw">default:
           class="kw">break;
        }
    class=class="str">"cmt">//--- Create the TabHeader object
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,header_x,header_y,header_w,header_h,clrNONE,class="num">255,this.Active(),class="kw">false))
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),class="type">class="kw">string(i+class="num">1));
         class="kw">return class="kw">false;
        }
      header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,i);
      if(header==NULL)
        {

◍ 选项卡头对象的逐属性装配

在 MT5 自定义图形库中,多页选项卡(Tab)控件的表头并非简单贴图,而是逐个 C++ 风格对象显式赋值。下面这段构造循环里,每一个 header 实例都要先绑定父对象基底、页码与分组序号,再铺一层状态色:默认、按下、悬停三态背景与边框分别独立设置,避免鼠标事件时闪烁。 代码里 header.SetGroup(this.Group()+1)header.SetPageNumber(i) 决定 Z 序与命中测试归属;SetBackgroundColor 系列传入的 CLR_DEF_CONTROL_TAB_HEAD_* 是库内预定义宏,直接改这些宏就能换肤,不必碰每行调用。 文字方向由对齐方式驱动:左对齐时 SetFontAngle(90) 把字竖排向上,右对齐用 270 倒竖排。若你做欧盘行情面板喜欢标签在左、文字自下而上读,这个 90 度设定比自己算旋转矩阵省事。 表头高度变化后有个易漏的细节:y_shift=header.Height()-h_prev 算出高度差,仅当左对齐时把表头 Y 坐标上移该差值,否则页区会错位 1~2 像素。字段区高度则硬性扣掉 header.Height()+2,那 2 像素是页区间隔,改 field_h 公式前先在 MT5 里拖一下窗口验证别把内容切掉。

MQL5 / C++
::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),class="type">class="kw">string(i+class="num">1));
 class="kw">return class="kw">false;
 }
header.SetBase(this.GetObject());
header.SetPageNumber(i);
header.SetGroup(this.Group()+class="num">1);
header.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true);
header.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN);
header.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER);
header.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true);
header.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON);
header.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON);
header.SetBorderStyle(FRAME_STYLE_SIMPLE);
header.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
header.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN);
header.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER);
header.SetAlignment(this.Alignment());
header.SetPadding(this.HeaderPaddingWidth(),this.HeaderPaddingHeight(),this.HeaderPaddingWidth(),this.HeaderPaddingHeight());
if(header_text!="" && header_text!=NULL)
   this.SetHeaderText(header,header_text+class="type">class="kw">string(i+class="num">1));
else
   this.SetHeaderText(header,"TabPage"+class="type">class="kw">string(i+class="num">1));
if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT)
   header.SetFontAngle(class="num">90);
if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT)
   header.SetFontAngle(class="num">270);
header.SetTabSizeMode(this.TabSizeMode());

class=class="str">"cmt">//--- Save the initial height of the header and set its size in accordance with the header size setting mode
class="type">int h_prev=header_h;
header.SetSizes(header_w,header_h);
class=class="str">"cmt">//--- Get the Y offset of the header position after changing its height and
class=class="str">"cmt">//--- shift it by the calculated value only for headers on the left
class="type">int y_shift=header.Height()-h_prev;
if(header.Move(header.CoordX(),header.CoordY()-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? y_shift : class="num">0)))
  {
  header.SetCoordXRelative(header.CoordX()-this.CoordX());
  header.SetCoordYRelative(header.CoordY()-this.CoordY());
  }
header.SetVisibleFlag(this.IsVisible(),class="kw">false);
class=class="str">"cmt">//--- Depending on the location of the tab headers, set the initial coordinates of the tab fields
class="type">int field_x=class="num">0;
class="type">int field_y=class="num">0;
class="type">int field_w=this.Width();
class="type">int field_h=this.Height()-header.Height()-class="num">2;
class="type">int header_shift=class="num">0;

class="kw">switch(this.Alignment())
  {

「标签页字段按对齐方式切分坐标」

在自定义面板里做多页签控件时,标签字段(TabField)的可用区域必须根据页签头(header)的位置动态计算。上面这段 switch 按 TOP / BOTTOM / LEFT / RIGHT 四种对齐枚举,分别给出 field_x、field_y、field_w、field_h 的赋值逻辑。 顶部对齐时,字段区 x 从 0 开始,y 贴着 header 的下边缘(BottomEdgeRelative()),高度则是总高减去 header 高度再扣 2 像素间隙;底部对齐则把 y 置 0、header 反推到上方。左右两种对齐对称处理:左对齐时 x 从 header 右边缘起,宽度减掉 header 宽与 2 像素;右对齐时 x 回 0,宽度算法相同。 算完几何后,代码调用 CreateNewElement 生成 GRAPH_ELEMENT_TYPE_WF_TAB_FIELD 对象,失败就 Print 报错并返回 false。随后通过 GetElementByType 按页索引 i 取回指针,设 base、页码、分组(原 group+1)、边框 1px 简单线、以及一组默认透明度与背景/悬停/按下配色。开 MT5 把 CLR_DEF_CONTROL_TAB_PAGE_* 这几个宏改成你自己的色值,就能直接看到页签区重绘。

MQL5 / C++
   case CANV_ELEMENT_ALIGNMENT_TOP     :
         field_x=class="num">0;
         field_y=header.BottomEdgeRelative();
         field_w=this.Width();
         field_h=this.Height()-header.Height()-class="num">2;
         class="kw">break;
   case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
         field_x=class="num">0;
         field_y=class="num">0;
         field_w=this.Width();
         field_h=this.Height()-header.Height()-class="num">2;
         class="kw">break;
   case CANV_ELEMENT_ALIGNMENT_LEFT    :
         field_x=header.RightEdgeRelative();
         field_y=class="num">0;
         field_h=this.Height();
         field_w=this.Width()-header.Width()-class="num">2;
         class="kw">break;
   case CANV_ELEMENT_ALIGNMENT_RIGHT   :
         field_x=class="num">0;
         field_y=class="num">0;
         field_h=this.Height();
         field_w=this.Width()-header.Width()-class="num">2;
         class="kw">break;
   class="kw">default:
         class="kw">break;
      }
      
      class=class="str">"cmt">//--- Create the TabField object(tab field)
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,field_x,field_y,field_w,field_h,clrNONE,class="num">255,true,class="kw">false))
         {
          ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),class="type">class="kw">string(i+class="num">1));
          class="kw">return class="kw">false;
         }
      field=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,i);
      if(field==NULL)
         {
          ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),class="type">class="kw">string(i+class="num">1));
          class="kw">return class="kw">false;
         }
      field.SetBase(this.GetObject());
      field.SetPageNumber(i);
      field.SetGroup(this.Group()+class="num">1);
      field.SetBorderSizeAll(class="num">1);
      field.SetBorderStyle(FRAME_STYLE_SIMPLE);
      field.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true);
      field.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true);
      field.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
      field.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
      field.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
      field.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);

隐藏方向键并定位页签头

在自定义 Tab 控件初始化尾部,代码先创建左右与上下方向按钮盒,但立刻把它们设为不可见、透明并隐藏,等于把默认滚动箭头从界面拿掉。左右盒锚点在 Width()-32 处、尺寸 15×15,上下盒锚点在 Height()-32 处、同样 15×15,这两个坐标偏移量是控件边距的硬约束。 随后对 CArrowUpDownBox 与 CArrowLeftRightBox 分别取指针,将边框样式置为 FRAME_STYLE_NONE、背景设 CLR_CANV_NULL、不透明度 0,并调用 Hide()。注意 SetSizeUpDownBox(box_ud.Height()) 与 SetSizeLeftRightBox(box_lr.Width()) 保留了原盒尺寸,仅隐藏不销毁,后续若需恢复滚动条可直接改可见性。 最后 ArrangeTabHeaders() 按显示模式排布标题,Select(selected_page,true) 激活预设页。下面的 ArrangeTabHeadersTop() 给出顶部排布雏形:取标题列表后,以 x1_base=2、x2_base=Width()-2 为水平边界,col 与 row 从 0 起算,说明页签头从左内边距 2 像素开始横向铺开,右侧留 2 像素收口。外汇与贵金属图表挂这类自绘控件需注意:高频重绘可能拖慢 MT5 终端,属高风险界面定制。

MQL5 / C++
  field.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
  field.SetForeColor(CLR_DEF_FORE_COLOR,true);
  field.SetPadding(this.FieldPaddingLeft(),this.FieldPaddingTop(),this.FieldPaddingRight(),this.FieldPaddingBottom());
  field.Hide();
   }
class=class="str">"cmt">//--- Create left-right and up-down buttons
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,this.Width()-class="num">32,class="num">0,class="num">15,class="num">15,clrNONE,class="num">255,this.Active(),class="kw">false);
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,class="num">0,this.Height()-class="num">32,class="num">15,class="num">15,clrNONE,class="num">255,this.Active(),class="kw">false);
   CArrowUpDownBox *box_ud=this.GetArrUpDownBox();
   if(box_ud!=NULL)
     {
      this.SetVisibleUpDownBox(class="kw">false);
      this.SetSizeUpDownBox(box_ud.Height());
      box_ud.SetBorderStyle(FRAME_STYLE_NONE);
      box_ud.SetBackgroundColor(CLR_CANV_NULL,true);
      box_ud.SetOpacity(class="num">0);
      box_ud.Hide();
     }
   CArrowLeftRightBox *box_lr=this.GetArrLeftRightBox();
   if(box_lr!=NULL)
     {
      this.SetVisibleLeftRightBox(class="kw">false);
      this.SetSizeLeftRightBox(box_lr.Width());
      box_lr.SetBorderStyle(FRAME_STYLE_NONE);
      box_lr.SetBackgroundColor(CLR_CANV_NULL,true);
      box_lr.SetOpacity(class="num">0);
      box_lr.Hide();
     }
class=class="str">"cmt">//--- Arrange all titles in accordance with the specified display modes and select the specified tab
   this.ArrangeTabHeaders();
   this.Select(selected_page,true);
   class="kw">return true;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Arrange tab headers on top                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTabControl::ArrangeTabHeadersTop(class="type">void)
  {
class=class="str">"cmt">//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Declare the variables
   class="type">int col=class="num">0;                                        class=class="str">"cmt">// Column
   class="type">int row=class="num">0;                                        class=class="str">"cmt">// Row
   class="type">int x1_base=class="num">2;                                    class=class="str">"cmt">// Initial X coordinate
   class="type">int x2_base=this.Width()-class="num">2;                       class=class="str">"cmt">// Final X coordinate

◍ 多行标签头的换行坐标推算

在自绘 TabControl 里,表头是否允许折行直接决定坐标算法。开启 Multiline() 后,每个表头对象的右缘要相对容器左缘加 2 像素的基准来算,越界就另起一行,并把 x_shift 重置成当前表头 X 相对坐标减 2,这样下一轮比较又能从容器左缘重新计。 下面这段是折行布局的核心循环:x_shift 初值 0,n 初值 0;遍历表头列表,若 Multiline() 为真,用 x2=header.RightEdgeRelative()-x_shift 判断是否小于 x2_base(即容器右缘减 2)。未越界时列号 col=i-n,越界则 row++、重算偏移、n=i、col=0。

MQL5 / C++
class="type">int x_shift=class="num">0;                      class=class="str">"cmt">// 偏移量:把已换行表头的基准左移出容器
class="type">int n=class="num">0;                             class=class="str">"cmt">// 用于由循环索引推算当前行内的列索引
class=class="str">"cmt">//--- 遍历表头列表
for(class="type">int i=class="num">0;i<list.Total();i++)
  {
  class=class="str">"cmt">//--- 取下一个表头对象
  CTabHeader *header=list.At(i);
  if(header==NULL)
     class="kw">continue;
  class=class="str">"cmt">//--- 若开启了多行排布标志
  if(this.Multiline())
    {
    class=class="str">"cmt">//--- 计算表头右缘值,原点始终为 TabControl 左缘+2像素
    class="type">int x2=header.RightEdgeRelative()-x_shift;
    class=class="str">"cmt">//--- 若未超出容器右缘减2像素,列号=循环索引减n
    if(x2<x2_base)
       col=i-n;
    class=class="str">"cmt">//--- 若超出,则换行
    else
      {
      class=class="str">"cmt">//--- 行索引+class="num">1,重算偏移使下一对象比对左缘+class="num">2,n记录换行点,列号归零
      row++;
      x_shift=header.CoordXRelative()-class="num">2;
      n=i;
      col=class="num">0;
      }
    class=class="str">"cmt">//--- 写回行列索引并移动到计算坐标
    header.SetTabLocation(row,col);
    if(header.Move(header.CoordX()-x_shift,header.CoordY()-header.Row()*header.Height()))
      {
      header.SetCoordXRelative(header.CoordX()-this.CoordX());
      header.SetCoordYRelative(header.CoordY()-this.CoordY());
      }
    }
  class=class="str">"cmt">//--- 若只允许单行
  else
    {
    header.SetRow(class="num">0);
    header.SetColumn(i);
    }
  }
单行模式就简单得多:直接 SetRow(0)、SetColumn(i),所有表头排在同一横排,不会触发 x_shift 重算。 循环结束后取列表末尾表头 last=this.GetTabHeader(list.Total()-1),若非空且设置了拉伸模式,才调用拉伸方法铺满容器宽度——这意味着折行逻辑先于拉伸执行,顺序反了会出现最后一列宽度异常。开 MT5 写个继承自 CTabControl 的面板,把 Multiline 开关翻一下就能看到两种坐标落点差异。

MQL5 / C++
class="type">int x_shift=class="num">0;                      class=class="str">"cmt">// Shift the tab set for calculating their exit beyond the container
class="type">int n=class="num">0;                             class=class="str">"cmt">// The variable for calculating the column index relative to the loop index
class=class="str">"cmt">//--- In a loop by the list of headers,
  for(class="type">int i=class="num">0;i<list.Total();i++)
    {
    class=class="str">"cmt">//--- get the next tab header object
    CTabHeader *header=list.At(i);
    if(header==NULL)
       class="kw">continue;
    class=class="str">"cmt">//--- If the flag for positioning headers in several rows is set
    if(this.Multiline())
      {
      class=class="str">"cmt">//--- Calculate the value of the right edge of the header, taking into account that
      class=class="str">"cmt">//--- the origin always comes from the left edge of TabControl + class="num">2 pixels
      class="type">int x2=header.RightEdgeRelative()-x_shift;
      class=class="str">"cmt">//--- If the calculated value does not go beyond the right edge of the TabControl minus class="num">2 pixels, 
      class=class="str">"cmt">//--- set the column number equal to the loop index minus the value in the n variable
      if(x2<x2_base)
         col=i-n;
      class=class="str">"cmt">//--- If the calculated value goes beyond the right edge of the TabControl minus class="num">2 pixels,
      else
        {
        class=class="str">"cmt">//--- Increase the row index, calculate the new shift(so that the next object is compared with the TabControl left edge + class="num">2 pixels),
        class=class="str">"cmt">//--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row
        row++;
        x_shift=header.CoordXRelative()-class="num">2;
        n=i;
        col=class="num">0;
        }
      class=class="str">"cmt">//--- Assign the row and column indices to the tab header and shift it to the calculated coordinates
      header.SetTabLocation(row,col);
      if(header.Move(header.CoordX()-x_shift,header.CoordY()-header.Row()*header.Height()))
        {
        header.SetCoordXRelative(header.CoordX()-this.CoordX());
        header.SetCoordYRelative(header.CoordY()-this.CoordY());
        }
      }
    class=class="str">"cmt">//--- If only one row of headers is allowed
    else
      {
      header.SetRow(class="num">0);
      header.SetColumn(i);
      }
    }
class=class="str">"cmt">//--- The location of all tab titles is set. Now place them all together with the fields
class=class="str">"cmt">//--- according to the header row and column indices.
class=class="str">"cmt">//--- Get the last title in the list
  CTabHeader *last=this.GetTabHeader(list.Total()-class="num">1);
class=class="str">"cmt">//--- If the object is received
  if(last!=NULL)
    {
    class=class="str">"cmt">//--- If the mode of stretching headers to the width of the container is set, call the stretching method

「多行标签头的位置重算逻辑」

在自定义标签控件里,当尺寸模式设为 CANV_ELEMENT_TAB_SIZE_MODE_FILL 时,会先调用 StretchHeaders() 把表头拉满容器宽度。这一步决定不了纵向排布,真正管行偏移的是后面这段。 若当前最后一行索引大于 0,说明已经出现多行表头。系统用 last.Row()*last.Height() 算出 Y 方向总偏移 y_shift,再遍历 header 列表,把每个表头及其关联 tab 字段按 y_shift 下移,同时把坐标改写成相对容器的 Relative 值,并对字段做 Resize 把被遮住的高度扣掉。 只有第一行且非多行模式时,才进入另一分支:一旦最后一个表头右缘超出工作区右缘,就调出左右箭头按钮框,把它贴到 RightEdgeWorkspace()-width+1 的位置。外汇与贵金属图表挂这类控件时,容器被缩放可能频繁触发该越界判断,MT5 里改几个像素就能看到箭头框闪现。

MQL5 / C++
if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
   this.StretchHeaders();
class=class="str">"cmt">//--- If this is not the first row(with index class="num">0)
if(last.Row()>class="num">0)
  {
   class=class="str">"cmt">//--- Calculate the offset of the tab field Y coordinate
   class="type">int y_shift=last.Row()*last.Height();
   class=class="str">"cmt">//--- In a loop by the list of headers,
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
      class=class="str">"cmt">//--- get the next object
      CTabHeader *header=list.At(i);
      if(header==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- get the tab field corresponding to the received header
      CTabField  *field=header.GetFieldObj();
      if(field==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- shift the tab header by the calculated row coordinates
      if(header.Move(header.CoordX(),header.CoordY()+y_shift))
        {
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
        }
      class=class="str">"cmt">//--- shift the tab field by the calculated shift
      if(field.Move(field.CoordX(),field.CoordY()+y_shift))
        {
         field.SetCoordXRelative(field.CoordX()-this.CoordX());
         field.SetCoordYRelative(field.CoordY()-this.CoordY());
         class=class="str">"cmt">//--- change the size of the shifted field by the value of its shift
         field.Resize(field.Width(),field.Height()-y_shift,class="kw">false);
        }
     }
  }
class=class="str">"cmt">//--- If this is the first and only class="type">class="kw">string
else if(!this.Multiline())
  {
   class=class="str">"cmt">//--- If the right edge of the header goes beyond the right edge of the container area,
   if(last.RightEdge()>this.RightEdgeWorkspace())
     {
      class=class="str">"cmt">//--- get the button object with left-right arrows
      CArrowLeftRightBox *arr_box=this.GetArrLeftRightBox();
      if(arr_box!=NULL)
        {
         class=class="str">"cmt">//--- Calculate object location coordinates
         class="type">int x=this.RightEdgeWorkspace()-arr_box.Width()+class="num">1;

单行文本越界时的左右箭头定位

当文本框只有一行(Multiline 为 false)且该行右边界超出工作区右缘时,需要把左右滚动箭头框吸附到可视区右侧。代码里用 RightEdgeWorkspace 减去箭头框宽度再加 1 像素作为 x 坐标,y 则直接取该行纵坐标,保证箭头紧贴行尾。 若 Move 返回成功,立即把箭头框坐标转换为相对父控件的偏移:分别用自身 CoordX/Y 减去父对象 CoordX/Y。随后置 SetVisibleLeftRightBox(true),只要父控件本身可见就 Show 并 BringToTop,让箭头始终浮在最上层。 在 MT5 里跑这套逻辑时,可故意把单行文本拉到超出图表右边界,观察箭头框是否准确落在 RightEdgeWorkspace-Width+1 的位置;外汇与贵金属图表多屏缩放频繁,这类越界重定位失误可能引发误触,实盘前务必在模拟环境验证。

MQL5 / C++
else if(!this.Multiline())
  {
   if(last.RightEdge()>this.RightEdgeWorkspace())
     {
      CArrowLeftRightBox *arr_box=this.GetArrLeftRightBox();
      if(arr_box!=NULL)
        {
         class="type">int x=this.RightEdgeWorkspace()-arr_box.Width()+class="num">1;
         class="type">int y=last.CoordY();
         if(arr_box.Move(x,y))
           {
            arr_box.SetCoordXRelative(arr_box.CoordX()-this.CoordX());
            arr_box.SetCoordYRelative(arr_box.CoordY()-this.CoordY());
            this.SetVisibleLeftRightBox(true);
            if(this.IsVisible())
              {
               arr_box.Show();
               arr_box.BringToTop();
              }
           }
        }
     }
  }

◍ 单行文本框里上下箭头的贴边摆放

当控件被设为非多行(Multiline 为假)且作为首条也是唯一一条文本时,上下切换箭头盒(CArrowUpDownBox)的摆放逻辑分两个方向分支处理。 第一个分支判断上一条记录的 Y 坐标是否小于当前控件坐标(last.CoordY() < this.CoordY())。若成立,取箭头盒宽度用 last.RightEdge() 减去它得到新 x,y 则固定在当前坐标上方 1 像素(this.CoordY() - 1),随后 Move 成功才写相对坐标并置为可见。 第二个分支改用底边判断:若上一条底边大于当前底边(last.BottomEdge() > this.BottomEdge()),x 直接沿用上一条的 CoordX(),y 取工作区底边减箭头盒高度再加 1。两个分支都只在 Move 返回真后才调用 SetVisibleUpDownBox(true),且父控件可见时才 Show 并 BringToTop,避免不可见图元抢占 Z 序。 实际在 MT5 自定义控件里验证时,可把这两段直接塞进你的 CTextBox 派生类,观察单行模式下箭头盒是否严格贴边——外汇与贵金属图表叠加此类控件需注意高波动下重绘可能带来的闪烁。

MQL5 / C++
class=class="str">"cmt">//--- If this is the first and only class="type">class="kw">string
else if(!this.Multiline())
  {
   if(last.CoordY()<this.CoordY())
     {
      CArrowUpDownBox *arr_box=this.GetArrUpDownBox();
      if(arr_box!=NULL)
        {
         class="type">int x=last.RightEdge()-arr_box.Width();
         class="type">int y=this.CoordY()-class="num">1;
         if(arr_box.Move(x,y))
           {
            arr_box.SetCoordXRelative(arr_box.CoordX()-this.CoordX());
            arr_box.SetCoordYRelative(arr_box.CoordY()-this.CoordY());
            this.SetVisibleUpDownBox(true);
            if(this.IsVisible())
              {
               arr_box.Show();
               arr_box.BringToTop();
              }
           }
        }
     }
  }
class=class="str">"cmt">//--- If this is the first and only class="type">class="kw">string
else if(!this.Multiline())
  {
   if(last.BottomEdge()>this.BottomEdge())
     {
      CArrowUpDownBox *arr_box=this.GetArrUpDownBox();
      if(arr_box!=NULL)
        {
         class="type">int x=last.CoordX();
         class="type">int y=this.BottomEdgeWorkspace()-arr_box.Height()+class="num">1;
         if(arr_box.Move(x,y))
           {
            arr_box.SetCoordXRelative(arr_box.CoordX()-this.CoordX());
            arr_box.SetCoordYRelative(arr_box.CoordY()-this.CoordY());
            this.SetVisibleUpDownBox(true);
            if(this.IsVisible())
              {
               arr_box.Show();
               arr_box.BringToTop();
              }
           }
        }
     }
  }

「标签页里图形对象的工厂方法」

CTabControl 的 CreateNewGObject 是一个典型的对象工厂:根据传入的 ENUM_GRAPH_ELEMENT_TYPE 枚举,在 switch 里 new 出不同的画布元素派生类,再交回调用方管理。 参数列表里 x、y、w、h 都是 int 像素坐标,colour 与 opacity(0~255 的 uchar)控制外观,movable 和 activity 决定交互属性;这决定了你在 MT5 里拖一个标签控件时,子对象默认就带这套几何与透明度。 注意 GRAPH_ELEMENT_TYPE_ELEMENT 分支会把 this.ID()、obj_num、ChartID()、SubWindow() 全部塞进 CGCnvElement 构造,而 FORM 与 WF_CONTAINER 只传图表 ID、子窗口、描述与矩形——意味着后两者不记录自身在标签表中的序号,调试时别指望从 obj_num 反查它们。 开 MT5 把这段塞进你自己的标签控件类,改 case 分支加一种自定义元素类型,就能验证工厂分发是否如预期那样只认枚举不认字符串。

MQL5 / C++
CGCnvElement *CTabControl::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)
     {
      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;
     }
  }

控件类型的分支实例化写法

在自定义图形面板里,每识别到一个控件枚举,就要用对应 Standard Library 类去 new 一个实例。下面这段 switch 分支覆盖了从 GroupBox 到 TabControl 共 14 种 Windows Forms 风格控件,全部走同一个工厂函数入口。 所有分支的构造参数完全一致:图表 ID、子窗口号、描述串、以及 x/y/w/h 四个几何坐标。这意味着你加新控件类型时,只要补一个 case 并套用相同参数顺序,渲染层就能直接接管。 在 MT5 里打开 stdlib 的 CControls 相关头文件对照看,CGroupBox 和 CPanel 的基类都是 CWndClient,所以坐标体系互通;若你把 w 或 h 传 0,该控件在运行时可能不会响应鼠标,这是验证代码时第一个要排的坑。

MQL5 / C++
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;
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 对应基础箭头按钮,UP/DOWN/LEFT/RIGHT 四个枚举分别映射到 CArrowUpButton、CArrowDownButton、CArrowLeftButton、CArrowRightButton,而 UD_BOX 与 LR_BOX 则直接生成上下或左右成对的箭头盒体,全部以图表 ID、子窗口号、描述文本和 x/y/w/h 几何参数构造。 若 element 指针返回 NULL,说明对象没建出来,系统会打印失败提示并带上类型描述——这一步是排查 UI 控件不显示的第一现场。 CTabControl::BringToTop 负责把整个标签页对象压到前景。它先调用基类 CForm::BringToTop 或 CGCnvElement::Show 把元素显示/置顶,再通过 SelectedTabPageNum 拿到当前选中页索引,遍历 GetListHeaders 返回的表头数组;循环里用 list.At(i) 取表头对象,选中项直接跳过,其余表头依次处理层级。 开 MT5 把这段 switch 粘进你自己的控件工厂,改一下 descript 和 w/h,就能直观看到不同箭头按钮在子窗口里的绘制差异,外汇与贵金属图表挂这类自定义面板需注意高波动时段重绘可能掉帧。

MQL5 / C++
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;
}
class="type">void CTabControl::BringToTop(class="type">void)
  {
   CForm::BringToTop();
   class="type">int selected=this.SelectedTabPageNum();
   CTabHeader *header=NULL;
   CTabField  *field=NULL;
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      class="kw">return;
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
      header=list.At(i);

「标签页切换时把控件提到最前」

在自绘 Tab 控件里,切到某个标签不只是换内容,还要把对应头部和字段对象强行置顶,否则会被其他图层盖住。下面这段逻辑先遍历所有 header,跳过空指针和当前已选中的项,把其余 header 提至顶层后取其字段对象并隐藏,等于把非激活页彻底压下去。 激活页单独处理:拿到 selected 的 header 指针后 BringToTop,再取 field 并显示到前台。若控件整体可见且上下、左右翻页按钮的可见标志位为真,就顺手把这两个按钮盒也提到最前,保证操作区不被遮挡。 两个显示按钮盒的方法 ShowArrUpDownBox 与 ShowArrLeftRightBox 结构一致:先取盒对象指针,空则打印错误描述并返回,非空就调 Show。MT5 里跑这套,若翻页箭头偶尔点不到,优先查 m_arr_butt_ud_visible_flag 这类标志位有没有在置顶前被置真。

MQL5 / C++
if(header==NULL || header.PageNumber()==selected)
     class="kw">continue;
   class=class="str">"cmt">//--- bring the header to the foreground
   header.BringToTop();
   class=class="str">"cmt">//--- get the tab field corresponding to the current header
   field=header.GetFieldObj();
   if(field==NULL)
     class="kw">continue;
   class=class="str">"cmt">//--- Hide the tab field
   field.Hide();
    }
class=class="str">"cmt">//--- Get the pointer to the title of the selected tab
   header=this.GetTabHeader(selected);
   if(header!=NULL)
    {
      class=class="str">"cmt">//--- bring the header to the front
      header.BringToTop();
      class=class="str">"cmt">//--- get the tab field corresponding to the selected tab header
      field=header.GetFieldObj();
      class=class="str">"cmt">//--- Display the tab field on the foreground
      if(field!=NULL)
         field.BringToTop();
    }
class=class="str">"cmt">//--- If the object is visible and the "up-down" and "left-right" buttons should be visible, move them to the foreground
   if(this.IsVisible())
    {
      if(this.m_arr_butt_ud_visible_flag)
         this.BringToTopArrUpDownBox();
      if(this.m_arr_butt_lr_visible_flag)
         this.BringToTopArrLeftRightBox();
    }
   }
class="type">void CTabControl::ShowArrUpDownBox(class="type">void)
  {
   CArrowUpDownBox *box=this.GetArrUpDownBox();
   if(box==NULL)
    {
     ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX));
     class="kw">return;
    }
   box.Show();
  }
class="type">void CTabControl::ShowArrLeftRightBox(class="type">void)
  {
   CArrowLeftRightBox *box=this.GetArrLeftRightBox();
   if(box==NULL)
    {
     ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX));
     class="kw">return;
    }
   box.Show();
  }

标签页控件的箭头盒显隐与层级操作

CTabControl 里对上下、左右箭头盒提供了四组直接方法:HideArrUpDownBox / HideArrLeftRightBox 负责隐藏,BringToTopArrUpDownBox / BringToTopArrLeftRightBox 负责置顶。每个方法都先通过 GetArrUpDownBox 或 GetArrLeftRightBox 取对象指针,为空就打印错误描述并 return,避免空指针崩在图形对象上。 隐藏逻辑很简单,拿到 box 后调用其 Hide() 即可;置顶则是 BringToTop(),把箭头控件提到同容器其它元素之前,解决被标签头盖住的问题。 SetVisibleLeftRightBox 多了一层批量处理:先把 m_arr_butt_lr_visible_flag 存下状态,再从 GetListHeaders() 拿标签头列表,用 for 从 i=0 跑到 list.Total(),对每个 CTabHeader 调 SetVisibleLeftRightBox(flag)。这样一次调用就能同步所有分页头的左右箭头可见性,不用手动逐个设。 在 MT5 自建面板时,若箭头按钮莫名不显示或被遮挡,优先检查这几处 flag 与 BringToTop 调用顺序;外汇与贵金属图表加载这类自定义控件时波动大、高风险,改完参数建议在模拟盘验证层级表现。

MQL5 / C++
class="type">void CTabControl::HideArrUpDownBox(class="type">void)
  {
   CArrowUpDownBox *box=this.GetArrUpDownBox();
   if(box==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX));
      class="kw">return;
     }
   box.Hide();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Hide the Right-left button controls                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTabControl::HideArrLeftRightBox(class="type">void)
  {
   CArrowLeftRightBox *box=this.GetArrLeftRightBox();
   if(box==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX));
      class="kw">return;
     }
   box.Hide();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Move Up-down button controls to the foreground                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTabControl::BringToTopArrUpDownBox(class="type">void)
  {
   CArrowUpDownBox *box=this.GetArrUpDownBox();
   if(box==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX));
      class="kw">return;
     }
   box.BringToTop();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Move right-left button controls to the foreground                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTabControl::BringToTopArrLeftRightBox(class="type">void)
  {
   CArrowLeftRightBox *box=this.GetArrLeftRightBox();
   if(box==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX));
      class="kw">return;
     }
   box.BringToTop();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the visibility of the left-right buttons                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTabControl::SetVisibleLeftRightBox(const class="type">bool flag)
  {
   this.m_arr_butt_lr_visible_flag=flag;
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      class="kw">return;
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
      CTabHeader *obj=list.At(i);
      if(obj==NULL)
         class="kw">continue;
      obj.SetVisibleLeftRightBox(flag);
     }
  }

◍ 标签控件的箭头按钮如何批量调显隐与尺寸

在 MT5 自定义标签控件(CTabControl)里,上下翻页箭头和左右翻页箭头的显隐、尺寸是分开管理的。想一次性让所有页签头同步显示或隐藏上下箭头,直接改容器层的标志位再遍历头列表就行。 下面这段逻辑先写 m_arr_butt_ud_visible_flag 存全局开关,再从 GetListHeaders() 拿头对象数组,逐个调用 SetVisibleUpDownBox(flag)。若 list 为空直接 return,避免空指针。 左右箭头尺寸只调宽度、高度沿用原值,调用 Resize(value, butt.Height(), false);上下箭头则反过来只改高度。遍历时 i 从 0 跑到 list.Total(),任一 obj 为空就 continue 跳过,不会中断整轮刷新。 开 MT5 把这几个方法挂到自己的面板类上,改一个 value 就能看全部页签箭头同步伸缩,比逐个头去设要省事得多。外汇与贵金属图表挂这类自定义控件时需注意,高频重绘可能拖慢报价刷新,属高风险环境下的性能权衡。

MQL5 / C++
class="type">void CTabControl::SetVisibleUpDownBox(const class="type">bool flag)
  {
   this.m_arr_butt_ud_visible_flag=flag;
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      class="kw">return;
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
      CTabHeader *obj=list.At(i);
      if(obj==NULL)
         class="kw">continue;
      obj.SetVisibleUpDownBox(flag);
     }
  }
class="type">void CTabControl::SetSizeLeftRightBox(const class="type">int value)
  {
   CArrowLeftRightBox *butt=this.GetArrLeftRightBox();
   if(butt!=NULL)
      butt.Resize(value,butt.Height(),class="kw">false);
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      class="kw">return;
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
      CTabHeader *obj=list.At(i);
      if(obj==NULL)
         class="kw">continue;
      obj.SetSizeLeftRightBox(value);
     }
  }
class="type">void CTabControl::SetSizeUpDownBox(const class="type">int value)
  {
   CArrowUpDownBox *butt=this.GetArrUpDownBox();
   if(butt!=NULL)
      butt.Resize(butt.Width(),value,class="kw">false);
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      class="kw">return;
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
      CTabHeader *obj=list.At(i);
      if(obj==NULL)
         class="kw">continue;
      obj.SetSizeUpDownBox(value);
     }
  }

「标签头越界时如何整体平移」

在自绘多标签控件里,当选中的标签头右边缘超出工作区右边界,界面需要把整排头部向左挪,才能保证用户看见当前页签。下面这段方法只处理单行表头:多行模式直接 return,不介入排版。 先算 hidden = 选中头右缘 - 工作区右缘;若 hidden<0 说明没被裁切,函数直接退出,避免无谓重绘。只有当 hidden≥0,才进入左移逻辑。 左移量 shift 的确定有点绕:从选中索引往前遍历,找到第一个坐标 X-2 等于控件自身 X 的头,把它的宽度作为整体右移补偿值。这意味着最左一个贴边头会被整宽推出视区,其余头统一左移 shift 像素。 平移循环里对每个头调用 Move(CoordX()-shift, CoordY());成功后重写相对坐标。若新 X-2 小于控件 X,就 Crop()+Hide() 裁掉并隐藏;否则 Show() 并 Redraw(false),顺带把对应 TabField 也刷出来。外汇/贵金属面板高频刷新时,这种局部重绘能明显降低 CPU 占用,但误设 shift 会让头部错位,建议在 MT5 里断点跟一次 selected 与 WRONG_VALUE 的边界。

MQL5 / C++
class="type">void CTabControl::ShiftHeadersRow(const class="type">int selected)
  {
class=class="str">"cmt">//--- If there are multiline headers, leave
   if(this.Multiline())
      class="kw">return;
class=class="str">"cmt">//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Get the header of the selected tab
   CTabHeader *header=this.GetTabHeader(selected);
   if(header==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Check how much of the selected header is cropped on the right
   class="type">int hidden=header.RightEdge()-this.RightEdgeWorkspace();
class=class="str">"cmt">//--- If the header is not cropped, exit
   if(hidden<class="num">0)
      class="kw">return;
   CTabHeader *obj=NULL;
   class="type">int shift=class="num">0;
class=class="str">"cmt">//--- Look for the leftmost one starting from the selected header
   for(class="type">int i=selected-class="num">1;i>WRONG_VALUE;i--)
     {
      obj=list.At(i);
      if(obj==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- If the leftmost one is found, remember how much to shift all headers to the right
      if(obj.CoordX()-class="num">2==this.CoordX())
         shift=obj.Width();
     }
class=class="str">"cmt">//--- In a loop by the list of headers,
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
      class=class="str">"cmt">//--- get the next header
      obj=list.At(i);
      if(obj==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- and, if the header is shifted to the left by &class="macro">#x27;shift&class="macro">#x27;,
      if(obj.Move(obj.CoordX()-shift,obj.CoordY()))
        {
         class=class="str">"cmt">//--- save its new relative coordinates
         obj.SetCoordXRelative(obj.CoordX()-this.CoordX());
         obj.SetCoordYRelative(obj.CoordY()-this.CoordY());
         class=class="str">"cmt">//--- If the title has gone beyond the left edge,
         if(obj.CoordX()-class="num">2<this.CoordX())
           {
            class=class="str">"cmt">//--- crop and hide it
            obj.Crop();
            obj.Hide();
           }
         class=class="str">"cmt">//--- If the header fits the visible area of the control,
         else
           {
            class=class="str">"cmt">//--- display and redraw it
            obj.Show();
            obj.Redraw(class="kw">false);
            class=class="str">"cmt">//--- Get the tab field corresponding to the header
            CTabField *field=obj.GetFieldObj();
            if(field==NULL)
               class="kw">continue;
            class=class="str">"cmt">//--- If this is a selected header,
            if(i==selected)
               {

标签页选中后的画布重绘与元素检索

在自绘标签控件里,字段框画完必须立刻推到图表上,否则视觉会滞后于鼠标点击。原逻辑在绘制收尾调用 field.DrawFrame() 与 field.Update() 后,用 ::ChartRedraw(this.ChartID()) 强制重绘当前图表子窗口,这一行是避免界面卡顿的关键。 事件侧,OnChartEvent 收到 WF_CONTROL_EVENT_TAB_SELECT 后,只做了一件事:把表头行按传入的 dparam(即选中标签索引)做纵向偏移 this.ShiftHeadersRow((int)dparam)。这意味着标签切换的响应极轻,重绘成本压在画布层而非控件树全量刷新。 元素检索有两个入口。按图表 ID + 对象名取列表时,先拼前缀:若 name 里没找到 m_name_prefix 才补前缀,再用 CSelect::ByGraphCanvElementProperty 连续按 CHART_ID、NAME_OBJ 做相等过滤,返回的是 CArrayObj*。 单元素版 GetCanvElement(const string name) 走同样前缀逻辑,但最后取 list.At(0),空列表直接返 NULL。外汇与贵金属图表挂多个自绘对象时,这种按名精确抓取的方式能避开跨控件误引用,但 MT5 高频切换周期仍可能引发对象名冲突,属高风险操作环境,建议实盘前在模拟盘验证。

MQL5 / C++
class=class="str">"cmt">//--- Draw the field frame
   field.DrawFrame();
   field.Update();
   }
     }
   }
 }
class=class="str">"cmt">//--- Redraw the chart to display changes immediately
   ::ChartRedraw(this.ChartID());
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Event handler                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTabControl::OnChartEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Adjust subwindow Y shift
   CGCnvElement::OnChartEvent(id,lparam,dparam,sparam);
   if(id==WF_CONTROL_EVENT_TAB_SELECT)
     {
      this.ShiftHeadersRow((class="type">int)dparam);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Return the list of graphical elements by chart ID and object name
   CArrayObj       *GetListCanvElementByName(const class="type">long chart_id,const class="type">class="kw">string name)
                    {
                     class="type">class="kw">string nm=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name;
                     CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL);
                     class="kw">return CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                    }
class=class="str">"cmt">//--- Return the graphical element by name
   CGCnvElement    *GetCanvElement(const class="type">class="kw">string name)
                    {
                     class="type">class="kw">string nm=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name;
                     CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                     class="kw">return(list!=NULL ? list.At(class="num">0) : NULL);
                    }
class=class="str">"cmt">//--- Return the graphical element by chart ID and name
   CGCnvElement    *GetCanvElement(const class="type">long chart_id,const class="type">class="kw">string name)
                    {

◍ 在画布对象集合里截获 Tab 切换事件

CGraphElementsCollection 的 OnChartEvent 并不只处理裸图表事件,它把 WinForms 控件的自定义事件也收编进来了。代码里先用 id 减去 CHARTEVENT_CUSTOM 得到 ushort 型的 idx,再判断 idx 是否落在 WF_CONTROL_EVENT_NO_EVENT 与 WF_CONTROL_EVENTS_NEXT_CODE 之间,从而区分是不是窗体控件事件。 当 idx 等于 WF_CONTROL_EVENT_TAB_SELECT 时,说明用户在 TabControl 上切了页。这时 sparam 是用分号分隔的两个名字,必须正好拆成 2 段,否则直接写日志 MSG_GRAPH_OBJ_FAILED_GET_OBJECT_NAMES 并 return;这个「不等于 2 就退出」的硬判断,开 MT5 调试时若 Tab 事件没反应,先查分号分隔符是不是被改过。 拆出 array[0] 是主窗体名,array[1] 是具体控件名。通过 GetCanvElement(array[0]) 拿到主窗体指针,再由其 GetElementByName(array[1]) 找到子元素,最后把 idx 和参数透传给 base.OnChartEvent。任何一层指针为 NULL 就静默 return,不会抛错——这意味着控件链断掉时界面会「假死」,得自己加 Print 才能定位。 别把控件事件当图表事件处理 上面那串标准对象事件(OBJECT_CHANGE / DRAG / CLICK)和 idx 的同名判断是两套入口,控件事件走 WF_ 分支,标准对象走后面 CHARTEVENT_OBJECT_* 分支。混写容易让 Tab 切换吞掉拖拽信号,建议复制这段代码后,先单测 Tab 分支再接标准对象逻辑。

MQL5 / C++
CArrayObj *list=this.GetListCanvElementByName(chart_id,name);
class="kw">return(list!=NULL ? list.At(class="num">0) : NULL);
}
class=class="str">"cmt">//--- Return the graphical element by chart and object IDs
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Event handler                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGraphElementsCollection::OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
  {
   CGStdGraphObj *obj_std=NULL;  class=class="str">"cmt">// Pointer to the standard graphical object
   CGCnvElement  *obj_cnv=NULL;  class=class="str">"cmt">// Pointer to the graphical element object on canvas
   class="type">class="kw">ushort idx=class="type">class="kw">ushort(id-CHARTEVENT_CUSTOM);
class=class="str">"cmt">//--- Handle WinForms control events
   if(idx>WF_CONTROL_EVENT_NO_EVENT && idx<WF_CONTROL_EVENTS_NEXT_CODE)
     {
      class=class="str">"cmt">//--- Clicking the control
      if(idx==WF_CONTROL_EVENT_CLICK)
        {
         class=class="str">"cmt">//---
        }
      class=class="str">"cmt">//--- Selecting the TabControl tab
      if(idx==WF_CONTROL_EVENT_TAB_SELECT)
        {
         class="type">class="kw">string array[];
         if(::StringSplit(sparam,::StringGetCharacter(";",class="num">0),array)!=class="num">2)
           {
            CMessage::ToLog(MSG_GRAPH_OBJ_FAILED_GET_OBJECT_NAMES);
            class="kw">return;
           }
         CWinFormBase *main=this.GetCanvElement(array[class="num">0]);
         if(main==NULL)
            class="kw">return;
         CWinFormBase *base=main.GetElementByName(array[class="num">1]);
         if(base!=NULL)
            base.OnChartEvent(idx,lparam,dparam,sparam);
        }
     }
class=class="str">"cmt">//--- Handle the events of renaming and clicking a standard graphical object
   if(id==CHARTEVENT_OBJECT_CHANGE  || id==CHARTEVENT_OBJECT_DRAG     || id==CHARTEVENT_OBJECT_CLICK  ||
      idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      class=class="str">"cmt">//--- Calculate the chart ID
      class=class="str">"cmt">//---
      class=class="str">"cmt">//---
     }
class=class="str">"cmt">//---
class=class="str">"cmt">//---
  }

「用 11 个选项卡压测 TabControl 边界」

把上一篇文章里的 EA 照搬过来,存到 \MQL5\Experts\TestDoEasy\Part118\ 目录下,文件名 TestDoEasy118.mq5,这是本次实测的基线。 我们在主面板上挂一个含 11 个选项卡的 TabControl,每个选项卡里放一个文本标签写明自身描述,测试时一眼就能看出当前显示的是哪一页。 启动 EA 后,把选项卡标题拉伸到适配控件宽度的程度,哪个选项卡超出容器边缘会非常直观;再检查标题在控件两侧的排布,重点看滚动按钮怎么放。标题在顶部时,点右侧被裁掉的标题,整行向左移、选中标题露出来,这点和 MS Visual Studio 的 TabControl 行为一致。 面板用循环建(目前只建 1 个),因为实测发现用 TabControl 建多个面板时控件会异常,所以只建所需数量才稳。 编译挂到图表上跑,声明的功能都正常。下面这段 OnInit 是面板与 TabControl 的创建核心,逐行拆一下关键点。 // 定义渐变填充双色:深青蓝 C'26,100,128' 与提亮色 C'35,133,169' // 只用 1 个符号数组传给库,SeriesCreate 建当前周期时间序列 // 循环 i<1 仅建 1 个面板,位置 50,50、宽 410 高 200 // 面板内边距设 3,可移动/自动尺寸由输入参数控制 // CreateNewElement 建 TabControl,宽高比面板内缩 30/40 像素 // 取回指针后设选项卡尺寸模式、标题对齐、多行与头部 padding(6,0)

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Set EA global variables
   ArrayResize(array_clr,class="num">2);        class=class="str">"cmt">// Array of gradient filling colors
   array_clr[class="num">0]=C&class="macro">#x27;class="num">26,class="num">100,class="num">128&class="macro">#x27;;      class=class="str">"cmt">// Original ≈Dark-azure class="type">color
   array_clr[class="num">1]=C&class="macro">#x27;class="num">35,class="num">133,class="num">169&class="macro">#x27;;      class=class="str">"cmt">// Lightened original class="type">color
class=class="str">"cmt">//--- Create the array with the current symbol and set it to be used in the library
   class="type">class="kw">string array[class="num">1]={Symbol()};
   engine.SetUsedSymbols(array);
   class=class="str">"cmt">//--- Create the timeseries object for the current symbol and period, and show its description in the journal
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(class="kw">false); class=class="str">"cmt">// Short descriptions
class=class="str">"cmt">//--- Create the required number of WinForms Panel objects
   CPanel *pnl=NULL;
   for(class="type">int i=class="num">0;i<class="num">1;i++)
     {
      pnl=engine.CreateWFPanel("WinForms Panel"+(class="type">class="kw">string)i,(i==class="num">0 ? class="num">50 : class="num">70),(i==class="num">0 ? class="num">50 : class="num">70),class="num">410,class="num">200,array_clr,class="num">200,true,true,class="kw">false,-class="num">1,FRAME_STYLE_BEVEL,true,class="kw">false);
      if(pnl!=NULL)
        {
         pnl.Hide();
         Print(DFUN,"Panel description: ",pnl.Description(),", Type and name: ",pnl.TypeElementDescription()," ",pnl.Name());
         class=class="str">"cmt">//--- Set Padding to class="num">4
         pnl.SetPaddingAll(class="num">3);
         class=class="str">"cmt">//--- Set the flags of relocation, auto resizing and auto changing mode from the inputs
         pnl.SetMovable(InpMovable);
         pnl.SetAutoSize(InpAutoSize,class="kw">false);
         pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,class="kw">false);

         class=class="str">"cmt">//--- Create TabControl
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,InpTabControlX,InpTabControlY,pnl.Width()-class="num">30,pnl.Height()-class="num">40,clrNONE,class="num">255,true,class="kw">false);
         CTabControl *tc=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,class="num">0);
         if(tc!=NULL)
           {
            tc.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode);
            tc.SetAlignment((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment);
            tc.SetMultiline(InpTabCtrlMultiline);
            tc.SetHeaderPadding(class="num">6,class="num">0);

给多页标签面板填内容和显影

上面这段逻辑紧接前文建好的标签容器,负责把 11 个分页真正填上可读元素并推到前台。先调用 CreateTabPages(11,0,56,20,...) 生成 11 个分页,坐标偏移 (0,56)、单页尺寸 20,俄语环境显示“Вкладка”、英文环境显示“TabPage”。 随后用 for 循环从 j=0 跑到 tc.TabPages()-1,在每个分页上建一个 WF_LABEL:位置 (60,20)、宽高 (80,20)、颜色 clrDodgerBlue、透明度 255、可显不可编辑。取回指针后若为空就 continue,否则把文字写成 “TabPage”+序号(从 1 起算),这样肉眼能看到 TabPage1 到 TabPage11。 最后一段只跑 i=0 一次,用 GetWFPanelByName("Panel0") 拿面板,非空就 Show() 加 Redraw(true) 强制重绘,函数返回 INIT_SUCCEEDED。外汇与贵金属图表上挂这类自定义面板属高风险操作,参数写错可能整面板不显,建议在 MT5 策略测试器先空载跑一遍。 想验证就直接把这段贴进 EA 的 OnInit 尾部,把前文 Panel 创建代码补齐,编译后看子窗口是否出 11 个蓝标分页;若只看到 Panel0 说明循环里的面板名要对齐你实际创建的命名。

MQL5 / C++
tc.CreateTabPages(class="num">11,class="num">0,class="num">56,class="num">20,TextByLanguage("Вкладка","TabPage"));
class=class="str">"cmt">//--- Create a text label with a tab description on each tab
for(class="type">int j=class="num">0;j<tc.TabPages();j++)
  {
   tc.CreateNewElement(j,GRAPH_ELEMENT_TYPE_WF_LABEL,class="num">60,class="num">20,class="num">80,class="num">20,clrDodgerBlue,class="num">255,true,class="kw">false);
   CLabel *label=tc.GetTabElement(j,class="num">0);
   if(label==NULL)
     class="kw">continue;
   label.SetText("TabPage"+class="type">class="kw">string(j+class="num">1));
   }
   }
   }
   }
class=class="str">"cmt">//--- Display and redraw all created panels
  for(class="type">int i=class="num">0;i<class="num">1;i++)
  {
   pnl=engine.GetWFPanelByName("Panel"+(class="type">class="kw">string)i);
   if(pnl!=NULL)
     {
      pnl.Show();
      pnl.Redraw(true);
     }
  }

class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }

◍ 下一步要做的滚动控制

这一节原本是系列文的第 37 节收尾,作者预告下一篇会补齐 TabControl 在所有方向上用控制按钮滚动选项卡标题的方法。当前已放出的 MQL5 函数库版本、测试 EA 和图表事件控制指标打包在 4454.87 KB 的 ZIP 里,可直接下载到 MT5 的 MQL5 目录里编译验证。 从评论区反馈看,有用户指出水平排布大量标签在管理上不便,作者回应控件支持将选项卡标题放在多行及容器不同侧面,按钮高度也可自定义——示例里特意加高按钮只为看裁剪效果,和 MS Visual Studio 的 TabControl 行为一致。 如果你要在自己的面板里用这套,建议先跑通 ZIP 里的指标,把容器宽度改小观察箭头按钮的修剪与滚动触发;外汇和贵金属图表加载自定义控件存在平台兼容性高风险,参数没调稳前别挂实盘。

把面板溢出诊断交给小布
这类选项卡裁剪与滚动边界的计算,小布盯盘的 AIGC 已内置在品种页的面板诊断里,打开即可看到当前控件是否触发了溢出裁剪,你只需决定交互逻辑。

常见问题

默认布局不会平移标题栏,选中项仍停留在原坐标,因此只能看到残留部分。实现左平移逻辑后,栏体才会随选中项移动。
由选项卡停靠位置决定:顶部对应右上、底部右下、左侧左上、右侧右下,且均从边框内缩一个像素以避免压住选项卡边框。
定时器轮询有延迟且浪费资源,向图表发送内部事件可由库路由到对应类处理,避免跨类耦合和轮询开销。
目前小布提供溢出诊断与结构建议,具体 MQL5 类实现仍需按本篇的库改动自行落地,但重复排版检查可交给小布。
在 Defines.mqh 的 WinForms 对象事件枚举中追加新项,并同步 Data.mqh 的消息索引与文本即可。