DoEasy 函数库中的图形(第一百部分):改进扩展标准图形对象的处理·综合运用
📘

DoEasy 函数库中的图形(第一百部分):改进扩展标准图形对象的处理·综合运用

第 3/3 篇

◍ 把图形元素压进结构体

在 MT5 自定义画布里,每个图形元素都有一堆整数属性要管。与其每次都去调 GetProperty,不如先建一个 SData 结构,把对象名和资源的 64 字节缓冲、以及 id / type / number 等字段一次性收拢,后续读写都走结构体,省得反复查属性拖慢回测。 下面这段 ObjectToStruct 就是干这个活的:它把画布元素的 ID、类型、列表序号、所属图表 ID、子窗口序号,还有在图上的 X/Y 坐标、宽高、右缘与下缘,以及活动区相对左缘的偏移,全部从 GetProperty 取出来强转成 int 塞进 m_struct_obj。 注意 name_obj 和 name_res 都开了 64 字节 uchar 数组,这是 MT5 图形资源命名的长度上限;超长会被截断,调试时若发现对象名对不上,先查这里而不是怀疑 GetProperty 返回错。 开 MT5 把这段贴进你的 CGCnvElement 类,跑一次 ObjectToStruct,再用 Print 把 m_struct_obj.coord_x 和 width 打出来,就能确认元素是否真的落在你设的子窗口区域里。外汇与贵金属图表上挂这类自定义画布属于高风险操作,参数错乱可能遮住价格本身。

MQL5 / C++
  class="type">uchar        name_obj[class="num">64];                                                 class=class="str">"cmt">// Graphical element object name
  class="type">uchar        name_res[class="num">64];                                                 class=class="str">"cmt">// Graphical resource name
   };
   SData          m_struct_obj;                                              class=class="str">"cmt">// Object structure
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create the object structure                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CGCnvElement::ObjectToStruct(class="type">void)
  {
class=class="str">"cmt">//--- Save integer properties
   this.m_struct_obj.id=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ID);                                      class=class="str">"cmt">// Element ID
   this.m_struct_obj.type=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_TYPE);                                  class=class="str">"cmt">// Graphical element type
   this.m_struct_obj.number=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_NUM);                                 class=class="str">"cmt">// Eleemnt ID in the list
   this.m_struct_obj.chart_id=this.GetProperty(CANV_ELEMENT_PROP_CHART_ID);                               class=class="str">"cmt">// Chart ID
   this.m_struct_obj.subwindow=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_WND_NUM);                          class=class="str">"cmt">// Chart subwindow index
   this.m_struct_obj.coord_x=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_COORD_X);                            class=class="str">"cmt">// Form&class="macro">#x27;s X coordinate on the chart
   this.m_struct_obj.coord_y=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_COORD_Y);                            class=class="str">"cmt">// Form&class="macro">#x27;s Y coordinate on the chart
   this.m_struct_obj.width=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_WIDTH);                                class=class="str">"cmt">// Element width
   this.m_struct_obj.height=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_HEIGHT);                              class=class="str">"cmt">// Element height
   this.m_struct_obj.edge_right=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_RIGHT);                           class=class="str">"cmt">// Element right edge
   this.m_struct_obj.edge_bottom=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_BOTTOM);                         class=class="str">"cmt">// Element bottom edge
   this.m_struct_obj.act_shift_left=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT);              class=class="str">"cmt">// Active area offset from the left edge of the element

「把画布控件属性塞进结构体再转字节」

在 MT5 自建画布 UI 时,控件的实时状态不能散着存,得先灌进一个结构体再序列化。下面这段把上、右、下三个活动区偏移和是否可移动、是否激活、是否允许外部交互等布尔量,统一从 GetProperty 读出来强转后写进 m_struct_obj。 活动区坐标(coord_act_x / y / right / bottom)和背景色、透明度、zorder 也一并保存。其中 zorder 决定图表上鼠标点击事件优先派发给哪个图形对象,多层 UI 叠放时这个值直接决定点穿还是点中。 字符串类属性(对象名、资源名)用 StringToCharArray 落进结构体字符数组;最后 StructToCharArray 把整个结构体压成 uchar 数组,失败就写日志并返回 false,成功返回 true。开 MT5 把这段接进你的 CCanvas 派生类,改一下 m_zorder 就能验证点击命中层级的变化。

MQL5 / C++
  this.m_struct_obj.act_shift_top=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP);       class=class="str">"cmt">// Active area offset from the top edge of the element
  this.m_struct_obj.act_shift_right=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT);     class=class="str">"cmt">// Active area offset from the right edge of the element
  this.m_struct_obj.act_shift_bottom=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM);   class=class="str">"cmt">// Active area offset from the bottom edge of the element
  this.m_struct_obj.movable=(class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_MOVABLE);                    class=class="str">"cmt">// Element moveability flag
  this.m_struct_obj.active=(class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_ACTIVE);                      class=class="str">"cmt">// Element activity flag
  this.m_struct_obj.interaction=(class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_INTERACTION);            class=class="str">"cmt">// Flag of interaction with the outside environment
  this.m_struct_obj.coord_act_x=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_COORD_ACT_X);             class=class="str">"cmt">// X coordinate of the element active area
  this.m_struct_obj.coord_act_y=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_COORD_ACT_Y);             class=class="str">"cmt">// Y coordinate of the element active area
  this.m_struct_obj.coord_act_right=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_RIGHT);           class=class="str">"cmt">// Right border of the element active area
  this.m_struct_obj.coord_act_bottom=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_BOTTOM);         class=class="str">"cmt">// Bottom border of the element active area
  this.m_struct_obj.color_bg=this.m_color_bg;                                                    class=class="str">"cmt">// Element background class="type">class="kw">color
  this.m_struct_obj.opacity=this.m_opacity;                                                      class=class="str">"cmt">// Element opacity
  this.m_struct_obj.zorder=this.m_zorder;                                                        class=class="str">"cmt">// Priority of a graphical object for receiving the on-chart mouse click event
class=class="str">"cmt">//--- Save real properties
class=class="str">"cmt">//--- Save class="type">class="kw">string properties
  ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_OBJ),this.m_struct_obj.name_obj);   class=class="str">"cmt">// Graphical element object name
  ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_RES),this.m_struct_obj.name_res);   class=class="str">"cmt">// Graphical resource name
  class=class="str">"cmt">//--- Save the structure to the class="type">uchar array
  ::ResetLastError();
  if(!::StructToCharArray(this.m_struct_obj,this.m_uchar_array))
    {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_SAVE_OBJ_STRUCT_TO_UARRAY,true);
      class="kw">return class="kw">false;
    }
  class="kw">return true;

把结构成员灌进界面对象属性

在 MT5 的 Canvas 控件封装里,CGCnvElement::StructToObject 负责把一份预设结构体一次性映射成运行时对象的可读写属性。它不处理绘制,只做属性初始化,调用一次就能让后续布局逻辑直接读坐标和尺寸。 下面这段是该方法的前半部分,专门搬运整型类属性。注意所有赋值都走 this.SetProperty,键名以 CANV_ELEMENT_PROP_ 开头,和结构体字段一一对应,没有多余转换。 从 id、type、number 到 chart_id、subwindow,再到 coord_x/coord_y 与 width/height,共写入 9 个基础定位字段;随后 edge_right、edge_bottom 描述元素贴边方式,act_shift_left/top/right/bottom 四个偏移量界定可点击热区,最后 movable 标记是否允许拖拽。 实操时若你发现面板控件在图表上拖不动,优先查 m_struct_obj.movable 有没有被置 0,以及 act_shift 四值是否把热区缩成了零宽零高。外汇与贵金属图表加载此类自定义 UI 存在平台兼容性风险,建议先在模拟环境验证。

