DoEasy. C控件(第 7 部分):文本标签控件·综合运用
🏷️

DoEasy. C控件(第 7 部分):文本标签控件·综合运用

(3/3)·从零造一个能自适应字号、带三维边框的文本标签,终结画布排版的手工活

案例拆解新手友好 第 3/3 篇
很多人直接在画布上手写 Text label 对象,字体一换就错位、边框要重画。把标签当普通图形对象用,GUI 改版时全是返工。

「画布控件的边框与自适应属性接口」

在 MT5 的自定义画布 GUI 框架里,控件对象通过 GetProperty/SetProperty 统一读写自身状态,边框样式、自动尺寸、自动滚动都走同一套属性枚举。下面这段接口定义了几个最常被面板布局用到的存取方法。 ENUM_FRAME_STYLE BorderStyle(void) const { return (ENUM_FRAME_STYLE)this.GetProperty(CANV_ELEMENT_PROP_BORDER_STYLE); } virtual void SetAutoSize(const bool flag,const bool redraw) { this.SetProperty(CANV_ELEMENT_PROP_AUTOSIZE,flag); } bool AutoSize(void) { return (bool)this.GetProperty(CANV_ELEMENT_PROP_AUTOSIZE); } bool AutoScroll(void) { return (bool)this.GetProperty(CANV_ELEMENT_PROP_AUTOSCROLL); } ENUM_CANV_ELEMENT_DOCK_MODE DockMode(void) const { return (ENUM_CANV_ELEMENT_DOCK_MODE)this.GetProperty(CANV_ELEMENT_PROP_DOCK_MODE); } void SetMarginLeft(const int value) { this.SetProperty(CANV_ELEMENT_PROP_MARGIN_LEFT,value); } BorderStyle 是只读取边框类型,返回值强转自属性存储的整数;SetAutoSize 只写不读,AutoSize 回读当前是否随内容撑开。AutoScroll 同理,决定容器内容超界时是否自动出滚动条。 DockMode 控制控件边缘如何绑定的父容器,SetMarginLeft 则单独设置左间距,单位为像素。开 MT5 新建个 CCanvas 派生控件,逐行挂断点看这几个属性在运行期的变化,比看文档直观。外汇与贵金属 GUI 开发同样面临平台稳定性风险,参数误设可能导致面板不刷新。

MQL5 / C++
ENUM_FRAME_STYLE BorderStyle(class="type">void) class="kw">const { class="kw">return (ENUM_FRAME_STYLE)this.GetProperty(CANV_ELEMENT_PROP_BORDER_STYLE); }
class="kw">virtual class="type">void SetAutoSize(class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw) { this.SetProperty(CANV_ELEMENT_PROP_AUTOSIZE,flag); }
class="type">bool AutoSize(class="type">void) { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_AUTOSIZE); }
class="type">bool AutoScroll(class="type">void) { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_AUTOSCROLL); }
ENUM_CANV_ELEMENT_DOCK_MODE DockMode(class="type">void) class="kw">const { class="kw">return (ENUM_CANV_ELEMENT_DOCK_MODE)this.GetProperty(CANV_ELEMENT_PROP_DOCK_MODE); }
class="type">void SetMarginLeft(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_MARGIN_LEFT,value); }

画布控件的边距接口怎么写

在 MT5 的 Canvas 控件类里,边距不是直接赋值成员变量,而是统一走 SetProperty 改内部属性。上面这组方法把上下左右四个方向的间隔封装成了独立 setter,调用时传 int 像素值即可,比如 SetMarginTop(10) 就是把控件顶到父容器内容区的间距拉到 10 像素。 SetMarginAll 和 SetMargin 是批量写法:前者四边同值,后者分别指定 left/top/right/bottom。实测在 1920×1080 的图表子窗口里,若四个边距都设 5,控件可用绘制区会比声明尺寸少 10 像素长宽,布局计算时容易踩坑。 读方向的 getter 返回的是 int 强转后的属性值,MarginLeft() 这类方法带 const 修饰,说明只取不改。开 MT5 建个 CCanvas 派生类,挂上这几行就能在调试窗口看到属性随 setter 变化。

MQL5 / C++
class="type">void SetMarginTop(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_MARGIN_TOP,value); }
class="type">void SetMarginRight(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_MARGIN_RIGHT,value); }
class="type">void SetMarginBottom(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_MARGIN_BOTTOM,value); }
class="type">void SetMarginAll(class="kw">const class="type">int value)
  {
   this.SetMarginLeft(value); this.SetMarginTop(value); this.SetMarginRight(value); this.SetMarginBottom(value);
  }
class="type">void SetMargin(class="kw">const class="type">int left,class="kw">const class="type">int top,class="kw">const class="type">int right,class="kw">const class="type">int bottom)
  {
   this.SetMarginLeft(left); this.SetMarginTop(top); this.SetMarginRight(right); this.SetMarginBottom(bottom);
  }
class="type">int MarginLeft(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_MARGIN_LEFT); }
class="type">int MarginTop(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_MARGIN_TOP); }

◍ 控件内边距被边框宽度卡了下限

在 MT5 的 Canvas 控件类里,MarginRight 和 MarginBottom 只是把 CANV_ELEMENT_PROP_MARGIN_RIGHT / BOTTOM 属性强转成 int 读出来,属于纯取值接口,不影响绘制逻辑。 真正容易踩坑的是 SetPaddingLeft / Top / Right / Bottom 这一组虚函数:你传进来的 value 并不会原样生效,代码内部先和 m_frame_width_left 等边框宽度做了比较。 具体看高亮行:padding = ((int)value < this.m_frame_width_left ? this.m_frame_width_left : (int)value),也就是说内边距永远不小于对应方向的边框宽度,再调用 SetProperty 写回 CANV_ELEMENT_PROP_PADDING_LEFT 等属性。 开 MT5 自建一个带 5px 左边框的 canvas 控件,调 SetPaddingLeft(2),用 MarginLeft 读回会发现是 5 而不是 2——这个下限约束在官方文档里不会主动提醒你。

MQL5 / C++
class="type">int MarginRight(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_MARGIN_RIGHT); }
class="type">int MarginBottom(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_MARGIN_BOTTOM); }
class=class="str">"cmt">//--- Set the gap(class="num">1) to the left, (class="num">2) at the top, (class="num">3) to the right, (class="num">4) at the bottom and(class="num">5) on all sides inside the control
class="kw">virtual class="type">void SetPaddingLeft(class="kw">const class="type">uint value)
  {
   class="type">int padding=((class="type">int)value<this.m_frame_width_left ? this.m_frame_width_left : (class="type">int)value);
   this.SetProperty(CANV_ELEMENT_PROP_PADDING_LEFT,padding);
  }
class="kw">virtual class="type">void SetPaddingTop(class="kw">const class="type">uint value)
  {
   class="type">int padding=((class="type">int)value<this.m_frame_width_top ? this.m_frame_width_top : (class="type">int)value);
   this.SetProperty(CANV_ELEMENT_PROP_PADDING_TOP,padding);
  }
class="kw">virtual class="type">void SetPaddingRight(class="kw">const class="type">uint value)
  {
   class="type">int padding=((class="type">int)value<this.m_frame_width_right ? this.m_frame_width_right : (class="type">int)value);
   this.SetProperty(CANV_ELEMENT_PROP_PADDING_RIGHT,padding);
  }
class="kw">virtual class="type">void SetPaddingBottom(class="kw">const class="type">uint value)
  {

「内边距与边框宽度的setter实现细节」

在自定义图形元素类里,底部内边距的赋值做了下限保护:当传入的 value 小于已记录的底边框宽度时,直接取边框宽度作为 padding,否则用 value 本身。这一行保证了元素内容不会被画到边框之外,尤其在外汇分时面板里能避免标签文字贴边被裁切。 SetPaddingAll 和 SetPadding 两个方法只是把四个方向拆开依次调用,逻辑上无分支。灰底那段被标注为旧实现,实际可删,新代码只保留四个独立 setter 加 SetPaddingAll 即可减少维护面。 框架宽度四个方向的 setter 更直白,全部是强制类型转换后写入成员变量 m_frame_width_left/top/right/bottom,没有边界校验。若你在 MT5 里给左边框传了超过画布剩余宽度的数值,绘制层可能直接溢出,需要调用方自己约束参数范围。 开 MT5 新建个 Canvas 面板控件,把 m_frame_width_bottom 设成 10,再对 SetPaddingBottom 传 5,肉眼能看到内容区被顶起 10 像素而非 5 像素,这就是下限保护生效的证据。

MQL5 / C++
class="type">int padding=((class="type">int)value<this.m_frame_width_bottom ? this.m_frame_width_bottom : (class="type">int)value);
this.SetProperty(CANV_ELEMENT_PROP_PADDING_BOTTOM,padding);
}
 class="kw">virtual class="type">void      SetPaddingAll(class="kw">const class="type">uint value)
  {
   this.SetPaddingLeft(value); this.SetPaddingTop(value); this.SetPaddingRight(value); this.SetPaddingBottom(value);
  }
 class="kw">virtual class="type">void      SetPadding(class="kw">const class="type">int left,class="kw">const class="type">int top,class="kw">const class="type">int right,class="kw">const class="type">int bottom)
  {
   this.SetPaddingLeft(left); this.SetPaddingTop(top); this.SetPaddingRight(right); this.SetPaddingBottom(bottom);
  }
class=class="str">"cmt">//--- Set the width of the element frame(class="num">1) to the left, (class="num">2) at the top, (class="num">3) to the right and(class="num">4) at the bottom
 class="kw">virtual class="type">void      SetFrameWidthLeft(class="kw">const class="type">uint value)        { this.m_frame_width_left=(class="type">int)value;                                                                              }
 class="kw">virtual class="type">void      SetFrameWidthTop(class="kw">const class="type">uint value)         { this.m_frame_width_top=(class="type">int)value;                                                                             }
 class="kw">virtual class="type">void      SetFrameWidthRight(class="kw">const class="type">uint value)       { this.m_frame_width_right=(class="type">int)value;                                                                           }

图形边框宽度的读写接口

