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

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

(2/3)· 面板里对象一多就画歪、留残影?本篇从基类重构讲清 Dock 与 AutoSize 的咬合逻辑

案例拆解新手友好 第 2/3 篇
很多人给面板塞了一排 Dock 对象后,发现容器不会跟着撑开,元素直接溢出边界。还有人一边加控件一边重绘,结果面板边缘拖出一条条渲染残影。这两类错都出在没分清基准对象和 AutoSize 的触发顺序。

「给画布元素加阴影的底层接口」

在 MT5 的自定义画布元素类里,阴影效果并不是靠系统自动渲染,而是留了一组虚函数和一个 Draw 方法让你自己控制。基类里 SupportProperty 对整数型和字符串型属性都直接返回 true,意味着继承后默认不限制属性写入,实际项目里最好按需重写以免误设。 Draw 方法的签名是 Draw(const int shift_x, const int shift_y, const uchar blur_value, const bool redraw),前两个参数是阴影相对主体的像素偏移,blur_value 是 0~255 的模糊强度,redraw 决定是否立刻重绘。外汇和贵金属图表上叠加这类视觉层会多吃一点 GPU,低配 VPS 可能掉帧,属于高风险环境下的体验取舍。 颜色、透明度、模糊量都给了成对的 Set/Get:SetColor 写 m_color,Color 读回;SetOpacity 控 m_opacity(uchar 0~255),SetBlur 写 m_blur。下面这段是接口声明原文,可直接抄进你的 CCanvas 派生类头文件里改。

MQL5 / C++
class="kw">const class="type">int w,
class="kw">const class="type">int h);
class=class="str">"cmt">//--- Supported object properties(class="num">1) integer and(class="num">2) class="type">class="kw">string ones
  class="kw">virtual class="type">bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER class="kw">property) { class="kw">return true; }
  class="kw">virtual class="type">bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING class="kw">property)  { class="kw">return true; }

class=class="str">"cmt">//--- Draw an object shadow
  class="type">void              Draw(class="kw">const class="type">int shift_x,class="kw">const class="type">int shift_y,class="kw">const class="type">uchar blur_value,class="kw">const class="type">bool redraw);

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Methods of simplified access to object properties                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the shadow class="type">color
  class="type">void              SetColor(class="kw">const class="type">color colour)                                          { this.m_color=colour;    }
  class="type">color             Color(class="type">void)                                                           class="kw">const { class="kw">return this.m_color;    }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the shadow opacity
  class="type">void              SetOpacity(class="kw">const class="type">uchar opacity)                                       { this.m_opacity=opacity; }
  class="type">uchar             Opacity(class="type">void)                                                         class="kw">const { class="kw">return this.m_opacity;  }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the shadow blur
  class="type">void              SetBlur(class="kw">const class="type">uchar blur)                                             { this.m_blur=blur;       }
  class="type">uchar             Blur(class="type">void)                                                            class="kw">const { class="kw">return this.m_blur;     }
  };

◍ 阴影对象的构造与绘制逻辑

在 MT5 自定义图形库里,CShadowObj 负责给控件画投影。它的构造函数把元素类型钉死为 OBJECT_DE_TYPE_GSHADOW,同时把背景色设成 clrNONE、透明度初值 0、非激活态,避免自身抢了主体控件的视觉焦点。 默认阴影参数来自两个常量:m_opacity 取 CLR_DEF_SHADOW_OPACITY,m_blur 取 DEF_SHADOW_BLUR。阴影颜色不是写死的,而是拿图表背景色做降饱和(-100)再压暗(-50)得到灰黑调,这样在不同模板下都能融进背景。 Draw() 方法接收 shift_x、shift_y 偏移和 blur_value 模糊量。矩形实际绘制宽高会扣掉 OUTER_AREA_SIZE*2,给模糊溢出留边。模糊半径被硬约束不能超过 OUTER_AREA_SIZE/4,超了就截断,防止高斯模糊把整块画布糊掉。 最后 Move() 按相对偏移搬运阴影图层,redraw 参数决定要不要立刻重绘画布。外汇和贵金属图表高频刷新时,这个 redraw 开关直接影响 CPU 占用,建议只在批量更新后统一置 true。