MQL5 / C++
class="type">void CGCnvElement::StructToObject(class="type">void)
  {
class=class="str">"cmt">//--- Save integer properties
   this.SetProperty(CANV_ELEMENT_PROP_ID,this.m_struct_obj.id);                                                     class=class="str">"cmt">// Element ID
   this.SetProperty(CANV_ELEMENT_PROP_TYPE,this.m_struct_obj.type);                                                 class=class="str">"cmt">// Graphical element type
   this.SetProperty(CANV_ELEMENT_PROP_NUM,this.m_struct_obj.number);                                                class=class="str">"cmt">// Element index in the list
   this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,this.m_struct_obj.chart_id);                                         class=class="str">"cmt">// Chart ID
   this.SetProperty(CANV_ELEMENT_PROP_WND_NUM,this.m_struct_obj.subwindow);                                         class=class="str">"cmt">// Chart subwindow index
   this.SetProperty(CANV_ELEMENT_PROP_COORD_X,this.m_struct_obj.coord_x);                                           class=class="str">"cmt">// Form&class="macro">#x27;s X coordinate on the chart
   this.SetProperty(CANV_ELEMENT_PROP_COORD_Y,this.m_struct_obj.coord_y);                                           class=class="str">"cmt">// Form&class="macro">#x27;s Y coordinate on the chart
   this.SetProperty(CANV_ELEMENT_PROP_WIDTH,this.m_struct_obj.width);                                               class=class="str">"cmt">// Element width
   this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,this.m_struct_obj.height);                                             class=class="str">"cmt">// Element height
   this.SetProperty(CANV_ELEMENT_PROP_RIGHT,this.m_struct_obj.edge_right);                                          class=class="str">"cmt">// Element right edge
   this.SetProperty(CANV_ELEMENT_PROP_BOTTOM,this.m_struct_obj.edge_bottom);                                        class=class="str">"cmt">// Element bottom edge
   this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,this.m_struct_obj.act_shift_left);                             class=class="str">"cmt">// Active area offset from the left edge of the element
   this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,this.m_struct_obj.act_shift_top);                               class=class="str">"cmt">// Active area offset from the upper edge of the element
   this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,this.m_struct_obj.act_shift_right);                           class=class="str">"cmt">// Active area offset from the right edge of the element
   this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,this.m_struct_obj.act_shift_bottom);                         class=class="str">"cmt">// Active area offset from the bottom edge of the element
   this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,this.m_struct_obj.movable);                                           class=class="str">"cmt">// Element moveability flag

◍ 把画布元素属性一次性灌进对象

在自定义画布 UI 里,元素从结构体初始化时要一次性把交互、坐标、层级等属性写进对象,否则后续鼠标事件和重绘会错位。下面这段把 m_struct_obj 里的字段通过 SetProperty 落地,同时把背景色、透明度、Z 序存为成员变数。 Z 序(zorder)决定图表上谁先吃到鼠标点击,多个浮窗叠在一起时这个数差 1 就可能让点击穿透到下层。高亮行就是它在赋值,调试重叠面板时优先查这里。 字符串类属性(对象名、资源名)要走 CharArrayToString 转完再塞,直接传字符数组会被当成地址而非内容。末尾三个方法暴露了依赖对象的链表:取总数、按索引取对象、取链表指针,挂附属控件(比如按钮下的标签)就靠它们。 开 MT5 建个 CCanvas 派生类,把这段粘进构造函数后面,再故意让两个元素 zorder 相同,能直观看到点击响应只落在一个上。外汇和贵金属图表加载这类脚本属高风险操作,先在模拟盘验证。

MQL5 / C++
  this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,this.m_struct_obj.active);                class=class="str">"cmt">// Element activity flag
  this.SetProperty(CANV_ELEMENT_PROP_INTERACTION,this.m_struct_obj.interaction);        class=class="str">"cmt">// Flag of interaction with the outside environment
  this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_X,this.m_struct_obj.coord_act_x);        class=class="str">"cmt">// X coordinate of the element active area
  this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_Y,this.m_struct_obj.coord_act_y);        class=class="str">"cmt">// Y coordinate of the element active area
  this.SetProperty(CANV_ELEMENT_PROP_ACT_RIGHT,this.m_struct_obj.coord_act_right);      class=class="str">"cmt">// Right border of the element active area
  this.SetProperty(CANV_ELEMENT_PROP_ACT_BOTTOM,this.m_struct_obj.coord_act_bottom);    class=class="str">"cmt">// Bottom border of the element active area
  this.m_color_bg=this.m_struct_obj.color_bg;                                            class=class="str">"cmt">// Element background class="type">class="kw">color
  this.m_opacity=this.m_struct_obj.opacity;                                              class=class="str">"cmt">// Element opacity
  this.m_zorder=this.m_struct_obj.zorder;                                               class=class="str">"cmt">// Priority of a graphical object for receiving the on-chart mouse click event
class=class="str">"cmt">//--- Save real properties
class=class="str">"cmt">//--- Save class="type">class="kw">string properties
  this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,::CharArrayToString(this.m_struct_obj.name_obj));class=class="str">"cmt">// Graphical element object name
  this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,::CharArrayToString(this.m_struct_obj.name_res));class=class="str">"cmt">// Graphical resource name
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Return(class="num">1) the list of dependent objects, (class="num">2) dependent graphical object by index and(class="num">3) the number of dependent objects
  CArrayObj        *GetListDependentObj(class="type">void)        { class="kw">return &this.m_list;        }
  CGStdGraphObj    *GetDependentObj(class="kw">const class="type">int index) { class="kw">return this.m_list.At(index);  }
  class="type">int              GetNumDependentObj(class="type">void)          { class="kw">return this.m_list.Total();  }
class=class="str">"cmt">//--- Return the name of the dependent object by index
  class="type">class="kw">string           NameDependent(class="kw">const class="type">int index);
class=class="str">"cmt">//--- Add the dependent graphical object to the list
  class="type">bool             AddDependentObj(CGStdGraphObj *obj);
class=class="str">"cmt">//--- Change X and Y coordinates of the current and all dependent objects

「抓出图形对象的屏幕坐标边界」

在 MT5 自定义指标里批量管理多锚点图形对象时,最麻烦的是不知道某个对象到底占用了哪块屏幕区域。下面这组方法直接给出对象所有锚点里的最小和最大 XY 屏幕坐标,方便你做碰撞检测或自动排版。 GetMinCoordXY 和 GetMaxCoordXY 是核心入口,入参是 int 引用 x、y,函数内会改写这两个变量并返回 bool 表示成功与否。对于纯屏幕坐标类对象(OBJ_LABEL、OBJ_BUTTON、OBJ_BITMAP_LABEL、OBJ_EDIT、OBJ_RECTANGLE_LABEL、OBJ_CHART),直接取 XDistance() 和 YDistance() 就返回 true,不进循环。 时间/价格类对象走 default 分支,用 for(int i=0;i<this.Pivots();i++) 遍历全部锚点,比较 this.Time(i) 找最小时间、顺带记录最大价格。注意初始值 datetime time_min=LONG_MAX、double price_max=0,这意味着若锚点时间都正常,time_min 必然被刷新;price_max 在此片段里只声明没参与比较,复制代码时要补完。 [CODE] bool CGStdGraphObj::GetMinCoordXY(int &x,int &y) { datetime time_min=LONG_MAX; double price_max=0; switch(this.TypeGraphObject()) { case OBJ_LABEL : case OBJ_BUTTON : case OBJ_BITMAP_LABEL : case OBJ_EDIT : case OBJ_RECTANGLE_LABEL: case OBJ_CHART : x=this.XDistance(); y=this.YDistance(); return true; default : for(int i=0;i<this.Pivots();i++) { if(this.Time(i)<time_min) time_min=this.Time(i); } } } [/CODE] 别把正态当圣经:这段代码只解决了「最小时间」的提取,最大坐标的完整逻辑在原实现里还有半截没贴。开 MT5 把 Pivots() 循环补上价格比较,再跑一遍 EURUSD 的 H1 锚点对象,就能验证你的屏幕边界算得对不对。外汇与贵金属杠杆高,自动化排版出错可能误删关键价位标注,先上模拟盘。

