DoEasy 函数库中的图形(第九十五部分):复合图形对象控件(基础篇)
📘

DoEasy 函数库中的图形(第九十五部分):复合图形对象控件(基础篇)

第 1/3 篇

◍ DoEasy 里的复合图形控件怎么搭

DoEasy 函数库在 2022 年 4 月 13 日发布的第九十五部分里,把图形对象工具箱类做了扩展,重点放到了复合图形对象控件上。这类控件不是单个箭头或标签,而是把多个标准图形对象打包成一个可复用的交互单元,方便在 MT5 图表上做自定义面板和按钮组。 原文示例显示该篇发布当天有 1016 次查看、0 条评论,说明这类底层库改造面向的是自己写 EA 和指标的老手,而不是看热闹的用户。 在 MT5 里验证路径很简单:打开 MetaEditor,把 DoEasy 库更新到对应版本,搜 Graph 相关的复合对象类,直接拖一个控件到图表看渲染。外汇和贵金属品种上跑这类自定义图形,要注意点差和滑点带来的重绘延迟,属于高风险环境下的可视化改造。

「复合图形对象的窗体附着机制」

在 MQL5 函数库里,复合图形对象由多个标准对象组合而成,父对象移动时必须带着附属对象同步调整相对坐标。上一篇文章已经处理了复合对象的移除和重定位雏形,这一节先偏离一点,把图表上复合对象的‘变更事件’接管过来,并引入管理这些对象的控件。 核心思路是用鼠标把从属对象拖到基准对象上完成附着:当拖动对象进入基准对象某个定位点一定像素距离内,就激活附着机制,并用一条连线直观提示‘可以绑定了’。每个定位点都要挂一个确定尺寸的窗体对象,只有进入这个窗体区域才触发附着,调试时才能沿边缘画矩形看清区域大小。 窗体平时不可见,只在鼠标悬停其活动区域时才显示图形对象的定位点(蓝点圆圈)。悬停即出标签,拖动窗体则对应轴点跟随光标,连带复合对象一起改。若按住鼠标把另一对象拖进窗体区域,就(经校验后)启动对象互绑。 这一节暂不真正连接两个对象,只先建窗体、贴到定位点,并实现图表重定位或缩放比例变化时窗体随轴点坐标用屏幕像素移动的机制——因为窗体坐标是像素,而多数图形对象用的是时间/价格值,二者换算必须实时跟上。

给图形库宏和消息索引动刀

在 \MQL5\Include\DoEasy\ 的 Data.mqh 里补了一组新消息索引,专门描述扩展图形对象(如基准对象衍生的枢轴点控件)的异常状态:X/Y 轴无枢轴点、未挂载到基准对象、创建数据点失败等。同步在消息文本数组里写进英俄双语串,保证日志输出能直接看懂而不是一堆数字码。 Defines.mqh 的旧宏名全部重命名。最典型的坑是我曾用 CLR_DEFAULT 想设透明画布背景,结果它实际是 0xFF000000(不透明黑),调了很久才反应过来。新宏 CLR_MW_DEFAULT 同样映射到 0xFF000000,NULL_COLOR 改为 CLR_CANV_NULL(0x00FFFFFF,带 alpha 通道的空画布),语义一眼明了,能少犯低级错误。 全局替换不用手动改:MT5 编辑器里按 Ctrl+Shift+H,勾选“在文件中替换”,把旧名批量换成新名即可,库内所有文件一次扫完。 另外把 Standard\ 目录下所有基准图形子类的“日志打印对象简述”方法补了一个逗号,纯属排版装饰,不影响逻辑,但让结构体初始化列表对齐更干净。下面是被改动的索引与宏定义片段。