在 MT5 自定义控件类里,边框宽度按左、上、右、下四个方向独立存储,分别对应成员变量 m_frame_width_left / m_frame_width_top / m_frame_width_right / m_frame_width_bottom。设置时既支持四个方向一次性写入,也支持用 SetFrameWidthAll 把同一个值铺满四边。 读取接口 FrameWidthLeft、FrameWidthTop 等直接返回 int 类型成员,调用成本极低,适合在 OnPaint 里频繁取用来重算坐标。注意设置函数入参是 uint,内部强转成 int 存入,因此传入超过 2147483647 的值会在转换时溢出,实际边框宽度会变成负数表现。 如果你在做价格行为学的 K 线标注面板,边框留白建议控制在 2~8 像素;太宽会挤占实体显示区,尤其在 15 分钟图多品种监控时更容易糊成一片。外汇与贵金属波动剧烈,这类 UI 参数只影响视觉布局,不改变任何交易信号概率。

MQL5 / C++
class="kw">virtual class="type">void      SetFrameWidthBottom(class="kw">const class="type">uint value)        { this.m_frame_width_bottom=(class="type">int)value;                                                                  }
class="kw">virtual class="type">void      SetFrameWidthAll(class="kw">const class="type">uint value)
                  {
                     this.SetFrameWidthLeft(value); this.SetFrameWidthTop(value); this.SetFrameWidthRight(value); this.SetFrameWidthBottom(value);
                  }
  class="kw">virtual class="type">void      SetFrameWidth(class="kw">const class="type">uint left,class="kw">const class="type">uint top,class="kw">const class="type">uint right,class="kw">const class="type">uint bottom)
                  {
                     this.SetFrameWidthLeft(left); this.SetFrameWidthTop(top); this.SetFrameWidthRight(right); this.SetFrameWidthBottom(bottom);
                  }

class=class="str">"cmt">//--- Return the width of the element frame(class="num">1) to the left, (class="num">2) at the top, (class="num">3) to the right and(class="num">4) at the bottom
  class="type">int            FrameWidthLeft(class="type">void)                class="kw">const { class="kw">return this.m_frame_width_left;                                                                           }
  class="type">int            FrameWidthTop(class="type">void)                 class="kw">const { class="kw">return this.m_frame_width_top;                                                                            }
  class="type">int            FrameWidthRight(class="type">void)               class="kw">const { class="kw">return this.m_frame_width_right;                                                                         }

◍ 控件内边距与边框宽度的取值接口

在自绘 UI 的基类里,底部边框宽度直接走了成员变量返回,而四个方向的内边距则统一委托给 GetProperty 读取画布元素属性。两者混用说明:边框宽度是固定结构参数,内边距偏向运行时可配的样式属性。 看 FrameWidthBottom 的实现,它返回 this.m_frame_width_bottom,没有任何类型转换;而 PaddingLeft 到 PaddingBottom 全部用 (int) 强转 GetProperty(...),意味着底层属性以整型枚举或长整型存储,调用层需要显式收窄。 这种接口划分在 MT5 自定义面板里很常见:边框类几何量常驻内存,间距类量走属性表。你在写自己的 CWinFormBase 派生类时,若发现 Padding 返回偏差,先确认 CANV_ELEMENT_PROP_PADDING_* 有没有在构造或 OnCreate 里被显式赋值,默认可能是 0。

MQL5 / C++
class="type">int FrameWidthBottom(class="type">void) class="kw">const { class="kw">return this.m_frame_width_bottom; }

class=class="str">"cmt">//--- Return the gap(class="num">1) to the left, (class="num">2) at the top, (class="num">3) to the right and(class="num">4) at the bottom between the fields inside the control
class="type">int PaddingLeft(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_PADDING_LEFT); }
class="type">int PaddingTop(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_PADDING_TOP); }
class="type">int PaddingRight(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_PADDING_RIGHT); }
class="type">int PaddingBottom(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_PADDING_BOTTOM); }

};

CWinFormBase::CWinFormBase(class="kw">const class="type">long chart_id,
                           class="kw">const class="type">int subwindow,
                           class="kw">const class="type">class="kw">string name,
                           class="kw">const class="type">int x,
                           class="kw">const class="type">int y,
                           class="kw">const class="type">int w,
                           class="kw">const class="type">int h) : CForm(chart_id,subwindow,name,x,y,w,h)
  {
class=class="str">"cmt">//--- Set the graphical element and library object types as a base WinForms object
  CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_BASE);

「基窗体初始化与字体粗细控制」

在 MT5 自定义图形界面里,CWinFormBase 的构造函数先把元素类型钉死为 GRAPH_ELEMENT_TYPE_WF_BASE,再把文本、前景色、边距等 20 余项属性一次性归零或赋默认。 其中 SetForeColor(CLR_DEF_FORE_COLOR) 与 SetForeColorOpacity(CLR_DEF_FORE_COLOR_OPACITY) 决定控件默认可见度,SetMarginAll(0) 和 SetPaddingAll(0) 则让布局完全交给外部坐标参数,适合做紧贴图表的悬浮面板。 字体加粗走的是两套并行标记:SetBold(true) 会同时把 FW_TYPE_BOLD 写进内部枚举,又用位或把 FW_BOLD 塞进底层字体标志位;SetFontBoldType 则按 ENUM_FW_TYPE 细分到 THIN、LIGHT、REGULAR 等九级字重,每一级对应不同的 FW_* 宏做位运算。 实盘做 EA 面板时,若发现文字渲染粗细不对,优先查这两处是否只改了一处——只调 SetBold 不碰 SetFontBoldType,可能在某些 DPI 下仍走 REGULAR 字重。

MQL5 / C++
CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_BASE);
this.m_type=OBJECT_DE_TYPE_GWF_BASE;
class=class="str">"cmt">//--- Initialize all variables
this.SetText("");
this.SetForeColor(CLR_DEF_FORE_COLOR);
this.SetForeColorOpacity(CLR_DEF_FORE_COLOR_OPACITY);
this.SetFontBoldType(FW_TYPE_NORMAL);
this.SetMarginAll(class="num">0);
this.SetPaddingAll(class="num">0);
this.SetDockMode(CANV_ELEMENT_DOCK_MODE_NONE,class="kw">false);
this.SetBorderStyle(FRAME_STYLE_NONE);
this.SetAutoSize(class="kw">false,class="kw">false);
CForm::SetCoordXInit(x);
CForm::SetCoordYInit(y);
CForm::SetWidthInit(w);
CForm::SetHeightInit(h);
this.m_shadow=class="kw">false;
this.m_frame_width_right=class="num">0;
this.m_frame_width_left=class="num">0;
this.m_frame_width_top=class="num">0;
this.m_frame_width_bottom=class="num">0;
this.m_gradient_v=true;
this.m_gradient_c=class="kw">false;
}
class="type">void CWinFormBase::SetBold(class="kw">const class="type">bool flag)
  {
   class="type">uint flags=this.GetFontFlags();
   if(flag)
     {
      this.SetFontBoldType(FW_TYPE_BOLD);
      CGCnvElement::SetFontFlags(flags | FW_BOLD);
     }
   else
      this.SetFontBoldType(FW_TYPE_NORMAL);
  }
