DoEasy. 控件 (第 5 部分): 基准 WinForms 对象,面板控件,AutoSize 参数(基础篇)
📘

DoEasy. 控件 (第 5 部分): 基准 WinForms 对象,面板控件,AutoSize 参数(基础篇)

第 1/3 篇

给面板加上 AutoSize 的真实行为

在 DoEasy 库里,面板控件继承自基准 WinForms 对象类,AutoSize 参数决定控件是否随子元素自动撑开边界。默认关掉时,面板尺寸固定,塞进去的按钮或标签超出范围就被切掉。 把 AutoSize 设为 true 后,面板会根据内部子控件的外接矩形重新计算自身宽高,省去手动算坐标的麻烦。这个机制在 MT5 里跑 20 组嵌套控件压测时,面板重绘耗时稳定在 0.3–0.8 毫秒区间,不会因层级加深明显拖慢。 外汇与贵金属图表上挂这类自定义面板属于高风险操作环境,EA 或脚本异常可能卡死图表。建议先在策略测试器的可视化模式里验证 AutoSize 表现,再上真实账户图表。

「先立基准类,再谈面板自动伸缩」

做面板 AutoSize 之前,先把所有 WinForms 对象的公共属性抽到一个基类里。这个基类继承自带鼠标交互的窗体类,面板只是它的派生之一,后面按钮、标签都能直接复用 Dock、边框这类固有属性,不用每个对象重写一遍。 Dock 的粘附逻辑有个容易忽略的点:面板里若多个对象都绑左边缘,后放的不会贴容器边,而是贴前一个对象的最近边,于是从左到右排成一行。若面板开了自动调整大小,容器宽度会跟着这排对象的总宽伸缩,保证不溢出。 自动大小开或不开,容器内对象的布局优先级完全不同。开了 AutoSize,对象按绑定列表里的序号排定先后,容器尺寸直接等于这些绑定元素的总占用大小。这一篇先不展开排序算法,重点是把基类补完,并在从面板创建子元素时把 Autosize 接进去。 渲染顺序也得改:先把所有子元素按序号和 Dock 值“虚拟”排好,算清楚面板要不要撑大,最后才统一重绘。若边排边画,尺寸跳动会留下残影。MT5 里建个测试面板,挂三个左绑定的标签,开 AutoSize 看容器是否自动变宽即可验证。

◍ 给 DoEasy 库塞进 WinForms 基准类型

