DoEasy 函数库中的图形(第九十九部分):依据单个控制点移动扩展图形对象·进阶篇
🖱️

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

(2/3)· 标准对象能中心拖拽整体走,扩展图形却卡在轴点?本篇补齐单点管控的移动缺口

实战向进阶 第 2/3 篇
扩展图形对象常有两个轴点调端点,却缺一个能整体平移的中心柄,手动拖会扭形。给复合对象加单控制点,移动它就移动全部,逻辑比想象中轻。注意外汇贵金属图表波动大,这类对象变形会误导判读,属高风险操作环境。

◍ 复合图形对象的基准锚点结构

在 MT5 里做可拖拽的复合图形对象,第一步是把基准对象的参考点先存下来。下面这组类成员变量就是干这个的:时间数组和价格数组各管一组锚点,X/Y 坐标记录基准对象在屏幕上的落点,m_ctrl_form_size 决定控制表单的像素尺寸,m_shift 用来微调表单位置,m_list_forms 则是一个对象数组容器,装所有管理参考点的表单。 对外接口上,CreateNewControlPointForm 按索引在基准点生成控制表单;GetControlPointCoordXY 取指定参考点的屏幕 X/Y 坐标(返回 bool 表示取到没);SetControlFormParams 给表单填参数。公开方法里 SetBaseObj 一口气把对象类型、名字、图表 ID、子窗口、锚点数、表单尺寸、坐标以及 time/price 数组全设进去,之后用 SetBaseObjTime / SetBaseObjPrice 按索引单独改某个锚点的时间或价格。 实盘里这类结构常见于自画线工具:比如你画一条趋势线,两个端点就是 base_time[2] 和 base_price[2],拖端点时只调其中一个索引的值,图形和表单位置跟着刷新。外汇和贵金属波动快,锚点坐标用屏幕像素存,遇图表缩放要重算,否则表单会漂。

MQL5 / C++
class="type">class="kw">datetime m_base_time[]; class=class="str">"cmt">// Time array of base object reference points
class="type">class="kw">double m_base_price[]; class=class="str">"cmt">// Price array of base object reference points
class="type">int m_base_x; class=class="str">"cmt">// Base object X coordinate
class="type">int m_base_y; class=class="str">"cmt">// Base object Y coordinate
class="type">int m_ctrl_form_size; class=class="str">"cmt">// Size of forms for managing reference points
class="type">int m_shift; class=class="str">"cmt">// Shift coordinates for adjusting the form location
CArrayObj m_list_forms; class=class="str">"cmt">// List of form objects for managing reference points
class=class="str">"cmt">//--- Create a form object on a base object reference point
CFormControl *CreateNewControlPointForm(class="kw">const class="type">int index);
class=class="str">"cmt">//--- Return X and Y(class="num">1) screen coordinates of the specified reference point of the graphical object
class="type">bool GetControlPointCoordXY(class="kw">const class="type">int index,class="type">int &x,class="type">int &y);
class=class="str">"cmt">//--- Set the parameters of a form object for managing pivot points
class="type">void SetControlFormParams(CFormControl *form,class="kw">const class="type">int index);
class="kw">public:
class=class="str">"cmt">//--- Set the parameters of the base object of a composite graphical object
class="type">void SetBaseObj(class="kw">const ENUM_OBJECT base_type,class="kw">const class="type">class="kw">string base_name,
                class="kw">const class="type">long base_chart_id,class="kw">const class="type">int base_subwindow,
                class="kw">const class="type">int base_pivots,class="kw">const class="type">int ctrl_form_size,
                class="kw">const class="type">int base_x,class="kw">const class="type">int base_y,
                class="kw">const class="type">class="kw">datetime &base_time[],class="kw">const class="type">class="kw">double &base_price[]);
