DoEasy 函数库中的图形(第九十五部分):复合图形对象控件·进阶篇
🧩

DoEasy 函数库中的图形(第九十五部分):复合图形对象控件·进阶篇

(2/3)·标准对象拼成单一复合体后,怎样让轴点窗体在重定位时自动跟随而不脱节

偏理论 第 2/3 篇

把多个标准图形对象拼成复合体只是第一步,多数人卡在重定位时子对象坐标漂移、附着触发无反馈。本篇先不急着连对象,而是把轴点窗体做出来,让像素级窗体在时间/价格坐标变动下仍黏住基准点。

◍ 复合图形对象的基类接口怎么搭

在 MT5 里做可拖拽的复合图形(比如带控制点的通道线),第一步是把基对象和它的参考点管理表单绑起来。下面这段类声明给出了最小可用的接口骨架:用 CArrayObj 存表单对象,用 CreateNewControlPointForm 按索引生成控制点 UI,再用 GetControlPointCoordXY 把图形对象的锚点换算成屏幕 X/Y。 基类参数通过 SetBaseObj 一次性灌入——对象类型、名字、图表 ID、子窗口、锚点数量、表单尺寸、初始坐标,以及 time[]/price[] 两个数组。注意 base_pivots 直接决定后面要建几个控制点表单,设错会导致坐标读取越界。 屏幕坐标的 setter 是全内联的:SetBaseObjCoordX 只改 m_base_x,SetBaseObjCoordXY 同时写 X/Y。实盘里若用鼠标拖基对象,每帧调一次 setter 就能让复合层跟随,但高频重绘要留意 CPU 占用。外汇与贵金属杠杆高,这类自定义对象仅作辅助判势,信号本身不预示单边行情。