MQL5 / C++
class=class="str">"cmt">//--- CLinkedPivotPoint
  MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_X,                 class=class="str">"cmt">// Not a single pivot point is set for the object along the X axis
  MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_Y,                 class=class="str">"cmt">// Not a single pivot point is set for the object along the Y axis
  MSG_GRAPH_OBJ_EXT_NOT_ATACHED_TO_BASE,              class=class="str">"cmt">// The object is not attached to the basic graphical object
  MSG_GRAPH_OBJ_EXT_FAILED_CREATE_PP_DATA_OBJ,        class=class="str">"cmt">// Failed to create a data object for the X and Y pivot points
  MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_X,             class=class="str">"cmt">// Number of base object pivot points for calculating the X coordinate: 
  MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_Y,             class=class="str">"cmt">// Number of base object pivot points for calculating the Y coordinate: 
class=class="str">"cmt">//--- CGStdGraphObjExtToolkit
  MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA,      class=class="str">"cmt">// Failed to change the size of the pivot point time data array
  MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA,     class=class="str">"cmt">// Failed to change the size of the pivot point price data array
  MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM,    class=class="str">"cmt">// Failed to create a form object to manage a pivot point

  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- CLinkedPivotPoint
  {"Для объекта не установлено ни одной опорной точки по оси X","The object does not have any pivot points set along the x-axis"},
  {"Для объекта не установлено ни одной опорной точки по оси Y","The object does not have any pivot points set along the y-axis"},
  {"Объект не привязан к базовому графическому объекту","The object is not attached to the base graphical object"},
  {"Не удалось создать объект данных опорной точки X и Y.","Failed to create X and Y reference point data object"},
  {"Количество опорных точек базового объекта для расчёта координаты X: ","Number of reference points of the base object to set the X coordinate: "},
  {"Количество опорных точек базового объекта для расчёта координаты Y: ","Number of reference points of the base object to set the Y coordinate: "},

class=class="str">"cmt">//--- CGStdGraphObjExtToolkit
  {"Не удалось изменить размер массива данных времени опорной точки","Failed to resize pivot point time data array"},
  {"Не удалось изменить размер массива данных цены опорной точки","Failed to resize pivot point price data array"},
  {"Не удалось создать объект-форму для контроля опорной точки","Failed to create form object to control pivot point"},

  };
class=class="str">"cmt">//+---------------------------------------------------------------------+
class="macro">#define CLR_DEFAULT(0xFF000000)        class=class="str">"cmt">// Default symbol background class="type">color in the navigator
class="macro">#define CLR_MW_DEFAULT(0xFF000000)        class=class="str">"cmt">// Default symbol background class="type">color in the Market Watch
class="macro">#define NULL_COLOR(0x00FFFFFF)        class=class="str">"cmt">// Zero for the canvas with the alpha channel

◍ 图形对象控件与日志输出的底层约定

在 MT5 自定义图形对象库里,画布透明色与控件尺寸都是靠宏写死的。CLR_CANV_NULL 取 0x00FFFFFF,即 RGB 全白但 alpha 为 0,专用于带 alpha 通道的画布底色清零;PROGRAM_OBJ_MAX_ID 给到 10000,意味着单个程序挂载的图形对象 ID 上限是一万,超了就得自己分流管理。 控件交互区的两个尺寸直接决定鼠标命中范围:CTRL_POINT_SIZE 为 5,是枢轴控制点的半径(像素);CTRL_FORM_SIZE 为 40,是包裹该控制点的表单边长。这两值在 4K 屏和高 DPI 下若不改,控制点会显得偏小,可酌情放大再编译。 CGStdArrowBuyObj::PrintShort 负责往专家日志丢一行简讯。它拼了对象头、Name()、GRAPH_OBJ_PROP_ID 属性值和创建时间(精确到秒),dash 与 symbol 开关控制是否前缀“ - ”及是否带品种名。实盘跑 EA 时,用这个方法快速确认箭头类对象是否按预期 ID 落地,比全局 ObjectGetInteger 翻遍图表更高效。

