DoEasy. 控件 (第 2 部分): 操控 CPanel 类·综合运用
📘

DoEasy. 控件 (第 2 部分): 操控 CPanel 类·综合运用

第 3/3 篇

◍ 面板类的三种构造入口

在 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 下可能拖慢终端,属高风险定制。

MQL5 / C++
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 和真实渲染是否同步最稳妥。

MQL5 / C++
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 :

MQL5 / C++
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 线空间。

MQL5 / C++
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 进数组取指针即可。

MQL5 / C++
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) 的命中率,能直观看到同名冲突时的返回路径。

MQL5 / C++
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 之间的数,就能验证半透明遮罩层是否如预期叠加——外汇与贵金属图表叠加自绘层时波动跳空多,这种显式擦除能降低重绘残影概率。

MQL5 / C++
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 用内存快照验证。

MQL5 / C++
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 个,可能拖慢终端响应,建议只在必要子窗口启用。

MQL5 / C++
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 响应,实盘前务必在策略测试器跑一遍资源占用。

MQL5 / C++
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 之前完成集合预热。

MQL5 / C++
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 异常,属高风险操作,建议先在模拟盘验证。

MQL5 / C++
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 阈值就能验证命中判定是否符合盘面手感。

MQL5 / C++
  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 属高风险操作,参数误改可能导致对象不可见。

MQL5 / C++
   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 响应,建议在模拟盘先跑。

MQL5 / C++
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 改两行坐标参数,就能把一堆杂乱指标收进一个面板——少画几条线,反而更容易盯住关键价位。

常见问题

用无参构造入口直接新建空白 CPanel 实例,再手动往里加控件,适合完全自定义布局的场景。
通过字体样式接口分别设下划线为 true、删除线为 false,两个标志位独立切换互不影响。
可以,把面板代码贴给小布,它会按枚举切换逻辑帮你核对粗细状态映射是否反了,并给出修正建议。
先确认图元已加入图形对象集合类,再用集合的按图表与名称检索接口查,漏注册就会返回空索引。
走枚举切换逻辑,把中间权重值映射到对应枚举项再设进面板,不要直接写数字避免越界返回错误码。