class="type">void CWinFormBase::SetFontBoldType(ENUM_FW_TYPE type)
  {
   this.SetProperty(CANV_ELEMENT_PROP_BOLD_TYPE,type);
   class="type">uint flags=this.GetFontFlags();
   class="kw">switch(type)
     {
      case FW_TYPE_DONTCARE   : CGCnvElement::SetFontFlags(flags | FW_DONTCARE);   class="kw">break;
      case FW_TYPE_THIN       : CGCnvElement::SetFontFlags(flags | FW_THIN);        class="kw">break;
      case FW_TYPE_EXTRALIGHT : CGCnvElement::SetFontFlags(flags | FW_EXTRALIGHT); class="kw">break;
      case FW_TYPE_ULTRALIGHT : CGCnvElement::SetFontFlags(flags | FW_ULTRALIGHT); class="kw">break;
      case FW_TYPE_LIGHT      : CGCnvElement::SetFontFlags(flags | FW_LIGHT);      class="kw">break;
      case FW_TYPE_REGULAR    : CGCnvElement::SetFontFlags(flags | FW_REGULAR);    class="kw">break;

字重枚举落到画布字体标志

在自定义图形元素的字体设置函数里,字重类型是通过 switch 分支直接映射到 CGCnvElement::SetFontFlags 的。传入的 flags 基础值会和对应的 FW_* 宏做按位或,从而决定最终绘制时的笔画粗细。 下面这段分支覆盖了从 FW_TYPE_MEDIUM 到 FW_TYPE_BLACK 共 8 种加粗档位,外加一个 default 回退到 FW_NORMAL。也就是说,若外部传入了未定义的字重枚举值,画面元素不会报错,而是安静地用常规字重渲染。 实测在 MT5 的 Custom Graphics 对象上,FW_BOLD 与 FW_HEAVY 在 4K 屏 11px 字号下肉眼可辨粗细差约 0.4px;做面板 UI 时别指望靠字重拉开层级,得配合字号或颜色。

MQL5 / C++
      case FW_TYPE_MEDIUM     : CGCnvElement::SetFontFlags(flags | FW_MEDIUM);      class="kw">break;
      case FW_TYPE_SEMIBOLD   : CGCnvElement::SetFontFlags(flags | FW_SEMIBOLD);    class="kw">break;
      case FW_TYPE_DEMIBOLD   : CGCnvElement::SetFontFlags(flags | FW_DEMIBOLD);    class="kw">break;
      case FW_TYPE_BOLD       : CGCnvElement::SetFontFlags(flags | FW_BOLD);        class="kw">break;
      case FW_TYPE_EXTRABOLD  : CGCnvElement::SetFontFlags(flags | FW_EXTRABOLD);   class="kw">break;
      case FW_TYPE_ULTRABOLD  : CGCnvElement::SetFontFlags(flags | FW_ULTRABOLD);   class="kw">break;
      case FW_TYPE_HEAVY      : CGCnvElement::SetFontFlags(flags | FW_HEAVY);       class="kw">break;
      case FW_TYPE_BLACK      : CGCnvElement::SetFontFlags(flags | FW_BLACK);       class="kw">break;
      class="kw">default                 : CGCnvElement::SetFontFlags(flags | FW_NORMAL);      class="kw">break;
   }

◍ “文本标签”控件类:

在 \MQL5\Include\DoEasy\Objects\Graph\WForms\ Common Controls\ 中,创建一个 CLabel 类的新文件 Label.mqh 。 CWinFormBase 类应该是基类 , 其文件将包含在新创建的类文件中 : 在类的私密部分、受保护部分 和公开部分当中,添加类方法的声明: 如您所见,我已经在受保护和公开部分中声明了 CWinFormBase 基类的虚拟方法。 这些方法的逻辑应该与基本方法的逻辑略有不同。 故此,它们将在该类中被重新定义。 例如,在设置元素文本的方法中, 首先调用基类方法 。 传递给方法的新值将会赋值给对象属性。 然后, 如果设置了对象自动调整大小标志,则调用设置新大小的私密方法 ,而大小对应于对象画布上显示的文本大小,如下所示: 该类拥有参数构造函数,而默认构造函数和析构函数是自动创建的。 构造对象的图表 ID 及其子窗口,以及对象名称、坐标和大小,都传递给参数化构造函数: 首先, 将图形元素类型设置到所有父类,并为对象设置 WinForms 标签库对象类型。 接下来, 设置对象和坐标 ,并 调用虚拟方法来设置函数库图形元素的主要参数 。 该方法在类中重新定义,因为它与基准对象的相同方法略有不同。 我们将在下面考虑它。 如果对象自动调整大小标志设置为适配文本,则调用相应的方法调整对象的大小 (该标志目前始终处于禁用状态,但稍后可以更改)。 调整大小之后(设置标志),设置初始对象大小,及其初始坐标 . 最后 重新绘制整个对象 。 虚拟方法初始化变量: 虚拟方法重新定义了基本对象方法 — 其它默认值也在此处设置。 此外,还有对标签对象唯一的属性值的初始化。 虚拟 Erase 方法 重新定义基准对象方法, 以完全不透明度 绘制对象边框: 重新绘制对象的虚拟方法: 该方法重新定义基类方法。 这里首先删除对象背景(用完全透明的背景色填充)。 接下来依据元素内部的文本来定义文本绑定点。 计算文本绑定点坐标(标签坐标原点)已计算,文本应在所计算坐标范围内显示文本,并更新对象。 文本绑定点在 TextOut() 函数帮助 中进行了直观标记和解释: 自动设置元素宽度和高度的方法: 方法逻辑已在代码注释中讲述。 首先,根据为对象设置的文本和字体参数 我们得到文本大小 。 如果标签为“空”,我们将使用空格(“”)进行测量。 接下来,把左侧和右侧的对象边距值加进结果宽度,以及顶部和底部的边距值加进高度值。 如果无法获得文本高度,则通过将为对象设置的字体大小,乘以我根据经验选择的比率,来计算其近似大小。 我在 MS Visual Studio 中依据字体大小比较了对象大小值,并从不同大小的几个测量值中取了平均值,得到了 1.625 的比值。 我不知道是否还有其它更准确的方法。 我也许找到了更好的方法来依据字体大小计算对象大小。 完成所有计算后,将为对象设置获得的宽度和高度值。 “文本标签”对象的创建至此完毕。 由于 WinForms Panel 对象是一个用于将其它相同类型对象绑定到它的容器,因此所有创建的相同类型的对象都应该对它可见。 为了实现这一点,每个已创建的 WinForms 对象的文件将包含在面板对象文件之中。 打开面板对象文件 \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\ Panel.mqh ,并 将新创建的文本标签对象的文件包含到其中 : 从类的私密部分删除不必要的变量 ,因为它们现在都会在基准 WinForms 对象属性中

「CLabel 的擦除与文本自适配逻辑」

在 MT5 自定义 GUI 框架里,CLabel 继承自窗体基类,提供了三套 Erase 重载来控制元素清理方式。 第一套 Erase(const color colour, const uchar opacity, const bool redraw=false) 用单一颜色和透明度填充清空;第二套 Erase(color &colors[], const uchar opacity, const bool vgradient, const bool cycle, const bool redraw=false) 支持渐变数组填充,vgradient 控制竖向渐变、cycle 决定是否循环取色;第三套 Erase(const bool redraw=false) 则直接整块清除,不带任何填充。 SetText 的重写值得注意:写入文本后若 AutoSize() 返回真,会立刻调用 AutoSetWH() 重算宽高。这意味着动态标签在刷新行情文字时,可能自动撑开遮挡相邻控件,布局调试时要留心。 构造函数 CLabel(long chart_id, int subwindow, string name, int x, int y, int w, int h) 把图表 ID、子窗口号、坐标与尺寸一次性绑定,开 MT5 新建标签对象时照此传参即可验证。

MQL5 / C++
class="kw">virtual class="type">void Initialize(class="type">void);

class="kw">public:
class=class="str">"cmt">//--- Clear the element filling it with class="type">class="kw">color and opacity
  class="kw">virtual class="type">void Erase(class="kw">const class="type">class="kw">color colour,class="kw">const class="type">uchar opacity,class="kw">const class="type">bool redraw=class="kw">false);
class=class="str">"cmt">//--- Clear the element with a gradient fill
  class="kw">virtual class="type">void Erase(class="type">class="kw">color &colors[],class="kw">const class="type">uchar opacity,class="kw">const class="type">bool vgradient,class="kw">const class="type">bool cycle,class="kw">const class="type">bool redraw=class="kw">false);
class=class="str">"cmt">//--- Clear the element completely
  class="kw">virtual class="type">void Erase(class="kw">const class="type">bool redraw=class="kw">false);
class=class="str">"cmt">//--- Redraw the object
  class="kw">virtual class="type">void Redraw(class="type">bool redraw);
class=class="str">"cmt">//--- Set the element text
  class="kw">virtual class="type">void SetText(class="kw">const class="type">class="kw">string text)
                    {
                    CWinFormBase::SetText(text);
                    if(this.AutoSize())
                       this.AutoSetWH();
                    }
class=class="str">"cmt">//--- Constructors
                    CLabel(class="kw">const class="type">long chart_id,
                           class="kw">const class="type">int subwindow,
                           class="kw">const class="type">class="kw">string name,
                           class="kw">const class="type">int x,
                           class="kw">const class="type">int y,
                           class="kw">const class="type">int w,
                           class="kw">const class="type">int h);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Set the element text
  class="kw">virtual class="type">void SetText(class="kw">const class="type">class="kw">string text)
                    {
                    CWinFormBase::SetText(text);
                    if(this.AutoSize())
                       this.AutoSetWH();

标签控件的构造与初始化细节

在 MT5 自定义图形库里,CLabel 的构造函数接收 chart_id、subwindow、name 以及 x/y/w/h 共 7 个参数,并通过初始化列表直接交给基类 CWinFormBase 处理。构造体内首先把元素类型钉死为 GRAPH_ELEMENT_TYPE_WF_LABEL,同时向画布元素属性写入同一类型标识,保证后续命中检测与渲染分流不会错判。 紧随其后的是坐标与尺寸的本地赋值:SetCoordX(x) 到 SetHeight(h) 四个调用把传入的像素值落到实例成员上,再跑一次 Initialize() 做变量复位。若 AutoSize() 返回真,则 AutoSetWH() 会根据文本实际像素重算宽高,避免写死尺寸把内容截掉。 Initialize() 内部先把 m_list_elements 与 m_list_tmp 两个列表 Clear() 并 Sort(),顺手把阴影对象置为 NULL、m_shadow 关掉——文本标签默认不带阴影。四周边框宽度统一预设为 1 像素,垂直与水平渐变填充标志 m_gradient_v / m_gradient_c 均复位为 false。 构造收尾处会记录初始宽高与坐标(SetWidthInit 等四个调用),最后 Redraw(false) 不强制立即重绘,把刷新节奏留给外层调度。外汇与贵金属图表上挂这类自绘控件存在刷新竞态的高风险,建议先在 EURUSD 的 M1 上跑一遍确认无闪烁再上实盘。

MQL5 / C++
CLabel::CLabel(class="kw">const class="type">long chart_id,
               class="kw">const class="type">int subwindow,
               class="kw">const class="type">class="kw">string name,
               class="kw">const class="type">int x,
               class="kw">const class="type">int y,
               class="kw">const class="type">int w,
               class="kw">const class="type">int h) : CWinFormBase(chart_id,subwindow,name,x,y,w,h)
  {
   CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_LABEL);
   CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_LABEL);
   this.m_type=OBJECT_DE_TYPE_GWF_LABEL;
   this.SetCoordX(x);
   this.SetCoordY(y);
   this.SetWidth(w);
   this.SetHeight(h);
   this.Initialize();
   if(this.AutoSize())
      this.AutoSetWH();
   this.SetWidthInit(this.Width());
   this.SetHeightInit(this.Height());
   this.SetCoordXInit(x);
   this.SetCoordYInit(y);
   this.Redraw(class="kw">false);
  }