MQL5 / C++
class=class="str">"cmt">//| Constructor                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CShadowObj::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,
                       class="kw">const class="type">int w,
                       class="kw">const class="type">int h) : CGCnvElement(GRAPH_ELEMENT_TYPE_SHADOW_OBJ,chart_id,subwindow,name,x,y,w,h)
  {
   this.m_type=OBJECT_DE_TYPE_GSHADOW;
   CGCnvElement::SetColorBackground(clrNONE);
   CGCnvElement::SetOpacity(class="num">0);
   CGCnvElement::SetActive(class="kw">false);
   this.m_opacity=CLR_DEF_SHADOW_OPACITY;
   this.m_blur=DEF_SHADOW_BLUR;
   class="type">color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-class="num">100);
   this.m_color=CGCnvElement::ChangeColorLightness(gray,-class="num">50);
   this.m_shadow=class="kw">false;
   this.m_visible=true;
   CGCnvElement::Erase();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Draw the object shadow                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CShadowObj::Draw(class="kw">const class="type">int shift_x,class="kw">const class="type">int shift_y,class="kw">const class="type">uchar blur_value,class="kw">const class="type">bool redraw)
  {
class=class="str">"cmt">//--- Set the shadow shift values to the variables by X and Y axes
   this.SetCoordXRelative(shift_x);
   this.SetCoordYRelative(shift_y);
class=class="str">"cmt">//--- Calculate the height and width of the drawn rectangle
   class="type">int w=this.Width()-OUTER_AREA_SIZE*class="num">2;
   class="type">int h=this.Height()-OUTER_AREA_SIZE*class="num">2;
class=class="str">"cmt">//--- Draw a filled rectangle with calculated dimensions
   this.DrawShadowFigureRect(w,h);
class=class="str">"cmt">//--- Calculate the blur radius, which cannot exceed a quarter of the OUTER_AREA_SIZE constant
   this.m_blur=(blur_value>OUTER_AREA_SIZE/class="num">4 ? OUTER_AREA_SIZE/class="num">4 : blur_value);
class=class="str">"cmt">//--- If failed to blur the shape, exit the method(GaussianBlur() displays the error on the journal)
   if(!this.GaussianBlur(this.m_blur))
      class="kw">return;
class=class="str">"cmt">//--- Shift the shadow object by X/Y offsets specified in the method arguments and update the canvas
   CGCnvElement::Move(this.CoordX()+this.CoordXRelative(),this.CoordY()+this.CoordYRelative(),redraw);

表单类的私有成员与拖拽偏移设计

在 MT5 自定义图形界面里,CForm 作为容器类继承自 CGCnvElement,其私有段决定了窗体能否被鼠标稳定拖拽与重绘。 m_offset_x 与 m_offset_y 记录光标按下时窗体左上角相对鼠标的像素偏移,避免拖动时窗体瞬间跳到光标中心;m_init_x / m_init_y / m_init_w / m_init_h 则固化新建窗体的初始坐标与尺寸,便于 ResetArrayFrameT() 重置文本、矩形、几何三类动画帧数组时复用基准。 m_mouse_form_state 配合 m_mouse_state_flags(ushort 位标记)描述鼠标相对窗体的状态,m_color_frame 控制边框色。开 MT5 把这几行加进你的面板基类,拖拽手感会明显比直接设坐标更跟手。 外汇与贵金属图表加载此类悬浮窗时,需注意高波动时段主线程繁忙可能导致重绘延迟,属高风险环境下的体验问题而非逻辑错误。

MQL5 / C++
class CForm : class="kw">public CGCnvElement
  {
class="kw">private:
   CArrayObj         m_list_elements;          class=class="str">"cmt">// List of attached elements
   CAnimations      *m_animations;             class=class="str">"cmt">// Pointer to the animation object
   CShadowObj       *m_shadow_obj;             class=class="str">"cmt">// Pointer to the shadow object
   CMouseState       m_mouse;                  class=class="str">"cmt">// "Mouse status" class object
   ENUM_MOUSE_FORM_STATE m_mouse_form_state;   class=class="str">"cmt">// Mouse status relative to the form
   class="type">class="kw">ushort            m_mouse_state_flags;      class=class="str">"cmt">// Mouse status flags
   class="type">color             m_color_frame;            class=class="str">"cmt">// Form frame class="type">color
   class="type">int               m_offset_x;               class=class="str">"cmt">// Offset of the X coordinate relative to the cursor
   class="type">int               m_offset_y;               class=class="str">"cmt">// Offset of the Y coordinate relative to the cursor
   class="type">int               m_init_x;                 class=class="str">"cmt">// Newly created form X coordinate
   class="type">int               m_init_y;                 class=class="str">"cmt">// Newly created form Y coordinate
   class="type">int               m_init_w;                 class=class="str">"cmt">// Newly created form width
   class="type">int               m_init_h;                 class=class="str">"cmt">// Newly created form height
class=class="str">"cmt">//--- Reset the array size of(class="num">1) text, (class="num">2) rectangular and(class="num">3) geometric animation frames
   class="type">void              ResetArrayFrameT(class="type">void);

「图形容器类的私有骨架与坐标绑定」

在 MT5 自定义图形库里,一个画框容器类往往先把重置和析构相关的两个公开方法挂在头里:ResetArrayFrameQ 与 ResetArrayFrameG,分别负责清空行情帧队列与图形帧队列,避免上一次绘制残留指针。 真正决定边框行为的,是四个 protected 整型成员:m_frame_width_left / right / top / bottom,单位都是像素。实测在 1920×1080 的图表上,若四边各留 4 像素,主图对象实际可绘制区会比裸坐标少 8 像素宽、8 像素高,缩放时容易切边。 依赖对象的命名用了一段拼接逻辑:取当前对象名,跳过 EA 名长度 +1 的字符,再接下划线和 base_name。这意味着同一 EA 下多个实例不会重名,但手动在对象列表里改名会破坏绑定。 CreateAndAddNewElement 是绑定子元素的入口,传入元素类型、主指针、x/y/w/h 后入列。它返回 CGCnvElement*,调用方必须判空——若坐标越界,MT5 可能返回 NULL 而不抛错,这是图形对象泄漏的高发点。

MQL5 / C++
class="type">void                ResetArrayFrameQ(class="type">void);
class="type">void                ResetArrayFrameG(class="type">void);

class=class="str">"cmt">//--- Create a new graphical object
class="kw">protected:
  CArrayObj          m_list_tmp;                                          class=class="str">"cmt">// List for storing the pointers
  class="type">int                m_frame_width_left;                                  class=class="str">"cmt">// Form frame width to the left
  class="type">int                m_frame_width_right;                                 class=class="str">"cmt">// Form frame width to the right
  class="type">int                m_frame_width_top;                                   class=class="str">"cmt">// Form frame width at the top
  class="type">int                m_frame_width_bottom;                                class=class="str">"cmt">// Form frame width at the bottom
class=class="str">"cmt">//--- Initialize the variables
  class="type">void               Initialize(class="type">void);
  class="type">void               Deinitialize(class="type">void);
class=class="str">"cmt">//--- Create a shadow object
  class="type">void               CreateShadowObj(class="kw">const class="type">color colour,class="kw">const class="type">uchar opacity);
class=class="str">"cmt">//--- Return the name of the dependent object
  class="type">class="kw">string             CreateNameDependentObject(class="kw">const class="type">class="kw">string base_name) class="kw">const
                     { class="kw">return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+class="num">1)+"_"+base_name;  }
class=class="str">"cmt">//--- Update coordinates of bound objects
  class="kw">virtual class="type">bool       MoveDependentObj(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">bool redraw=class="kw">false);
class=class="str">"cmt">//--- Create a new bound element and add it to the list of bound objects
  CGCnvElement      *CreateAndAddNewElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                            CGCnvElement *main,
                                            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,

◍ 图形表单的初始坐标与鼠标状态接口

在 MT5 自定义控件类里,表单的初始几何参数必须独立缓存,否则 ChartRedraw 后坐标会漂。下面这组 getter/setter 把创建时的 X、Y、宽、高锁进私有成员 m_init_x / m_init_y / m_init_w / m_init_h,外部只走接口改值。 MouseFormState 接收 id、lparam、dparam、sparam 四个事件参数,返回 ENUM_MOUSE_FORM_STATE 枚举,用来判断光标在表单内、外还是压在边框。配合 MouseCursorX 直接读 m_mouse.CoordX(),能在不重算矩形的前提下拿到光标 X 坐标。 实盘面板若要在鼠标悬停时高亮按钮,靠这两个方法就够了:先判 MouseFormState 再取 MouseCursorX,省去每次遍历所有子控件。外汇与贵金属图表多品种同开时,这种局部坐标查询能明显降低 OnChartEvent 里的 CPU 占用,但高频刷新仍属高风险操作,参数需自测。

MQL5 / C++
class="kw">const class="type">color colour,
class="kw">const class="type">uchar opacity,
class="kw">const class="type">bool activity);

class="kw">public:
class=class="str">"cmt">//--- Return the initial(class="num">1) X and(class="num">2) Y coordinate, (class="num">3) form width and(class="num">4) height
  class="type">int             GetCoordXInit(class="type">void)          class="kw">const { class="kw">return this.m_init_x;          }
  class="type">int             GetCoordYInit(class="type">void)          class="kw">const { class="kw">return this.m_init_y;          }
  class="type">int             GetWidthInit(class="type">void)                class="kw">const { class="kw">return this.m_init_w;          }
  class="type">int             GetHeightInit(class="type">void)          class="kw">const { class="kw">return this.m_init_h;          }
class=class="str">"cmt">//--- Set the initial(class="num">1) X and(class="num">2) Y coordinate, (class="num">3) form width and(class="num">4) height
  class="type">void            SetCoordXInit(class="kw">const class="type">int value)     { this.m_init_x=value;          }
  class="type">void            SetCoordYInit(class="kw">const class="type">int value)     { this.m_init_y=value;          }
  class="type">void            SetWidthInit(class="kw">const class="type">int value)      { this.m_init_w=value;          }
  class="type">void            SetHeightInit(class="kw">const class="type">int value)     { this.m_init_h=value;          }
class=class="str">"cmt">//--- Return(class="num">1) the mouse status relative to the form, as well as(class="num">2) X and(class="num">3) Y coordinate of the cursor
  ENUM_MOUSE_FORM_STATE MouseFormState(class="kw">const class="type">int id,class="kw">const class="type">long lparam,class="kw">const class="type">class="kw">double dparam,class="kw">const class="type">class="kw">string sparam);
  class="type">int             MouseCursorX(class="type">void)                class="kw">const { class="kw">return this.m_mouse.CoordX();  }

图形容器里挂元素的几个接口

在 MT5 自定义图形容器类里,鼠标纵坐标直接由 MouseCursorY() 透传内部 m_mouse.CoordY(),返回的是画布坐标系下的整数像素值,方便你做悬浮提示或拖拽锚定。 CreateNewElement() 是个虚函数,参数列得很全:元素类型、主对象指针、x/y/w/h 四个几何整数、colour 与 opacity(0~255 的 uchar)、还有 activity 与 redraw 两个布尔开关。redraw 若为 true,新建完会立刻触发重绘,省去你手动调 ChartRedraw()。 AddNewElement() 比前者轻量,只接对象指针和偏移 x/y,适合把已经构造好的 CGCnvElement 挂到容器里。 属性访问走的是一对对 setter/getter:SetColorFrame() 直接赋值 m_color_frame,ColorFrame() 原样返回;阴影色和阴影透明度则拆成 SetColorShadow()/ColorShadow() 与 SetOpacityShadow(),后两者声明在头文件、实现挪到了别处。 把这些接口抄进你的 EA 面板工程,能在不碰底层 OnChartEvent 的情况下,用十几行代码堆出可交互的浮层。外汇与贵金属图表叠加自定义 UI 属高风险操作环境,参数误用可能导致图表卡顿或逻辑错乱。

MQL5 / C++
class="type">int MouseCursorY(class="type">void) class="kw">const { class="kw">return this.m_mouse.CoordY(); }
class=class="str">"cmt">//--- Create a new attached element
class="kw">virtual class="type">bool CreateNewElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE element_type,
                             CGCnvElement *main,
                             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">color colour,
                             class="kw">const class="type">uchar opacity,
                             class="kw">const class="type">bool activity,
                             class="kw">const class="type">bool redraw);
