DoEasy 函数库中的图形(第八十六部分):图形对象集合 - 管理属性修改·综合运用
「图形对象整型属性的批量抓取」
在 MT5 自定义指标或 EA 里接管图表对象时,最常踩的坑是对象属性没同步进类成员,导致后续绘制或交互逻辑读到脏值。下面这段把 14 个整型属性一次性从图表拉进 m_long_prop 数组,覆盖射线、椭圆、箭头、锚点、像素偏移、甘恩方向、艾略特层级等典型对象类型。
逐行看,ObjectGetInteger 的三个参数固定为图表 ID、对象名、属性宏;比如 OBJPROP_RAY_LEFT 取左射线开关,OBJPROP_XDISTANCE / OBJPROP_YDISTANCE 拿的是相对基准角的像素距离,做悬浮面板定位时这两个值直接决定控件会不会跑出可视区。
OBJPROP_CHART_ID 与 OBJPROP_PERIOD 仅对子图图表对象(OBJ_CHART)有效,普通趋势线读出来是 0。外汇与贵金属波动快,这类对象属性若在 OnChartEvent 里漏抓,按钮状态 OBJPROP_STATE 可能延迟一帧才更新,高频择时策略要警惕误触。
this.m_long_prop[GRAPH_OBJ_PROP_RAY_LEFT] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_RAY_LEFT); class=class="str">"cmt">// Ray goes to the left this.m_long_prop[GRAPH_OBJ_PROP_RAY_RIGHT] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_RAY_RIGHT); class=class="str">"cmt">// Ray goes to the right this.m_long_prop[GRAPH_OBJ_PROP_RAY] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_RAY); class=class="str">"cmt">// Vertical line goes through all windows of a chart this.m_long_prop[GRAPH_OBJ_PROP_ELLIPSE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ELLIPSE); class=class="str">"cmt">// Display the full ellipse of the Fibonacci Arc object this.m_long_prop[GRAPH_OBJ_PROP_ARROWCODE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ARROWCODE); class=class="str">"cmt">// Arrow code for the "Arrow" object this.m_long_prop[GRAPH_OBJ_PROP_ANCHOR] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ANCHOR); class=class="str">"cmt">// Position of the binding point of the graphical object this.m_long_prop[GRAPH_OBJ_PROP_XDISTANCE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_XDISTANCE); class=class="str">"cmt">// Distance from the base corner along the X axis in pixels this.m_long_prop[GRAPH_OBJ_PROP_YDISTANCE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_YDISTANCE); class=class="str">"cmt">// Distance from the base corner along the Y axis in pixels this.m_long_prop[GRAPH_OBJ_PROP_DIRECTION] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DIRECTION); class=class="str">"cmt">// Gann object trend this.m_long_prop[GRAPH_OBJ_PROP_DEGREE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DEGREE); class=class="str">"cmt">// Elliott wave marking level this.m_long_prop[GRAPH_OBJ_PROP_DRAWLINES] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DRAWLINES); class=class="str">"cmt">// Display lines for Elliott wave marking this.m_long_prop[GRAPH_OBJ_PROP_STATE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_STATE); class=class="str">"cmt">// Button state(pressed/released) this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_CHART_ID] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_CHART_ID); class=class="str">"cmt">// Chart object ID(OBJ_CHART). this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_PERIOD] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_PERIOD); class=class="str">"cmt">// Chart object period
抓取图表对象的整型与双精度属性
在 MT5 自定义图形类里,把对象当前状态落盘到内存数组,靠的是 ObjectGetInteger 与 ObjectGetDouble 两套读取接口。下面这段从 CGStdGraphObj 方法摘出的代码,一次性把时间轴、价格轴、缩放、像素尺寸、偏移、锚点角等整型属性读回 m_long_prop 数组。 注意 OBJPROP_XSIZE / OBJPROP_YSIZE 返回的是像素单位的宽高,而 OBJPROP_XOFFSET / OBJPROP_YOFFSET 是可见区域左上角的坐标——这两个概念常被混淆,导致面板重绘时位置漂移。 双精度部分只取了 OBJPROP_PRICE 与 OBJPROP_LEVELVALUE:前者是对象绑定的价格坐标,后者是水平线等对象的价位数值。若你写的指标要在 Tick 事件里重定位对象,这两行是必须保留的读取点。 开 MT5 新建 EA 把下面代码塞进类方法,编译后挂 EURUSD 图表,用 ObjectsDeleteAll 前先打印 m_long_prop[GRAPH_OBJ_PROP_XSIZE],大概率能看到非 0 像素值,证明对象已实际占据空间。外汇与贵金属杠杆高,图形对象只是辅助定位,不构成任何方向暗示。
this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_DATE_SCALE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DATE_SCALE); class=class="str">"cmt">// Time scale display flag for the Chart object this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_PRICE_SCALE]= ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_PRICE_SCALE);class=class="str">"cmt">// Price scale display flag for the Chart object this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_CHART_SCALE]= ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_CHART_SCALE);class=class="str">"cmt">// Chart object scale this.m_long_prop[GRAPH_OBJ_PROP_XSIZE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_XSIZE); class=class="str">"cmt">// Object width along the X axis in pixels. this.m_long_prop[GRAPH_OBJ_PROP_YSIZE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_YSIZE); class=class="str">"cmt">// Object height along the Y axis in pixels. this.m_long_prop[GRAPH_OBJ_PROP_XOFFSET] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_XOFFSET); class=class="str">"cmt">// X coordinate of the upper-left corner of the visibility area. this.m_long_prop[GRAPH_OBJ_PROP_YOFFSET] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_YOFFSET); class=class="str">"cmt">// Y coordinate of the upper-left corner of the visibility area. this.m_long_prop[GRAPH_OBJ_PROP_BGCOLOR] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_BGCOLOR); class=class="str">"cmt">// Background class="type">class="kw">color for OBJ_EDIT, OBJ_BUTTON, OBJ_RECTANGLE_LABEL this.m_long_prop[GRAPH_OBJ_PROP_CORNER] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_CORNER); class=class="str">"cmt">// Chart corner for binding a graphical object this.m_long_prop[GRAPH_OBJ_PROP_BORDER_TYPE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_BORDER_TYPE);class=class="str">"cmt">// Border type for "Rectangle border" this.m_long_prop[GRAPH_OBJ_PROP_BORDER_COLOR]= ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_BORDER_COLOR);class=class="str">"cmt">// Border class="type">class="kw">color for OBJ_EDIT and OBJ_BUTTON } class="type">void CGStdGraphObj::GetAndSaveDBL(class="type">void) { this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_PRICE)] = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_PRICE); class=class="str">"cmt">// Price coordinate this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_LEVELVALUE)] = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_LEVELVALUE); class=class="str">"cmt">// Level value
◍ 把对象属性抓进类内部缓存
想在 EA 里反复读甘氏线或斐波那契弧的缩放比例,每次都调 ObjectGetDouble 会很拖。CGStdGraphObj 的做法是在类里用 m_double_prop 数组把双精度属性先缓存下来,下标由 IndexProp 映射。 下面这段把 SCALE、ANGLE、DEVIATION 三个双精度属性一次性拉进数组:SCALE 专供甘氏对象和斐波那契弧,DEVIATION 是标准差通道的偏离值。 字符串侧另起 GetAndSaveSTR,把 TEXT、TOOLTIP、LEVELTEXT、FONT、BMPFILE、SYMBOL 全部用 ObjectGetString 存进 m_string_prop。这样鼠标悬停提示和水平位描述都能离线读,不依赖终端实时查询。 PropertiesRefresh 只是把三个 Get 函数串起来:先整型、再双精度、最后字符串。实盘外汇或贵金属图表对象多时,手动调一次 PropertiesRefresh 比每次散查快一个数量级,但对象被脚本改了属性后必须重刷,否则缓存会滞后。
this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_SCALE)] = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_SCALE); class=class="str">"cmt">// Scale(class="kw">property of Gann objects and Fibonacci Arcs objects) this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_ANGLE)] = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_ANGLE); class=class="str">"cmt">// Corner this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_DEVIATION)] = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_DEVIATION); class=class="str">"cmt">// Deviation of the standard deviation channel } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Get and save the class="type">class="kw">string properties | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGStdGraphObj::GetAndSaveSTR(class="type">void) { this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_TEXT)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_TEXT); class=class="str">"cmt">// Object description(the text contained in the object) this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_TOOLTIP)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_TOOLTIP); class=class="str">"cmt">// Tooltip text this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_LEVELTEXT)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_LEVELTEXT);class=class="str">"cmt">// Level description this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_FONT)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_FONT); class=class="str">"cmt">// Font this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_BMPFILE)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_BMPFILE); class=class="str">"cmt">// BMP file name for the "Bitmap Level" object this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_SYMBOL); class=class="str">"cmt">// Chart object symbol } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Overwrite all graphical object properties | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGStdGraphObj::PropertiesRefresh(class="type">void) { this.GetAndSaveINT(); this.GetAndSaveDBL(); this.GetAndSaveSTR(); }
「图形对象属性变更的快照比对」
在 MT5 自定义图形类里,要判断一个对象属性是否被人改过,最干脆的办法是每次刷新前拿「上一帧」数据做差。CGStdGraphObj 用三个数组分别存 long、double、string 类型的当前属性,再开一个 *_prev 同名数组存上一次的状态。 PropertiesCopyToPrevData() 只干一件事:把当前三套属性数组整体拷进 prev 数组。ArrayCopy 是深拷贝,调用后 prev 与当前完全脱钩,下次比对才不会串味。 真正的变更探测在 PropertiesCheckChanged() 里。它把整数、浮点、字符串三类属性枚举区间拼接起来轮询:先扫 GRAPH_OBJ_PROP_INTEGER_TOTAL 个整型,再偏移 double 总量扫双精度,最后偏移 string 总量扫字符串。每个属性先用 SupportProperty 过滤掉对象不支持的枚举值,避免越界误报。 只要当前值与 prev 值不等,就置 changed=true 并打印对象名和属性描述(中俄双语走 TextByLanguage)。三类循环跑完,若 changed 为真才回写 prev——这意味着一次无变动的帧不会触发无谓的内存拷贝。 实盘接这套逻辑时,把 Print 换成你自己的事件回调,就能在外汇或贵金属图表对象被拖拽、改色、改线宽时立刻感知;这类品种波动剧烈、高风险,属性监听别拿来当入场信号,只做界面状态同步更稳。
class=class="str">"cmt">//| Copy the current data to the previous one | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGStdGraphObj::PropertiesCopyToPrevData(class="type">void) { ::ArrayCopy(this.m_long_prop_prev,this.m_long_prop); ::ArrayCopy(this.m_double_prop_prev,this.m_double_prop); ::ArrayCopy(this.m_string_prop_prev,this.m_string_prop); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check object class="kw">property changes | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGStdGraphObj::PropertiesCheckChanged(class="type">void) { class="type">bool changed=class="kw">false; class="type">int beg=class="num">0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i; if(!this.SupportProperty(prop)) class="kw">continue; if(this.GetProperty(prop)!=this.GetPropertyPrev(prop)) { changed=true; ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified class="kw">property: "),GetPropertyDescription(prop)); } } beg=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i; if(!this.SupportProperty(prop)) class="kw">continue; if(this.GetProperty(prop)!=this.GetPropertyPrev(prop)) { changed=true; ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified class="kw">property: "),GetPropertyDescription(prop)); } } beg=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i; if(!this.SupportProperty(prop)) class="kw">continue; if(this.GetProperty(prop)!=this.GetPropertyPrev(prop)) { changed=true; ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified class="kw">property: "),GetPropertyDescription(prop)); } } if(changed) PropertiesCopyToPrevData(); } class=class="str">"cmt">//+------------------------------------------------------------------+
图形对象删除的追踪与集合同步
图形对象的增删只能在图形对象集合类里统一跟踪,因为该类维护着所有已开图表上对象的完整清单。对象自身只管属性变更,而集合类负责在图表事件触发时比对清单与终端实际对象数,差额为负即代表有对象被删。 在 GraphElementsCollection.mqh 中,私密部分新增方法扫描「存在于集合但图表已无」的对象:按图表 ID 取集合列表,逐个用标准类取对象,若 ChartObjectFind 返回 -1 则说明该对象已从图表消失,返回其指针交由删除逻辑处理。 删除方法本身先给列表打已排序标志,调 CArrayObj::Search() 拿索引,索引小于 0 直接返 false,否则调 Delete() 返回结果。实测中,手动删一个趋势线会让 m_delta_graph_obj 变 -1,集合类下一帧轮询即可将其清出列表。 事件处理程序目前只接当前图表的对象变更与移动事件。多锚点对象首次单击只发创建事件,属性常常填不全;等用户拖完发移动事件时,处理程序借机重写属性,把正确锚点数据补回。这样集合与图表不会因构建中途的状态不一致而漏删或误留。 外汇与贵金属图表物件频繁增减,MT5 对象追踪逻辑若漏掉删除事件,可能造成内存中对象堆积,建议在策略 tester 里连续删建 50 个对象验证集合计数归零。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Chart object management class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CChartObjectsControl : class="kw">public CObject { class="kw">private: CArrayObj m_list_new_graph_obj; class=class="str">"cmt">// List of added graphical objects ENUM_TIMEFRAMES m_chart_timeframe; class=class="str">"cmt">// Chart timeframe class="type">long m_chart_id; class=class="str">"cmt">// Chart ID class="type">class="kw">string m_chart_symbol; class=class="str">"cmt">// Chart symbol class="type">bool m_is_graph_obj_event; class=class="str">"cmt">// Event flag in the list of graphical objects class="type">int m_total_objects; class=class="str">"cmt">// Number of graphical objects class="type">int m_last_objects; class=class="str">"cmt">// Number of graphical objects during the previous check class="type">int m_delta_graph_obj; class=class="str">"cmt">// Difference in the number of graphical objects compared to the previous check class=class="str">"cmt">//--- Return the name of the last graphical object added to the chart class="type">class="kw">string LastAddedGraphObjName(class="type">void); class=class="str">"cmt">//--- Set the permission to track mouse events and graphical objects class="type">void SetMouseEvent(class="type">void); class="kw">public: class=class="str">"cmt">//--- Return the variable values ENUM_TIMEFRAMES Timeframe(class="type">void) class="kw">const { class="kw">return this.m_chart_timeframe; }
◍ 抓图表对象事件的控制类接口
在 MT5 自定义指标或 EA 里,想监听用户在图表上画的对象(趋势线、矩形、斐波那契等),靠的是一组封装好的访问器。下面这个类把图表 ID、品种、是否发生对象事件、对象总数和增量都暴露成只读方法,调用方不用直接碰全局函数。 long ChartID(void) const { return this.m_chart_id; }——返回当前绑定的图表句柄;string Symbol(void) const { return this.m_chart_symbol; }——返回该图表所属品种,比如"XAUUSD";bool IsEvent(void) const { return this.m_is_graph_obj_event; }——标记本轮是否触发了图形对象事件;int TotalObjects(void) const { return this.m_total_objects; }——当前图表对象总数;int Delta(void) const { return this.m_delta_graph_obj; }——相比上一次刷新的对象数差值,正数为新增。 CreateNewGraphObj() 按 ENUM_OBJECT 类型与 chart_id、name 新建标准图形对象;GetListNewAddedObj() 直接回传 m_list_new_graph_obj 的指针,新增对象都堆在这张表里。Refresh() 负责重新扫描图表,更新上面那几个计数与事件标记。 构造函数里先把新增列表 Clear() 并 Sort(),再把 m_chart_id 设为 ::ChartID()、周期取 ChartPeriod()、品种取 ChartSymbol(),然后把事件标记、总数、上次总数全清零。实盘跑外汇或贵金属时,图表对象增减很频繁,Delta() 配合 IsEvent() 能让你只处理"刚画的那一根线",避免每帧重算全部对象——这属于高频图表交互里的高风险细节,参数没过滤好可能反复触发重绘。
class="type">long ChartID(class="type">void) class="kw">const { class="kw">return this.m_chart_id; } class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.m_chart_symbol; } class="type">bool IsEvent(class="type">void) class="kw">const { class="kw">return this.m_is_graph_obj_event; } class="type">int TotalObjects(class="type">void) class="kw">const { class="kw">return this.m_total_objects; } class="type">int Delta(class="type">void) class="kw">const { class="kw">return this.m_delta_graph_obj; } class=class="str">"cmt">//--- Create a new standard graphical object CGStdGraphObj *CreateNewGraphObj(class="kw">const ENUM_OBJECT obj_type,class="kw">const class="type">long chart_id, class="kw">const class="type">class="kw">string name); class=class="str">"cmt">//--- Return the list of newly added objects CArrayObj *GetListNewAddedObj(class="type">void) { class="kw">return &this.m_list_new_graph_obj;} class=class="str">"cmt">//--- Check the chart objects class="type">void Refresh(class="type">void); class=class="str">"cmt">//--- Constructors CChartObjectsControl(class="type">void) { this.m_list_new_graph_obj.Clear(); this.m_list_new_graph_obj.Sort(); this.m_chart_id=::ChartID(); this.m_chart_timeframe=(ENUM_TIMEFRAMES)::ChartPeriod(this.m_chart_id); this.m_chart_symbol=::ChartSymbol(this.m_chart_id); this.m_is_graph_obj_event=class="kw">false; this.m_total_objects=class="num">0; this.m_last_objects=class="num">0;
「图表控件类的构造与比较逻辑」
在封装图表对象控制的类里,两个构造函数都先清空新建对象列表并调用 Sort(),再把传入的 chart_id 存进 m_chart_id。随后通过 ChartPeriod() 和 ChartSymbol() 拿到该图表的时间周期与品种,分别写入 m_chart_timeframe 与 m_chart_symbol。 初始化时 m_is_graph_obj_event 置为 false,m_total_objects、m_last_objects、m_delta_graph_obj 三个计数全部归零,最后统一调用 SetMouseEvent() 挂上鼠标事件。这样每个实例在诞生时就处于干净的待追踪状态。 Compare() 方法让列表能按图表 ID 排序:当前对象 ChartID() 大于传入节点时返回 1,小于返回 -1,相等返回 0。这个三元比较是后续用 CList 管理多图表控件的前提。 OnChartEvent() 声明为接收 id、lparam、dparam、sparam 四个参数的事件处理函数,由系统回调驱动。外汇与贵金属图表事件密集,实盘挂这个类前先在策略测试器跑一遍,确认事件频率不会拖慢主图帧率。
this.m_delta_graph_obj=class="num">0; this.SetMouseEvent(); } CChartObjectsControl(class="kw">const class="type">long chart_id) { this.m_list_new_graph_obj.Clear(); this.m_list_new_graph_obj.Sort(); this.m_chart_id=chart_id; this.m_chart_timeframe=(ENUM_TIMEFRAMES)::ChartPeriod(this.m_chart_id); this.m_chart_symbol=::ChartSymbol(this.m_chart_id); this.m_is_graph_obj_event=class="kw">false; this.m_total_objects=class="num">0; this.m_last_objects=class="num">0; this.m_delta_graph_obj=class="num">0; this.SetMouseEvent(); } class=class="str">"cmt">//--- Compare CChartObjectsControl objects by a chart ID(for sorting the list by an object class="kw">property) class="kw">virtual class="type">int Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const { class="kw">const CChartObjectsControl *obj_compared=node; class="kw">return(this.ChartID()>obj_compared.ChartID() ? class="num">1 : this.ChartID()<obj_compared.ChartID() ? -class="num">1 : class="num">0); } class=class="str">"cmt">//--- Event handler class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam); };
给图表挂上鼠标与物件事件监听
想在 MT5 上做交互式图形分析,第一步是让图表把鼠标移动、滚轮、物件创建与删除都报上来。下面这个函数就是干这事:对当前图表 ID 连续调四次 ChartSetInteger,把四类事件开关全置 true。 [CChartObjectsControl::SetMouseEvent 代码逐行拆解] void CChartObjectsControl::SetMouseEvent(void) // 定义类方法,无入参 ::ChartSetInteger(this.ChartID(),CHART_EVENT_MOUSE_MOVE,true); // 开启鼠标移动事件 ::ChartSetInteger(this.ChartID(),CHART_EVENT_MOUSE_WHEEL,true); // 开启滚轮缩放/平移事件 ::ChartSetInteger(this.ChartID(),CHART_EVENT_OBJECT_CREATE,true);// 开启图形对象被创建的事件 ::ChartSetInteger(this.ChartID(),CHART_EVENT_OBJECT_DELETE,true);// 开启图形对象被删除的事件 事件打开后,真正的容器类 CGraphElementsCollection 才派得上用场。它用三个列表分别装图表控制器、画布元素和图形对象,并维护 m_total_objects 与 m_delta_graph_obj 两个计数器来感知物件数量变化。 m_delta_graph_obj 是和上次检查相比的物件数差值,若不为 0,说明用户手动加了线或删了标注。外汇与贵金属波动剧烈,这类交互对象可能随价格跳空频繁增减,实盘使用前建议在策略测试器里先跑一轮事件回放验证。
class=class="str">"cmt">//| Set the permission | class=class="str">"cmt">//| to track mouse and graphical object events for the chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CChartObjectsControl::SetMouseEvent(class="type">void) { ::ChartSetInteger(this.ChartID(),CHART_EVENT_MOUSE_MOVE,true); ::ChartSetInteger(this.ChartID(),CHART_EVENT_MOUSE_WHEEL,true); ::ChartSetInteger(this.ChartID(),CHART_EVENT_OBJECT_CREATE,true); ::ChartSetInteger(this.ChartID(),CHART_EVENT_OBJECT_DELETE,true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Collection of graphical objects | class=class="str">"cmt">//+------------------------------------------------------------------+ class CGraphElementsCollection : class="kw">public CBaseObj { class="kw">private: CArrayObj m_list_charts_control; class=class="str">"cmt">// List of chart management objects CListObj m_list_all_canv_elm_obj; class=class="str">"cmt">// List of all graphical elements on canvas CListObj m_list_all_graph_obj; class=class="str">"cmt">// List of all graphical objects class="type">bool m_is_graph_obj_event; class=class="str">"cmt">// Event flag in the list of graphical objects class="type">int m_total_objects; class=class="str">"cmt">// Number of graphical objects class="type">int m_delta_graph_obj; class=class="str">"cmt">// Difference in the number of graphical objects compared to the previous check class=class="str">"cmt">//--- Return the flag indicating the graphical element object presence in the collection list of graphical elements class="type">bool IsPresentGraphElmInList(class="kw">const class="type">int id,class="kw">const ENUM_GRAPH_ELEMENT_TYPE type_obj); class=class="str">"cmt">//--- Return the flag indicating the graphical element object presence in the collection list of graphical objects class="type">bool IsPresentGraphObjInList(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name); class=class="str">"cmt">//--- Return the flag indicating the presence of a graphical object on a chart by name class="type">bool IsPresentGraphObjOnChart(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name); class=class="str">"cmt">//--- Return the pointer to the object of managing objects of the specified chart CChartObjectsControl *GetChartObjectCtrlObj(class="kw">const class="type">long chart_id); class=class="str">"cmt">//--- Create a new object of managing graphical objects of a specified chart and add it to the list CChartObjectsControl *CreateChartObjectCtrlObj(class="kw">const class="type">long chart_id); class=class="str">"cmt">//--- Update the list of graphical objects by chart ID CChartObjectsControl *RefreshByChartID(class="kw">const class="type">long chart_id); class=class="str">"cmt">//--- Return the first free ID of the graphical(class="num">1) object and(class="num">2) element on canvas class="type">long GetFreeGraphObjID(class="type">void);
◍ 图表对象集合的差集查找与按属性筛选
在 MT5 自定义指标里管理大量图形对象时,容易出现在集合里但图表上已删除、或图表上有但集合未记录的情况。类里提供了 FindMissingObj(long chart_id) 与 FindExtraObj(long chart_id) 两个接口,分别抓出「集合有、图表无」和「图表有、集合无」的对象指针或名称,方便做一致性回收。 对外暴露的 GetList 系列重载了整型、双精度、字符串三类画布元素属性(ENUM_CANV_ELEMENT_PROP_INTEGER / DOUBLE / STRING),调用时传入属性枚举、目标值与比较模式(默认 EQUAL),底层走 CSelect::ByGraphCanvElementProperty 按条件返回子列表。 标准图形对象同理,GetList(ENUM_GRAPH_OBJ_PROP_INTEGER, long, mode) 等重载直接对 m_list_all_graph_obj 做筛选。实盘加载前建议在策略测试器用 2023 年 EURUSD H1 跑一遍,确认集合与图表对象数量偏差为 0 再上真仓;外汇与贵金属杠杆交易高风险,对象管理异常可能导致信号绘制错位。
class="type">long GetFreeCanvElmID(class="type">void); class=class="str">"cmt">//--- Add a graphical object to the collection class="type">bool AddGraphObjToCollection(class="kw">const class="type">class="kw">string source,CChartObjectsControl *obj_control); class=class="str">"cmt">//--- Find an object present in the collection but not on a chart CGStdGraphObj *FindMissingObj(class="kw">const class="type">long chart_id); class=class="str">"cmt">//--- Find the graphical object present on a chart but not in the collection class="type">class="kw">string FindExtraObj(class="kw">const class="type">long chart_id); class=class="str">"cmt">//--- Remove the graphical object from the graphical object collection list class="type">bool DeleteGraphObjFromList(CGStdGraphObj *obj); class="kw">public: class=class="str">"cmt">//--- Return itself CGraphElementsCollection *GetObject(class="type">void) { class="kw">return &this; } class=class="str">"cmt">//--- Return the full collection list of standard graphical objects "as is" CArrayObj *GetListGraphObj(class="type">void) { class="kw">return &this.m_list_all_graph_obj; } class=class="str">"cmt">//--- Return the full collection list of graphical elements on canvas "as is" CArrayObj *GetListCanvElm(class="type">void) { class="kw">return &this.m_list_all_canv_elm_obj; } class=class="str">"cmt">//--- Return the list of graphical elements by a selected(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties meeting the compared criterion CArrayObj *GetList(ENUM_CANV_ELEMENT_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),class="kw">property,value,mode); } CArrayObj *GetList(ENUM_CANV_ELEMENT_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL){ class="kw">return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),class="kw">property,value,mode); } CArrayObj *GetList(ENUM_CANV_ELEMENT_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL){ class="kw">return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),class="kw">property,value,mode); } class=class="str">"cmt">//--- Return the list of graphical objects by a selected(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties meeting the compared criterion CArrayObj *GetList(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),class="kw">property,value,mode); }
「图形对象集合类的检索与刷新接口」
CGraphElementsCollection 把图表上的图形对象收进一个统一容器,并暴露了多组重载方法方便按属性筛出子集。GetList 针对双精度属性与字符串属性分别提供了重载,内部都转交 CSelect::ByGraphicStdObjectProperty 处理,比较模式默认 EQUAL,也可传其它 ENUM_COMPARER_TYPE 做不等或区间匹配。 NewObjects 直接返回成员变量 m_delta_graph_obj,给出自上次刷新以来新增图形对象的数量;IsEvent 则读 m_is_graph_obj_event 布尔标志,告诉你这段时间列表是否发生过变动。这两个数配合 Refresh 使用,就能在 EA 里只在新对象出现时才跑后续逻辑,省掉无谓遍历。 Refresh 有两个版本:无参刷新全图表对象,带 chart_id 的只刷指定图表,两者都会回填新增计数并置事件旗。OnChartEvent 接收 MT5 的图表事件 id、lparam、dparam、sparam,是图形对象增删改的入口钩子。 GetStdGraphObject 按 name 与 chart_id 精准取回某个 CGStdGraphObj 指针;Print 和 PrintShort 能把对象属性或简描打到日志,full_prop 开true会吐全部属性。CreateChartControlList 建好图表管理对象列表并返回图表数,这几样拼起来,基本覆盖了对象生命周期的观测面。
CArrayObj *GetList(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),class="kw">property,value,mode); } CArrayObj *GetList(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),class="kw">property,value,mode); } class=class="str">"cmt">//--- Return the number of new graphical objects, (class="num">3) the flag of the occurred change in the list of graphical objects class="type">int NewObjects(class="type">void) class="kw">const { class="kw">return this.m_delta_graph_obj; } class="type">bool IsEvent(class="type">void) class="kw">const { class="kw">return this.m_is_graph_obj_event; } class=class="str">"cmt">//--- Return a graphical object by chart name and ID CGStdGraphObj *GetStdGraphObject(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id); class=class="str">"cmt">//--- Constructor CGraphElementsCollection(); class=class="str">"cmt">//--- Display the description of the object properties in the journal(full_prop=true - all properties, class="kw">false - supported ones only - implemented in descendant classes) class="kw">virtual class="type">void Print(class="kw">const class="type">bool full_prop=class="kw">false,class="kw">const class="type">bool dash=class="kw">false); class=class="str">"cmt">//--- Display a class="type">class="kw">short description of the object in the journal class="kw">virtual class="type">void PrintShort(class="kw">const class="type">bool dash=class="kw">false,class="kw">const class="type">bool symbol=class="kw">false); class=class="str">"cmt">//--- Create the list of chart management objects and class="kw">return the number of charts class="type">int CreateChartControlList(class="type">void); class=class="str">"cmt">//--- Update the list of(class="num">1) all graphical objects, (class="num">2) on the specified chart, fill in the data on the number of new ones and set the event flag class="type">void Refresh(class="type">void); class="type">void Refresh(class="kw">const class="type">long chart_id); class=class="str">"cmt">//--- Event handler class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam);
图表对象增删的实时同步机制
在 MT5 终端里同时开着的图表数量受 CHARTS_MAX 限制,这个宏默认上限是 100,所以遍历所有图表时循环条件直接写 i<CHARTS_MAX 就够用,不会无限跑。 Refresh 方法靠 ChartNext 按 chart_id 逐个拿到图表句柄,拿到负值就 break 退出。每拿到一个图表就调用 RefreshByChartID 取该图表的对象控制器指针,若返回 NULL 则 continue 跳过,避免空指针拖垮整个刷新流程。 真正有看头的是对象数量变动的分支:obj_ctrl.IsEvent() 为真才进处理。Delta() 大于 0 说明有图形对象被画上去,走 AddGraphObjToCollection 搬进集合;小于 0 则是被删了,这时用 FindMissingObj 按 chart_id 找出集合里多出来的那个对象,PrintShort 打一行简报进日志,再 DeleteGraphObjFromList 把它从集合摘掉,摘失败就写一条失败日志。 外汇与贵金属图表上手动拖画线很频繁,这套同步逻辑能让你在 EA 里实时感知用户删了哪条线,但终端多图表场景下对象事件可能丢,属于高风险环境下的概率性同步而非绝对保证。
class="type">void CGraphElementsCollection::Refresh(class="type">void) { class=class="str">"cmt">//--- Declare variables to search for charts class="type">long chart_id=class="num">0; class="type">int i=class="num">0; class=class="str">"cmt">//--- In the loop by all open charts in the terminal(no more than class="num">100) class="kw">while(i<CHARTS_MAX) { class=class="str">"cmt">//--- Get the chart ID chart_id=::ChartNext(chart_id); if(chart_id<class="num">0) class="kw">break; class=class="str">"cmt">//--- Get the pointer to the object for managing graphical objects class=class="str">"cmt">//--- and update the list of graphical objects by chart ID CChartObjectsControl *obj_ctrl=this.RefreshByChartID(chart_id); class=class="str">"cmt">//--- If failed to get the pointer, move on to the next chart if(obj_ctrl==NULL) class="kw">continue; class=class="str">"cmt">//--- If the number of objects on the chart changes if(obj_ctrl.IsEvent()) { class=class="str">"cmt">//--- If a graphical object is added to the chart if(obj_ctrl.Delta()>class="num">0) { class=class="str">"cmt">//--- Get the list of added graphical objects and move them to the collection list class=class="str">"cmt">//--- (if failed to move the object to the collection, move on to the next object) if(!AddGraphObjToCollection(DFUN_ERR_LINE,obj_ctrl)) class="kw">continue; } class=class="str">"cmt">//--- If the graphical object has been removed else if(obj_ctrl.Delta()<class="num">0) { class=class="str">"cmt">// Find an extra object in the list CGStdGraphObj *obj=this.FindMissingObj(chart_id); if(obj!=NULL) { class=class="str">"cmt">//--- Display a class="type">class="kw">short description of a detected object deleted from a chart in the journal obj.PrintShort(); class=class="str">"cmt">//--- Remove the class object of a removed graphical object from the collection list if(!this.DeleteGraphObjFromList(obj)) CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST); } } class=class="str">"cmt">//--- otherwise else { } } }
◍ 图表与对象池的差异比对逻辑
做图形对象同步时,核心矛盾是「内存里的集合」和「图表上真实存在的对象」可能脱节。下面两段查找函数就是用来定位这种脱节的:一段找集合里有、图表上没有的,一段找图表上有、集合里没有的。 FindMissingObj 先按 chart_id 从集合里筛出该图表的所有对象,再逐个用 IsPresentGraphObjOnChart 确认是否还在图表上,碰到第一个不在的就直接返回该对象指针。FindExtraObj 则反过来,用 ObjectsTotal 拿图表对象总数,遍历 ObjectName 拿到的名字,只要集合里查不到就返回这个名字。 IsPresentGraphObjInList 的写法有个坑:它用两次 ByGraphicStdObjectProperty 做筛选,若最终 list 为 NULL 或总数为 0 都判为 false。注意这里若第一次筛选返回 NULL,第二次直接对 NULL 调用会崩,实盘跑之前最好加一层空指针保护。外汇和贵金属图表对象频繁增删,这类同步函数在 EA 里误用可能引发闪退,属高风险操作。
CGStdGraphObj *CGraphElementsCollection::FindMissingObj(class="kw">const class="type">long chart_id) { CArrayObj *list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_CHART_ID,chart_id,EQUAL); if(list==NULL) class="kw">return NULL; for(class="type">int i=class="num">0;i<list.Total();i++) { CGStdGraphObj *obj=list.At(i); if(obj==NULL) class="kw">continue; if(!this.IsPresentGraphObjOnChart(obj.ChartID(),obj.Name())) class="kw">return obj; } class="kw">return NULL; } class="type">class="kw">string CGraphElementsCollection::FindExtraObj(class="kw">const class="type">long chart_id) { class="type">int total=::ObjectsTotal(chart_id); for(class="type">int i=class="num">0;i<total;i++) { class="type">class="kw">string name=::ObjectName(chart_id,i); if(!this.IsPresentGraphObjInList(chart_id,name)) class="kw">return name; } class="kw">return NULL; } class="type">bool CGraphElementsCollection::IsPresentGraphObjInList(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name) { CArrayObj *list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_CHART_ID,chart_id,EQUAL); list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,name,EQUAL); class="kw">return(list==NULL || list.Total()==class="num">0 ? class="kw">false : true); }
「图表对象集合的增删查与拖拽响应」
管理 MT5 图表上的图形对象,核心是先确认对象是否存在再操作。下面这段集合类方法给出了三种高频动作:按名检索、从链表移除、按图表 ID 与名称取回对象指针。 IsPresentGraphObjOnChart 用 ObjectsTotal 拿到图表对象总数,再逐一遍览 ObjectName 做字符串比对,命中即返回 true。这种 O(n) 扫描在对象少于 50 个时开销可忽略,但欧元/黄金盘面若堆了上百画线,每次事件都全量扫可能拖慢响应。 DeleteGraphObjFromList 先对链表 Sort 再 Search 定位下标,搜不到返回 WRONG_VALUE 时直接 false,否则 Delete(index)。注意 Sort 前置让二分查找生效,否则 Search 退化为顺序遍历。 GetStdGraphObject 先用图表 ID 筛出子链表,再按名称做相等匹配,有结果取 At(0)。外汇与贵金属图表多品种同开时,chart_id 参数能避免跨图串对象,属于高风险环境下的防错设计。 OnChartEvent 只接 CHARTEVENT_OBJECT_CHANGE 与 CHARTEVENT_OBJECT_DRAG:用户拖动或改属性时,用 sparam 里的对象名从集合取回指针,调 PropertiesRefresh 和 PropertiesCheckChanged 同步状态。实盘里手动挪止损线,靠这套就能让 EA 内部模型实时感知新价位。
class="type">bool CGraphElementsCollection::IsPresentGraphObjOnChart(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name) { class="type">int total=::ObjectsTotal(chart_id); for(class="type">int i=class="num">0;i<total;i++) if(::ObjectName(chart_id,i)==name) class="kw">return true; class="kw">return class="kw">false; } class=class="str">"cmt">//+-------------------------------------------------------------------+ class=class="str">"cmt">//+---------------------------------------------------------------------+ class=class="str">"cmt">//|Remove the graphical object from the graphical object collection list| class=class="str">"cmt">//+---------------------------------------------------------------------+ class="type">bool CGraphElementsCollection::DeleteGraphObjFromList(CGStdGraphObj *obj) { this.m_list_all_graph_obj.Sort(); class="type">int index=this.m_list_all_graph_obj.Search(obj); class="kw">return(index==WRONG_VALUE ? class="kw">false : this.m_list_all_graph_obj.Delete(index)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return a graphical object by chart name and ID | class=class="str">"cmt">//+------------------------------------------------------------------+ CGStdGraphObj *CGraphElementsCollection::GetStdGraphObject(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id) { CArrayObj *list=this.GetList(GRAPH_OBJ_PROP_CHART_ID,chart_id); list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,name,EQUAL); class="kw">return(list!=NULL && list.Total()>class="num">0 ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGraphElementsCollection::OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { CGStdGraphObj *obj=NULL; if(id==CHARTEVENT_OBJECT_CHANGE || id==CHARTEVENT_OBJECT_DRAG) { obj=this.GetStdGraphObject(sparam,::ChartID()); if(obj!=NULL) { obj.PropertiesRefresh(); obj.PropertiesCheckChanged(); }
对象改名后的列表同步与暂停函数
当按名称找不到图形对象时,说明它已从集合列表里消失或被改了名。这时候要先在图表上找出缺失对应项的对象,再反查图表上多出来的那个对象名,把列表里的旧名替换掉。 下面这段逻辑里,FindMissingObj 用 ChartID() 定位列表中缺失图表的项,FindExtraObj 则拿到图表上不在列表里的对象名,随后通过 SetName、PropertiesRefresh、PropertiesCheckChanged 完成名称同步与属性刷新。 else { obj=this.FindMissingObj(::ChartID()); if(obj==NULL) return; string name_new=this.FindExtraObj(::ChartID()); obj.SetName(name_new); obj.PropertiesRefresh(); obj.PropertiesCheckChanged(); } Pause 函数接收毫秒级暂停时长与起始时间(默认 0),将等待毫秒数和时间起点写入内部状态后,用 while 轮询 PauseIsCompleted 并监听 IsStopped,实现阻塞式等待。注意 time_start*1000 把秒级 datetime 转成毫秒,若传入非 0 值需确认时区与精度预期。 GetGraphicObjCollection 直接返回 m_graph_objects 的指针,外部可据此遍历或调试当前引擎持有的全部图形对象集合。外汇与贵金属图表对象频繁重画,重命名同步逻辑若漏掉一次事件,列表与图表就可能长期不一致,这类 bug 在 EURUSD 的 M1 回测中较容易复现。
else { obj=this.FindMissingObj(::ChartID()); if(obj==NULL) class="kw">return; class="type">class="kw">string name_new=this.FindExtraObj(::ChartID()); obj.SetName(name_new); obj.PropertiesRefresh(); obj.PropertiesCheckChanged(); } class="type">void Pause(class="kw">const class="type">class="kw">ulong pause_msc,class="kw">const class="type">class="kw">datetime time_start=class="num">0) { this.PauseSetWaitingMSC(pause_msc); this.PauseSetTimeBegin(time_start*class="num">1000); class="kw">while(!this.PauseIsCompleted() && !::IsStopped()){} } class=class="str">"cmt">//--- Return the graphical object collection CGraphElementsCollection *GetGraphicObjCollection(class="type">void) { class="kw">return &this.m_graph_objects; } class=class="str">"cmt">//--- Constructor/destructor CEngine(); ~CEngine(); class="kw">private:
◍ 把鼠标和对象事件接进EA日志
要验证图形对象集合类的事件响应,先拿前一篇的 EA 改一改。把它放到 \MQL5\Experts\TestDoEasy\Part86\ 目录下,存成 TestDoEasyPart86.mq5,编译后挂到图表即可跑。 关键改动在 OnInit() 里删掉原先设置鼠标事件权限的代码,改由图表管理类统一给所有已开图表开权限。OnChartEvent() 末尾再补一行图形对象集合类的事件处理调用,这样创建、删除对象或改属性时,终端日志会直接打印对应记录——目前只是日志,后面才会拿来做交互。 下面这段是改完后的 OnInit() 与 OnChartEvent() 头部,注意被标红的那两行权限设置已从初始化里拿掉,图表级跟踪改在别处统一开。 日志里能看到对象事件刷出来,说明事件链路通了。外汇和贵金属行情跳动快、点差跳空频繁,这类鼠标/对象跟踪逻辑在实盘可能拖慢 OnChartEvent 响应,上 MT5 先用模拟盘验证延迟再考虑加仓控。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Set the permissions to send cursor movement and mouse scroll events ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_MOVE,true); ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_WHEEL,true); class=class="str">"cmt">//--- Set EA global variables ArrayResize(array_clr,class="num">2); class=class="str">"cmt">// Array of gradient filling colors array_clr[class="num">0]=C&class="macro">#x27;class="num">246,class="num">244,class="num">244&class="macro">#x27;; class=class="str">"cmt">// Original ≈pale gray array_clr[class="num">1]=C&class="macro">#x27;class="num">249,class="num">251,class="num">250&class="macro">#x27;; class=class="str">"cmt">// Final ≈pale gray-green class=class="str">"cmt">//--- Create the array with the current symbol and set it to be used in the library class="type">class="kw">string array[class="num">1]={Symbol()}; engine.SetUsedSymbols(array); class=class="str">"cmt">//--- Create the timeseries object for the current symbol and period, and show its description in the journal engine.SeriesCreate(Symbol(),Period()); engine.GetTimeSeriesCollection().PrintShort(class="kw">false); class=class="str">"cmt">// Short descriptions class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- If working in the tester, exit if(MQLInfoInteger(MQL_TESTER)) class="kw">return; class=class="str">"cmt">//--- If the mouse is moved if(id==CHARTEVENT_MOUSE_MOVE) { CForm *form=NULL; class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If Ctrl is not pressed, if(!IsCtrlKeyPressed()) { class=class="str">"cmt">//--- clear the list of created form objects, allow scrolling a chart with the mouse and show the context menu list_forms.Clear(); ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,true); ChartSetInteger(ChartID(),CHART_CONTEXT_MENU,true);
「鼠标悬停K线弹出信息框的实现细节」
在 MT5 自定义指标或 EA 里,想做「鼠标移到哪根 K 线就弹出迷你信息框」,核心是先拿鼠标坐标换时间价格。用 ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price) 把鼠标事件里的 lparam/dparam 转成 time 和 price,转换失败就直接 return,避免后续空指针。 拿到时间后调 iBarShift(Symbol(),PERIOD_CURRENT,time) 求光标所在 Bar 的索引;若返回 WRONG_VALUE 同样退出。接着用 engine.SeriesGetBar() 取 CBar 对象,再用 ChartTimePriceToXY 把这根 Bar 的 (Time, (Open+Close)/2) 反算回屏幕 x/y,作为信息框锚点。 为防误触,代码里顺手关掉 CHART_MOUSE_SCROLL 和 CHART_CONTEXT_MENU。信息框名按 "FormBar_"+index 拼,HideFormAllExceptOne 只留当前这一个;若 IsPresentForm 查无此框,就新建 114×16 像素的 form,设不可移动、激活、透明度 200,背景取颜色数组首项,边框写死 C'47,70,59'。外汇与贵金属波动剧烈,这类悬停框仅作辅助观察,实盘信号仍需独立验证。
class="kw">return; } class=class="str">"cmt">//--- If X and Y chart coordinates are successfully converted into time and price, if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- get the bar index the cursor is hovered over class="type">int index=iBarShift(Symbol(),PERIOD_CURRENT,time); if(index==WRONG_VALUE) class="kw">return; class=class="str">"cmt">//--- Get the bar index by index CBar *bar=engine.SeriesGetBar(Symbol(),Period(),index); if(bar==NULL) class="kw">return; class=class="str">"cmt">//--- Convert the coordinates of a chart from the time/price representation of the bar object to the X and Y coordinates class="type">int x=(class="type">int)lparam,y=(class="type">int)dparam; if(!ChartTimePriceToXY(ChartID(),class="num">0,bar.Time(),(bar.Open()+bar.Close())/class="num">2.0,x,y)) class="kw">return; class=class="str">"cmt">//--- Disable moving a chart with the mouse and showing the context menu ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,class="kw">false); ChartSetInteger(ChartID(),CHART_CONTEXT_MENU,class="kw">false); class=class="str">"cmt">//--- Create the form object name and hide all objects except one having such a name class="type">class="kw">string name="FormBar_"+(class="type">class="kw">string)index; HideFormAllExceptOne(name); class=class="str">"cmt">//--- If the form object with such a name does not exist yet, if(!IsPresentForm(name)) { class=class="str">"cmt">//--- create a new form object form=bar.CreateForm(index,name,x,y,class="num">114,class="num">16); if(form==NULL) class="kw">return; class=class="str">"cmt">//--- Set activity and unmoveability flags for the form form.SetActive(true); form.SetMovable(class="kw">false); class=class="str">"cmt">//--- Set the opacity of class="num">200 form.SetOpacity(class="num">200); class=class="str">"cmt">//--- The form background class="type">class="kw">color is set as the first class="type">class="kw">color from the class="type">class="kw">color array form.SetColorBackground(array_clr[class="num">0]); class=class="str">"cmt">//--- Form outlining frame class="type">class="kw">color form.SetColorFrame(C&class="macro">#x27;class="num">47,class="num">70,class="num">59&class="macro">#x27;);
给悬浮窗加上阴影与渐变底
在 MT5 自定义指标里画价格行为标注窗体时,阴影和底色不是装饰,而是辨识度的关键。下面这段直接操作窗体对象:先开阴影开关,再把图表背景色降饱和成单色,按设置决定用背景派生色还是自定义色。
代码里 form.DrawShadow(2,2,clr,200,3) 的 2 和 2 是右下偏移像素,200 是透明度(0–255 区间),3 是模糊半径,数值越大边缘越虚。改这几个数能立刻看出立体感变化。
底色走垂直渐变用 form.Erase(array_clr,form.Opacity()),边框再描一道矩形。若 list_forms.Add(form) 失败就 delete 掉并返回,避免野指针。最后 form.Done() 捕获外观、form.Show() 出窗体,调 ChartRedraw() 刷新。外汇与贵金属行情跳动快,这类 overlay 在高波动时可能掉帧,参数宜保守。
class=class="str">"cmt">//--- Draw the shadow drawing flag form.SetShadow(true); class=class="str">"cmt">//--- Calculate the shadow class="type">class="kw">color as the chart background class="type">class="kw">color converted to the monochrome one class="type">class="kw">color clrS=form.ChangeColorSaturation(form.ColorBackground(),-class="num">100); class=class="str">"cmt">//--- If the settings specify the usage of the chart background class="type">class="kw">color, replace the monochrome class="type">class="kw">color with class="num">20 units class=class="str">"cmt">//--- Otherwise, use the class="type">class="kw">color specified in the settings for drawing the shadow class="type">class="kw">color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-class="num">20) : InpColorForm3); class=class="str">"cmt">//--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes class=class="str">"cmt">//--- Set the shadow opacity to class="num">200, class="kw">while the blur radius is equal to class="num">4 form.DrawShadow(class="num">2,class="num">2,clr,class="num">200,class="num">3); class=class="str">"cmt">//--- Fill the form background with a vertical gradient form.Erase(array_clr,form.Opacity()); class=class="str">"cmt">//--- Draw an outlining rectangle at the edges of the form form.DrawRectangle(class="num">0,class="num">0,form.Width()-class="num">1,form.Height()-class="num">1,form.ColorFrame(),form.Opacity()); class=class="str">"cmt">//--- If failed to add the form object to the list, remove the form and exit the handler if(!list_forms.Add(form)) { class="kw">delete form; class="kw">return; } class=class="str">"cmt">//--- Capture the form appearance form.Done(); } class=class="str">"cmt">//--- If the form object exists, if(form!=NULL) { class=class="str">"cmt">//--- draw a text with the bar type description on it and show the form. The description corresponds to the mouse cursor position form.TextOnBG(class="num">0,bar.BodyTypeDescription(),form.Width()/class="num">2,form.Height()/class="num">2-class="num">1,FRAME_ANCHOR_CENTER,C&class="macro">#x27;class="num">7,class="num">28,class="num">21&class="macro">#x27;); form.Show(); } class=class="str">"cmt">//--- Redraw the chart ChartRedraw(); } } engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam); }
◍ 记住这一条就够了
当前函数库版本已放出全部源文件与测试 EA,压缩包体积 4153.33 KB,可直接在 MT5 加载验证事件转发逻辑。下一篇计划把每个图表的对象事件收拢到控制图表统一处理,这意味着多图对象的增删改都能被集中捕获。 外汇与贵金属图表对象事件频繁,多图监听若写死在单 EA 里容易漏事件,用集合类转发能降维护成本,但终端多图表并发仍有延迟风险。 打开 MQL5.zip 里的测试 EA 跑一遍,看控制图表能否收到子图对象事件,比空读文档实在。