DoEasy. 控件 (第 3 部分): 创建绑定控件·进阶篇
🧩

DoEasy. 控件 (第 3 部分): 创建绑定控件·进阶篇

(2/3)· 手动维护浮动面板的子控件坐标,是 MQL5 GUI 开发里最隐蔽的时间黑洞

案例拆解 第 2/3 篇
把面板拖来拖去却忘了同步子控件坐标,界面瞬间错位还查不出原因。多数人用硬编码偏移量修补,结果越改越乱。绑定控件不是锦上添花,而是多元素 GUI 不出 bug 的底线。

「图形控件基类的指针与画布接口」

在 MT5 自定义图形库里,CGCnvElement 作为所有画布控件的父类,先要解决「谁是我的上级」和「去哪拿画布」这两个问题。下面这段声明给出了最基础的存取接口。 SetBase() 把传入的 CGCnvElement 指针存进 m_element_base,等于给控件挂上父节点;GetBase() 原样返回这个指针,方便在嵌套控件里向上回溯。GetCanvasObj() 直接返回内部 m_canvas 的地址,意味着子类绘图不用自己再建画布,复用同一块 CCanvas 能省掉重复申请资源。 DuplicateResArraySize() 调用 ArraySize 读 m_duplicate_res 数组长度,这个数组存的是被复制过的图形资源,返回 0 表示当前还没缓存任何副本。Move() 被标成 virtual,说明子类可以重写坐标偏移逻辑,x、y 是相对位移,redraw 默认 false 不立即重绘。 构造函数放在 protected 区,外部不能直接 new 这个基类,只能从其派生类实例化,这是典型的接口类写法。

MQL5 / C++
class="kw">const class="type">bool redraw=class="kw">false);
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the pointer to the parent control
  class="type">void                 SetBase(CGCnvElement *element)                                                    { this.m_element_base=element;                    }
  CGCnvElement        *GetBase(class="type">void)                                                                      { class="kw">return this.m_element_base;                    }
class=class="str">"cmt">//--- Return the pointer to a canvas object
  CCanvas             *GetCanvasObj(class="type">void)                                                                 { class="kw">return &this.m_canvas;                         }
class=class="str">"cmt">//--- Return the size of the graphical resource copy array
  class="type">uint                DuplicateResArraySize(class="type">void)                                                         { class="kw">return ::ArraySize(this.m_duplicate_res);      }

class=class="str">"cmt">//--- Update the coordinates(shift the canvas)
  class="kw">virtual class="type">bool        Move(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">//--- Save an image to the array
  class="type">bool                ImageCopy(class="kw">const class="type">class="kw">string source,class="type">uint &array[]);
class="kw">protected:
class=class="str">"cmt">//--- Protected constructor
                     CGCnvElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                 class="kw">const class="type">long   chart_id,
                                 class="kw">const class="type">int    wnd_num,
                                 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,

画布元素的相对坐标与基对象绑定

在 MT5 自定义图形库里,每个画布元素都可以挂在一个基对象上,再用 X/Y 偏移量定位。CGCnvElement 类用两个私有成员 m_shift_coord_x 和 m_shift_coord_y 记录相对位移,默认构造时都初始化为 0,意味着不偏移、紧贴基对象左上角。 SetCoordXRelative / SetCoordYRelative 负责写偏移,CoordXRelative / CoordYRelative 负责读偏移。如果你在 EURUSD 的 H1 图上把基对象放在现价位,给 m_shift_coord_y 设 30,子元素就会在基对象下方 30 像素处绘制,具体视觉距离随图表缩放比例变化。 构造函数里还顺手抓了背景色:m_chart_color_bg 通过 ChartGetInteger(ChartID(), CHART_COLOR_BACKGROUND) 拿到当前图表背景,元素销毁时 m_canvas.Destroy() 释放资源。这套机制让你做复合指标时,不用每次重算绝对坐标,改一个基对象位置,整组图形跟着挪。 外汇与贵金属图表叠加自绘元素波动剧烈,高杠杆下误读坐标偏移可能造成错判,实盘前请在策略测试器里先验证偏移量表现。

MQL5 / C++
class="kw">const class="type">int    h);
class="kw">public:
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the X coordinate shift relative to the base object
   class="type">void        SetCoordXRelative(class="kw">const class="type">int value)                                                 { this.m_shift_coord_x=value;       }
   class="type">int         CoordXRelative(class="type">void)                                                                class="kw">const { class="kw">return this.m_shift_coord_x;      }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the Y coordinate shift relative to the base object
   class="type">void        SetCoordYRelative(class="kw">const class="type">int value)                                                 { this.m_shift_coord_y=value;       }
   class="type">int         CoordYRelative(class="type">void)                                                                class="kw">const { class="kw">return this.m_shift_coord_y;      }