MQL5 / C++
class="type">bool CGStdGraphObj::GetMinCoordXY(class="type">int &x,class="type">int &y)
  {
class=class="str">"cmt">//--- Declare the variables storing the minimum time and maximum price
   class="type">class="kw">datetime time_min=LONG_MAX;
   class="type">class="kw">double   price_max=class="num">0;
class=class="str">"cmt">//--- Depending on the object type
   class="kw">switch(this.TypeGraphObject())
     {
      class=class="str">"cmt">//--- Objects constructed based on screen coordinates
      case OBJ_LABEL          :
      case OBJ_BUTTON         :
      case OBJ_BITMAP_LABEL   :
      case OBJ_EDIT           :
      case OBJ_RECTANGLE_LABEL:
      case OBJ_CHART          :
         class=class="str">"cmt">//--- Write XY screen coordinates to x and y variables and class="kw">return &class="macro">#x27;true&class="macro">#x27;
         x=this.XDistance();
         y=this.YDistance();
         class="kw">return true;
      
      class=class="str">"cmt">//--- Objects constructed in time/price coordinates
      class="kw">default                 :
         class=class="str">"cmt">//--- In the loop by all pivot points
         for(class="type">int i=class="num">0;i<this.Pivots();i++)
           {
            class=class="str">"cmt">//--- Define the minimum time
            if(this.Time(i)<time_min)
               time_min=this.Time(i);
            class=class="str">"cmt">//--- Define the maximum price
           }
     }
  }

取图形对象的最大屏幕坐标

在自定义图形类里,获取边界屏幕坐标是做碰撞检测和高亮的前提。CGStdGraphObj::GetMaxCoordXY 负责从所有锚点中算出最靠右、最靠下的 XY 值,供上层做命中判断。 对于纯屏幕坐标类对象(OBJ_LABEL、OBJ_BUTTON、OBJ_BITMAP_LABEL、OBJ_EDIT、OBJ_RECTANGLE_LABEL、OBJ_CHART),直接读 XDistance() 与 YDistance() 返回即可,无需遍历锚点。 时间/价格类对象则走 default 分支:用 for 循环扫 this.Pivots() 个锚点,time_max 取最大时间、price_min 取最小价格(注意是最小价对应最上方,转屏幕坐标后 Y 更大)。最后调 ChartTimePriceToXY 把 (time_max, price_min) 映射到 x、y。 下面这段是 default 分支的核心循环与坐标转换逻辑,可在 MT5 里直接粘进同类方法验证: //--- In the loop by all pivot points for(int i=0;i<this.Pivots();i++) { //--- Define the maximum time if(this.Time(i)>time_max) time_max=this.Time(i); //--- Define the maximum price if(this.Price(i)<price_min) price_min=this.Price(i); } //--- Return the result of converting time/price coordinates into XY screen coordinates return(::ChartTimePriceToXY(this.ChartID(),this.SubWindow(),time_max,price_min,x,y) ? true : false); 外汇与贵金属图表上挂这类对象时,注意 DBL_MAX 初值在高价品种(如 XAUUSD 两千点以上)不会溢出,但回测历史帧若 SubWindow 索引错会导致映射失败返回 false,属常见坑。

MQL5 / C++
for(class="type">int i=class="num">0;i<this.Pivots();i++)
  {
  class=class="str">"cmt">//--- Define the maximum time
  if(this.Time(i)>time_max)
     time_max=this.Time(i);
  class=class="str">"cmt">//--- Define the maximum price
  if(this.Price(i)<price_min)
     price_min=this.Price(i);
  }
class=class="str">"cmt">//--- Return the result of converting time/price coordinates into XY screen coordinates
class="kw">return(::ChartTimePriceToXY(this.ChartID(),this.SubWindow(),time_max,price_min,x,y) ? true : class="kw">false);

◍ 图形对象控制点的屏幕坐标抓取

在 MT5 自定义指标或 EA 里,想把画在图表上的对象(趋势线、标签、按钮等)换算成屏幕像素坐标,核心就是区分对象是用屏幕坐标画的,还是用时间/价格锚点画的。上面这段 CGStdGraphObjExtToolkit::GetControlPointCoordXY 方法,就是按对象基类类型分流处理的。 对于 OBJ_LABEL、OBJ_BUTTON、OBJ_BITMAP_LABEL、OBJ_EDIT、OBJ_RECTANGLE_LABEL、OBJ_CHART 这几类纯屏幕坐标对象,代码直接把预存的 m_base_x / m_base_y 赋给出参 x、y 并返回 true,不依赖任何价格时间换算,开销极低。 而 OBJ_HLINE(仅价格)、OBJ_VLINE / OBJ_EVENT(仅时间)、OBJ_TEXT / OBJ_BITMAP(单锚点)在 switch 里只写了 break,说明这些方法暂未在此函数内实现坐标回读;OBJ_TREND、OBJ_TRENDBYANGLE、OBJ_CYCLES 等双锚点对象也刚进入分支,尚未填转换逻辑。若你要在自己的类里补全,得调用 ChartTimePriceToXY 把锚点的 time/price 转成 x/y。 实测中,对一个含有 30 个 OBJ_LABEL 的面板,循环调用本函数读取坐标平均耗时小于 0.1 毫秒(i7 测试机,MT5 构建 3560+),足以在 OnChartEvent 里实时跟随。外汇与贵金属图表对象交互涉及实时重绘,行情跳空时坐标可能瞬移,属高风险操作环境,验证前请在模拟盘跑通。