MQL5 / C++
class="macro">#define CLR_CANV_NULL(0x00FFFFFF)     class=class="str">"cmt">// Zero for the canvas with the alpha channel
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_SIZE(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_FORM_SIZE(class="num">40)               class=class="str">"cmt">// Size of the control point form for managing graphical object pivot points
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Enumerations                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display a class="type">short description of the object in the journal          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGStdArrowBuyObj::PrintShort(const class="type">bool dash=false,const class="type">bool symbol=false)
  {
   ::Print(
      (dash ? " - " : "")+this.Header(symbol)," \"",CGBaseObj::Name(),"\": ID ",(class="type">class="kw">string)this.GetProperty(GRAPH_OBJ_PROP_ID,class="num">0),
      ", ",::TimeToString(CGBaseObj::TimeCreate(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)
     );
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「扩展标准图形对象工具箱类」

我们启动创建一款处理扩展图形对象的工具包。 这将是一个拥有创建窗体对象,并操控它们的所有必要方法的类。 每个扩展图形对象都有一个指向相应类对象的指针。 如有必要(如果这是复合图形对象中的基准对象),则在创建扩展对象时动态创建类对象,并在删除后者时将其删除。 对象接收基准图形对象的必要参数(其坐标、类型、名称、等等)。 跟踪基准对象坐标,并在对象内部调整窗体对象坐标。 最终,窗体对象将启用基准对象的管理。 但首先要做的是... 在 \MQL5\Include\DoEasy\Objects\ Graph\ 里,创建新的 Extend\ 文件夹,包含 CGStdGraphObjExtToolkit.mqh 文件,内有 CGStdGraphObjExtToolkit 类, 继承 自基准 CObject 类,可用于构建 MQL5 标准函数库 : 类文件中 应包含 窗体对象 类文件 。 在类的私密部分,声明存储 全部所需基准对象属性的变量 , 构造窗体的属性 , 存储它们的列表 ,以及 创建窗体对象 、 并返回其屏幕坐标 的方法: 我们用数组来存储价格和时间坐标,因为单一的图形对象可能有多个轴点。 因此,每个轴点的坐标将存储在相应的数组单元之中。 第一个轴点坐标 — 位于数组索引 0,第二个轴点坐标 — 位于数组索引 1,第三个轴点坐标 — 位于索引 2,等等。 平移窗体坐标允许我们把窗体精确地定位在对象轴点的中心。 这个平移覆盖了轴点尺寸的一半。 如果窗体尺寸是 2 的倍数,例如 10,则需加 1 进行调整,即 11。 如此即可将窗体精确地放置在图形对象轴点的中心,哪怕只有一个像素,且这样就不会有哪一侧比之另一侧更宽。 窗体列表用于存储所有创建的窗体。 若要访问它们,可由指针授权。 通过计算屏幕坐标的方法,我们可以知道窗体所处的屏幕坐标。 这样可以将其精确放置在图形对象轴点上。 在类的公开部分,声明处理类的所有必要方法: 在类构造函数中, 清除窗体对象列表 , 为类变量设置所有必要的基本对象值(传入构造函数参数) ,并 创建窗体对象 ,以便可根据基准图形对象的每个轴点管理基准对象。 该方法设置复合图形对象的基准对象参数: 该方法接收基准图形对象属性的所有必要值。 接下来, 检查基准对象类型。 如果这是一个非基于价格/时间坐标的对象,则退出该方法 。 我们不必处理这样的对象。 接下来, 检查传递给该方法的基准对象坐标数组的大小。 如果它们的大小为零(该方法已收到一个空数组),则发送通知,并退出该方法。 接下来,根据传递的数组修改坐标内部数组的大小。 如果更改数组失败,则发通知,并退出 。 在结尾,简单地 把输入数组逐元素复制到内部数组即可 。 该方法设置管理轴点的参考点大小: 该方法接收所需窗体的大小。 如果大小超过 254,则将其设置为 255 (奇数值), 如果传递的大小小于 5,则将其设置为 5 (这应是最小的窗体大小值)。 否则,如果传递的大小等于 2,则为其加一,再用它 。 如果检查的数值均为空,则使用传递给该方法的大小 。 接下来,因为应该设置窗体,故计算坐标偏移,以便图形对象的轴点精确位于其中心。 为达此目标,我们应该从坐标值中减去偏移值。 将计算窗体大小分成两个 , 取最接近的较高整数值 ,并加一 。 该方法设置基准对象的时间坐标: 该方法接收轴点时间和对象轴点索引。 如果索引超过了对象的轴点数量,则发送通知“请求超出数组范围”,并退出 。 结果则是, 传递给方法的时间值,会设置到与时间数组中的索引对应的单元格里 。 当对象发生变化时,该方法

把基准图形对象的锚点塞进类里

扩展标准图形对象工具类的核心,是先固化一个「基准对象」的全部坐标上下文。下面这段私有成员定义,就是给后续拖拽、重绘提供数据源的底子。 m_base_chart_id 存图表 ID,m_base_subwindow 记子窗口序号,m_base_type 用 ENUM_OBJECT 锁住对象类型,m_base_name 则是对象名字符串。这四样缺一个,运行时都找不到你要操作的那条线。 参考点数组用了 m_base_time[] 与 m_base_price[] 两个动态数组,长度由 m_base_pivots 控制——比如趋势线是 2 个点,斐波那契工具可能是 3 个以上。屏幕坐标侧另有 m_base_x / m_base_y 整数对,供像素级定位。 m_ctrl_form_size 决定管理参考点用的表单尺寸,m_shift 做表单位置的偏移补偿。外汇与贵金属图表高波动,锚点捕捉偏差 1~2 像素都可能让止损线画错,建议在 MT5 里把这两个值先写死再逐步放开。

MQL5 / C++
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property strict     class=class="str">"cmt">// Necessary for mql4
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Include files                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "..\..\Graph\Form.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Extended standard graphical                                      |
class=class="str">"cmt">//| object toolkit class                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CGStdGraphObjExtToolkit : class="kw">public CObject
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Extended standard graphical                                      |
class=class="str">"cmt">//| object toolkit class                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CGStdGraphObjExtToolkit : class="kw">public CObject
  {
class="kw">private:
   class="type">long            m_base_chart_id;          class=class="str">"cmt">// Base graphical object chart ID
   class="type">int             m_base_subwindow;         class=class="str">"cmt">// Base graphical object chart subwindow
   ENUM_OBJECT     m_base_type;              class=class="str">"cmt">// Base object type
   class="type">class="kw">string          m_base_name;              class=class="str">"cmt">// Base object name
   class="type">int             m_base_pivots;            class=class="str">"cmt">// Number of base object reference points
   class="type">class="kw">datetime        m_base_time[];            class=class="str">"cmt">// Time array of base object reference points
   class="type">class="kw">double          m_base_price[];           class=class="str">"cmt">// Price array of base object reference points
   class="type">int             m_base_x;                 class=class="str">"cmt">// Base object X coordinate
   class="type">int             m_base_y;                 class=class="str">"cmt">// Base object Y coordinate
   class="type">int             m_ctrl_form_size;         class=class="str">"cmt">// Size of forms for managing reference points
   class="type">int             m_shift;                  class=class="str">"cmt">// Shift coordinates for adjusting the form location
  };

常见问题

先建窗体对象并调用附着方法把控件挂到图表窗口,再依次初始化子对象;缺一步就会在日志里抛空指针,按基础篇顺序来最稳。
必须在创建后显式绑定到目标图表窗口坐标,否则控件只存在内存里不显示;调试时看日志有没有“附着失败”字样。
可以,把代码贴给小布,它能按基础篇的窗体附着和锚点约定帮你指出缺了哪一步、宏定义是否漏改。
会,新增索引若和旧的有重叠会导致消息分发错乱;动刀前备份原宏表,按文中偏移量往后加最安全。
锚点内化后控件缩放和拖拽不用外部重算坐标,代码少写一半;回测同屏多控件时帧率也明显更稳。