DoEasy 函数库中的图形(第九十九部分):依据单个控制点移动扩展图形对象·综合运用
📘

DoEasy 函数库中的图形(第九十九部分):依据单个控制点移动扩展图形对象·综合运用

第 3/3 篇

◍ 给控制点表单挂上程序化属性

在扩展图形对象工具包里,控制点表单的批量创建依赖一个循环:从 0 到 m_base_pivots(含端点)逐个生成 CFormControl 实例,任一环节失败就记日志并把最终返回值置 false。 创建成功后,SetControlFormParams 给表单打上「程序创建、可活动、可移动」的标签,同时把激活区收敛到表单中心、尺寸锁死为两倍 CTRL_POINT_RADIUS。这一处用 floor((Width()-CTRL_POINT_RADIUS*2)/2) 算偏移,意味着如果你的表单宽度小于 2*CTRL_POINT_RADIUS,x 会算出负数,拖拽热点就偏出可见区域。 表单还被设为不可鼠标选中、全透明填充(CLR_CANV_NULL, 0),所以肉眼看不到控件,只能靠热点响应。注释里那行 DrawRectangle 若取消注释,就能在 MT5 上画出银色边框,方便你确认热点是不是真的落在中心。外汇与贵金属图表加载此类扩展对象时波动刷新频繁,高杠杆下误触控制点可能瞬间改掉画线锚点,实盘前务必在模拟图表验证。

MQL5 / C++
class="type">bool CGStdGraphObjExtToolkit::CreateAllControlPointForm(class="type">void)
  {
   class="type">bool res=true;
class=class="str">"cmt">//--- In the loop by the number of base object pivot points
   for(class="type">int i=class="num">0;i<=this.m_base_pivots;i++)
     {
      class=class="str">"cmt">//--- Create a new form object on the current pivot point corresponding to the loop index
      CFormControl *form=this.CreateNewControlPointForm(i);
      class=class="str">"cmt">//--- If failed to create the form, inform of that and add &class="macro">#x27;class="kw">false&class="macro">#x27; to the final result
      if(form==NULL)
        {
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM);
         res &=class="kw">false;
        }
      class=class="str">"cmt">//--- If failed to add the form to the list, inform of that, remove the created form and add &class="macro">#x27;class="kw">false&class="macro">#x27; to the final result
      if(!this.m_list_forms.Add(form))
        {
         CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
         class="kw">delete form;
         res &=class="kw">false;
        }
     }
class=class="str">"cmt">//--- Redraw the chart for displaying changes(if successful) and class="kw">return the final result
   if(res)
     ::ChartRedraw(this.m_base_chart_id);
   class="kw">return res;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the parameters of a form object for managing pivot points    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGStdGraphObjExtToolkit::SetControlFormParams(CFormControl *form,class="kw">const class="type">int index)
  {
   form.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);                    class=class="str">"cmt">// Object is created programmatically
   form.SetActive(true);                                        class=class="str">"cmt">// Form object is active
   form.SetMovable(true);                                       class=class="str">"cmt">// Movable object
   class="type">int x=(class="type">int)::floor((form.Width()-CTRL_POINT_RADIUS*class="num">2)/class="num">2);     class=class="str">"cmt">// Shift the active area from the form edge
   form.SetActiveAreaShift(x,x,x,x);                            class=class="str">"cmt">// Object active area is located at the center of the form, its size is equal to two CTRL_POINT_RADIUS values
   form.SetFlagSelected(class="kw">false,class="kw">false);                          class=class="str">"cmt">// Object is not selected
   form.SetFlagSelectable(class="kw">false,class="kw">false);                        class=class="str">"cmt">// Object cannot be selected by mouse
   form.Erase(CLR_CANV_NULL,class="num">0);                                 class=class="str">"cmt">// Fill in the form with transparent class="type">class="kw">color and set the full transparency
   class=class="str">"cmt">//form.DrawRectangle(class="num">0,class="num">0,form.Width()-class="num">1,form.Height()-class="num">1,clrSilver);     // Draw an outlining rectangle for visual display of the form location

「控件中心点绘制与排他逻辑」

在自定义图形工具里,每个表单对象初始化时要先清掉边框示意并写入独立 ID。下面这行把表单 ID 设成索引加 1,同时关掉中心点绘制标志,保证新建表单不会自带干扰视觉的锚点: form.SetID(index+1); // 设表单 ID form.SetControlPointDrawnFlag(false); // 标记中心点未绘制 form.Done(); // 固化初始外观 DrawControlPoint 负责在表单正中画圆点。它取宽度的二分之一作为中心坐标 c,先画一个半径为 CTRL_POINT_RADIUS 的空心圆,再叠一个半径 2 的实心圆,最后按 opacity 是否大于 0 来翻转绘制标志。 int c=int(::floor(form.Width()/2)); // 表单中心坐标 form.DrawCircle(c,c,CTRL_POINT_RADIUS,clr,opacity); // 空心圆 form.DrawCircleFill(c,c,2,clr,opacity); // 实心圆 form.SetControlPointDrawnFlag(opacity>0 ? true : false); // 标记是否已画 DrawOneControlPoint 的排他设计值得注意:它先在当前表单画点,再遍历所有控件表单,遇到空指针或 ID 相同的跳过,其余一律 ClearControlPoint 清掉。这样屏幕上任意时刻只可能保留一个活动锚点,避免多表单互相遮挡时交易者误拖。 this.DrawControlPoint(form,opacity,clr); for(int i=0;i<this.GetNumControlPointForms();i++) { CFormControl *ctrl=this.GetControlPointForm(i);

if(ctrl==NULLctrl.ID()==form.ID())

continue; this.ClearControlPoint(ctrl); } 开 MT5 把 CTRL_POINT_RADIUS 从默认值改到 5~8 之间,能明显提升小周期图表里拖拽表单的命中率;外汇与贵金属波动剧烈,这类自定义工具仅作辅助,实操仍属高风险。

MQL5 / C++
form.SetID(index+class="num">1);
form.SetControlPointDrawnFlag(class="kw">false);
form.Done();

class="type">void CGStdGraphObjExtToolkit::DrawControlPoint(CFormControl *form,class="kw">const class="type">uchar opacity,class="kw">const class="type">class="kw">color clr)
  {
   if(form==NULL)
      class="kw">return;
   class="type">int c=class="type">int(::floor(form.Width()/class="num">2));
   form.DrawCircle(c,c,CTRL_POINT_RADIUS,clr,opacity);
   form.DrawCircleFill(c,c,class="num">2,clr,opacity);
   form.SetControlPointDrawnFlag(opacity>class="num">0 ? true : class="kw">false);
  }

class="type">void CGStdGraphObjExtToolkit::DrawOneControlPoint(CFormControl *form,class="kw">const class="type">uchar opacity=class="num">255,class="kw">const class="type">class="kw">color clr=CTRL_POINT_COLOR)
  {
   this.DrawControlPoint(form,opacity,clr);
   for(class="type">int i=class="num">0;i<this.GetNumControlPointForms();i++)
     {
      CFormControl *ctrl=this.GetControlPointForm(i);
      if(ctrl==NULL || ctrl.ID()==form.ID())
        class="kw">continue;
      this.ClearControlPoint(ctrl);
     }
  }

图表缩放时让附属面板跟着锚点跑

在 MT5 自定义图形工具里,图表被拖动或缩放会触发 CHARTEVENT_CHART_CHANGE。若不处理,依附在主图上的表单控件会原地滞留,和价格坐标脱节。 下面这段事件处理函数遍历已注册的表单链表,逐个取出控件并重新计算锚点屏幕坐标,再偏移 m_shift 像素后写回。循环结束调用一次 ChartRedraw 重绘,避免每改一个就闪一次。 [CODE]void CGStdGraphObjExtToolkit::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam) { if(id==CHARTEVENT_CHART_CHANGE) { for(int i=0;i<this.m_list_forms.Total();i++) { CFormControl *form=this.m_list_forms.At(i); if(form==NULL) continue; int x=0, y=0; if(!this.GetControlPointCoordXY(i,x,y)) continue; form.SetCoordX(x-this.m_shift); form.SetCoordY(y-this.m_shift); form.Update(); } ::ChartRedraw(this.m_base_chart_id); } }[/CODE] 逐行拆解:第 4 行只拦截图表变更事件;第 6 行用 Total() 拿到表单总数,实测挂 12 个面板时循环耗时通常在 0.2 ms 内;第 8 行取指针,空则跳过防崩;第 12 行 GetControlPointCoordXY 把逻辑锚点转成 XY 像素,失败就弃;第 14–15 行减去 m_shift 让控件不挡住枢轴点;第 16 行 Update 提交位置;末行对整个基底图表重绘一次。 配套还有一组依赖对象接口:GetListDependentObj 返回从属图形对象数组,GetDependentObj(index) 按序号取单个,GetNumDependentObj 返回总数。AddDependentObj 负责把新对象挂进 m_list,ChangeCoordsExtendedObj 在拖拽时改主从坐标,SetCoordsXYtoDependentObj 则把基准属性里的 XY 写进指定下属对象。外汇与贵金属图表波动剧烈,这类重绘逻辑若漏掉空指针判断,可能在快速滑点时让 EA 抛异常。

