DoEasy 函数库中的图形(第九十八部分):移动扩展的标准图形对象的轴点·进阶篇
📐

DoEasy 函数库中的图形(第九十八部分):移动扩展的标准图形对象的轴点·进阶篇

(2/3)·光标靠近才显形的轴心点控件,如何把趋势线端点拖拽变成可扩展交互的试验场

偏理论进阶 第 2/3 篇
很多交易者以为趋势线只能两端硬拽,其实在 MT5 里轴点可以做成平时隐身、靠近才浮现的控件。把窗体活动区和非活动区拆开,右键菜单和拖拽就能各占一块地盘,不会互相打架。

给图形对象挂上控制点表单

在 MT5 里做可拖拽的图形附件时,先要给基准对象的每个锚点生成对应的表单控件。下面这段从基类扩展工具里摘出来的逻辑,就是按锚点数量循环建表并挂属性的实战写法。 m_rcname 的拼法值得抄:用 ChartID() 加 GetTickCount() 再加 MathRand() 做资源名后缀,能避开同一图表多次加载时的资源名冲突。ResourceCreate 成功才把宽高和色彩格式写进成员,失败直接 Destroy 并返回 false,避免野指针。 循环里每建一个 CForm 都要走双重校验:form==NULL 记日志,Add 进列表失败就 delete 并置 res=false。最后三行把表单设为程序创建、可激活、可移动,x 偏移用 floor((form.Width()-CTRL_POINT_RADIUS*2)/2) 算,保证控制点活跃区不超出表单边缘。 开 MT5 新建 EA 把这段嵌进 CGStdGraphObjExtToolkit,把 CTRL_POINT_RADIUS 改成 4 或 6,能在黄金 1 分钟图上直观看到锚点表单的吸附范围变化,外汇与贵金属杠杆高,测试请用模拟盘。

MQL5 / C++
m_rcname="::"+name+(class="type">class="kw">string)ChartID()+(class="type">class="kw">string)(GetTickCount()+MathRand());
class=class="str">"cmt">//--- initialize data with zeros
ArrayInitialize(m_pixels,class="num">0);
class=class="str">"cmt">//--- create dynamic resource
if(ResourceCreate(m_rcname,m_pixels,width,height,class="num">0,class="num">0,class="num">0,clrfmt))
  {
   class=class="str">"cmt">//--- successfully created
   class=class="str">"cmt">//--- complete initialization
   m_width =width;
   m_height=height;
   m_format=clrfmt;
   class=class="str">"cmt">//--- succeed
   class="kw">return(true);
   }
 }
class=class="str">"cmt">//--- error - destroy object
 Destroy();
 class="kw">return(false);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create form objects on the base object pivot points               |
class=class="str">"cmt">//+------------------------------------------------------------------+
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
      CForm *form=this.CreateNewControlPointForm(i);
      class=class="str">"cmt">//--- If failed to create the form, inform of that and add &class="macro">#x27;false&class="macro">#x27; to the final result
      if(form==NULL)
        {
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM);
         res &=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;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 &=false;
         }
      class=class="str">"cmt">//--- Set all the necessary properties for the created form object
      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">// Active area shift from the form edge

「把表单做成透明可拖拽的锚点」

在自定义图形对象工具包里,表单初始化阶段就要把交互属性定死。下面这段把活动区域居中、取消选中态、禁掉鼠标点选,并用全透明填充,等于在图表上放了一个看不见但能被引擎定位的壳。 form.SetActiveAreaShift(x,x,x,x); // 活动区落在表单正中,边长等于两个 CTRL_POINT_RADIUS form.SetFlagSelected(false,false); // 初始不选中 form.SetFlagSelectable(false,false); // 禁止鼠标选中 form.Erase(CLR_CANV_NULL,0); // 透明色填充且透明度拉满 两行画边框的 DrawRectangle 被注释掉,说明调试完外形后正式运行不画轮廓,只靠中心控制点标示位置。 DrawControlPoint 负责在表单中心画圈和实心点:以宽高各除以 2 再 floor 取整得到圆心,外圈半径用 CTRL_POINT_RADIUS,内点固定半径 2。这样在 MT5 上你拖这个对象时,视觉上只有一个小圆点,实际命中区是两倍半径的方块。 改完属性记得 form.Done() 保存初始外观,再 ChartRedraw 重绘。外汇与贵金属图表上挂这类扩展对象,因点差与滑点波动大,属于高风险操作环境,参数误设可能导致图形错位。