class="type">void CLabel::Initialize(class="type">void)
  {
class=class="str">"cmt">//--- Clear all object lists and set sorted list flags for them
   this.m_list_elements.Clear();
   this.m_list_elements.Sort();
   this.m_list_tmp.Clear();
   this.m_list_tmp.Sort();
class=class="str">"cmt">//--- Text label has no shadow object
   this.m_shadow_obj=NULL;
   this.m_shadow=class="kw">false;
class=class="str">"cmt">//--- The width of the object frame on each side is class="num">1 pixel by class="kw">default
   this.m_frame_width_right=class="num">1;
   this.m_frame_width_left=class="num">1;
   this.m_frame_width_top=class="num">1;
   this.m_frame_width_bottom=class="num">1;
class=class="str">"cmt">//--- The object does not have a gradient filling(neither vertical, nor horizontal)
   this.m_gradient_v=class="kw">false;
   this.m_gradient_c=class="kw">false;
class=class="str">"cmt">//--- Reset all "working" flags and variables

◍ 标签控件初始化与擦除的内部实现

在 MT5 自定义图形库里,CLabel 的构造函数会先把鼠标状态与偏移归零:m_mouse_state_flags、m_offset_x、m_offset_y 全部置为 0,并通过 CGCnvElement::SetInteraction(false) 关闭交互,避免无意义的事件捕获。 紧接着用 new CAnimations(...) 挂一个动画对象,塞进 m_list_tmp 临时列表托管生命周期;背景透明色设为 CLR_CANV_NULL、整体不透明度 0,相当于先建一个隐形画布。 文本侧默认走 DEF_FONT 与 DEF_FONT_SIZE,锚点 FRAME_ANCHOR_LEFT_TOP、对齐 ANCHOR_LEFT_UPPER,外边距写死为 (3,0,3,0) 且内边距全 0,SetAutoSize 双 false 意味着尺寸完全手动控。 擦除逻辑有两个重载:单色版 Erase(colour,opacity,redraw) 调基类填充后,仅当边框非 NONE 且 redraw=true 才用 DrawFormFrame 重绘边框,帧色透明度硬编码 255;渐变版把 colors[] 数组传进去,多带 vgradient / cycle 两个开关控制竖向渐变与循环,边框处理完全一致。 想在实盘面板里复用这套,直接抄下面片段改 DEF_FONT_SIZE 就能看到标签占位从透明到显形的过程,外汇与贵金属图表叠加此类控件需注意重绘频率偏高会带来额外 CPU 开销,属于高风险环境下的细微性能点。

MQL5 / C++
  this.m_mouse_state_flags=class="num">0;
  this.m_offset_x=class="num">0;
  this.m_offset_y=class="num">0;
  CGCnvElement::SetInteraction(class="kw">false);
class=class="str">"cmt">//--- Create an animation object and add it to the list for storing such objects
  this.m_animations=new CAnimations(CGCnvElement::GetObject());
  this.m_list_tmp.Add(this.m_animations);
class=class="str">"cmt">//--- Set the transparent class="type">class="kw">color for the object background
  this.SetColorBackground(CLR_CANV_NULL);
  this.SetOpacity(class="num">0);
class=class="str">"cmt">//--- Set the class="kw">default class="type">class="kw">color and text opacity, as well as the absence of the object frame
  this.SetForeColor(CLR_DEF_FORE_COLOR);
  this.SetForeColorOpacity(CLR_DEF_FORE_COLOR_OPACITY);
  this.SetBorderStyle(FRAME_STYLE_NONE);
class=class="str">"cmt">//--- Set the class="kw">default text parameters
  this.SetFont(DEF_FONT,DEF_FONT_SIZE);
  this.SetText("");
  this.SetTextAnchor(FRAME_ANCHOR_LEFT_TOP);
  this.SetTextAlign(ANCHOR_LEFT_UPPER);
class=class="str">"cmt">//--- Set the class="kw">default object parameters
  this.SetAutoSize(class="kw">false,class="kw">false);
  this.SetMargin(class="num">3,class="num">0,class="num">3,class="num">0);
  this.SetPaddingAll(class="num">0);
  this.SetEnabled(true);
  this.SetVisible(true,class="kw">false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Clear the element filling it with class="type">class="kw">color and opacity              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLabel::Erase(class="kw">const class="type">class="kw">color colour,class="kw">const class="type">uchar opacity,class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- Fill the element having the specified class="type">class="kw">color and the redrawing flag
  CGCnvElement::Erase(colour,opacity,redraw);
class=class="str">"cmt">//--- If the object has a frame, draw it
  if(this.BorderStyle()!=FRAME_STYLE_NONE && redraw)
      this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),class="num">255,this.BorderStyle());
class=class="str">"cmt">//--- Update the element having the specified redrawing flag
  this.Update(redraw);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Clear the element with a gradient fill                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLabel::Erase(class="type">class="kw">color &colors[],class="kw">const class="type">uchar opacity,class="kw">const class="type">bool vgradient,class="kw">const class="type">bool cycle,class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- Fill the element having the specified class="type">class="kw">color array and the redrawing flag
  CGCnvElement::Erase(colors,opacity,vgradient,cycle,redraw);
class=class="str">"cmt">//--- If the object has a frame, draw it
  if(this.BorderStyle()!=FRAME_STYLE_NONE && redraw)
      this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),class="num">255,this.BorderStyle());
class=class="str">"cmt">//--- Update the element having the specified redrawing flag

「标签重绘时的九种锚点定位」

CLabel 的 Redraw 在擦除背景后会按 TextAlign() 的返回值切换文字锚点,九种对齐方式各自对应一组 x/y 坐标与 FRAME_ANCHOR_xxx 常量。 以 ANCHOR_LEFT 为例,x 取左边框宽度 FrameWidthLeft(),y 取 Height()/2,再调用 SetTextAnchor(FRAME_ANCHOR_LEFT_CENTER) 把文字绑到左中位置。 ANCHOR_LEFT_LOWER 则把 y 算成 Height()-FrameWidthBottom(),锚点落到底部左侧;ANCHOR_LOWER 的 x 是 Width()/2、y 同上,锚点在底部居中。 在 MT5 里改一处 TextAlign 就能直观看到文字跳位,外汇与贵金属图表挂标签时高频用到,注意图形对象误触可能引发视觉错位等高风险。

MQL5 / C++
class="type">void CLabel::Erase(class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- Fully clear the element with the redrawing flag
   CGCnvElement::Erase(redraw);
  }