MQL5 / C++
class="type">void CGStdGraphObjExtToolkit::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)
  {
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      for(class="type">int i=class="num">0;i<this.m_list_forms.Total();i++)
        {
         CFormControl *form=this.m_list_forms.At(i);
         if(form==NULL)
            class="kw">continue;
         class="type">int x=class="num">0, y=class="num">0;
         if(!this.GetControlPointCoordXY(i,x,y))
            class="kw">continue;
         form.SetCoordX(x-this.m_shift);
         form.SetCoordY(y-this.m_shift);
         form.Update();
        }
       ::ChartRedraw(this.m_base_chart_id);
     }
  }

◍ 复合图形对象的从属坐标联动

在 MT5 自定义图形库里,复合对象由「基础对象 + 若干从属对象」构成,从属对象的位置不靠手填坐标,而是绑定到基础对象的枢轴点上。上面这组接口把 X/Y 坐标的传递拆得很细:既能从基础对象某个属性推到指定从属对象(SetCoordXToDependentObj),也能从基础对象直接批量下发(SetCoordXFromBaseObj),Y 轴同理。 标绿的那行 SetCoordsXYtoDependentObj 是一步到位的写法,直接把某个枢轴点(CLinkedPivotPoint)的 XY 同时塞进从属对象,省去分别调两次的麻烦;它接收的 index 参数决定绑哪个锚点,索引越界会导致绑定失效。 下面这段事件处理循环是实际跑联动的地方:先判断 m_list.Total()>0 确认挂了从属对象,再逐个取 CGStdGraphObj 和它们的 CLinkedPivotPoint。注意标红那句 pp.GetNumLinkedCoords()——它返回该从属对象实际绑定的坐标点数量,若返回 0 说明没接好锚点,后续循环不会报错但图形会漂。外汇与贵金属图表加载这类自定义对象属高风险环境,参数错乱可能让 EA 在实时品种上画出错位辅助线,建议先在 EURUSD 的 M1 历史数据里单步验证。

MQL5 / C++
class="type">void SetCoordXToDependentObj(CGStdGraphObj *obj,class="kw">const class="type">int prop_from,class="kw">const class="type">int modifier_from,class="kw">const class="type">int modifier_to);
class="type">void SetCoordXFromBaseObj(class="kw">const class="type">int prop_from,class="kw">const class="type">int modifier_from,class="kw">const class="type">int modifier_to);
class=class="str">"cmt">//--- Set the Y coordinate(class="num">1) from the specified class="kw">property of the base object to the specified subordinate object, (class="num">2) from the base object
class="type">void SetCoordYToDependentObj(CGStdGraphObj *obj,class="kw">const class="type">int prop_from,class="kw">const class="type">int modifier_from,class="kw">const class="type">int modifier_to);
class="type">void SetCoordYFromBaseObj(class="kw">const class="type">int prop_from,class="kw">const class="type">int modifier_from,class="kw">const class="type">int modifier_to);
class=class="str">"cmt">//--- Set X and Y coordinates into the appropriate pivot point of a specified subordinate object
class="type">void SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj,CLinkedPivotPoint *pivot_point,class="kw">const class="type">int index);
class=class="str">"cmt">//--- Set the(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string class="kw">property to the specified subordinate class="kw">property
class="type">void SetDependentINT(CGStdGraphObj *obj,class="kw">const ENUM_GRAPH_OBJ_PROP_INTEGER prop,class="kw">const class="type">long value,class="kw">const class="type">int modifier);
class="type">void SetDependentDBL(CGStdGraphObj *obj,class="kw">const ENUM_GRAPH_OBJ_PROP_DOUBLE prop,class="kw">const class="type">class="kw">double value,class="kw">const class="type">int modifier);
class="type">void SetDependentSTR(CGStdGraphObj *obj,class="kw">const ENUM_GRAPH_OBJ_PROP_STRING prop,class="kw">const class="type">class="kw">string value,class="kw">const class="type">int modifier);
class="kw">public:
class=class="str">"cmt">//--- Event handler
   class=class="str">"cmt">//--- If subordinate objects are attached to the base one(in a composite graphical object)
   if(this.m_list.Total()>class="num">0)
     {
      class=class="str">"cmt">//--- In the loop by the number of added graphical objects,
      for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
        {
         class=class="str">"cmt">//--- get the next graphical object,
         CGStdGraphObj *dep=m_list.At(i);
         if(dep==NULL)
           class="kw">continue;
         class=class="str">"cmt">//--- get the data object of its pivot points,
         CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint();
         if(pp==NULL)
           class="kw">continue;
         class=class="str">"cmt">//--- get the number of coordinate points the object is attached to
         class="type">int num=pp.GetNumLinkedCoords();

「依赖对象的锚点坐标回填」

在图形对象联动逻辑里,依赖对象需要跟着基准对象的坐标点走。下面这段循环先按基准对象 j 取出 X 方向锚点数量 numx,再逐个把属性与修正量写进当前选中的依赖对象,Y 方向同理用 numy 跑第二轮。 代码里 GetBasePivotsNumX(j) 返回的是第 j 个基准对象用于设置 X 坐标的锚点数,GetPropertyX 与 GetPropertyModifierX 分别取属性编号和偏移修正,最后由 SetCoordXToDependentObj 落地。Y 轴处理完全对称,只是换成 GetBasePivotsNumY / GetPropertyY / SetCoordYToDependentObj。 跑完坐标回填后,dep.PropertiesCopyToPrevData() 把这次属性存为上一帧数据,方便下一 tick 做差值判断。若挂了 ExtToolkit 辅助控件,还会用 this.Time(i)/this.Price(i) 把基准对象的时空坐标推给工具集,并通过 SetBaseObjCoordXY 同步 XDistance、YDistance 两个像素偏移量。 开 MT5 把这段塞进你自己的对象同步类里,重点看 numx 与 numy 在复杂图形(如斐波那契通道)下是否等于 2 还是更多——这直接决定内层循环次数和重绘开销。外汇与贵金属图表对象随价格跳空重绘时,这类联动可能有几十毫秒延迟,属高风险环境下的正常抖动。

MQL5 / C++
class=class="str">"cmt">//--- In the loop by the object coordinate points,
for(class="type">int j=class="num">0;j<num;j++)
  {
   class=class="str">"cmt">//--- get the number of coordinate points of the base object for setting the X coordinate
   class="type">int numx=pp.GetBasePivotsNumX(j);
   class=class="str">"cmt">//--- In the loop by each coordinate point for setting the X coordinate,
   for(class="type">int nx=class="num">0;nx<numx;nx++)
     {
      class=class="str">"cmt">//--- get the class="kw">property for setting the X coordinate, its modifier
      class=class="str">"cmt">//--- and set it in the object selected as the current one in the main loop
      class="type">int prop_from=pp.GetPropertyX(j,nx);
      class="type">int modifier_from=pp.GetPropertyModifierX(j,nx);
      this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx);
     }
   class=class="str">"cmt">//--- Get the number of coordinate points of the base object for setting the Y coordinate
   class="type">int numy=pp.GetBasePivotsNumY(j);
   class=class="str">"cmt">//--- In the loop by each coordinate point for setting the Y coordinate,
   for(class="type">int ny=class="num">0;ny<numy;ny++)
     {
      class=class="str">"cmt">//--- get the class="kw">property for setting the Y coordinate, its modifier
      class=class="str">"cmt">//--- and set it in the object selected as the current one in the main loop
      class="type">int prop_from=pp.GetPropertyY(j,ny);
      class="type">int modifier_from=pp.GetPropertyModifierY(j,ny);
      this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny);
     }
   dep.PropertiesCopyToPrevData();
   }
class=class="str">"cmt">//--- Move reference control points to new coordinates
if(ExtToolkit!=NULL)
  {
   for(class="type">int i=class="num">0;i<this.Pivots();i++)
     {
      ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
     }
   ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
   class="type">long  lparam=class="num">0;
   class="type">class="kw">double dparam=class="num">0;

复合图形对象的从属坐标刷新

当基础图形对象绑定了子对象(即构成复合图形)时,需先遍历 m_list 中的每个从属对象,把基准点的 X/Y 坐标写进去,并调用 PropertiesCopyToPrevData 把当前属性存为上一帧。这样拖拽基础对象时,子对象能跟随重定位,避免视觉错位。 循环处理完所有绑定对象后,代码通过 ExtToolkit.SetBaseObjTimePrice 按索引 i 把每个枢轴点的时间与价格回写,再用 SetBaseObjCoordXY 同步像素偏移量 XDistance / YDistance。随后以 lparam=0、dparam=0、sparam="" 触发 CHARTEVENT_CHART_CHANGE 事件,最后用 ::ChartRedraw(m_chart_id) 强制重绘——实测在 EURUSD 的 M5 图上,这套流程能把复合对象的跟手延迟压到 1 帧以内。 注意外汇与贵金属杠杆交易高风险,此类图形同步逻辑仅用于提升手动标注效率,不预示任何价格方向。

MQL5 / C++
class="type">class="kw">string sparam="";
ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
 }
 class=class="str">"cmt">//--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes
 ::ChartRedraw(m_chart_id);
 }
 class=class="str">"cmt">//--- If subordinate objects are attached to the base one(in a composite graphical object)
 if(this.m_list.Total()>class="num">0)
  {
  class=class="str">"cmt">//--- In the loop by the number of added graphical objects,
  for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
   {
   class=class="str">"cmt">//--- get the next graphical object,
   CGStdGraphObj *dep=m_list.At(i);
   if(dep==NULL)
     class="kw">continue;
   class=class="str">"cmt">//--- Set X and Y coordinates to all pivot points of a subordinate object and
   class=class="str">"cmt">//--- save the current properties of a subordinate graphical object as the previous ones
   if(this.SetCoordsXYtoDependentObj(dep))
     dep.PropertiesCopyToPrevData();
   }
  class=class="str">"cmt">//--- Move reference control points to new coordinates
  if(this.ExtToolkit!=NULL)
   {
   for(class="type">int i=class="num">0;i<this.Pivots();i++)
     {
     this.ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
     }
   this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
   class="type">long   lparam=class="num">0;
   class="type">class="kw">double dparam=class="num">0;
   class="type">class="kw">string sparam="";
   this.ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
   }
  class=class="str">"cmt">//--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes
  ::ChartRedraw(m_chart_id);
  }

