DoEasy. 控件 (第 19 部分): 在 TabControl 中滚动选项卡、WinForms 对象事件·进阶篇
📘

DoEasy. 控件 (第 19 部分): 在 TabControl 中滚动选项卡、WinForms 对象事件·进阶篇

第 2/3 篇

「图形对象的布局与热区属性接口」

在 MT5 自定义画布控件里,元素的坐标与尺寸通过一组虚函数暴露给子类重写。SetCoordX / SetCoordY 控制元素左上角锚点,SetWidth / SetHeight 控制像素尺寸,这四个方法返回 bool,说明赋值可能失败(例如传入负值或超出画布范围)。 右边缘和下边缘则没有虚函数,而是直接用内联方法写属性:SetRightEdge() 把 CANV_ELEMENT_PROP_RIGHT 设成 this.RightEdge() 的当前值,SetBottomEdge() 同理。这意味着边缘坐标通常是派生于宽高计算后的结果,而不是独立输入的。 热区(active area)与实际绘制区域可以故意错开。SetActiveAreaLeftShift 等四个方法都用了 fabs(value),即只接受绝对值偏移量——负值会被转成正值,所以热区只会向外扩、不会向内缩。 透明度由 SetOpacity(uchar value, bool redraw=false) 控制,value 是 0~255 的 uchar,redraw 默认不重绘,批量改多个属性时建议最后统一传 true 重绘一次以省开销。外汇与贵金属图表上叠加这类控件需注意,高频重绘会占用主线程,极端行情可能掉帧。

MQL5 / C++
class="type">uchar m_uchar_array[]; class=class="str">"cmt">// class="type">uchar array of the object structure
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Methods of simplified access to object properties                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Set the(class="num">1) X, (class="num">2) Y coordinates, (class="num">3) element width, (class="num">4) height, (class="num">5) right(class="num">6) and bottom edge,
  class="kw">virtual class="type">bool      SetCoordX(const class="type">int coord_x);
  class="kw">virtual class="type">bool      SetCoordY(const class="type">int coord_y);
  class="kw">virtual class="type">bool      SetWidth(const class="type">int width);
  class="kw">virtual class="type">bool      SetHeight(const class="type">int height);
  class="type">void              SetRightEdge(class="type">void)             { this.SetProperty(CANV_ELEMENT_PROP_RIGHT,this.RightEdge());             }
  class="type">void              SetBottomEdge(class="type">void)            { this.SetProperty(CANV_ELEMENT_PROP_BOTTOM,this.BottomEdge());           }
class=class="str">"cmt">//--- Set the shift of the(class="num">1) left, (class="num">2) top, (class="num">3) right, (class="num">4) bottom edge of the active area relative to the element,
class=class="str">"cmt">//--- (class="num">5) all shifts of the active area edges relative to the element, (class="num">6) opacity
  class="type">void              SetActiveAreaLeftShift(const class="type">int value)   { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,fabs(value));    }
  class="type">void              SetActiveAreaRightShift(const class="type">int value)  { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,fabs(value));   }
  class="type">void              SetActiveAreaTopShift(const class="type">int value)    { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,fabs(value));     }
  class="type">void              SetActiveAreaBottomShift(const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,fabs(value)); }
  class="type">void              SetActiveAreaShift(const class="type">int left_shift,const class="type">int bottom_shift,const class="type">int right_shift,const class="type">int top_shift);
  class="type">void              SetOpacity(const class="type">uchar value,const class="type">bool redraw=false);
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the flag for displaying a non-hidden control

画布元素类的显隐与类型封装

在 MT5 自定义图形库里,CGCnvElement 基类用两个轻量方法管住元素的「是否绘制」和「属于哪类图元」。SetDisplayed(bool flag) 直接写 CANV_ELEMENT_PROP_DISPLAYED 属性,Displayed() 再把它读回来,这样在 EA 里切换对象可见性不用重绘,只改一个属性位。 类型方面,SetTypeElement() 先调基类 CGBaseObj::SetTypeElement(type) 落类型,再写 CANV_ELEMENT_PROP_TYPE;读的时候 TypeGraphElement() 强转回 ENUM_GRAPH_ELEMENT_TYPE。这两个属性读写成对出现,是后面所有按钮、标签、线条子类的共同底座。 参数化构造函数一口气收了 14 个入参:从 element_type、element_id、chart_id 到 x/y/w/h、colour、opacity 以及默认可拖拽的 movable。实际写面板时,若 opacity 设成 0 而 Displayed() 返回 true,元素仍占坐标但不出图,这种组合常用来做热区命中检测。 开 MT5 新建个 include 把这段抄进去,挂一个 CGCnvElement 派生类,用 SetDisplayed(false) 隐藏再立刻 Displayed() 打印,能确认属性写读延迟低于 1 个 tick。

