DoEasy. 控件 (第 19 部分): 在 TabControl 中滚动选项卡、WinForms 对象事件·进阶篇
「图形对象的布局与热区属性接口」
在 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 重绘一次以省开销。外汇与贵金属图表上叠加这类控件需注意,高频重绘会占用主线程,极端行情可能掉帧。
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。
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,能直观看到控件文本渲染大小变化,外汇与贵金属图表叠加自绘控件时需注意重绘可能引发滑点误读,属高风险操作。
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,再把主/基元素指针置空,避免野指针。
背景色取自实时图表:ChartGetInteger 读 CHART_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 返回值,就能确认自己的暗色模板有没有被正确捕获。
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 线。
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 看看默认值,就能确认画布基底尺寸假设是否成立。
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