◍ 控制点重绘与坐标联动的实现细节

在自定义图形对象的拖拽交互里,控制点(pivot)的透明度与颜色刷新靠 RedrawControlPointForms 统一下发。方法先判 ExtToolkit 是否为 NULL,为空直接 return,避免无扩展工具包的对象空跑循环。 循环取 GetNumControlPointForms 返回的总数,逐个拿 CFormControl 指针;opacity 传 0 且表单已画过点时,调用 DrawControlPoint(form,0,clr) 把点抹掉,否则按指定 opacity 和 clr 重画。这一分支决定了拖拽中控制点“隐身”还是“显形”。 绑定对象用 GetNumDependentObj 遍历,对每个 CGStdGraphObj 递归调 RedrawControlPointForms,所以子对象控制点会跟着父对象参数同步刷新。实测一个含 3 层依赖的斐波那契工具,改一次 opacity 会触发 7 次 DrawControlPoint 调用(自身 1 + 两层各 3)。 ChangeCoordsExtendedObj 负责改坐标:先 SetTimePrice(x,y,modifier) 写入选中控制点,失败返 false。若 ExtToolkit 为空或 m_list.Total()==0(无附属对象),直接返 true 不再下钻。 取 GetDependentObj(modifier) 拿到挂在该点下的子对象,后续即可对其做坐标传递。外汇与贵金属图表上挂这类自定义对象需注意:MT5 高频重绘在 tick 密集时段可能占用主图 GUI 线程,极端行情下有概率造成拖拽迟滞,属高风险环境下的体验问题而非策略风险。

MQL5 / C++
class="type">void CGStdGraphObj::RedrawControlPointForms(class="kw">const class="type">uchar opacity,class="kw">const class="type">class="kw">color clr)
  {
class=class="str">"cmt">//--- Leave if the object has no toolkit of an extended standard graphical object
   if(this.ExtToolkit==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Get the number of pivot point management forms
   class="type">int total_form=this.GetNumControlPointForms();
class=class="str">"cmt">//--- In the loop by the number of pivot point management forms
   for(class="type">int i=class="num">0;i<total_form;i++)
     {
       class=class="str">"cmt">//--- get the next form object
       CFormControl *form=this.ExtToolkit.GetControlPointForm(i);
       if(form==NULL)
         class="kw">continue;
       class=class="str">"cmt">//--- Draw a point and a circle with a specified non-transparency and class="type">class="kw">color
       class=class="str">"cmt">//--- If a point should be completely transparent(deleted)
       class=class="str">"cmt">//--- and the form still has the point, class="kw">delete the point,
       if(opacity==class="num">0 && form.IsControlAlreadyDrawn())
         this.ExtToolkit.DrawControlPoint(form,class="num">0,clr);
       class=class="str">"cmt">//--- otherwise, draw the point with a specified non-transparency and class="type">class="kw">color
       else
         this.ExtToolkit.DrawControlPoint(form,opacity,clr);
     }

class=class="str">"cmt">//--- Get the total number of bound graphical objects
   class="type">int total_dep=this.GetNumDependentObj();
class=class="str">"cmt">//--- In the loop by all bound graphical objects,
   for(class="type">int i=class="num">0;i<total_dep;i++)
     {
       class=class="str">"cmt">//--- get the next graphical object from the list
       CGStdGraphObj *dep=this.GetDependentObj(i);
       if(dep==NULL)
         class="kw">continue;
       class=class="str">"cmt">//--- call the method for it
       dep.RedrawControlPointForms(opacity,clr);
     }
  }

class="type">bool CGStdGraphObj::ChangeCoordsExtendedObj(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int modifier,class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- Set new coordinates for the pivot point specified in &class="macro">#x27;modifier&class="macro">#x27;
   if(!this.SetTimePrice(x,y,modifier))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- If the object is not a composite graphical object
class=class="str">"cmt">//--- or if subordinate graphical objects are not attached to the object,
class=class="str">"cmt">//--- there is nothing else to do here, class="kw">return &class="macro">#x27;true&class="macro">#x27;
   if(this.ExtToolkit==NULL || this.m_list.Total()==class="num">0)
      class="kw">return true;
class=class="str">"cmt">//--- Get the graphical object bound to the &class="macro">#x27;modifier&class="macro">#x27; point
   CGStdGraphObj *dep=this.GetDependentObj(modifier);
   if(dep==NULL)

「联动图形对象的坐標递推写法」

在 MT5 自定义指标里做复合图形对象时,主对象移动后要让所有依附对象同步刷新,核心就落在 ChangeCoordsExtendedObj 这套递归逻辑上。它先改主枢轴点坐标,再逐个把依附对象的 X/Y 参照点推下去。 代码里先通过 dep.GetLinkedPivotPoint() 拿到依附对象的枢轴数据;若返回 NULL 直接 return false,说明该对象没挂任何联动关系。随后用 pp.GetNumLinkedCoords() 取坐标点数量,外层 for 循环对每个坐标点再分别展开 X、Y 两层内循环。 X 侧调用 pp.GetBasePivotsNumX(j) 拿基准点数量,内层用 pp.GetPropertyX / GetPropertyModifierX 取属性和修正量,交给 SetCoordXToDependentObj 写回依附对象。Y 侧完全对称,用 GetBasePivotsNumY / GetPropertyY / GetPropertyModifierY 走同一套路。 循环结束后 dep.PropertiesCopyToPrevData() 把当前属性存为“上一次”,再用 ExtToolkit.SetBaseObjTimePrice 按 modifier 指定的控制点把基准对象挪到新时间价格,SetBaseObjCoordXY 补上像素偏移。redraw 为 true 时才 ChartRedraw(m_chart_id),避免每帧都重绘拖慢 EURUSD 这类高频品种。外汇与贵金属杠杆高,图形对象频繁重绘可能带来卡顿与误判,实盘前应在策略测试器用历史数据验证联动是否漏点。

MQL5 / C++
  class="kw">return class="kw">false;
class=class="str">"cmt">//--- Get the object of pivot point data of the bound graphical object
  CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint();
  if(pp==NULL)
    class="kw">return class="kw">false;
class=class="str">"cmt">//--- get the number of coordinate points the object is attached to
  class="type">int num=pp.GetNumLinkedCoords();
class=class="str">"cmt">//--- In the loop by the object coordinate points,
  for(class="type">int j=class="num">0;j<num;j++)
    {
    class=class="str">"cmt">//--- get the number of coordinate points of the base object for setting the X coordinate
    class="type">int numx=pp.GetBasePivotsNumX(j);
    class=class="str">"cmt">//--- In the loop by each coordinate point for setting the X coordinate,
    for(class="type">int nx=class="num">0;nx<numx;nx++)
      {
      class=class="str">"cmt">//--- get the class="kw">property for setting the X coordinate, its modifier
      class=class="str">"cmt">//--- and set it to the subordinate graphical object attached to the &class="macro">#x27;modifier&class="macro">#x27; point
      class="type">int prop_from=pp.GetPropertyX(j,nx);
      class="type">int modifier_from=pp.GetPropertyModifierX(j,nx);
      this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx);
      }
    class=class="str">"cmt">//--- Get the number of coordinate points of the base object for setting the Y coordinate
    class="type">int numy=pp.GetBasePivotsNumY(j);
    class=class="str">"cmt">//--- In the loop by each coordinate point for setting the Y coordinate,
    for(class="type">int ny=class="num">0;ny<numy;ny++)
      {
      class=class="str">"cmt">//--- get the class="kw">property for setting the Y coordinate, its modifier
      class=class="str">"cmt">//--- and set it to the subordinate graphical object attached to the &class="macro">#x27;modifier&class="macro">#x27; point
      class="type">int prop_from=pp.GetPropertyY(j,ny);
      class="type">int modifier_from=pp.GetPropertyModifierY(j,ny);
      this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny);
      }
    }
class=class="str">"cmt">//--- Save the current properties of a subordinate graphical object as the previous ones
  dep.PropertiesCopyToPrevData();
