DoEasy. 控件(第 16 部分):TabControl WinForms 对象 多行选项卡标题,拉伸标题适配容器·进阶篇
(2/3)· 当 TabControl 选项卡溢出容器,多行排布与全宽拉伸才是 MT5 界面不破框的关键
「TabHeader 的选中态尺寸与行列索引」
在 MT5 的 WForms 封装里,CTabHeader 继承自 CButton,但它比普通按钮多管了「释放 / 选中」两套尺寸与行列坐标。m_width_off、m_height_off 是未选中时的宽高,m_width_on、m_height_on 是选中态的宽高,运行时靠 WHProcessStateOn / WHProcessStateOff 在两个尺寸间切换。 标红的 m_col 与 m_row 是表头在 TabControl 网格里的列、行下标。这两个整型变量决定了 DrawFrame 绘制边框时往哪偏移,也影响 CorrectSelectedRowTop / CorrectSelectedRowBottom 如何把选中标签推到正确位置(顶部、底部、左、右四种布局之一)。 上面那段 property 三元表达式说明了属性读取的兜底逻辑:遇到 CANV_ELEMENT_PROP_TAB_PAGE_COLUMN 或 CANV_ELEMENT_PROP_ALIGNMENT 时,先取多语言文本,再判断是否 only_prop;若不支持该属性则拼 ': 不支持' 提示,否则把 GetProperty 的返回值转成字符串或对齐描述。复制进 MT5 的 Include 工程即可断点看 SupportProperty 的返回。
class="kw">property==CANV_ELEMENT_PROP_TAB_PAGE_COLUMN ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_COLUMN)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_ALIGNMENT ? CMessage::Text(MSG_CANV_ELEMENT_PROP_ALIGNMENT)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+AlignmentDescription((ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(class="kw">property)) ) : "" ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TabHeader object class of WForms TabControl | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTabHeader : class="kw">public CButton { class="kw">private: class="type">int m_width_off; class=class="str">"cmt">// Object width in the released state class="type">int m_height_off; class=class="str">"cmt">// Object height in the released state class="type">int m_width_on; class=class="str">"cmt">// Object width in the selected state class="type">int m_height_on; class=class="str">"cmt">// Object height in the selected state class="type">int m_col; class=class="str">"cmt">// Header column index class="type">int m_row; class=class="str">"cmt">// Header row index class=class="str">"cmt">//--- Adjust the size and location of the element depending on the state class="type">bool WHProcessStateOn(class="type">void); class="type">bool WHProcessStateOff(class="type">void); class=class="str">"cmt">//--- Draws a header frame depending on its position class="kw">virtual class="type">void DrawFrame(class="type">void); class=class="str">"cmt">//--- Set the class="type">class="kw">string of the selected tab header to the correct position, (class="num">1) top, (class="num">2) bottom, (class="num">3) left and(class="num">4) right class="type">void CorrectSelectedRowTop(class="type">void); class="type">void CorrectSelectedRowBottom(class="type">void);
◍ Tab 控件行列与尺寸偏移的读取接口
在自定义 Tab 控件类里,protected 区暴露了一组只读尺寸函数,用来拿控件在开启/关闭两种状态下的宽高偏移。m_width_off / m_height_off 对应未激活页签的占位,m_width_on / m_height_on 对应激活页签的占位,四个函数均声明为 const,调用不改动对象状态。 行列索引走的是 Canvas 元素属性通道,不是裸成员变量。SetRow 把行号写进 CANV_ELEMENT_PROP_TAB_PAGE_ROW,Row 再从这个属性读回来强转 int;列方向对称地用 CANV_ELEMENT_PROP_TAB_PAGE_COLUMN。这样设计的好处是行列状态和渲染层属性绑定,避免自己再维护一套容易和界面脱节的整型字段。 SetTabLocation 接收 row 和 col 两个 const int,是同时落行列的入口。如果你在 MT5 里做多页签面板,直接调它比分别调 SetRow/SetColumn 少一次属性写入,界面重绘概率上更跟手。外汇与贵金属标的波动大、 panels 刷新频繁,这种细节点可能影响卡顿体感。
class="type">void CorrectSelectedRowLeft(class="type">void); class="type">void CorrectSelectedRowRight(class="type">void); class="kw">protected: class=class="str">"cmt">//--- Returns the control size class="type">int WidthOff(class="type">void) class="kw">const { class="kw">return this.m_width_off; } class="type">int HeightOff(class="type">void) class="kw">const { class="kw">return this.m_height_off; } class="type">int WidthOn(class="type">void) class="kw">const { class="kw">return this.m_width_on; } class="type">int HeightOn(class="type">void) class="kw">const { class="kw">return this.m_height_on; } class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the Tab row index class="type">void SetRow(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_ROW,value); } class="type">int Row(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_ROW); } class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the Tab column index class="type">void SetColumn(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,value); } class="type">int Column(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_COLUMN); } class=class="str">"cmt">//--- Set the tab location class="type">void SetTabLocation(class="kw">const class="type">int row,class="kw">const class="type">int col)
表头尺寸随对齐方式翻转宽高
在自定义 Tab 控件的 Normal 尺寸模式下,表头按钮的宽高计算完全由 Alignment() 决定。顶部或底部对齐时,文本按宽高顺序测量,左右内边距累加进 width;而左、右对齐时 TextSize 的参数被故意调转,height 接收文本宽、width 接收文本高,padding 的左右值反而累加到 height 上。 这种反转意味着:同一段标签文字,在上下表头里可能只占 60×18,转到左右表头就变成 18×60(假设 padding 对称)。写 GUI 时若硬编码宽高,拖到侧边就会挤成一团。 激活态的尺寸补偿也看对齐:上下对齐给宽度 +4、高度 +2;左右对齐的代码段只列了 WidthOff 赋值,On 态的宽高增量在原文 default 分支前被截断,实跑时建议补打日志确认是否漏写。外汇/贵金属面板高波动时标签频繁重绘,这种 2~4 像素差异可能让点击热区偏移,概率上增加误触。
{
this.SetRow(row);
this.SetColumn(col);
}
class=class="str">"cmt">//--- Depending on the header size setting mode
class="kw">switch(this.TabSizeMode())
{
class=class="str">"cmt">//--- set the width and height for the Normal mode
case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL :
class="kw">switch(this.Alignment())
{
case CANV_ELEMENT_ALIGNMENT_TOP :
case CANV_ELEMENT_ALIGNMENT_BOTTOM :
this.TextSize(this.Text(),width,height);
width+=this.PaddingLeft()+this.PaddingRight();
height=h+this.PaddingTop()+this.PaddingBottom();
class="kw">break;
case CANV_ELEMENT_ALIGNMENT_LEFT :
case CANV_ELEMENT_ALIGNMENT_RIGHT :
this.TextSize(this.Text(),height,width);
height+=this.PaddingLeft()+this.PaddingRight();
width=w+this.PaddingTop()+this.PaddingBottom();
class="kw">break;
class="kw">default:
class="kw">break;
}
class="kw">break;
class=class="str">"cmt">//---CANV_ELEMENT_TAB_SIZE_MODE_FIXED
class=class="str">"cmt">//---CANV_ELEMENT_TAB_SIZE_MODE_FILL
class=class="str">"cmt">//--- For the Fixed mode, the dimensions remain specified,
class=class="str">"cmt">//--- In case of Fill, they are calculated in the StretchHeaders methods of the TabControl class
class="kw">default: class="kw">break;
}
class=class="str">"cmt">//--- Set the results of changing the width and height to &class="macro">#x27;res&class="macro">#x27;
class=class="str">"cmt">//--- Set the changed size for different button states
class="kw">switch(this.Alignment())
{
case CANV_ELEMENT_ALIGNMENT_TOP :
case CANV_ELEMENT_ALIGNMENT_BOTTOM :
this.SetWidthOn(this.Width()+class="num">4);
this.SetHeightOn(this.Height()+class="num">2);
this.SetWidthOff(this.Width());「标签头尺寸按对齐方向反算」
CTabHeader::SetSizes 里先做了下限保护:传入宽或高小于 4 像素时,强制拉到 4。这意味着你即便在 MT5 面板里把标签头压到 1×1,底层也不会真给你 1 像素,渲染最小占位就是 4×4。 进入 CANV_ELEMENT_TAB_SIZE_MODE_NORMAL 后,尺寸计算完全跟着 Alignment 走。上下对齐(TOP/BOTTOM)时,先用 TextSize 量文字占的宽高,再往宽里加 PaddingLeft+PaddingRight,高则直接吃外部传入的 h 加上下内边距;左右对齐(LEFT/RIGHT)时,TextSize 的参数被故意调反——文字尺寸按 (height,width) 取,随后高加左右 padding、宽吃外部 w 加上下 padding。 这种反算写法容易在自绘标签控件时坑到人:如果你在 LEFT/RIGHT 模式下发现标签宽度不对,优先查是不是把 w/h 传反了,而不是怀疑 padding 取值。开 MT5 把这段塞进自己的 CCanvas 派生类,改 Alignment 切一下就能看到占位变化。
class="type">bool CTabHeader::SetSizes(class="kw">const class="type">int w,class="kw">const class="type">int h) { class="type">int width=(w<class="num">4 ? class="num">4 : w); class="type">int height=(h<class="num">4 ? class="num">4 : h); class="kw">switch(this.TabSizeMode()) { case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL : class="kw">switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : case CANV_ELEMENT_ALIGNMENT_BOTTOM : this.TextSize(this.Text(),width,height); width+=this.PaddingLeft()+this.PaddingRight(); height=h+this.PaddingTop()+this.PaddingBottom(); class="kw">break; case CANV_ELEMENT_ALIGNMENT_LEFT : case CANV_ELEMENT_ALIGNMENT_RIGHT : this.TextSize(this.Text(),height,width); height+=this.PaddingLeft()+this.PaddingRight(); width=w+this.PaddingTop()+this.PaddingBottom(); class="kw">break; class="kw">default: class="kw">break; } class="kw">break; class=class="str">"cmt">//---CANV_ELEMENT_TAB_SIZE_MODE_FIXED class=class="str">"cmt">//---CANV_ELEMENT_TAB_SIZE_MODE_FILL class=class="str">"cmt">//--- For the Fixed mode, the dimensions remain specified,
◍ 标签页选中态的尺寸与位移处理
在自定义 TabControl 里,选中态(StateOn)的视觉放大不是简单改色,而是真去动宽高与坐标。代码里先调 SetSizeOn() 设置新尺寸,失败直接返回 false,避免后续坐标错乱。 根据对齐方向,顶部/底部对齐时宽度 +4、高度 +2;左/右对齐时宽度 +2、高度 +4。这个 2 和 4 像素的差值,就是选中标签相对未选中态的“溢出余量”,用来盖住相邻边框。 顶部对齐时还会额外调用 CorrectSelectedRowTop() 修正整行位置,再把自身坐标向左上各移 2 像素(CoordX()-2, CoordY()-2),同步把相对坐标也减 2,保证父容器重绘时不会错位。 开 MT5 自己写个 CTabHeader 派生类,把 +4/+2 改成 +6/+3 看看标签重叠是否更明显,能直观验证这套位移逻辑。外汇与贵金属 GUI 自动化涉及实盘高风险,参数改动仅限模拟环境验证。
class="type">bool CTabHeader::WHProcessStateOn(class="type">void) { if(!this.SetSizeOn()) class="kw">return class="kw">false; CWinFormBase *base=this.GetBase(); if(base==NULL) class="kw">return class="kw">false; class="kw">switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP: this.CorrectSelectedRowTop(); if(this.Move(this.CoordX()-class="num">2,this.CoordY()-class="num">2)) { this.SetCoordXRelative(this.CoordXRelative()-class="num">2);
按锚定方位回弹表头坐标
释放表头后,系统依据元素锚定方位做反向 2 像素回弹,避免按住时的视觉偏移残留。底部对齐时只动 X 轴减 2、Y 相对坐标不变;左侧与右侧对齐则 X、Y 同步减 2 像素,保证选中行和表头重新贴合。 代码里每个 case 都先调 CorrectSelectedRow*() 修正选中行位置,再靠 Move() 返回值判断是否写入相对坐标。若 Move() 失败,相对坐标不更新,表头可能和内部行产生肉眼可见的错位。 这种 2 像素步进是 MT5 画布控件的常见手感参数,你在自定义面板里改这个数值,能直接改变拖拽表头后的吸附松紧度。
this.SetCoordYRelative(this.CoordYRelative()-class="num">2); } class="kw">break; case CANV_ELEMENT_ALIGNMENT_BOTTOM : class=class="str">"cmt">//--- Adjust the location of the row with the selected header this.CorrectSelectedRowBottom(); class=class="str">"cmt">//--- shift the header by two pixels to the new location coordinates and class=class="str">"cmt">//--- set the new 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">//--- Adjust the location of the row with the selected header this.CorrectSelectedRowLeft(); class=class="str">"cmt">//--- shift the header by two pixels to the new location coordinates and class=class="str">"cmt">//--- set the new 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">//--- Adjust the location of the row with the selected header this.CorrectSelectedRowRight(); class=class="str">"cmt">//--- shift the header by two pixels to the new location coordinates and class=class="str">"cmt">//--- set the new 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;