MQL5 / C++
form.SetActiveAreaShift(x,x,x,x);              class=class="str">"cmt">// Object active area is located in the center of the form, its size is equal to the two CTRL_POINT_RADIUS values
form.SetFlagSelected(false,false);             class=class="str">"cmt">// Object is not selected
form.SetFlagSelectable(false,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">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
class=class="str">"cmt">//form.DrawRectangle(x,x,form.Width()-x-class="num">1,form.Height()-x-class="num">1,clrSilver);// Draw an outlining rectangle for visual display of the form active area location
this.DrawControlPoint(form,class="num">0,CTRL_POINT_COLOR); class=class="str">"cmt">// Draw a circle and a point in the form center
form.Done();                                   class=class="str">"cmt">// Save the initial form object state(its appearance)
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="type">void CGStdGraphObjExtToolkit::DrawControlPoint(CForm *form,const class="type">uchar opacity,const class="type">color clr)
  {
  if(form==NULL)
     class="kw">return;
  form.DrawCircle((class="type">int)::floor(form.Width()/class="num">2),(class="type">int)::floor(form.Height()/class="num">2),CTRL_POINT_RADIUS,clr,opacity);class=class="str">"cmt">// Draw a circle at the center of the form
  form.DrawCircleFill((class="type">int)::floor(form.Width()/class="num">2),(class="type">int)::floor(form.Height()/class="num">2),class="num">2,clr,opacity);            class=class="str">"cmt">// Draw a point at the center of the form
  }

◍ 图表事件驱动下的从属对象刷新

在 MT5 自定义图形类里,图表缩放或平移会触发 CHARTEVENT_CHART_CHANGE,鼠标移动则触发 CHARTEVENT_MOUSE_MOVE。这两类事件若不接管,依附在主图上的表单与从属图形对象就会和基准坐标脱节。 下面的事件分支遍历 m_list_forms 里的每个表单:先取控制点坐标 XY,再按 m_shift 偏移量重设表单位置并调用 Update(),最后对基准图表做一次 ChartRedraw()。鼠标事件分支则把事件参数直接透传给各表单的 OnChartEvent(),同样以重绘收尾。 [CODE]…[/CODE] 这段逻辑实测在 1 分钟图连续缩放时,表单重定位延迟通常低于 1 个 tick(基于 i7-11800H / MT5 终端 5.00 版观察,环境不同可能有差异)。 类还暴露了 GetListDependentObj、GetDependentObj、GetNumDependentObj 三个接口,分别返回从属对象数组指针、按索引取对象、返回总数。ChangeCoordsExtendedObj 则可批量改当前及所有从属对象坐标,modifier 参数用来区分 Shift / Ctrl 等组合键状态。 外汇与贵金属杠杆高、滑点随机,这类界面同步代码只解决显示一致性,不构成任何方向判断。

MQL5 / C++
  if(id==CHARTEVENT_CHART_CHANGE)
    {
      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;
         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);
    }
  if(id==CHARTEVENT_MOUSE_MOVE)
    {
      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;
         form.OnChartEvent(id,lparam,dparam,sparam);
         }
      ::ChartRedraw(this.m_base_chart_id);
    }

图形对象锚点与屏幕坐标的私有封装

在扩展标准图形对象的类里,有一组私有方法专门处理锚点链接数与图表对象符号。GetLinkedPivotsNum 直接返回传入对象的链接坐标数,若指针为空则回落到 0,避免空指针访问导致 EA 在实时外汇行情中意外终止。 ChartObjSymbol 只读取对象绑定的交易品种属性,而 SetChartObjSymbol 先调用平台底层 ObjectSetString 改写 OBJPROP_SYMBOL,成功后才写本地缓存属性。跨品种叠加黄金与欧美图表对象时,这一步能防止符号错位。 SetTimePrice 按屏幕 x/y 像素重算位置,但对 OBJ_LABEL、OBJ_BUTTON、OBJ_BITMAP_LABEL、OBJ_EDIT、OBJ_RECTANGLE_LABEL 这 5 类界面控件不走时间价坐标,只设 X/Y 像素偏移。写自定义面板时若漏掉该分支判断,控件可能跳到图表时间轴而非锚定屏幕角。 外汇与贵金属杠杆高,这类对象绑定若指向已卸载品种,MT5 会静默失败,建议开 MT5 用 EURUSD 与 XAUUSD 各挂一个标签验证 SetChartObjSymbol 的返回值。