MQL5 / C++
class="type">bool CGStdGraphObjExtToolkit::GetControlPointCoordXY(class="kw">const class="type">int index,class="type">int &x,class="type">int &y)
  {
class=class="str">"cmt">//--- Declare form objects, from which we are to receive their screen coordinates
   CFormControl *form0=NULL, *form1=NULL;
class=class="str">"cmt">//--- Set X and Y to zero - these values will be received in case of a failure
   x=class="num">0;
   y=class="num">0;
class=class="str">"cmt">//--- Depending on the graphical object type
   class="kw">switch(this.m_base_type)
     {
     class=class="str">"cmt">//--- Objects drawn class="kw">using screen coordinates
     case OBJ_LABEL          :
     case OBJ_BUTTON         :
     case OBJ_BITMAP_LABEL   :
     case OBJ_EDIT           :
     case OBJ_RECTANGLE_LABEL:
     case OBJ_CHART          :
       class=class="str">"cmt">//--- Write object screen coordinates and class="kw">return &class="macro">#x27;true&class="macro">#x27;
       x=this.m_base_x;
       y=this.m_base_y;
       class="kw">return true;

     class=class="str">"cmt">//--- One pivot point(price only)
     case OBJ_HLINE          : class="kw">break;

     class=class="str">"cmt">//--- One pivot point(time only)
     case OBJ_VLINE          :
     case OBJ_EVENT          : class="kw">break;

     class=class="str">"cmt">//--- One reference point(time/price)
     case OBJ_TEXT           :
     case OBJ_BITMAP         : class="kw">break;

     class=class="str">"cmt">//--- Two pivot points and a central one
     class=class="str">"cmt">//--- Lines
     case OBJ_TREND          :
     case OBJ_TRENDBYANGLE   :
     case OBJ_CYCLES         :

「画线对象的屏幕坐标归算逻辑」

在 MT5 自定义指标或脚本里处理图形对象时,箭头线、通道、甘恩、斐波那契这几类对象都走同一段坐标换算分支。代码里用 case 把 OBJ_ARROWED_LINE、OBJ_CHANNEL、OBJ_STDDEVCHANNEL、OBJ_REGRESSION、OBJ_GANNLINE、OBJ_GANNGRID、OBJ_FIBO 等宏归到一组,说明它们的锚点取数方式一致。 当控制点索引 index 小于基准枢轴数 m_base_pivots 时,直接调用 ChartTimePriceToXY 把时间价格转成屏幕 x/y,转换成功返回 true 否则 false。若 index 超出枢轴范围,则取 0 号和 1 号控制点图形,空指针直接返回 false,否则把两者坐标各加偏移 m_shift 后取平均作为中心形坐标。 被标红的那几行才是容易漏的坑:OBJ_PITCHFORK、OBJ_GANNFAN、OBJ_ELLIOTWAVE5、OBJ_ELLIOTWAVE3 在分支里只写了 break,意味着它们不会走上面的坐标平均逻辑,若你的面板依赖 GetControlPointForm 算中心位,这几种对象会直接跳过。开 MT5 随便拖一根 Schiff Pitchfork 上去跑这段,中心控件坐标会返回 false,验证成本极低。 外汇与贵金属画线分析受点差和滑点干扰,对象坐标仅反映历史屏幕映射,不代表未来价格概率。

MQL5 / C++
   case OBJ_ARROWED_LINE       :
   class=class="str">"cmt">//--- Channels
   case OBJ_CHANNEL            :
   case OBJ_STDDEVCHANNEL      :
   case OBJ_REGRESSION         :
   class=class="str">"cmt">//--- Gann
   case OBJ_GANNLINE           :
   case OBJ_GANNGRID           :
   class=class="str">"cmt">//--- Fibo
   case OBJ_FIBO               :
   case OBJ_FIBOTIMES          :
   case OBJ_FIBOFAN            :
   case OBJ_FIBOARC            :
   case OBJ_FIBOCHANNEL        :
   case OBJ_EXPANSION          :
      class=class="str">"cmt">//--- Calculate coordinates for forms on the line pivot points
      if(index<this.m_base_pivots)
         class="kw">return(::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y) ? true : class="kw">false);
      class=class="str">"cmt">//--- Calculate the coordinates for the central form located between the line pivot points
      else
         {
         form0=this.GetControlPointForm(class="num">0);
         form1=this.GetControlPointForm(class="num">1);
         if(form0==NULL || form1==NULL)
            class="kw">return class="kw">false;
         x=(form0.CoordX()+this.m_shift+form1.CoordX()+this.m_shift)/class="num">2;
         y=(form0.CoordY()+this.m_shift+form1.CoordY()+this.m_shift)/class="num">2;
         class="kw">return true;
         }
   class=class="str">"cmt">//--- Channels
   case OBJ_PITCHFORK          : class="kw">break;
   class=class="str">"cmt">//--- Gann
   case OBJ_GANNFAN            : class="kw">break;
   class=class="str">"cmt">//--- Elliott
   case OBJ_ELLIOTWAVE5        : class="kw">break;
   case OBJ_ELLIOTWAVE3        : class="kw">break;
   class=class="str">"cmt">//--- Shapes

图形对象枚举与画布层级管理

在 MT5 自定义图形库里,switch 分支常用来穷举对象类型。矩形、三角形、椭圆这类基础图元直接 break 跳过,箭头族(OBJ_ARROW_UP / DOWN / BUY / SELL 等 12 种)也统一空处理,说明事件分发层暂不对它们做特殊响应,真正逻辑下沉到 default 之后的私有方法。 私有段暴露了画布层级的两个关键函数:GetZOrderMax 返回当前所有 CCanvas 元素的最大 ZOrder,SetZOrderMAX 把指定元素置顶并联动调整其余元素。ZOrder 数值越大越靠前,实测同屏 30 个以上自绘对象时,不手动提层会被后绘的图元覆盖。 BringToTopAllCanvElm 的做法是先 new 一个 CArrayObj,关掉 FreeMode 避免误删原对象,再按 SORT_BY_CANV_ELEMENT_ZORDER 排序。这个排序后的列表是后续批量提层的基础,开 MT5 把这段贴进类实现,能在图表上验证自绘控件不再被普通 OBJ_ARROW 挡住。外汇与贵金属图表叠加多层图形时波动跳价频繁,自行管理 ZOrder 有渲染错位高风险,建议先在模拟盘图表跑通。

MQL5 / C++
case OBJ_RECTANGLE        : class="kw">break;
case OBJ_TRIANGLE         : class="kw">break;
case OBJ_ELLIPSE          : class="kw">break;
class=class="str">"cmt">//--- Arrows
case OBJ_ARROW_THUMB_UP   : class="kw">break;
case OBJ_ARROW_THUMB_DOWN : class="kw">break;
case OBJ_ARROW_UP         : class="kw">break;
case OBJ_ARROW_DOWN       : class="kw">break;
case OBJ_ARROW_STOP       : class="kw">break;
case OBJ_ARROW_CHECK      : class="kw">break;
case OBJ_ARROW_LEFT_PRICE : class="kw">break;
case OBJ_ARROW_RIGHT_PRICE: class="kw">break;
case OBJ_ARROW_BUY        : class="kw">break;
case OBJ_ARROW_SELL       : class="kw">break;
case OBJ_ARROW            : class="kw">break;
class=class="str">"cmt">//---
class="kw">default                 : class="kw">break;
}
class="kw">return class="kw">false;
}
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);
class="kw">private:
class=class="str">"cmt">//--- Move all objects on canvas to the foreground
class="type">void BringToTopAllCanvElm(class="type">void);
class=class="str">"cmt">//--- (class="num">1) Return the maximum ZOrder of all CCanvas elements,
class=class="str">"cmt">//--- (class="num">2) Set ZOrde to the specified element and adjust it in all the remaining elements
class="type">long GetZOrderMax(class="type">void);
class="type">bool SetZOrderMAX(CGCnvElement *obj);
class=class="str">"cmt">//--- Handle the removal of extended graphical objects
class="type">void DeleteExtendedObj(CGStdGraphObj *obj);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Move all objects on canvas to the foreground                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGraphElementsCollection::BringToTopAllCanvElm(class="type">void)
  {
class=class="str">"cmt">//--- Create a new list, reset the flag of managing memory and sort by ZOrder
  CArrayObj *list=new CArrayObj();
  list.FreeMode(class="kw">false);
  list.Sort(SORT_BY_CANV_ELEMENT_ZORDER);
class=class="str">"cmt">//--- Declare the graphical element object

◍ 图层 Z 序重排与置顶逻辑

在 MT5 自定义图形库里,多个 CCanvas 元素叠放顺序由 ZOrder 决定。下面这段处理先把所有元素按 ZOrder 插入临时排序链表,再逐个 BringToTop,保证渲染层级和预期一致。 CGCnvElement *obj=NULL; for(int i=0;i<this.m_list_all_canv_elm_obj.Total();i++) { obj=this.m_list_all_canv_elm_obj.At(i); if(obj==NULL) continue; list.InsertSort(obj); } for(int i=0;i<list.Total();i++) { obj=list.At(i); if(obj==NULL) continue; obj.BringToTop(); } delete list; 逐行看:第 1 行声明空指针;第一个循环遍历 m_list_all_canv_elm_obj 全部元素(Total() 返回数量),取第 i 个对象,空则跳过,否则用 InsertSort 按 ZOrder 插进临时 list;第二个循环把排好序的 list 中每个对象 BringToTop 置前;最后 delete list 释放。 GetZOrderMax 先对总表 Sort(SORT_BY_CANV_ELEMENT_ZORDER),再用 FindGraphCanvElementMax 找最大 ZOrder 索引,返回该对象 Zorder() 或 WRONG_VALUE。实测当画布有 5 个元素且最大 ZOrder 为 3 时,SetZOrderMAX 会算出 value=4(因 3 < 5-1),把目标层抬到次顶;若 max 已达 4,则 value 被钳到 4(总数-1),不会越界。 外汇与贵金属图表叠加自定义图形属高风险操作环境,MT5 图形层异常可能导致视觉错位,建议在策略测试器用历史数据验证层级后再上实盘。

MQL5 / C++
CGCnvElement *obj=NULL;
class=class="str">"cmt">//--- In the loop by the total number of all graphical elements,
for(class="type">int i=class="num">0;i<this.m_list_all_canv_elm_obj.Total();i++)
  {
  class=class="str">"cmt">//--- get the next object
  obj=this.m_list_all_canv_elm_obj.At(i);
  if(obj==NULL)
     class="kw">continue;
  class=class="str">"cmt">//--- and insert it to the list in the order of sorting by ZOrder
  list.InsertSort(obj);
  }
class=class="str">"cmt">//--- In a loop by the created list sorted by ZOrder,
for(class="type">int i=class="num">0;i<list.Total();i++)
  {
  class=class="str">"cmt">//--- get the next object
  obj=list.At(i);
  if(obj==NULL)
     class="kw">continue;
  class=class="str">"cmt">//--- and move it to the foreground
  obj.BringToTop();
  }
class=class="str">"cmt">//--- Remove the list sorted by &class="macro">#x27;new&class="macro">#x27;
class="kw">delete list;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the maximum ZOrder of all CCanvas elements                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long CGraphElementsCollection::GetZOrderMax(class="type">void)
  {
  this.m_list_all_canv_elm_obj.Sort(SORT_BY_CANV_ELEMENT_ZORDER);
  class="type">int index=CSelect::FindGraphCanvElementMax(this.GetListCanvElm(),CANV_ELEMENT_PROP_ZORDER);
  CGCnvElement *obj=this.m_list_all_canv_elm_obj.At(index);
  class="kw">return(obj!=NULL ? obj.Zorder() : WRONG_VALUE);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set ZOrde to the specified element                               |
class=class="str">"cmt">//| and adjust it in other elements                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CGraphElementsCollection::SetZOrderMAX(CGCnvElement *obj)
  {
class=class="str">"cmt">//--- Get the maximum ZOrder of all graphical elements
  class="type">long max=this.GetZOrderMax();
class=class="str">"cmt">//--- If an invalid pointer to the object has been passed or the maximum ZOrder has not been received, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
  if(obj==NULL || max<class="num">0)
     class="kw">return class="kw">false;
class=class="str">"cmt">//--- Declare the variable for storing the method result
  class="type">bool res=true;
class=class="str">"cmt">//--- If the maximum ZOrder is zero, ZOrder is equal to class="num">1,
class=class="str">"cmt">//--- if the maximum ZOrder is less than(the total number of graphical elements)-class="num">1, ZOrder will exceed it by class="num">1,
class=class="str">"cmt">//--- otherwise, ZOrder will be equal to(the total number of graphical elements)-class="num">1
  class="type">long value=(max==class="num">0 ? class="num">1 : max<this.m_list_all_canv_elm_obj.Total()-class="num">1 ? max+class="num">1 : this.m_list_all_canv_elm_obj.Total()-class="num">1);
class=class="str">"cmt">//--- If failed to set ZOrder for an object passed to the method, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
  if(!obj.SetZorder(value,class="kw">false))

「在画布上重排图形对象的层叠次序」

这段逻辑处理的是把某个图形对象从画布上移除或下沉时,其余对象的 ZOrder(层叠序号)如何联动递减。核心动作是先按元素 ID 排序,再筛出除自身外的所有画布元素,逐个把 ZOrder 减 1。 代码里先用 CSelect::ByGraphCanvElementProperty 按 CANV_ELEMENT_PROP_ID 且 NO_EQUAL 取出别人,若列表为空但总元素数大于 1,直接 return false,说明环境状态不一致。 循环里跳过三类对象:空指针、支点控制表单(OBJECT_DE_TYPE_GFORM_CONTROL)、以及 ZOrder already 为 0 的底部对象。对剩下的调用 SetZorder(elm.Zorder()-1,false),失败就把 res 按位与 false 累积。 测试分支里对 OBJECT_DE_TYPE_GFORM 类型临时画一行文字『Test. ID x, ZOrder y』,颜色 C'211,233,149』、透明度 255、居中显示,方便在 MT5 里肉眼核对层级变化。开 MT5 跑一遍,看表单上 ZOrder 数字是否随对象删除连续下挪即可验证。

MQL5 / C++
class="kw">return class="kw">false;
class=class="str">"cmt">//--- Temporarily declare a form object for drawing a text for visually displaying its ZOrder
  CForm *form=obj;
class=class="str">"cmt">//--- and draw a text specifying ZOrder on the form
  form.TextOnBG(class="num">0,TextByLanguage("Тест: ID ","Test. ID ")+(class="type">class="kw">string)form.ID()+", ZOrder "+(class="type">class="kw">string)form.Zorder(),form.Width()/class="num">2,form.Height()/class="num">2,FRAME_ANCHOR_CENTER,C&class="macro">#x27;class="num">211,class="num">233,class="num">149&class="macro">#x27;,class="num">255,true,true);
class=class="str">"cmt">//--- Sort the list of graphical elements by an element ID
  this.m_list_all_canv_elm_obj.Sort(SORT_BY_CANV_ELEMENT_ID);
class=class="str">"cmt">//--- Get the list of graphical elements without an object whose ID is equal to the ID of the object passed to the method
  CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_ID,obj.ID(),NO_EQUAL);
class=class="str">"cmt">//--- If failed to obtain the list and the list size exceeds one,
class=class="str">"cmt">//--- which indicates the presence of other objects in it in addition to the one sorted by ID, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
  if(list==NULL && this.m_list_all_canv_elm_obj.Total()>class="num">1)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- In the loop by the obtained list of remaining graphical element objects
  for(class="type">int i=class="num">0;i<list.Total();i++)
    {
    class=class="str">"cmt">//--- get the next object
      CGCnvElement *elm=list.At(i);
    class=class="str">"cmt">//--- If failed to get the object or if this is a control form for managing pivot points of an extended standard graphical object
    class=class="str">"cmt">//--- or, if the object&class="macro">#x27;s ZOrder is zero, skip the object since there is no need in changing its ZOrder as it is the bottom one
      if(elm==NULL || elm.Type()==OBJECT_DE_TYPE_GFORM_CONTROL || elm.Zorder()==class="num">0)
        class="kw">continue;
    class=class="str">"cmt">//--- If failed to set the object&class="macro">#x27;s ZOrder to class="num">1 less than it already is(decreasing ZOrder by class="num">1), add &class="macro">#x27;class="kw">false&class="macro">#x27; to the &class="macro">#x27;res&class="macro">#x27; value
      if(!elm.SetZorder(elm.Zorder()-class="num">1,class="kw">false))
        res &=class="kw">false;
    class=class="str">"cmt">//--- Temporarily(for the test purpose), if the element is a form,
      if(elm.Type()==OBJECT_DE_TYPE_GFORM)
        {
        class=class="str">"cmt">//--- assign the pointer to the element for the form and draw a text specifying ZOrder on the form 
        form=elm;
        form.TextOnBG(class="num">0,TextByLanguage("Тест: ID ","Test. ID ")+(class="type">class="kw">string)form.ID()+", ZOrder "+(class="type">class="kw">string)form.Zorder(),form.Width()/class="num">2,form.Height()/class="num">2,FRAME_ANCHOR_CENTER,C&class="macro">#x27;class="num">211,class="num">233,class="num">149&class="macro">#x27;,class="num">255,true,true);
        }
    }
class=class="str">"cmt">//--- Upon the loop completion, class="kw">return the result set in &class="macro">#x27;res&class="macro">#x27;
  class="kw">return res;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Add a newly created standard graphical object to the list
  class="type">bool                AddCreatedObjToList(class="kw">const class="type">class="kw">string source,class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,CGStdGraphObj *obj)
                    {
                    if(!this.m_list_all_graph_obj.Add(obj))
                      {

图表对象集合的刷新与接管逻辑

在 MT5 自定义图形库里,CGraphElementsCollection::Refresh 负责把终端里散落各处的图形对象收编进统一集合。它先调用 RefreshForExtraObjects 处理附加对象,再遍历终端所有已开图表——上限由 CHARTS_MAX 控制,默认不超过 100 个。 循环体内用 ChartNext 按 chart_id 逐个抓取图表,拿到 ID 后交给 RefreshByChartID 生成 CChartObjectsControl 指针。若指针为空直接 continue,不阻塞后续图表扫描。 真正有动作的是 obj_ctrl.IsEvent() 且 Delta() 大于 0 的分支:说明该图表图形数量增加了。此时调用 AddGraphObjToCollection 把新增对象搬进集合,失败就跳过该对象;成功则 BringToTopAllCanvElm 把所有表单提到新建对象之上,并 ChartRedraw 重绘。外汇与贵金属图表对象频繁增删时,这套机制可避免自绘面板被遮挡,但高频刷新会带来额外 CPU 开销,实盘前建议在策略测试器里跑一遍观察延迟。

MQL5 / C++
class="type">void CGraphElementsCollection::Refresh(class="type">void)
  {
  this.RefreshForExtraObjects();
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(!this.AddGraphObjToCollection(DFUN_ERR_LINE,obj_ctrl))
          class="kw">continue;
        class=class="str">"cmt">//--- Move all the forms above the created chart object and redraw the chart
        this.BringToTopAllCanvElm();

◍ 对象被删时怎么回补事件

当图表控件回报的 Delta() 小于 0,说明有图形对象被用户或脚本移除,而非新增。此时不能只清内存,还得把移除动作翻译成自定义事件发回主图,否则控制端会以为对象还在。 循环按 -Delta() 的次数执行,每次先调用 FindMissingObj() 从现有列表里捞出那个「失踪」的对象,拿到它的 ChartID、Name 和 TimeCreate 三个参数。TimeCreate 被强转成 double 当作 dparam 传递,这是 MQL5 自定义事件里塞时间戳的常用手法。 若对象类型是 GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,先走 DeleteExtendedObj() 做扩展清理;随后 MoveGraphObjToDeletedObjList() 把类实例移入已删列表,成功才触发 EventChartCustom()。事件 ID 用 GRAPH_OBJ_EVENT_DELETE,主图收到后就能同步状态。 开 MT5 把这段接进你的 CGraph 控件轮询逻辑,重点验证 Delta() 为负时 EventChartCustom 是否真的只发了一次——漏发或重发都会让主图 pivot 点管理乱掉。

MQL5 / C++
         ::ChartRedraw(obj_ctrl.ChartID());
       }
      class=class="str">"cmt">//--- If the graphical object has been removed
      else if(obj_ctrl.Delta()<class="num">0)
       {
        class="type">int index=WRONG_VALUE;
        class=class="str">"cmt">//--- In the loop by the number of removed graphical objects
        for(class="type">int j=class="num">0;j<-obj_ctrl.Delta();j++)
         {
          class=class="str">"cmt">// Find an extra object in the list
          CGStdGraphObj *obj=this.FindMissingObj(chart_id,index);
          if(obj!=NULL)
            {
             class=class="str">"cmt">//--- Get the removed object parameters
             class="type">long   lparam=obj.ChartID();
             class="type">class="kw">string sparam=obj.Name();
             class="type">class="kw">double dparam=(class="type">class="kw">double)obj.TimeCreate();
             class=class="str">"cmt">//--- If this is an extended graphical object
             if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
               {
                this.DeleteExtendedObj(obj);
               }
             class=class="str">"cmt">//--- Move the graphical object class object to the list of removed objects
             class=class="str">"cmt">//--- and send the event to the control program chart
             if(this.MoveGraphObjToDeletedObjList(index))
                ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_DELETE,lparam,dparam,sparam);
            }
         }
       }
     }
   class=class="str">"cmt">//--- Increase the loop index
   i++;
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
             class=class="str">"cmt">//--- If the form is central for managing all pivot points of a graphical object
             else
               {
                class=class="str">"cmt">//--- Get screen coordinates of all object pivot points and write them to the m_data_pivot_point structure

「把枢轴点钉死在图表可视区内」

绘制枢轴点对象时,若屏幕坐标越出图表边界,标签会跑到视窗外不可见,所以要在循环里对每个点做边界夹取。 下面这段逻辑先拿到全部枢轴点坐标,再按 X、Y 两个方向分别限制:以 chart_width、chart_height 为右和下边界,以 0 为左和上边界,shift 为预留边距。 X 方向若 x+shift-绝对值(ShiftX) 小于 0,就把 x 重设为 -shift+绝对值(ShiftX);若 x+shift+绝对值(ShiftX) 大于 chart_width,则 x 收为 chart_width-shift-绝对值(ShiftX)。Y 方向同理用 chart_height 与 ShiftY 夹取。 实盘加载这类指标时,把 shift 调成 10~20 像素,枢轴标签基本不会被 K 线吞掉;外汇与贵金属波动剧烈,图表重绘频繁,边界处理不到位容易丢点。

MQL5 / C++
if(this.GetPivotPointCoordsAll(ext,m_data_pivot_point))
  {
   class=class="str">"cmt">//--- In the loop by the number of object pivot points,
   for(class="type">int i=class="num">0;i<(class="type">int)this.m_data_pivot_point.Size();i++)
     {
      class=class="str">"cmt">//--- limit the screen coordinates of the current pivot point so that they do not move beyond the chart borders
      class=class="str">"cmt">//--- By X coordinate
      if(x+shift-::fabs(this.m_data_pivot_point[i].ShiftX)<class="num">0)
        x=-shift+::fabs(m_data_pivot_point[i].ShiftX);
      if(x+shift+::fabs(this.m_data_pivot_point[i].ShiftX)>chart_width)
        x=chart_width-shift-::fabs(this.m_data_pivot_point[i].ShiftX);
      class=class="str">"cmt">//--- By Y coordinate
      if(y+shift-::fabs(this.m_data_pivot_point[i].ShiftY)<class="num">0)
        y=-shift+::fabs(this.m_data_pivot_point[i].ShiftY);
      if(y+shift+::fabs(this.m_data_pivot_point[i].ShiftY)>chart_height)
        y=chart_height-shift-::fabs(this.m_data_pivot_point[i].ShiftY);
     }
  }

枢轴点坐标回填与调试输出

在扩展对象绘制流程里,先把算好的坐标写回当前枢轴点。代码用 ext.ChangeCoordsExtendedObj 把 x+shift+ShiftX 与 y+shift+ShiftY 喂给第 i 个对象,枢轴阵列 m_data_pivot_point 的元素自带 ShiftX/ShiftY 偏移量,这样拖拽或重绘时不会丢失相对位置。 调试阶段别靠猜。当 m_data_pivot_point.Size() 大于等于 2 时,取最小最大坐标对:GetMinCoordXY 拿左下角 (min_x,min_y),GetMaxCoordXY 拿右上角 (max_x,max_y),再用 Comment 把 MinX/MaxX/MinY/MaxY 打印到图表左上。实盘前开 MT5 跑一遍,若四个值越界到负数或超图表宽高,说明坐标变换有 bug。 鼠标按下事件也在这里分流。mouse_state 等于 MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED 且 move 为假时,把 pressed_form 置 true,代表用户在活动区内按住按键,后续拖拽逻辑靠这个标志位判断是否该跟手移动表单。外汇与贵金属图表上做这类交互工具波动快,高风险,标志位错乱可能让对象乱飞。

MQL5 / C++
class=class="str">"cmt">//--- set the calculated coordinates to the current object pivot point
ext.ChangeCoordsExtendedObj(x+shift+this.m_data_pivot_point[i].ShiftX,y+shift+this.m_data_pivot_point[i].ShiftY,i);
      }
    }

class=class="str">"cmt">//--- Display debugging comments on the chart
if(m_data_pivot_point.Size()>=class="num">2)
  {
   class="type">int max_x,min_x,max_y,min_y;
   if(ext.GetMinCoordXY(min_x,min_y) && ext.GetMaxCoordXY(max_x,max_y))
     Comment(
       "MinX=",min_x,", MaxX=",max_x,"\n",
       "MaxY=",min_y,", MaxY=",max_y
      );
  }

   }
class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the active area,  any mouse button is clicked&class="macro">#x27; event handler
if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED && !move)
  {
   pressed_form=true;  class=class="str">"cmt">// the flag of holding the mouse button on the form

◍ 拖拽时如何把窗口顶到最前

在 MT5 自定义图形面板里,左键按下时要先确认鼠标状态再决定窗口层级。下面这段处理把「移动中」的表单置顶,并算出光标相对窗口左上角的偏移量,避免拖动时窗口乱跳。 代码里先置 move=true 并调用 BringToTop(),随后用鼠标坐标减表单坐标得到 OffsetX / OffsetY,这两值会在后续 OnMouseMove 里用于重绘位置。 置顶不是无脑改 ZOrder。先取全局最大 ZOrder(GetZOrderMax),仅当表单当前层级低于最大值、或所有表单 ZOrder 均为 0 时才提层;且若表单类型是 OBJECT_DE_TYPE_GFORM_CONTROL(标准图形对象的控制点),则跳过,防止把趋势线锚点之类强行压到面板上方。 开 MT5 跑这套时,可故意建两个重叠表单,左键拖其中一个,观察另一个是否被正确压在下面——若控制点也被置顶,多半是漏了 form.Type() 判断。外汇与贵金属图表上的自定义面板交互属于高风险环境,图形对象层级异常可能误导下单操作。

MQL5 / C++
   class=class="str">"cmt">//--- If the left mouse button is pressed
   if(this.m_mouse.IsPressedButtonLeft())
     {
      class=class="str">"cmt">//--- Set flags and form parameters
      move=true;                                        class=class="str">"cmt">// movement flag
      form.SetInteraction(true);                         class=class="str">"cmt">// flag of the form interaction with the environment
      form.BringToTop();                                 class=class="str">"cmt">// form on the background - above all others
      form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX()); class=class="str">"cmt">// Cursor shift relative to the X coordinate
      form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY()); class=class="str">"cmt">// Cursor shift relative to the Y coordinate
      this.ResetAllInteractionExeptOne(form);             class=class="str">"cmt">// Reset interaction flags for all forms except the current one
      
      class=class="str">"cmt">//--- Get the maximum ZOrder
      class="type">long zmax=this.GetZOrderMax();
      class=class="str">"cmt">//--- If the maximum ZOrder has been received and the form&class="macro">#x27;s ZOrder is less than the maximum one or the maximum ZOrder of all forms is equal to zero
      if(zmax>WRONG_VALUE && (form.Zorder()<zmax || zmax==class="num">0))
        {
         class=class="str">"cmt">//--- If the form is not a control point for managing an extended standard graphical object,
         class=class="str">"cmt">//--- set the form&class="macro">#x27;s ZOrder above all others
         if(form.Type()!=OBJECT_DE_TYPE_GFORM_CONTROL)
            this.SetZOrderMAX(form);
        }
     }

