DoEasy. 控件(第 16 部分):TabControl WinForms 对象  多行选项卡标题,拉伸标题适配容器·综合运用
📘

DoEasy. 控件(第 16 部分):TabControl WinForms 对象 多行选项卡标题,拉伸标题适配容器·综合运用

第 3/3 篇

标签头复位时的坐标回退逻辑

在自定义标签控件里,鼠标移开(状态 off)时需要把表头推回原始位置,同时把相对坐标也还原,否则下次悬停会叠加上一次的偏移量。 下面这段处理函数先尝试重置尺寸,失败直接返回 false;随后按对齐方式分四种情况平移。顶部和左侧对齐都向右下各移 2 像素(X+2, Y+2),底部对齐只右移 2 像素(X+2, Y 不变),右侧对齐只下移 2 像素(X 不变, Y+2),每次移动成功才同步改写相对坐标。 [CODE]bool CTabHeader::WHProcessStateOff(void) { //--- If failed to set a new size, leave if(!this.SetSizeOff()) return false; //--- Depending on the title location, switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : //--- shift the header to its original position and set the previous relative coordinates if(this.Move(this.CoordX()+2,this.CoordY()+2)) { this.SetCoordXRelative(this.CoordXRelative()+2); this.SetCoordYRelative(this.CoordYRelative()+2); } break; case CANV_ELEMENT_ALIGNMENT_BOTTOM : //--- shift the header to its original position and set the previous relative coordinates if(this.Move(this.CoordX()+2,this.CoordY())) { this.SetCoordXRelative(this.CoordXRelative()+2); this.SetCoordYRelative(this.CoordYRelative()); } break; case CANV_ELEMENT_ALIGNMENT_LEFT : //--- shift the header to its original position and set the previous relative coordinates if(this.Move(this.CoordX()+2,this.CoordY()+2)) { this.SetCoordXRelative(this.CoordXRelative()+2); this.SetCoordYRelative(this.CoordYRelative()+2); } break; case CANV_ELEMENT_ALIGNMENT_RIGHT : //--- shift the header to its original position and set the previous relative coordinates if(this.Move(this.CoordX(),this.CoordY()+2)) { this.SetCoordXRelative(this.CoordXRelative()); this.SetCoordYRelative(this.CoordYRelative()+2); } break; default: break; } return true; }[/CODE] 逐行看:第 4 行 if(!this.SetSizeOff()) 是尺寸复位守门员;第 9–11 行顶部对齐先 Move(X+2,Y+2),成功才把相对坐标也 +2,保证视觉与内部状态一致。 在 MT5 里自己写画布标签时,若发现悬停多次后表头越漂越远,八成是漏了相对坐标回退——把上面 2 像素的补偿对照着对齐方式抄进你的 state-off 分支即可。外汇与贵金属图表挂这类自定义控件时,注意高分屏下 2 像素可能需按缩放系数放大,否则复位观感会偏。

MQL5 / C++
class="type">bool CTabHeader::WHProcessStateOff(class="type">void)
  {
  class=class="str">"cmt">//--- If failed to set a new size, leave
   if(!this.SetSizeOff())
        class="kw">return class="kw">false;
  class=class="str">"cmt">//--- Depending on the title location,
   class="kw">switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        class=class="str">"cmt">//--- shift the header to its original position and set the previous relative coordinates
        if(this.Move(this.CoordX()+class="num">2,this.CoordY()+class="num">2))
         {
          this.SetCoordXRelative(this.CoordXRelative()+class="num">2);
          this.SetCoordYRelative(this.CoordYRelative()+class="num">2);
         }
        class="kw">break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        class=class="str">"cmt">//--- shift the header to its original position and set the previous relative coordinates
        if(this.Move(this.CoordX()+class="num">2,this.CoordY()))
         {
          this.SetCoordXRelative(this.CoordXRelative()+class="num">2);
          this.SetCoordYRelative(this.CoordYRelative());
         }
        class="kw">break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        class=class="str">"cmt">//--- shift the header to its original position and set the previous relative coordinates
        if(this.Move(this.CoordX()+class="num">2,this.CoordY()+class="num">2))
         {
          this.SetCoordXRelative(this.CoordXRelative()+class="num">2);
          this.SetCoordYRelative(this.CoordYRelative()+class="num">2);
         }
        class="kw">break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        class=class="str">"cmt">//--- shift the header to its original position and set the previous relative coordinates
        if(this.Move(this.CoordX(),this.CoordY()+class="num">2))
         {
          this.SetCoordXRelative(this.CoordXRelative());
          this.SetCoordYRelative(this.CoordYRelative()+class="num">2);
         }
        class="kw">break;
      class="kw">default:
        class="kw">break;
     }
   class="kw">return true;
  }

◍ 标签页拖拽后零行与选中行的坐标互换

在 MT5 自定义 TabControl 里,用户点选非第 0 行的表头后,需要把被点行沉到最底、原零行顶上去。CTabHeader::CorrectSelectedRowBottom 干的就是这套行交换,不涉及任何绘制重算,只动坐标和 Row 标签。 函数开头先抓两个值:row_pressed 是被点表头的 Row(),y_pressed 是所有零行表头应当移到的 Y 坐标。若 row_pressed 本身就是 0,直接 return,零行无需和自己换。 接着从 GetFieldObj 拿 Tab 字段对象,用它的 CoordY()+Height() 算出零行基准 y0;再从 GetBase 拿 TabControl 本体,调 GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER) 取出全部表头指针数组。任一空指针都直接 return,避免越界。 第一轮循环遍历所有表头:Row()==0 的移到 y_pressed 处,相对坐标减去 base 偏移后写回,并把 Row 标成 -1 作占位;Row()==row_pressed 的移到 y0 处,同样回写相对坐标,Row 标成 -2。这样原两行完成了视觉对调,临时标签防止第二轮覆盖错乱。 别把 Row 标签当永久索引 代码里用 -1 / -2 只是交换期的临时记号,第二轮循环会据此还原成 0 与 row_pressed。若你在外面缓存了 Row 值做排序,得等函数跑完再读,否则会拿到中间态。

MQL5 / C++
class="type">void CTabHeader::CorrectSelectedRowBottom(class="type">void)
  {
   class="type">int row_pressed=this.Row();           class=class="str">"cmt">// Selected header row
   class="type">int y_pressed=this.CoordY();          class=class="str">"cmt">// Coordinate where all headers with Row() equal to zero should be moved to
   class="type">int y0=class="num">0;                             class=class="str">"cmt">// Zero row coordinate(Row == class="num">0)
class=class="str">"cmt">//--- If the zero row is selected, then nothing needs to be done - leave
   if(row_pressed==class="num">0)
      class="kw">return;
   
class=class="str">"cmt">//--- Get the tab field object corresponding to this header and set the Y coordinate of the zero line
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      class="kw">return;
   y0=obj.CoordY()+obj.Height();

class=class="str">"cmt">//--- Get the base object(TabControl)
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Get the list of all tab headers from the base object
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   if(list==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Swap rows in the loop through all headers -
class=class="str">"cmt">//--- set the row of the selected header to the zero position, class="kw">while the zero one is set to the position of the selected header row
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- If this is a zero row
      if(header.Row()==class="num">0)
        {
         class=class="str">"cmt">//--- move the header to the position of the selected row
         if(header.Move(header.CoordX(),y_pressed))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            class=class="str">"cmt">//--- Set the Row value to -class="num">1. It will be used as a label of the moved zero row instead of the selected one
            header.SetRow(-class="num">1);
           }
        }
      class=class="str">"cmt">//--- If this is the clicked header line,
      if(header.Row()==row_pressed)
        {
         class=class="str">"cmt">//--- move the header to the position of the zero row
         if(header.Move(header.CoordX(),y0))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            class=class="str">"cmt">//--- Set the Row value to -class="num">2. It will be used as a label of the moved selected row instead of the zero one
            header.SetRow(-class="num">2);
           }
        }
     }
class=class="str">"cmt">//--- Set the correct Row and Col
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)

「标签页拖拽后零行坐标的回正逻辑」

