DoEasy 函数库中的图形(第九十八部分):移动扩展的标准图形对象的轴点·进阶篇
(2/3)·光标靠近才显形的轴心点控件,如何把趋势线端点拖拽变成可扩展交互的试验场
给图形对象挂上控制点表单
在 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 分钟图上直观看到锚点表单的吸附范围变化,外汇与贵金属杠杆高,测试请用模拟盘。
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 重绘。外汇与贵金属图表上挂这类扩展对象,因点差与滑点波动大,属于高风险操作环境,参数误设可能导致图形错位。
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 等组合键状态。 外汇与贵金属杠杆高、滑点随机,这类界面同步代码只解决显示一致性,不构成任何方向判断。
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 的返回值。
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 里直观看到锚点手柄的视觉反馈变化。
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 可能缓解渲染开销。外汇与贵金属图表对象联动涉及实时重绘,滑点与时延可能造成图形短暂失准,属高风险操作环境。
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); }