DoEasy 函数库中的图形(第九十四部分):移动和删除复合图形对象(基础篇)
复合图形对象的拖拽与清除
DoEasy 函数库在 2022 年 4 月的迭代里,把复合图形对象(由多个基础图元组合的自定义对象)的移动与删除逻辑下沉到了库类层,调用方不再需要逐个遍历子对象做坐标偏移。 之前要在图表上拖动一组关联线条,得自己写循环去重设每个子对象的 anchor 点;现在库内部维护了复合对象的拓扑关系,移动时只改父容器坐标,子对象跟随刷新。 删除动作同理:对复合对象调一次销毁接口,库会递归清理所有派生图元,避免残留匿名对象占用图表资源。外汇与贵金属图表高频重绘场景下,这种封装能明显降低脚本卡顿概率,但 MT5 自定义对象本就存在多品种切换时状态不同步的高风险,验证前先在 demo 账户跑。
◍ 复合图形对象的坐标重算与防破坏机制
MQL5 里扩展标准图形对象构成复合体后,基准对象的坐标变化必须带动从属对象同步移动。原生 CHARTEVENT_OBJECT_DRAG 只在鼠标松开时触发一次,这意味着拖拽过程中若不额外跟踪鼠标按下和移动,从属对象会停在旧位置,直到松手才瞬间跳到新锚点。 要实时跟随,就得自己捕获鼠标按钮在基准对象锚点(或中心)按下的状态,并在移动过程中持续重算从属对象坐标;最终在 CHARTEVENT_OBJECT_DRAG 里固化基准坐标,再批量更新绑定对象。当前版本我直接禁用了从属对象的鼠标选择,删复合体只能选基准对象删,避免误拆。 风险点在 Ctrl+B 对象列表:用户仍能手动把某个绑定对象设为可选或直接删掉,复合体就会残缺。外汇与贵金属图表上跑这类脚本属于高风险操作,参数误设可能导致图形层错乱,建议先在模拟图表验证。 后续会补上“有意破坏”处理——任意绑定对象被删时,整体复合对象全部清除,而不是留半截。
「图形对象库的底层类名与绑定逻辑重整」
在 DoEasy 库的路径 \MQL5\Include\DoEasy\Objects\Graph\Standard\ 下,抽象标准图形对象类 GStdGraphObj.mqh 做了几处务实修改。所有衍生类在显示对象简述的方法里保留了未使用的输入参数,但新增了一个破折号标志:当传入 true 时,返回的短名前会加一个连字符,方便在题头下枚举对象名。 从属对象轴点数据类里,原本声明为 m_property_x[][2] 的坐标数组被改名为 m_property[][2]——作者实验后放弃分 X/Y 两个类各存数组的思路,但数组名残留不正确,这次顺手修正。该类公开方法现在能显示坐标轴名称、返回属性、返回属性修饰符,以及输出多个基准轴点描述用于调试。 复合对象轴点类把 CreateNewLinkedPivotPoint() 重命名为 CreateNewLinkedCord(),原因是原意混淆:轴点(pivot)只是基准对象 X 或 Y 的定位参考,可多个叠加算出一个坐标点(cord),方法实际是加坐标点而非加轴点。同时用三元运算符压缩了若干方法代码,逻辑不变。 绑定数据类给从属对象加了新的属性设置入口:取基准对象指定属性的 X 坐标(整数,时间或像素)设给从属对象;Y 坐标则需判断整数(像素)或实数(价格)分别赋值。新对象默认 selection=false 且类型标为“扩展标准图形对象”,鼠标不可选,只能按基准类型与名称编程选取。 消息索引里新增了两条删除失败提示与两条轴点数量提示(见代码高亮段),用于运行时排查从列表/图表删对象失败,以及输出计算 X/Y 所用的基准轴点数。趋势线有两个锚点,目前属性方法只能整列打印,作者预留了索引默认值,后续可删默认值并加错误处理来按锚点选择性显示。
class=class="str">"cmt">//--- CGraphElementsCollection MSG_GRAPH_OBJ_FAILED_GET_ADDED_OBJ_LIST, class=class="str">"cmt">// Failed to get the list of newly added objects MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST, class=class="str">"cmt">// Failed to remove a graphical object from the list MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_LIST, class=class="str">"cmt">// Failed to remove a graphical object from the list MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART, class=class="str">"cmt">// Failed to remove a graphical object from the chart MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_DEL_LIST, class=class="str">"cmt">// Failed to set a graphical object to the list of removed objects MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_RNM_LIST, class=class="str">"cmt">// Failed to set a graphical object to the list of renamed objects 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">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- CGraphElementsCollection {"Не удалось получить список вновь добавленных объектов","Failed to get the list of newly added objects"}, {"Не удалось изъять графический объект из списка","Failed to detach graphic object from the list"},
图形对象报错与枢轴点数据类的内部构造
在自定义图形对象库里,删除和重命名操作都有明确的失败回报文本。比如「Failed to delete graphic object from the list」「Failed to place graphic object in the list of renamed objects」,这些字符串直接进日志,方便定位是列表维护出错还是图表层出错。 CLinkedPivotPoint 相关提示暴露了依赖对象的硬约束:对象若没设 X 轴或 Y 轴枢轴点,会报「The object does not have any pivot points set along the x-axis」;没挂到基对象则报「The object is not attached to the base graphical object」。这类错误在 MT5 加载 EA 调对象时大概率静默拖慢逻辑,建议开终端看 Experts 标签逐条核对。
| PrintShort 方法把对象头、名称、GRAPH_OBJ_PROP_ID 属性和创建时间拼成一行日志。时间用 TimeToString 带 DATE | MINUTES | SECONDS 格式,精确到秒,便于回看某箭头是何时画的。 |
|---|
CPivotPointData 类用 m_axis_x 标记该枢轴点是否属于 X 坐标,m_property[][2] 存属性键值对。SetAxisX / IsAxisX 是一对极简接口,外部靠 bool 标志区分轴向,避免 X/Y 混淆。外汇与贵金属图表对象多受报价跳空影响,枢轴点绑定失误可能让画线偏移到异常价位,属高风险操作。
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 CPivotPointData { class="kw">private: class="type">bool m_axis_x; class="type">int m_property[][class="num">2]; class="kw">public: class="type">void SetAxisX(const class="type">bool axis_x) { this.m_axis_x=axis_x; } class="type">bool IsAxisX(class="type">void) const { class="kw">return this.m_axis_x; } };
◍ 依赖坐标的基准锚点怎么管
在 MT5 自定义图形对象里,依赖对象的坐标往往要挂在一个或多个基准对象的枢轴上算出来。下面这段类方法就是专门维护「基准枢轴点数组」的,直接决定了依赖点能不能正确跟着基准动。 AxisDescription 只返回 "X" 或 "Y",靠 m_axis_x 布尔判断当前对象是横轴还是纵轴相关;GetBasePivotsNum 用 ArrayRange(m_property,0) 取第一维长度,也就是当前已登记的基准枢轴数量。 AddNewBasePivotPoint 先拿现有数量当新索引,若 ArrayResize 没能扩出 +1 的空间就写日志返回 false,否则转交 ChangeBasePivotPoint 去填值。ChangeBasePivotPoint 里若数组长度为 0 会按横纵轴分别报 MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_X / Y 的错——说明没基准就别想改枢轴。 开 MT5 建个测试指标,把这俩方法挂到你的图形扩展类里,故意先不 Add 就调 Change,能在专家日志看到对应_axis 的空枢轴报错,验证数组逻辑没写反。
class="type">class="kw">string AxisDescription(class="type">void) const { class="kw">return(this.m_axis_x ? "X" : "Y");} class=class="str">"cmt">//--- Return the number of base object pivot points for calculating the coordinate of the dependent one class="type">int GetBasePivotsNum(class="type">void) const { class="kw">return ::ArrayRange(this.m_property,class="num">0); } class=class="str">"cmt">//--- Add the new pivot point of the base object for calculating the coordinate of a dependent one class="type">bool AddNewBasePivotPoint(const class="type">class="kw">string source,const class="type">int pivot_prop,const class="type">int pivot_num) { class=class="str">"cmt">//--- Get the array size class="type">int pivot_index=this.GetBasePivotsNum(); class=class="str">"cmt">//--- if failed to increase the array size, inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(::ArrayResize(this.m_property,pivot_index+class="num">1)!=pivot_index+class="num">1) { CMessage::ToLog(source,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); class="kw">return false; } class=class="str">"cmt">//--- Return the result of changing the values of a newly added new array dimension class="kw">return this.ChangeBasePivotPoint(source,pivot_index,pivot_prop,pivot_num); } class=class="str">"cmt">//--- Change the specified pivot point of the base object for calculating the coordinate of a dependent one class="type">bool ChangeBasePivotPoint(const class="type">class="kw">string source,const class="type">int pivot_index,const class="type">int pivot_prop,const class="type">int pivot_num) { class=class="str">"cmt">//--- Get the array size. If it is zero, inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27; class="type">int n=this.GetBasePivotsNum(); if(n==class="num">0) { CMessage::ToLog(source,(this.IsAxisX() ? MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_X : MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_Y));
「枢轴属性读写里的越界拦截」
这段收尾代码处理的是枢轴点属性数组的写入与读取,核心动作就两件:按索引存属性与修饰值,以及按索引往外取。写方法里先判 pivot_index 是否落在 0 到 n-1 之间,越界就写日志并返回 false,不碰数组。 读方法 GetProperty 与 GetPropertyModifier 逻辑对称,用 index 和 GetBasePivotsNum()-1 比边界,越界返回 WRONG_VALUE(宏值通常为负常数),正常则返回 m_property[index][0] 或 [1]。 这种越界前置判断在 MT5 自建枢轴库里很实用——实盘跑 EA 时若传入了错位索引,日志会直接报 MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY,而不是让终端抛数组越界异常挂掉。复制下面片段到你的类里,把 CMessage 换成 Print 就能先验证逻辑。
class="kw">return false; } class=class="str">"cmt">//--- If the specified index goes beyond the array range, inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(pivot_index<class="num">0 || pivot_index>n-class="num">1) { CMessage::ToLog(source,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY); class="kw">return false; } class=class="str">"cmt">//--- Set the values, passed to the method, in the specified array cells by index this.m_property[pivot_index][class="num">0]=pivot_prop; this.m_property[pivot_index][class="num">1]=pivot_num; class="kw">return true; } class=class="str">"cmt">//--- Return(class="num">1) a class="kw">property and(class="num">2) a modifier of the class="kw">property from the array class="type">int GetProperty(const class="type">class="kw">string source,const class="type">int index) const { if(index<class="num">0 || index>this.GetBasePivotsNum()-class="num">1) { CMessage::ToLog(source,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY); class="kw">return WRONG_VALUE; } class="kw">return this.m_property[index][class="num">0]; } class="type">int GetPropertyModifier(const class="type">class="kw">string source,const class="type">int index) const { if(index<class="num">0 || index>this.GetBasePivotsNum()-class="num">1) {