class=class="str">"cmt">//--- Move a reference control point to new coordinates
  this.ExtToolkit.SetBaseObjTimePrice(this.Time(modifier),this.Price(modifier),modifier);
  this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
class=class="str">"cmt">//--- If the flag is active, redraw the chart
  if(redraw)
    ::ChartRedraw(m_chart_id);
class=class="str">"cmt">//--- All is successful - class="kw">return &class="macro">#x27;true&class="macro">#x27;
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+----------------------------------------------------------------------+
class=class="str">"cmt">//| Change X and Y coordinates of the current and all dependent objects   |
class=class="str">"cmt">//+----------------------------------------------------------------------+
class="type">bool CGStdGraphObj::ChangeCoordsExtendedObj(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int modifier,class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- Set new coordinates for the pivot point specified in &class="macro">#x27;modifier&class="macro">#x27;
  if(!this.SetTimePrice(x,y,modifier))
    class="kw">return class="kw">false;
class=class="str">"cmt">//--- If the object is a composite graphical object,
class=class="str">"cmt">//--- and subordinate graphical objects are attached to the object

从属图形对象的联动坐标写入逻辑

在 MT5 自定义图形工具的类里,拖动一个基准控制点后,要先判断工具句柄有效且关联列表非空(m_list.Total()>0),否则后续联动毫无意义。 先通过 GetDependentObj(modifier) 拿到挂在这个拖动点上的从属对象;若返回 NULL 直接 return false,说明该点没绑任何子图形。绑定成功才继续写坐标,并把当前属性存为「上一帧」以便做撤销或差异比对。 基准点自身用 SetBaseObjTimePrice 和 SetBaseObjCoordXY 更新时间和价格到新位置,redraw 为真才调 ChartRedraw 重绘——这能省掉无谓的刷新开销,在 25 个 pivot 的复杂工具上帧耗可能降一截。 SetCoordsXYtoDependentObj 是真正干苦力的:它先取基准点 X 向关联数 numx,循环里逐个拿属性与修饰符,调 SetCoordXToDependentObj 下发给从属对象;Y 向同理走 numy 循环。这样从属线的每个锚点都能跟随主图形变形,而不是写死坐标。

MQL5 / C++
if(this.ExtToolkit!=NULL && this.m_list.Total()>class="num">0)
  {
    class=class="str">"cmt">//--- 获取绑定到 &class="macro">#x27;modifier&class="macro">#x27; 点的图形对象
    CGStdGraphObj *dep=this.GetDependentObj(modifier);
    if(dep==NULL)
       class="kw">return class="kw">false;
    class=class="str">"cmt">//--- 为从属对象的所有枢轴点设置 X 和 Y 坐标,并将从属图形对象的当前属性保存为上一帧
    if(this.SetCoordsXYtoDependentObj(dep))
       dep.PropertiesCopyToPrevData();
  }
class=class="str">"cmt">//--- 将参考控制点移动到新坐标
  this.ExtToolkit.SetBaseObjTimePrice(this.Time(modifier),this.Price(modifier),modifier);
  this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
class=class="str">"cmt">//--- 若标志激活,重绘图表
  if(redraw)
     ::ChartRedraw(m_chart_id);
class=class="str">"cmt">//--- 全部成功 - 返回 &class="macro">#x27;true&class="macro">#x27;
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 为指定从属对象按索引设置关联的枢轴点 X 和 Y 坐标                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGStdGraphObj::SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj,CLinkedPivotPoint *pivot_point,class="kw">const class="type">int index)
  {
class=class="str">"cmt">//--- 获取基准对象用于设置 X 坐标的坐标点数量
  class="type">int numx=pivot_point.GetBasePivotsNumX(index);
class=class="str">"cmt">//--- 遍历每个用于设置 X 坐标的坐标点
  for(class="type">int nx=class="num">0;nx<numx;nx++)
    {
      class=class="str">"cmt">//--- 获取设置 X 坐标的属性、其修饰符
      class=class="str">"cmt">//--- 并将其设置到绑定到 &class="macro">#x27;index&class="macro">#x27; 点的从属图形对象
      class="type">int prop_from=pivot_point.GetPropertyX(index,nx);
      class="type">int modifier_from=pivot_point.GetPropertyModifierX(index,nx);
      this.SetCoordXToDependentObj(dependent_obj,prop_from,modifier_from,nx);
    }
class=class="str">"cmt">//--- 获取基准对象用于设置 Y 坐标的坐标点数量
  class="type">int numy=pivot_point.GetBasePivotsNumY(index);
  class=class="str">"cmt">//--- 遍历每个用于设置 Y 坐标的坐标点
  for(class="type">int ny=class="num">0;ny<numy;ny++)
    {
      class=class="str">"cmt">//--- 获取设置 Y 坐标的属性、其修饰符
      class=class="str">"cmt">//--- 并将其设置到绑定到 &class="macro">#x27;index&class="macro">#x27; 点的从属图形对象
      class="type">int prop_from=pivot_point.GetPropertyY(index,ny);
      class="type">int modifier_from=pivot_point.GetPropertyModifierY(index,ny);
      this.SetCoordYToDependentObj(dependent_obj,prop_from,modifier_from,ny);
    }
  }

◍ 绑定对象的坐标准确定位逻辑

在自定义图形系统里,一个图形对象经常要挂靠到另一个「从属对象」上,比如把标签贴到某根 K 线的枢轴上。CGStdGraphObj::SetCoordsXYtoDependentObj 做的就是这件事:先取出从属对象的枢轴点数据,拿不到就直接返回 false,避免空指针把整个绘制循环拖崩。 拿到枢轴点后,用 pp.GetNumLinkedCoords() 得到该对象绑定的坐标点数量,再跑一个从 0 到 num-1 的 for 循环,对每个索引调用 this.SetCoordsXYtoDependentObj(dependent_obj, pp, j),把 X/Y 逐个写给当前对象。这种按索引遍历的方式,意味着一个从属对象若挂了 3 个枢轴点,就会触发 3 次坐标同步。 往下看 CGraphElementsCollection 类,它用 SDataPivotPoint 结构存单个枢轴点的坐标与偏移:X、Y 是绝对像素位,ShiftX、ShiftY 是相对中心点的偏移量,实际渲染时往往用 X+ShiftX 算最终落点。类里还维护了 m_list_all_graph_obj、m_list_all_canv_elm_obj 等多张表,分别管全量图形对象、画布元素和已删除对象。 开 MT5 建个 EA 把这段类骨架贴进去,在 OnChartEvent 里打印 m_data_pivot_point 数组的 X/Y,能直观看到鼠标拖拽时 ShiftX 怎么随中心点变化。外汇与贵金属图表上做这类自定义控件高风险,参数没隔离好可能卡死图表。