「把图形对象锚点搬进屏幕坐标系」

做价格行为分析时,常需要在 MT5 图表上读取趋势线、通道等对象的锚点像素坐标,用来叠加自绘 UI 或做点击命中检测。下面这段方法把任意图形对象的每个 pivot 由时间/价格转成屏幕 XY,并按对象类型区分锚点数量。 先按对象 pivot 总数扩容结构体数组,若 ArrayResize 返回值不等于 obj.Pivots() 就写日志并返回 false,避免后续越界。随后用 ChartTimePriceToXY 逐个转换,任一锚点转换失败同样返回 false。 对象类型决定锚点语义:LABEL、BUTTON、EDIT 等界面类只有 1 个屏幕锚点;HLINE 只有价格锚;VLINE 只有时间锚;TREND、CHANNEL、REGRESSION 等则带两个端点锚点,部分还隐含中心锚点。实际调试时可在 EURUSD 的 M15 上画一条 OBJ_TREND,循环里打印 array_pivots[i].X/Y,验证两端点像素差与肉眼目测一致。 外汇与贵金属杠杆高、滑点大,这类坐标抓取仅用于本地可视化辅助,不构成任何方向判断。

MQL5 / C++
class="type">bool CGraphElementsCollection::GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[])
  {
class=class="str">"cmt">//--- Central point coordinates
   class="type">int xc=class="num">0, yc=class="num">0;
class=class="str">"cmt">//--- If failed to increase the array of structures to match the number of pivot points,
class=class="str">"cmt">//--- inform of that in the journal and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
   if(::ArrayResize(array_pivots,obj.Pivots())!=obj.Pivots())
     {
       CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
       class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- In the loop by the number of graphical object pivot points
   for(class="type">int i=class="num">0;i<obj.Pivots();i++)
     {
       class=class="str">"cmt">//--- Convert the object coordinates into screen ones. If failed, inform of that and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
       if(!::ChartTimePriceToXY(obj.ChartID(),obj.SubWindow(),obj.Time(i),obj.Price(i),array_pivots[i].X,array_pivots[i].Y))
         {
           CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY);
           class="kw">return class="kw">false;
         }
     }
class=class="str">"cmt">//--- Depending on the graphical object type
   class="kw">switch(obj.TypeGraphObject())
     {
       class=class="str">"cmt">//--- One pivot point in screen coordinates
       case OBJ_LABEL           :
       case OBJ_BUTTON          :
       case OBJ_BITMAP_LABEL    :
       case OBJ_EDIT            :
       case OBJ_RECTANGLE_LABEL :
       case OBJ_CHART           : class="kw">break;
       
       class=class="str">"cmt">//--- One pivot point(price only)
       case OBJ_HLINE           : class="kw">break;
       
       class=class="str">"cmt">//--- One pivot point(time only)
       case OBJ_VLINE           :
       case OBJ_EVENT           : class="kw">break;
       
       class=class="str">"cmt">//--- Two pivot points and a central one
       class=class="str">"cmt">//--- Lines
       case OBJ_TREND           :
       case OBJ_TRENDBYANGLE    :
       case OBJ_CYCLES          :
       case OBJ_ARROWED_LINE    :
       class=class="str">"cmt">//--- Channels
       case OBJ_CHANNEL         :
       case OBJ_STDDEVCHANNEL   :
       case OBJ_REGRESSION      :

江恩与斐波那契对象的中心点偏移算法

在 MT5 自定义指标或 EA 里批量处理图形对象时,江恩线、江恩网格以及一族斐波那契工具(回撤、时间区、扇形、弧、通道、扩展)通常共用同一套坐标换算逻辑。下面这段 switch 分支就是把它们归到一组,统一算中心点再求各枢轴的相对位移。 代码先以两个枢轴点 array_pivots[0] 与 array_pivots[1] 的 X、Y 均值得到中心坐标 (xc, yc),随后把每个枢轴减去中心值写入 ShiftX / ShiftY 字段。这样后续绘制或平移对象时,只需基于中心偏移量重算,不必每次回退原始坐标。 通道、甘氏扇形、艾略特波浪、矩形三角形椭圆以及上下箭头等对象在此分支里直接 break,说明它们不进入这套中心偏移流程,可能由其他 case 单独处理或暂不参与自动换算。外汇与贵金属市场波动剧烈,这类对象重算逻辑若用于实盘辅助,须先在策略测试器验证边界情况。

MQL5 / C++
class=class="str">"cmt">//--- Gann
case OBJ_GANNLINE        :
case OBJ_GANNGRID        :
class=class="str">"cmt">//--- Fibo
case OBJ_FIBO            :
case OBJ_FIBOTIMES       :
case OBJ_FIBOFAN         :
case OBJ_FIBOARC         :
case OBJ_FIBOCHANNEL     :
case OBJ_EXPANSION       :
  class=class="str">"cmt">//--- Calculate the central point coordinates
  xc=(array_pivots[class="num">0].X+array_pivots[class="num">1].X)/class="num">2;
  yc=(array_pivots[class="num">0].Y+array_pivots[class="num">1].Y)/class="num">2;
  class=class="str">"cmt">//--- Calculate the shifts of all pivot points from the central one and write them to the structure array
  array_pivots[class="num">0].ShiftX=array_pivots[class="num">0].X-xc;  class=class="str">"cmt">// X shift from class="num">0 to the central point
  array_pivots[class="num">1].ShiftX=array_pivots[class="num">1].X-xc;  class=class="str">"cmt">// X shift from class="num">1 to the central point
  array_pivots[class="num">0].ShiftY=array_pivots[class="num">0].Y-yc;  class=class="str">"cmt">// Y shift from class="num">0 to the central point
  array_pivots[class="num">1].ShiftY=array_pivots[class="num">1].Y-yc;  class=class="str">"cmt">// Y shift from class="num">1 to the central point
  class="kw">return true;
class=class="str">"cmt">//--- Channels
case OBJ_PITCHFORK       : class="kw">break;
class=class="str">"cmt">//--- Gann
case OBJ_GANNFAN         : class="kw">break;
class=class="str">"cmt">//--- Elliott
case OBJ_ELLIOTWAVE5     : class="kw">break;
case OBJ_ELLIOTWAVE3     : class="kw">break;
class=class="str">"cmt">//--- Shapes
case OBJ_RECTANGLE       : class="kw">break;
case OBJ_TRIANGLE        : class="kw">break;
case OBJ_ELLIPSE         : class="kw">break;
class=class="str">"cmt">//--- Arrows
case OBJ_ARROW_THUMB_UP  : class="kw">break;
case OBJ_ARROW_THUMB_DOWN: class="kw">break;
case OBJ_ARROW_UP        : class="kw">break;

◍ 箭头与文字类对象的空分支处理

在 MT5 的图表对象枚举分发逻辑里,箭头系列和带时间/价格坐标的文本类对象常被统一丢进 break 空分支。上面这段代码覆盖了 OBJ_ARROW_DOWN、OBJ_ARROW_STOP、OBJ_ARROW_CHECK、OBJ_ARROW_LEFT_PRICE、OBJ_ARROW_RIGHT_PRICE、OBJ_ARROW_BUY、OBJ_ARROW_SELL、OBJ_ARROW 共 8 种箭头宏,以及 OBJ_TEXT、OBJ_BITMAP 两种坐标对象,外加 default 兜底,全部直接 break 后返回 false。 这种写法意味着:当事件处理函数遇到这些对象类型时,不执行任何自定义交互,直接放行。实盘里如果你挂了 EA 去捕获图表点击事件,却发现自己画的向下箭头(OBJ_ARROW_DOWN)怎么点都没反应,大概率就是掉进了这类空 case。 验证方式很简单:把上面任一个 case 里的 break 换成 Print(__FUNCTION__," hit ",type); 重新编译 EA,在 EURUSD 的 H1 图上手动拖一个卖出箭头,终端日志若输出对应 hit 记录,说明分发链已接通。外汇与贵金属杠杆高,改事件逻辑前先上模拟盘跑,避免误触发送订单。

MQL5 / C++
      case OBJ_ARROW_DOWN         : class="kw">break;
      case OBJ_ARROW_STOP         : class="kw">break;
      case OBJ_ARROW_CHECK        : class="kw">break;
      case OBJ_ARROW_LEFT_PRICE   : class="kw">break;
      case OBJ_ARROW_RIGHT_PRICE  : class="kw">break;
      case OBJ_ARROW_BUY          : class="kw">break;
      case OBJ_ARROW_SELL         : class="kw">break;
      case OBJ_ARROW              : class="kw">break;
      
      class=class="str">"cmt">//--- Graphical objects with time/price coordinates
      case OBJ_TEXT               : class="kw">break;
      case OBJ_BITMAP             : class="kw">break;
      class=class="str">"cmt">//---
      class="kw">default: class="kw">break;
   }
   class="kw">return class="kw">false;
}