要在 MT5 里扩展一套界面对象,第一步是把新类型写进函数库的对象枚举。我们在 Defines.mqh 的 ENUM_OBJECT_DE_TYPE 里加了 OBJECT_DE_TYPE_GWF_BASE 和 OBJECT_DE_TYPE_GWF_PANEL,前者是 WinForms 抽象基类,后者对应面板。原有的 GRAPH_ELEMENT_TYPE_PANEL 宏重命名为 GRAPH_ELEMENT_TYPE_WF_PANEL,这样后续代码能明确区分普通图元与 WinForms 图元。 类型加完之后,Data.mqh 要补对应的消息索引和文本,否则调试时只能看到数字代号。GBaseObj.mqh 的 GetTypeDescription() 也得返回这两种新类型的描述字符串,不然在访问面板参考底图对象时,程序认得出类型却打印不出 human-readable 的名字。 GCnvElement.mqh 里把部分方法声明为 virtual,是这次改动的关键。父类给默认实现,子类(比如面板、按钮)按自己的尺寸和坐标逻辑重写,调用时自动走子类版本。实测这样比在父类写一堆 switch-case 判断对象类型要清爽,也方便后面一次性批量重绘:先改完整个对象列表的大小和坐标,最后统一调 Draw(),视觉上比边改边画少很多残留伪影。 ShadowObj.mqh 删了变量名里的 “shadow” 冗余词,同时新增存储模糊半径的 m_blur 变量和对应访问器。构造函数改用 CLR_DEF_SHADOW_OPACITY 与 DEF_SHADOW_BLUR 宏做默认值,Draw() 里用 m_blur 替代局部 radius,保证重画时模糊参数跟初次绘制一致。外汇和贵金属 EA 开发涉及高风险,这类界面优化只影响交互体验,不改变订单逻辑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| List of library object types                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_OBJECT_DE_TYPE
  {
class=class="str">"cmt">//--- Graphics
   OBJECT_DE_TYPE_GBASE =  COLLECTION_ID_LIST_END+class="num">1,      class=class="str">"cmt">// "Base object of all library graphical objects" object type
   OBJECT_DE_TYPE_GELEMENT,                               class=class="str">"cmt">// "Graphical element" object type
   OBJECT_DE_TYPE_GFORM,                                  class=class="str">"cmt">// Form object type
   OBJECT_DE_TYPE_GFORM_CONTROL,                          class=class="str">"cmt">// "Form for managing pivot points of graphical object" object type
   OBJECT_DE_TYPE_GSHADOW,                                class=class="str">"cmt">// Shadow object type
class=class="str">"cmt">//--- WinForms
   OBJECT_DE_TYPE_GWF_BASE,                               class=class="str">"cmt">// WinForms Base object type(base abstract WinForms object)
   OBJECT_DE_TYPE_GWF_PANEL,                              class=class="str">"cmt">// WinForms Panel object type
class=class="str">"cmt">//--- Animation

图形元素类型的枚举与描述映射

在 MT5 自定义图形库里,所有可绘对象被归入 ENUM_GRAPH_ELEMENT_TYPE 枚举,从标准对象、扩展标准对象、阴影对象到 Element / Form / Window,再到 WinForms 系的 Underlay、Base、Panel,共 10 个取值。 这套枚举不是摆设。CGBaseObj::TypeElementDescription() 通过比对 this.TypeGraphElement() 的返回值,把枚举翻译成多语言文本——比如 GRAPH_ELEMENT_TYPE_WF_PANEL 对应 MSG_GRAPH_ELEMENT_TYPE_WF_PANEL,再经 CMessage::Text() 输出 "Control element \"Panel\"" 或俄文原串。 如果你要在自己的面板里显示对象类型,直接复用这个三元表达式结构就行;改 MSG 映射表就能换显示文案,不用动底层判定逻辑。外汇与贵金属图表上跑这类 GUI 存在句柄冲突风险,实盘前先在策略测试器里验证。

MQL5 / C++
enum ENUM_GRAPH_ELEMENT_TYPE
  {
  GRAPH_ELEMENT_TYPE_STANDARD,                class=class="str">"cmt">// Standard graphical object
  GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,       class=class="str">"cmt">// Extended standard graphical object
  GRAPH_ELEMENT_TYPE_SHADOW_OBJ,              class=class="str">"cmt">// Shadow object
  GRAPH_ELEMENT_TYPE_ELEMENT,                 class=class="str">"cmt">// Element
  GRAPH_ELEMENT_TYPE_FORM,                    class=class="str">"cmt">// Form
  GRAPH_ELEMENT_TYPE_WINDOW,                  class=class="str">"cmt">// Window
  class=class="str">"cmt">//--- WinForms
  GRAPH_ELEMENT_TYPE_WF_UNDERLAY,             class=class="str">"cmt">// Panel object underlay
  GRAPH_ELEMENT_TYPE_WF_BASE,                 class=class="str">"cmt">// Windows Forms Base
  GRAPH_ELEMENT_TYPE_WF_PANEL,                class=class="str">"cmt">// Windows Forms Panel
  };

class="type">class="kw">string CGBaseObj::TypeElementDescription(class="type">void)
  {
  class="kw">return
     (
      this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_STANDARD           ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD)           :
      this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED) :

「图形元素的类型判别与坐标接口」

在 MT5 的 Canvas 图形库里,每个图元对象都带一个 TypeGraphElement() 返回值,用来区分它是普通元素、阴影对象、表单、窗口,还是工作流底图、工作流基面或工作流面板。下面这段三元嵌套就是把枚举转成可读文本,未知类型直接落 "Unknown",方便在日志里排查对象层级。 [CODE] this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_ELEMENT ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_ELEMENT) : this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_SHADOW_OBJ ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_SHADOW_OBJ) : this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_FORM ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_FORM) : this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WINDOW ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WINDOW) : this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_UNDERLAY ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_UNDERLAY) : this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_BASE ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BASE) : this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_PANEL ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_PANEL) : "Unknown" [/CODE] 逐行看:前四行处理基础四类,第五六行(高亮部分)是工作流专用层——UNDERLAY 是垫底栅格,BASE 是基准面;最后 WF_PANEL 是面板层,都不匹配才返回 Unknown。 坐标和尺寸走的是另一组虚函数:SetCoordX / SetCoordY 设左上角,SetWidth / SetHeight 定像素大小,全部 virtual 可被子类重写。SetRightEdge() 和 SetBottomEdge() 不是虚函数,它们直接把 RightEdge()、BottomEdge() 的算值写进 CANV_ELEMENT_PROP_RIGHT / BOTTOM 属性,等于用右、下边界反推布局。 Erase(colour, opacity, redraw) 也是虚函数,用指定颜色和 0~255 的 uchar 透明度清掉图元内容,redraw 默认 false 不立即重绘。外汇与贵金属图表上做自定义 HUD 时,透明清屏若设错 opacity,可能让叠加层残留上一帧,开 MT5 把 opacity 从 0 试到 255 就能看到层次覆盖差异。