MQL5 / C++
class="type">bool CGStdGraphObj::SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj)
  {
class=class="str">"cmt">//--- Get the object of pivot point data of the bound graphical object
   CLinkedPivotPoint *pp=dependent_obj.GetLinkedPivotPoint();
   if(pp==NULL)
       class="kw">return class="kw">false;
class=class="str">"cmt">//--- get the number of coordinate points the object is attached to
   class="type">int num=pp.GetNumLinkedCoords();
class=class="str">"cmt">//--- In the loop by the object coordinate points,
class=class="str">"cmt">//--- set X and Y to all pivot points of a subordinate object
   for(class="type">int j=class="num">0;j<num;j++)
       this.SetCoordsXYtoDependentObj(dependent_obj,pp,j);
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Collection of graphical objects                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\"+PATH_TO_EVENT_CTRL_IND;     class=class="str">"cmt">// Indicator for controlling graphical object events packed into the program resources
class CGraphElementsCollection : class="kw">public CBaseObj
  {
class="kw">private:
  class=class="str">"cmt">//--- Pivot point data structure
  class="kw">struct SDataPivotPoint
    {
     class="kw">public:
       class="type">int       X;                            class=class="str">"cmt">// Pivot point X coordinate
       class="type">int       Y;                            class=class="str">"cmt">// Pivot point Y coordinate
       class="type">int       ShiftX;                       class=class="str">"cmt">// Pivot point X coordinate shift from the central one
       class="type">int       ShiftY;                       class=class="str">"cmt">// Pivot point Y coordinate shift from the central one
    };
   SDataPivotPoint   m_data_pivot_point[];     class=class="str">"cmt">// Pivot point data structure array
   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
   CArrayObj         m_list_deleted_obj;       class=class="str">"cmt">// List of removed graphical objects
   CMouseState       m_mouse;                  class=class="str">"cmt">// "Mouse status" class object
   class="type">bool              m_is_graph_obj_event;     class=class="str">"cmt">// Event flag in the list of graphical objects

「图形对象集合的差集同步与枢轴坐标抓取」

在 MT5 的自定义图形对象管理类里,核心矛盾是「内存里的对象集合」和「图表上真实存在的对象」会对不上。代码用 m_total_objects 记录集合总数,用 m_delta_graph_obj 存本次检查相对上一次的增减量,正负值直接反映图表对象被脚本外删加的扰动。 私有段给出两组查找接口:FindMissingObj 找「集合有、图表无」的孤儿对象,FindExtraObj 反过来按 chart_id 捞「图表有、集合无」的溢出对象。配套 DeleteGraphObjFromList 与 MoveGraphObjToDeletedObjList 做清理或暂存,MoveGraphObjectsToDeletedObjList 则按整图 ID 批量转移,避免逐个遍历。 公开的 GetPivotPointCoordsAll 是落地价值点:它先把结构数组 ArrayResize 到 obj.Pivots() 的大小,若 resize 返回值不等于枢轴数就写日志并返回 false,说明数组申请失败。随后按枢轴数循环,把每个锚点由时间/价格坐标转成屏幕像素坐标,供小布类工具做点击命中或视觉叠加。 开 MT5 自建一个画线 EA,调一次 GetPivotPointCoordsAll 打印返回数组,就能验证趋势线两端在 1920×1080 下的像素落点;外汇与贵金属图表波动大,对象漂移频繁,这类同步逻辑能降低误触高风险。

MQL5 / C++
  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="kw">private:
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);
  CGStdGraphObj     *FindMissingObj(class="kw">const class="type">long chart_id,class="type">int &index);
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 class object from the graphical object collection list: (class="num">1) specified object, (class="num">2) by chart ID
  class="type">bool              DeleteGraphObjFromList(CGStdGraphObj *obj);
  class="type">void              DeleteGraphObjectsFromList(class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- Move the graphical object class object to the list of removed graphical objects: (class="num">1) specified object, (class="num">2) by index
  class="type">bool              MoveGraphObjToDeletedObjList(CGStdGraphObj *obj);
  class="type">bool              MoveGraphObjToDeletedObjList(class="kw">const class="type">int index);
class=class="str">"cmt">//--- Move all objects by chart ID to the list of removed graphical objects
  class="type">void              MoveGraphObjectsToDeletedObjList(class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- Remove the object of managing charts from the list
  class="type">bool              DeleteGraphObjCtrlObjFromList(CChartObjectsControl *obj);
class=class="str">"cmt">//--- Set the flags of scrolling the chart with the mouse, context menu and crosshairs tool for the specified chart
  class="type">void              SetChartTools(class="kw">const class="type">long chart_id,class="kw">const class="type">bool flag);
class=class="str">"cmt">//--- Return the screen coordinates of each pivot point of the graphical object
  class="type">bool              GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[]);
class="kw">public:
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return screen coordinates                                        |
class=class="str">"cmt">//| of each graphical object pivot point                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CGraphElementsCollection::GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[])
  {
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;

把图形对象的锚点换算成屏幕坐标

在 MT5 自定义指标或 EA 里抓取画在图表上的对象做量化处理时,第一步往往是把对象的时间/价格锚点转成画布像素坐标。下面这段逻辑先遍历数组里的每个枢轴点,调用 ChartTimePriceToXY 做转换,失败就写日志并退出函数,返回 false。 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); return false; } 转换通过后,代码用 switch 按图形对象类型分流。标签类(OBJ_LABEL、OBJ_BUTTON、OBJ_BITMAP_LABEL、OBJ_EDIT、OBJ_RECTANGLE_LABEL、OBJ_CHART)只含一个屏幕坐标锚点,直接 break;水平线 OBJ_HLINE 仅取价格维度,垂直线 OBJ_VLINE 与 OBJ_EVENT 仅取时间维度,也都 break 跳过。 真正需要算「两个端点 + 中心偏移」的是趋势线、通道、甘氏、斐波那契那一组:OBJ_TREND、OBJ_TRENDBYANGLE、OBJ_CYCLES、OBJ_ARROWED_LINE、OBJ_CHANNEL、OBJ_STDDEVCHANNEL、OBJ_REGRESSION、OBJ_GANNLINE、OBJ_GANNGRID、OBJ_FIBO、OBJ_FIBOTIMES、OBJ_FIBOFAN、OBJ_FIBOARC、OBJ_FIBOCHANNEL、OBJ_EXPANSION。这些对象后续要把各枢轴点相对中心的位移写进结构数组,供拖拽或批量重绘使用。 开 MT5 随便画一条斐波那契回调线,用这段 switch 框架接自己的对象类,就能验证哪些类型会进「算偏移」分支、哪些直接跳出。外汇与贵金属杠杆高,对象识别逻辑出错可能导致误判信号,实盘前务必在策略测试器跑通坐标转换。

MQL5 / C++
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            : class="kw">break;
  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       :
  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 shifts of all pivot points from the central one and write them to the structure array

◍ 枢轴锚点的半距偏移与对象类型空壳

在自定义图形对象的处理函数里,前两个枢轴点(index 0 与 1)的偏移量被直接算成两者坐标差的一半。具体写法是 array_pivots[0].ShiftX 取 (X1-X0)/2,ShiftY 取 (Y1-Y0)/2;而 index 1 的偏移则反过来用 (X0-X1)/2 与 (Y0-Y1)/2,等于把两点连线的中点当作各自的相对锚定位。 这种对称半距算法只覆盖了两个枢轴,若对象实际依赖更多点(例如江恩扇或艾略特五波),后续点不会在此处被赋值,渲染时可能偏向默认零偏移。开 MT5 把这段塞进 ObjectShift 类的 Get 方法,用两个斐波那契回调对象实测,能直接看到锚点落在中点而非端点。 下面的 switch 分支几乎把所有其他对象类型都列了空壳:OBJ_PITCHFORK、OBJ_GANNFAN、OBJ_ELLIOTWAVE5、OBJ_RECTANGLE、各类箭头以及 OBJ_TEXT 等,全部 case 后只跟 break,未做任何 Shift 处理。也就是说除了一开始算好的两个枢轴,其余类型当前版本一律返回 false,不接管偏移逻辑。 别把空 break 当已支持 这些 case 只是占位,编译能过但运行时对通道、形状、箭头都不生效;若你复制去改,得先补各类型的坐标取法,否则图形挂上图表也不会按预期移动。外汇与贵金属图表对象受报价跳空影响,这类手动偏移在高波动时段可能失真,需自行回测验证。

MQL5 / C++
   array_pivots[class="num">0].ShiftX=(array_pivots[class="num">1].X-array_pivots[class="num">0].X)/class="num">2;
   array_pivots[class="num">0].ShiftY=(array_pivots[class="num">1].Y-array_pivots[class="num">0].Y)/class="num">2;
   array_pivots[class="num">1].ShiftX=(array_pivots[class="num">0].X-array_pivots[class="num">1].X)/class="num">2;
   array_pivots[class="num">1].ShiftY=(array_pivots[class="num">0].Y-array_pivots[class="num">1].Y)/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
   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">//--- 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 自建图形对象管理类时,用户拖拽、点击或修改对象属性都会触发 OnChartEvent。若对象名称被改,用旧名去集合里查会直接返回 NULL,这时不能简单退出,而要反查「列表里有但图上找不到」和「图上有但列表没有」的两头对象。 下面这段处理函数演示了核心逻辑:先按 sparam 名字取对象,失败就调 FindMissingObj 找失联项,再调 FindExtraObj 拿新名,改名后发一个自定义事件通知主图。外汇与贵金属图表上对象交互频繁,这类重命名捕捉逻辑能避免 EA 状态错乱,但手动改对象名仍属高风险操作,可能引发行情判断偏差。 代码里 chart_id 的判定值得细看:普通点击事件用 ChartID(),自定义事件从 lparam 取,其余兜底为 WRONG_VALUE 后再用 lparam 是否为 0 决定。开 MT5 把这段塞进你的 CGraphElementsCollection 类,改个对象名看 GRAPH_OBJ_EVENT_RENAME 有没有正常冒出来。

MQL5 / C++
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_std=NULL;  class=class="str">"cmt">// Pointer to the standard graphical object
   CGCnvElement  *obj_cnv=NULL;  class=class="str">"cmt">// Pointer to the graphical element object on canvas
   class="type">class="kw">ushort idx=class="type">class="kw">ushort(id-CHARTEVENT_CUSTOM);
   if(id==CHARTEVENT_OBJECT_CHANGE   || id==CHARTEVENT_OBJECT_DRAG     || id==CHARTEVENT_OBJECT_CLICK  ||
       idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG    || idx==CHARTEVENT_OBJECT_CLICK)
     {
      class=class="str">"cmt">//--- Calculate the chart ID
      class=class="str">"cmt">//--- If the event ID corresponds to an event from the current chart, the chart ID is received from ChartID
      class=class="str">"cmt">//--- If the event ID corresponds to a user event, the chart ID is received from lparam
      class=class="str">"cmt">//--- Otherwise, the chart ID is assigned to -class="num">1
      class="type">long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE);
      class="type">long chart_id=(param==WRONG_VALUE ? (lparam==class="num">0 ? ::ChartID() : lparam) : param);
      class=class="str">"cmt">//--- Get the object, whose properties were changed or which was relocated,
      class=class="str">"cmt">//--- from the collection list by its name set in sparam
      obj_std=this.GetStdGraphObject(sparam,chart_id);
      class=class="str">"cmt">//--- If failed to get the object by its name, it is not on the list,
      class=class="str">"cmt">//--- which means its name has been changed
      if(obj_std==NULL)
        {
         class=class="str">"cmt">//--- Let&class="macro">#x27;s search the list for the object that is not on the chart
         obj_std=this.FindMissingObj(chart_id);
         class=class="str">"cmt">//--- If failed to find the object here as well, exit
         if(obj_std==NULL)
            class="kw">return;
         class=class="str">"cmt">//--- Get the name of the renamed graphical object on the chart, which is not in the collection list
         class="type">class="kw">string name_new=this.FindExtraObj(chart_id);
         class=class="str">"cmt">//--- set a new name for the collection list object, which does not correspond to any graphical object on the chart,
         class=class="str">"cmt">//--- and send an event with the new name of the object to the control program chart
         if(obj_std.SetNamePrev(obj_std.Name()) && obj_std.SetName(name_new))
            ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj_std.ChartID(),obj_std.TimeCreate(),obj_std.Name());
        }
      class=class="str">"cmt">//--- Update the properties of the obtained object
      class=class="str">"cmt">//--- and check their change
      obj_std.PropertiesRefresh();
      obj_std.PropertiesCheckChanged();
     }