class=class="str">"cmt">//--- Set the base object(class="num">1) time, (class="num">2) price, (class="num">3) time and price coordinates
class="type">void SetBaseObjTime(class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">int index);
class="type">void SetBaseObjPrice(class="kw">const class="type">class="kw">double price,class="kw">const class="type">int index);

锚点表单的存取与创建接口

在 MT5 自定义控件里,枢轴点(pivot)管理依赖一组明确的 setter / getter。基类把基准对象的屏幕坐标拆成 X、Y 独立写入,m_base_x 与 m_base_y 直接赋值,不走中间计算,方便在 OnChartEvent 里随鼠标拖拽实时刷新。 SetBaseObjTimePrice() 接收 datetime、double、int 三参,把基准对象绑到具体 K 线时间与价格;SetControlFormSize() 则决定每个控制点外框的像素尺寸,GetControlFormSize() 原样回读 m_ctrl_form_size,默认未显式赋值时该成员为 0,需调用方先设。 取表单指针有两条路:按索引 GetControlPointForm(int) 直接走 m_list_forms.At(index),按名称的版本多带一个 int &index 出参,便于反向定位。GetNumPivotsBaseObj() 回 m_base_pivots,GetNumControlPointForms() 回 m_list_forms.Total(),两者差就是尚未生成表单的裸枢轴数。 真正把基准枢轴变成可交互控件靠 CreateAllControlPointForm(),返回 bool 暴露创建成败;DrawControlPoint() 接收 CFormControl*、opacity(uchar 0–255)、color,单表单绘制或跨表单排他绘制由重载区分。外汇与贵金属图表上挂这类对象属高风险操作,坐标错乱可能误导下单,建议先在模拟盘验证。

MQL5 / C++
class="type">void SetBaseObjTimePrice(class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price,class="kw">const class="type">int index);
class=class="str">"cmt">//--- Set the base object(class="num">1) X, (class="num">2) Y, (class="num">3) X and Y screen coordinates
class="type">void SetBaseObjCoordX(class="kw">const class="type">int value) { this.m_base_x=value; }
class="type">void SetBaseObjCoordY(class="kw">const class="type">int value) { this.m_base_y=value; }
class="type">void SetBaseObjCoordXY(class="kw">const class="type">int value_x,class="kw">const class="type">int value_y) { this.m_base_x=value_x; this.m_base_y=value_y; }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the size of the form of pivot point management control points
class="type">void SetControlFormSize(class="kw">const class="type">int size);
class="type">int GetControlFormSize(class="type">void) class="kw">const { class="kw">return this.m_ctrl_form_size; }
class=class="str">"cmt">//--- Return the pointer to the pivot point form by(class="num">1) index and(class="num">2) name
CFormControl *GetControlPointForm(class="kw">const class="type">int index) { class="kw">return this.m_list_forms.At(index); }
CFormControl *GetControlPointForm(class="kw">const class="type">class="kw">string name,class="type">int &index);
class=class="str">"cmt">//--- Return the number of(class="num">1) base object pivot points and(class="num">2) newly created form objects for managing control points
class="type">int GetNumPivotsBaseObj(class="type">void) class="kw">const { class="kw">return this.m_base_pivots; }
class="type">int GetNumControlPointForms(class="type">void) class="kw">const { class="kw">return this.m_list_forms.Total(); }
class=class="str">"cmt">//--- Create form objects on the base object pivot points
class="type">bool CreateAllControlPointForm(class="type">void);
class=class="str">"cmt">//--- (class="num">1) Draw a control point on the form, (class="num">2) draw a control point on the form and class="kw">delete it on all other forms
class="type">void DrawControlPoint(CFormControl *form,class="kw">const class="type">uchar opacity,class="kw">const class="type">class="kw">color clr);

「控制点绘制与构造器的内部调用链」

在扩展图形工具类里,控制点的显隐靠两个轻量包装函数完成:DrawControlPoint 用默认不透明度 255 和 CTRL_POINT_COLOR 上色,ClearControlPoint 则把不透明度传 0 实现“擦除”,两者都转调同一个底层 DrawOneControlPoint。 构造器 CGStdGraphObjExtToolkit 接收 base_type、base_name、base_chart_id 等 11 个参数,先 m_list_forms.Clear() 清空表单链表,再 SetBaseObj 写入基准对象属性,最后 CreateAllControlPointForm 批量生成控制点表单。 想验证这套逻辑,直接在 MT5 里 new 一个派生类实例,传一组 5 个元素的 base_time[] 与 base_price[],观察图表是否按 ctrl_form_size 弹出对应数量的控制点;外汇与贵金属图表对象受 tick 驱动,高频行情下控制点重绘可能有轻微延迟,属正常现象。

MQL5 / C++
class="type">void 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);
class=class="str">"cmt">//--- (class="num">1) Draw class="kw">using a class="kw">default class="type">class="kw">color, (remove) a control point on the form
class="type">void DrawControlPoint(CFormControl *form) { this.DrawControlPoint(form,class="num">255,CTRL_POINT_COLOR);}
class="type">void ClearControlPoint(CFormControl *form){ this.DrawControlPoint(form,class="num">0,CTRL_POINT_COLOR);  }
class=class="str">"cmt">//--- Remove all form objects from the list
class="type">void DeleteAllControlPointForm(class="type">void);

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=class="str">"cmt">//--- Constructor/destructor
CGStdGraphObjExtToolkit(class="kw">const ENUM_OBJECT base_type,class="kw">const class="type">class="kw">string base_name,
                        class="kw">const class="type">long base_chart_id,class="kw">const class="type">int base_subwindow,
                        class="kw">const class="type">int base_pivots,class="kw">const class="type">int ctrl_form_size,
                        class="kw">const class="type">int base_x,class="kw">const class="type">int base_y,
                        class="kw">const class="type">class="kw">datetime &base_time[],class="kw">const class="type">class="kw">double &base_price[])
   {
    this.m_list_forms.Clear();
    this.SetBaseObj(base_type,base_name,base_chart_id,base_subwindow,base_pivots,ctrl_form_size,base_x,base_y,base_time,base_price);
    this.CreateAllControlPointForm();
   }