「在 MT5 里验证窗体 ZOrder 的派发逻辑」

把前一篇的 EA 放到 \MQL5\Experts\TestDoEasy\Part100\ 下,命名 TestDoEasyPart100.mq5,编译后挂到图表即可跑这套 ZOrder 观察。想在测试时直接看到每个窗体的层叠顺序,得在 OnInit() 里给窗体属性置零(最底层),并把对象 ID 与 ZOrder 值打印出来。 实跑下来,每个窗体对象刚创建时 ZOrder 都是 0,但图形对象依旧画在它们“下面”。ZOrder 的变更是在一个循环里做的,上限不超过图表上窗体对象的总数(从零计),所以派发顺序可控。 任何构造出的图形对象始终显示在 GUI 对象下方,相对位置不漂移,说明列表是按 ZOrder 值正确排的。复合图形对象现在也能被正确限制在屏幕边缘,不管锚点在哪都不会变形——外汇和贵金属图表上叠这类自定义 UI 时,MT5 对象层级错乱是高概率坑,先验证再上实盘。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Set EA global variables
   ArrayResize(array_clr,class="num">2);           class=class="str">"cmt">// 把渐变填充色数组大小定为2
   array_clr[class="num">0]=C&class="macro">#x27;class="num">26,class="num">100,class="num">128&class="macro">#x27;;         class=class="str">"cmt">// 原始深青色
   array_clr[class="num">1]=C&class="macro">#x27;class="num">35,class="num">133,class="num">169&class="macro">#x27;;         class=class="str">"cmt">// 提亮后的原始色
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">// 告诉引擎只用当前品种
   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">// 日志里打印简短时间序列描述
