DoEasy. 控件 (第 2 部分): 操控 CPanel 类·综合运用
◍ 面板类的三种构造入口
在 MT5 自定义图形库里,CPanel 提供了三个重载构造函数,分别对应不同的图表上下文。最完整的版本要求显式传入 chart_id 与 subwindow,适合多图表、多子窗口的面板布局;另外两个则默认绑定当前图表(ChartID()),区别仅在是否指定子窗口编号。 三个构造体内部逻辑高度一致:先调 CForm 基类初始化位置与尺寸,再把元素类型锁死为 GRAPH_ELEMENT_TYPE_PANEL,前景色取 CLR_FORE_COLOR,字重设为 FW_TYPE_NORMAL。注意 MarginAll(3) 与 PaddingAll(0) 是硬编码默认值——边距 3 像素、内边距 0,若你想做紧凑 HUD 就得事后覆写。 下面贴出第三段(当前图表主窗口)的源码,便于直接抄进 EA 里改: CPanel::CPanel(const string name, const int x, const int y, const int w, const int h) : CForm(::ChartID(),0,name,x,y,w,h) { CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL); this.m_type=OBJECT_DE_TYPE_GWF_PANEL; this.m_fore_color=CLR_FORE_COLOR; this.m_bold_type=FW_TYPE_NORMAL; this.MarginAll(3); this.PaddingAll(0); this.Initialize(); } 逐行拆解:第 1 行声明构造器,仅收 name 与坐标宽高,子窗口硬编码 0 即主图;第 2 行冒号后调基类 CForm,用 ChartID() 抓当前图表句柄;第 4 行登记图形元素类别为面板;第 5 行写内部枚举 m_type;第 6 行设前景色常量;第 7 行字重常规;第 8~9 行统一边距与内边距;第 10 行跑 Initialize() 完成子对象挂载。外汇与贵金属图表加载此类面板存在刷新开销,高频 Tick 下可能拖慢终端,属高风险定制。
CPanel::CPanel(const class="type">class="kw">string name, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h) : CForm(::ChartID(),class="num">0,name,x,y,w,h) { CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL); this.m_type=OBJECT_DE_TYPE_GWF_PANEL; this.m_fore_color=CLR_FORE_COLOR; this.m_bold_type=FW_TYPE_NORMAL; this.MarginAll(class="num">3); this.PaddingAll(class="num">0); this.Initialize(); }
面板字体样式的取与设
在 MT5 自定义面板类里,字体状态不是直接存字符串,而是靠一个 uint 型的 flags 位掩码来记录。CPanel::GetFontFlags 先向 CGCnvElement 要回 name、size、flags、angle 四个量,只把 flags 返出来,其余三个是借壳取数。 加粗逻辑分 setter 和 getter 两套。Bold(true) 会先取当前 flags,再用按位或塞进 FW_BOLD,同时把成员 m_bold_type 标成 FW_TYPE_BOLD;Bold(false) 分支里只改了 m_bold_type 回 FW_TYPE_NORMAL,却没调 SetFontFlags 去清位——这是个容易漏的坑,实际去粗体可能得自己补一句清位。 Italic 的写法更短:Italic(true) 用 flags | FONT_ITALIC 设位,Italic(false) 整个方法体里根本没有,意味着想关斜体得另写清位代码。Italic(void) 的判定是 (flags & FONT_ITALIC)==FONT_ITALIC,位与相等才返真。 别把取数当改数 GetFontFlags 只是读,Bold/Italic 的 false 分支不回写就只是改了类内标记,画面上字体不会跟着变。开 MT5 挂个测试面板,断点看 m_bold_type 和真实渲染是否同步最稳妥。
class="type">uint CPanel::GetFontFlags(class="type">void) { class="type">class="kw">string name; class="type">int size; class="type">uint flags; class="type">uint angle; CGCnvElement::GetFont(name,size,flags,angle); class="kw">return flags; } class="type">void CPanel::Bold(const class="type">bool flag) { class="type">uint flags=this.GetFontFlags(); if(flag) { this.m_bold_type=FW_TYPE_BOLD; CGCnvElement::SetFontFlags(flags | FW_BOLD); } else this.m_bold_type=FW_TYPE_NORMAL; } class="type">bool CPanel::Bold(class="type">void) { class="type">uint flags=this.GetFontFlags(); class="kw">return(flags & FW_BOLD)==FW_BOLD; } class="type">void CPanel::Italic(const class="type">bool flag) { class="type">uint flags=this.GetFontFlags(); if(flag) CGCnvElement::SetFontFlags(flags | FONT_ITALIC); } class="type">bool CPanel::Italic(class="type">void) { class="type">uint flags=this.GetFontFlags(); class="kw">return(flags & FONT_ITALIC)==FONT_ITALIC; }
「面板文字的删除线与下划线控制」
在 MT5 自定义面板类里,删除线和下划线并不是直接画上去的,而是通过对字体标志位做位或运算来切换。CPanel 用两组重载方法分别负责「设」和「查」:带 bool 参数的版本改写标志,无参版本回读当前状态。 下面这段是删除线的核心实现。Strikeout(true) 先取现有 flags,再和 FONT_STRIKEOUT 做按位或,从而把删除线位点亮;Strikeout() 无参时则用按位与判断该位是否为 1。 void CPanel::Strikeout(const bool flag) { uint flags=this.GetFontFlags(); if(flag) CGCnvElement::SetFontFlags(flags | FONT_STRIKEOUT); } //+------------------------------------------------------------------+ bool CPanel::Strikeout(void) { uint flags=this.GetFontFlags(); return(flags & FONT_STRIKEOUT)==FONT_STRIKEOUT; } 下划线逻辑完全对称:Underline(true) 置位 FONT_UNDERLINE,Underline() 回读该位。 void CPanel::Underline(const bool flag) { uint flags=this.GetFontFlags(); if(flag) CGCnvElement::SetFontFlags(flags | FONT_UNDERLINE); } //+------------------------------------------------------------------+ bool CPanel::Underline(void) { uint flags=this.GetFontFlags(); return(flags & FONT_UNDERLINE)==FONT_UNDERLINE; } FontDrawStyle 用 switch 把枚举转成具体调用:传 FONT_STYLE_STRIKEOUT 就触发 Strikeout(true),传 FONT_STYLE_UNDERLINE 触发 Underline(true),ITALIC 则走 Italic(true)。回读时按优先级返回第一个命中的样式。 void CPanel::FontDrawStyle(ENUM_FONT_STYLE style) { switch(style) { case FONT_STYLE_ITALIC : this.Italic(true); break; case FONT_STYLE_UNDERLINE : this.Underline(true); break; case FONT_STYLE_STRIKEOUT : this.Strikeout(true); break; default: break; } } //+------------------------------------------------------------------+ ENUM_FONT_STYLE CPanel::FontDrawStyle(void) { return ( this.Italic() ? FONT_STYLE_ITALIC :
class="type">void CPanel::Strikeout(const class="type">bool flag) { class="type">uint flags=this.GetFontFlags(); if(flag) CGCnvElement::SetFontFlags(flags | FONT_STRIKEOUT); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPanel::Strikeout(class="type">void) { class="type">uint flags=this.GetFontFlags(); class="kw">return(flags & FONT_STRIKEOUT)==FONT_STRIKEOUT; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPanel::Underline(const class="type">bool flag) { class="type">uint flags=this.GetFontFlags(); if(flag) CGCnvElement::SetFontFlags(flags | FONT_UNDERLINE); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPanel::Underline(class="type">void) { class="type">uint flags=this.GetFontFlags(); class="kw">return(flags & FONT_UNDERLINE)==FONT_UNDERLINE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPanel::FontDrawStyle(ENUM_FONT_STYLE style) { class="kw">switch(style) { case FONT_STYLE_ITALIC : this.Italic(true); break; case FONT_STYLE_UNDERLINE : this.Underline(true); break; case FONT_STYLE_STRIKEOUT : this.Strikeout(true); break; class="kw">default: break; } } class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_FONT_STYLE CPanel::FontDrawStyle(class="type">void) { class="kw">return ( this.Italic() ? FONT_STYLE_ITALIC :
◍ 面板字体粗细的枚举切换逻辑
在自建 MT5 面板类里,字体粗细不是直接写死字重,而是用 ENUM_FW_TYPE 枚举统一接管,再映射到 GDI 的 FW_* 标志位。 CPanel::FontBoldType 先存下当前选择的 type 到成员变量 m_bold_type,随后取出已有字体 flags,按 switch 分支把对应字重常量用位或叠加上去。 这套枚举覆盖了从 FW_TYPE_THIN(极细)到 FW_TYPE_HEAVY(极粗)共 13 个档位,其中 FW_TYPE_DONTCARE 让系统按默认处理,FW_TYPE_BOLD 对应常规加粗。 在 MT5 里跑这段,改一行 FontBoldType(FW_TYPE_SEMIBOLD) 就能实时看到面板文字字重变化,方便按深色背景调可读性。外汇与贵金属图表叠加自定义面板时,注意过度加粗可能在小周期挤占 K 线空间。
class="type">void CPanel::FontBoldType(ENUM_FW_TYPE type) { this.m_bold_type=type; class="type">uint flags=this.GetFontFlags(); class="kw">switch(type) { case FW_TYPE_DONTCARE : CGCnvElement::SetFontFlags(flags | FW_DONTCARE); break; case FW_TYPE_THIN : CGCnvElement::SetFontFlags(flags | FW_THIN); break; case FW_TYPE_EXTRALIGHT : CGCnvElement::SetFontFlags(flags | FW_EXTRALIGHT); break; case FW_TYPE_ULTRALIGHT : CGCnvElement::SetFontFlags(flags | FW_ULTRALIGHT); break; case FW_TYPE_LIGHT : CGCnvElement::SetFontFlags(flags | FW_LIGHT); break; case FW_TYPE_REGULAR : CGCnvElement::SetFontFlags(flags | FW_REGULAR); break; case FW_TYPE_MEDIUM : CGCnvElement::SetFontFlags(flags | FW_MEDIUM); break; case FW_TYPE_SEMIBOLD : CGCnvElement::SetFontFlags(flags | FW_SEMIBOLD); break; case FW_TYPE_DEMIBOLD : CGCnvElement::SetFontFlags(flags | FW_DEMIBOLD); break; case FW_TYPE_BOLD : CGCnvElement::SetFontFlags(flags | FW_BOLD); break; case FW_TYPE_EXTRABOLD : CGCnvElement::SetFontFlags(flags | FW_EXTRABOLD); break; case FW_TYPE_ULTRABOLD : CGCnvElement::SetFontFlags(flags | FW_ULTRABOLD); break; case FW_TYPE_HEAVY : CGCnvElement::SetFontFlags(flags | FW_HEAVY); break; } }
图形对象集合类的返回码与索引查找
在封装 MT5 画布控件时,把添加结果显式枚举出来比直接返回 bool 更利于排错。下面这段枚举给出了三种状态:成功入列、对象已存在、入列失败,调用方据此决定是新建引用还是复用旧实例。 ENUM_ADD_OBJ_RET_CODE 的三个成员对应实际开发里的高频场景——同一个前缀 m_name_prefix 下的对象重复 Add,会拿到 EXIST 而不是 SUCCESS,避免画面元素被静默覆盖。 集合类 CGraphElementsCollection 用 m_total_objects 记录当前对象数,m_delta_graph_obj 存本次检查相对上次的增量,这两个整型字段配合 GetIndexGraphElement(chart_id,name) 可以按图表 ID 与对象名反查索引。回测面板若挂在 3 个图表上,同名前缀对象数超过 1 时,增量差就能提示你是否有残留控件未清理。 AddOrGetCanvElmToCollection 通过引用参数 int &id 回写对象下标,比单纯 bool 返回值多带一个定位信息,后续交互事件分发直接拿 id 进数组取指针即可。
enum ENUM_ADD_OBJ_RET_CODE { ADD_OBJ_RET_CODE_SUCCESS, ADD_OBJ_RET_CODE_EXIST, ADD_OBJ_RET_CODE_ERROR, }; class="type">int GetIndexGraphElement(const class="type">long chart_id,const class="type">class="kw">string name); ENUM_ADD_OBJ_RET_CODE AddOrGetCanvElmToCollection(CGCnvElement *element,class="type">int &id);
「按图表与名称检索画布图元的实现细节」
在自定义画布管理类里,通过图表 ID 与对象名定位图元是高频操作。下面这段逻辑先拼出带前缀的对象名,再从全局画布元素列表里做两次属性过滤,最终返回匹配集合。 string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name; CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL); return CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL); 第一行用 StringFind 判断用户传入的 name 是否已带 m_name_prefix 前缀,未带则补上,避免重复拼接。第二行按 CANV_ELEMENT_PROP_CHART_ID 相等条件筛出该图表上的所有画布元素,第三行在结果中再按对象名精确匹配,返回 CArrayObj 指针。 GetCanvElement 函数直接调用上面的按名检索,取列表第 0 项作为唯一结果:若列表非空返回 list.At(0),否则返回 NULL。这意味着同一图表+同名只允许一个实例,重复创建会在上层被拦掉。 私有段里 BringToTopAllCanvElm 负责把画布对象置顶,而 AddCanvElmToCollection 失败时会 delete obj 并返回 WRONG_VALUE,说明对象进集合前就分配了堆内存,集合接纳失败必须手动释放,否则在 EA 反复刷新图表时可能泄漏句柄。开 MT5 把这段塞进你自己的 CGCnvElement 派生类,断点跟一下 list.At(0) 的命中率,能直观看到同名冲突时的返回路径。
class="type">class="kw">string nm=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name; CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL); class="kw">return CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL); CGCnvElement *GetCanvElement(const class="type">long chart_id,const class="type">class="kw">string name) { CArrayObj *list=this.GetListCanvElementByName(chart_id,name); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); }
◍ 给画布元素登记身份并兜底重复创建
在自绘图形库里,每生成一个画布元素都要先到总表领一个序号,避免 ID 乱套。下面这段构造函数入口就干了这件事:用 m_list_all_canv_elm_obj.Total() 取当前元素总数作为新 id,新建 CGCnvElement 后再交给 AddOrGetCanvElmToCollection 决定是落库还是复用。 如果返回 ADD_OBJ_RET_CODE_ERROR,直接甩 WRONG_VALUE 出来,调用方马上能知道建对象失败了;若是 ADD_OBJ_RET_CODE_EXIST,说明集合里已有同款,就把已有对象的 ID 刷成新的 id,保证后续擦除和重绘指向正确目标。 最后无论走哪条分支,都会调一次 obj.Erase(clr,opacity,redraw) 用指定颜色和透明度清掉初始像素,再 return obj.ID()。在 MT5 里把 opacity 设成 0 到 255 之间的数,就能验证半透明遮罩层是否如预期叠加——外汇与贵金属图表叠加自绘层时波动跳空多,这种显式擦除能降低重绘残影概率。
const class="type">class="kw">string name, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">class="kw">color clr, const class="type">uchar opacity, const class="type">bool movable, const class="type">bool activity, const class="type">bool redraw=false) { class="type">int id=this.m_list_all_canv_elm_obj.Total(); CGCnvElement *obj=new CGCnvElement(GRAPH_ELEMENT_TYPE_ELEMENT,id,class="num">0,chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,activity,redraw); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) class="kw">return WRONG_VALUE; if(res==ADD_OBJ_RET_CODE_EXIST) obj.SetID(id); obj.Erase(clr,opacity,redraw); class="kw">return obj.ID(); }
往画布集合里塞一个窗体对象
在 MT5 自定义面板开发里,动态创建可交互窗体通常走一个统一入口函数,把图表 ID、子窗口序号、对象名和坐标宽高一次性传进去。下面这段实现先按当前集合总数拿一个自增 id,再 new 出 CForm 实例。 代码里用 AddOrGetCanvElmToCollection 把对象收编进 m_list_all_canv_elm_obj,返回错误就直接 WRONG_VALUE 退出,避免野指针堆积。 收编成功后,依次设置 id、激活态、可移动、背景与边框同色、透明度(第二参 false 表示暂不重绘)以及阴影开关。外汇与贵金属图表上挂这类 UI 属于高频重绘场景,杠杆与滑点风险高,参数乱设可能导致终端卡顿。 别把 new 出来的对象当免死金牌:若 AddOrGet 返回错误分支提前 return,上方 new 的 CForm 若未被集合接管就泄漏了,实盘前应在 MT5 用内存快照验证。
const class="type">int subwindow, const class="type">class="kw">string name, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">class="kw">color clr, const class="type">uchar opacity, const class="type">bool movable, const class="type">bool activity, const class="type">bool shadow=false, const class="type">bool redraw=false) { class="type">int id=this.m_list_all_canv_elm_obj.Total(); CForm *obj=new CForm(chart_id,subwindow,name,x,y,w,h); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) class="kw">return WRONG_VALUE; obj.SetID(id); obj.SetActive(activity); obj.SetMovable(movable); obj.SetColorBackground(clr); obj.SetColorFrame(clr); obj.SetOpacity(opacity,false); obj.SetShadow(shadow);
「在指定图表上画出可拖拽的画布面板」
想在 MT5 的某个图表子窗口里挂一块自定义面板,靠的是 CreatePanel 这套封装。它把画布坐标、尺寸、颜色、透明度以及边框样式全部参数化,调用一次就能返回一个整数 ID 供后续引用。 函数签名里 frame_width 默认 -1 表示沿用对象自身边框宽度,frame_style 默认 FRAME_STYLE_BEVEL 即斜角浮雕感;shadow 和 redraw 默认关 false,意味着不画阴影、也不强制重绘,省资源但首次创建后可能要手动 Refresh。 创建末尾先 DrawRectangle 用 (0,0) 到 (Width()-1, Height()-1) 描出边框区域,再 Done 收尾,最后 Erase(clr,opacity,redraw) 按给定颜色和透明度清掉底图并返回 obj.ID()。这套顺序不能乱,先画框再结束再擦底,否则面板可能闪一下残影。 外汇与贵金属图表叠加这类图形对象存在重绘开销,高频 tick 下若面板数超过 20 个,可能拖慢终端响应,建议只在必要子窗口启用。
obj.DrawRectangle(class="num">0,class="num">0,obj.Width()-class="num">1,obj.Height()-class="num">1,obj.ColorFrame(),obj.Opacity()); obj.Done(); obj.Erase(clr,opacity,redraw); class="kw">return obj.ID(); class=class="str">"cmt">//--- Create a graphical object form object on canvas on a specified chart and subwindow with the vertical gradient filling class=class="str">"cmt">//--- Create graphical object WinForms Panel object on canvas on a specified chart and subwindow class="type">int CreatePanel(const class="type">long chart_id, const class="type">int subwindow, const class="type">class="kw">string name, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">class="kw">color clr, const class="type">uchar opacity, const class="type">bool movable, const class="type">bool activity, const class="type">int frame_width=-class="num">1, ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL, const class="type">bool shadow=false, const class="type">bool redraw=false) { class="type">int id=this.m_list_all_canv_elm_obj.Total();
◍ 面板对象的实例化与集合注册
在 MT5 自定义图形界面开发中,CPanel 的创建不是简单 new 一个对象就完事,还要把它挂进图形元素集合并初始化外观属性。下面这段逻辑展示了从内存分配到画布绘制的完整链路。 CPanel *obj=new CPanel(chart_id,subwindow,name,x,y,w,h); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) return WRONG_VALUE; obj.SetID(id); obj.SetActive(activity); obj.SetMovable(movable); obj.SetColorBackground(clr); obj.SetColorFrame(clr); obj.SetOpacity(opacity,false); obj.SetShadow(shadow); obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity()); obj.Erase(clr,opacity,redraw); if(frame_width>0) obj.FrameWidthAll(frame_width); obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop()); obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),frame_style); obj.Done(); return obj.ID(); 逐行拆解:new CPanel 传入图表 ID、子窗口号、对象名及 x/y/w/h 坐标尺寸;AddOrGetCanvElmToCollection 尝试注册,失败直接返回 WRONG_VALUE 避免野指针。随后一连串 Set 方法设定 ID、激活态、可移动、背景与边框色、透明度、阴影。DrawRectangle 用宽高减 1 画边界,Erase 按背景色清屏,frame_width>0 才统一设边框。最后 SetActiveAreaShift 偏移响应区,DrawFormFrame 描框,Done 收尾并返回 ID。 集合类构造里也有坑:CGraphElementsCollection 构造函数强制开启 CHART_EVENT_MOUSE_MOVE 与 CHART_EVENT_MOUSE_WHEEL 两个图表事件,意味着只要用了这个库,鼠标移动和滚轮事件就会被全局捕获,EA 里若另写 OnChartEvent 需留意事件竞争。m_list_all_graph_obj 按 CANV_ELEMENT_ID 排序后 Clear,保证每次启动干净状态。外汇与贵金属图表挂这类界面,高频重绘叠加事件监听,可能拖慢 tick 响应,实盘前务必在策略测试器跑一遍资源占用。
CPanel *obj=new CPanel(chart_id,subwindow,name,x,y,w,h); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) class="kw">return WRONG_VALUE; obj.SetID(id); obj.SetActive(activity); obj.SetMovable(movable); obj.SetColorBackground(clr); obj.SetColorFrame(clr); obj.SetOpacity(opacity,false); obj.SetShadow(shadow); obj.DrawRectangle(class="num">0,class="num">0,obj.Width()-class="num">1,obj.Height()-class="num">1,obj.ColorFrame(),obj.Opacity()); obj.Erase(clr,opacity,redraw); if(frame_width>class="num">0) obj.FrameWidthAll(frame_width); obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop()); obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),frame_style); obj.Done(); class="kw">return obj.ID(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CGraphElementsCollection::CGraphElementsCollection() { this.m_type=COLLECTION_GRAPH_OBJ_ID; this.m_name_prefix=this.m_name_program+"_"; ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_MOVE,true); ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_WHEEL,true); this.m_list_all_graph_obj.Type(COLLECTION_GRAPH_OBJ_ID); this.m_list_all_graph_obj.Sort(SORT_BY_CANV_ELEMENT_ID); this.m_list_all_graph_obj.Clear();
画布图元集合的增删与查重逻辑
在 MT5 自定义图形库里,CGraphElementsCollection 负责统管所有画布元素。重置时先对图表控件列表做 Sort 再 Clear,同时把 m_total_objects 置 0、m_is_graph_obj_event 置 false,并清空已删除对象列表与全画布元素列表,保证状态机回到干净起点。 新增元素走 AddCanvElmToCollection:若 m_list_all_canv_elm_obj.Add(element) 返回失败,就写日志 MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST 并回 false,否则回 true。这一步没有任何隐式去重,纯粹是尾部追加。 真正去重在 AddOrGetCanvElmToCollection。传入空指针直接回 ADD_OBJ_RET_CODE_ERROR;若 IsPresentCanvElmInList 按 chart_id+name 命中,则取回已有对象、把其 ID 通过引用参数回传,并返回 ADD_OBJ_RET_CODE_EXIST,调用方据此跳过重复创建。 添加失败时该函数会 delete element 再回错误码,避免悬空指针。GetIndexGraphElement 则按 chart_id 与 name 反查列表下标,供后续定位使用。外汇与贵金属图表加载这类自绘库时波动剧烈,可能触发频繁重建,建议在策略初始化阶段先于 OnChartEvent 之前完成集合预热。
this.m_list_charts_control.Sort(); this.m_list_charts_control.Clear(); this.m_total_objects=class="num">0; this.m_is_graph_obj_event=false; this.m_list_deleted_obj.Clear(); this.m_list_deleted_obj.Sort(); this.m_list_all_canv_elm_obj.Clear(); this.m_list_all_canv_elm_obj.Sort(); } class="type">bool CGraphElementsCollection::AddCanvElmToCollection(CGCnvElement *element) { if(!this.m_list_all_canv_elm_obj.Add(element)) { CMessage::ToLog(DFUN+element.Name()+": ",MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST); class="kw">return false; } class="kw">return true; } ENUM_ADD_OBJ_RET_CODE CGraphElementsCollection::AddOrGetCanvElmToCollection(CGCnvElement *element,class="type">int &id) { if(element==NULL) { CMessage::ToLog(DFUN,MSG_GRAPH_ELM_COLLECTION_ERR_EMPTY_OBJECT); class="kw">return ADD_OBJ_RET_CODE_ERROR; } if(this.IsPresentCanvElmInList(element.ChartID(),element.Name())) { CMessage::ToLog(DFUN+element.Name()+": ",MSG_LIB_SYS_OBJ_ALREADY_IN_LIST); element=this.GetCanvElement(element.ChartID(),element.Name()); if(element==NULL) { CMessage::ToLog(DFUN,MSG_GRAPH_ELM_COLLECTION_ERR_FAILED_GET_ELEMENT); class="kw">return ADD_OBJ_RET_CODE_ERROR; } id=element.ID(); class="kw">return ADD_OBJ_RET_CODE_EXIST; } if(!this.AddCanvElmToCollection(element)) { class="kw">delete element; class="kw">return ADD_OBJ_RET_CODE_ERROR; } class="kw">return ADD_OBJ_RET_CODE_SUCCESS; } class="type">int CGraphElementsCollection::GetIndexGraphElement(const class="type">long chart_id,const class="type">class="kw">string name) {
「从集合里捞回游离的图形对象」
在自绘图形框架里,图表上的对象可能被手动删掉,但内存集合还留着指针。FindMissingObj 就是专门扫一遍 m_list_all_graph_obj,把「集合里有、图表上查不到」的对象揪出来,同时把它的下标写进引用参数 index。 代码里先给 index 赋 WRONG_VALUE 保底,再跑 for 循环用 this.m_list_all_graph_obj.Total() 控制边界;一旦 IsPresentGraphObjOnChart 返回 false,就锁定 index=i 并 return obj,调用方拿到非空指针即代表发现了失联对象。 另一段交互逻辑里,elm.TypeGraphElement()>=GRAPH_ELEMENT_TYPE_FORM 是判定「是否为表单类或其子类」的硬门槛。满足后调 form.MouseFormState 取鼠标状态,只要 mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL 就直接 return form,说明光标已在表单内,无需再遍历其余画布元素。 开 MT5 把这段塞进你的 CGraphElementsCollection 类,故意在图表删一个标签对象,再断点跟 FindMissingObj,能看到 index 被写成对应列表位置而非 WRONG_VALUE,就能确认回收链路通了。外汇与贵金属图表脚本改动涉及实时对象生命周期,误删或野指针可能引发 EA 异常,属高风险操作,建议先在模拟盘验证。
CGStdGraphObj *CGraphElementsCollection::FindMissingObj(const class="type">long chart_id,class="type">int &index) { index=WRONG_VALUE; for(class="type">int i=class="num">0;i<this.m_list_all_graph_obj.Total();i++) { CGStdGraphObj *obj=this.m_list_all_graph_obj.At(i); if(obj==NULL) class="kw">continue; if(!this.IsPresentGraphObjOnChart(obj.ChartID(),obj.Name())) { index=i; class="kw">return obj; } } class="kw">return NULL; }
◍ 表单交互标记与层级置顶的清理逻辑
在图形元素集合类里,鼠标事件分发先尝试把命中表单的指针回传:当 mouse_state 大于 MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL 时直接 return,说明光标已落在表单范围内,外部无需再遍历其余对象。
ResetAllInteractionExeptOne 负责把集合中除指定表单外的所有交互标志清零。循环里用 obj.TypeGraphElement()<GRAPH_ELEMENT_TYPE_FORM 过滤掉非表单及派生类,并用名称加 ChartID 双重判断跳过传入的那个表单——这种双重判定能避免不同图表上同名表单被误清。
置顶函数 SetZOrderMAX 会临时借表单对象在背景中央画一行字:form.TextOnBG(0, 描述+ID+ZD, 宽/2, 高/2, 居中, 颜色 C'211,233,149', 255, true, true),把元素类型和 ZOrder 可视化出来,方便在 MT5 上肉眼核对层级是否如预期刷新。随后对全量元素按 ID 排序,为后续 ZOrder 重排做准备。
循环处理剩余对象时,若取不到指针、或是管理扩展标准对象控制点的表单、又或 ZOrder 已是 0,就直接跳过——0 层即底层的设定让置顶操作不会无谓地改写最下层。开 MT5 把这段塞进自己的 CGraphElementsCollection 派生类,断点跟一遍 mouse_state 阈值就能验证命中判定是否符合盘面手感。
class=class="str">"cmt">//--- Assign the pointer to the element for the form object pointer form=elm; class=class="str">"cmt">//--- Get the mouse status relative to the form mouse_state=form.MouseFormState(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor is within the form, class="kw">return the pointer to the form if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL) class="kw">return form; } } class=class="str">"cmt">//--- If there is no a single form object from the collection list class=class="str">"cmt">//--- Get the list of extended standard graphical objects class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//| Reset all interaction flags for all forms except the specified one | class=class="str">"cmt">//+--------------------------------------------------------------------+ class="type">void CGraphElementsCollection::ResetAllInteractionExeptOne(CGCnvElement *form_exept) { class=class="str">"cmt">//--- In the loop by all graphical element collection class objects class="type">int total=this.m_list_all_canv_elm_obj.Total(); for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- get the pointer to the object CGCnvElement *obj=this.m_list_all_canv_elm_obj.At(i); class=class="str">"cmt">//--- if failed to receive the pointer, or it is not a form or its descendants, or it is not a form whose pointer has been passed to the method, move on if(obj==NULL || obj.TypeGraphElement()<GRAPH_ELEMENT_TYPE_FORM || (obj.Name()==form_exept.Name() && obj.ChartID()==form_exept.ChartID())) class="kw">continue; class=class="str">"cmt">//--- Reset the interaction flag for the current form in the loop obj.SetInteraction(false); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set ZOrde to the specified element | class=class="str">"cmt">//| and adjust it in other elements | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CGraphElementsCollection::SetZOrderMAX(CGCnvElement *obj) { class=class="str">"cmt">//--- ... class=class="str">"cmt">//--- Temporarily declare a form object for drawing a text for visually displaying its ZOrder CForm *form=obj; class=class="str">"cmt">//--- and draw a text specifying ZOrder on the form form.TextOnBG(class="num">0,form.TypeElementDescription()+": ID "+(class="type">class="kw">string)form.ID()+", ZD "+(class="type">class="kw">string)form.Zorder(),form.Width()/class="num">2,form.Height()/class="num">2,FRAME_ANCHOR_CENTER,C&class="macro">#x27;class="num">211,class="num">233,class="num">149&class="macro">#x27;,class="num">255,true,true); class=class="str">"cmt">//--- Sort the list of graphical elements by an element ID this.m_list_all_canv_elm_obj.Sort(SORT_BY_CANV_ELEMENT_ID); class=class="str">"cmt">//--- ... class=class="str">"cmt">//--- ... class=class="str">"cmt">//--- In the loop by the obtained list of remaining graphical element objects for(class="type">int i=class="num">0;i<list.Total();i++) { class=class="str">"cmt">//--- get the next object CGCnvElement *elm=list.At(i); class=class="str">"cmt">//--- If failed to get the object or if this is a control form for managing pivot points of an extended standard graphical object class=class="str">"cmt">//--- or, if the object&class="macro">#x27;s ZOrder is zero, skip the object since there is no need in changing its ZOrder as it is the bottom one
图形元素 Z 序下调与反初始化清理
在遍历图形元素集合时,先跳过空指针、表单控件以及 Z 序为 0 的对象——这些不需要参与层级下调。若元素类型大于等于 OBJECT_DE_TYPE_GFORM,则视为表单,把指针赋给 form 变量并在背景中央绘制一行文字,显示类型描述、ID 与当前 ZD 值,便于肉眼核对层级变化。 下调逻辑本身很直接:对每个有效元素调用 SetZorder(elm.Zorder()-1, false),把 Z 序减 1;只要有一次失败,就用位与把 res 置为 false,循环结束直接返回 res。注意第二个参数 false 表示不强制重绘,避免每改一个元素就刷一次画布。 反初始化时两层清理必须配对:CGraphElementsCollection::OnDeinit 清空 m_list_all_canv_elm_obj,CEngine::OnDeinit 先清指标列表再调 m_graph_objects.OnDeinit()。漏掉任一层的 Clear,EA 重载后可能残留旧画布对象,在 MT5 里表现为图形不刷新或报错。 开 MT5 把这段接进自己的 GUI 框架,改 Zorder()-1 为 -2 可观察跨两层遮挡效果;外汇与贵金属图表叠加自定义 UI 属高风险操作,参数误改可能导致对象不可见。
if(elm==NULL || elm.Type()==OBJECT_DE_TYPE_GFORM_CONTROL || elm.Zorder()==class="num">0) class="kw">continue; class=class="str">"cmt">//--- If failed to set the object&class="macro">#x27;s ZOrder to class="num">1 less than it already is(decreasing ZOrder by class="num">1), add &class="macro">#x27;false&class="macro">#x27; to the &class="macro">#x27;res&class="macro">#x27; value if(!elm.SetZorder(elm.Zorder()-class="num">1,false)) res &=false; class=class="str">"cmt">//--- Temporarily(for the test purpose), if the element is a form, if(elm.Type()>=OBJECT_DE_TYPE_GFORM) { class=class="str">"cmt">//--- assign the pointer to the element for the form and draw a text specifying ZOrder on the form form=elm; form.TextOnBG(class="num">0,form.TypeElementDescription()+": ID "+(class="type">class="kw">string)form.ID()+", ZD "+(class="type">class="kw">string)form.Zorder(),form.Width()/class="num">2,form.Height()/class="num">2,FRAME_ANCHOR_CENTER,C&class="macro">#x27;class="num">211,class="num">233,class="num">149&class="macro">#x27;,class="num">255,true,true); } class=class="str">"cmt">//--- Upon the loop completion, class="kw">return the result set in &class="macro">#x27;res&class="macro">#x27; class="kw">return res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deinitialization event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGraphElementsCollection::OnDeinit(class="type">void) { this.m_list_all_canv_elm_obj.Clear(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deinitialize library | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEngine::OnDeinit(class="type">void) { this.m_indicators.GetList().Clear(); this.m_graph_objects.OnDeinit(); } class=class="str">"cmt">//+------------------------------------------------------------------+
「面板边框与粗体字的实盘验证」
把上一篇文章里的 EA 原样搬过来,存到 \MQL5\Experts\TestDoEasy\Part102\ 并改名 TestDoEasyPart102.mq5,我只动了图形对象的坐标间距和面板尺寸,没碰核心逻辑。 编译挂上图表后能看到:所有对象都能用鼠标拖拽交互,面板带上了边框,文字按设定以粗体渲染;切换品种图表时对象不再莫名消失,但新位置不会被记住——这得等我把整套对象继承写完后,用文件读写来固化。 OnInit() 里建面板时给了 230×150 的画布、透明度 200,右侧偏移 elm.RightEdge()+20。下面这段代码就是面板创建后立刻设普通体+粗体,并把类型/ID/ZOrder 打到日志里,方便你对照 MT5 终端输出。 外汇与贵金属挂 EA 属高风险操作,面板显示异常可能拖慢 tick 响应,建议在模拟盘先跑。
class=class="str">"cmt">//--- Create WinForms Panel object CPanel *pnl=NULL; obj_id=engine.GetGraphicObjCollection().CreatePanel(ChartID(),class="num">0,"WFPanel",elm.RightEdge()+class="num">20,class="num">50,class="num">230,class="num">150,array_clr[class="num">0],class="num">200,true,true); list=engine.GetListCanvElementByID(ChartID(),obj_id); pnl=list.At(class="num">0); if(pnl!=NULL) { pnl.FontDrawStyle(FONT_STYLE_NORMAL); pnl.Bold(true); Print(DFUN,EnumToString(pnl.FontDrawStyle())); Print(DFUN,EnumToString(pnl.FontBoldType())); pnl.SetFontSize(class="num">10); pnl.TextOnBG(class="num">0,pnl.TypeElementDescription()+": ID "+(class="type">class="kw">string)pnl.ID()+", ZD "+(class="type">class="kw">string)pnl.Zorder(),pnl.Width()/class="num">2,pnl.Height()/class="num">2,FRAME_ANCHOR_CENTER,C&class="macro">#x27;class="num">211,class="num">233,class="num">149&class="macro">#x27;,pnl.Opacity()); pnl.Update(true); } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); }
◍ 画得少,看得清
面板对象类的雏形已经搭起,下一篇会接着把这套类结构补全,让控件在 MT5 图表上真正可拖可点。当前函数库版本、测试 EA 和图表事件控制指标都打进了那个 4384.12 KB 的压缩包,下载丢进 MT5 的 MQL5 目录就能直接编译跑。 外汇和贵金属图表上的控件交互属于高频试错场景,权限和事件钩子没调顺之前,别拿实盘账户硬刚。先把示例 EA 在策略测试器里过一遍,看面板响应和你预期是否一致。 等类封完,你开 MT5 改两行坐标参数,就能把一堆杂乱指标收进一个面板——少画几条线,反而更容易盯住关键价位。