DoEasy 函数库中的图形(第九十八部分):移动扩展的标准图形对象的轴点(基础篇)
◍ DoEasy 里移动扩展图形的轴点怎么抓
DoEasy 函数库做到第九十八部分时,重点落在移动扩展(moving expansion)这一类标准图形对象的轴点处理上。移动扩展在 MT5 里常用于标出一段行情的潜在扩展位,但它的轴点并不像趋势线那样直接暴露在对象属性面板,需要走库里的图形类接口才能稳定取到。 原文示例发布于 2022 年 5 月 9 日,在 MetaTrader 5 环境下演示,发布时页面显示浏览量 1252、评论 0,说明这套接口改动偏底层、受众主要是改库的人而非普通跟单用户。 做验证最直接:开 MT5 加载 DoEasy 对应版本,新建一个移动扩展对象,用库里的轴点获取方法打印三个锚点坐标,看是否和手动拖出的节点一致。外汇与贵金属杠杆高、滑点大,图形锚点只是辅助定位,别拿它当进场唯一依据。
「趋势线轴点控件的透明窗体交互逻辑」
在扩展标准图形对象的开发里,复合图形对象以普通趋势线为原型,末端挂了额外价格标签。其轴点控件本质是一个基于 CCanvas 改进而来的窗体对象,平时完全透明不可见,只在光标逼近轴心点一定距离、进入透明窗体区域时才浮出带圆圈的点。 窗体尺寸大于活动区域。活动区域负责拖拽——抓住它移动,依附在里面的趋势线端点会同步位移;窗体其余区域则可安排别的互动,比如右键呼出关联菜单。这样一来,光标在活动区内能拖动并呼菜单,在活动区外只能触发菜单,互不打架。 这套机制目前只是复合图形对象新功能的验证原型。等创建和处理窗体所需的工具齐备,才会沉淀出一组标准库复合对象供自定义调用。当下每一步都可当作“按原样”复用的底子,省去从头造轮子。外汇与贵金属图表交互改造属高风险实验,参数距离阈值需你在 MT5 里实测。
改进库类
打开 \MQL5\Include\DoEasy\ Defines.mqh ,并实现一些修改。 轴心点控件窗体包含一个点和一个圆圈。 所应用颜色之前已在代码中正确指定。 我们 添加含有默认颜色的宏替换 : 将 CTRL_POINT_SIZE 宏替换 重命名为 CTRL_POINT_RADIUS ,因为这不是圆圈的完整尺寸,而是圆圈的半径。 在计算窗体对象的活动区域时,宏替换名称有点误导。 在图形元素对象类的文件 \MQL5\Include\DoEasy\Objects\Graph\ GCnvElement.mqh 中,稍微改进创建图形元素对象的方法。 不幸的是,在调用 CCanvas 类的 CreateBitmapLabel() 方法时,没有返回错误代码。 因此, 在调用方法之前重置最后一个错误代码 。 如果无法创建图形标签, 在日志中会显示该错误代码。 这略微改善了调试。 我为什么要这么做? 在创建窗体对象时,我花了很多时间试图理解为什么我无法创建图形资源。 最后,事实证明一个图形资源的创建名称超过了 63 个字符。 如果 创建 CCanvas 类的图形标签的方法 报告了错误,我们就不必搜索其它任何内容。 取而代之,我们会立即得到错误代码,我不必遍历所有循环调用不同类中的各种方法。 不过,如果资源的名称长度超过 63 个字符,这种改进将毫无帮助: 返回错误代码 但这依旧是较好的,因为这立刻引发了一个问题:“为什么没有创建图形资源?”... 我们来改进 \MQL5\Include\DoEasy\Objects\Graph\Extend\ CGStdGraphObjExtToolkit.mqh 文件中的扩展标准图形对象工具箱类。 每个图形对象都有一个或多个轴点,用于在图表上定位对象。 每个轴点上都会附着一个窗体对象。 标准图形对象有自己的重新定位点。 它们在选择图形对象时出现。 我们不打算在函数库中管理扩展的标准图形对象。 为了实现该功能,我们用窗体对象来移动图形对象的轴心点更便捷。 这样的窗体对象也许比控制点更多。 因此,我们不能通过图形对象的轴点数量来定义窗体对象的数量。 然而,我们需要知道这个数量。 因此,在类的公开部分, 添加返回附着于所管理轴点上的已创建窗体对象数量的方法 ,并 声明在完全透明的窗体对象上绘制所管理轴点的方法 : 在基于对象轴心点创建窗体对象的方法中, 截短已创建对象的名称 — 替换 "_TKPP_" 为 "_CP_" : 在此(以及在测试 EA 文件中),我必须缩短所创建窗体对象的名称,因为图形资源的名称超过了 63 个字符,故平台没有创建任何对象。 原因在于 CCanvas 类中的动态资源创建方法,其中 所创建资源的名称由 “::“ 符号 + 传递给方法的名称(并在上述方法中指定)+ 图表 ID + 自系统启动以来经历的毫秒数 + 伪随机数 : 不幸的是,所有这些都严重限制了为所创建对象选择一个可理解的名称。 在基准对象轴点上创建窗体对象的方法中, 计算窗体对象每侧的缩进量 ,以便指定位于窗体中心的活动区域的位置和大小,而其大小应等于 CTRL_POINT_RADIUS 宏替换中设置的两个值。 由于我们要处理半径,我们需要用到两个半径值,从窗体宽度(窗体高度与宽度相等)中减去它们,然后将获得的值除以 2,这样窗体活动区域就等于在其中心绘制的圆圈 在 SetActiveAreaShift() 方法中指定从窗体边缘开始的活动区域边框缩进 : 创建窗体时,我们可以绘制矩形,显示窗体的大小及其活动区域,以便调试 — 这些代码已被
◍ 盯盘面板的底层常量与画布参数
在 MT5 里做自定义盯盘面板,第一步往往是先锁死一批宏常量,避免运行期反复调参把内存吃爆。下面这组定义直接决定了 tick 序列和 DOM 快照的存储上限:单品种 tick 数据默认只抓 1 天、总量封顶 200000 条;深度报价的快照序列同样默认 1 天、上限 200000 帧。 画布刷新节奏由 PAUSE_FOR_CANV_UPDATE 控制,设为 16 毫秒,意味着理论最高约 62.5 帧/秒的重绘频率,实际受主线程负载影响会更低。CLR_CANV_NULL 用 0x00FFFFFF 表示带 alpha 通道的透明底色,OUTER_AREA_SIZE 16 像素则是表单工作区外围留白。 图形对象这边,PROGRAM_OBJ_MAX_ID 给到 10000,足够挂满一屏辅助线;CTRL_POINT_RADIUS 5 像素、CTRL_POINT_COLOR 用 clrDodgerBlue、CTRL_FORM_SIZE 40 像素,三者合力定义了拖拽锚点的命中范围和视觉样式。开 MT5 把这几行抄进头文件,改改颜色就能看出面板交互手感差异。
class="macro">#define TICKSERIES_DEFAULT_DAYS_COUNT(class="num">1) class=class="str">"cmt">// Required number of days for tick data in class="kw">default series class="macro">#define TICKSERIES_MAX_DATA_TOTAL(class="num">200000) class=class="str">"cmt">// Maximum number of stored tick data of a single symbol class=class="str">"cmt">//--- Parameters of the DOM snapshot series class="macro">#define MBOOKSERIES_DEFAULT_DAYS_COUNT(class="num">1) class=class="str">"cmt">// The class="kw">default required number of days for DOM snapshots in the series class="macro">#define MBOOKSERIES_MAX_DATA_TOTAL(class="num">200000) class=class="str">"cmt">// Maximum number of stored DOM snapshots of a single symbol class=class="str">"cmt">//--- Canvas parameters class="macro">#define PAUSE_FOR_CANV_UPDATE(class="num">16) class=class="str">"cmt">// Canvas update frequency class="macro">#define CLR_CANV_NULL(0x00FFFFFF) class=class="str">"cmt">// Zero for the canvas with the alpha channel class="macro">#define OUTER_AREA_SIZE(class="num">16) class=class="str">"cmt">// Size of one side of the outer area around the form workspace class=class="str">"cmt">//--- Graphical object parameters class="macro">#define PROGRAM_OBJ_MAX_ID(class="num">10000) class=class="str">"cmt">// Maximum value of an ID of a graphical object belonging to a program class="macro">#define CTRL_POINT_RADIUS(class="num">5) class=class="str">"cmt">// Radius of the control point on the form for managing graphical object pivot points class="macro">#define CTRL_POINT_COLOR(clrDodgerBlue) class=class="str">"cmt">// Radius of the control point on the form for managing graphical object pivot points class="macro">#define CTRL_FORM_SIZE(class="num">40) class=class="str">"cmt">// Size of the control point form for managing graphical object pivot points
「画布标签创建与资源报错边界」
在 MT5 里用 CCanvas 做图形面板时,CreateBitmapLabel 的入参顺序直接决定标签能否挂上指定子窗口:name 是元素名(长度受限于底层资源名规则),x/y 是相对坐标,w/h 是像素尺寸,colour 与 opacity 控制底色和透明度,redraw 默认 false 表示不立即重绘。 下面这段是创建位图标签方法的核心骨架,建议直接拷进 MT5 的包含类里验证编译: 函数先调用 ::ResetLastError() 清掉旧错误码,再尝试在 chart_id 指定的图表、wnd_num 子窗口创建位图标签。成功后会擦成透明底、更新画布,并把图表子窗口的 Y 距离存进 m_shift_y 供后续定位;失败则写日志并返回 false。 注意两个资源类报错码是实打实的坑:ERR_RESOURCE_NAME_IS_TOO_LONG = 4018,资源名超过 63 字符会直接废;ERR_RESOURCE_NOT_FOUND = 4016,EX5 里找不到对应资源名也会建不出标签。外汇与贵金属图表叠加自绘控件属于高风险操作,参数错配可能导致面板不显示或占用额外资源。 控制点表单的尺寸用 SetControlFormSize 设定、GetControlFormSize 读取,而按索引取表单指针用 m_list_forms.At(index)——这套接口在写枢轴点管理 UI 时基本绕不开。
const class="type">class="kw">string name, class=class="str">"cmt">// Element name const class="type">int x, class=class="str">"cmt">// X coordinate const class="type">int y, class=class="str">"cmt">// Y coordinate const class="type">int w, class=class="str">"cmt">// Width const class="type">int h, class=class="str">"cmt">// Height const class="type">color colour, class=class="str">"cmt">// Background class="type">color const class="type">uchar opacity, class=class="str">"cmt">// Opacity const class="type">bool redraw=false) class=class="str">"cmt">// Flag indicating the need to redraw { ::ResetLastError(); if(this.m_canvas.CreateBitmapLabel(chart_id,wnd_num,name,x,y,w,h,COLOR_FORMAT_ARGB_NORMALIZE)) { this.Erase(CLR_CANV_NULL); this.m_canvas.Update(redraw); this.m_shift_y=(class="type">int)::ChartGetInteger(chart_id,CHART_WINDOW_YDISTANCE,wnd_num); class="kw">return true; } CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ ERR_RESOURCE_NAME_IS_TOO_LONG class="num">4018 The resource name exceeds class="num">63 characters ERR_RESOURCE_NOT_FOUND class="num">4016 Resource with such a name is not found in EX5 class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the size of the form of pivot point management control points class="type">void SetControlFormSize(const class="type">int size); class="type">int GetControlFormSize(class="type">void) const { class="kw">return this.m_ctrl_form_size; } class=class="str">"cmt">//--- Return the pointer to the pivot point form by(class="num">1) index and(class="num">2) name CForm *GetControlPointForm(const class="type">int index) { class="kw">return this.m_list_forms.At(index); }
用代码接管基准对象的控制点表单
在 MT5 自定义图形工具里,基准对象(如趋势线、通道)的锚点往往要靠手动拖。上面这组接口把锚点抽象成「控制点表单(CForm)」,由程序批量生成和绘制,省掉反复手点。 GetNumPivotsBaseObj 返回基准对象本身的锚点总数(m_base_pivots),GetNumControlPointForms 返回已建表单数,即 m_list_forms.Total()。这两个整数直接决定你要循环建多少个可拖控件。 CreateNewControlPointForm 的命名规则很直白:name = 基准名 + "_CP_" + 序号,序号小于锚点数用数字,超出则标 "X"。它先查重(已存在同 index 的 form 就返回 NULL),再取锚点屏幕坐标 (x,y),最后以 x-m_shift、y-m_shift 为左上角、边长 GetControlFormSize() 生成 CForm。 CCanvas::Create 里 ArrayResize(m_pixels, width*height) 这一行是硬指标:画布像素数组必须等于宽乘高,width 和 height 任一 ≤0 或分配失败都会跳过资源生成。开 MT5 把这段贴进 EA 或脚本,改 m_shift 和 GetControlFormSize() 两个值,就能看到控件框相对锚点的偏移和大小变化。外汇与贵金属图表上做这类扩展工具,杠杆与滑点风险仍高,参数验证请在模拟盘先跑。
CForm *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const class="type">int index) { class="type">class="kw">string name=this.m_base_name+"_CP_"+(index<this.m_base_pivots ? (class="type">class="kw">string)index : "X"); CForm *form=this.GetControlPointForm(index); if(form!=NULL) class="kw">return NULL; class="type">int x=class="num">0, y=class="num">0; if(!this.GetControlPointCoordXY(index,x,y)) class="kw">return NULL; class="kw">return new CForm(this.m_base_chart_id,this.m_base_subwindow,name,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize()); } class="type">bool CCanvas::Create(const class="type">class="kw">string name,const class="type">int width,const class="type">int height,ENUM_COLOR_FORMAT clrfmt) { Destroy(); if(width>class="num">0 && height>class="num">0 && ArrayResize(m_pixels,width*height)>class="num">0) { class=class="str">"cmt">//--- generate resource name