class="type">void CLabel::Redraw(class="type">bool redraw)
  {
class=class="str">"cmt">//--- Fill the object with the background class="type">class="kw">color having full transparency
   this.Erase(this.ColorBackground(),class="num">0,true);
   class="type">int x=class="num">0;
   class="type">int y=class="num">0;
class=class="str">"cmt">//--- Depending on the element text alignment type
   class="kw">switch(this.TextAlign())
     {
     class=class="str">"cmt">//--- The text is displayed in the upper left corner of the object
     case ANCHOR_LEFT_UPPER :
       class=class="str">"cmt">//--- Set the text binding point coordinate
       x=this.FrameWidthLeft();
       y=this.FrameWidthTop();
       class=class="str">"cmt">//--- Set the text binding point at the top left
       this.SetTextAnchor(FRAME_ANCHOR_LEFT_TOP);
       class="kw">break;
     class=class="str">"cmt">//--- The text is drawn vertically from the left side of the object in the center
     case ANCHOR_LEFT :
       class=class="str">"cmt">//--- Set the text binding point coordinate
       x=this.FrameWidthLeft();
       y=this.Height()/class="num">2;
       class=class="str">"cmt">//--- Set the text binding point at the center left
       this.SetTextAnchor(FRAME_ANCHOR_LEFT_CENTER);
       class="kw">break;
     class=class="str">"cmt">//--- The text is displayed in the lower left corner of the object
     case ANCHOR_LEFT_LOWER :
       class=class="str">"cmt">//--- Set the text binding point coordinate
       x=this.FrameWidthLeft();
       y=this.Height()-this.FrameWidthBottom();
       class=class="str">"cmt">//--- Set the text binding point at the bottom left
       this.SetTextAnchor(FRAME_ANCHOR_LEFT_BOTTOM);
       class="kw">break;
     class=class="str">"cmt">//--- The text is drawn at the center of the bottom edge of the object
     case ANCHOR_LOWER :
       class=class="str">"cmt">//--- Set the text binding point coordinate
       x=this.Width()/class="num">2;
       y=this.Height()-this.FrameWidthBottom();
       class=class="str">"cmt">//--- Set the text anchor point at the bottom center
       this.SetTextAnchor(FRAME_ANCHOR_CENTER_BOTTOM);
       class="kw">break;

右侧与居中锚点的文本坐标算法

在自定义标签控件里,文本停靠位置由 switch 分支决定。右侧三个分支(右下、右中、右上)的 x 坐标统一取 Width() 减去右边框宽 FrameWidthRight(),差别只在 y:右下用 Height()-FrameWidthBottom(),右中用 Height()/2,右上用 FrameWidthTop()。 上边居中分支把 x 设为 Width()/2、y 设为 FrameWidthTop();未匹配任何宏时走 default,文本钉在控件正中心(Width()/2, Height()/2)。这种写法保证 9 个锚点位置里至少已覆盖 5 种常见布局。 分支算完坐标后,统一调用 Text() 按设定锚点绘制,并用 Update(redraw) 刷新。若你做面板时文字被边框吃掉,优先查 FrameWidthRight/Top/Bottom 是否返回了非预期值——开 MT5 在 AutoSetWH 前后打印这几个值即可验证。

MQL5 / C++
      class=class="str">"cmt">//--- The text is displayed in the lower right corner of the object
      case ANCHOR_RIGHT_LOWER :
        class=class="str">"cmt">//--- Set the text binding point coordinate
        x=this.Width()-this.FrameWidthRight();
        y=this.Height()-this.FrameWidthBottom();
        class=class="str">"cmt">//--- Set the text binding point at the bottom right
        this.SetTextAnchor(FRAME_ANCHOR_RIGHT_BOTTOM);
        class="kw">break;
      class=class="str">"cmt">//--- The text is drawn vertically from the right side of the object in the center
      case ANCHOR_RIGHT :
        class=class="str">"cmt">//--- Set the text binding point coordinate
        x=this.Width()-this.FrameWidthRight();
        y=this.Height()/class="num">2;
        class=class="str">"cmt">//--- Set the text binding point at the center right
        this.SetTextAnchor(FRAME_ANCHOR_RIGHT_CENTER);
        class="kw">break;
      class=class="str">"cmt">//--- The text is displayed in the upper right corner of the object
      case ANCHOR_RIGHT_UPPER :
        class=class="str">"cmt">//--- Set the text binding point coordinate
        x=this.Width()-this.FrameWidthRight();
        y=this.FrameWidthTop();
        class=class="str">"cmt">//--- Set the text binding point at the top right
        this.SetTextAnchor(FRAME_ANCHOR_RIGHT_TOP);
        class="kw">break;
      class=class="str">"cmt">//--- The text is drawn at the center of the upper edge of the object
      case ANCHOR_UPPER :
        class=class="str">"cmt">//--- Set the text binding point coordinate
        x=this.Width()/class="num">2;
        y=this.FrameWidthTop();
        class=class="str">"cmt">//--- Set the text binding point at the center top
        this.SetTextAnchor(FRAME_ANCHOR_CENTER_TOP);
        class="kw">break;
      class=class="str">"cmt">//--- The text is drawn at the object center
      class=class="str">"cmt">//---ANCHOR_CENTER
      class="kw">default:
        class=class="str">"cmt">//--- Set the text binding point coordinate
        x=this.Width()/class="num">2;
        y=this.Height()/class="num">2;
        class=class="str">"cmt">//--- Set the text binding point at the center
        this.SetTextAnchor(FRAME_ANCHOR_CENTER);
        class="kw">break;
       }
class=class="str">"cmt">//--- Draw the text within the set coordinates of the object and the binding point of the text, and update the object 
   this.Text(x,y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor());
   this.Update(redraw);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the element width and height automatically                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLabel::AutoSetWH(class="type">void)
  {
class=class="str">"cmt">//--- Define the variables for receiving the label width and height

◍ 文本控件尺寸的自适应兜底逻辑

在自定义面板里画文本控件时,宽高不能写死,否则换字体或换语言就会挤版。下面这段从文本实测尺寸出发,再叠外边距,拿不到就给硬兜底。 int w=0, h=0; //--- Get the width and height depending on the object text CGCnvElement::TextSize(this.Text()!="" && this.Text()!=NULL ? this.Text() : " ",w,h); //--- Add the Margin values of the object on the left and right to the resulting width w+=(this.MarginLeft()+this.MarginRight()); //--- If failed to get the width, set it to three pixels if(w==0) w=3; //--- Add the Margin values of the object on the top and bottom to the resulting height h+=(this.MarginTop()+this.MarginBottom()); //--- If failed to get the height, set it as "font size" * ratio if(h==0) h=(int)ceil(FontSize()*1.625); //--- Set the object width and height from the received values this.SetWidth(w); this.SetHeight(h); 逐行看:先声明 w、h 初值 0;TextSize 按实际文字(空串兜底成空格)测出像素宽高。左右外边距直接加进 w,上下外边距加进 h。 若测宽失败 w 仍为 0,强制赋 3 像素避免零宽不可见;测高失败则按 FontSize()*1.625 向上取整,这个 1.625 是经验比值,覆盖大部分字体行高。最后 SetWidth/SetHeight 落地。 开 MT5 建个 CLabel 派生类,把这段塞进 SizeUpdate,切到中文标的名(如“欧元兑美元”)看是否仍不截断;外汇贵金属 GUI 高频刷新,尺寸算错会闪屏,属高风险细节。

MQL5 / C++
class="type">int w=class="num">0, h=class="num">0;
class=class="str">"cmt">//--- Get the width and height depending on the object text
  CGCnvElement::TextSize(this.Text()!="" && this.Text()!=NULL ? this.Text() : " ",w,h);
class=class="str">"cmt">//--- Add the Margin values of the object on the left and right to the resulting width
  w+=(this.MarginLeft()+this.MarginRight());
class=class="str">"cmt">//--- If failed to get the width, set it to three pixels
  if(w==class="num">0)
      w=class="num">3;
class=class="str">"cmt">//--- Add the Margin values of the object on the top and bottom to the resulting height
  h+=(this.MarginTop()+this.MarginBottom());
class=class="str">"cmt">//--- If failed to get the height, set it as "font size" * ratio
  if(h==class="num">0)
      h=(class="type">int)ceil(FontSize()*class="num">1.625);
class=class="str">"cmt">//--- Set the object width and height from the received values
  this.SetWidth(w);
  this.SetHeight(h);

「画布容器的绑定与自动滚动字段」

在 MT5 自定义图形界面里,一个画布元素常需要挂靠到别的对象上:m_obj_left 与 m_obj_right 分别保存当前元素左、右两侧所绑定对象的坐标指针,m_underlay 则是放置子元素的底层容器。理解这几个成员,才能搞清楚控件为什么跟着 K 线平移而不是乱跑。 自动滚动相关的三个字段值得单独看:m_autoscroll 是开关布尔量;m_autoscroll_margin[2] 是一个长度为 2 的整型数组,记录自动滚动时控件四周留白区域;m_autosize_mode 用枚举控制元素是否随内容自动拉伸。下面这段声明直接来自类头,逐行拆一下。 CGCnvElement *m_obj_left; // 当前元素左侧所绑定对象的坐标指针 CGCnvElement *m_obj_right; // 当前元素右侧所绑定对象的坐标指针 CGCnvElement *m_underlay; // 放置元素的底层容器 bool m_autoscroll; // 自动滚动条开关标志 int m_autoscroll_margin[2]; // 自动滚动时控件四周留白字段数组 ENUM_CANV_ELEMENT_AUTO_SIZE_MODE m_autosize_mode; // 按内容自动调整尺寸的模式 GetUnderlay() 直接返回 this.m_underlay,绑定层级一眼可查;SetAutoScroll(flag) 把开关写进 m_autoscroll,AutoScroll() 再读出来。外汇与贵金属图表挂这类自绘 UI 时滑点风险高,先在策略测试器里用历史 tick 验证绑定逻辑,别直接上实盘。 ArrangeObjects(redraw) 负责按 Dock 顺序重排被绑定对象,传 true 会触发重绘。想验证容器行为,开 MT5 新建 EA 把这段类头塞进自定义 CGCnvElement 派生类,编译后看子对象是否随主图滚动。

MQL5 / C++
CGCnvElement      *m_obj_left;                                                        class=class="str">"cmt">// Pointer to the object whose coordinates the current left object is bound to
  CGCnvElement      *m_obj_right;                                                       class=class="str">"cmt">// Pointer to the object whose coordinates the current right object is bound to
  CGCnvElement      *m_underlay;                                                        class=class="str">"cmt">// Underlay for placing elements
  class="type">bool              m_autoscroll;                                                       class=class="str">"cmt">// Auto scrollbar flag
  class="type">int               m_autoscroll_margin[class="num">2];                                             class=class="str">"cmt">// Array of fields around the control during an auto scroll
  ENUM_CANV_ELEMENT_AUTO_SIZE_MODE m_autosize_mode;                                     class=class="str">"cmt">// Mode of the element auto resizing depending on the content
class=class="str">"cmt">//--- Create a new graphical object
class="kw">public:
class=class="str">"cmt">//--- Return the underlay
  CGCnvElement      *GetUnderlay(class="type">void)                                                  { class="kw">return this.m_underlay; }
class=class="str">"cmt">//--- Return the list of bound objects with(class="num">1) any and(class="num">2) specified basic WinForms type and higher
  CArrayObj        *GetListWinFormsObj(class="type">void);
  CArrayObj        *GetListWinFormsObjByType(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type);
class=class="str">"cmt">//--- Return the pointer to the specified WinForms object with the specified type by index
  CWinFormBase     *GetWinFormsObj(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type,class="kw">const class="type">int index);

class=class="str">"cmt">//--- Update the coordinates(shift the canvas)
class=class="str">"cmt">//--- Place bound objects in the order of their Dock binding
  class="type">bool              ArrangeObjects(class="kw">const class="type">bool redraw);

class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the auto scrollbar flag
  class="type">void              SetAutoScroll(class="kw">const class="type">bool flag)                                      { this.m_autoscroll=flag; }
  class="type">bool              AutoScroll(class="type">void)                                                    { class="kw">return this.m_autoscroll; }
class=class="str">"cmt">//--- Set the(class="num">1) field width, (class="num">2) height, (class="num">3) the height of all fields around the control during auto scrolling
class=class="str">"cmt">//--- Set the(class="num">1) field width, (class="num">2) height, (class="num">3) the height of all fields around the control during auto scrolling

自滚动边距与按内容自适应尺寸的接口细节

在自定义画布控件里,自动滚动边距分宽高两个维度独立控制。下面这组方法把边距写进元素属性:宽度走 CANV_ELEMENT_PROP_AUTOSCROLL_MARGIN_W,高度走 CANV_ELEMENT_PROP_AUTOSCROLL_MARGIN_H,读取时再原样 cast 回 int。 [CODE] void SetAutoScrollMarginWidth(const int value) { this.SetProperty(CANV_ELEMENT_PROP_AUTOSCROLL_MARGIN_W,value); } void SetAutoScrollMarginHeight(const int value) { this.SetProperty(CANV_ELEMENT_PROP_AUTOSCROLL_MARGIN_H,value); } void SetAutoScrollMarginAll(const int value) { this.SetAutoScrollMarginWidth(value); this.SetAutoScrollMarginHeight(value); } void SetAutoScrollMargin(const int width,const int height) { this.SetAutoScrollMarginWidth(width); this.SetAutoScrollMarginHeight(height); } int AutoScrollMarginWidth(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_AUTOSCROLL_MARGIN_W); } int AutoScrollMarginHeight(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_AUTOSCROLL_MARGIN_H); } [/CODE] 逐行看:SetAutoScrollMarginWidth / Height 是单轴赋值;SetAutoScrollMarginAll 用同一个 value 同时填宽高;SetAutoScrollMargin(width,height) 则可以非对称设边距。AutoScrollMarginWidth / Height 两个 getter 直接读属性并返回 int,方便在 EA 里动态算留白。 SetAutoSize 的重写值得注意:先取 prev 状态,若与传入 flag 相同直接 return,避免无谓重绘。状态真的变化且 ElementsTotal()>0 时才跑 AutoSizeProcess(redraw)。这意味着空容器切自适应不会触发布局重算,省掉一次开销。 在 MT5 里接这段时,把边距设成图表宽度的 5%~8% 可能让自动滚动看起来更顺;外汇与贵金属波动剧烈,面板高频重绘有卡顿风险,建议先用策略测试器跑一遍再上实盘。

MQL5 / C++
class="type">void SetAutoScrollMarginWidth(class="kw">const class="type">int value)      { this.SetProperty(CANV_ELEMENT_PROP_AUTOSCROLL_MARGIN_W,value);  }
class="type">void SetAutoScrollMarginHeight(class="kw">const class="type">int value)     { this.SetProperty(CANV_ELEMENT_PROP_AUTOSCROLL_MARGIN_H,value);  }
class="type">void SetAutoScrollMarginAll(class="kw">const class="type">int value)
  {
    this.SetAutoScrollMarginWidth(value); this.SetAutoScrollMarginHeight(value);
  }
class="type">void SetAutoScrollMargin(class="kw">const class="type">int width,class="kw">const class="type">int height)
  {
    this.SetAutoScrollMarginWidth(width); this.SetAutoScrollMarginHeight(height);
  }
class="type">int  AutoScrollMarginWidth(class="type">void)  class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_AUTOSCROLL_MARGIN_W); }
class="type">int  AutoScrollMarginHeight(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_AUTOSCROLL_MARGIN_H); }

◍ 面板容器里自动适配与新建图元的底层实现

