DoEasy 函数库中的图形(第九十五部分):复合图形对象控件·进阶篇
(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 占用。外汇与贵金属杠杆高,这类自定义对象仅作辅助判势,信号本身不预示单边行情。
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() 保证列表空态起步,避免残留表单引发下标越界。
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_LABEL、OBJ_BUTTON、OBJ_BITMAP_LABEL、OBJ_EDIT 等少数几种能当基底。若你传个 OBJ_TREND 进去,后续 CreateAllControlPointForm 生成控制点表单时可能直接漏掉坐标绑定,拖不动。
开 MT5 验证时,可先建一个 OBJ_BUTTON 基底,把 ctrl_form_size 设成 12 对比设成 20,看控制点表单在小屏上的拥挤程度差异。外汇与贵金属图表波动大,这类自定义工具在高杠杆品种上误触概率会偏高,务必先在模拟盘跑通。
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 故意传超,观察专家日志是否抛出该错误码即可。
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 的调用会静默丢弃。外汇与贵金属图表上做对象锚定属于高风险操作,坐标算错可能让提示框漂到看不见的位置。
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 建议先在模拟盘验证。
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; }