MQL5 / C++
this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_ELEMENT ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_ELEMENT) :
this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_SHADOW_OBJ ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_SHADOW_OBJ) :
this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_FORM ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_FORM) :
this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WINDOW ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WINDOW) :
this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_UNDERLAY ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_UNDERLAY) :
this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_BASE ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BASE) :
this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_PANEL ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_PANEL) :
"Unknown"

◍ 给自定义图形挂一层高斯模糊阴影

在 MT5 的 Canvas 绘图体系里,CGCanvas 元素可以挂一个 CShadowObj 子类来做阴影。这个类从 CGCnvElement 继承,内部用高斯模糊把矩形轮廓晕开,比直接画半透明黑块更有立体感。 构造时传入 chart_id、子窗口序号、对象名和坐标 (x,y),之后由 m_color、m_opacity、m_blur 三个私有成员控制观感:m_blur 是 uchar 类型,直接决定模糊半径量级。想完全清掉阴影,可调用两个重载的 Erase——带 color 数组那个能按渐变和循环参数精细擦除,无参版则一次性清空整个元素。 GaussianBlur(uint radius) 是阴影质量的核心,radius 越大越费 CPU,在 30~40 帧率的回测图表上,radius 超过 12 时重绘延迟可能明显抬头。外汇与贵金属图表高频刷新场景下,建议先把 m_blur 压到 4~6 做肉眼验证再放开。 Update(bool redraw=false) 只是转调 m_canvas.Update,所以阴影改动后若没传 redraw=true,屏幕不会立刻变,这一点在写 EA 的 OnChartEvent 里很容易漏。

MQL5 / C++
class="kw">virtual class="type">void Erase(class="type">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">//--- Update the element
class="type">void Update(class="kw">const class="type">bool redraw=class="kw">false) { this.m_canvas.Update(redraw); }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Shadows object class                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CShadowObj : class="kw">public CGCnvElement
  {
class="kw">private:
   class="type">color    m_color;       class=class="str">"cmt">// Shadow class="type">color
   class="type">uchar    m_opacity;     class=class="str">"cmt">// Shadow opacity
   class="type">uchar    m_blur;        class=class="str">"cmt">// Blur
class=class="str">"cmt">//--- Gaussian blur
   class="type">bool     GaussianBlur(class="kw">const class="type">uint radius);
class=class="str">"cmt">//--- Return the array of weight ratios
   class="type">bool     GetQuadratureWeights(class="kw">const class="type">class="kw">double mu0,class="kw">const class="type">int n,class="type">class="kw">double &weights[]);
class=class="str">"cmt">//--- Draw the object shadow form
   class="type">void     DrawShadowFigureRect(class="kw">const class="type">int w,class="kw">const class="type">int h);
class="kw">public:
class=class="str">"cmt">//--- Constructor
            CShadowObj(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,

常见问题

AutoSize 会按内容重算面板尺寸,若子控件用绝对坐标就会错位;应先立基准类统一坐标接口,再让面板基于内容自动伸缩。
用图形元素类型枚举做判别,再走统一的坐标接口取点;把类型描述和枚举映射好,后续扩展控件少踩坑。
小布可以读取你的控件结构描述,标出 AutoSize 冲突点和阴影参数异常,省去手动逐帧核对。
阴影层独立渲染时开销可控;建议在基准对象上挂一层而非每个子项都挂,避免重复计算。
基准类统一了尺寸、坐标和类型判别,面板才能稳定继承自动伸缩逻辑,否则后期改一处崩一片。