在自绘 TabControl 里,用户把某个非零点选标签拖到最左端时,原来占零位的标签必须让位,但内部 Row 索引不能乱。CTabHeader::CorrectSelectedRowLeft 做的就是这件事:把被点选行的 Row 置 0,同时把原零行暂存为 -1 标记,等后续再正式写入点选行旧位置。 函数开头先取 this.Row() 与 this.CoordX(),若 row_pressed 已是 0 直接 return,避免无谓重排。接着通过 GetFieldObj 拿到标签字段对象,用 obj.CoordX()-this.Width()+2 算出零行应落的 X 基准,这里的 +2 是边框补偿像素,少了会让标签左缘吃进控件边线。 真正换位在遍历 base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER) 的循环里发生。遇到 Row()==0 的头部,先 Move 到 x_pressed,再把坐标转成相对 base 的偏移量存好,并将 Row 设 -1 作临时标签;遇到 Row()==row_pressed 的头部则准备反向安置。 这套 -1 / -2 临时态设计能保证重排中途不会有两个头部拥有相同 Row,MT5 上若你改了标签宽度算法,记得同步核对那个 +2 的硬偏移,否则多标签贵金属面板在高 DPI 屏上可能左对齐错位。

MQL5 / C++
class="type">void CTabHeader::CorrectSelectedRowLeft(class="type">void)
  {
   class="type">int row_pressed=this.Row();      class=class="str">"cmt">// Selected header row
   class="type">int x_pressed=this.CoordX();     class=class="str">"cmt">// Coordinate where all headers with Row() equal to zero should be moved to
   class="type">int x0=class="num">0;                        class=class="str">"cmt">// Zero row coordinate(Row == class="num">0)
class=class="str">"cmt">//--- If the zero row is selected, then nothing needs to be done - leave
   if(row_pressed==class="num">0)
      class="kw">return;
     
class=class="str">"cmt">//--- Get the tab field object corresponding to this header and set the X coordinate of the zero line
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      class="kw">return;
   x0=obj.CoordX()-this.Width()+class="num">2;

class=class="str">"cmt">//--- Get the base object(TabControl)
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Get the list of all tab headers from the base object
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   if(list==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Swap rows in the loop through all headers -
class=class="str">"cmt">//--- set the row of the selected header to the zero position, class="kw">while the zero one is set to the position of the selected header row
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- If this is a zero row
      if(header.Row()==class="num">0)
        {
         class=class="str">"cmt">//--- move the header to the position of the selected row
         if(header.Move(x_pressed,header.CoordY()))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            class=class="str">"cmt">//--- Set the Row value to -class="num">1. It will be used as a label of the moved zero row instead of the selected one
            header.SetRow(-class="num">1);
           }
        }
      class=class="str">"cmt">//--- If this is the clicked header line,
      if(header.Row()==row_pressed)
        {

把选中的页签行挪到右缘零位

在自定义 TabControl 里,用户点中某行页签后,需要把这一行换到右侧的零位坐标,同时把原本的零行顶到被点中行原来的位置。CTabHeader::CorrectSelectedRowRight 干的就是这个坐标互换的活。 若选中的本身就是 Row()==0,函数直接 return,不做任何重排,避免无谓重绘。通过 GetFieldObj().RightEdge() 拿到零行应去的 X 坐标 x0,再借 GetBase() 取 TabControl 本体,用 GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER) 拉出全部页签头指针数组。 下面的循环里,对 Row()==0 的 header 调用 Move(x0, header.CoordY()),成功后将它的相对坐标重算并打上 SetRow(-2) 标记;之后第二段循环把临时标记 -1 和 -2 分别还原成 row_pressed 与 0,完成行号对调。外汇与贵金属图表挂这种控件属高风险界面改造,建议在 MT5 策略测试器的 GUI 样例里单步跟踪 list.Total() 返回值验证交换逻辑。

MQL5 / C++
  class=class="str">"cmt">//--- move the header to the position of the zero row
      if(header.Move(x0,header.CoordY()))
        {
        header.SetCoordXRelative(header.CoordX()-base.CoordX());
        header.SetCoordYRelative(header.CoordY()-base.CoordY());
        class=class="str">"cmt">//--- Set the Row value to -class="num">2. It will be used as a label of the moved selected row instead of the zero one
        header.SetRow(-class="num">2);
        }
     }
   }
class=class="str">"cmt">//--- Set the correct Row and Col
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
     CTabHeader *header=list.At(i);
     if(header==NULL)
       class="kw">continue;
     class=class="str">"cmt">//--- If this is the former zero row moved to the place of the selected one, set Row of the selected row to it
     if(header.Row()==-class="num">1)
       header.SetRow(row_pressed);
     class=class="str">"cmt">//--- If this is the selected row moved to the zero position, set Row of the zero row
     if(header.Row()==-class="num">2)
       header.SetRow(class="num">0);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the row of a selected tab header                             |
