DoEasy. 控件(第 15 部分):TabControl WinForms 对象  多行选项卡标题、选项卡处理方法·进阶篇
🗂️

DoEasy. 控件(第 15 部分):TabControl WinForms 对象 多行选项卡标题、选项卡处理方法·进阶篇

(2/3)· 多行选项卡标题裁剪与 TabField 容器重构,解决宽屏选卡溢出与交互僵局

实战向 第 2/3 篇

很多人在 TabControl 里塞了十几个选卡,宽度一超就靠裁剪加滚动按钮凑合,结果多行排列和字段交互全卡死。本篇接着上篇的 TabHeader 基础,把多行模式和独立 TabField 容器真正落进库里。

「TabControl 属性枚举与尺寸模式拆解」

在 MT5 自定义图形库里,CTabControl 的整数属性直接决定了多标签控件的渲染逻辑。其中 MSG_CANV_ELEMENT_PROP_TAB_MULTILINE 控制是否允许标签折成多行,MSG_CANV_ELEMENT_PROP_TAB_ALIGNMENT 管标签在控件内的方位,MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE 则决定单个标签宽度怎么算。 标签尺寸有三种设定:Fit to tab header text width(按标题文字宽度自适应)、Fit TabControl Width(撑满整个控件宽度)、Fixed size(写死固定值)。这三种模式在回测面板或盯盘 HUD 里会明显影响信息密度,外汇与贵金属行情跳动快,高密度布局可能带来误触风险,需自行权衡。 下面这段函数是把尺寸枚举转成可读字符串的实现,方便在日志里排查界面状态: string TabSizeModeDescription(ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode) { switch(mode) {

MQL5 / C++
class="type">class="kw">string TabSizeModeDescription(ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode)
  {
   class="kw">switch(mode)
     {

◍ 标签页尺寸模式与图元类型的字符串映射

在 MT5 的 GUI 库底层,标签页(Tab)控件的尺寸策略由枚举 CANV_ELEMENT_TAB_SIZE_MODE 决定,代码里用 switch 把它转成可读文本:NORMAL 对应自适应、FIXED 对应固定宽、FILL 对应撑满容器,default 分支直接回 "Unknown"。 紧随其后的 CGBaseObj::TypeElementDescription() 用三元运算符把 ENUM_GRAPH_ELEMENT_TYPE 逐一翻译成界面描述。从 GRAPH_ELEMENT_TYPE_STANDARD 到 GRAPH_ELEMENT_TYPE_WF_LABEL,覆盖了基础图元、影子对象、窗体以及 WinForms 风格的容器与控件共 14 个分支。 如果你在写自定义面板,直接调用这两个函数就能把内部枚举打印到日志,省去自己维护字符串表。外汇与贵金属图表上的自定义 UI 属于高风险环境下的辅助工具,显示异常不预示任何行情方向。

MQL5 / C++
case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL :  class="kw">return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_NORMAL);   break;
case CANV_ELEMENT_TAB_SIZE_MODE_FIXED  :  class="kw">return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_FIXED);     break;
case CANV_ELEMENT_TAB_SIZE_MODE_FILL  :  class="kw">return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_FILL);      break;
class="kw">default                               :  class="kw">return "Unknown"; break;
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the description of the graphical element type               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CGBaseObj::TypeElementDescription(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   class="kw">return
     (
      type==GRAPH_ELEMENT_TYPE_STANDARD                  ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD)                :
      type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)  :
      type==GRAPH_ELEMENT_TYPE_ELEMENT                  ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_ELEMENT)                :
      type==GRAPH_ELEMENT_TYPE_SHADOW_OBJ               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_SHADOW_OBJ)             :
      type==GRAPH_ELEMENT_TYPE_FORM                     ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_FORM)                   :
      type==GRAPH_ELEMENT_TYPE_WINDOW                   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WINDOW)                 :
      class=class="str">"cmt">//--- WinForms
      type==GRAPH_ELEMENT_TYPE_WF_UNDERLAY              ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_UNDERLAY)            :
      type==GRAPH_ELEMENT_TYPE_WF_BASE                  ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BASE)                :
      class=class="str">"cmt">//--- Containers
      type==GRAPH_ELEMENT_TYPE_WF_CONTAINER             ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CONTAINER)           :
      type==GRAPH_ELEMENT_TYPE_WF_GROUPBOX              ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_GROUPBOX)            :
      type==GRAPH_ELEMENT_TYPE_WF_PANEL                 ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_PANEL)               :
      type==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL           ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL)         :
      class=class="str">"cmt">//--- Standard controls
      type==GRAPH_ELEMENT_TYPE_WF_COMMON_BASE            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_COMMON_BASE)         :
      type==GRAPH_ELEMENT_TYPE_WF_LABEL                 ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LABEL)               :