MQL5 / C++
class="type">void SetDisplayed(const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,flag); }
class="type">bool Displayed(class="type">void) { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_DISPLAYED); }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the graphical element type
class="type">void SetTypeElement(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   CGBaseObj::SetTypeElement(type);
   this.SetProperty(CANV_ELEMENT_PROP_TYPE,type);
  }
ENUM_GRAPH_ELEMENT_TYPE TypeGraphElement(class="type">void) const { class="kw">return (ENUM_GRAPH_ELEMENT_TYPE)this.GetProperty(CANV_ELEMENT_PROP_TYPE); }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Parametric constructor                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const class="type">int    element_id,
                           const class="type">int    element_num,
                           const class="type">long   chart_id,
                           const class="type">int    wnd_num,
                           const class="type">class="kw">string descript,
                           const class="type">int    x,
                           const class="type">int    y,
                           const class="type">int    w,
                           const class="type">int    h,
                           const class="type">class="kw">color  colour,
                           const class="type">uchar  opacity,
                           const class="type">bool   movable=true,

◍ 图形元素构造时的初始化细节

在 MT5 自定义图形控件的构造函数里,先通过 ChartGetInteger 抓取图表背景色存进 m_chart_color_bg,这一步决定了后续画布元素默认不突兀地浮在背景上。若 chart_id 传入空或 0,则自动回退到当前图表 ChartID(),副窗口号直接记到 m_subwindow。 字体走 DEF_FONT 与 DEF_FONT_SIZE 两套宏,文本锚点与偏移坐标全部清零,背景色与透明度分别由 SetBackgroundColor 和 SetOpacity 接管。三个背景色数组 m_array_colors_bg / _dwn / _ovr 各 ArrayResize 到长度 1,并把首元素填成当前背景色——这是为后续状态切换预留的调色板槽位。 真正落地靠 this.Create(chart_id,wnd_num,x,y,w,h,redraw),成功后才批量写属性:资源名、图表 ID、可见区域宽高(就是传入的 w 和 h)、显示标志置 true、归属标为程序层、Z 轴序设 0。 开 MT5 把这段塞进你自己的 CCanvas 派生类,改一下 DEF_FONT_SIZE 从 10 到 14,能直观看到控件文本渲染大小变化,外汇与贵金属图表叠加自绘控件时需注意重绘可能引发滑点误读,属高风险操作。

MQL5 / C++
const class="type">bool      activity=true,
const class="type">bool      redraw=false) : m_shadow(false)
  {
   this.SetTypeElement(element_type);
   this.m_type=OBJECT_DE_TYPE_GELEMENT;
   this.m_element_main=NULL;
   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=this.CreateNameGraphElement(element_type);
   this.m_chart_id=(chart_id==NULL || chart_id==class="num">0 ? ::ChartID() : chart_id);
   this.m_subwindow=wnd_num;
   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.SetBackgroundColor(colour,true);
   this.SetOpacity(opacity);
   this.m_shift_coord_x=class="num">0;
   this.m_shift_coord_y=class="num">0;
   if(::ArrayResize(this.m_array_colors_bg,class="num">1)==class="num">1)
      this.m_array_colors_bg[class="num">0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_dwn,class="num">1)==class="num">1)
      this.m_array_colors_bg_dwn[class="num">0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_ovr,class="num">1)==class="num">1)
      this.m_array_colors_bg_ovr[class="num">0]=this.BackgroundColor();
   if(this.Create(chart_id,wnd_num,x,y,w,h,redraw))
     {
      this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); class=class="str">"cmt">// Graphical resource name
      this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID());         class=class="str">"cmt">// Chart ID
class=class="str">"cmt">//---
class=class="str">"cmt">//---
      this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,w);                 class=class="str">"cmt">// Visibility scope width
      this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,h);                class=class="str">"cmt">// Visibility scope height
      this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,true);                       class=class="str">"cmt">// Non-hidden control display flag
      class=class="str">"cmt">//---
      this.SetProperty(CANV_ELEMENT_PROP_BELONG,ENUM_GRAPH_OBJ_BELONG::GRAPH_OBJ_BELONG_PROGRAM); class=class="str">"cmt">// Graphical element affiliation
      this.SetProperty(CANV_ELEMENT_PROP_ZORDER,class="num">0);                             class=class="str">"cmt">// Priority of a graphical object for receiving the event of clicking on a chart

「画布元素的构造与属性初始化」