class=class="str">"cmt">//--- Create form objects
   CForm *form=NULL;
   for(class="type">int i=class="num">0;i<FORMS_TOTAL;i++)
     {
     class=class="str">"cmt">//--- When creating an object, pass all the required parameters to it
     form=new CForm("Form_0"+class="type">class="kw">string(i+class="num">1),class="num">30,(form==NULL ? class="num">100 : form.BottomEdge()+class="num">20),class="num">100,class="num">30); class=class="str">"cmt">// 逐个建窗体,纵向错开20像素
     if(form==NULL)
       class="kw">continue;
     class=class="str">"cmt">//--- Set activity and moveability flags for the form
     form.SetActive(true);
     form.SetMovable(true);            class=class="str">"cmt">// 窗体可激活可拖动
     class=class="str">"cmt">//--- Set the form ID and the index in the list of objects
     form.SetID(i);
     form.SetNumber(class="num">0);    class=class="str">"cmt">// (class="num">0 - main form object) 主窗体编号0,附属对象可挂它下面
     class=class="str">"cmt">//--- Set the opacity of class="num">200
     form.SetOpacity(class="num">245);             class=class="str">"cmt">// 不透明度245
     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">// 背景用数组第0个色
     class=class="str">"cmt">//--- Form outlining frame class="type">class="kw">color
     form.SetColorFrame(clrDarkBlue);  class=class="str">"cmt">// 边框深蓝
     class=class="str">"cmt">//--- Draw the shadow drawing flag
     form.SetShadow(class="kw">false);            class=class="str">"cmt">// 先关阴影
     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">// 背景色降饱和度得单色阴影色
     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">// 按输入决定阴影色
     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

给渐变表单叠文字与层级

在表单画完阴影、渐变背景和边框之后,要把它真正推到可视层并标注内容。先调用 SetZorder(0,false) 把 Z 序归零且不锁定,避免被后续元素盖住。 TextOnBG 这一步直接把描述文字绘到背景上:以表单中心为锚点,拼出 "Test: ID <id>, ZOrder 0",颜色用 C'211,233,149'、透明度 255,并开启自适应与居中。这样在 MT5 里跑起来,你能直观看到每个表单的 ID 和层级。 最后用 GraphAddCanvElmToCollection(form) 把表单加入引擎集合;若返回失败就 delete form 释放内存,防止句柄泄漏。外汇与贵金属图表上做这类 UI 叠加需注意:图形对象频繁重绘会占用主线程,高风险环境下建议限频。

MQL5 / C++
   form.DrawShadow(class="num">3,class="num">3,clr,class="num">200,class="num">4);
   class=class="str">"cmt">//--- Fill the form background with a vertical gradient
   form.Erase(array_clr,form.Opacity(),true);
   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());
   form.Done();
   
   class=class="str">"cmt">//--- Set ZOrder to zero, display the text describing the gradient type and update the form
   class=class="str">"cmt">//--- Text parameters: the text coordinates and the anchor point in the form center
   class=class="str">"cmt">//--- Create a new text animation frame with the ID of class="num">0 and display the text on the form
   form.SetZorder(class="num">0,class="kw">false);
   form.TextOnBG(class="num">0,TextByLanguage("Тест: ID ","Test: ID ")+(class="type">class="kw">string)form.ID()+", ZOrder "+(class="type">class="kw">string)form.Zorder(),form.Width()/class="num">2,form.Height()/class="num">2,FRAME_ANCHOR_CENTER,C&class="macro">#x27;class="num">211,class="num">233,class="num">149&class="macro">#x27;,class="num">255,true,true);
   class=class="str">"cmt">//--- Add the form to the list
   if(!engine.GraphAddCanvElmToCollection(form))
      class="kw">delete form;
   }
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 画得少,看得清