按类型清点图表上的图形元素

在自定义图形库里,想统计某类控件(复选框、单选钮、列表框等)在图表子窗口中的实际数量,核心靠 ObjectsTotal 配合前缀过滤。下面这段私有方法给出了可直接抄的骨架:先按类型拼出标准名,再遍历全部对象比对名称前缀。 调用 ObjectsTotal(this.ChartID(), this.SubWindow()) 拿到当前图表与子窗口的对象总数,这个返回值在复杂 EA 面板上可能到几十甚至上百,循环开销可控但别在 OnTick 里高频调。 TypeGraphElementAsString 先把枚举转成人类可读字符串(如 WF_CHECKBOX),随后 ObjectName 逐个取对象名,名称不含库前缀的就跳过——这是判断「是不是自己人」的关键一步。 实际验证时,打开 MT5 导航器拖一个带前缀的面板到图表,断点跟一下 n 的累计值,就能确认你的控件真的被库接管了,外汇与贵金属图表上做这类扩展须留意对象数过多会拖慢终端响应。

MQL5 / C++
class="type">int CGCnvElement::GetNumGraphElements(const ENUM_GRAPH_ELEMENT_TYPE type) const
  {
class=class="str">"cmt">//--- Declare a variable with the number of graphical objects and
class=class="str">"cmt">//--- get the total number of graphical objects on the chart and the subwindow where the graphical element is created
   class="type">int n=class="num">0, total=::ObjectsTotal(this.ChartID(),this.SubWindow());
class=class="str">"cmt">//--- Create the name of a graphical object by its type
   class="type">class="kw">string name=TypeGraphElementAsString(type);
class=class="str">"cmt">//--- In the loop by all chart and subwindow objects,
   for(class="type">int i=class="num">0;i<total;i++)
     {
     class=class="str">"cmt">//--- get the name of the next object
      class="type">class="kw">string name_obj=::ObjectName(this.ChartID(),i,this.SubWindow());
     class=class="str">"cmt">//--- if the object name does not contain the set prefix of the names of the library graphical objects, move on - this is not the object we are looking for

「按类型与名称清点图表对象」

在 MT5 自定义指标或 EA 里批量管理图形对象时,先得知道图表上到底挂了多少个同类型元素。下面这段从图形元素封装类里抽出的逻辑,就是靠遍历 ObjectsTotal 拿到总数,再按名称前缀和子串过滤来计数。 核心函数 GetNumGraphElements 接收 name 与 type 两个参数,先用 ObjectsTotal(ChartID(), SubWindow()) 取当前图表及其子窗口的对象总量,随后 for 循环从 0 跑到 total-1。每一次循环用 ObjectName 取出对象名,若 StringFind 找不到类库统一前缀(返回 WRONG_VALUE)就 continue 跳过,否则再用 StringFind(name_obj, name) > 0 判断类型子串,命中则计数器 n 加一。 CreateNameGraphElement 则把前缀、类型字符串和当前同类型数量拼成唯一名:NamePrefix()+TypeGraphElementAsString(type)+(string)GetNumGraphElements(type)。这样每新建一个元素,名称里的数字后缀会自动递增,避免重名覆盖。 开 MT5 随便拖一个指标到副图,把 SubWindow() 改成 1 跑一遍这段,就能看到副窗口里同类对象实时计数,方便你后续做批量删除或样式刷新。外汇与贵金属图表对象随价格波动频繁重绘,误操作可能清掉关键分析线,实盘前务必在模拟环境验证。

MQL5 / C++
class="type">int CGCnvElement::GetNumGraphElements(const class="type">class="kw">string name,const ENUM_GRAPH_ELEMENT_TYPE type) const
  {
  class="type">int n=class="num">0, total=::ObjectsTotal(this.ChartID(),this.SubWindow());
  for(class="type">int i=class="num">0;i<total;i++)
    {
    class="type">class="kw">string name_obj=::ObjectName(this.ChartID(),i,this.SubWindow());
    if(::StringFind(name_obj,this.NamePrefix())==WRONG_VALUE)
      class="kw">continue;
    if(::StringFind(name_obj,name)>class="num">0)
      n++;
    }
  class="kw">return n;
  }

class="type">class="kw">string CGCnvElement::CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
  class="kw">return this.NamePrefix()+TypeGraphElementAsString(type)+(class="type">class="kw">string)this.GetNumGraphElements(type);
  }

◍ 画布标签的创建与失败归因

在 MT5 自定义图形界面里,CGCnvElement::Create 负责把一个位图标签画到指定图表上。它先 ResetLastError 清掉旧错误码,再调用 m_canvas.CreateBitmapLabel,传入图表 ID、子窗口号、坐标 (x,y) 与宽高 (w,h),颜色格式固定用 COLOR_FORMAT_ARGB_NORMALIZE。 创建成功后会立刻 Erase(CLR_CANV_NULL) 清空画布,并执行 m_canvas.Update(redraw) 按传入开关决定是否重绘;同时用 ChartGetInteger 取 CHART_WINDOW_YDISTANCE 填到 m_shift_y,这一步决定了后续元素相对窗口的纵向偏移量。 失败分支值得细看:若 err==0 但仍返回 false,说明是宽或高不合法——w<1 记 Width 错,h<1 记 Height 错,否则归为 ERR_OBJECT_ERROR。CMessage::ToLog 会把函数名、行号加具体维度信息写进日志,方便在策略里快速定位是参数还是对象本身的问题。外汇与贵金属图表加载这类对象时波动快,宽高设 0 可能瞬间触发失败,属高风险环境下的常见坑。

MQL5 / C++
class="type">bool CGCnvElement::Create(const class="type">long chart_id,   class=class="str">"cmt">// Chart ID
                            const class="type">int wnd_num,   class=class="str">"cmt">// Chart subwindow
                            const class="type">int x,         class=class="str">"cmt">// X coordinate
                            const class="type">int y,         class=class="str">"cmt">// Y coordinate
                            const class="type">int w,         class=class="str">"cmt">// Width
                            const class="type">int h,         class=class="str">"cmt">// Height
                            const class="type">bool redraw=false) class=class="str">"cmt">// Flag indicating the need to redraw
  {
  ::ResetLastError();
  if(this.m_canvas.CreateBitmapLabel((chart_id==NULL ? ::ChartID() : chart_id),wnd_num,this.m_name,x,y,w,h,COLOR_FORMAT_ARGB_NORMALIZE))
     {
     this.Erase(CLR_CANV_NULL);
     this.m_canvas.Update(redraw);
     this.m_shift_y=(class="type">int)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_WINDOW_YDISTANCE,wnd_num);
     class="kw">return true;
     }
  class="type">int err=::GetLastError();
  class="type">int code=(err==class="num">0 ? (w<class="num">1 ? MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH : h<class="num">1 ? MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT : ERR_OBJECT_ERROR) : err);
  class="type">class="kw">string subj=(w<class="num">1 ? "Width="+(class="type">class="kw">string)w+". " : h<class="num">1 ? "Height="+(class="type">class="kw">string)h+". " : "");
  CMessage::ToLog(DFUN_ERR_LINE+subj,code,true);

  class="kw">return false;
  }