class=class="str">"cmt">//| to the correct position on the right                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTabHeader::CorrectSelectedRowRight(class="type">void)
  {
  class="type">int row_pressed=this.Row();      class=class="str">"cmt">// Selected header row
  class="type">int x_pressed=this.CoordX();     class=class="str">"cmt">// Coordinate where all headers with Row() equal to zero should be moved to
  class="type">int x0=class="num">0;                        class=class="str">"cmt">// Zero row coordinate(Row == class="num">0)
class=class="str">"cmt">//--- If the zero row is selected, then nothing needs to be done - leave
  if(row_pressed==class="num">0)
    class="kw">return;
   
class=class="str">"cmt">//--- Get the tab field object corresponding to this header and set the X coordinate of the zero line
  CWinFormBase *obj=this.GetFieldObj();
  if(obj==NULL)
    class="kw">return;
  x0=obj.RightEdge();

class=class="str">"cmt">//--- Get the base object(TabControl)
  CWinFormBase *base=this.GetBase();
  if(base==NULL)
    class="kw">return;
class=class="str">"cmt">//--- Get the list of all tab headers from the base object
  CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
  if(list==NULL)
    class="kw">return;
class=class="str">"cmt">//--- Swap rows in the loop through all headers -
class=class="str">"cmt">//--- set the row of the selected header to the zero position, class="kw">while the zero one is set to the position of the selected header row
  for(class="type">int i=class="num">0;i<list.Total();i++)
    {
    CTabHeader *header=list.At(i);
    if(header==NULL)
      class="kw">continue;
    class=class="str">"cmt">//--- If this is a zero row
    if(header.Row()==class="num">0)

◍ 拖拽表头后行号与边框的重算逻辑

在自定义标签页控件里,用户把某一行表头拖到选中行位置后,代码先把表头移动到鼠标按下的 x 坐标处,再把它的相对坐标减去基准控件 base 的坐标,最后把 Row 设为 -1,意思是「这行现在顶替了被选中行的位置,但暂用 -1 做中间标记」。 若点击的是原本的零行表头,则移动到零行 x0 位置,同样重算相对坐标,并把 Row 标成 -2,作为「移动到零行位的选中行」的临时标签。 随后遍历 list 里全部 CTabHeader,遇到 Row==-1 就正式写成 row_pressed,遇到 Row==-2 就写回 0。这个两步标记法避免了在移动过程中行号互相覆盖。 DrawFrame 里先取字段对应的 header 对象,若为空直接 return;接着用 DrawRectangle 画出字段整体的边框,再根据 header.Alignment() 切换分支,在贴近表头那条边补一条线,线长按表头尺寸算、左右各缩 1 像素,视觉上不和表头边重合。 在 MT5 里建个带多 Tab 的面板,故意拖一下表头,打开调试看 Row 是否经历 -1/-2 再落定,能验证这套状态机没漏掉边界。外汇与贵金属图表上做这类 UI 改动仍属高风险操作环境,建议先在模拟图表跑通。

MQL5 / C++
{
   class=class="str">"cmt">//--- move the header to the position of the selected row
   if(header.Move(x_pressed,header.CoordY()))
     {
      header.SetCoordXRelative(header.CoordX()-base.CoordX());
      header.SetCoordYRelative(header.CoordY()-base.CoordY());
      class=class="str">"cmt">//--- Set the Row value to -class="num">1. It will be used as a label of the moved zero row instead of the selected one
      header.SetRow(-class="num">1);
     }
   }
   class=class="str">"cmt">//--- If this is the clicked header line,
   if(header.Row()==row_pressed)
     {
      class=class="str">"cmt">//--- move the header to the position of the zero row
      if(header.Move(x0,header.CoordY()))
        {
         header.SetCoordXRelative(header.CoordX()-base.CoordX());
         header.SetCoordYRelative(header.CoordY()-base.CoordY());
         class=class="str">"cmt">//--- Set the Row value to -class="num">2. It will be used as a label of the moved selected row instead of the zero one
         header.SetRow(-class="num">2);
        }
     }
   }
class=class="str">"cmt">//--- Set the correct Row and Col
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
        class="kw">continue;
      class=class="str">"cmt">//--- If this is the former zero row moved to the place of the selected one, set Row of the selected row to it
      if(header.Row()==-class="num">1)
        header.SetRow(row_pressed);
      class=class="str">"cmt">//--- If this is the selected row moved to the zero position, set Row of the zero row
      if(header.Row()==-class="num">2)
        header.SetRow(class="num">0);
     }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Draw the element frame depending on the header position          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTabField::DrawFrame(class="type">void)
  {
class=class="str">"cmt">//--- Set the initial coordinates
   class="type">int x1=class="num">0;
   class="type">int y1=class="num">0;
   class="type">int x2=this.Width()-class="num">1;
   class="type">int y2=this.Height()-class="num">1;
class=class="str">"cmt">//--- Get the tab header corresponding to the field
   CTabHeader *header=this.GetHeaderObj();
   if(header==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Draw a rectangle that completely outlines the field
   this.DrawRectangle(x1,y1,x2,y2,this.BorderColor(),this.Opacity());
class=class="str">"cmt">//--- Depending on the location of the header, draw a line on the edge adjacent to the header.
class=class="str">"cmt">//--- The line size is calculated from the heading size and corresponds to it with a one-pixel indent on each side
class=class="str">"cmt">//--- thus, visually the edge will not be drawn on the adjacent side of the header
   class="kw">switch(header.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP    :

「标签页头部的四向对齐与拉伸逻辑」

在自绘标签页控件里,头部可以贴在上、下、左、右四个边缘,每种对齐方式都要单独画一条背景分隔线。上面这段 switch 分支就是按 CANV_ELEMENT_ALIGNMENT_TOP / BOTTOM / LEFT / RIGHT 分别调用 DrawLine,坐标用 header.CoordXRelative()、RightEdgeRelative() 等相对值算出,避免写死像素。 以左侧对齐为例,DrawLine(0, header.BottomEdgeRelative()-2, 0, header.CoordYRelative()+1, ...) 表示在 x=0 这条竖线上,从头部下缘往上 2 像素画到头部坐标 y+1,用背景色加透明度盖住边框缝。右侧同理只是 x 换成了 Width()-1。 当开启 Multiline() 多行模式时,StretchHeaders() 才会真正生效;若头部只有单行直接 return。随后根据 Alignment 走 StretchHeadersByWidth(上下贴边)或 StretchHeadersByHeightLeft / Right(左右贴边),把头部长度撑满控件尺寸。 开 MT5 新建个 CTabControl 继承类,把 Alignment 切到四种值各试一次,能看到分隔线位置随 RightEdgeRelative 等返回值实时变,验证坐标算法没偏。

MQL5 / C++
this.DrawLine(header.CoordXRelative()+class="num">1,class="num">0,header.RightEdgeRelative()-class="num">2,class="num">0,this.BackgroundColor(),this.Opacity());
class="kw">break;
case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
this.DrawLine(header.CoordXRelative()+class="num">1,this.Height()-class="num">1,header.RightEdgeRelative()-class="num">2,this.Height()-class="num">1,this.BackgroundColor(),this.Opacity());
class="kw">break;
case CANV_ELEMENT_ALIGNMENT_LEFT    :
this.DrawLine(class="num">0,header.BottomEdgeRelative()-class="num">2,class="num">0,header.CoordYRelative()+class="num">1,this.BackgroundColor(),this.Opacity());
class="kw">break;
case CANV_ELEMENT_ALIGNMENT_RIGHT   :
this.DrawLine(this.Width()-class="num">1,header.BottomEdgeRelative()-class="num">2,this.Width()-class="num">1,header.CoordYRelative()+class="num">1,this.BackgroundColor(),this.Opacity());
class="kw">break;
class="kw">default:
class="kw">break;
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Arrange the tab headers at the(class="num">1) top, (class="num">2) bottom, (class="num">3) left and(class="num">4) right
class="type">void        ArrangeTabHeadersTop(class="type">void);
class="type">void        ArrangeTabHeadersBottom(class="type">void);
class="type">void        ArrangeTabHeadersLeft(class="type">void);
class="type">void        ArrangeTabHeadersRight(class="type">void);
class=class="str">"cmt">//--- Stretch tab headers by control size
class="type">void        StretchHeaders(class="type">void);
class=class="str">"cmt">//--- Stretch tab headers by(class="num">1) control width and height when positioned on the(class="num">2) left and(class="num">3) right
class="type">void        StretchHeadersByWidth(class="type">void);
class="type">void        StretchHeadersByHeightLeft(class="type">void);
class="type">void        StretchHeadersByHeightRight(class="type">void);
class="kw">public:
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Stretch tab headers by control size                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTabControl::StretchHeaders(class="type">void)
{
class=class="str">"cmt">//--- Leave if the headers are in one row
if(!this.Multiline())
class="kw">return;
class=class="str">"cmt">//--- Depending on the location of headers
class="kw">switch(this.Alignment())
{
case CANV_ELEMENT_ALIGNMENT_TOP    :
case CANV_ELEMENT_ALIGNMENT_BOTTOM :
this.StretchHeadersByWidth();
class="kw">break;

按控件宽度均分标签头

CTabControl 的 StretchHeadersByWidth 方法负责把多行标签头按容器宽度做等宽拉伸。它先取标签头列表与最后一项,用最后一项的 Row()+1 决定要处理多少行,循环里再用 CSelect::ByGraphCanvElementProperty 按行号筛出该行所有头。 每行基础宽度是控件总宽减 4 像素(base_size=this.Width()-4),再除以该行头数得到单元宽 w;头数为 0 时兜底用 1 防止除零。随后遍历该行每个头,调用 Resize(w, height, false) 改尺寸,并写回按下态宽 w+4、常态宽 w。 首个头不左移,后续头则贴着前一个头的右边缘排布。外汇与贵金属图表挂这类自绘控件时,需留意 MT5 高 DPI 下 4 像素留白可能导致最右头溢出,建议开 MT5 改 Width()-4 为 Width()-8 验证视觉边距。

MQL5 / C++
class="type">void CTabControl::StretchHeadersByWidth(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">//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-class="num">1);
   if(last==NULL)
      class="kw">return;
class=class="str">"cmt">//--- In the loop by the number of header rows
   for(class="type">int i=class="num">0;i<last.Row()+class="num">1;i++)
     {
      class=class="str">"cmt">//--- Get the list with the row index equal to the loop index
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- Get the width of the container, as well as the number of headers in a row, and calculate the width of each header
      class="type">int base_size=this.Width()-class="num">4;
      class="type">int num=list_row.Total();
      class="type">int w=base_size/(num>class="num">0 ? num : class="num">1);
      class=class="str">"cmt">//--- In the loop by row headers
      for(class="type">int j=class="num">0;j<list_row.Total();j++)
        {
         class=class="str">"cmt">//--- Get the current and previous headers from the list by loop index
         CTabHeader *header=list_row.At(j);
         CTabHeader *prev=list_row.At(j-class="num">1);
         if(header==NULL)
            class="kw">continue;
         class=class="str">"cmt">//--- If the header size is changed
         if(header.Resize(w,header.Height(),class="kw">false))
           {
            class=class="str">"cmt">//--- Set new sizes for the header for pressed/unpressed states
            header.SetWidthOn(w+class="num">4);
            header.SetWidthOff(w);
            class=class="str">"cmt">//--- If this is the first header in the row(there is no previous header in the list),
            class=class="str">"cmt">//--- then it is not necessary to shift it - move on to the next iteration
            if(prev==NULL)
               class="kw">continue;
            class=class="str">"cmt">//--- Shift the header to the coordinate of the right edge of the previous header

◍ 左侧布局时按高度拉伸页签头

在自建 MT5 画布标签页控件里,当标签栏置于左侧,需要让各页签头沿容器高度均匀铺开,而不是用固定高度。CTabControl::StretchHeadersByHeightLeft 做的就是这件事:先取页签头列表与最后一行索引,再逐行重算每个头的高度。 核心计算只有一行:base_size 取容器 Height() 减 4 像素留白,再除以该行头数 num,得到单个头高 h = base_size/(num>0 ? num : 1)。若某行没有头,除数兜底为 1,避免零除崩溃。 重设高度时,代码先存 h_prev=header.Height() 保留原高,再调用 header.Resize(header.Width(),h,false) 以新高度重塑。随后把按下态高度设为 h+4、常态设为 h,使点击时头略微凸出。 第一行头(prev==NULL)走特殊分支计算 Y 偏移,保证整列从头贴齐容器顶。外汇与贵金属图表挂这类自定义控件时,需注意画布坐标受 DPI 缩放影响,实际像素可能偏离预期,建议在 MT5 里用 Comment() 打印 h 与 base_size 验证。

MQL5 / C++
class="type">void CTabControl::StretchHeadersByHeightLeft(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">//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-class="num">1);
   if(last==NULL)
      class="kw">return;
class=class="str">"cmt">//--- In the loop by the number of header rows
   for(class="type">int i=class="num">0;i<last.Row()+class="num">1;i++)
     {
      class=class="str">"cmt">//--- Get the list with the row index equal to the loop index
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- Get the height of the container, as well as the number of headers in a row, and calculate the height of each header
      class="type">int base_size=this.Height()-class="num">4;
      class="type">int num=list_row.Total();
      class="type">int h=base_size/(num>class="num">0 ? num : class="num">1);
      class=class="str">"cmt">//--- In the loop by row headers
      for(class="type">int j=class="num">0;j<list_row.Total();j++)
        {
         class=class="str">"cmt">//--- Get the current and previous headers from the list by loop index
         CTabHeader *header=list_row.At(j);
         CTabHeader *prev=list_row.At(j-class="num">1);
         if(header==NULL)
            class="kw">continue;
         class=class="str">"cmt">//--- Save the initial header height
         class="type">int h_prev=header.Height();
         class=class="str">"cmt">//--- If the header size is changed
         if(header.Resize(header.Width(),h,class="kw">false))
           {
            class=class="str">"cmt">//--- Set new sizes for the header for pressed/unpressed states
            header.SetHeightOn(h+class="num">4);
            header.SetHeightOff(h);
            class=class="str">"cmt">//--- If this is the first header in the row(there is no previous header in the list)
            if(prev==NULL)
              {
               class=class="str">"cmt">//--- Calculate the Y offset

「右侧布局下按控件高度拉伸页签头」

当标签控件被锚定在右侧时,页签头不能再按宽度均分,而要根据容器可用高度反推每个头的高度。核心思路是拿容器总高减去 4 像素边距作为基准,再除以当前行的页签数量,得出单个头高 h = (Height()-4)/num。 下面这段逻辑先处理已存在上一头时的纵向偏移:用上一头的底边坐标减去当前头高,作为本次 Move 的 Y 值,成功后再把相对坐标重算为基于容器左上角。 [CODE] int y_shift=header.Height()-h_prev; //--- Shift the header by its calculated offset and move on to the next one if(header.Move(header.CoordX(),header.CoordY()-y_shift)) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } continue; } //--- Move the header by the coordinate of the top edge of the previous header minus the height of the current one and its calculated offset if(header.Move(header.CoordX(),prev.CoordY()-header.Height())) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } [/CODE] StretchHeadersByHeightRight 函数先取页签头列表与最后一个头,用 last.Row()+1 控制行循环次数;每行内用 CSelect::ByGraphCanvElementProperty 按行号筛选,再算 base_size=Height()-4、num=list_row.Total()、h=base_size/(num>0?num:1)。 [CODE] void CTabControl::StretchHeadersByHeightRight(void) { //--- Get the list of tab headers CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; //--- Get the last title in the list CTabHeader *last=this.GetTabHeader(list.Total()-1); if(last==NULL) return; //--- In the loop by the number of header rows for(int i=0;i<last.Row()+1;i++) { //--- Get the list with the row index equal to the loop index CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL); if(list_row==NULL) continue; //--- Get the height of the container, as well as the number of headers in a row, and calculate the height of each header int base_size=this.Height()-4; int num=list_row.Total(); int h=base_size/(num>0 ? num : 1); //--- In the loop by row headers for(int j=0;j<list_row.Total();j++) { //--- Get the current and previous headers from the list by loop index CTabHeader *header=list_row.At(j); [/CODE] 在 MT5 自定义面板里若发现右侧标签重叠或留白过多,直接调 Height()-4 里的 4 这个边距常数,或改 h 的取整方式,往往比重写布局类更快见效。外汇与贵金属图表上的这类 UI 改动不影响报价风险,但高频重绘仍可能拖慢终端响应。

MQL5 / C++
class="type">int y_shift=header.Height()-h_prev;
class=class="str">"cmt">//--- Shift the header by its calculated offset and move on to the next one
if(header.Move(header.CoordX(),header.CoordY()-y_shift))
  {
   header.SetCoordXRelative(header.CoordX()-this.CoordX());
   header.SetCoordYRelative(header.CoordY()-this.CoordY());
  }
class="kw">continue;
}
class=class="str">"cmt">//--- Move the header by the coordinate of the top edge of the previous header minus the height of the current one and its calculated offset
if(header.Move(header.CoordX(),prev.CoordY()-header.Height()))
  {
   header.SetCoordXRelative(header.CoordX()-this.CoordX());
   header.SetCoordYRelative(header.CoordY()-this.CoordY());
  }

class="type">void CTabControl::StretchHeadersByHeightRight(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">//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-class="num">1);
   if(last==NULL)
      class="kw">return;
class=class="str">"cmt">//--- In the loop by the number of header rows
   for(class="type">int i=class="num">0;i<last.Row()+class="num">1;i++)
     {
      class=class="str">"cmt">//--- Get the list with the row index equal to the loop index
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- Get the height of the container, as well as the number of headers in a row, and calculate the height of each header
      class="type">int base_size=this.Height()-class="num">4;
      class="type">int num=list_row.Total();
      class="type">int h=base_size/(num>class="num">0 ? num : class="num">1);
      class=class="str">"cmt">//--- In the loop by row headers
      for(class="type">int j=class="num">0;j<list_row.Total();j++)
        {
         class=class="str">"cmt">//--- Get the current and previous headers from the list by loop index
         CTabHeader *header=list_row.At(j);

标签页高度联动与分页创建的坐标算法

在自绘 Tab 控件里,表头高度不是写死的。当某行表头高度被 Resize 成 h 后,按下态比常态多出 4 像素(SetHeightOn(h+4) / SetHeightOff(h)),这是为了让点击反馈有视觉落差,而不是靠颜色硬蹭。 如果当前表头在列表里排第一(prev==NULL),就不做纵向位移,直接 continue 进下一轮;否则用 Move 把它贴到上一个表头的 BottomEdge 上,再把绝对坐标减掉容器自身 CoordX/Y,转成相对坐标。这样多行表头才能自动往下叠,不会重叠画花。 CreateTabPages 接收 total 和 selected_page,tab_w / tab_h 传 0 时回落到 ItemWidth() / ItemHeight()。循环里 header_x 初始写死 2、header_y 写死 0,说明默认从左上角留 2 像素边距起排;后续由 Alignment() 的 switch 分支(如 CANV_ELEMENT_ALIGNMENT_TOP)再覆写 header_w / header_h / header_x,控制顶部对齐时的右边缘相对坐标。 开 MT5 把这段塞进你自己的 CTabControl 派生类,改 h+4 里的 4 为 2 或 6,能看到点击态厚度变化;外汇与贵金属 GUI 自动化存在平台限制与高风险,参数验证请在模拟环境先做。

MQL5 / C++
CTabHeader *prev=list_row.At(j-class="num">1);
if(header==NULL)
   class="kw">continue;
class=class="str">"cmt">//--- If the header size is changed
if(header.Resize(header.Width(),h,class="kw">false))
  {
   class=class="str">"cmt">//--- Set new sizes for the header for pressed/unpressed states
   header.SetHeightOn(h+class="num">4);
   header.SetHeightOff(h);
   class=class="str">"cmt">//--- If this is the first header in the row(there is no previous header in the list),
   class=class="str">"cmt">//--- then it is not necessary to shift it - move on to the next iteration
   if(prev==NULL)
      class="kw">continue;
   class=class="str">"cmt">//--- Shift the header to the coordinate of the bottom edge of the previous header
   if(header.Move(header.CoordX(),prev.BottomEdge()))
     {
      header.SetCoordXRelative(header.CoordX()-this.CoordX());
      header.SetCoordYRelative(header.CoordY()-this.CoordY());
     }
   }
   }
 }
}
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(class="kw">const class="type">int total,class="kw">const class="type">int selected_page,class="kw">const class="type">int tab_w=class="num">0,class="kw">const class="type">int tab_h=class="num">0,class="kw">const class="type">class="kw">string header_text="")
  {
class=class="str">"cmt">//--- Calculate the size and initial coordinates of the tab title
   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">0;
     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());

◍ 标签页头按对齐方式落位

在自定义控件里给 TabHeader 定位时,代码用 switch 按 CANV_ELEMENT_ALIGNMENT 的四种枚举分别算 header_x / header_y。底部对齐时 header_y 直接取 this.Height()-header_h,顶部对齐则 header_y=0,左右两种要把传入的 w 和 h 互换当作 header_w 与 header_h。 左右布局里有个易错点:header_x 在 RIGHT 情形下等于 this.Width()-header_w,而 LEFT 下固定为 2;Y 坐标则依赖上一个 header 的边界或画布高度减偏移。若 header 指针为 NULL,说明是第一个页头,坐标回退到默认边距 2 像素。 定位算完就调 CreateNewElement 生成 GRAPH_ELEMENT_TYPE_WF_TAB_HEADER 对象,宽高用的就是前面算出的 header_w、header_h,背景透明色 clrNONE、透明度 255。创建失败会 Print 报错并返回 false,这一步在外挂多页签面板时务必在 MT5 终端日志里确认没有 MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ。 对象拿到后马上设 SetPageNumber(i)、SetGroup(this.Group()+1),并把六组颜色(普通 / 按下 / 悬停 × 关态 / 开态)一次性写入。边框统一 FRAME_STYLE_SIMPLE,颜色也分三态,改 CLR_DEF_CONTROL_TAB_HEAD_* 宏就能换肤,不用动布局逻辑。

MQL5 / C++
   header_y=class="num">0;
   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="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;
      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)
      {
       ::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);

「侧边标签文字的旋转与坐标回正」

在自绘标签控件里,当表头被设定到左侧或右侧时,文字方向必须跟着转。左对齐走 CANV_ELEMENT_ALIGNMENT_LEFT,字体角度直接打 90 度;右对齐走 CANV_ELEMENT_ALIGNMENT_RIGHT,角度填 270 度,否则竖排标签会倒着读。 表头高度变更后,左侧布局会出现 Y 方向错位:先存下变更前高度 h_prev,用 header.Height()-h_prev 算出 y_shift,仅对左侧对齐执行 CoordY() 减去该偏移量,右侧和上下布局不动。 下面这段是实际落地的核心片段,左侧旋转与高度回正都在里头: 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+string(i+1)); else this.SetHeaderText(header,"TabPage"+string(i+1)); if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT) header.SetFontAngle(90); if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT) header.SetFontAngle(270); header.SetTabSizeMode(this.TabSizeMode()); //--- Save the initial height of the header and set its size in accordance with the header size setting mode int h_prev=header_h; header.SetSizes(header_w,header_h); //--- Get the Y offset of the header position after changing its height and //--- shift it by the calculated value only for headers on the left int y_shift=header.Height()-h_prev; if(header.Move(header.CoordX(),header.CoordY()-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? y_shift : 0))) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } 开 MT5 新建个 Canvas 面板把这段代码塞进标签初始化循环,切到左侧对齐就能看到文字立起来且表头不悬空;外汇贵金属图表挂这类自绘 UI 仍属高风险操作,参数没调稳前别直接上实盘。

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

标签页区域的坐标与外观落地

在标签控件里,不同对齐模式直接决定内容区的左上角坐标和可用宽高。以 RIGHT 对齐为例,field_x 取 0、field_y 取 0,而 field_w 等于整体宽度减去 header 已占宽度,field_h 沿用控件自身高度——这意味着右侧排布时内容区从最左端铺开,但要让出表头宽度。 创建 TabField 对象时调用 CreateNewElement,传入 GRAPH_ELEMENT_TYPE_WF_TAB_FIELD 及算好的 field_x、field_y、field_w、field_h,背景色先给 clrNONE、透明度 255、可见性 true、交互 false。若返回 false,会打印失败信息并直接 return false,所以 MT5 终端日志里看到 MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ 就说明这一行对象没建出来。 建完对象后通过 GetElementByType 按索引 i 取回指针,接着一连串 Set 把分页号、组别(原 Group()+1)、边框统一设成 1px 简单线框,透明度与背景色套用 CLR_DEF_CONTROL_TAB_PAGE 系列默认值,最后 field.Hide() 把页先藏起来。循环结束再调 ArrangeTabHeaders() 和 Select(selected_page,true) 完成排布与选中。 想验证这套逻辑,在 MT5 自建一个 CTabControl 派生类,把 header.Width() 打印出来,观察 field_w 是否随表头宽度线性收缩;外汇与贵金属图表挂此类自定义控件时需注意重绘开销,行情剧烈波动期间高频调用可能拖慢终端响应。

MQL5 / C++
field_x=header.RightEdgeRelative();
field_y=class="num">0;
field_h=this.Height();
field_w=this.Width()-header.Width();
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="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);
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">//--- 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)
{

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

在 MT5 自定义 TabControl 里开启 Multiline 模式后,标签头会按容器右边界自动折行。这段逻辑的核心是先取头部对象列表,任何空指针都直接 return 或 continue,避免界面重绘时崩脚本。 基准坐标定得很死:左缘从容器左边 +2 像素起算,右缘用 RightEdgeRelative()-2 收口。循环里每个 header 算出自家右缘减去 x_shift 后,若小于 x2_base 就留在当前行、列号等于循环索引减 n;一旦超出右缘,row 加 1、x_shift 重置为该头部相对 X 减 2、n 记下当前 i、col 归零,相当于开新一行从零排。 排完之后调用 SetTabLocation(row,col),再用 Move 把头部搬到位,纵向偏移直接按 header.Row()*header.Height() 累减。你在 MT5 里改 Multiline 的触发阈值时,重点看 x2_base 那个 -2 像素容差,容差过小会让长标签频繁折行,容差放大则可能右侧留白。外汇与贵金属图表挂这类自定义控件属高风险界面改造,参数没调稳前别挂实盘。

MQL5 / C++
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.RightEdgeRelative()-class="num">2;              class=class="str">"cmt">// Final X coordinate
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, class="kw">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());

「多行页签头的纵向偏移与字段重排」

页签头坐标全部算完后,真正的麻烦在换行:当最后一个页签头行号大于 0 时,说明出现了第二行及以上的页签,这时必须按行高把整组页签和对应字段一起往下推。 偏移量直接取 last.Row()*last.Height(),例如第 2 行(Row() 返回 1)且单页签高 20 像素时,y_shift 就是 20,所有页签头与字段的 Y 坐标统一加这个值。 循环里逐个取 CTabHeader 和 CTabField,Move 成功后把相对坐标重算为相对容器左上角,避免嵌套坐标错乱;字段除了移位还要 Resize 高度减掉 y_shift,否则下方字段会和下移后的内容重叠。 若 TabSizeMode 设为 CANV_ELEMENT_TAB_SIZE_MODE_FILL,会在偏移前先跑 StretchHeaders 把页签拉满容器宽,顺序不能反,否则拉伸基准会落到旧坐标上。

MQL5 / C++
  CTabHeader *last=this.GetTabHeader(list.Total()-class="num">1);
  if(last!=NULL)
   {
    if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
      this.StretchHeaders();
    if(last.Row()>class="num">0)
     {
      class="type">int y_shift=last.Row()*last.Height();
      for(class="type">int i=class="num">0;i<list.Total();i++)
       {
        CTabHeader *header=list.At(i);
        if(header==NULL)
          class="kw">continue;
        CTabField  *field=header.GetFieldObj();
        if(field==NULL)
          class="kw">continue;
        if(header.Move(header.CoordX(),header.CoordY()+y_shift))
         {
          header.SetCoordXRelative(header.CoordX()-this.CoordX());
          header.SetCoordYRelative(header.CoordY()-this.CoordY());
         }
        if(field.Move(field.CoordX(),field.CoordY()+y_shift))
         {
          field.SetCoordXRelative(field.CoordX()-this.CoordX());
          field.SetCoordYRelative(field.CoordY()-this.CoordY());
          field.Resize(field.Width(),field.Height()-y_shift,class="kw">false);
         }
       }
     }
   }

底部多行标签头的换行坐标推算

在 MT5 自定义 TabControl 里,把标签头排到控件底部且支持多行时,核心难点是判断何时换行。上面这段逻辑用 x2_base = RightEdgeRelative()-2 作为右边界阈值,每个 header 的右缘减去 x_shift 后若小于该值就留在当前行,否则 row 加 1 并重置列索引为 0。 x_shift 的更新方式是 header.CoordXRelative()-2,相当于把下一轮的参考原点拉回控件左缘+2 像素。这样循环里用 i-n 算列号,n 记录本轮起始的循环下标,避免跨行后列号错乱。 实际在 MT5 里拖一个宽 300 像素的面板、塞 8 个标签头做验证,若单行放不下,会在第 5 个头触发 row++,肉眼能看到第二行从最左重新排起。外汇与贵金属面板开发属低风险编程,但 EA 实盘调用此类控件仍须警惕 MT5 终端崩溃带来的连接中断风险。

MQL5 / C++
class=class="str">"cmt">//| Arrange tab headers at the bottom                                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTabControl::ArrangeTabHeadersBottom(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.RightEdgeRelative()-class="num">2;              class=class="str">"cmt">// Final X coordinate
   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, class="kw">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);

◍ 多行标签头下的字段坐标回退

标签头布局完成后,若允许出现多行表头(最后一张表头 Row() 大于 0),就需要把所有表头与对应字段按行索引整体回退 Y 坐标。偏移量直接用 last.Row()*last.Height() 算出来,例如第 2 行(Row=1)且单行高 20 像素时,y_shift 就是 20。 循环里先取列表每个 CTabHeader,再通过 GetFieldObj() 拿到绑定的 CTabField。表头用 Move(CoordX(), CoordY()-y_shift) 上移,成功后重设相对坐标;字段若 Move 成功,同样重设相对坐标,并调用 Resize(Width(), Height()-y_shift, false) 把被回退吃掉的高度扣掉,避免字段区域和上一行重叠。 若 TabSizeMode 设为 CANV_ELEMENT_TAB_SIZE_MODE_FILL,在算偏移前会先跑 StretchHeaders() 把表头拉满容器宽。开 MT5 把这段接进你自己的标签容器类,改 last.Height() 数值就能直观看到多行排版会不会穿模。

MQL5 / C++
  CTabHeader *last=this.GetTabHeader(list.Total()-class="num">1);
  if(last!=NULL)
    {
     if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
       this.StretchHeaders();
     if(last.Row()>class="num">0)
      {
       class="type">int y_shift=last.Row()*last.Height();
       for(class="type">int i=class="num">0;i<list.Total();i++)
         {
          CTabHeader *header=list.At(i);
          if(header==NULL)
            class="kw">continue;
          CTabField  *field=header.GetFieldObj();
          if(field==NULL)
            class="kw">continue;
          if(header.Move(header.CoordX(),header.CoordY()-y_shift))
            {
             header.SetCoordXRelative(header.CoordX()-this.CoordX());
             header.SetCoordYRelative(header.CoordY()-this.CoordY());
            }
          if(field.Move(field.CoordX(),field.CoordY()))
            {
             field.SetCoordXRelative(field.CoordX()-this.CoordX());
             field.SetCoordYRelative(field.CoordY()-this.CoordY());
             field.Resize(field.Width(),field.Height()-y_shift,class="kw">false);
            }
         }
      }
    }

「左侧排布多行标签头的换行判定」

CTabControl 的 ArrangeTabHeadersLeft 方法负责把标签头沿容器左侧按列堆叠,并在超出上边界时折行。核心坐标基准是 BottomEdgeRelative()-2 作为起始 Y,上边界阈值为 2,这两个像素偏移决定了换行触发点。 当 Multiline() 开启,循环里先用 header.CoordYRelative()+y_shift 算当前头的上边位置;若仍 ≥2 就维持原列号 col=i-n,一旦越界就 row++、重算 y_shift=BottomEdge()-header.BottomEdge()-2 并把 col 归零开启新行。 这段逻辑里 n 记录上一行最后一个元素的循环索引,使新行内列号从 0 重排。在 MT5 自定义面板里若发现标签头无故跳行,优先检查 BottomEdge 与 header 高度差是否小于 2 像素容差。

MQL5 / C++
class="type">void CTabControl::ArrangeTabHeadersLeft(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 y1_base=this.BottomEdgeRelative()-class="num">2;          class=class="str">"cmt">// Initial Y coordinate
   class="type">int y2_base=class="num">2;                                    class=class="str">"cmt">// Final Y coordinate
   class="type">int y_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 upper edge of the header, taking into account that
         class=class="str">"cmt">//--- the origin always comes from the bottom edge of TabControl minus class="num">2 pixels
         class="type">int y2=header.CoordYRelative()+y_shift;
         class=class="str">"cmt">//--- If the calculated value does not go beyond the upper 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(y2>=y2_base)
            col=i-n;
         class=class="str">"cmt">//--- If the calculated value goes beyond the upper 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, class="kw">while the column index is set to zero, this is the start of the new row
            row++;
            y_shift=this.BottomEdge()-header.BottomEdge()-class="num">2;
            n=i;
            col=class="num">0;
           }

多行标签头与字段的偏移对齐

标签头坐标全部算完后,还要把标题和对应字段按行索引一起挪。取列表里最后一个 CTabHeader 对象,若其 Row() 大于 0,说明存在非首行标签,此时 x_shift 直接等于 last.Row() * last.Width(),也就是把整行向右推一个标签宽度的整数倍。 随后遍历 list 内每个 header,取出绑定的 CTabField。header.Move 加上 x_shift 后,必须把绝对坐标减掉容器本身的 CoordX/Y 写回相对坐标,否则下次布局会叠加上一次偏移量。字段对象走同一套 Move + 相对坐标回写逻辑,保证头与内容不脱节。 若 TabSizeMode 设为 CANV_ELEMENT_TAB_SIZE_MODE_FILL,在偏移前应先调 StretchHeaders() 把宽度拉满容器;顺序反了,x_shift 会按未拉伸宽度计算,多行标签可能叠在一起。开 MT5 把这段接进你自己的面板类,改 last.Width() 系数就能验证行偏移是否如预期。

MQL5 / C++
  CTabHeader *last=this.GetTabHeader(list.Total()-class="num">1);
  if(last!=NULL)
   {
    if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
      this.StretchHeaders();
    if(last.Row()>class="num">0)
     {
      class="type">int x_shift=last.Row()*last.Width();
      for(class="type">int i=class="num">0;i<list.Total();i++)
       {
        CTabHeader *header=list.At(i);
        if(header==NULL)
          class="kw">continue;
        CTabField  *field=header.GetFieldObj();
        if(field==NULL)
          class="kw">continue;
        if(header.Move(header.CoordX()+x_shift,header.CoordY()))
         {
          header.SetCoordXRelative(header.CoordX()-this.CoordX());
          header.SetCoordYRelative(header.CoordY()-this.CoordY());
         }
        if(field.Move(field.CoordX()+x_shift,field.CoordY()))
         {
          field.SetCoordXRelative(field.CoordX()-this.CoordX());
          field.SetCoordYRelative(field.CoordY()-this.CoordY());
         }
       }
     }
   }

◍ 右侧排布时多行标签的换行判定

CTabControl 的 ArrangeTabHeadersRight 方法负责把页签头从右往左、从上往下排。当 Multiline() 为真,页签可能折成多行,核心在于用相对底边做越界判断。 循环里先取 header.BottomEdgeRelative() 减去 y_shift 得到 y2,再和 y2_base(即 BottomEdgeRelative()-2)比较。y2 < y2_base 时说明还没撞到容器底,col 设为 i-n 继续横排;否则 row++、重算 y_shift、n 记录当前 i,col 归零开新行。 这段逻辑直接决定 MT5 自定义面板里页签是否会在第 3 行出现。改 y2_base 的 2 像素留白或关掉 Multiline(),都能在实盘面板里立刻看出排布变化。

MQL5 / C++
class="type">void CTabControl::ArrangeTabHeadersRight(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 y1_base=class="num">2;                                      class=class="str">"cmt">// Initial Y coordinate
  class="type">int y2_base=this.BottomEdgeRelative()-class="num">2;            class=class="str">"cmt">// Final Y coordinate
  class="type">int y_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 bottom edge of the header, taking into account that
      class=class="str">"cmt">//--- the origin always comes from the upper edge of TabControl + class="num">2 pixels
      class="type">int y2=header.BottomEdgeRelative()-y_shift;
      class=class="str">"cmt">//--- If the calculated value does not go beyond the bottom 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(y2<y2_base)
        col=i-n;
      class=class="str">"cmt">//--- If the calculated value goes beyond the bottom 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 bottom edge + class="num">2 pixels),
        class=class="str">"cmt">//--- set the loop index for the n variable, class="kw">while the column index is set to zero, this is the start of the new row
        row++;

「多行表头下的坐标偏移与字段裁剪」

当标签页允许排成多行时,每一行的表头需要根据行号做横向位移。代码里先用 last.Row() 算出最后一行索引,再乘以单页签宽度 last.Width() 得到 x_shift,这个偏移量直接决定了非首行表头向左收多少、对应字段被截掉多少。 循环遍历表头列表时,对每个 CTabHeader 取关联的 CTabField,调用 header.Move(header.CoordX()-x_shift, header.CoordY()) 把表头左移,随后用 field.Resize(field.Width()-x_shift, field.Height(), false) 同步把字段宽度砍掉同一像素值。若 Row() 为 0,则整段偏移逻辑跳过,表头维持原始 X 坐标。 如果 TabSizeMode 设为 CANV_ELEMENT_TAB_SIZE_MODE_FILL,在算偏移前会先跑 StretchHeaders() 把表头拉满容器宽,这时 x_shift 基于拉伸后的 last.Width() 计算,字段裁剪量也会跟着变大。开 MT5 把 Row() 改成 2、宽度设 120 跑一遍,能直观看到第三行字段被削掉 240 像素。

MQL5 / C++
y_shift=header.CoordYRelative()-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()+header.Row()*header.Width(),header.CoordY()-y_shift))
     {
      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
     {
     
     }
   }
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
     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 X coordinate
       class="type">int x_shift=last.Row()*last.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 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()-x_shift,header.CoordY()))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
            class=class="str">"cmt">//--- change the tab field size to the X offset value
            field.Resize(field.Width()-x_shift,field.Height(),class="kw">false);
           }
         }
       }
     }
   }