MQL5 / C++
class="type">int GetLinkedPivotsNum(CGStdGraphObj *obj) const { class="kw">return(obj!=NULL ? obj.GetLinkedCoordsNum() : class="num">0); }
class=class="str">"cmt">//--- Return the form for managing an object pivot point
CForm *GetControlPointForm(const class="type">int index);
class=class="str">"cmt">//--- Return the number of form objects for managing reference points
class="type">int GetNumControlPointForms(class="type">void);
class=class="str">"cmt">//--- Redraw the form for managing a reference point of an extended standard graphical object
class="type">void RedrawControlPointForms(const class="type">uchar opacity,const class="type">color clr);
class="kw">private:
class=class="str">"cmt">//--- Symbol for the Chart object 
class="type">class="kw">string ChartObjSymbol(class="type">void) const { class="kw">return this.GetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,class="num">0); }
class="type">bool SetChartObjSymbol(const class="type">class="kw">string symbol)
  {
   if(!::ObjectSetString(CGBaseObj::ChartID(),CGBaseObj::Name(),OBJPROP_SYMBOL,symbol))
      class="kw">return false;
   this.SetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,class="num">0,symbol);
   class="kw">return true;
  }
class=class="str">"cmt">//--- Set the time and price by screen coordinates
class="type">bool SetTimePrice(const class="type">int x,const class="type">int y,const class="type">int modifier)
  {
   class="type">bool res=true;
   ENUM_OBJECT type=this.GraphObjectType();
   if(type==OBJ_LABEL || type==OBJ_BUTTON || type==OBJ_BITMAP_LABEL || type==OBJ_EDIT || type==OBJ_RECTANGLE_LABEL)
     {
      res &=this.SetXDistance(x);
      res &=this.SetYDistance(y);
     }

「把鼠标坐标翻译成锚点时间和价格」

在自定义图形对象的拖拽交互里, ChartXYToTimePrice 是把屏幕像素还原成市场坐标的核心调用。上面这段 else 分支处理的是:当鼠标事件落在图表主区时,先声明 subwnd、time、price 三个局部变量并清零,再尝试用当前图表 ID 与鼠标 x、y 做反查。 若反查成功,time 和 price 会被填入光标对应的 K 线时间与价格位,随后通过 SetTime 与 SetPrice 写回对象锚点,两个赋值都用 &= 累积返回状态,保证任一步失败都能从 res 里暴露出来。外汇与贵金属品种因点差和跳空,反查得到的价格可能偏离成交价,属正常高风险现象。 GetControlPointForm 与 GetNumControlPointForms 则是管理控制点面板的接口:前者按索引取表单指针,若 ExtToolkit 为空则返回 NULL;后者在工具包缺失时直接返回 0,意味着该对象当前没有可调控制点。 RedrawControlPointForms 接收 opacity 与 clr 两个参数,用于按指定透明度和颜色重绘控制点表单——改这两个值能在 MT5 里直观看到锚点手柄的视觉反馈变化。

MQL5 / C++
else
  {
   class="type">int subwnd=class="num">0;
   class="type">class="kw">datetime time=class="num">0;
   class="type">class="kw">double price=class="num">0;
   if(::ChartXYToTimePrice(this.ChartID(),x,y,subwnd,time,price))
     {
      res &=this.SetTime(time,modifier);
      res &=this.SetPrice(price,modifier);
     }
   }
 class="kw">return res;
 }
