DoEasy. 控件(第 16 部分):TabControl WinForms 对象 多行选项卡标题,拉伸标题适配容器(基础篇)
◍ 给 TabControl 加上多行选项卡与自适应拉伸
DoEasy 库在 WinForms 控件体系里继续补完 TabControl:这一版重点解决选项卡标题在多行排布时的适配问题,以及标题宽度如何跟随容器尺寸自动拉伸,而不是写死像素。 对 MT5 自定义面板来说,选项卡一多就会挤成一行并出现滚动条,实际盯盘时很碍事。让标题支持多行换行、并按容器宽度均分拉伸,能直接在有限面板空间里铺开更多品种或周期切换页。 本次示例发布于 2022 年 12 月 19 日,原文示例页显示浏览量 1053、评论 0,说明该控件改造偏底层工具向,使用者多为自己写 EA/指标界面的老手。 在 MT5 里打开对应库工程,把 TabControl 对象挂到你的 CPanel 派生类上,调一下多行与拉伸开关,就能直接看到标题随窗口拖拽重排的效果,外汇与贵金属图表界面改造前请先认清自定义控件失效可能导致误触的高风险。
选项卡标题的排布与拉伸逻辑
MT5 自定义控件里,选项卡标题超出容器宽度时,默认做法是裁剪多余标题并加滚动按钮;若开启多行模式,标题会按控件尺寸自动折成若干行排布。标题宽度由 SizeMode 决定:Normal 按文本宽度加 Padding 留白,Fixed 用固定宽(超长裁切),FillToRight 则把整行选项卡拉伸铺满控件宽或高。 在激活多行模式并选中某选项卡时,它所在整行会向选项卡字段靠拢,相邻标题取代原选中行位置——这是原生 CAppDialog 没有的视觉反馈。我已实现顶部放置,本文扩展到四侧都支持多行,并补上 FillToRight:顶/底沿控件宽拉伸,左/右沿高拉伸。 关键细节:拉伸区每侧留 2 像素间隙,因为选中时标题尺寸会增大 4 像素。若不预留,选中边缘就会溢出控件边界。单行模式标题超量时目前直接排到容器外,因暂无裁剪越界图元能力,留待后续处理。外汇与贵金属图表上做这类自定义面板属高风险环境,参数误设可能导致界面异常。
「给 TabControl 补上行列定位属性」
要在 MT5 里把多选项卡标题排成同一行,核心是先让图形元素知道自己处在哪一行、哪一列。原 DoEasy 库没有这个能力,得往画布元素的整数属性里加两个新项,并把属性总数从 90 扩到 92,同时在排序枚举里补上对应条件,后面才能用一行代码捞出同排标题。 具体落点在三个文件:Defines.mqh 里加属性编号与枚举;Data.mqh 里加消息索引和英俄文本(数组可扩维加中文);GCnvElement.mqh 的宽度高度方法里补调整大小错误描述,方便以后排错。WinFormBase.mqh 则要加一个返回整数属性描述的方法,按传入属性拼文本。 TabHeader 类以前用私有变量存行、列,现在改成直接读写对象属性。构造函数仍给默认大小,之后再按容器模式调——这种“先建后改”的便利换来的是初始坐标会漂移:标题文本已知时正常模式能预判大小,但顶部/底部要把 PaddingLeft、Right 加进宽度,Top、Bottom 加进高度;左侧/右侧旋转 90° 后恰好反过来。 实测选中态标题会变大 2~4 像素:水平摆放宽+4、高+2;垂直摆放宽+2、高+4。若不做坐标回移,标题就会溢出容器。底部定位以前漏了 break 没出错,现在补了偏移模块后必须加上,否则选中逻辑会误处理后续空分支。 多行排布时,选中不直接接壤字段的标题要整行挪近字段。左/右定位沿 X 轴移,底部沿 Y 轴移,逻辑和前文顶部一致。字段类里还要用背景色在左、右侧画一条线,擦掉字段和标题间的可视边界,看起来才像一体。 TabControl 里新增四种拉伸方法:按宽度拉伸只需一种(标题总从左排到右),按高度拉伸要分左(底到顶)、右(顶到底)两种。宽度拉伸时从容器宽扣掉左右各 2 像素再除以行内标题数,除数为 0 时兜底除以 1。左侧标题因锚点在左上角,改高后下边缘会超底,需先存原高、算偏移再把首个标题上移。右侧则从顶往下长,不用回移。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Integer properties of the graphical element on the canvas | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_CANV_ELEMENT_PROP_INTEGER { CANV_ELEMENT_PROP_ID = class="num">0, class=class="str">"cmt">// Element ID
◍ 画布控件的标签索引与排序枚举
在 MT5 的 Canvas 图形库里,控件元素用一组整数属性描述自身状态,其中标签类控件会用到行、列、页码三个定位属性:CANV_ELEMENT_PROP_TAB_PAGE_ROW 记录标签所在行索引,CANV_ELEMENT_PROP_TAB_PAGE_COLUMN 记录列索引,配合 CANV_ELEMENT_PROP_TAB_PAGE_NUMBER 才能唯一锁定某个子页。 整数属性总量由宏定义为 92 个(CANV_ELEMENT_PROP_INTEGER_TOTAL),跳过排序的为 0 个,所以 FIRST_CANV_ELEMENT_DBL_PROP 直接从 92 开始排,字符串属性基址再叠加重型属性总数偏移。 排序枚举 ENUM_SORT_CANV_ELEMENT_MODE 把上述属性直接映射成排序键。例如 SORT_BY_CANV_ELEMENT_TAB_PAGE_ROW 与 SORT_BY_CANV_ELEMENT_TAB_PAGE_COLUMN 让多标签面板能按界面布局重排;字符串类则从 FIRST_CANV_ELEMENT_STR_PROP 起算,覆盖对象名、资源名、文本与描述。 开 MT5 新建脚本把下面枚举贴进去,编译后鼠标悬停各成员即可看注释,验证行/列排序键是否和你面板的实际索引一致。
enum ENUM_SORT_CANV_ELEMENT_MODE { class=class="str">"cmt">//--- Sort by integer properties SORT_BY_CANV_ELEMENT_ID = class="num">0, class=class="str">"cmt">// Sort by element ID SORT_BY_CANV_ELEMENT_TYPE, class=class="str">"cmt">// Sort by graphical element type class=class="str">"cmt">//---... class=class="str">"cmt">//---... SORT_BY_CANV_ELEMENT_TAB_SIZE_MODE, class=class="str">"cmt">// Sort by the mode of setting the tab size SORT_BY_CANV_ELEMENT_TAB_PAGE_NUMBER, class=class="str">"cmt">// Sort by the tab index number SORT_BY_CANV_ELEMENT_TAB_PAGE_ROW, class=class="str">"cmt">// Sort by tab row index SORT_BY_CANV_ELEMENT_TAB_PAGE_COLUMN, class=class="str">"cmt">// Sort by tab column index SORT_BY_CANV_ELEMENT_ALIGNMENT, class=class="str">"cmt">// Sort by the location of the object inside the control class=class="str">"cmt">//--- Sort by real properties class=class="str">"cmt">//--- Sort by class="type">class="kw">string properties SORT_BY_CANV_ELEMENT_NAME_OBJ = FIRST_CANV_ELEMENT_STR_PROP, class=class="str">"cmt">// Sort by an element object name SORT_BY_CANV_ELEMENT_NAME_RES, class=class="str">"cmt">// Sort by the graphical resource name SORT_BY_CANV_ELEMENT_TEXT, class=class="str">"cmt">// Sort by graphical element text SORT_BY_CANV_ELEMENT_DESCRIPTION, class=class="str">"cmt">// Sort by graphical element description };
画布元素属性枚举与尺寸重设接口
在 MT5 自定义图形库里,画布元素(CGCnvElement)的属性被拆成枚举常量与多语言字符串表两层。上面这段枚举片段里,MSG_CANV_ELEMENT_PROP_TAB_PAGE_ROW 与 MSG_CANV_ELEMENT_PROP_TAB_PAGE_COLUMN 专门管 Tab 控件的行列索引,而 MSG_CANV_ELEMENT_PROP_NAME_OBJ、MSG_CANV_ELEMENT_PROP_TEXT 等则对应图形对象的名称与显示文本,开发者改界面文案时直接动那张俄英对照字符串表即可。 尺寸调整走的是 SetWidth / SetHeight 两个成员函数,不是直接改成员变量。SetWidth 先比对 CANV_ELEMENT_PROP_WIDTH 当前值,若相等直接返回 true,避免无谓的 canvas 重绘;否则调用 m_canvas.Resize(width, 原高度) 重设底层画布。 若 Resize 失败,函数会通过 CMessage::ToLog 打一条带元素类型描述与目标宽度的日志,并返回 false。外汇与贵金属图表上挂这类自定义控件时,频繁 Resize 可能触发重绘卡顿,建议在 tick 事件外缓存目标尺寸再批量提交。
class="type">bool CGCnvElement::SetWidth(class="kw">const class="type">int width) { if(this.GetProperty(CANV_ELEMENT_PROP_WIDTH)==width) class="kw">return true; if(!this.m_canvas.Resize(width,this.m_canvas.Height())) { CMessage::ToLog(DFUN+this.TypeElementDescription()+": width="+(class="type">class="kw">string)width+": ",MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH); class="kw">return class="kw">false; } this.SetProperty(CANV_ELEMENT_PROP_WIDTH,width); class="kw">return true; } class="type">bool CGCnvElement::SetHeight(class="kw">const class="type">int height) { if(this.GetProperty(CANV_ELEMENT_PROP_HEIGHT)==height) class="kw">return true;
「控件高度重设与属性描述的分支处理」
在自定义画布控件里,高度不是直接写个变量就完事。先调 Canvas 的 Resize 把底层位图缓冲跟着拉高,失败就写日志并返回 false,成功才用 SetProperty 把 CANV_ELEMENT_PROP_HEIGHT 记进对象属性,返回 true 让上层知道改完了。 GetPropertyDescription 这块是典型的嵌套三元表达式,按属性枚举挨个返回人类可读文案。only_prop 为 true 时只给属性名,否则还要判断 SupportProperty:不支持就拼“不支持”提示,支持就实时取 GetProperty 的值转成字符串。 注意 CANV_ELEMENT_PROP_TAB_PAGE_ROW 这一支,它返回的是当前 Tab 页的行序号,直接 (%string)this.GetProperty(property) 强转。若你写的面板支持多排标签页,这个值会随用户点击变,调试时打印出来就能确认行索引是不是预期的那一排。 外汇与贵金属图表上挂这类自绘面板,重载属性描述能省不少排查时间,但 MT5 自定义控件在高波动行情可能掉帧,属于高风险调试场景,参数改动后建议在模拟盘先跑。
if(!this.m_canvas.Resize(this.m_canvas.Width(),height)) { CMessage::ToLog(DFUN+this.TypeElementDescription()+": height="+(class="type">class="kw">string)height+": ",MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT); class="kw">return class="kw">false; } this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,height); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the control integer class="kw">property | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CWinFormBase::GetPropertyDescription(ENUM_CANV_ELEMENT_PROP_INTEGER class="kw">property,class="type">bool only_prop=class="kw">false) { class="kw">return ( class="kw">property==CANV_ELEMENT_PROP_ID ? CMessage::Text(MSG_CANV_ELEMENT_PROP_ID)+ (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_TYPE ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TYPE)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+this.TypeElementDescription() ) : class=class="str">"cmt">//--- class=class="str">"cmt">//--- class="kw">property==CANV_ELEMENT_PROP_TAB_SIZE_MODE ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+TabSizeModeDescription((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(class="kw">property)) ) : class="kw">property==CANV_ELEMENT_PROP_TAB_PAGE_NUMBER ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER)+ (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_TAB_PAGE_ROW ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_ROW)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) :