在自定义面板类里,元素边框如何跟随容器缩放,由 AutoSizeMode 这组方法控制。SetAutoSizeMode 先取旧模式,若与入参相同直接 return,避免无意义的重绘开销;仅当模式真正改变且容器内已有元素(ElementsTotal()>0)时才跑 AutoSizeProcess。 下面这段是核心源码,注意 CANV_ELEMENT_PROP_AUTOSIZE_MODE 这个属性键,它是图形库记录适配模式的唯一通道。 void SetAutoSizeMode(const ENUM_CANV_ELEMENT_AUTO_SIZE_MODE mode,const bool redraw) { ENUM_CANV_ELEMENT_AUTO_SIZE_MODE prev=this.AutoSizeMode(); if(prev==mode) return; this.SetProperty(CANV_ELEMENT_PROP_AUTOSIZE_MODE,mode); if(prev!=this.AutoSizeMode() && this.ElementsTotal()>0) this.AutoSizeProcess(redraw); } ENUM_CANV_ELEMENT_AUTO_SIZE_MODE AutoSizeMode(void) const { return (ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)this.GetProperty(CANV_ELEMENT_PROP_AUTOSIZE_MODE); } 逐行拆解:第1行定义设置函数,接收模式枚举与是否重绘;第3行读当前模式到 prev;第4-5行相同则跳过;第6行写入新适配模式属性;第7行判断属性确实变更且元素数大于0;第8行触发自动尺寸处理;末尾的 AutoSizeMode 只读函数直接取属性强转枚举返回。 CreateNewGObject 则是面板里生新图元的入口,参数表暴露了定位与样式维度:类型、序号、名字、x/y 坐标、w/h 尺寸、colour 颜色、opacity 透明度(uchar 0-255)、movable 是否可拖拽。外汇与贵金属图表上挂这类自绘面板属高风险操作,参数错配可能导致对象不显示或遮挡报价区。 实盘前建议在 MT5 策略测试器里先用 EURUSD 1 分钟数据跑一遍空面板,确认 ElementsTotal 从 0 到 1 时 AutoSizeProcess 不会被误触发——这是很多自定义面板在加载时闪退的隐藏点。

MQL5 / C++
class="type">void SetAutoSizeMode(class="kw">const ENUM_CANV_ELEMENT_AUTO_SIZE_MODE mode,class="kw">const class="type">bool redraw)
  {
   ENUM_CANV_ELEMENT_AUTO_SIZE_MODE prev=this.AutoSizeMode();
   if(prev==mode)
      class="kw">return;
   this.SetProperty(CANV_ELEMENT_PROP_AUTOSIZE_MODE,mode);
   if(prev!=this.AutoSizeMode() && this.ElementsTotal()>class="num">0)
      this.AutoSizeProcess(redraw);
  }
ENUM_CANV_ELEMENT_AUTO_SIZE_MODE AutoSizeMode(class="type">void) class="kw">const { class="kw">return (ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)this.GetProperty(CANV_ELEMENT_PROP_AUTOSIZE_MODE); }

CGCnvElement *CPanel::CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type,
                     class="kw">const class="type">int obj_num,
                     class="kw">const class="type">class="kw">string obj_name,
                     class="kw">const class="type">int x,
                     class="kw">const class="type">int y,
                     class="kw">const class="type">int w,
                     class="kw">const class="type">int h,
                     class="kw">const class="type">class="kw">color colour,
                     class="kw">const class="type">uchar opacity,
                     class="kw">const class="type">bool movable,

「面板里按类型生成图形元素的工厂方法」

在自定义面板类里,新建图形元素通常不是直接 new 具体类,而是走一个工厂函数,根据传入的 ENUM_GRAPH_ELEMENT_TYPE 决定实例化 CGCnvElement、CForm、CPanel 还是 CLabel。下面这段实现里,GRAPH_ELEMENT_TYPE_WF_LABEL 分支就会生成 CLabel 并落到当前图表子窗口的坐标系中。 [CODE]const bool activity) { string name=this.CreateNameDependentObject(obj_name); CGCnvElement *element=NULL; switch(type) { case GRAPH_ELEMENT_TYPE_ELEMENT : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity); break; case GRAPH_ELEMENT_TYPE_FORM : element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_PANEL : element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_LABEL : element=new CLabel(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; default: break; } if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name); return element; }[/CODE] 逐行拆解:函数先通过 CreateNameDependentObject 用传入的 obj_name 拼出唯一对象名,避免同图表多实例重名;element 初值 NULL,若后续 new 失败就保持 NULL 并返回。switch 里 GRAPH_ELEMENT_TYPE_ELEMENT 会带全套坐标色透明度和 movable 参数建基类元素,FORM 和 WF_PANEL 只吃图表ID、子窗口、名字和 xywh,WF_LABEL 同理建标签。任何分支 new 返回 NULL 时,用 ::Print 打出函数名加失败提示和对象名,方便在 MT5 专家日志里定位。 CPanel::CreateNewElement 则是另一层封装,参数表里 x/y/w/h 是 int,opacity 是 uchar(0–255 区间),redraw 控制是否立刻重绘。你在写外汇或贵金属图表悬浮面板时,若标签不显示,先查这个工厂返回的是不是 NULL,再核对 ChartID 和 SubWindow 是否和主面板一致。外汇贵金属行情跳空频繁,这类 GUI 对象在高波动时可能来不及 redraw 而残留旧位置,属于高风险环境下的常见现象。

MQL5 / C++
class="kw">const class="type">bool activity)
  {
  class="type">class="kw">string name=this.CreateNameDependentObject(obj_name);
  CGCnvElement *element=NULL;
  class="kw">switch(type)
    {
    case GRAPH_ELEMENT_TYPE_ELEMENT :
      element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity);
      class="kw">break;
    case GRAPH_ELEMENT_TYPE_FORM :
      element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
    case GRAPH_ELEMENT_TYPE_WF_PANEL :
      element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
    case GRAPH_ELEMENT_TYPE_WF_LABEL :
      element=new CLabel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
    class="kw">default:
      class="kw">break;
    }
  if(element==NULL)
    ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
  class="kw">return element;
  }

面板里塞控件时的类型校验与取对象指针

在 MT5 自定义图形面板里动态挂子控件时,第一道关是类型下限检查:若传入的 element_type 小于 GRAPH_ELEMENT_TYPE_WF_BASE,直接写日志并返回 false,避免把非 WinForms 基类的东西强塞进面板。 真正建对象走 CForm::CreateAndAddNewElement,拿到指针若为 NULL 同样返回 false;随后按类型补样式——面板类把边框色设成背景色,文本标签则按传入 colour 或面板前景色定字色,边框回退到背景或字色。 开了 AutoSize 且 ElementsTotal()>0 时,会触发 AutoSizeProcess 重排;最后 Redraw 并返回 true。实测只要绑了 1 个以上子对象,自动缩放就会介入,界面抖动概率随子项增多而上升。 想反查某类子对象,GetListWinFormsObjByType 用 CSelect::ByGraphCanvElementProperty 按 CANV_ELEMENT_PROP_TYPE 过滤;GetWinFormsObj 则先拿列表再 At(index),列表空就返 NULL,调用前最好自己判空。

MQL5 / C++
class=class="str">"cmt">//--- If the object type is less than the base WinForms object
   if(element_type<GRAPH_ELEMENT_TYPE_WF_BASE)
     {
       class=class="str">"cmt">//--- report the error and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
       CMessage::ToLog(DFUN,MSG_PANEL_OBJECT_ERR_OBJ_MUST_BE_WFBASE);
       class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- If failed to create a new graphical element, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
   CWinFormBase *obj=CForm::CreateAndAddNewElement(element_type,main,x,y,w,h,colour,opacity,activity);
   if(obj==NULL)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Set the text class="type">class="kw">color of the created object as that of the base panel
   obj.SetForeColor(this.ForeColor());
class=class="str">"cmt">//--- If the object type is a panel
   if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_PANEL)
     {
       class=class="str">"cmt">//--- set the frame class="type">class="kw">color equal to the background class="type">class="kw">color 
       obj.SetColorFrame(obj.ColorBackground());
     }
class=class="str">"cmt">//--- If the object type is a text label,
   if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_LABEL)
     {
       class=class="str">"cmt">//--- set the object text class="type">class="kw">color depending on the one passed to the method
       class=class="str">"cmt">//--- or the panel text class="type">class="kw">color or the one passed to the method and the frame class="type">class="kw">color equal to the text class="type">class="kw">color 
       obj.SetForeColor(colour==clrNONE ? this.ForeColor() : colour);
       obj.SetColorFrame(main!=NULL ? main.ColorBackground() : obj.ForeColor());
     }
class=class="str">"cmt">//--- If the panel has auto resize enabled and features bound objects, call the resize method
   if(this.AutoSize() && this.ElementsTotal()>class="num">0)
      this.AutoSizeProcess(redraw);
class=class="str">"cmt">//--- Redraw the panel and all added objects, and class="kw">return &class="macro">#x27;true&class="macro">#x27;
   this.Redraw(redraw);
   class="kw">return true;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the list of bound objects                                |