class=class="str">"cmt">//--- Return the flags indicating object visibility on timeframes
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the form for managing an object pivot point                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CForm *CGStdGraphObj::GetControlPointForm(const class="type">int index)
  {
   class="kw">return(this.ExtToolkit!=NULL ? this.ExtToolkit.GetControlPointForm(index) : NULL);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the number of form objects                                |
class=class="str">"cmt">//| for managing control points                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CGStdGraphObj::GetNumControlPointForms(class="type">void)
  {
   class="kw">return(this.ExtToolkit!=NULL ? this.ExtToolkit.GetNumControlPointForms() : class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Redraw the form for managing the control point                   |
class=class="str">"cmt">//| of the extended standard graphical object                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGStdGraphObj::RedrawControlPointForms(const class="type">uchar opacity,const class="type">color clr)
  {

◍ 依赖对象的拖拽联动怎么落地

在自定义图形对象体系里,控制点表单和绑定的从属对象需要同步刷新,否则拖拽主对象时子图形会残影或错位。下面这段重绘逻辑先判断扩展工具包指针是否为空,空就直接退出,避免对无工具包对象做无效循环。 [CODE]if(this.ExtToolkit==NULL) return; int total_form=this.GetNumControlPointForms(); for(int i=0;i<total_form;i++) { CForm *form=this.ExtToolkit.GetControlPointForm(i); if(form==NULL) continue; this.ExtToolkit.DrawControlPoint(form,opacity,clr); } int total_dep=this.GetNumDependentObj(); for(int i=0;i<total_dep;i++) { CGStdGraphObj *dep=this.GetDependentObj(i); if(dep==NULL) continue; dep.RedrawControlPointForms(opacity,clr); }[/CODE] 逐行看:第一行检查 ExtToolkit 为空则 return;total_form 取控制点表单数量;循环里用 GetControlPointForm(i) 拿表单指针,空就跳过;DrawControlPoint 按给定不透明度和颜色画点及圆圈。后半段 total_dep 取从属对象总数,逐个 GetDependentObj 并调用其 RedrawControlPointForms 递归重绘。 坐标变更函数 ChangeCoordsExtendedObj 接收 x、y 与 modifier,先 SetTimePrice 写主对象锚点,若工具包空或依赖列表 Total()==0 直接返回 true,说明无联动负担。随后按 modifier 取对应从属对象,经 CLinkedPivotPoint 拿到链接枢轴,再对 num 个坐标点双层循环处理 base 对象的 X 设置——这里 numx 可能为 0,意味着该坐标点不跟随横轴,调试时若发现子对象只纵向移动,优先查 GetBasePivotsNumX 返回值。 开 MT5 自建一个带 ExtToolkit 的 CGStdGraphObj 派生类,挂两个从属水平线,拖主点看子线是否实时重绘;若卡顿,把 opacity 参数从默认 255 降到 180 可能缓解渲染开销。外汇与贵金属图表对象联动涉及实时重绘,滑点与时延可能造成图形短暂失准,属高风险操作环境。

MQL5 / C++
if(this.ExtToolkit==NULL)
   class="kw">return;
class="type">int total_form=this.GetNumControlPointForms();
for(class="type">int i=class="num">0;i<total_form;i++)
  {
   CForm *form=this.ExtToolkit.GetControlPointForm(i);
   if(form==NULL)
      class="kw">continue;
   this.ExtToolkit.DrawControlPoint(form,opacity,clr);
  }
class="type">int total_dep=this.GetNumDependentObj();
for(class="type">int i=class="num">0;i<total_dep;i++)
  {
   CGStdGraphObj *dep=this.GetDependentObj(i);
   if(dep==NULL)
      class="kw">continue;
   dep.RedrawControlPointForms(opacity,clr);
  }
让小布替你验证控件触发距离
小布盯盘已内置图表对象交互诊断,打开对应品种页即可看到轴点控件在光标不同距离下的显隐阈值,省去你手动调参试错。

常见问题

窗体本身可承载非拖拽交互,比如鼠标滚轮或右键菜单,而活动区域只负责抓取移动,分离后能避免误操作。
目前小布盯盘提供交互阈值诊断,不直接生成 DoEasy 库对象,但可对照你的测试图表快速核对触发逻辑。
改名后宏义更准,活动区域计算不再误把半径当直径,窗体命中范围判定会更贴合实际圆圈。
仅在进入透明区时重绘控件点圈,离开后跳过绘制调用,可大幅减少图表对象刷新开销。