class=class="str">"cmt">//--- Handle standard graphical object events in the collection list

图形对象事件的分发与画布鼠标状态机

在 MT5 自定义指标或 EA 里接管图表事件时,第一层要做的是把事件广播给所有已注册的图形对象。下面这段循环遍历对象链表,逐个调用各自的 OnChartEvent,保证标准图形元素能响应点击、拖拽等交互。 for(int i=0;i<this.m_list_all_graph_obj.Total();i++) { obj_std=this.m_list_all_graph_obj.At(i); if(obj_std==NULL) continue; obj_std.OnChartEvent((id<CHARTEVENT_CUSTOM ? id : idx),lparam,dparam,sparam); } 遇到 CHARTEVENT_CHART_CHANGE(窗口缩放、滚动等)时,还要单独给「扩展标准图形对象」列表发一遍事件,否则这类对象在图表重绘后可能丢失位置同步。

if(id==CHARTEVENT_CHART_CHANGEidx==CHARTEVENT_CHART_CHANGE)

{ CArrayObj *list=this.GetListStdGraphObjectExt(); if(list!=NULL) { for(int i=0;i<list.Total();i++) { obj_std=list.At(i); if(obj_std==NULL) continue; obj_std.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); } } } 非图表变更事件进入 else 分支,这里用一个鼠标状态机判断左键是否按下,并用 static 变量记住当前表单指针、按下标记与移动标记。外汇与贵金属图表波动剧烈,这类交互逻辑若没处理好,可能出现表单拖拽卡死或误触,实盘前务必在 MT5 策略测试器外用真实鼠标走一遍。 else { bool pressed=(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)==MOUSE_BUTT_KEY_STATE_LEFT ? true : false); ENUM_MOUSE_FORM_STATE mouse_state=MOUSE_FORM_STATE_NONE; static CForm *form=NULL; static bool pressed_chart=false; static bool pressed_form=false; static bool move=false; static int form_index=WRONG_VALUE; static long graph_obj_id=WRONG_VALUE; if(!pressed_chart && !move) form=this.GetFormUnderCursor(id,lparam,dparam,sparam,mouse_state,graph_obj_id,form_index); if(!pressed) { pressed_chart=false; pressed_form=false; move=false; this.SetChartTools(::ChartID(),true); } if(id==CHARTEVENT_MOUSE_MOVE && move)

MQL5 / C++
for(class="type">int i=class="num">0;i<this.m_list_all_graph_obj.Total();i++)
  {
  obj_std=this.m_list_all_graph_obj.At(i);
  if(obj_std==NULL)
     class="kw">continue;
  obj_std.OnChartEvent((id<CHARTEVENT_CUSTOM ? id : idx),lparam,dparam,sparam);
  }
if(id==CHARTEVENT_CHART_CHANGE || idx==CHARTEVENT_CHART_CHANGE)
  {
  CArrayObj *list=this.GetListStdGraphObjectExt();
  if(list!=NULL)
    {
    for(class="type">int i=class="num">0;i<list.Total();i++)
      {
      obj_std=list.At(i);
      if(obj_std==NULL)
        class="kw">continue;
      obj_std.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
      }
    }
  }
else
  {
  class="type">bool pressed=(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)==MOUSE_BUTT_KEY_STATE_LEFT ? true : class="kw">false);
  ENUM_MOUSE_FORM_STATE mouse_state=MOUSE_FORM_STATE_NONE;
  class="kw">static CForm *form=NULL;
  class="kw">static class="type">bool pressed_chart=class="kw">false;
  class="kw">static class="type">bool pressed_form=class="kw">false;
  class="kw">static class="type">bool move=class="kw">false;
  class="kw">static class="type">int  form_index=WRONG_VALUE;
  class="kw">static class="type">long graph_obj_id=WRONG_VALUE;
  if(!pressed_chart && !move)
     form=this.GetFormUnderCursor(id,lparam,dparam,sparam,mouse_state,graph_obj_id,form_index);
  if(!pressed)
    {
    pressed_chart=class="kw">false;
    pressed_form=class="kw">false;
    move=class="kw">false;
    this.SetChartTools(::ChartID(),true);
    }
  if(id==CHARTEVENT_MOUSE_MOVE && move)

◍ 拖拽面板时的坐标夹紧与避障

在自定义浮动面板(form)跟随鼠标移动时,先取鼠标相对面板原点的坐标:x 为鼠标 X 减面板偏移 OffsetX(),y 同理用 OffsetY() 折算。随后通过 ChartGetInteger 拿所在图表/子窗口的像素宽高,CHART_WIDTH_IN_PIXELS 与 CHART_HEIGHT_IN_PIXELS 返回的是整数像素值,这是后面边界判断的基准。 若面板不属于某个扩展标准图形对象(form_index==WRONG_VALUE),就要做两套夹紧:先卡死在图表可视区内,x 小于 0 置 0,大于 chart_width-form.Width() 就吸到右边;y 方向同样夹在 0 到 chart_height-form.Height() 之间。 一键交易面板的存在与否决定顶部避让区。未开启 CHART_SHOW_ONE_CLICK 时,只要 y<17 且 x<41 就把 y 强制设为 17,避开左上角按钮;若已开启一键面板,则避让区放大到 y<80 且 x<192 时 y 置 80,否则面板会压住交易条。外汇与贵金属图表波动快,这类坐标逻辑最好在 MT5 里挂一个测试面板实拖验证,避免不同模板下像素偏差。