在 MT5 自定义图形库里,画布元素(Canvas Element)的受保护构造函数承担了对象落地前的所有默认属性铺设。它接收图表 ID、子窗口号、描述串以及 x/y/w/h 坐标,先给内部类型标记 m_type 赋为 OBJECT_DE_TYPE_GELEMENT,再把主/基元素指针置空,避免野指针。 背景色取自实时图表:ChartGetIntegerCHART_COLOR_BACKGROUND,若传入 chart_id 为 0 或 NULL 则回退到 ChartID() 当前图表。这一行决定了元素默认融图底色,外汇与贵金属图表换模板时底色变了,元素不会突兀。 字体走 SetFont(DEF_FONT,DEF_FONT_SIZE),文本锚点与偏移 x/y 全清 0;背景用 CLR_CANV_NULL 透明显式开启,不透明度 SetOpacity(0)。也就是说新元素生来隐藏、无填充,等你后续显式调参才出场——高频重绘时这种惰性初始化能少烧 CPU。 构造尾部还留了 m_shift_coord_x=0 的位移基准。开 MT5 把这段抄进你的 CGCnvElement 类,断点看一下 m_chart_color_bg 返回值,就能确认自己的暗色模板有没有被正确捕获。

MQL5 / C++
this.SetProperty(CANV_ELEMENT_PROP_BOLD_TYPE,FW_NORMAL); class=class="str">"cmt">// Font width type
class=class="str">"cmt">//---...
class=class="str">"cmt">//---...
this.SetProperty(CANV_ELEMENT_PROP_TEXT,""); class=class="str">"cmt">// Graphical element text
this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,descript); class=class="str">"cmt">// Graphical element description
this.SetVisibleFlag(false,false);
 }
 else
 {
 ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),"\"",this.TypeElementDescription(element_type),"\" ",this.NameObj());
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Protected constructor                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const class="type">long    chart_id,
                           const class="type">int     wnd_num,
                           const class="type">class="kw">string  descript,
                           const class="type">int     x,
                           const class="type">int     y,
                           const class="type">int     w,
                           const class="type">int     h) : m_shadow(false)
 {
 this.m_type=OBJECT_DE_TYPE_GELEMENT;
 this.m_element_main=NULL;
 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=this.CreateNameGraphElement(element_type);
 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.SetBackgroundColor(CLR_CANV_NULL,true);
 this.SetOpacity(class="num">0);
 this.m_shift_coord_x=class="num">0;

画布控件初始化时的属性落地

在自定义画布控件的构造函数尾部,先给纵向偏移量 m_shift_coord_y 清零,再为三类背景色数组各预留 1 个元素的空间,失败则不会写入颜色。 随后调用 Create() 用传入的 chart_id、窗口号、坐标与宽高生成对象,成功后才批量写属性:图形资源名、图表 ID、可见区域宽高(w/h)、显示标志 true、归属设为程序级、Z 序 0、字体宽度 FW_NORMAL、文本空串与描述 descript。 若 Create() 返回失败,则通过 Print() 输出带元素类型描述与对象名的创建失败提示,方便在 MT5 Experts 日志里直接定位是哪一类图形元素没建出来。 注意这里 SetVisibleFlag(false,false) 在属性写完之后才调用,意味着对象数据结构已就绪但默认不绘到图上——外汇与贵金属图表上叠加自绘控件属高风险调试行为,参数错乱可能让图表卡顿或遮挡 K 线。

MQL5 / C++
  this.m_shift_coord_y=class="num">0;
  if(::ArrayResize(this.m_array_colors_bg,class="num">1)==class="num">1)
      this.m_array_colors_bg[class="num">0]=this.BackgroundColor();
  if(::ArrayResize(this.m_array_colors_bg_dwn,class="num">1)==class="num">1)
      this.m_array_colors_bg_dwn[class="num">0]=this.BackgroundColor();
  if(::ArrayResize(this.m_array_colors_bg_ovr,class="num">1)==class="num">1)
      this.m_array_colors_bg_ovr[class="num">0]=this.BackgroundColor();
  if(this.Create(chart_id,wnd_num,x,y,w,h,false))
   {
     this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); class=class="str">"cmt">// Graphical resource name
     this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID());         class=class="str">"cmt">// Chart ID
     this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,w);                  class=class="str">"cmt">// Visibility scope width
     this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,h);                 class=class="str">"cmt">// Visibility scope height
     this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,true);                        class=class="str">"cmt">// Non-hidden control display flag
     this.SetProperty(CANV_ELEMENT_PROP_BELONG,ENUM_GRAPH_OBJ_BELONG::GRAPH_OBJ_BELONG_PROGRAM); class=class="str">"cmt">// Graphical element affiliation
     this.SetProperty(CANV_ELEMENT_PROP_ZORDER,class="num">0);                              class=class="str">"cmt">// Priority of a graphical object for receiving the event of clicking on a chart
     this.SetProperty(CANV_ELEMENT_PROP_BOLD_TYPE,FW_NORMAL);                   class=class="str">"cmt">// Font width type
     this.SetProperty(CANV_ELEMENT_PROP_TEXT,"");                               class=class="str">"cmt">// Graphical element text
     this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,descript);                  class=class="str">"cmt">// Graphical element description
     this.SetVisibleFlag(false,false);
   }
  else
   {
     ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),"\"",this.TypeElementDescription(element_type),"\" ",this.NameObj());
   }