class=class="str">"cmt">//--- Event handler
class=class="str">"cmt">//--- Default constructor/Destructor
                  CGCnvElement() : m_shadow(class="kw">false),m_chart_color_bg((class="type">class="kw">color)::ChartGetInteger(::ChartID(),CHART_COLOR_BACKGROUND))
                  { this.m_type=OBJECT_DE_TYPE_GELEMENT; this.m_element_base=NULL; this.m_shift_coord_x=class="num">0; this.m_shift_coord_y=class="num">0; }
                 ~CGCnvElement()
                  { this.m_canvas.Destroy();      }
class=class="str">"cmt">//--- Return(class="num">1) the background class="type">class="kw">color, (class="num">2) the opacity, coordinate(class="num">3) of the right and(class="num">4) bottom element edge
   class="type">class="kw">color        ColorBackground(class="type">void)    class="kw">const { class="kw">return this.m_color_bg;                                   }

◍ 画布元素的边界坐标与置顶控制

在 MT5 自定义图形库里,每个画布元素都得先算清自己占哪儿。右边缘等于元素 X 坐标加画布宽度,底边缘等于 Y 坐标加画布高度;相对坐标版本同理,只是换用 CoordXRelative / CoordYRelative,方便做子图嵌套布局。 Opacity() 直接返回 m_opacity 成员,透明度本身不参与边界运算,但会影响重叠时的视觉层次。RightEdge 与 BottomEdge 返回的是绝对像素,RightEdgeRelative 与 BottomEdgeRelative 返回相对父级坐标,两者差一个基准偏移。 BringToTop 的写法很取巧:先 SetVisible(false,false) 隐藏再 SetVisible(true,false) 显示,利用对象树的重绘顺序把元素顶到最上层。Show / Hide 只是单层显隐,不会改动层级。 外汇与贵金属图表叠加自定义元素时波动剧烈,建议先在 EURUSD 的 M1 周期跑一遍边界函数,确认 RightEdge 不超过图表可视区宽度(通常 0~ChartWidth 像素),否则元素会被裁切。

MQL5 / C++
  class="type">uchar                Opacity(class="type">void)                                class="kw">const { class="kw">return this.m_opacity;                                                                     }
  class="type">int                 RightEdge(class="type">void)                              class="kw">const { class="kw">return this.CoordX()+this.m_canvas.Width();                                                    }
  class="type">int                 BottomEdge(class="type">void)                             class="kw">const { class="kw">return this.CoordY()+this.m_canvas.Height();                                                   }
class=class="str">"cmt">//--- Return the relative coordinate of the(class="num">1) right and(class="num">2) bottom element edge
  class="type">int                 RightEdgeRelative(class="type">void)                     class="kw">const { class="kw">return this.CoordXRelative()+this.m_canvas.Width();                                             }
  class="type">int                 BottomEdgeRelative(class="type">void)                    class="kw">const { class="kw">return this.CoordYRelative()+this.m_canvas.Height();                                            }
class=class="str">"cmt">//--- Return the(class="num">1) X, (class="num">2) Y coordinates, (class="num">3) element width and(class="num">4) height,
class=class="str">"cmt">//--- Set the object above all
  class="kw">virtual class="type">void        BringToTop(class="type">void)                                      { CGBaseObj::SetVisible(class="kw">false,class="kw">false); CGBaseObj::SetVisible(true,class="kw">false);}