MQL5 / C++
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
CForm *CreateNewControlPointForm(const class="type">int index);
class=class="str">"cmt">//--- Return X and Y screen coordinates of the specified reference point of the graphical object
class="type">bool GetControlPointCoordXY(const class="type">int index,class="type">int &x,class="type">int &y);
class="kw">public:
class="kw">public:
class=class="str">"cmt">//--- Set the parameters of the base object of a composite graphical object
class="type">void SetBaseObj(const ENUM_OBJECT base_type,const class="type">class="kw">string base_name,
const class="type">long base_chart_id,const class="type">int base_subwindow,
const class="type">int base_pivots,const class="type">int ctrl_form_size,
const class="type">int base_x,const class="type">int base_y,
const class="type">class="kw">datetime &base_time[],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(const class="type">class="kw">datetime time,const class="type">int index);
class="type">void SetBaseObjPrice(const class="type">class="kw">double price,const class="type">int index);
class="type">void SetBaseObjTimePrice(const class="type">class="kw">datetime time,const class="type">class="kw">double price,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(const class="type">int value) { this.m_base_x=value; }
class="type">void SetBaseObjCoordY(const class="type">int value) { this.m_base_y=value; }
class="type">void SetBaseObjCoordXY(const class="type">int value_x,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

控件尺寸与枢轴表单的存取接口

在扩展图形对象工具类里,控件表单的尺寸由 m_ctrl_form_size 成员记录,外部通过 SetControlFormSize(int size) 写入、GetControlFormSize() 只读返回,调参时直接改这个 int 就能整体缩放拖拽手柄。 枢轴点表单存放在 m_list_forms 指针列表,GetControlPointForm(int index) 按序号取指针,GetControlPointForm(string name,int &index) 则按名称查并回写序号;m_base_pivots 存基础对象的枢轴总数,GetNumPivotsBaseObj() 暴露该值供循环边界判断。 CreateAllControlPointForm() 负责在基础对象每个枢轴坐标上生成 CForm 控件,DeleteAllControlPointForm() 清空列表释放资源;图表事件统一进 OnChartEvent(int id,long& lparam,double& dparam,string& sparam) 分发。 构造函数收齐基础对象类型、名称、图表 ID、子窗口、枢轴数、控件尺寸与坐标数组,初始化块里先执行 this.m_list_forms.Clear() 保证列表空态起步,避免残留表单引发下标越界。

MQL5 / C++
class="type">void SetControlFormSize(const class="type">int size);
class="type">int GetControlFormSize(class="type">void) 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
CForm *GetControlPointForm(const class="type">int index) { class="kw">return this.m_list_forms.At(index); }
CForm *GetControlPointForm(const class="type">class="kw">string name,class="type">int &index);
class=class="str">"cmt">//--- Return the number of the base object pivot points
class="type">int GetNumPivotsBaseObj(class="type">void) const { class="kw">return this.m_base_pivots; }
class=class="str">"cmt">//--- Create form objects on the base object pivot points
class="type">bool CreateAllControlPointForm(class="type">void);
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(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam);
class=class="str">"cmt">//--- Constructor/destructor
CGStdGraphObjExtToolkit(const ENUM_OBJECT base_type,const class="type">class="kw">string base_name,
                        const class="type">long base_chart_id,const class="type">int base_subwindow,
                        const class="type">int base_pivots,const class="type">int ctrl_form_size,
                        const class="type">int base_x,const class="type">int base_y,
                        const class="type">class="kw">datetime &base_time[],const class="type">class="kw">double &base_price[])
   {
   this.m_list_forms.Clear();

「给复合图形对象打底座」

在 MT5 里做可拖拽的复合图形工具,第一步是把基底对象(base object)的参数一次性灌进类内部。下面这段 SetBaseObj 就是干这个的:它接收对象类型、名称、图表 ID、子窗口、锚点数、控件尺寸以及时间和价格数组,逐一对成员赋值。 注意 m_base_pivots 决定基底对象有几个参考点(比如趋势线 2 个、通道 3 个),而 ctrl_form_size 通过 SetControlFormSize 控制每个参考点旁边管理表单的像素尺寸,直接影响你鼠标点选时的热区大小。 代码里有一段类型白名单判断:只有 OBJ_LABELOBJ_BUTTONOBJ_BITMAP_LABELOBJ_EDIT 等少数几种能当基底。若你传个 OBJ_TREND 进去,后续 CreateAllControlPointForm 生成控制点表单时可能直接漏掉坐标绑定,拖不动。 开 MT5 验证时,可先建一个 OBJ_BUTTON 基底,把 ctrl_form_size 设成 12 对比设成 20,看控制点表单在小屏上的拥挤程度差异。外汇与贵金属图表波动大,这类自定义工具在高杠杆品种上误触概率会偏高,务必先在模拟盘跑通。

MQL5 / C++
class="type">void CGStdGraphObjExtToolkit::SetBaseObj(const ENUM_OBJECT base_type,const class="type">class="kw">string base_name,
                     const class="type">long base_chart_id,const class="type">int base_subwindow,
                     const class="type">int base_pivots,const class="type">int ctrl_form_size,
                     const class="type">int base_x,const class="type">int base_y,
                     const class="type">class="kw">datetime &base_time[],const class="type">class="kw">double &base_price[])
  {
   this.m_base_chart_id=base_chart_id;      class=class="str">"cmt">// Base graphical object chart ID
   this.m_base_subwindow=base_subwindow;    class=class="str">"cmt">// Base graphical object chart subwindow
   this.m_base_type=base_type;              class=class="str">"cmt">// Base object type
   this.m_base_name=base_name;              class=class="str">"cmt">// Base object name
   this.m_base_pivots=base_pivots;          class=class="str">"cmt">// Number of base object reference points
   this.m_base_x=base_x;                    class=class="str">"cmt">// Base object X coordinate
   this.m_base_y=base_y;                    class=class="str">"cmt">// Base object Y coordinate
   this.SetControlFormSize(ctrl_form_size); class=class="str">"cmt">// Size of forms for managing reference points

   if(this.m_base_type==OBJ_LABEL       || this.m_base_type==OBJ_BUTTON  ||
      this.m_base_type==OBJ_BITMAP_LABEL || this.m_base_type==OBJ_EDIT    ||

◍ 控制点尺寸与基准数组的边界处理

在扩展图形对象工具里,基准枢轴点的时间和价格数组必须先做空校验,否则后续 Resize 会直接写越界。代码对 base_time 和 base_price 都调了 ArraySize,若为 0 就走 CMessage::ToLog 打错误码并返回,这一步能避免 MT5 图表上出现隐形对象。 ArrayResize 的返回值要和 m_base_pivots 严格相等才继续;不相等说明内存分配失败,同样记日志返回。之后用 for 循环把外部传入的 base_time[i]、base_price[i] 逐点拷进成员数组,循环上限就是 m_base_pivots,索引不会溢出。 SetControlFormSize 里有个容易忽略的钳制逻辑:传入 size 大于 254 时强制成 255,小于 5 时强制成 5,偶数则加 1 变奇数。这么做是为了让控制框始终保持奇数边长,m_shift 用 ceil(m_ctrl_form_size/2)+1 算出中心偏移,鼠标拾取枢轴时才不会偏半像素。 SetBaseObjTime 对 index 做了越界守门:index 大于 m_base_pivots-1 就报 MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY 并 return。想验证的话,在 EA 里把 index 故意传超,观察专家日志是否抛出该错误码即可。

MQL5 / C++
  if(::ArraySize(base_time)==class="num">0)
    {
      CMessage::ToLog(DFUN+"base_time: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY);
      class="kw">return;
    }
  if(::ArraySize(base_price)==class="num">0)
    {
      CMessage::ToLog(DFUN+"base_price: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY);
      class="kw">return;
    }
  if(::ArrayResize(this.m_base_time,this.m_base_pivots)!=this.m_base_pivots)
    {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA);
      class="kw">return;
    }
  if(::ArrayResize(this.m_base_price,this.m_base_pivots)!=this.m_base_pivots)
    {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA);
      class="kw">return;
    }
  for(class="type">int i=class="num">0;i<this.m_base_pivots;i++)
    {
      this.m_base_time[i]=base_time[i];      class=class="str">"cmt">// Time(i) of the base object pivot point
      this.m_base_price[i]=base_price[i];    class=class="str">"cmt">// Price(i) of the base object pivot point
    }
}
class="type">void CGStdGraphObjExtToolkit::SetControlFormSize(const class="type">int size)
  {
   this.m_ctrl_form_size=(size>class="num">254 ? class="num">255 : size<class="num">5 ? class="num">5 : size%class="num">2==class="num">0 ? size+class="num">1 : size);
   this.m_shift=(class="type">int)ceil(m_ctrl_form_size/class="num">2)+class="num">1;
  }
class="type">void CGStdGraphObjExtToolkit::SetBaseObjTime(const class="type">class="kw">datetime time,const class="type">int index)
  {
   if(index>this.m_base_pivots-class="num">1)
    {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      class="kw">return;
    }
   this.m_base_time[index]=time;
  }

图形对象基准坐标的写入与屏幕映射

扩展工具类里对基准点的写入做了硬边界:SetBaseObjPrice 与 SetBaseObjTimePrice 都先判断 index 是否超过 m_base_pivots-1,越界就写日志并返回,不会让数组越界把 EA 拖崩。 这两个方法分别只写价格、或同时写时间与价格到 m_base_time[index] 和 m_base_price[index],是后续所有屏幕坐标换算的源头数据。 GetControlPointCoordXY 按对象类型分流:LABEL、BUTTON、BITMAP_LABEL、EDIT、RECTANGLE_LABEL、CHART 这类无时间轴绑定的对象,直接返回 m_base_x / m_base_y;其余类型走 ChartTimePriceToXY,把第 index 个基准点的时间价格转成屏幕 x/y,失败则置 0 并返回 false。 在 MT5 里接这套逻辑时,先确认 m_base_pivots 已正确初始化(例如设为 3 表示支持 3 个枢轴点),否则 index=2 的调用会静默丢弃。外汇与贵金属图表上做对象锚定属于高风险操作,坐标算错可能让提示框漂到看不见的位置。

MQL5 / C++
class="type">void CGStdGraphObjExtToolkit::SetBaseObjPrice(const class="type">class="kw">double price,const class="type">int index)
  {
   if(index>this.m_base_pivots-class="num">1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      class="kw">return;
     }
   this.m_base_price[index]=price;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the time and price coordinates of the base object              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGStdGraphObjExtToolkit::SetBaseObjTimePrice(const class="type">class="kw">datetime time,const class="type">class="kw">double price,const class="type">int index)
  {
   if(index>this.m_base_pivots-class="num">1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      class="kw">return;
     }
   this.m_base_time[index]=time;
   this.m_base_price[index]=price;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the X and Y coordinates of the specified pivot point         |
class=class="str">"cmt">//| of the graphical object in screen coordinates                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CGStdGraphObjExtToolkit::GetControlPointCoordXY(const class="type">int index,class="type">int &x,class="type">int &y)
  {
   class="kw">switch(this.m_base_type)
     {
      case OBJ_LABEL            :
      case OBJ_BUTTON           :
      case OBJ_BITMAP_LABEL     :
      case OBJ_EDIT             :
      case OBJ_RECTANGLE_LABEL  :
      case OBJ_CHART            :
       x=this.m_base_x;
       y=this.m_base_y;
       break;
      class="kw">default:
       if(!::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y))
         {
          x=class="num">0;
          y=class="num">0;
          class="kw">return false;
         }
     }

「控件表单的检索与创建逻辑」

在图形对象扩展工具包里,按名称取回枢轴点表单指针靠的是 GetControlPointForm:先置 index 为 WRONG_VALUE,再遍历 m_list_forms,命中名称就回写下标并返回表单,否则返回 NULL。 新建控件表单时,CreateNewControlPointForm 会拼出类似 "基名_TKPP_0" 的 name;若同名下表单已存在则直接返 NULL,避免重复挂载。坐标由 GetControlPointCoordXY 取出后,向左上各偏移 m_shift 像素生成 CForm。 批量建表走 CreateAllControlPointForm,循环 m_base_pivots 次,每次调单点创建;任一表单为 NULL 就写日志并把最终结果用 &=false 折叠。外汇与贵金属图表物件交互高风险,参数 m_shift 和 m_base_pivots 建议先在模拟盘验证。

MQL5 / C++
CForm *CGStdGraphObjExtToolkit::GetControlPointForm(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++)
     {
      CForm *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;
  }
CForm *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const class="type">int index)
  {
   class="type">class="kw">string name=this.m_base_name+"_TKPP_"+(index<this.m_base_pivots ? (class="type">class="kw">string)index : "X");
   CForm *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;
   class="kw">return new CForm(this.m_base_chart_id,this.m_base_subwindow,name,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize());
  }
class="type">bool CGStdGraphObjExtToolkit::CreateAllControlPointForm(class="type">void)
  {
   class="type">bool res=true;
   for(class="type">int i=class="num">0;i<this.m_base_pivots;i++)
     {
      CForm *form=this.CreateNewControlPointForm(i);
      if(form==NULL)
        {
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM);
         res &=false;
        }
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到复合对象的轴点窗体高亮与附着距离提示,把重复劳动交给小布,你专注决策。

常见问题

复合图形由多个标准对象组合成单一逻辑体,父对象变更时子对象需保持相对位置并自动调整,普通组合则各自独立。
可以,小布盯盘对应品种页内置了轴点窗体高亮与附着距离可视化,省去手动调试矩形边缘的麻烦。
因为大多数图形对象以时间/价格值显示,而窗体需响应鼠标悬停与拖拽,用像素才能精准捕捉光标活动区域。
准备工作未完成,先实现窗体附加与图表变更时的移动机制,为后续拖拽附着打好基础。
它标记图形对象定位点,无需单击高亮即可移动修改扩展对象,拖动时附属复合体同步变更。