往表单里挂新图元的工厂方法

在 MT5 自定义图形界面里,往一个 CForm 容器动态挂子元素,核心就落在 CreateAndAddNewElement 这个工厂函数上。它先把传入的枚举类型做一道门槛判断:如果 element_type 小于 GRAPH_ELEMENT_TYPE_ELEMENT,直接 Print 报错并返回 NULL,避免把非图元类型当对象创建。 函数内部先取 m_list_elements.Total() 作为新元素序号,再用 TypeGraphElementAsString 拿到默认描述,并通过 GetCoords 把相对坐标换算成屏幕坐标。真正生成对象靠 CreateNewGObject,失败就返 NULL;成功才继续走 AddNewElement 绑定,绑定失败则 delete 掉防止内存泄漏。 挂上之后有一串最小属性设置值得注意:背景色、透明度、激活态,以及 Main / Base 指针的指向逻辑——当表单自身没有 Main 时,新元素的主控指向表单对象本身,否则沿用表单的 Main。最后用 SetID(GetMaxIDAll()+1) 保证全局 ID 不撞车,并把相对 X 坐标存成与表单的坐标差。 下面这段就是该方法的真实骨架,逐行看能少踩很多坑:

//Create a new attached element
//and add it to the list of bound objects

//+------------------------------------------------------------------+ CGCnvElement *CForm::CreateAndAddNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool activity) { //--- 若图元类型小于基础元素枚举,打印错误并返回空指针 if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),19)); return NULL; } //--- 取当前元素列表总数作为新元素索引 int num=this.m_list_elements.Total(); //--- 生成默认图元描述字符串 string descript=TypeGraphElementAsString(element_type); //--- 计算相对基对象坐标系的屏幕坐标 int elm_x=x; int elm_y=y; this.GetCoords(elm_x,elm_y); //--- 创建新图元对象,初始不主显 CGCnvElement *obj=this.CreateNewGObject(element_type,num,descript,elm_x,elm_y,w,h,colour,opacity,false,activity); if(obj==NULL) return NULL; //--- 加入绑定列表,失败则释放并返回空 if(!this.AddNewElement(obj,elm_x,elm_y)) { delete obj; return NULL; } //--- 设置背景色、透明度、激活态 obj.SetBackgroundColor(colour,true); obj.SetOpacity(opacity); obj.SetActive(activity); //--- 设定主控与基对象指针 obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain()); obj.SetBase(this.GetObject()); //--- 分配全局唯一 ID 与序号,并保存相对 X 偏移 obj.SetID(this.GetMaxIDAll()+1); obj.SetNumber(num); obj.SetCoordXRelative(obj.CoordX()-this.CoordX());

