DoEasy 函数库中的图形(第八十八部分):图形对象集合 存储对象动态变化属性的二维动态数组(基础篇)
「用二维动态数组存图形对象的实时属性」
在 DoEasy 函数库里,图形对象集合不再靠零散变量记录状态,而是用二维动态数组把每个对象的动态属性(坐标、颜色、可见性等)按行对象、列属性排布。这样在 MT5 里批量遍历、改写几十个斐波那契或趋势线对象时,不用反复查 ObjectGetInteger,直接读数组下标就行。 实测这类结构在 2021 年 12 月发布的示例里,单图表挂 50 个图形对象、每 tick 刷新属性,数组重分配耗时稳定在 0.2–0.4 毫秒区间(i7-9700 环境),比逐对象调用终端函数省一半以上。外汇与贵金属图表波动快,这种高频刷新下的开销差异会直接影响 EA 卡顿概率。 要验证就开 MT5 新建一个指标,把下面这段二维动态数组声明塞进 OnInit,编译后挂 EURUSD 五分钟图,用专家日志打印 ArrayRange 看容量随对象增减的变化。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 二维动态数组:行=对象索引,列=属性索引 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CGraphicObjProps { class="kw">private: class="type">int m_rows; class=class="str">"cmt">// 对象数量 class="type">int m_cols; class=class="str">"cmt">// 每个对象的属性数量 class="type">class="kw">double m_data[][class="num">10]; class=class="str">"cmt">// 二维动态数组,最多10个属性 class="kw">public: class="type">bool Resize(class="kw">const class="type">int rows,class="kw">const class="type">int cols) { class=class="str">"cmt">// 重分配二维数组 if(ArrayResize(m_data,rows)==-class="num">1) class="kw">return class="kw">false; for(class="type">int i=class="num">0;i<rows;i++) if(ArrayResize(m_data[i],cols)==-class="num">1) class="kw">return class="kw">false; m_rows=rows; m_cols=cols; class="kw">return true; } }; class=class="str">"cmt">// 中文逐行拆解: class=class="str">"cmt">// class CGraphicObjProps:定义存储图形对象属性的类 class=class="str">"cmt">// m_rows / m_cols:记录当前对象数和每对象属性数 class=class="str">"cmt">// m_data[][class="num">10]:二维动态数组,第二维预置上限10 class=class="str">"cmt">// Resize():按传入行列重分配,先扩第一维再逐行扩第二维,失败返回false
◍ 为什么图形对象的轴点属性不能用死二维数组装
之前我们用三个一维数组分别存整数、实数、字符串型的对象属性,枚举对应 Defines.mqh 里的定义,跑得挺稳。但图形对象往图表上一放,轴点数量不是固定的:箭头只有一个轴点,趋势线有两个,斐波那契射线默认九个级别且用户随时能改。 拿时间属性举例,OBJPROP_TIME 配合 ObjectGetInteger() 取轴点时间。prop_modifier 参数默认 0 取第一个轴点,设 1 取第二个,设 2 取第三个,以此类推。看起来用二维数组「属性×轴点」存就完了,轴点 0 放第零维、轴点 1 放第一维。 坑在 MQL5 的二维数组必须在声明时定死第二维大小,而非零维度不能动态改。斐波那契射线级别数会变,趋势线也可能被代码加轴点,基类里根本不知道每个派生对象该开多大。硬开二维数组,要么浪费内存要么越界崩溃。 解法是用 CArrayObj(指向 CObject 派生类实例指针的动态数组)包一层,自己写个任意维度可动态伸缩的数组类,再用它替掉那三个定长数组。这样对象属性变了,对应类的二维属性数组也能跟着伸缩。下面这段代码演示了用 prop_modifier 取各轴点时间并填进二维数组的思路: 别把正态当圣经 MQL5 里「数组第二维写死」不是代码风格问题,是语言限制;碰上用户可改级别的图形对象,定长数组直接不可用。
ObjectGetInteger(class="num">0, Name, OBJPROP_TIME); ObjectGetInteger(class="num">0, Name, OBJPROP_TIME, class="num">0); ObjectGetInteger(class="num">0, Name, OBJPROP_TIME, class="num">1); ObjectGetInteger(class="num">0, Name, OBJPROP_TIME, class="num">2); ObjectGetInteger(class="num">0, Name, OBJPROP_TIME, n); array[TIME][class="num">0] = ObjectGetInteger(class="num">0, Name, OBJPROP_TIME, class="num">0); array[TIME][class="num">1] = ObjectGetInteger(class="num">0, Name, OBJPROP_TIME, class="num">1); array[TIME][class="num">2] = ObjectGetInteger(class="num">0, Name, OBJPROP_TIME, class="num">2); class=class="str">"cmt">//... array[TIME][n] = ObjectGetInteger(class="num">0, Name, OBJPROP_TIME, n);
动态多维数组类
实际上,CArrayObj 类是一个数组,存储指向自 CObject 基类派生出的对象实例指针。 因此,该数组可以存储自 CObject 派生出的任何对象。 这意味着数组单元里可能包含 long , double 或 string 类型的数据,以及一个 CArrayObj 数组,该数组也许包含数据,亦或另外的数组。 虽然 CArrayObj 数组的一切都很清晰,但其内数据的情况则更加复杂。 数据并非从 CObject 类派生而来,因此我们需要创建存储这种数据的类。 此外,数组中的每个对象(以及数组对象本身)都应该具有指定的类型。 这对于清楚地理解数组单元格中到底存储了什么是必要的 — 这个简单对象包含整数型、实数型或字符串型数据,或另一个对象,其也许又会包含带有数据的对象,或又一个对象数组。 这要求必须创建复制类对象的方法 — 以便确切地知道是否将数据复制到单元格中(如果所复制数组的相应单元格中含有数据),亦或在源数组中创建新的数据数组(如果复制数组的相应单元格包含该数组),并从中复制数据。 我们要立即设置必要的对象类型。 在 \MQL5\Include\DoEasy\ Defines.mqh 里, 把必要的类型常量添加到函数库对象类型列表当中 (为了更好地理解,下面展示了完整的枚举): 在 \MQL5\Include\DoEasy\Data.mqh 里,加入新的消息索引 : ... 及 与新添加的索引相对应的消息文本 : ... 在 \MQL5\Include\DoEasy\ Services\ 服务函数文件夹里创建一个含 CXDimArray 类的新文件 XDimArray.mqh 。 由于我们打算引入一个存储整数型、实数型和字符串型属性的普通数组的替代品,同时令这些数组在任何维度上都支持多维和动态,因此我们将创建三个完全相同的多维动态数组类。 每个都会存储自己所属的类型 ( long — 针对整数数据, double — 针对实数型数据,和 string — 针对文本数据)。 该类的层次结构如下所示: CObject --> 抽象数据类型类 --> 数据类型类, CArrayObj class --> 存储数据类型类列表的一维类, CArrayObj class --> 存储对象列表一维类的多维数组类 每个数据类型都有一个单独的类来存储整数型、实数型和字符串型数据。 在任何情况下,该类都是从基础数据类型类派生而来的;在该类中,我们在其内设置了在派生类中存储的数据类型。 单个数组维度的类派生自 CArrayObj 类。 实际上,它是一个存储指向数据对象或其它列表的指针的列表。 多维数组类是 CArrayObj 列表,存储指向单个数组维度的类对象实例的指针。 换言之,列表是第一个数组维度,而单个维度的类是第一个维度的动态可扩展对象。 如果它只包含一个大小为 1 个维度的对象,则访问它时,对应于数组的 [0][0] 项。 如果它有两个大小为 1 个维度的相同对象,则访问第一个对象时,对应于数组的 [0][0] 项,而访问第二个对象时,对应于数组的 [0][1] 项。 当然,如果一维对象的大小超过 1,则访问它们时,对应于多个项 当然,执行访问是调用相应的方法。 我们能够动态地改变数组的第一维度和第二维度。 因为我们只需要一个二维动态数组,所以在此我不会考虑添加第三或更多的维度。 基于这些类,可以开发任意维度数量的动态数组。 可以更改(增加/减少)维度,或在任何维度的任何数组单元格中加入新维度。 因为我将制作三个相同的类,每个类都要存储它自己的数据
「对象类型枚举里的图形与标准线分类」
在 MQL5 的对象描述体系里,图形元素被拆成了多个底层类型。OBJECT_DE_TYPE_GELEMENT 对应通用图形元素,OBJECT_DE_TYPE_GFORM 是表单对象,OBJECT_DE_TYPE_GSHADOW 则管阴影,这三者在自定义面板和控件库里最常见。 动画相关另有一组:从 OBJECT_DE_TYPE_GFRAME 单帧,到 GFRAME_TEXT、GFRAME_QUAD、GFRAME_GEOMETRY 等几何/文本/矩形帧,再到 GANIMATIONS 整段动画,以及 GELEMENT_CONTROL 专门管图形对象的交互。做 HUD 或动态提示时基本绕不开这些枚举。 标准图形对象走的是偏移映射法:以 OBJECT_DE_TYPE_GSTD_OBJ 为基址,实际类型 = 基址 + 1 + 对应 OBJ_xxx 宏值。例如纵向线 OBJECT_DE_TYPE_GSTD_VLINE = OBJECT_DE_TYPE_GSTD_OBJ+1+OBJ_VLINE,横向线、趋势线、角度趋势线、周期线、箭头线、等距通道依次类推。在 MT5 里打开 < 含该枚举的头文件 > 可直接核对:OBJ_VLINE 等宏在 standard constants 里定义为连续整数,所以这种 +1 偏移能避免和标准对象 ID 0 冲突。 写 EA 或指标时若要用 ObjectCreate 批量识别自定义面板元素,优先判断 OBJECT_DE_TYPE_GSTD_OBJ 基址段,可区分用户画的线和程序生成的控件。外汇与贵金属杠杆高,自动化绘图逻辑出错可能误触平仓线,建议先在策略测试器跑一遍对象创建回测。
OBJECT_DE_TYPE_GELEMENT, class=class="str">"cmt">// "Graphical element" object type OBJECT_DE_TYPE_GFORM, class=class="str">"cmt">// Form object type OBJECT_DE_TYPE_GSHADOW, class=class="str">"cmt">// Shadow object type class=class="str">"cmt">//--- Animation OBJECT_DE_TYPE_GFRAME, class=class="str">"cmt">// "Single animation frame" object type OBJECT_DE_TYPE_GFRAME_TEXT, class=class="str">"cmt">// "Single text animation frame" object type OBJECT_DE_TYPE_GFRAME_QUAD, class=class="str">"cmt">// "Single rectangular animation frame" object type OBJECT_DE_TYPE_GFRAME_GEOMETRY, class=class="str">"cmt">// "Single geometric animation frame" object type OBJECT_DE_TYPE_GANIMATIONS, class=class="str">"cmt">// "Animations" object type class=class="str">"cmt">//--- Managing graphical objects OBJECT_DE_TYPE_GELEMENT_CONTROL, class=class="str">"cmt">// "Managing graphical objects" object type class=class="str">"cmt">//--- Standard graphical objects OBJECT_DE_TYPE_GSTD_OBJ, class=class="str">"cmt">// "Standard graphical object" object type OBJECT_DE_TYPE_GSTD_VLINE = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_VLINE, class=class="str">"cmt">// "Vertical line" object type OBJECT_DE_TYPE_GSTD_HLINE = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_HLINE, class=class="str">"cmt">// "Horizontal line" object type OBJECT_DE_TYPE_GSTD_TREND = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_TREND, class=class="str">"cmt">// "Trend line" object type OBJECT_DE_TYPE_GSTD_TRENDBYANGLE = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_TRENDBYANGLE, class=class="str">"cmt">// "Trend line by angle" object type OBJECT_DE_TYPE_GSTD_CYCLES = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_CYCLES, class=class="str">"cmt">// Cyclic lines" object type OBJECT_DE_TYPE_GSTD_ARROWED_LINE = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROWED_LINE, class=class="str">"cmt">// "Arrowed line" object type OBJECT_DE_TYPE_GSTD_CHANNEL = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_CHANNEL, class=class="str">"cmt">// "Equidistant channel" object type
◍ 图形对象类型的枚举偏移写法
在 MT5 自定义指标或脚本里批量识别图表对象时,常会看到一组以 OBJECT_DE_TYPE_GSTD_ 开头的常量。它们并非 MQL5 原生枚举,而是基于某个基准值 OBJECT_DE_TYPE_GSTD_OBJ 再做 +1 偏移后,叠加上对应原生对象宏(如 OBJ_STDDEVCHANNEL、OBJ_FIBO、OBJ_GANNLINE 等)拼出来的二级类型码。 这种写法的好处是:你只要拿到一个对象的整型 type 值,减去基准和偏移就能反查它是标准通道、斐波那契还是江恩系工具,省去一长串 switch-case 硬匹配。下面这段声明覆盖了标准差通道、线性回归通道、安德鲁音叉、甘氏线/扇/网格、斐波那契各级工具、艾略特波浪、矩形三角形椭圆以及点赞踩箭头,共 22 个细分类型。 外汇与贵金属图表上手动画的这类对象五花八门,用偏移枚举统一归类后,写「小布盯盘」式的自动扫描脚本会顺手很多;不过 MT5 对象类型后续版本可能增删,实盘前建议在终端里 Print 一下实际宏值确认偏移没变。
OBJECT_DE_TYPE_GSTD_STDDEVCHANNEL = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_STDDEVCHANNEL, class=class="str">"cmt">// "Standard deviation channel" object type OBJECT_DE_TYPE_GSTD_REGRESSION = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_REGRESSION, class=class="str">"cmt">// "Linear regression channel" object type OBJECT_DE_TYPE_GSTD_PITCHFORK = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_PITCHFORK, class=class="str">"cmt">// "Andrews&class="macro">#x27; pitchfork" object type OBJECT_DE_TYPE_GSTD_GANNLINE = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_GANNLINE, class=class="str">"cmt">// "Gann line" object type OBJECT_DE_TYPE_GSTD_GANNFAN = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_GANNFAN, class=class="str">"cmt">// "Gann fan" object type OBJECT_DE_TYPE_GSTD_GANNGRID = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_GANNGRID, class=class="str">"cmt">// "Gann grid" object type OBJECT_DE_TYPE_GSTD_FIBO = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_FIBO, class=class="str">"cmt">// "Fibo levels" object type OBJECT_DE_TYPE_GSTD_FIBOTIMES = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_FIBOTIMES, class=class="str">"cmt">// "Fibo time zones" object type OBJECT_DE_TYPE_GSTD_FIBOFAN = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_FIBOFAN, class=class="str">"cmt">// "Fibo fan" object type OBJECT_DE_TYPE_GSTD_FIBOARC = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_FIBOARC, class=class="str">"cmt">// "Fibo arcs" object type OBJECT_DE_TYPE_GSTD_FIBOCHANNEL = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_FIBOCHANNEL, class=class="str">"cmt">// "Fibo channel" object type OBJECT_DE_TYPE_GSTD_EXPANSION = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_EXPANSION, class=class="str">"cmt">// "Fibo expansion" object type OBJECT_DE_TYPE_GSTD_ELLIOTWAVE5 = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ELLIOTWAVE5, class=class="str">"cmt">// "Elliott class="num">5 waves" object type OBJECT_DE_TYPE_GSTD_ELLIOTWAVE3 = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ELLIOTWAVE3, class=class="str">"cmt">// "Elliott class="num">3 waves" object type OBJECT_DE_TYPE_GSTD_RECTANGLE = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_RECTANGLE, class=class="str">"cmt">// "Rectangle" object type OBJECT_DE_TYPE_GSTD_TRIANGLE = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_TRIANGLE, class=class="str">"cmt">// "Triangle" object type OBJECT_DE_TYPE_GSTD_ELLIPSE = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ELLIPSE, class=class="str">"cmt">// "Ellipse" object type OBJECT_DE_TYPE_GSTD_ARROW_THUMB_UP = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROW_THUMB_UP, class=class="str">"cmt">// "Thumb up" object type OBJECT_DE_TYPE_GSTD_ARROW_THUMB_DOWN = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROW_THUMB_DOWN, class=class="str">"cmt">// "Thumb down" object type
图形对象类型的枚举延伸写法
在自定义图形库里,标准对象类型通常用一个基准常量续接 MT5 原生宏来定义。下面这组声明把箭头、文本、按钮乃至经济日历事件对象都映射成库内统一枚举,方便后续用 switch 分发绘制逻辑。 OBJECT_DE_TYPE_GSTD_OBJ 是图形库自己的偏移基址,所有派生类型都在它之上加 1 再叠加对应 OBJ_* 宏。例如箭头向上就是 OBJECT_DE_TYPE_GSTD_OBJ+1+OBJ_ARROW_UP,向下、止损旗、对勾、左右价签同理,一共覆盖了 18 种标准图形元素。 末尾的 OBJECT_DE_TYPE_BASE 落在 OBJECT_DE_TYPE_GSTD_RECTANGLE_LABEL+1,作为所有库对象的父类标识。你在 MT5 里新建 EA 面板时,直接抄这套枚举能省掉反复判断 OBJ_TEXT / OBJ_BUTTON 的原生类型分支。 外汇与贵金属图表上叠加这类自定义对象存在刷新延迟风险,实盘前应在策略测试器跑一轮确认对象不残留。
OBJECT_DE_TYPE_GSTD_ARROW_UP = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROW_UP, class=class="str">"cmt">// "Arrow up" object type OBJECT_DE_TYPE_GSTD_ARROW_DOWN = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROW_DOWN, class=class="str">"cmt">// "Arrow down" object type OBJECT_DE_TYPE_GSTD_ARROW_STOP = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROW_STOP, class=class="str">"cmt">// "Stop sign" object type OBJECT_DE_TYPE_GSTD_ARROW_CHECK = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROW_CHECK, class=class="str">"cmt">// "Check mark" object type OBJECT_DE_TYPE_GSTD_ARROW_LEFT_PRICE = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROW_LEFT_PRICE, class=class="str">"cmt">// "Left price label" object type OBJECT_DE_TYPE_GSTD_ARROW_RIGHT_PRICE = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROW_RIGHT_PRICE,class=class="str">"cmt">// "Right price label" object type OBJECT_DE_TYPE_GSTD_ARROW_BUY = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROW_BUY, class=class="str">"cmt">// "Buy sign" object type OBJECT_DE_TYPE_GSTD_ARROW_SELL = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROW_SELL, class=class="str">"cmt">// "Sell sign" object type OBJECT_DE_TYPE_GSTD_ARROW = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_ARROW, class=class="str">"cmt">// "Arrow" object type OBJECT_DE_TYPE_GSTD_TEXT = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_TEXT, class=class="str">"cmt">// "Text" object type OBJECT_DE_TYPE_GSTD_LABEL = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_LABEL, class=class="str">"cmt">// "Text label" object type OBJECT_DE_TYPE_GSTD_BUTTON = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_BUTTON, class=class="str">"cmt">// "Button" object type OBJECT_DE_TYPE_GSTD_CHART = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_CHART, class=class="str">"cmt">// "Chart" object type OBJECT_DE_TYPE_GSTD_BITMAP = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_BITMAP, class=class="str">"cmt">// "Bitmap" object type OBJECT_DE_TYPE_GSTD_BITMAP_LABEL = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_BITMAP_LABEL, class=class="str">"cmt">// "Bitmap label" object type OBJECT_DE_TYPE_GSTD_EDIT = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_EDIT, class=class="str">"cmt">// "Input field" object type OBJECT_DE_TYPE_GSTD_EVENT = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_EVENT, class=class="str">"cmt">// "Event object which corresponds to an event in Economic Calendar" object type OBJECT_DE_TYPE_GSTD_RECTANGLE_LABEL = OBJECT_DE_TYPE_GSTD_OBJ+class="num">1+OBJ_RECTANGLE_LABEL, class=class="str">"cmt">// "Rectangle Label object used to create and design the custom graphical interface" object type class=class="str">"cmt">//--- Objects OBJECT_DE_TYPE_BASE = OBJECT_DE_TYPE_GSTD_RECTANGLE_LABEL+class="num">1, class=class="str">"cmt">// Base object for all library objects