DoEasy 函数库中的图形(第九十九部分):依据单个控制点移动扩展图形对象(基础篇)
📘

DoEasy 函数库中的图形(第九十九部分):依据单个控制点移动扩展图形对象(基础篇)

第 1/3 篇

用单控制点拖动扩展图形对象

在 MT5 里扩展图形对象(如自定义多边形、通道)默认不支持只拖一个控制点就整体跟随移动,DoEasy 库这第九十九部分补上了这个交互缺口。 原文示例发布于 2022 年 5 月 24 日 09:29,截至文档统计获 1,247 次查看、2 条评论,说明这类底层图形控制需求在实战用户里并不冷门。 打开 MT5 后,把库里扩展对象绑定到鼠标事件,只抓单个控制点的 CHARTEVENT_OBJECT_DRAG,就能让其余锚点按相对偏移重算——不用重写整个对象结构。外汇与贵金属图表上拖拽验证时需注意点差滑点带来的高风险,图形仅作参考。

◍ 给复合图形对象加一个整体移动中心点

前文已打通控件窗体拖动来改图形对象轴点的能力,但还差一步:把图形对象当整体搬动。标准对象拖中心就能整移,这里也要做一个单一中心点,拖它即可平移整个对象而不动轴点。 测试我挑了由趋势线加末端价格标签组成的复合对象,它有两个轴点管端点重定位、一个中心点管整体平移。后续再扩展到三个以上控制点的对象,窗体结构保持一致。 ChartTimePriceToXY() 只返回图表可视区坐标,越界一律给 0。也就是说,复合对象左端拖出屏幕左边框时,左枢轴 X 坐标被锁死在像素 0,对象直接变形;右、上、下边界同理。外汇/贵金属图表高波动,这种变形会误导读图。 为防撞框失真,我给复合对象加了可视区边界限制:任意边缘不允许移出屏幕边框。这样拖动时整体平移但不变形,逻辑也更稳。 顺带把轴点屏幕坐标计算拆成独立方法,主模块不再堆大段中间运算,读调用比读嵌套容易得多。本文只讲从规划到落地的过程,并非所有实现都一次如预期。

「给管控点窗体单独开一种对象类型」