把选项卡标题挪到侧边跑一遍

拿上一篇的 EA 放到 \MQL5\Experts\TestDoEasy\Part116\ 下,改名 TestDoEasy116.mq5,在输入参数里加两个变量:一个管选项卡标题是否多行排,一个管标题贴哪一侧。 面板主宽度加了 10 像素,第二个 GroupBox 容器宽了 12 像素——纯粹是因为现在还没做图形元素不可见部分的裁剪,父容器装不下时子对象会越界。标题放左侧时,选项卡字段里的复选框会撑到字段外、甚至盖住右侧 TabControl 的标题,属于已知瑕疵。 OnInit() 里建完 TabControl 后,按输入设定标题位置和多行权限;第三个选项卡的 ListBox 的 Y 坐标从原来的 12 改到更靠顶,否则多行标题时它会从底部漏出字段。 编译挂上图表,左右侧标题布局都正常。外汇和贵金属图表上跑这类自定义面板属高风险操作,参数没调稳前别直接上实盘。 下一篇再修这些越界毛病,眼下先能跑通。

MQL5 / C++
class=class="str">"cmt">//--- class="kw">input parameters
sinput  class="type">bool                 InpMovable       =  true;                class=class="str">"cmt">// Panel Movable flag
sinput  ENUM_INPUT_YES_NO     InpAutoSize      =  INPUT_YES;            class=class="str">"cmt">// Panel Autosize
sinput  ENUM_AUTO_SIZE_MODE   InpAutoSizeMode  =  AUTO_SIZE_MODE_GROW;  class=class="str">"cmt">// Panel Autosize mode
sinput  ENUM_BORDER_STYLE     InpFrameStyle    =  BORDER_STYLE_SIMPLE;  class=class="str">"cmt">// Label border style
sinput  ENUM_ANCHOR_POINT     InpTextAlign     =  ANCHOR_CENTER;        class=class="str">"cmt">// Label text align
sinput  ENUM_INPUT_YES_NO     InpTextAutoSize  =  INPUT_NO;             class=class="str">"cmt">// Label autosize
sinput  ENUM_ANCHOR_POINT     InpCheckAlign    =  ANCHOR_LEFT;          class=class="str">"cmt">// Check flag align
sinput  ENUM_ANCHOR_POINT     InpCheckTextAlign=  ANCHOR_LEFT;          class=class="str">"cmt">// Check label text align
sinput  ENUM_CHEK_STATE       InpCheckState    =  CHEK_STATE_UNCHECKED; class=class="str">"cmt">// Check flag state
sinput  ENUM_INPUT_YES_NO     InpCheckAutoSize =  INPUT_YES;            class=class="str">"cmt">// CheckBox autosize
sinput  ENUM_BORDER_STYLE     InpCheckFrameStyle = BORDER_STYLE_NONE;   class=class="str">"cmt">// CheckBox border style
sinput  ENUM_ANCHOR_POINT     InpButtonTextAlign = ANCHOR_CENTER;       class=class="str">"cmt">// Button text align
sinput  ENUM_INPUT_YES_NO     InpButtonAutoSize  = INPUT_YES;            class=class="str">"cmt">// Button autosize
sinput  ENUM_AUTO_SIZE_MODE   InpButtonAutoSizeMode= AUTO_SIZE_MODE_GROW;class=class="str">"cmt">// Button Autosize mode
sinput  ENUM_BORDER_STYLE     InpButtonFrameStyle = BORDER_STYLE_NONE;   class=class="str">"cmt">// Button border style