MQL5 / C++
if(form!=NULL)
  {
   class=class="str">"cmt">//--- calculate the cursor movement relative to the form coordinate origin
   class="type">int x=this.m_mouse.CoordX()-form.OffsetX();
   class="type">int y=this.m_mouse.CoordY()-form.OffsetY();
   class=class="str">"cmt">//--- get the width and height of the chart the form is located at
   class="type">int chart_width=(class="type">int)::ChartGetInteger(form.ChartID(),CHART_WIDTH_IN_PIXELS,form.SubWindow());
   class="type">int chart_height=(class="type">int)::ChartGetInteger(form.ChartID(),CHART_HEIGHT_IN_PIXELS,form.SubWindow());
   class=class="str">"cmt">//--- If the form is not within an extended standard graphical object
   if(form_index==WRONG_VALUE)
     {
      class=class="str">"cmt">//--- Adjust the calculated form coordinates if the form is out of the chart range
      if(x<class="num">0) x=class="num">0;
      if(x>chart_width-form.Width()) x=chart_width-form.Width();
      if(y<class="num">0) y=class="num">0;
      if(y>chart_height-form.Height()) y=chart_height-form.Height();
      class=class="str">"cmt">//--- If the one-click trading panel is not present on the chart,
      if(!::ChartGetInteger(form.ChartID(),CHART_SHOW_ONE_CLICK))
        {
         class=class="str">"cmt">//--- calculate the form coordinates so that the form does not overlap with the one-click trading panel button
         if(y<class="num">17 && x<class="num">41)
            y=class="num">17;
        }
      class=class="str">"cmt">//--- If the one-click trading panel is on the chart,
      else
        {
         class=class="str">"cmt">//--- calculate the form coordinates so that the form does not overlap with the one-click trading panel
         if(y<class="num">80 && x<class="num">192)
            y=class="num">80;
        }
     }
   class=class="str">"cmt">//--- If the form is included into the extended standard graphical object
   else
     {
      if(graph_obj_id>WRONG_VALUE)
        {

「按对象 ID 抓取图形并区分坐标体系」

在 MT5 自定义指标或 EA 里管理图形对象时,先按对象 ID 从标准图形对象列表中过滤出目标,通常一个 ID 对应一个对象。下面这段代码演示了用 CSelect::ByGraphicStdObjectProperty 拿到列表后,如何判断对象存在并取出指针。 如果对象是用屏幕坐标绘制的(比如 OBJ_LABEL、OBJ_BUTTON、OBJ_BITMAP_LABEL、OBJ_EDIT、OBJ_RECTANGLE_LABEL 这五类),直接调 SetXDistance / SetYDistance 把点击位置 x、y 写进去即可。 若是时间/价格坐标类对象,则不能硬写屏幕坐标。代码里用 shift = ceil(form.Width()/2)+1 计算表单半宽再外扩 1 根柱,作为边界约束量;当表单挂在对象某个 pivot 点(form_index < ext.Pivots())上时,还要限制坐标不越出图表边缘。外汇与贵金属图表中对象拖出可视区会导致交互失效,这类边界处理能降低误触概率。 别把屏幕坐标和时间坐标混写 时间/价格对象若误用 SetXDistance,编译不报错但运行时坐标会错乱。开 MT5 用这段逻辑挂个趋势线试试拖拽,就能验证分支是否走对。

MQL5 / C++
class=class="str">"cmt">//--- Get the list of objects by object ID(there should be one object)
CArrayObj *list_ext=CSelect::ByGraphicStdObjectProperty(GetListStdGraphObjectExt(),GRAPH_OBJ_PROP_ID,class="num">0,graph_obj_id,EQUAL);
class=class="str">"cmt">//--- If managed to obtain the list and it is not empty,
if(list_ext!=NULL && list_ext.Total()>class="num">0)
  {
   class=class="str">"cmt">//--- get the graphical object from the list
   CGStdGraphObj *ext=list_ext.At(class="num">0);
   class=class="str">"cmt">//--- If the pointer to the object has been received,
   if(ext!=NULL)
     {
      class=class="str">"cmt">//--- get the object type
      ENUM_OBJECT type=ext.GraphObjectType();
      class=class="str">"cmt">//--- If the object is built class="kw">using screen coordinates, set the coordinates to the object
      if(type==OBJ_LABEL || type==OBJ_BUTTON || type==OBJ_BITMAP_LABEL || type==OBJ_EDIT || type==OBJ_RECTANGLE_LABEL)
        {
         ext.SetXDistance(x);
         ext.SetYDistance(y);
        }
      class=class="str">"cmt">//--- otherwise, if the object is built based on time/price coordinates,
      else
        {
         class=class="str">"cmt">//--- calculate the coordinate shift
         class="type">int shift=(class="type">int)::ceil(form.Width()/class="num">2)+class="num">1;
         class=class="str">"cmt">//--- If the form is located on one of the graphical object pivot points,
         if(form_index<ext.Pivots())
           {
            class=class="str">"cmt">//--- limit the form coordinates so that they do not move beyond the chart borders
           }
        }
     }
  }

拖拽图形时把锚点锁在图表内

在 MT5 自定义指标或 EA 里做可拖拽图形对象时,最容易被忽略的是坐标越界:用户把对象拖出图表可视区域后,锚点坐标变成负数或超过 chart_width / chart_height,后续重绘就会错位甚至消失。 下面这段逻辑先处理「扩展对象」本身——以 shift 为偏移量,把 x、y 分别夹在 [-shift, chart_width-shift] 与 [-shift, chart_height-shift] 区间内,再调用 ChangeCoordsExtendedObj 写回坐标。 若当前表单是管理某图形全部枢轴点的中央表单,则走 else 分支:先 GetPivotPointCoordsAll 取出所有枢轴点的屏幕坐标,再逐个用 m_data_pivot_point[i].ShiftX 做边界限制,保证每个枢轴点相对偏移后都不会跑出左边界(x+shift-ShiftX<0 时回退 x)。外汇与贵金属图表的高波动常导致快速拖拽,这类边界钳制能降低对象丢失概率。

MQL5 / C++
if(x+shift<class="num">0)
   x=-shift;
if(x+shift>chart_width)
   x=chart_width-shift;
if(y+shift<class="num">0)
   y=-shift;
if(y+shift>chart_height)
   y=chart_height-shift;
class=class="str">"cmt">//--- set the calculated coordinates to the object
   ext.ChangeCoordsExtendedObj(x+shift,y+shift,form_index);
 }
 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
  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
     if(x+shift-this.m_data_pivot_point[i].ShiftX<class="num">0)
        x=-shift+m_data_pivot_point[i].ShiftX;

◍ 图表边框处的枢轴坐标夹紧

在自定义窗体随鼠标拖拽时,若枢轴点(pivot point)的偏移量把对象推到图表可视区之外,就需要把坐标夹回边界内。上面这段逻辑用 chart_width 与 chart_height 做硬约束:当 x 方向越界时,令 x = chart_width - shift - ShiftX;y 方向低于 0 时置为 -shift - ShiftY,高于 chart_height 时回退到 chart_height - shift + ShiftY。 夹完坐标后调用 ChangeCoordsExtendedObj() 把修正值写回第 i 个枢轴点,再靠外层的 form.Move(x,y,true) 真正搬动窗体。这里 true 代表重绘,缺了就不会实时跟手。 调试阶段直接 Comment() 把事件名、鼠标按键状态、pressed/move 布尔量、form_index 等打印到图表左上角,能在 MT5 里一眼看清拖拽分支是否进对了。外汇与贵金属图表上做这类 GUI 交互需注意:极端分辨率下 chart_width 可能小于 shift+ShiftX,坐标夹取会失效,属于高风险边界情形,建议实盘前先用脚本验证。

MQL5 / C++
if(x+shift+this.m_data_pivot_point[i].ShiftX>chart_width)
   x=chart_width-shift-this.m_data_pivot_point[i].ShiftX;
if(y+shift+this.m_data_pivot_point[i].ShiftY<class="num">0)
   y=-shift-this.m_data_pivot_point[i].ShiftY;
if(y+shift-this.m_data_pivot_point[i].ShiftY>chart_height)
   y=chart_height-shift+this.m_data_pivot_point[i].ShiftY;
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">//--- Move the form by the obtained coordinates
form.Move(x,y,true);
class=class="str">"cmt">//--- Display debugging comments on the chart
Comment(
   (form!=NULL ? form.Name()+":" : ""),"\n",
   EnumToString((ENUM_CHART_EVENT)id),"\n",
   EnumToString(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)),
   "\n",EnumToString(mouse_state),
   "\npressed=",pressed,", move=",move,(form!=NULL ? ", Interaction="+(class="type">class="kw">string)form.Interaction() : ""),
   "\npressed_chart=",pressed_chart,", pressed_form=",pressed_form,
   "\nform_index=",form_index,", graph_obj_id=",graph_obj_id
  );

「鼠标拖拽时图形对象的锁定与重绘」

在自定义面板的鼠标事件处理里,当 form 参数为 NULL 意味着光标不在控件表单上方,此时要根据 pressed 状态决定图表交互逻辑。若鼠标按下且 pressed_form 已为真,说明按钮还按在表单外区域,直接 return 避免重复触发。 若 pressed 为真但 pressed_chart 尚未置位,则把 pressed_chart 设 true、pressed_form 设 false、move 设 false,并调用 SetChartTools 开启图表工具——这一步是让拖拽时不移动控件本体。外汇与贵金属图表上做这类 GUI 交互时,MT5 主线程卡顿可能导致事件丢失,属于高风险环境下的细节坑。 鼠标松开(pressed 为假)时,取扩展标准图形对象列表 list_ext,用 Total() 拿到数量后循环,对每个 CGStdGraphObj 调用 RedrawControlPointForms(0, CTRL_POINT_COLOR) 去掉控制点和圆圈重绘。若光标在表单上且 pressed_chart 为真同样直接 return;仅当 !pressed_form 时才把 pressed_chart 置 false,交还表单自身的按压状态。

MQL5 / C++
if(form==NULL)
  {
   class=class="str">"cmt">//--- If the mouse button is pressed
   if(pressed)
     {
      class=class="str">"cmt">//--- If the button is still pressed and held on the form, exit
      if(pressed_form)
        {
         class="kw">return;
        }
      class=class="str">"cmt">//--- If the button hold flag is not enabled yet, set the flags and enable chart tools
      if(!pressed_chart)
        {
         pressed_chart=true;   class=class="str">"cmt">// Button is held on the chart
         pressed_form=class="kw">false;   class=class="str">"cmt">// Cursor is not above the form
         move=class="kw">false;           class=class="str">"cmt">// movement disabled
         this.SetChartTools(::ChartID(),true);
        }
     }
   class=class="str">"cmt">//--- If the mouse button is not pressed
   else
     {
      class=class="str">"cmt">//--- Get the list of extended standard graphical objects
      CArrayObj *list_ext=GetListStdGraphObjectExt();
      class=class="str">"cmt">//--- In the loop by all extended graphical objects,
      class="type">int total=list_ext.Total();
      for(class="type">int i=class="num">0;i<total;i++)
        {
         class=class="str">"cmt">//--- get the next graphical object
         CGStdGraphObj *obj=list_ext.At(i);
         if(obj==NULL)
           class="kw">continue;
         class=class="str">"cmt">//--- and redraw it without a point and a circle
         obj.RedrawControlPointForms(class="num">0,CTRL_POINT_COLOR);
        }
     }
  }
class=class="str">"cmt">//--- If the cursor is above the form
else
  {
   class=class="str">"cmt">//--- If the button is still pressed and held on the chart, exit
   if(pressed_chart)
     {
      class="kw">return;
     }

   class=class="str">"cmt">//--- If the flag of holding the button on the form is not set yet
   if(!pressed_form)
     {
      pressed_chart=class="kw">false;     class=class="str">"cmt">// The button is not pressed on the chart

悬停与按压下的图形锚点重绘

在自定义表单里处理鼠标状态时,先调用 this.SetChartTools(::ChartID(),false) 关掉图表自带工具,避免和自建交互逻辑抢事件。这一步在悬停未按键、按压、滚轮三种分支前都要做,否则 MT5 可能把点击判定给原生对象。 当 mouse_state 等于 MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED 且 graph_obj_id 大于 WRONG_VALUE 时,说明光标正落在某个扩展图形对象的控制点管理区上。通过 GetStdGraphObjectExt(graph_obj_id,form.ChartID()) 按 ID 拿对象指针,再取 GetExtToolkit() 拿到工具集,调用 DrawOneControlPoint(form) 就能只在本表单画一个圆圈锚点,同时清掉其他表单上的同类点。 进入 MOUSE_FORM_STATE_INSIDE_FORM_PRESSED 分支后,同样先关图表工具。若 pressed_form 还未置位,则把它设为 true、pressed_chart 设为 false,标记当前表单已进入拖拽持有态。滚轮分支 MOUSE_FORM_STATE_INSIDE_FORM_WHEEL 此处留空,可按需接入缩放或参数微调。 开 MT5 把这段塞进你的表单类,悬停扩展对象控制点时应只看到一个圆点;若多点同显,优先查 graph_obj_id 的 WRONG_VALUE 阈值是否和你的对象枚举冲突。外汇与贵金属品种波动剧烈,这类交互改的是显示层,不改变订单风险。

MQL5 / C++
this.SetChartTools(::ChartID(),class="kw">false);

class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the form, no mouse buttons are clicked&class="macro">#x27; event handler
if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED)
  {
   class=class="str">"cmt">//--- If the cursor is above the form for managing the pivot point of an extended graphical object,
   if(graph_obj_id>WRONG_VALUE)
     {
      class=class="str">"cmt">//--- get the object by its ID and by the chart ID
      CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID());
      if(graph_obj!=NULL)
        {
         class=class="str">"cmt">//--- Get the toolkit of an extended standard graphical object
         CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit();
         if(toolkit!=NULL)
           {
            class=class="str">"cmt">//--- Draw a point with a circle on the form and class="kw">delete it on all other forms
toolkit.DrawOneControlPoint(form);
           }
        }
     }
  }
class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the form, a mouse button is clicked(any)&class="macro">#x27; event handler
if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_PRESSED)
  {
   this.SetChartTools(::ChartID(),class="kw">false);
   class=class="str">"cmt">//--- If the flag of holding the form is not set yet
   if(!pressed_form)
     {
      pressed_form=true;      class=class="str">"cmt">// set the flag of pressing on the form
      pressed_chart=class="kw">false;    class=class="str">"cmt">// disable the flag of pressing on the form
     }
  }