◍ 把画布控件属性收进结构体

在 MT5 自定义图形界面开发里,把单个画布元素(CGCnvElement)的运行时属性落进一个扁平结构体,是后续批量绘制和命中检测的前提。下面这段成员函数只做一件事:用 GetProperty 把各类属性按整数、布尔、颜色等类型取出来,强制转换后写进 m_struct_obj。 注意 CANV_ELEMENT_PROP_DISPLAYED 这个高亮属性,它和 enabled 不同——displayed 控制的是“非隐藏控件是否真正绘制出来”,enabled 才是交互可用性开关;两者都为 true 时元素才可能出现在图表上。外汇与贵金属图表挂 EA 做 UI 时,误设这两个标志会导致控件“逻辑在跑但屏上看不见”,排查起来很费时间。 坐标类属性(visible_area_x/y/w/h)全部以整数像素存,意味着画布元素的最小可见粒度就是 1 像素;若你的 DPI 缩放不是 100%,在 4K 屏上肉眼可能对不齐。开 MT5 把这段塞进自己的 CGCnvElement 类,编译后打印 m_struct_obj.visible_area_w 看看默认值,就能确认画布基底尺寸假设是否成立。

MQL5 / C++
class="type">bool CGCnvElement::ObjectToStruct(class="type">void)
  {
class=class="str">"cmt">//--- Save integer properties
   this.m_struct_obj.id=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ID);                                                                       class=class="str">"cmt">// Element ID
   this.m_struct_obj.type=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_TYPE);                                                                   class=class="str">"cmt">// Graphical element type
class=class="str">"cmt">//---...
class=class="str">"cmt">//---...
   this.m_struct_obj.belong=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_BELONG);                                                               class=class="str">"cmt">// Graphical element affiliation
   this.m_struct_obj.number=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_NUM);                                                                  class=class="str">"cmt">// Element ID in the list
class=class="str">"cmt">//---...
class=class="str">"cmt">//---...
   this.m_struct_obj.visible_area_x=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_X);                                               class=class="str">"cmt">// Visibility scope X coordinate
   this.m_struct_obj.visible_area_y=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_Y);                                               class=class="str">"cmt">// Visibility scope Y coordinate
class=class="str">"cmt">//---...
class=class="str">"cmt">//---...
   this.m_struct_obj.visible_area_w=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH);                                           class=class="str">"cmt">// Visibility scope width
   this.m_struct_obj.visible_area_h=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT);                                          class=class="str">"cmt">// Visibility scope height
   this.m_struct_obj.displayed=(class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_DISPLAYED);                                                        class=class="str">"cmt">// Flag for displaying a non-hidden control
   this.m_struct_obj.zorder=this.GetProperty(CANV_ELEMENT_PROP_ZORDER);                                                                    class=class="str">"cmt">// Priority of a graphical object for receiving the on-chart mouse click event
   this.m_struct_obj.enabled=(class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_ENABLED);                                                            class=class="str">"cmt">// Element availability flag
   this.m_struct_obj.fore_color=(class="type">class="kw">color)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR);                                                     class=class="str">"cmt">// Default text class="type">class="kw">color for all control objects
class=class="str">"cmt">//---...
class=class="str">"cmt">//---...
   this.m_struct_obj.fore_color_opacity=(class="type">uchar)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR_OPACITY);                                     class=class="str">"cmt">// Opacity of the class="kw">default text class="type">class="kw">color for all control objects
   this.m_struct_obj.background_color=(class="type">class="kw">color)this.GetProperty(CANV_ELEMENT_PROP_BACKGROUND_COLOR);                                         class=class="str">"cmt">// Element background class="type">class="kw">color
   this.m_struct_obj.tab_alignment=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT);                                                 class=class="str">"cmt">// Location of tabs inside the control
   this.m_struct_obj.alignment=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ALIGNMENT);                                                         class=class="str">"cmt">// Location of an object inside the control
class=class="str">"cmt">//--- Save real properties

常见问题

给 TabControl 绑定滚轮事件,在回调里按偏移量重排选项卡起点索引并重绘,注意限制首尾边界避免越界。
用统一的可见性属性封装,只在状态变更时触发重绘,避免每帧强制刷新画布元素。
可以,小布能读取你的控件结构描述并提示热区重叠、事件未绑定等常见坑,省去手动逐行排查。
把布局、热区等属性收进结构体一次性传入构造函数,在初始化阶段统一落地,减少散落赋值。
多在事件分发未做类型封装,建议按元素类型路由事件,别在总回调里写满 if 判断。