DoEasy. 控件(第 15 部分):TabControl WinForms 对象 多行选项卡标题、选项卡处理方法·进阶篇
(2/3)· 多行选项卡标题裁剪与 TabField 容器重构,解决宽屏选卡溢出与交互僵局
很多人在 TabControl 里塞了十几个选卡,宽度一超就靠裁剪加滚动按钮凑合,结果多行排列和字段交互全卡死。本篇接着上篇的 TabHeader 基础,把多行模式和独立 TabField 容器真正落进库里。
「TabControl 属性枚举与尺寸模式拆解」
在 MT5 自定义图形库里,CTabControl 的整数属性直接决定了多标签控件的渲染逻辑。其中 MSG_CANV_ELEMENT_PROP_TAB_MULTILINE 控制是否允许标签折成多行,MSG_CANV_ELEMENT_PROP_TAB_ALIGNMENT 管标签在控件内的方位,MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE 则决定单个标签宽度怎么算。 标签尺寸有三种设定:Fit to tab header text width(按标题文字宽度自适应)、Fit TabControl Width(撑满整个控件宽度)、Fixed size(写死固定值)。这三种模式在回测面板或盯盘 HUD 里会明显影响信息密度,外汇与贵金属行情跳动快,高密度布局可能带来误触风险,需自行权衡。 下面这段函数是把尺寸枚举转成可读字符串的实现,方便在日志里排查界面状态: string TabSizeModeDescription(ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode) { switch(mode) {
class="type">class="kw">string TabSizeModeDescription(ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode) { class="kw">switch(mode) {
◍ 标签页尺寸模式与图元类型的字符串映射
在 MT5 的 GUI 库底层,标签页(Tab)控件的尺寸策略由枚举 CANV_ELEMENT_TAB_SIZE_MODE 决定,代码里用 switch 把它转成可读文本:NORMAL 对应自适应、FIXED 对应固定宽、FILL 对应撑满容器,default 分支直接回 "Unknown"。 紧随其后的 CGBaseObj::TypeElementDescription() 用三元运算符把 ENUM_GRAPH_ELEMENT_TYPE 逐一翻译成界面描述。从 GRAPH_ELEMENT_TYPE_STANDARD 到 GRAPH_ELEMENT_TYPE_WF_LABEL,覆盖了基础图元、影子对象、窗体以及 WinForms 风格的容器与控件共 14 个分支。 如果你在写自定义面板,直接调用这两个函数就能把内部枚举打印到日志,省去自己维护字符串表。外汇与贵金属图表上的自定义 UI 属于高风险环境下的辅助工具,显示异常不预示任何行情方向。
case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL : class="kw">return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_NORMAL); break; case CANV_ELEMENT_TAB_SIZE_MODE_FIXED : class="kw">return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_FIXED); break; case CANV_ELEMENT_TAB_SIZE_MODE_FILL : class="kw">return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_FILL); break; class="kw">default : class="kw">return "Unknown"; break; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the graphical element type | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CGBaseObj::TypeElementDescription(const ENUM_GRAPH_ELEMENT_TYPE type) { class="kw">return ( type==GRAPH_ELEMENT_TYPE_STANDARD ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD) : type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED) : type==GRAPH_ELEMENT_TYPE_ELEMENT ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_ELEMENT) : type==GRAPH_ELEMENT_TYPE_SHADOW_OBJ ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_SHADOW_OBJ) : type==GRAPH_ELEMENT_TYPE_FORM ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_FORM) : type==GRAPH_ELEMENT_TYPE_WINDOW ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WINDOW) : class=class="str">"cmt">//--- WinForms type==GRAPH_ELEMENT_TYPE_WF_UNDERLAY ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_UNDERLAY) : type==GRAPH_ELEMENT_TYPE_WF_BASE ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BASE) : class=class="str">"cmt">//--- Containers type==GRAPH_ELEMENT_TYPE_WF_CONTAINER ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CONTAINER) : type==GRAPH_ELEMENT_TYPE_WF_GROUPBOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_GROUPBOX) : type==GRAPH_ELEMENT_TYPE_WF_PANEL ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_PANEL) : type==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL) : class=class="str">"cmt">//--- Standard controls type==GRAPH_ELEMENT_TYPE_WF_COMMON_BASE ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_COMMON_BASE) : type==GRAPH_ELEMENT_TYPE_WF_LABEL ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LABEL) :
按类型清点图表上的图形元素
在自定义图形库里,想统计某类控件(复选框、单选钮、列表框等)在图表子窗口中的实际数量,核心靠 ObjectsTotal 配合前缀过滤。下面这段私有方法给出了可直接抄的骨架:先按类型拼出标准名,再遍历全部对象比对名称前缀。
调用 ObjectsTotal(this.ChartID(), this.SubWindow()) 拿到当前图表与子窗口的对象总数,这个返回值在复杂 EA 面板上可能到几十甚至上百,循环开销可控但别在 OnTick 里高频调。
TypeGraphElementAsString 先把枚举转成人类可读字符串(如 WF_CHECKBOX),随后 ObjectName 逐个取对象名,名称不含库前缀的就跳过——这是判断「是不是自己人」的关键一步。
实际验证时,打开 MT5 导航器拖一个带前缀的面板到图表,断点跟一下 n 的累计值,就能确认你的控件真的被库接管了,外汇与贵金属图表上做这类扩展须留意对象数过多会拖慢终端响应。
class="type">int CGCnvElement::GetNumGraphElements(const ENUM_GRAPH_ELEMENT_TYPE type) const { class=class="str">"cmt">//--- Declare a variable with the number of graphical objects and class=class="str">"cmt">//--- get the total number of graphical objects on the chart and the subwindow where the graphical element is created class="type">int n=class="num">0, total=::ObjectsTotal(this.ChartID(),this.SubWindow()); class=class="str">"cmt">//--- Create the name of a graphical object by its type class="type">class="kw">string name=TypeGraphElementAsString(type); class=class="str">"cmt">//--- In the loop by all chart and subwindow objects, for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- get the name of the next object class="type">class="kw">string name_obj=::ObjectName(this.ChartID(),i,this.SubWindow()); class=class="str">"cmt">//--- if the object name does not contain the set prefix of the names of the library graphical objects, move on - this is not the object we are looking for
「按类型与名称清点图表对象」
在 MT5 自定义指标或 EA 里批量管理图形对象时,先得知道图表上到底挂了多少个同类型元素。下面这段从图形元素封装类里抽出的逻辑,就是靠遍历 ObjectsTotal 拿到总数,再按名称前缀和子串过滤来计数。 核心函数 GetNumGraphElements 接收 name 与 type 两个参数,先用 ObjectsTotal(ChartID(), SubWindow()) 取当前图表及其子窗口的对象总量,随后 for 循环从 0 跑到 total-1。每一次循环用 ObjectName 取出对象名,若 StringFind 找不到类库统一前缀(返回 WRONG_VALUE)就 continue 跳过,否则再用 StringFind(name_obj, name) > 0 判断类型子串,命中则计数器 n 加一。 CreateNameGraphElement 则把前缀、类型字符串和当前同类型数量拼成唯一名:NamePrefix()+TypeGraphElementAsString(type)+(string)GetNumGraphElements(type)。这样每新建一个元素,名称里的数字后缀会自动递增,避免重名覆盖。 开 MT5 随便拖一个指标到副图,把 SubWindow() 改成 1 跑一遍这段,就能看到副窗口里同类对象实时计数,方便你后续做批量删除或样式刷新。外汇与贵金属图表对象随价格波动频繁重绘,误操作可能清掉关键分析线,实盘前务必在模拟环境验证。
class="type">int CGCnvElement::GetNumGraphElements(const class="type">class="kw">string name,const ENUM_GRAPH_ELEMENT_TYPE type) const { class="type">int n=class="num">0, total=::ObjectsTotal(this.ChartID(),this.SubWindow()); for(class="type">int i=class="num">0;i<total;i++) { class="type">class="kw">string name_obj=::ObjectName(this.ChartID(),i,this.SubWindow()); if(::StringFind(name_obj,this.NamePrefix())==WRONG_VALUE) class="kw">continue; if(::StringFind(name_obj,name)>class="num">0) n++; } class="kw">return n; } class="type">class="kw">string CGCnvElement::CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type) { class="kw">return this.NamePrefix()+TypeGraphElementAsString(type)+(class="type">class="kw">string)this.GetNumGraphElements(type); }
◍ 画布标签的创建与失败归因
在 MT5 自定义图形界面里,CGCnvElement::Create 负责把一个位图标签画到指定图表上。它先 ResetLastError 清掉旧错误码,再调用 m_canvas.CreateBitmapLabel,传入图表 ID、子窗口号、坐标 (x,y) 与宽高 (w,h),颜色格式固定用 COLOR_FORMAT_ARGB_NORMALIZE。 创建成功后会立刻 Erase(CLR_CANV_NULL) 清空画布,并执行 m_canvas.Update(redraw) 按传入开关决定是否重绘;同时用 ChartGetInteger 取 CHART_WINDOW_YDISTANCE 填到 m_shift_y,这一步决定了后续元素相对窗口的纵向偏移量。 失败分支值得细看:若 err==0 但仍返回 false,说明是宽或高不合法——w<1 记 Width 错,h<1 记 Height 错,否则归为 ERR_OBJECT_ERROR。CMessage::ToLog 会把函数名、行号加具体维度信息写进日志,方便在策略里快速定位是参数还是对象本身的问题。外汇与贵金属图表加载这类对象时波动快,宽高设 0 可能瞬间触发失败,属高风险环境下的常见坑。
class="type">bool CGCnvElement::Create(const class="type">long chart_id, class=class="str">"cmt">// Chart ID const class="type">int wnd_num, class=class="str">"cmt">// Chart subwindow const class="type">int x, class=class="str">"cmt">// X coordinate const class="type">int y, class=class="str">"cmt">// Y coordinate const class="type">int w, class=class="str">"cmt">// Width const class="type">int h, class=class="str">"cmt">// Height const class="type">bool redraw=false) class=class="str">"cmt">// Flag indicating the need to redraw { ::ResetLastError(); if(this.m_canvas.CreateBitmapLabel((chart_id==NULL ? ::ChartID() : chart_id),wnd_num,this.m_name,x,y,w,h,COLOR_FORMAT_ARGB_NORMALIZE)) { this.Erase(CLR_CANV_NULL); this.m_canvas.Update(redraw); this.m_shift_y=(class="type">int)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_WINDOW_YDISTANCE,wnd_num); class="kw">return true; } class="type">int err=::GetLastError(); class="type">int code=(err==class="num">0 ? (w<class="num">1 ? MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH : h<class="num">1 ? MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT : ERR_OBJECT_ERROR) : err); class="type">class="kw">string subj=(w<class="num">1 ? "Width="+(class="type">class="kw">string)w+". " : h<class="num">1 ? "Height="+(class="type">class="kw">string)h+". " : ""); CMessage::ToLog(DFUN_ERR_LINE+subj,code,true); class="kw">return false; }
往表单里挂新图元的工厂方法
在 MT5 自定义图形界面里,往一个 CForm 容器动态挂子元素,核心就落在 CreateAndAddNewElement 这个工厂函数上。它先把传入的枚举类型做一道门槛判断:如果 element_type 小于 GRAPH_ELEMENT_TYPE_ELEMENT,直接 Print 报错并返回 NULL,避免把非图元类型当对象创建。 函数内部先取 m_list_elements.Total() 作为新元素序号,再用 TypeGraphElementAsString 拿到默认描述,并通过 GetCoords 把相对坐标换算成屏幕坐标。真正生成对象靠 CreateNewGObject,失败就返 NULL;成功才继续走 AddNewElement 绑定,绑定失败则 delete 掉防止内存泄漏。 挂上之后有一串最小属性设置值得注意:背景色、透明度、激活态,以及 Main / Base 指针的指向逻辑——当表单自身没有 Main 时,新元素的主控指向表单对象本身,否则沿用表单的 Main。最后用 SetID(GetMaxIDAll()+1) 保证全局 ID 不撞车,并把相对 X 坐标存成与表单的坐标差。 下面这段就是该方法的真实骨架,逐行看能少踩很多坑:
| // | Create a new attached element |
|---|---|
| // | and add it to the list of bound objects |
//+------------------------------------------------------------------+ CGCnvElement *CForm::CreateAndAddNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool activity) { //--- 若图元类型小于基础元素枚举,打印错误并返回空指针 if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),19)); return NULL; } //--- 取当前元素列表总数作为新元素索引 int num=this.m_list_elements.Total(); //--- 生成默认图元描述字符串 string descript=TypeGraphElementAsString(element_type); //--- 计算相对基对象坐标系的屏幕坐标 int elm_x=x; int elm_y=y; this.GetCoords(elm_x,elm_y); //--- 创建新图元对象,初始不主显 CGCnvElement *obj=this.CreateNewGObject(element_type,num,descript,elm_x,elm_y,w,h,colour,opacity,false,activity); if(obj==NULL) return NULL; //--- 加入绑定列表,失败则释放并返回空 if(!this.AddNewElement(obj,elm_x,elm_y)) { delete obj; return NULL; } //--- 设置背景色、透明度、激活态 obj.SetBackgroundColor(colour,true); obj.SetOpacity(opacity); obj.SetActive(activity); //--- 设定主控与基对象指针 obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain()); obj.SetBase(this.GetObject()); //--- 分配全局唯一 ID 与序号,并保存相对 X 偏移 obj.SetID(this.GetMaxIDAll()+1); obj.SetNumber(num); obj.SetCoordXRelative(obj.CoordX()-this.CoordX());
class=class="str">"cmt">//| Create a new attached element | class=class="str">"cmt">//| and add it to the list of bound objects | class=class="str">"cmt">//+------------------------------------------------------------------+ CGCnvElement *CForm::CreateAndAddNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">class="kw">color colour, const class="type">uchar opacity, const class="type">bool activity) { class=class="str">"cmt">//--- If the type of a created graphical element is less than the "element", inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),class="num">19)); class="kw">return NULL; } class=class="str">"cmt">//--- Specify the element index in the list class="type">int num=this.m_list_elements.Total(); class=class="str">"cmt">//--- Create a description of the class="kw">default graphical element class="type">class="kw">string descript=TypeGraphElementAsString(element_type); class=class="str">"cmt">//--- Get the screen coordinates of the object relative to the coordinate system of the base object class="type">int elm_x=x; class="type">int elm_y=y; this.GetCoords(elm_x,elm_y); class=class="str">"cmt">//--- Create a new graphical element CGCnvElement *obj=this.CreateNewGObject(element_type,num,descript,elm_x,elm_y,w,h,colour,opacity,false,activity); if(obj==NULL) class="kw">return NULL; class=class="str">"cmt">//--- and add it to the list of bound graphical elements if(!this.AddNewElement(obj,elm_x,elm_y)) { class="kw">delete obj; class="kw">return NULL; } class=class="str">"cmt">//--- Set the minimum properties for a bound graphical element obj.SetBackgroundColor(colour,true); obj.SetOpacity(opacity); obj.SetActive(activity); obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain()); obj.SetBase(this.GetObject()); obj.SetID(this.GetMaxIDAll()+class="num">1); obj.SetNumber(num); obj.SetCoordXRelative(obj.CoordX()-this.CoordX());