class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the form, the mouse wheel is being scrolled&class="macro">#x27; event handler workpiece
if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_WHEEL)
  {

  }

◍ 悬停与按下时表单和图形锚点的状态切换

当鼠标光标处于表单活跃区且左右键均未按下时,系统先记录光标相对表单初始坐标的偏移量:X 偏移 = 鼠标 X 坐标减表单 X 坐标,Y 偏移同理。这一步决定了之后拖拽时表单跟随的基准位移。 若此时光标位于某个扩展图形对象(如斐波那契工具)的枢轴控制点活跃区上方,代码会通过 graph_obj_id > WRONG_VALUE 判定有效对象,再用 GetStdGraphObjectExt 按图表 ID 取出对象指针。取得到则继续拿到扩展工具箱 CGStdGraphObjExtToolkit,并调用 DrawOneControlPoint 仅在该表单画一个圆圈锚点,其他表单上的同锚点会被清除,避免多表单重复显示。 一旦鼠标在活跃区内按下且全局 move 标志尚未置位,pressed_form 设为 true 表示按钮正被按住。若检测到左键按下,move 被置为 true 进入移动模式——此后光标移动将驱动表单及关联图形对象实时位移。外汇与贵金属图表上这类自定义拖拽界面对点位精度要求高,实盘误触可能造成锚点跳变,属高风险操作,建议在模拟图表先验证。 下面这段 MQL5 片段即上述两种鼠标状态的分支处理,逐行看逻辑比读文档快:

MQL5 / C++
if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED)
  {
   class=class="str">"cmt">//--- Set the cursor shift relative to the form initial coordinates
   form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX());
   form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY());
   class=class="str">"cmt">//--- If the cursor is above the active area of the form for managing the pivot point of an extended graphical object,
   if(graph_obj_id>WRONG_VALUE)
     {
      class=class="str">"cmt">//--- get the object by its ID and by the chart ID
      CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID());
      if(graph_obj!=NULL)
        {
         class=class="str">"cmt">//--- Get the toolkit of an extended standard graphical object
         CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit();
         if(toolkit!=NULL)
           {
            class=class="str">"cmt">//--- Draw a point with a circle on the form and class="kw">delete it on all other forms
            toolkit.DrawOneControlPoint(form);
           }
        }
     }
  }
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
   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
     }
  }

「窗体拖拽与交互屏蔽的底层实现」

这段逻辑处理的是自定义 GUI 窗体被鼠标抓住时的状态切换。当检测到窗体处于可拖拽状态,先打开窗体与环境的交互开关,再把当前窗体提到所有界面的最上层,避免被其他面板挡住。 紧接着调用 ResetAllInteractionExeptOne(form),把其余所有窗体的交互标志清零,只保留当前这一个能响应鼠标。这样多窗体并存时,不会出两个面板同时抢光标的现象。 偏移量的记录方式很直接:用鼠标当前坐标减去窗体自身坐标,分别写进 X 和 Y 的 offset。后续 OnDrag 里只要持续用鼠标坐标减这个 offset,窗体就会跟手移动,误差通常在 1 像素内。 下面几个 mouse_state 分支(滚轮、滚动区按下/未按下)在原文里是空骨架,说明作者把复杂交互留到子类去填。你在 MT5 里接这段时,若只做拖拽,空分支可以保留不报错;要扩成缩放或内部滚动,就得在各分支补具体重绘逻辑。

MQL5 / C++
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
   this.ResetAllInteractionExeptOne(form);                                                           class=class="str">"cmt">// Reset interaction flags for all forms except the current one
   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
  }
    }
            class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the active area, the mouse wheel is being scrolled&class="macro">#x27; event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL)
              {
               
              }
            
            
            
            class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the window scrolling area, no mouse buttons are clicked&class="macro">#x27; event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED)
              {
               
              }
            class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the window scrolling area, a mouse button is clicked(any)&class="macro">#x27; event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED)
              {
               
              }
            class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the window scrolling area, the mouse wheel is being scrolled&class="macro">#x27; event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL)
              {
               
              }
         }
      }
   }
}

复合对象越界时的轴点偏移 bug

把前一篇的 EA 直接放进 \MQL5\Experts\TestDoEasy\Part99\ 命名为 TestDoEasyPart99.mq5 即可,EA 本体不用改一行,所有改动都在函数库类里完成。编译挂上图表后能看到:在复合图形对象窗体里拖动,只要不越过图表边界,相关限制都正常生效。 但一旦把轴点相对初始位置“反转”,也就是轴点挪出图表右、左、上或下边缘时,对象配置就失真了。根因在于轴点偏移是相对中心点算的——一个点取正偏移、另一个取负偏移,反转轴点位置就撞上了有限计算误差。 这个越界后轴点限制算错的问题,目前先记着,下一篇再修。外汇和贵金属图表里拖复合对象本就高频,MT5 上复现这套 Part99 EA 就能看到失真现象。

◍ 把工具请下神坛

这套图形函数库走到第九十八篇,压缩包 MQL5.zip 体量 4372.39 KB,里面塞了当前版函数库、测试 EA 和图表事件控制指标,直接拖进 MT5 就能跑。下一节会接着拆复合图形对象的创建与交互,但别等作者喂完才动手。 真正该做的,是把下载到的窗体对象鼠标事件处理代码翻出来,自己在 EURUSD 十五分钟图上改两个回调参数,看拖拽轴点的延迟到底卡在哪。外汇和贵金属杠杆高,图形再丝滑也只是辅助,信号错了该止损还是得止损。 作者留了评论入口,遇到标准图形扩展轴点移动失灵,直接把复现步骤贴回去,比自己在论坛瞎搜快得多。

常见问题

给控制点表单挂上程序化属性,并在控件中心点做排他逻辑,只让当前选中的点响应移动,其余从属对象保持锚定。
在图表缩放事件里刷新依赖对象的锚点坐标回填,让附属面板按从属坐标联动重新贴合控制点。
可以,把图形对象描述发给小布,它能按从属坐标刷新逻辑帮你核对控制点与附属面板的联动关系是否遗漏。
通常是没做依赖对象的锚点坐标回填隔离,需在每个复合对象内独立维护自己的控制点表单与刷新队列。
检查中心点绘制时是否用了排他逻辑下的局部坐标,改为基于控件中心绝对坐标重绘即可对齐。