DoEasy 函数库里管理扩展图形对象轴点的窗体,原本没被收进图形对象集合,调试时很难判断当前活动对象是谁。直接在 Defines.mqh 的 ENUM_OBJECT_DE_TYPE 里补一个 OBJECT_DE_TYPE_GFORM_CONTROL,就能用类型名精准识别这类窗体。 类型定了之后,Data.mqh 要补对应的消息索引和文案。重点防一个坑:ChartTimePriceToXY() 在 ChartWnd 类里也可能转换失败,所以在 TimePriceToXY() 方法里先打“未能将时间/价格坐标转换为屏幕坐标”,再跟错误描述和错误代码,能把逻辑错和坐标错快速拆开。 CGStdGraphObjExtToolkit.mqh 里新建的类继承自窗体对象类,两个私有变量很关键:m_drawn 记“已在窗体画点”的标志,避免鼠标移出活动区就无脑重画所有窗体;m_pivot_point 存管理的轴点索引,比如趋势线 0/1 是端点、2 是中心,多出来的一个索引专门管整体移动。 公开方法就是这对变量的 getter/setter,构造函数里默认型填 OBJECT_DE_TYPE_GFORM_CONTROL,参数型把图形对象轴点索引一并传给父类。之后把工具箱里所有 CForm 管控窗体替换成 CFormControl,GetControlPointCoordXY() 也改了——用 switch 按对象类型算坐标,索引小于轴点总数取轴点,否则取中心,目前只实装了“两边缘+一中心”的对象。 循环建窗体时数量比轴点多半個,多出的最后一个就是中心移动窗体。画点逻辑也重排了:悬停才画点,移开本应删点,但窗体一重叠就会连环触发,所以改成“在悬停窗体画点、同时清掉同对象其余窗体残留点”的方法。 GStdGraphObj.mqh 把重复代码抽成独立方法,删控制点前先判“不透明度为零且 m_drawn 仍置位”,不再每帧重绘。GraphElementsCollection.mqh 则建了存 X/Y 和中心偏移的结构数组,事件处理里依据中心窗体偏移整体拖对象,并调用“画点+清残留”避免重叠窗体都亮。 外汇/贵金属图表上挂这类扩展对象,MT5 里改完重编译测试,注意坐标转换失败多在极端缩放或离线图表,属高频但低损的异常。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| List of library object types                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_OBJECT_DE_TYPE
  {
class=class="str">"cmt">//--- Graphics
   OBJECT_DE_TYPE_GBASE =  COLLECTION_ID_LIST_END+class="num">1,      class=class="str">"cmt">// "Base object of all library graphical objects" object type
   OBJECT_DE_TYPE_GELEMENT,                               class=class="str">"cmt">// "Graphical element" object type
   OBJECT_DE_TYPE_GFORM,                                  class=class="str">"cmt">// Form object type

图形对象坐标转换的枚举与容错

在 MT5 的图表对象体系里,OBJECT_DE_TYPE 系列枚举覆盖了从枢轴点表单(OBJECT_DE_TYPE_GFORM_CONTROL)到阴影(OBJECT_DE_TYPE_GSHADOW),再到逐帧动画(OBJECT_DE_TYPE_GFRAME / GFRAME_TEXT / GFRAME_QUAD / GFRAME_GEOMETRY)及动画集合(OBJECT_DE_TYPE_GANIMATIONS)的控制类型。这些常量直接决定 ObjectCreate 时引擎如何解析交互与渲染层级。 系统消息枚举里有一组越界请求标识:MSG_LIB_SYS_REQUEST_OUTSIDE_LONG_ARRAY、_DOUBLE_ARRAY、_STRING_ARRAY 以及通用的 MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY,对应数组访问越界时的日志文案。 真正容易踩坑的是坐标转换失败。MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY 与 MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY 表示图形对象坐标或时间/价格坐标无法映射到屏幕 XY。外汇与贵金属波动大、图表重绘频繁,这类转换失败概率会随行情跳动上升,属高风险环境下的常见异常。 下面这段 CChartWnd::TimePriceToXY 展示了标准容错写法:先 ResetLastError 清状态,再调 ChartTimePriceToXY,失败立刻写日志。你可以直接抄进自己的面板类做坐标调试。

MQL5 / C++
class="type">bool CChartWnd::TimePriceToXY(class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
  {
   ::ResetLastError();
   if(!::ChartTimePriceToXY(this.m_chart_id,this.WindowNum(),time,price,this.m_wnd_coord_x,this.m_wnd_coord_y))
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY);

◍ 图形对象控制点的表单封装

在 MT5 自定义指标里管理图形对象的锚点,最麻烦的是跟踪「哪个控制点已经画在表单上」。上面这段把状态收进 CFormControl 类,用 m_drawn 标记绘制状态,用 m_pivot_point 记录当前表单接管的具体锚点序号。 IsControlAlreadyDrawn 直接返回 m_drawn,调用方在重绘前先查这个标志,能避免同一锚点被重复添加到表单导致坐标错乱。SetControlPointDrawnFlag 接收外部 bool 来翻转标记,通常在鼠标拖拽结束或对象创建回调里写 true。 GraphObjPivotPoint 返回受管锚点编号,配合前面两个方法,你可以在 OnChartEvent 里用「编号 + 已绘标记」做精确分流:只处理本表单负责的那个点。外汇和贵金属图表上这类交互对象受 tick 频率影响,重绘逻辑写错可能在高波动时卡顿,建议开 MT5 用样例类挂一个趋势线实测拖拽响应。

MQL5 / C++
class CFormControl : class="kw">public CForm
  {
class="kw">private:
   class="type">bool               m_drawn;            class=class="str">"cmt">// Flag indicating that the pivot point is drawn on the form
   class="type">int                m_pivot_point;      class=class="str">"cmt">// Pivot point managed by the form
class="kw">public:
class=class="str">"cmt">//--- (class="num">1) Return and(class="num">2) set the drawn point flag
   class="type">bool               IsControlAlreadyDrawn(class="type">void)                class="kw">const { class="kw">return this.m_drawn;                    }
   class="type">void               SetControlPointDrawnFlag(class="kw">const class="type">bool flag)        { this.m_drawn=flag;                        }
class=class="str">"cmt">//--- (class="num">1) Return and(class="num">2) set the pivot point managed by the form
   class="type">int                GraphObjPivotPoint(class="type">void)                           class="kw">const { class="kw">return this.m_pivot_point;            }

「控件基类的锚点与扩展工具箱落地」

CFormControl 的带参构造函数把 pivot_point 作为显式入参传入,并在初始化列表里先调 CForm(chart_id,subwindow,name,x,y,w,h) 完成父级布局,再把 m_type 固定为 OBJECT_DE_TYPE_GFORM_CONTROL。这意味着每个表单控件在实例化时就绑定了基准锚点索引,后续重绘或响应鼠标时无需再查表。 CGStdGraphObjExtToolkit 继承 CObject,私有成员直接暴露了扩展标准图元所需的全部底座:m_base_chart_id(图表 ID)、m_base_subwindow(子窗口号)、m_base_type(ENUM_OBJECT 类型)、m_base_name(对象名)、m_base_pivots(基准对象参考点数量)。写 EA 时若要在已有趋势线或矩形上挂自定义手柄,这几个字段就是必须先填的上下文。 在 MT5 里新建一个继承自 CGStdGraphObjExtToolkit 的类,把 m_base_pivots 设成 2(例如趋势线两个端点),即可在 OnChartEvent 里用 CHARTEVENT_OBJECT_DRAG 捕获拖拽并回写坐标。外汇与贵金属图表波动剧烈,这类自定义控件在高波动时段可能漏掉快速拖动事件,建议先在模拟盘验证。

MQL5 / C++
class="type">void SetGraphObjPivotPoint(class="kw">const class="type">int index) { this.m_pivot_point=index; }
class=class="str">"cmt">//--- Constructor
 CFormControl(class="type">void) { this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL; }
 CFormControl(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int pivot_point,class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int w,class="kw">const class="type">int h) :
  CForm(chart_id,subwindow,name,x,y,w,h)
  {
   this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL;
   this.m_pivot_point=pivot_point;
  }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Extended standard graphical 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
};

常见问题

给复合图形对象单独设一个整体移动控制点,绑定到对象组的锚点,拖这个控制点即可整体平移,不用逐个改坐标。
单独类型能把控制点窗体和普通控件在事件与渲染上隔离,避免误触发和坐标串扰,拖拽更稳定。
可以,小布能扫描当前图表复合对象并高亮建议的控制点位置,你点一下就能用单点拖动整组图形。
防比例尺切换、子窗口坐标错位导致控制点算飞,容错能在转换失败时回退上一帧坐标,不丢对象。
锚点字段要带默认值且旧对象加载时补默认锚,扩展工具箱按版本号隔离新旧方法,避免老图读取越界。