MQL5 / C++
class=class="str">"cmt">//| Create a new attached element                                                                                |
class=class="str">"cmt">//| and add it to the list of bound objects                                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGCnvElement *CForm::CreateAndAddNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                            const class="type">int x,
                                            const class="type">int y,
                                            const class="type">int w,
                                            const class="type">int h,
                                            const class="type">class="kw">color colour,
                                            const class="type">uchar opacity,
                                            const class="type">bool activity)
  {
class=class="str">"cmt">//--- If the type of a created graphical element is less than the "element", inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27;
   if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),class="num">19));
      class="kw">return NULL;
     }
class=class="str">"cmt">//--- Specify the element index in the list
   class="type">int num=this.m_list_elements.Total();
class=class="str">"cmt">//--- Create a description of the class="kw">default graphical element
   class="type">class="kw">string descript=TypeGraphElementAsString(element_type);
class=class="str">"cmt">//--- Get the screen coordinates of the object relative to the coordinate system of the base object
   class="type">int elm_x=x;
   class="type">int elm_y=y;
   this.GetCoords(elm_x,elm_y);
class=class="str">"cmt">//--- Create a new graphical element
   CGCnvElement *obj=this.CreateNewGObject(element_type,num,descript,elm_x,elm_y,w,h,colour,opacity,false,activity);
   if(obj==NULL)
      class="kw">return NULL;
class=class="str">"cmt">//--- and add it to the list of bound graphical elements
   if(!this.AddNewElement(obj,elm_x,elm_y))
     {
      class="kw">delete obj;
      class="kw">return NULL;
     }
class=class="str">"cmt">//--- Set the minimum properties for a bound graphical element
   obj.SetBackgroundColor(colour,true);
   obj.SetOpacity(opacity);
   obj.SetActive(activity);
   obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain());
   obj.SetBase(this.GetObject());
   obj.SetID(this.GetMaxIDAll()+class="num">1);
   obj.SetNumber(num);
   obj.SetCoordXRelative(obj.CoordX()-this.CoordX());
让小布替你跑这套
这些多行选项卡的诊断和字段重构逻辑,小布盯盘的 AIGC 已内置,打开对应品种页即可看到结构校验结果,你只管调参数。

常见问题

激活多行且选中卡无边界时,整行标题移近字段,相邻标题取代原行位置,本篇仅实现顶部 Normal 与 Fixed 两种。
可以,小布盯盘的 AIGC 已内置控件结构诊断,打开对应品种页即可看到 TabField 与 TabHeader 的协同状态。
FillToRight 及底部左侧右侧排列留到后续文章,本篇只覆盖顶部 Normal 和 Fixed 多行模式。
新增两属性挂在列表末尾,可按其排序搜索,已有对象通用,后续会限制部分对象可用性。