class=class="str">"cmt">//--- (class="num">1) Show and(class="num">2) hide the element
  class="kw">virtual class="type">void        Show(class="type">void)                                           { CGBaseObj::SetVisible(true,class="kw">false);                                                               }
  class="kw">virtual class="type">void        Hide(class="type">void)                                           { CGBaseObj::SetVisible(class="kw">false,class="kw">false);                                                              }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Parametric constructor                                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE element_type,

「图形基类的构造参数与坐标初始化」

在 MT5 自定义图形库里,图形元素基类靠一长串参数把位置、尺寸、颜色一次性锁死。上面这段构造函数签名里,element_id 与 element_num 用来区分同类型多个实例,chart_id 和 wnd_num 决定挂到哪张图、哪个子窗口,x/y/w/h 则是以像素为单位的左上角坐标和宽高。 构造函数体内先把 m_type 设为 OBJECT_DE_TYPE_GELEMENT,再把 m_element_base 置空,随后用 ChartGetInteger 取背景色存进 m_chart_color_bg——如果传进来的 chart_id 是 NULL 就回退到当前图 ChartID()。名字处理上有个细节:若传入 name 里没找到前缀 m_name_prefix,就自动拼上,避免和其他对象重名。 坐标偏移量 m_shift_coord_x 和 m_shift_coord_y 在初始化时都清零,这意味着对象刚建出来时不会带任何平移。最后调用 this.Create(...) 真正落盘到图表,redraw 默认 false 表示不立即重绘,由外部逻辑控制刷新节奏。 外汇与贵金属图表上叠加这类自绘元素,属于高风险环境下的视觉辅助,参数填错可能导致对象不可见或遮挡 K 线,建议开 MT5 用脚本单步验证坐标范围。