◍ 面板里嵌 GroupBox 与多行 Tab 的实写参数

在 MT5 用 WinForms 引擎搭面板时,先把交互开关用 sinput 暴露出来更方便现场调。下面这组输入项直接决定了按钮、列表框和选项卡控件的呈现方式:按钮总开关默认开,列表框多选关、多列开,选项卡控件多行显示开(InpTabCtrlMultiline=true),页头对齐贴顶、尺寸模式普通。 [CODE] sinput bool InpButtonToggle = true ; // Button toggle flag sinput bool InpButtListMSelect = false; // ButtonListBox Button MultiSelect flag sinput bool InpListBoxMColumn = true; // ListBox MultiColumn flag sinput bool InpTabCtrlMultiline = true; // Tab Control Multiline flag sinput ENUM_ELEMENT_ALIGNMENT InpHeaderAlignment = ELEMENT_ALIGNMENT_TOP; // TabHeader Alignment sinput ENUM_ELEMENT_TAB_SIZE_MODE InpTabPageSizeMode = ELEMENT_TAB_SIZE_MODE_NORMAL; // TabHeader Size Mode //--- global variables //--- Create WinForms Panel object CPanel *pnl=NULL; pnl=engine.CreateWFPanel("WFPanel",50,50,410,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false); if(pnl!=NULL) { //--- Create the GroupBox2 WinForms object CGroupBox *gbox2=NULL; //--- The indent from the attached panels by 6 pixels will be the Y coordinate of GrotupBox2 w=gbox1.Width()+12; int x=gbox1.RightEdgeRelative()+1; int h=gbox1.BottomEdgeRelative()-6; //--- If the attached GroupBox object is created if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,x,2,w,h,C'0x91,0xAA,0xAE',0,true,false)) { //--- Create the TabControl object gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,4,12,gbox2.Width()-12,gbox2.Height()-20,clrNONE,255,true,false); //--- get the pointer to the TabControl object by its index in the list of bound objects of the TabControl type CTabControl *tab_ctrl=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0); //--- If TabControl is created and the pointer to it is received if(tab_ctrl!=NULL) { [/CODE] 逐行看关键处:sinput 声明的 InpTabCtrlMultiline 置 true,才能让标签多行排布,标签一多也不会挤成一行。面板用 CreateWFPanel 在坐标(50,50)画出 410×200 的容器,背景透明度 200。 GroupBox2 的宽按已有 gbox1 的宽度加 12 像素推算,x 取 gbox1 右缘相对坐标加 1,高取底缘相对值减 6,这样两个框视觉上留有 6 像素 indent。在 GroupBox 内再建 TabControl,左上偏移(4,12)、尺寸扣掉边距,最后用 GetElementByType 按索引 0 拿指针——外汇与贵金属 GUI ea 开发高风险,参数坐标写错可能让控件画出屏外,建议开 MT5 把 410 和 12 这两个数改小跑一遍。