class=class="str">"cmt">//| with the specified WinForms object type                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
CArrayObj *CPanel::GetListWinFormsObjByType(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   class="kw">return CSelect::ByGraphCanvElementProperty(this.GetListElements(),CANV_ELEMENT_PROP_TYPE,type,EQUAL);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the pointer to the specified WinForms object             |
class=class="str">"cmt">//| with the specified type by index                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWinFormBase *CPanel::GetWinFormsObj(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type,class="kw">const class="type">int index)
  {
   CArrayObj *list=this.GetListWinFormsObjByType(type);
   class="kw">return(list!=NULL ? list.At(index) : NULL);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 面板文本标签的实盘挂载验证

把上一篇文章里的 EA 存到 \MQL5\Experts\TestDoEasy\Part107\ 并改名 TstDE107.mq5 后,第一道坑是图形对象名超长。CCanvas 的 Create() 用「图表ID + 处理器跳价次数 + 伪随机数」拼资源名,嵌套层级一多,名字很容易突破 63 字符上限,直接报资源创建错误。现在的权宜之计是先砍短程序名本身。 主面板上挂了 6 个面板对象,每个面板内再建 1 个文本标签,尺寸取面板大小减两侧各 2 点。EA 输入项里暴露了标签边框类型和文本对齐方式,跑起来就能肉眼核对文字在框内的落点。 OnInit() 里先给标签绑定的面板设文本背景色,标签会继承这个色——这正是要测的继承逻辑,之后才在标签自身上改色。OnTick() 按列表奇偶索引分流:偶数写 Bid、奇数写 Ask,价格随报价刷新实时更。 编译挂图后能看到文字位置正确、边框显隐可控,但对象构建和重绘时有明显闪烁。这种视觉不适不是逻辑 bug,后面做显示优化时再消。外汇与贵金属品种报价跳动快,此类面板刷新可能引发渲染抖动,实盘前建议在模拟盘先跑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create dynamic resource                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CCanvas::Create(class="kw">const class="type">class="kw">string name,class="kw">const class="type">int width,class="kw">const class="type">int height,ENUM_COLOR_FORMAT clrfmt)
  {
   Destroy();
class=class="str">"cmt">//--- prepare data array
   if(width>class="num">0 && height>class="num">0 && ArrayResize(m_pixels,width*height)>class="num">0)
     {
      class=class="str">"cmt">//--- generate resource name
      m_rcname="::"+name+(class="type">class="kw">string)ChartID()+(class="type">class="kw">string)(GetTickCount()+MathRand());
      class=class="str">"cmt">//--- initialize data with zeros
      ArrayInitialize(m_pixels,class="num">0);
      class=class="str">"cmt">//--- create dynamic resource
      if(ResourceCreate(m_rcname,m_pixels,width,height,class="num">0,class="num">0,class="num">0,clrfmt))
        {
         class=class="str">"cmt">//--- successfully created
         class=class="str">"cmt">//--- complete initialization
         m_width =width;
         m_height=height;
         m_format=clrfmt;
         class=class="str">"cmt">//--- succeed
         class="kw">return(true);
        }
     }
class=class="str">"cmt">//--- error - destroy object
   Destroy();
   class="kw">return(class="kw">false);
  }

「图形控件的键盘映射与边框枚举」

在 MT5 自定义图形面板里,用宏把按键码固化下来能省掉一堆魔法数字。下面这组定义把 A/D/W/X 映射成左移、右移、上移、下移,S 切填充,Z 回默认,Q 按索引切换,对应虚拟键码分别是 65、68、87、88、83、90、81。 画布元素自适应的两种模式用枚举收口:GROW 只撑大不缩,GROW_SHRINK 双向伸缩,底层挂的是 CANV_ELEMENT_AUTO_SIZE_MODE 那组常量。 边框样式在编译语言分支下有两套文案,但枚举值完全对齐 FRAME_STYLE_NONE / SIMPLE / FLAT / BEVEL / STAMP。BEVEL 是凸浮雕,STAMP 是凹浮雕,英文版叫 Embossed (bevel) 和 Embossed (stamp),俄文版标 convex / concave,渲染层不受影响。 开 MT5 把这段贴进 include 头文件,改 KEY_LEFT 的 65 为别的码就能重绑快捷键,验证面板响应是否跟着变。外汇与贵金属图表加载此类 EA 控件仍属高风险操作,参数误绑可能让图形错位。

MQL5 / C++
class="macro">#define  FORMS_TOTAL(class="num">3)   class=class="str">"cmt">// Number of created forms
class="macro">#define  START_X(class="num">4)   class=class="str">"cmt">// Initial X coordinate of the shape
class="macro">#define  START_Y(class="num">4)   class=class="str">"cmt">// Initial Y coordinate of the shape
class="macro">#define  KEY_LEFT(class="num">65)  class=class="str">"cmt">// (A) Left
class="macro">#define  KEY_RIGHT(class="num">68)  class=class="str">"cmt">// (D) Right
class="macro">#define  KEY_UP(class="num">87)  class=class="str">"cmt">// (W) Up
class="macro">#define  KEY_DOWN(class="num">88)  class=class="str">"cmt">// (X) Down
class="macro">#define  KEY_FILL(class="num">83)  class=class="str">"cmt">// (S) Filling
class="macro">#define  KEY_ORIGIN(class="num">90)  class=class="str">"cmt">// (Z) Default
class="macro">#define  KEY_INDEX(class="num">81)  class=class="str">"cmt">// (Q) By index
class=class="str">"cmt">//--- enumerations by compilation language
class="macro">#ifdef COMPILE_EN
enum ENUM_AUTO_SIZE_MODE
  {
   AUTO_SIZE_MODE_GROW=CANV_ELEMENT_AUTO_SIZE_MODE_GROW,                     class=class="str">"cmt">// Grow
   AUTO_SIZE_MODE_GROW_SHRINK=CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK   class=class="str">"cmt">// Grow and Shrink
  };
enum ENUM_BORDER_STYLE
  {
   BORDER_STYLE_NONE=FRAME_STYLE_NONE,                                            class=class="str">"cmt">// None
   BORDER_STYLE_SIMPLE=FRAME_STYLE_SIMPLE,                                        class=class="str">"cmt">// Simple
   BORDER_STYLE_FLAT=FRAME_STYLE_FLAT,                                            class=class="str">"cmt">// Flat
   BORDER_STYLE_BEVEL=FRAME_STYLE_BEVEL,                                          class=class="str">"cmt">// Embossed(bevel)
   BORDER_STYLE_STAMP=FRAME_STYLE_STAMP,                                          class=class="str">"cmt">// Embossed(stamp)
  };
class="macro">#else
enum ENUM_AUTO_SIZE_MODE
  {
   AUTO_SIZE_MODE_GROW=CANV_ELEMENT_AUTO_SIZE_MODE_GROW,                     class=class="str">"cmt">// Increase only
   AUTO_SIZE_MODE_GROW_SHRINK=CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK   class=class="str">"cmt">// Increase and decrease
  };
enum ENUM_BORDER_STYLE
  {
   BORDER_STYLE_NONE=FRAME_STYLE_NONE,                                            class=class="str">"cmt">// No frame
   BORDER_STYLE_SIMPLE=FRAME_STYLE_SIMPLE,                                        class=class="str">"cmt">// Simple frame
   BORDER_STYLE_FLAT=FRAME_STYLE_FLAT,                                            class=class="str">"cmt">// Flat frame
   BORDER_STYLE_BEVEL=FRAME_STYLE_BEVEL,                                          class=class="str">"cmt">// Embossed(convex)
   BORDER_STYLE_STAMP=FRAME_STYLE_STAMP,                                          class=class="str">"cmt">// Embossed(concave)
  };
class="macro">#endif
class=class="str">"cmt">//--- class="kw">input parameters

用 WinForms 面板搭可拖拽控件矩阵

在 MT5 里做可视化面板,CEngine 的 CreateWFPanel 比手撸 OBJ_RECTANGLE_LABEL 省事得多。下面这段初始化逻辑直接产出一个 230×150 的面板,坐标锁在图表左上 (50,50),渐变填充用了两个自定义 RGB:深青 C'26,100,128' 到亮青 C'35,133,169',透明度 200。 面板创建后先 SetPaddingAll(4) 留边距,再把输入参数 InpMovable、InpAutoSize、InpAutoSizeMode 灌进去——这意味着你在 EA 外部参数里改「是否可拖拽」不用动代码,重载即可生效。 核心循环是 6 个子面板的绑定排布:前 3 个沿 X 轴贴着上一个的相对坐标,后 3 个则强制 xb+prev.Width()+20 做换行错位。这种写法让你复制去改 i<3 的阈值,就能把 2×3 矩阵变成 3×2 或 1×6,外汇和贵金属图表挂多周期监控时挺实用,但这类自定义面板在极端行情滑点时可能拖累刷新,属高风险环境下的辅助工具。

MQL5 / C++
sinput  class="type">bool               InpMovable       = true;             class=class="str">"cmt">// Movable forms flag
sinput  ENUM_INPUT_YES_NO   InpAutoSize      = INPUT_YES;          class=class="str">"cmt">// Autosize
sinput  ENUM_AUTO_SIZE_MODE InpAutoSizeMode  = AUTO_SIZE_MODE_GROW; class=class="str">"cmt">// Autosize mode
sinput  ENUM_BORDER_STYLE   InpFrameStyle    = BORDER_STYLE_NONE;  class=class="str">"cmt">// Label border style
sinput  ENUM_ANCHOR_POINT   InpTextAlign     = ANCHOR_LEFT_UPPER;  class=class="str">"cmt">// Label text align
class=class="str">"cmt">//--- global variables
CEngine      engine;
class="type">class="kw">color        array_clr[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Set EA global variables
   ArrayResize(array_clr,class="num">2);          class=class="str">"cmt">// Array of gradient filling colors
   array_clr[class="num">0]=C&class="macro">#x27;class="num">26,class="num">100,class="num">128&class="macro">#x27;;        class=class="str">"cmt">// Original ≈Dark-azure class="type">class="kw">color
   array_clr[class="num">1]=C&class="macro">#x27;class="num">35,class="num">133,class="num">169&class="macro">#x27;;        class=class="str">"cmt">// Lightened original class="type">class="kw">color
class=class="str">"cmt">//--- Create the array with the current symbol and set it to be used in the library
   class="type">class="kw">string array[class="num">1]={Symbol()};
   engine.SetUsedSymbols(array);
   class=class="str">"cmt">//--- Create the timeseries object for the current symbol and period, and show its description in the journal
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(class="kw">false); class=class="str">"cmt">// Short descriptions
class=class="str">"cmt">//--- Create WinForms Panel object
   CPanel *pnl=NULL;
   pnl=engine.CreateWFPanel("WFPanel",class="num">50,class="num">50,class="num">230,class="num">150,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">//--- Set Padding to class="num">4
     pnl.SetPaddingAll(class="num">4);
     class=class="str">"cmt">//--- Set the flags of relocation, auto resizing and auto changing mode from the inputs
     pnl.SetMovable(InpMovable);
     pnl.SetAutoSize(InpAutoSize,class="kw">false);
     pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,class="kw">false);
     class=class="str">"cmt">//--- In the loop, create class="num">6 bound panel objects
     for(class="type">int i=class="num">0;i<class="num">6;i++)
       {
       class=class="str">"cmt">//--- create the panel object with coordinates along the X axis in the center and class="num">10 along the Y axis, the width of class="num">80 and the height of class="num">50
       CPanel *prev=pnl.GetElement(i-class="num">1);
       class="type">int xb=class="num">0, yb=class="num">0;
       class="type">int x=(i<class="num">3 ? (prev==NULL ? xb : prev.CoordXRelative()) : xb+prev.Width()+class="num">20);

◍ 面板里塞价格标签的逐行门道

这段逻辑在 MT5 自定义图形库里批量生成带边框的面板,并在每个面板内嵌一个文本标签显示报价。循环变量 i 控制索引,前 3 个与第 4 个的纵向坐标 y 走特殊分支,其余统一相对上一个面板底边再下移 16 像素,避免重叠。 int y=(i<3 ? (prev==NULL ? yb : prev.BottomEdgeRelative()+16) : (i==3 ? yb : prev.BottomEdgeRelative()+16)); if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_PANEL,pnl,x,y,90,40,C'0xCD,0xDA,0xD7',200,true,false)) { CPanel *obj=pnl.GetElement(i); if(obj==NULL) continue; obj.SetFrameWidthAll(3); obj.SetBorderStyle(FRAME_STYLE_BEVEL); obj.SetColorBackground(obj.ChangeColorLightness(obj.ColorBackground(),4*i)); obj.SetForeColor(clrRed); //--- Calculate the width and height of the future text label object int w=obj.Width()-obj.FrameWidthLeft()-obj.FrameWidthRight()-4; int h=obj.Height()-obj.FrameWidthTop()-obj.FrameWidthBottom()-4; //--- Create a text label object obj.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_LABEL,pnl,2,2,w,h,clrNONE,255,false,false); //--- Get the pointer to a newly created object CLabel *lbl=obj.GetElement(0); if(lbl!=NULL) { //--- If the object has an even or zero index in the list, set the default text color for it if(i % 2==0) lbl.SetForeColor(CLR_DEF_FORE_COLOR); //--- If the object index in the list is odd, set the object opacity to 127 else lbl.SetForeColorOpacity(127); //--- Set the font Black width type and //--- specify the text alignment from the EA settings lbl.SetFontBoldType(FW_TYPE_BLACK); lbl.SetTextAlign(InpTextAlign); //--- For an object with an even or zero index, specify the Bid price for the text, otherwise - the Ask price of the symbol lbl.SetText(GetPrice(i % 2==0 ? SYMBOL_BID : SYMBOL_ASK)); //--- Set the frame width and type for a text label and update the modified object lbl.SetFrameWidthAll(1); lbl.SetBorderStyle((ENUM_FRAME_STYLE)InpFrameStyle); lbl.Update(true); } 面板底色按 4*i 做亮度偏移,i 越大越亮;前景色被硬设为红色,但内嵌标签的前景色走另一套规则:偶数索引用默认色,奇数索引透明度压到 127。 标签文字内容由 i 的奇偶决定——偶数为 SYMBOL_BID、奇数为 SYMBOL_ASK,也就是说相邻面板会交替显示买卖价。外汇与贵金属波动剧烈、杠杆风险高,这类面板只作行情参考,不构成方向判断。 直接把上面代码贴进你的 EA 图形模块,改 InpTextAlign 和 InpFrameStyle 两个外部输入,就能在 MT5 上看到买卖价交替浮窗的实际效果。

MQL5 / C++
class="type">int y=(i<class="num">3 ? (prev==NULL ? yb : prev.BottomEdgeRelative()+class="num">16) : (i==class="num">3 ? yb : prev.BottomEdgeRelative()+class="num">16));
if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_PANEL,pnl,x,y,class="num">90,class="num">40,C&class="macro">#x27;0xCD,0xDA,0xD7&class="macro">#x27;,class="num">200,true,class="kw">false))
  {
  CPanel *obj=pnl.GetElement(i);
  if(obj==NULL)
    class="kw">continue;
  obj.SetFrameWidthAll(class="num">3);
  obj.SetBorderStyle(FRAME_STYLE_BEVEL);
  obj.SetColorBackground(obj.ChangeColorLightness(obj.ColorBackground(),class="num">4*i));
  obj.SetForeColor(clrRed);
  class=class="str">"cmt">//--- Calculate the width and height of the future text label object
  class="type">int w=obj.Width()-obj.FrameWidthLeft()-obj.FrameWidthRight()-class="num">4;
  class="type">int h=obj.Height()-obj.FrameWidthTop()-obj.FrameWidthBottom()-class="num">4;
  class=class="str">"cmt">//--- Create a text label object
  obj.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_LABEL,pnl,class="num">2,class="num">2,w,h,clrNONE,class="num">255,class="kw">false,class="kw">false);
  class=class="str">"cmt">//--- Get the pointer to a newly created object
  CLabel *lbl=obj.GetElement(class="num">0);
  if(lbl!=NULL)
    {
    class=class="str">"cmt">//--- If the object has an even or zero index in the list, set the class="kw">default text class="type">class="kw">color for it
    if(i % class="num">2==class="num">0)
      lbl.SetForeColor(CLR_DEF_FORE_COLOR);
    class=class="str">"cmt">//--- If the object index in the list is odd, set the object opacity to class="num">127
    else
      lbl.SetForeColorOpacity(class="num">127);
    class=class="str">"cmt">//--- Set the font Black width type and
    class=class="str">"cmt">//--- specify the text alignment from the EA settings
    lbl.SetFontBoldType(FW_TYPE_BLACK);
    lbl.SetTextAlign(InpTextAlign);
    class=class="str">"cmt">//--- For an object with an even or zero index, specify the Bid price for the text, otherwise - the Ask price of the symbol 
    lbl.SetText(GetPrice(i % class="num">2==class="num">0 ? SYMBOL_BID : SYMBOL_ASK));
    class=class="str">"cmt">//--- Set the frame width and type for a text label and update the modified object
    lbl.SetFrameWidthAll(class="num">1);
    lbl.SetBorderStyle((ENUM_FRAME_STYLE)InpFrameStyle);
    lbl.Update(true);
    }

「在 OnTick 里刷新面板报价标签」

EA 的 OnTick 是每笔行情 tick 触发的入口,这里用 engine.GetWFPanel("WFPanel") 按名字拿回面板指针,拿不到就直接跳过,避免空指针崩在实时行情上。 拿到面板后,用 GetListWinFormsObjByType(GRAPH_ELEMENT_TYPE_WF_PANEL) 取出所有绑定的子面板,再跑 for 循环逐个处理。循环里对每个子面板取序号 0 的 CLabel 文本对象,通过 i % 2 == 0 的奇偶判断交替写入 SYMBOL_BID 或 SYMBOL_ASK,实现相邻标签分别显示买卖价。 GetPrice 函数封装了 SymbolInfoDouble 取价和 DoubleToString 按当前品种精度 Digits() 格式化,返回如 "Bid: 1.08452" 这类字符串。外汇与贵金属杠杆高、点差跳变频繁,这类实时标签只作盯盘参考,不代表任何方向确定性。 标红那行 lbl.SetText(GetPrice(i % 2 == 0 ? SYMBOL_BID : SYMBOL_ASK)) 是核心:把奇偶映射成买卖价,lbl.Redraw(false) 仅重绘该标签而非整面板,能降低 MT5 主图对象的重绘开销。

MQL5 / C++
   }
      }
      class=class="str">"cmt">//--- Redraw all objects according to their hierarchy
      pnl.Redraw(true);
   }
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
  class=class="str">"cmt">//--- Get the pointer to the panel object by name
  CPanel *pnl=engine.GetWFPanel("WFPanel");
  if(pnl!=NULL)
    {
      class=class="str">"cmt">//--- Get the list of all bound panel objects
      CArrayObj *list=pnl.GetListWinFormsObjByType(GRAPH_ELEMENT_TYPE_WF_PANEL);
      class=class="str">"cmt">//--- In the loop by bound panel objects,
      for(class="type">int i=class="num">0;i<list.Total();i++)
        {
          class=class="str">"cmt">//--- get the pointer to the next panel object
          CPanel *obj=pnl.GetWinFormsObj(GRAPH_ELEMENT_TYPE_WF_PANEL,i);
          if(obj!=NULL)
            {
              class=class="str">"cmt">//--- take the pointer to the first(and only) text label object from the received object
              CLabel *lbl=obj.GetWinFormsObj(GRAPH_ELEMENT_TYPE_WF_LABEL,class="num">0);
              if(lbl!=NULL)
                {
                  class=class="str">"cmt">//--- set the new text for the object and redraw it
                  lbl.SetText(GetPrice(i % class="num">2==class="num">0 ? SYMBOL_BID : SYMBOL_ASK));
                  lbl.Redraw(class="kw">false);
                }
            }
        }
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return Bid/Ask class="type">class="kw">string value                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string GetPrice(class="kw">const ENUM_SYMBOL_INFO_DOUBLE price)
  {
  class="kw">return((price==SYMBOL_ASK ? "Ask: " : "Bid: ")+DoubleToString(SymbolInfoDouble(Symbol(),price),Digits()));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

继续扩展 WinForms 对象的方向

这一阶段的控件封装先告一段落,当前发布的 MQL5 函数库版本、测试 EA 与图表事件控制指标已打包为 MQL5.zip(约 4357.39 KB),可直接在 MT5 里加载验证面板驻靠与自动尺寸逻辑。 下一篇会接着写 WinForms 对象的进一步开发,重点是把已实现的 CPanel 基类往更复杂的交互控件上推。 若你在跑这套库时发现 Dock 或 AutoSize 行为异常,建议先用文章里的测试 EA 在策略测试器复现,再把现象带到评论区,比空谈架构更容易定位问题。

让小布替你跑这套
这些控件结构小布盯盘的 AIGC 已内置,打开对应品种页即可看到现成的标签渲染,不必自己重写枚举。

常见问题

在类里读取所用字体度量,按文本宽高重算对象矩形,再重绘容器,不写死宽高即可自适应。
沿标签边界画明暗两条矩形线模拟立体边,参数里切换平面或三维模式,由属性枚举控制。
可以,小布内置的 AIGC 渲染层支持同类标签控件,品种页里能看到自适应文本与边框效果。
只是扩展枚举总量,旧索引不变,已写逻辑不受影响,新属性按需取用即可。
可叠多层标签做遮挡过渡,或按预警级别调灰显隐,不必删对象就能控可见度。