MQL5 / C++
                                                                 class="kw">const class="type">int      element_id,
                                                                 class="kw">const class="type">int      element_num,
                                                                 class="kw">const class="type">long     chart_id,
                                                                 class="kw">const class="type">int      wnd_num,
                                                                 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="kw">const class="type">class="kw">color    colour,
                                                                 class="kw">const class="type">uchar    opacity,
                                                                 class="kw">const class="type">bool     movable=true,
                                                                 class="kw">const class="type">bool     activity=true,
                                                                 class="kw">const class="type">bool     redraw=class="kw">false) : m_shadow(class="kw">false)
  {
   this.m_type=OBJECT_DE_TYPE_GELEMENT;
   this.m_element_base=NULL;
   this.m_chart_color_bg=(class="type">class="kw">color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND);
   this.m_name=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name;
   this.m_chart_id=(chart_id==NULL || chart_id==class="num">0 ? ::ChartID() : chart_id);
   this.m_subwindow=wnd_num;
   this.m_type_element=element_type;
   this.SetFont(DEF_FONT,DEF_FONT_SIZE);
   this.m_text_anchor=class="num">0;
   this.m_text_x=class="num">0;
   this.m_text_y=class="num">0;
   this.m_color_bg=colour;
   this.m_opacity=opacity;
   this.m_shift_coord_x=class="num">0;
   this.m_shift_coord_y=class="num">0;
   if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,colour,opacity,redraw))
     {

给画布元素加投影偏移量

在 MT5 的 Canvas 图形封装里,阴影不是后期滤镜,而是作为独立元素类挂到基类 CGCnvElement 下的。CShadowObj 这个类就专门管一个图形对象的投影,核心参数只有四个:颜色、不透明度、X 偏移、Y 偏移。 其中 m_offset_x 和 m_offset_y 是整型字段,决定阴影相对原图往右和下挪多少像素;OffsetX() 与 OffsetY() 是两个只读方法,直接回传这两个私有成员,外部绘制时拿它算投影坐标。 构造函数把 chart_id、subwindow、name 以及 x/y/w/h 全部透传给基类,元素类型写死为 GRAPH_ELEMENT_TYPE_SHADOW_OBJ。SupportProperty 对整型和字符串属性都返回 true,说明阴影对象在属性系统里不挑字段,便于运行时动态改色或改偏移。 实盘画 K 线标注时,若给信号箭头挂 2~3 像素的偏移阴影,在灰白背景上层次感会明显些;外汇与贵金属波动剧烈,这类纯视觉辅助不影响任何信号逻辑,但过度堆叠绘制调用可能拖慢老旧终端的刷新帧率。

MQL5 / C++
class CShadowObj : class="kw">public CGCnvElement
  {
class="kw">private:
   class="type">class="kw">color            m_color_shadow;            class=class="str">"cmt">// Shadow class="type">class="kw">color
   class="type">uchar            m_opacity_shadow;          class=class="str">"cmt">// Shadow opacity
   class="type">int              m_offset_x;                class=class="str">"cmt">// Shadow X axis shift
   class="type">int              m_offset_y;                class=class="str">"cmt">// Shadow Y axis shift
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">//--- Return the shadow shift by(class="num">1) X and(class="num">2) Y
   class="type">int              OffsetX(class="type">void)              class="kw">const { class="kw">return this.m_offset_x;     }
   class="type">int              OffsetY(class="type">void)              class="kw">const { class="kw">return this.m_offset_y;     }
class=class="str">"cmt">//--- Draw an object shadow
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)
  {

◍ 阴影偏移与模糊半径的落地细节

在 CShadowObj 的绘制流程里,DrawShadow 先把传入的 shift_x / shift_y 写进成员变量 m_offset_x 与 m_offset_y,这两个值直接决定阴影相对主体的像素位移。想验证偏移效果,把调用处的 shift_x 改成 10、shift_y 改成 5,MT5 画布上阴影就会向右下错开对应像素。 矩形尺寸用 Width() 和 Height() 减去两倍的 OUTER_AREA_SIZE 得出,意味着阴影图永远比外框内缩一圈。模糊半径被硬性夹在 OUTER_AREA_SIZE/4 以内——比如 OUTER_AREA_SIZE 为 40 时,blur_value 传 20 也只生效 10,超阈值的模糊参数会被静默截断。 最后 Move() 把坐标加上偏移量并 Update() 刷新画布;若 GaussianBlur() 返回失败则直接 return,不抛异常只会在日志报错。外汇与贵金属图表上叠加这类自定义阴影元素时,注意过度模糊会增加 CPU 占用,高波动时段可能拖累刷新帧率。

MQL5 / C++
this.m_type=OBJECT_DE_TYPE_GSHADOW;
CGCnvElement::SetColorBackground(clrNONE);
CGCnvElement::SetOpacity(class="num">0);
CGCnvElement::SetActive(class="kw">false);
this.m_opacity_shadow=class="num">127;
class="type">class="kw">color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-class="num">100);
this.m_color_shadow=CGCnvElement::ChangeColorLightness(gray,-class="num">50);
this.m_shadow=class="kw">false;
this.m_visible=true;
this.m_offset_x=class="num">0;
this.m_offset_y=class="num">0;
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::DrawShadow(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=class="str">"cmt">//--- Set the shadow shift values to the variables by X and Y axes
  this.m_offset_x=shift_x;
  this.m_offset_y=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
  class="type">int radius=(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(radius))
      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.m_offset_x,this.CoordY()+this.m_offset_y);
  CGCnvElement::Update();
  }
把控件联动逻辑交给小布
这些绑定关系的诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只需专注面板层级怎么划分最顺手。

常见问题

通常是因为子对象没有绑定到面板基类,或创建时未走库内的绑定方法。本篇演示的附着流程可让移动事件自动级联。
原文调整了阴影投射逻辑,使其叠加在所属对象上方而非图表顶层,涉及 Defines 里默认值的重排。
目前小布提供结构诊断与示例对照,不代写完整库代码;你可以把文中枚举重排思路贴进小布做差异检查。
会影响。把阴影常量挪到 GUI 对象分组之外,才能用枚举值快速圈定待处理对象集。
因为它在继承链中首次具备内部再创建图元的能力,更上层容器只负责装载而不生产。