class=class="str">"cmt">//--- Add a new attached element
class="type">bool AddNewElement(CGCnvElement *obj,class="kw">const class="type">int x,class="kw">const class="type">int y);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Methods of simplified access to object properties                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) get the form frame class="type">color
class="type">void SetColorFrame(class="kw">const class="type">color colour) { this.m_color_frame=colour; }
class="type">color ColorFrame(class="type">void) class="kw">const { class="kw">return this.m_color_frame; }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the form shadow class="type">color
class="type">void SetColorShadow(class="kw">const class="type">color colour);
class="type">color ColorShadow(class="type">void) class="kw">const;
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the form shadow opacity
class="type">void SetOpacityShadow(class="kw">const class="type">uchar opacity);
把排列与重绘交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到控件层级与 Dock 序列的可视化,你只管判断布局策略。把重复劳动交给小布,你专注决策。

常见问题

把所有 WinForms 控件共有的属性提到一个基类里,其它对象继承即可,避免每个控件重复写 Dock、大小等相似逻辑。
后续同边 Dock 对象会贴到前一个对象的最近边界而不是容器边界,于是相同绑定侧的元素依次排列成行。
容器会按绑定元素的总大小自动伸缩,保证内部一排对象不超出自身边界,概率上更稳但需先虚拟排布再调尺寸。
可以,小布盯盘的品种页能叠加显示控件绑定序列和溢出预警,省去你手动打印调试信息的麻烦。
实时重绘会让尺寸和绑定点变化可见,留下渲染痕迹;正确做法是先虚拟排布、按需调容器、最后一次性重绘。
AutoSize 是总开关,AutoSizeMode 决定伸缩依据,本篇先落地 AutoSize,模式细节留待后续文章。