DoEasy. 控件 (第 17 部分): 裁剪对象不可见部分、辅助箭头按钮 WinForms 对象·进阶篇
(2/3)· 画布不支持位图裁剪属性,靠自绘矩形区域遮住越界像素,并补上滚动条要用的箭头按钮
很多交易者以为把控件塞进容器就自动只显示框内部分,结果标签超出边框还挂在背景上。MQL 的 OBJ_BITMAP 裁剪属性对内存画布无效,直接套用只会得到全显或全隐。这篇用自绘透明区解决越界,并顺手造出箭头按钮给后续 TabControl 滚动用。
「画布元素排序与控件枚举的底层常量」
在 MT5 自定义图形界面开发里,画布元素(Canvas Element)有一套固定的排序枚举,用来决定遍历或命中测试时的先后顺序。上面这段枚举片段列出了按可见区域宽高、所属分组、Z 序(点击接收优先级)、Tab 列索引以及对象内对齐方式等维度排序的常量,例如 SORT_BY_CANV_ELEMENT_VISIBLE_AREA_WIDTH 对应按可见范围宽度排,SORT_BY_CANV_ELEMENT_ZORDER 决定图表点击事件的分发层级。 紧接着是一组界面控件类型消息常量,从 TabControl 到 ArrowButton 系列(上/下/左/右及组合框)都有独立标识。高亮部分如 MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP 专门指代向上箭头按钮控件,方便在消息路由里精准区分用户点了哪个方向键。 画布元素属性侧也有坐标与尺寸类消息常量,像 MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_X / Y / WIDTH 分别描述可见区域的左上角坐标与宽度。实盘写面板时,直接用这些枚举做 switch 分支,比字符串比对快且不易拼错;外汇与贵金属图表挂 EA 做 UI 时波动滑点风险高,先在策略测试器里验证控件响应再上真仓。
SORT_BY_CANV_ELEMENT_VISIBLE_AREA_WIDTH, class=class="str">"cmt">// Sort by visibility scope width SORT_BY_CANV_ELEMENT_VISIBLE_AREA_HEIGHT, class=class="str">"cmt">// Sort by visibility scope height SORT_BY_CANV_ELEMENT_GROUP, class=class="str">"cmt">// Sort by a group the graphical element belongs to SORT_BY_CANV_ELEMENT_ZORDER, class=class="str">"cmt">// Sort by the priority of a graphical object for receiving the event of clicking on a chart class=class="str">"cmt">//---... class=class="str">"cmt">//---... SORT_BY_CANV_ELEMENT_TAB_PAGE_COLUMN, class=class="str">"cmt">// Sort by tab column index SORT_BY_CANV_ELEMENT_ALIGNMENT, class=class="str">"cmt">// Sort by the location of the object inside the control class=class="str">"cmt">//--- Sort by real properties class=class="str">"cmt">//--- Sort by class="type">class="kw">string properties SORT_BY_CANV_ELEMENT_NAME_OBJ = FIRST_CANV_ELEMENT_STR_PROP,class=class="str">"cmt">// Sort by an element object name SORT_BY_CANV_ELEMENT_NAME_RES, class=class="str">"cmt">// Sort by the graphical resource name SORT_BY_CANV_ELEMENT_TEXT, class=class="str">"cmt">// Sort by graphical element text SORT_BY_CANV_ELEMENT_DESCRIPTION, class=class="str">"cmt">// Sort by graphical element description }; class=class="str">"cmt">//+------------------------------------------------------------------+ MSG_GRAPH_ELEMENT_TYPE_WF_TAB_FIELD, class=class="str">"cmt">// TabControl tab field MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL, class=class="str">"cmt">// TabControl MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON, class=class="str">"cmt">// ArrowButton control MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP, class=class="str">"cmt">// UpArrowButton control MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN, class=class="str">"cmt">// DownArrowButton control MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT, class=class="str">"cmt">// LeftArrowButton control MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT, class=class="str">"cmt">// RightArrowButton control MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX, class=class="str">"cmt">// UpDownArrowBox control MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX, class=class="str">"cmt">// LeftRightArrowBox control MSG_GRAPH_OBJ_BELONG_PROGRAM, class=class="str">"cmt">// Graphical object belongs to a program MSG_GRAPH_OBJ_BELONG_NO_PROGRAM, class=class="str">"cmt">// Graphical object does not belong to a program MSG_CANV_ELEMENT_PROP_ACT_RIGHT, class=class="str">"cmt">// Right border of the element active area MSG_CANV_ELEMENT_PROP_ACT_BOTTOM, class=class="str">"cmt">// Bottom border of the element active area MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_X, class=class="str">"cmt">// Visibility scope X coordinate MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_Y, class=class="str">"cmt">// Visibility scope Y coordinate MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH, class=class="str">"cmt">// Visibility scope width
把图形元素枚举名转成可读字符串
在 MT5 自定义控件库里,图形元素类型用 ENUM_GRAPH_ELEMENT_TYPE 枚举表达,但枚举名自带前缀且全大写,直接显示给用户会很丑。下面这段函数专门干「枚举转字符串」的脏活。
它先用 EnumToString 拿到完整枚举名,再从第 18 个字符起截取子串——因为前缀 GRAPH_ELEMENT_TYPE_ 刚好占 18 个字符(含末尾下划线)。截完丢进 ushort 数组,逐字符处理:遇到下划线(ASCII 95)就跳过一个字符不处理,其余字符统一加 0x20 转成小写。
最后用 ShortArrayToString 拼回字符串,并把控件基类后缀 _Wf_Base 替换成 WFBase。跑一遍你能看到 GRAPH_ELEMENT_TYPE_TAB_CONTROL 变成 tab control,可读性立刻上来。外汇与贵金属图表挂这种自绘面板时,注意控件坐标基于画布像素,缩放后可能错位,属高风险定制项。
别把枚举前缀长度写死
上面那个魔法数字 18 只在这套枚举命名规则下成立。哪天官方改了前缀长度,截取就会从中间断词。更稳的做法是用 StringFind 定位首个下划线后再 Substr,别赌命名规范不变。
class="type">class="kw">string TypeGraphElementAsString(const ENUM_GRAPH_ELEMENT_TYPE type) { class="type">class="kw">ushort array[]; class="type">int total=StringToShortArray(StringSubstr(EnumToString(type),class="num">18),array); class=class="str">"cmt">// 枚举名去前18字符转ushort数组 for(class="type">int i=class="num">0;i<total-class="num">1;i++) { if(array[i]==class="num">95) class=class="str">"cmt">// 95即&class="macro">#x27;_&class="macro">#x27;,遇到下划线 { i+=class="num">1; class=class="str">"cmt">// 跳过下划线后的那个字符 class="kw">continue; } else array[i]+=0x20; class=class="str">"cmt">// 非下划线字符转小写 } class="type">class="kw">string txt=ShortArrayToString(array); class=class="str">"cmt">// 数组合回字符串 StringReplace(txt,"_Wf_Base","WFBase"); class=class="str">"cmt">// 替换基类后缀
◍ 列表项对象的命名清洗与构造细节
在图形库里,列表框子项从原始对象名生成可读文本时,要先做一轮字符串规整。下面这段连续调用会把 '_Wf_'、'_Obj'、单独下划线全部剔掉,并把 'Groupbox' 修正为 'GroupBox',顺手把 'ButtonsUdBox'、'ButtonsLrBox' 的大小写补全为 'ButtonsUDBox'、'ButtonsLRBox',避免界面上出现五花八门的标签。 StringReplace(txt,"_Wf_",""); StringReplace(txt,"_Obj",""); StringReplace(txt,"_",""); StringReplace(txt,"Groupbox","GroupBox"); StringReplace(txt,"ButtonsUdBox","ButtonsUDBox"); StringReplace(txt,"ButtonsLrBox","ButtonsLRBox"); return txt; 清洗完名字后,CListBoxItem 的受保护构造函数接管对象本体。它接收图形元素类型、图表 ID、子窗口号以及坐标宽高,并直接复用 CButton 的初始化链:把元素类型写回内部字段,将 m_type 固定为 OBJECT_DE_TYPE_GWF_HELPER,再把文本对齐设为左锚、文字左侧留 1 像素间距。 CListBoxItem::CListBoxItem(const ENUM_GRAPH_ELEMENT_TYPE type, const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h) : CButton(type,chart_id,subwindow,descript,x,y,w,h) { this.SetTypeElement(type); this.m_type=OBJECT_DE_TYPE_GWF_HELPER; this.SetTextAlign(ANCHOR_LEFT); this.SetTextShiftSpace(1); } 还有一个对外暴露的轻量构造版本,只吃 chart_id、subwindow、descript 和 x/y,其余宽高走默认值,方便在 EA 里快速挂一个列表项而不必每次算尺寸。外汇与贵金属图表加载这类自定义图形对象存在刷新失败可能,建议在 MT5 策略测试器里先跑一次创建/销毁循环验证稳定性。
StringReplace(txt,"_Wf_",""); StringReplace(txt,"_Obj",""); StringReplace(txt,"_",""); StringReplace(txt,"Groupbox","GroupBox"); StringReplace(txt,"ButtonsUdBox","ButtonsUDBox"); StringReplace(txt,"ButtonsLrBox","ButtonsLRBox"); class="kw">return txt; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Protected constructor with an object type, chart ID and subwindow | class=class="str">"cmt">//+------------------------------------------------------------------+ CListBoxItem::CListBoxItem(const ENUM_GRAPH_ELEMENT_TYPE type, const class="type">long chart_id, const class="type">int subwindow, 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) : CButton(type,chart_id,subwindow,descript,x,y,w,h) { class=class="str">"cmt">//--- Set the specified graphical element type for the object and assign the library object type to the current object this.SetTypeElement(type); this.m_type=OBJECT_DE_TYPE_GWF_HELPER; this.SetTextAlign(ANCHOR_LEFT); this.SetTextShiftSpace(class="num">1); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CListBoxItem::CListBoxItem(const class="type">long chart_id, const class="type">int subwindow, const class="type">class="kw">string descript, const class="type">int x, const class="type">int y,
「自建列表项控件的层叠与偏移写法」
在 MT5 自定义图形库里,列表框子项常从 CButton 派生,构造时直接把元素类型钉死为 GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM,并顺手把 m_type 标成 OBJECT_DE_TYPE_GWF_HELPER,避免后续事件路由误判。 SetZorder 这层封装值得留意:它先 ResetLastError,再走 ObjectSetInteger 改 OBJPROP_ZORDER;若 only_prop 为真就只存内存值不碰图表对象,返回 true 的概率取决于图表 ID 与对象名是否有效。外汇与贵金属图表上叠太多辅助控件时,Z 序乱了点不到是常事,属于高概率但非必现的坑。 SetXOffset 针对 OBJ_BITMAP_LABEL / OBJ_BITMAP 的可见区左上角 X 坐标,失败就吐错误码并返回 false。下面这段是原文核心,逐行拆一遍就能直接抄进自己的类。 虚拟方法重写时若省掉 only_prop 分支,控件在脚本重载后可能丢失层叠状态,倾向在初始化阶段显式调一次 SetZorder(0,false) 做基线。
const class="type">int w, const class="type">int h) : CButton(GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM,chart_id,subwindow,descript,x,y,w,h) { this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM); this.m_type=OBJECT_DE_TYPE_GWF_HELPER; this.SetTextAlign(ANCHOR_LEFT); this.SetTextShiftSpace(class="num">1); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Set the priority of a graphical object for receiving the event of clicking on a chart class="kw">virtual class="type">bool SetZorder(const class="type">long value,const class="type">bool only_prop) { ::ResetLastError(); if((!only_prop && ::ObjectSetInteger(this.m_chart_id,this.m_name,OBJPROP_ZORDER,value)) || only_prop) { this.m_zorder=value; class="kw">return true; } else CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; } class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the X coordinate of the upper left corner of the rectangle visibility scope of the OBJ_BITMAP_LABEL and OBJ_BITMAP graphical object class="kw">virtual class="type">bool SetXOffset(const class="type">long value) { ::ResetLastError(); if(!::ObjectSetInteger(this.m_chart_id,this.m_name,OBJPROP_XOFFSET,value)) { CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; }
图形对象的偏移与尺寸读写接口
在 MT5 自定义图形类里,位图标签(OBJ_BITMAP_LABEL)和位图(OBJ_BITMAP)的可见范围定位,靠的是 XOffset / YOffset 这两个虚函数。Set 方法先 ResetLastError 清掉旧错误码,再用 ObjectSetInteger 写 OBJPROP_XOFFSET / OBJPROP_YOFFSET,失败就通过 CMessage::ToLog 把 GetLastError 打进日志并返回 false,成功返回 true。 读取值时直接用 XOffset() 和 YOffset(),内部把 ObjectGetInteger 的返回值强转成 int,零参数调用,开销极低。 宽度控制走 SetXSize,目标属性是 OBJPROP_XSIZE,适用于 OBJ_LABEL(只读)、OBJ_BUTTON、OBJ_CHART、OBJ_BITMAP、OBJ_BITMAP_LABEL、OBJ_EDIT 和 OBJ_RECTANGLE_LABEL。设置逻辑与偏移一致:清错误码→写整数→失败记日志返 false。外汇与贵金属图表上叠加这类对象时,坐标偏移量建议用像素整数,避免高分屏下定位漂移。
class="kw">virtual class="type">int XOffset(class="type">void) const { class="kw">return (class="type">int)::ObjectGetInteger(this.m_chart_id,this.m_name,OBJPROP_XOFFSET); } class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the Y coordinate of the upper left corner of the rectangle visibility scope of the OBJ_BITMAP_LABEL and OBJ_BITMAP graphical object class="kw">virtual class="type">bool SetYOffset(const class="type">long value) { ::ResetLastError(); if(!::ObjectSetInteger(this.m_chart_id,this.m_name,OBJPROP_YOFFSET,value)) { CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; } class="kw">return true; } class="kw">virtual class="type">int YOffset(class="type">void) const { class="kw">return (class="type">int)::ObjectGetInteger(this.m_chart_id,this.m_name,OBJPROP_YOFFSET); } class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the width of OBJ_LABEL(read only), OBJ_BUTTON, OBJ_CHART, OBJ_BITMAP, OBJ_BITMAP_LABEL, OBJ_EDIT and OBJ_RECTANGLE_LABEL objects class="kw">virtual class="type">bool SetXSize(const class="type">long value) { ::ResetLastError(); if(!::ObjectSetInteger(this.m_chart_id,this.m_name,OBJPROP_XSIZE,value)) { CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; }
◍ 对象尺寸与跨周期可见性的底层封装
在 MT5 自定义图形对象类里,XSize 与 YSize 是两个只读取值器,直接调 ObjectGetInteger 抓 OBJPROP_XSIZE / OBJPROP_YSIZE,返回 int 类型像素值。这两个方法对 OBJ_LABEL、OBJ_BUTTON、OBJ_RECTANGLE_LABEL 等带尺寸的控件都有效,写面板时拿它做自适应布局比硬编常数稳。 SetYSize 的写法是先 ResetLastError,再 ObjectSetInteger 写 OBJPROP_YSIZE,失败就通过 CMessage::ToLog 把错误码丢进日志并返回 false,成功返回 true。注意它只负责写属性,不回读校验,若图表对象被手动删了,调用可能静默失败,得靠日志排查。 SetVisibleFlag 用 OBJ_ALL_PERIODS 和 OBJ_NO_PERIODS 两个宏切换对象在所有周期显隐。only_prop 参数为 true 时跳过实际 ObjectSetInteger 调用,只改类内 m_visible 标记——这在进行批量状态同步、暂不落盘时很有用。外汇与贵金属图表上过度绘制跨周期对象会拖慢终端,实盘前建议在模拟盘测一下对象数量上限。
class="kw">virtual class="type">int XSize(class="type">void) const { class="kw">return (class="type">int)::ObjectGetInteger(this.m_chart_id,this.m_name,OBJPROP_XSIZE); } class="kw">virtual class="type">bool SetYSize(const class="type">long value) { ::ResetLastError(); if(!::ObjectSetInteger(this.m_chart_id,this.m_name,OBJPROP_YSIZE,value)) { CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; } class="kw">return true; } class="kw">virtual class="type">int YSize(class="type">void) const { class="kw">return (class="type">int)::ObjectGetInteger(this.m_chart_id,this.m_name,OBJPROP_YSIZE); } class=class="str">"cmt">//--- Set object visibility on all timeframes class="type">bool SetVisibleFlag(const class="type">bool flag,const class="type">bool only_prop) { class="type">long value=(flag ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS); ::ResetLastError(); if((!only_prop && ::ObjectSetInteger(this.m_chart_id,this.m_name,OBJPROP_TIMEFRAMES,value)) || only_prop) { this.m_visible=flag;