CGStdGraphObjExtToolkit(){;}
~CGStdGraphObjExtToolkit(){;}
};

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

在 MT5 自定义指标或 EA 里,经常需要拿到画线、标签等图形对象的屏幕像素坐标,用来做鼠标碰撞检测或自绘面板对齐。下面这段方法封装了按对象基类分流取坐标的逻辑,核心是把用屏幕坐标直接画的对象(标签、按钮、位图标签等)直接回写 m_base_x / m_base_y,其余类型先 break 留待后续处理。 调用前先把入参 x、y 置 0,失败时就凭这两个零值判断,避免野坐标。OBJ_LABEL、OBJ_BUTTON、OBJ_BITMAP_LABEL、OBJ_EDIT、OBJ_RECTANGLE_LABEL、OBJ_CHART、OBJ_EVENT 共 7 类对象在 switch 里直接返回 true 并写出坐标,其余趋势线、通道、甘氏、斐波那契类只 break 不赋值。 实盘写外汇或贵金属面板时留意:这类对象坐标抓取依赖图表窗口尺寸,换屏分辨率或缩放比例后像素值会变,属高风险界面的细微坑点,建议每次 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           :
      case OBJ_EVENT           :
         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">//--- Lines(vertical and horizontal)
      case OBJ_VLINE           : class="kw">break;
      case OBJ_HLINE           : class="kw">break;
      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         :

斐波那契通道与扩展对象的控制点坐标算法

在 MT5 自定义图形类的坐标计算分支里,OBJ_FIBOCHANNEL 与 OBJ_EXPANSION 共用同一段逻辑:当控制点索引小于基准枢轴数时,直接调用 ChartTimePriceToXY 把时间价格转成屏幕 XY,成功返回 true,否则 false。 若索引落在基准枢轴之后,说明要计算两条枢轴线之间的中心形态坐标。代码先取 0 号和 1 号控制点形态,任一为 NULL 就返回 false;否则 x、y 各取两个形态坐标加偏移 m_shift 后的平均值,即 (form0.X + m_shift + form1.X + m_shift) / 2。 其余对象如 OBJ_PITCHFORK、OBJ_GANNFAN、OBJ_ELLIOTWAVE5、各类箭头与形状在该分支均只写 break,意味着这些类型暂不参与这套坐标推导。实盘加载此类 EA 时,若发现江恩或 Elliott 线控件错位,大概率就是分支未实现而非参数错配。外汇与贵金属波动剧烈,此类图形辅助仅作概率参考,请先在策略测试器验证再上真实账户。