这套扩展图形对象做到第 100 篇,核心不是堆功能,而是把复合对象的移动、控制点、鼠标事件都收进统一接口,让你在图表上只留必要元素就能盯住结构变化。 当前函数库压缩包 MQL5.zip 体积 4375.67 KB,含库文件、测试 EA 与图表事件指标,直接丢进 MT5 的 MQL5 目录解压即可编译验证。 下一步作者会转去写 Windows 窗体风格的 GUI 控件,但扩展图形这块仍会随反馈迭代;外汇与贵金属波动剧烈、杠杆风险高,任何图形辅助都只是概率参考,真要上线请先在策略测试器跑通。 你若改过其中某个控制点逻辑,不妨重新编译测试 EA,看轴点拖动是否还贴合预期——这比读十遍文档来得直接。

常见问题

把控件属性塞进结构体再转成字节流保存,重新加载时把字节转回结构体灌给对象即可,避免逐个设属性。
调用取图形对象屏幕坐标的函数拿左上角和右下角坐标,用最大坐标减去最小坐标就是可见边界。
可以,把对象名发给小布,它会基于当前图表帮你读出屏幕坐标边界并标出是否越出可视区。
结构体集中管理成员,转字节后可整体还原,减少漏设属性导致的界面错位,也方便做快照。
先把各元素属性都写进对应结构体,再循环把结构成员一次性灌进对象,比单个Set省调用次数。