MQL5 / C++
sinput  class="type">bool                InpButtonToggle      =  true ;               class=class="str">"cmt">// Button toggle flag
sinput  class="type">bool                InpButtListMSelect  =  class="kw">false;               class=class="str">"cmt">// ButtonListBox Button MultiSelect flag
sinput  class="type">bool                InpListBoxMColumn   =  true;                class=class="str">"cmt">// ListBox MultiColumn flag
sinput  class="type">bool                InpTabCtrlMultiline =  true;                class=class="str">"cmt">// Tab Control Multiline flag
sinput  ENUM_ELEMENT_ALIGNMENT    InpHeaderAlignment  =  ELEMENT_ALIGNMENT_TOP;   class=class="str">"cmt">// TabHeader Alignment
sinput  ENUM_ELEMENT_TAB_SIZE_MODE InpTabPageSizeMode =  ELEMENT_TAB_SIZE_MODE_NORMAL; class=class="str">"cmt">// TabHeader Size Mode
class=class="str">"cmt">//--- global variables
class=class="str">"cmt">//--- Create WinForms Panel object
  CPanel *pnl=NULL;
  pnl=engine.CreateWFPanel("WFPanel",class="num">50,class="num">50,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)
    {
     class=class="str">"cmt">//--- Create the GroupBox2 WinForms object
     CGroupBox *gbox2=NULL;
     class=class="str">"cmt">//--- The indent from the attached panels by class="num">6 pixels will be the Y coordinate of GrotupBox2
     w=gbox1.Width()+class="num">12;
     class="type">int x=gbox1.RightEdgeRelative()+class="num">1;
     class="type">int h=gbox1.BottomEdgeRelative()-class="num">6;
     class=class="str">"cmt">//--- If the attached GroupBox object is created
     if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,x,class="num">2,w,h,C&class="macro">#x27;0x91,0xAA,0xAE&class="macro">#x27;,class="num">0,true,class="kw">false))
      {
       class=class="str">"cmt">//--- Create the TabControl object
       gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,class="num">4,class="num">12,gbox2.Width()-class="num">12,gbox2.Height()-class="num">20,clrNONE,class="num">255,true,class="kw">false);
       class=class="str">"cmt">//--- get the pointer to the TabControl object by its index in the list of bound objects of the TabControl type
       CTabControl *tab_ctrl=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,class="num">0);
       class=class="str">"cmt">//--- If TabControl is created and the pointer to it is received
       if(tab_ctrl!=NULL)
        {

「标签页控件与列表框的实例化细节」

在 MT5 自定义图形面板里,标签页控件(Tab Control)的排版与多行显示由几个输入参数直接决定。下面这段实例化代码把页头对齐方式、是否多行、内边距和九个子页一次性建出来,改一个输入就可能让界面布局完全不同。 代码里 SetAlignment 接的是 InpHeaderAlignment,它控制页标题靠左、居中还是靠右;SetMultilineInpTabCtrlMultiline,为真时标签多到一行放不下会自动折行,假则压缩或滚动。这两个开关配合 SetHeaderPadding(6,0) 的左右 6 像素留白,是面板是否挤成一团的关键。 第三个标签上挂了一个 ListBox:CreateNewElement(2,GRAPH_ELEMENT_TYPE_WF_LIST_BOX,...) 中第一个参数 2 即第三页(从 0 计)。宽度默认 146,但若 InpListBoxMColumn 为假则收窄到 100,高度固定 60。外汇与贵金属图表上做这类 UI 改动属低风险实验,但实盘加载自定义面板前应在模拟账户验证,避免对象句柄获取失败导致脚本异常。

MQL5 / C++
   class=class="str">"cmt">//--- Set the location of the tab titles on the element and the tab text, as well as create nine tabs
   tab_ctrl.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode);
   tab_ctrl.SetAlignment((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment);
   tab_ctrl.SetMultiline(InpTabCtrlMultiline);
   tab_ctrl.SetHeaderPadding(class="num">6,class="num">0);
   tab_ctrl.CreateTabPages(class="num">9,class="num">0,class="num">50,class="num">16,TextByLanguage("Вкладка","TabPage"));
   class=class="str">"cmt">//--- Create the ListBox object on the third tab
   class="type">int lbw=class="num">146;
   if(!InpListBoxMColumn)
      lbw=class="num">100;
   tab_ctrl.CreateNewElement(class="num">2,GRAPH_ELEMENT_TYPE_WF_LIST_BOX,class="num">4,class="num">2,lbw,class="num">60,clrNONE,class="num">255,true,class="kw">false);
   class=class="str">"cmt">//--- get the pointer to the ListBox object from the third tab by its index in the list of attached objects of the ListBox type

继续拆 TabControl 前的材料包

这一节把当前函数库版本、测试 EA 和图表事件控制指标的文件一并给出,压缩包体积 4435.36 KB,直接在 MT5 里解压就能跑。下一篇会接着啃 TabControl 的交互细节,建议先把这套现成工程载入验证,别等后面代码堆高了再回头找基线。 想跟作者对齐思路,可以在评论区抛具体疑问;俄文原版和英文译版的差异点,自己比对源码里的注释更靠谱。 外汇与贵金属品种波动大、杠杆高,用这些示例工程做回测时,先在小周期模拟盘跑通鼠标事件响应,再考虑实盘映射。

常见问题

在拖拽结束事件中交换两行记录的起始 Y 坐标与行号字段,再触发重绘即可完成零行与选中行的坐标互换。
需在拖拽后置标志位,重新遍历可见行调用行号重算与边框重算方法,确保每行宽度与容器边缘对齐。
可以,把控件代码贴给小布,它能对照回正逻辑逐段指出坐标回退与行号重算的遗漏点。
启用拉伸适配标志,按容器客户区宽度均分各标题列宽,并在容器 resize 时重算四向对齐参数。
在选中事件中将该行标记为右缘零位,回退其他行坐标并重排,使选中行固定贴右缘显示。