MQL5 / C++
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
  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;

「按名字找回图形的控制点表单」

在自定义图形工具包里,按名称定位枢轴表单靠的是 GetControlPointForm。它先置 index 为 WRONG_VALUE,再遍历 m_list_forms,命中 Name() 一致就回传指针并写回下标,否则返回 NULL。 CreateNewControlPointForm 则负责在基准对象的参考点上造新表单:名字拼成 base_name+"_CP_"+序号(超出枢轴数标 X),若同名表单已存在直接返 NULL。坐标通过 GetControlPointCoordXY 拿到后,向左上偏移 m_shift 像素生成 CFormControl。 [CODE] CFormControl *CGStdGraphObjExtToolkit::GetControlPointForm(const string name,int &index) { index=WRONG_VALUE; for(int i=0;i<this.m_list_forms.Total();i++) { CFormControl *form=this.m_list_forms.At(i); if(form==NULL) continue; if(form.Name()==name) { index=i; return form; } } return NULL; } [/CODE] 这段逻辑在 MT5 里可直接抄进 EA 的图形编辑模块;把 base_name 换成你标的趋势线前缀,就能用名称反查第几个锚点被拖动了。外汇与贵金属图形对象受报价跳空影响,枢轴坐标可能瞬时失效,属高风险操作。 [CODE] CFormControl *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const int index) { string name=this.m_base_name+"_CP_"+(index<this.m_base_pivots ? (string)index : "X"); CFormControl *form=this.GetControlPointForm(index); if(form!=NULL) return NULL; int x=0, y=0; if(!this.GetControlPointCoordXY(index,x,y)) return NULL; form=new CFormControl(this.m_base_chart_id,this.m_base_subwindow,name,index,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize()); if(form!=NULL) this.SetControlFormParams(form,index); return form; } [/CODE] m_shift 默认取控件半宽较稳,设 0 会让表单压在锚点正上,鼠标点不中。实盘前在策略测试器拖几根线验证表单跟随即可。

MQL5 / C++
CFormControl *CGStdGraphObjExtToolkit::GetControlPointForm(class="kw">const class="type">class="kw">string name,class="type">int &index)
  {
   index=WRONG_VALUE;
   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;
      if(form.Name()==name)
        {
         index=i;
         class="kw">return form;
        }
     }
   class="kw">return NULL;
  }

CFormControl *CGStdGraphObjExtToolkit::CreateNewControlPointForm(class="kw">const class="type">int index)
  {
   class="type">class="kw">string name=this.m_base_name+"_CP_"+(index<this.m_base_pivots ? (class="type">class="kw">string)index : "X");
   CFormControl *form=this.GetControlPointForm(index);
   if(form!=NULL)
      class="kw">return NULL;
   class="type">int x=class="num">0, y=class="num">0;
   if(!this.GetControlPointCoordXY(index,x,y))
      class="kw">return NULL;
   form=new CFormControl(this.m_base_chart_id,this.m_base_subwindow,name,index,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize());
   if(form!=NULL)
      this.SetControlFormParams(form,index);
   class="kw">return form;
  }
把坐标越界判断交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到对象是否贴近屏幕边框而可能失真,你专注决策而非算像素。

常见问题

ChartTimePriceToXY 只返回可视部分坐标,越界请求得 0,导致枢轴点粘在边框像素,图形被拉歪。本篇用限制位移解决。
窗体不在图形对象集合内,加类型后可用统一接口识别活动对象,方便后续集合遍历与事件分发。
可以,小布盯盘的 AIGC 模块会标注对象是否触边变形,省去你每次手动核对屏幕坐标。
把屏幕坐标计算拆成独立方法,主逻辑变短,读调用比读大块内联容易,后续维护三点以上控件也清晰。
本篇聚焦两点加中心的结构,三控制点以上的窗体在同系列下一篇展开,此处先打基础。