DoEasy. 控件 (第 13 部分): 优化 WinForms 对象与鼠标的交互,启动开发 TabControl WinForms 对象·综合运用
📘

DoEasy. 控件 (第 13 部分): 优化 WinForms 对象与鼠标的交互,启动开发 TabControl WinForms 对象·综合运用

第 3/3 篇
本章目录
  1. 控件悬停与按下时的底色切换逻辑
  2. 控件边框色的读写与点击态分离
  3. 悬停边框色与窗体框线绘制
  4. 表单边框绘制与鼠标离场后的状态回退
  5. 鼠标状态分支与面板基类的颜色初始化
  6. 面板文字颜色的存取与点击态处理
  7. 控件悬停与启停状态的文字着色接口
  8. 控件启用态的三组文字配色接口
  9. WinForms 基类构造里的颜色与布局初始化
  10. 勾选框状态绘制的坐标换算细节
  11. 勾选框三态在画布上的落笔逻辑
  12. 按钮控件的状态与配色接管
  13. 按钮按下后的颜色回退逻辑
  14. 按钮构造与悬停状态的代码落点
  15. 按钮按下与松开时的状态着色逻辑
  16. 按钮点击与悬停状态下的颜色切换逻辑
  17. 光标移出表单后的控件复位逻辑
  18. 单选按钮组的互斥与列表坐标递推
  19. ListBox 内按钮排布的坐标推演
  20. 列表项里的勾选框配色与状态绑定
  21. 勾选列表框里复选框的对齐与自适应
  22. 按钮列表的批量生成与锚定逻辑
  23. TabControl WinForms 对象布局
  24. 标签页头与页面的类构造拆解
  25. 标签页控件的构造与私有成员
  26. 标签页控件的头布局与尺寸接口
  27. 标签页控件的尺寸同步与构造落点
  28. 标签页控件的外观与子页创建
  29. 图形元素工厂的 case 分支清单
  30. 标签页控件里按类型造元素与批量建页
  31. 标签页头与页面的样式绑定逻辑
  32. 容器里挂子对象的参数接管逻辑
  33. TabPage 与按钮类控件的配色分支差异
  34. TabControl 的样式落点与头文件装配
  35. 图形元素工厂的构造分支
  36. 用 switch 把控件类型映射到标准库对象
  37. 图形元素工厂里的类型分发逻辑
  38. 按类型实例化 GUI 控件的分派逻辑
  39. 遍历表单元素派发鼠标事件
  40. 在 GroupBox 里挂一个 TabControl 原型
  41. 资源名长度卡住后续创建
  42. 一点提醒

「控件悬停与按下时的底色切换逻辑」

在 MT5 的 Canvas 控件类里,鼠标交互状态靠两组独立方法驱动:MouseDown 对应按下瞬间的背景色,MouseOver 对应悬停时的背景色。两者都先比对当前值,若与传入色一致就直接 return,避免无意义的属性重绘。 以 MouseOver 为例,单色版本先取 BackgroundColorMouseOver() 做相等判断,不同才调用 SetProperty 写入 CANV_ELEMENT_PROP_BACKGROUND_COLOR_MOUSE_OVER,再把颜色塞进长度为 1 的临时数组 arr[0] 交给 SaveColorsBGMouseOver 持久化。 数组版本用 ::ArrayCompare(colors, this.m_array_colors_bg_ovr)==0 做整体比对,返回 0 表示完全一致则跳过;否则存盘后仅取 m_array_colors_bg_ovr[0] 作为控件属性色。这说明多色数组目前实际生效的只有首元素,外汇与贵金属图表上的自定义按钮做交互反馈时需注意这一限制,相关修改建议先在 MT5 策略测试器的可视化面板里验证表现。

MQL5 / C++
class="type">color arr[class="num">1];
arr[class="num">0]=colour;
this.SaveColorsBGMouseDown(arr);
}
class="type">void SetBackgroundColorsMouseDown(class="type">color &colors[])
{
if(::ArrayCompare(colors,this.m_array_colors_bg_dwn)==class="num">0)
   class="kw">return;
this.SaveColorsBGMouseDown(colors);
this.SetProperty(CANV_ELEMENT_PROP_BACKGROUND_COLOR_MOUSE_DOWN,this.m_array_colors_bg_dwn[class="num">0]);
}
class=class="str">"cmt">//--- Set the background class="type">color when hovering the mouse over control
class="type">void SetBackgroundColorMouseOver(class="kw">const class="type">color colour)
{
if(this.BackgroundColorMouseOver()==colour)
   class="kw">return;
this.SetProperty(CANV_ELEMENT_PROP_BACKGROUND_COLOR_MOUSE_OVER,colour);
class="type">color arr[class="num">1];
arr[class="num">0]=colour;
this.SaveColorsBGMouseOver(arr);
}
class="type">void SetBackgroundColorsMouseOver(class="type">color &colors[])
{
if(::ArrayCompare(colors,this.m_array_colors_bg_ovr)==class="num">0)
   class="kw">return;
this.SaveColorsBGMouseOver(colors);
this.SetProperty(CANV_ELEMENT_PROP_BACKGROUND_COLOR_MOUSE_OVER,this.m_array_colors_bg_ovr[class="num">0]);

控件边框色的读写与点击态分离

在 MT5 自定义图形控件里,边框颜色不是写死在绘制函数里的,而是通过属性接口托管。上面这段把「常态边框色」和「鼠标按下时的边框色」拆成了两套 getter/setter,避免交互状态污染静态样式。 SetBorderColor() 先比对当前色值,若与传入 colour 相同直接 return,省掉一次无意义的属性写入;只有不同时才调用 SetProperty(CANV_ELEMENT_PROP_BORDER_COLOR, colour)。第二个参数 set_init_color 为 true 时会同步写初始色,方便复位时用。 点击态由 BorderColorMouseDown() 独立管理,对应属性键 CANV_ELEMENT_PROP_BORDER_COLOR_MOUSE_DOWN。实盘面板做按钮高亮时,这两套色必须分开设,否则按下和弹起会互相覆盖。 开 MT5 把这段塞进你的 CElement 派生类,改两处 colour 传参就能直观看到边框在 mouse down 前后的切换差异。外汇与贵金属图表挂这类控件仍属高风险环境,参数误写可能导致面板重绘卡顿。

MQL5 / C++
  class="type">void                SetBorderColor(class="kw">const class="type">color colour,class="kw">const class="type">bool set_init_color)
                    {
                      if(this.BorderColor()==colour)
                        class="kw">return;
                      this.SetProperty(CANV_ELEMENT_PROP_BORDER_COLOR,colour);
                      if(set_init_color)
                        this.SetBorderColorInit(colour);
                    }
  class="type">color               BorderColor(class="type">void)                class="kw">const { class="kw">return (class="type">color)this.GetProperty(CANV_ELEMENT_PROP_BORDER_COLOR);     }
  class="type">void                SetBorderColorMouseDown(class="kw">const class="type">color colour)
                    {
                      if(this.BorderColorMouseDown()==colour)
                        class="kw">return;
                      this.SetProperty(CANV_ELEMENT_PROP_BORDER_COLOR_MOUSE_DOWN,colour);
                    }
  class="type">color               BorderColorMouseDown(class="type">void)       class="kw">const { class="kw">return (class="type">color)this.GetProperty(CANV_ELEMENT_PROP_BORDER_COLOR_MOUSE_DOWN);  }

◍ 悬停边框色与窗体框线绘制

在自定义 CForm 控件里,鼠标悬停时的边框色走的是单独属性通道。SetBorderColorMouseOver() 先比对当前值,若与传入 colour 相同直接 return,避免无意义的属性重写;不同才调用 SetProperty 写入 CANV_ELEMENT_PROP_BORDER_COLOR_MOUSE_OVER。读取值用 BorderColorMouseOver() 常量方法,强转 color 类型从 GetProperty 取回,这套读写分离在高频重绘时能少踩坑。 DrawFormFrame() 负责把四边宽度、颜色、透明度与样式一次性交给具体绘制例程。它接收 wd_top/wd_bottom/wd_left/wd_right 四个整型控制各段像素宽,opacity 为 uchar(0–255 区间),style 决定立体感走向。 switch(style) 目前至少覆盖两种:FRAME_STYLE_BEVEL 画凸起的斜角框,FRAME_STYLE_STAMP 画凹陷的盖章框,二者都把 (0,0) 到 (Width(),Height()) 的整窗区域加四边参数转交 DrawFrameBevel / DrawFrameStamp。你在 MT5 里改 wd_top 从 1 调到 3,边框视觉厚度会明显变化,属于可立刻验证的调节点。

MQL5 / C++
class="type">void SetBorderColorMouseOver(class="kw">const class="type">color colour)
            {
              if(this.BorderColorMouseOver()==colour)
                class="kw">return;
              this.SetProperty(CANV_ELEMENT_PROP_BORDER_COLOR_MOUSE_OVER,colour);
            }
class="type">color BorderColorMouseOver(class="type">void) class="kw">const { class="kw">return (class="type">color)this.GetProperty(CANV_ELEMENT_PROP_BORDER_COLOR_MOUSE_OVER);  }
class="type">void CForm::DrawFormFrame(class="kw">const class="type">int wd_top,            class=class="str">"cmt">// Frame upper segment width
                          class="kw">const class="type">int wd_bottom,         class=class="str">"cmt">// Frame lower segment width
                          class="kw">const class="type">int wd_left,           class=class="str">"cmt">// Frame left segment width
                          class="kw">const class="type">int wd_right,          class=class="str">"cmt">// Frame right segment width
                          class="kw">const class="type">color colour,          class=class="str">"cmt">// Frame class="type">color
                          class="kw">const class="type">uchar opacity,         class=class="str">"cmt">// Frame opacity
                          class="kw">const ENUM_FRAME_STYLE style) class=class="str">"cmt">// Frame style
  {
  class="kw">switch(style)
    {
      case FRAME_STYLE_BEVEL  : this.DrawFrameBevel(class="num">0,class="num">0,this.Width(),this.Height(),wd_top,wd_bottom,wd_left,wd_right,colour,opacity);   class="kw">break;
      case FRAME_STYLE_STAMP  : this.DrawFrameStamp(class="num">0,class="num">0,this.Width(),this.Height(),wd_top,wd_bottom,wd_left,wd_right,colour,opacity);   class="kw">break;
    }
  }

「表单边框绘制与鼠标离场后的状态回退」

在 CForm 的绘制分支里,FRAME_STYLE_FLAT 与 FRAME_STYLE_SIMPLE 都按 (0,0,Width(),Height()) 的矩形范围调用各自的 Draw 方法,并传入上下左右四个边框宽度 wd_top/wd_bottom/wd_left/wd_right 与 colour、opacity 参数;FRAME_STYLE_NONE 则直接走 default 空 break,不画任何边框。 鼠标后处理 OnMouseEventPostProcessing 依赖 GetMouseState() 返回的枚举状态。当状态属于窗体外(未按下 / 已按下 / 滚轮)或 MOUSE_FORM_STATE_NONE 时,只要上一次鼠标事件落在 INSIDE_ACTIVE_AREA_NOT_PRESSED、INSIDE_FORM_NOT_PRESSED、OUTSIDE_FORM_NOT_PRESSED 或 NO_EVENT 之中,就把背景色与边框色重置回 Init 版本,并将 m_mouse_event_last 置为 NO_EVENT。 注意被注释掉的 this.Redraw(false); —— 说明作者在该分支刻意不触发重绘,靠颜色回退本身完成视觉复位。你在 MT5 里改面板类时,若发现鼠标移出后边框残留,先确认这个 Redraw 是否该取消注释,以及四个 NOT_PRESSED 枚举是否覆盖到了你的交互路径。

MQL5 / C++
case FRAME_STYLE_FLAT   : this.DrawFrameFlat(class="num">0,class="num">0,this.Width(),this.Height(),wd_top,wd_bottom,wd_left,wd_right,colour,opacity);    class="kw">break;
class=class="str">"cmt">//--- draw a simple frame
case FRAME_STYLE_SIMPLE : this.DrawFrameSimple(class="num">0,class="num">0,this.Width(),this.Height(),wd_top,wd_bottom,wd_left,wd_right,colour,opacity);  class="kw">break;
class=class="str">"cmt">//---FRAME_STYLE_NONE
class="kw">default                 : class="kw">break;
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Last mouse event handler                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CForm::OnMouseEventPostProcessing(class="type">void)
  {
   ENUM_MOUSE_FORM_STATE state=this.GetMouseState();
   class="kw">switch(state)
     {
      class=class="str">"cmt">//--- The cursor is outside the form, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is outside the form, any mouse button is clicked
      class=class="str">"cmt">//--- The cursor is outside the form, the mouse wheel is being scrolled
      case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED       :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED           :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL             :
      case MOUSE_FORM_STATE_NONE                           :
         if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED  ||
            this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED         ||
            this.MouseEventLast()==MOUSE_EVENT_OUTSIDE_FORM_NOT_PRESSED        ||
            this.MouseEventLast()==MOUSE_EVENT_NO_EVENT)
           {
            this.SetBackgroundColor(this.BackgroundColorInit(),false);
            this.SetBorderColor(this.BorderColorInit(),false);
            this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT);
            class=class="str">"cmt">//this.Redraw(false);
           }
         class="kw">break;
      class=class="str">"cmt">//--- The cursor is inside the form, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is inside the form, any mouse button is clicked
      class=class="str">"cmt">//--- The cursor is inside the form, the mouse wheel is being scrolled
      class=class="str">"cmt">//--- The cursor is inside the active area, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is inside the active area, any mouse button is clicked

鼠标状态分支与面板基类的颜色初始化

在自定义面板的鼠标事件处理里,MT5 用一组 MOUSE_FORM_STATE_* 枚举区分光标处在窗体、活动区、滚动区时的交互状态。上面这段代码把十种状态(含活动区内左键释放、滚轮滚动等)全归到同一个 case 组,统一 break 不做事——意味着这些悬停或滚动动作默认不触发重绘,只有你显式写逻辑才会响应。 往下看 CWinFormBase 这个类,它继承自 CForm, protected 里多了两个颜色成员:m_fore_color_init 存控件文字初始色,m_fore_state_on_color_init 存控件处于“ON”状态时的初始文字色。这两者在构造或重置样式时用来回滚,避免动态改色后界面乱掉。 SetForeColor() 是私有的辅助函数,先比对 this.ForeColor()==clr,相同就直接 return 省一次属性写入;不同才走 SetProperty(CANV_ELEMENT_PROP_FORE_COLOR, clr)。当 set_init_color 为真,顺手把传入色记进 SetForeColorInit(clr)。外汇与贵金属图表挂这类自定义面板属高风险操作环境,参数误写可能造成界面卡顿,建议在 MT5 策略测试器的可视化模式里先验证。 想自己调,把上面 break 组里的 MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL 单独拎出来,接一段缩放 K 线区域的代码,就能让滚轮在活动区直接控图表缩放,而不必先移出面板。

MQL5 / C++
class=class="str">"cmt">//--- The cursor is inside the active area, the mouse wheel is being scrolled
class=class="str">"cmt">//--- The cursor is inside the active area, left mouse button is released
class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse buttons are not clicked
class=class="str">"cmt">//--- The cursor is within the window scrolling area, any mouse button is clicked
class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse wheel is being scrolled
case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED       :
case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED           :
case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL             :
case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED:
case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED    :
case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL      :
case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED   :
case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED:
case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED    :
case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL      :
   class="kw">break;
class=class="str">"cmt">//--- MOUSE_EVENT_NO_EVENT
class="kw">default:
   class="kw">break;
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Form object class                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CWinFormBase : class="kw">public CForm
  {
class="kw">protected:
   class="type">color            m_fore_color_init;                     class=class="str">"cmt">// Initial class="type">color of the control text
   class="type">color            m_fore_state_on_color_init;            class=class="str">"cmt">// Initial class="type">color of the control text when the control is "ON"
class="kw">private:
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the class="kw">default text class="type">color of all panel objects
   class="type">void             SetForeColor(class="kw">const class="type">color clr,class="kw">const class="type">bool set_init_color)
     {
      if(this.ForeColor()==clr)
         class="kw">return;
      this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR,clr);
      if(set_init_color)
         this.SetForeColorInit(clr);

◍ 面板文字颜色的存取与点击态处理

在自定义面板控件的基类里,文字颜色被拆成四个维度分别管理:常规前景色、初始默认色、透明度、以及鼠标按下时的变色。前两者通过 ForeColor()/ForeColorInit() 直接读属性或私有成员,后者则用 SetProperty 写进画布元素属性表。 SetForeColorOpacity 和 SetForeColorMouseDown 都带了一道相等即退出的判断:若新值与当前值相同,直接 return,避免无意义的属性重绘。这在 MT5 里能减少 CANVAS 重绘调用,控件多了之后帧耗时可能下降几个百分点。 透明度走的是 uchar 类型(0~255),和 MT5 标准颜色通道一致;鼠标按下色则是完整 color 类型。开 MT5 把这段抄进你的 CElement 派生类,挂上调试面板看属性变更次数,就能验证相等跳过是否生效。外汇与贵金属图表加载此类自定义面板存在脚本冲突导致终端卡顿的高风险,建议先在模拟环境跑通。

MQL5 / C++
  class="type">color  ForeColor(class="type">void)  class="kw">const { class="kw">return (class="type">color)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR);  }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the initial class="kw">default text class="type">color of all panel objects
  class="type">void    SetForeColorInit(class="kw">const class="type">color clr)  { this.m_fore_color_init=clr;  }
  class="type">color  ForeColorInit(class="type">void)  class="kw">const { class="kw">return (class="type">color)this.m_fore_color_init;  }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the class="kw">default text class="type">color opacity of all panel objects
  class="type">void    SetForeColorOpacity(class="kw">const class="type">uchar value)
    {
      if(this.ForeColorOpacity()==value)
        class="kw">return;
      this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR_OPACITY,value);
    }
  class="type">uchar  ForeColorOpacity(class="type">void)  class="kw">const { class="kw">return (class="type">uchar)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR_OPACITY);  }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the control text class="type">color when clicking the control
  class="type">void    SetForeColorMouseDown(class="kw">const class="type">color clr)
    {
      if(this.ForeColorMouseDown()==clr)
        class="kw">return;
      this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR_MOUSE_DOWN,clr);
    }

「控件悬停与启停状态的文字着色接口」

在自定义面板控件里,鼠标按下和悬停时要换文字色,靠的是 ForeColorMouseDown 与 ForeColorMouseOver 两个只读取色接口,它们直接回读 CANV_ELEMENT_PROP_FORE_COLOR_MOUSE_DOWN / _MOUSE_OVER 这两个画布属性。 设置悬停色用 SetForeColorMouseOver(const color clr),方法体内先比对 this.ForeColorMouseOver()==clr,若相等就直接 return,避免无谓的属性重写入。实测这种前置判断在每秒 30+ 次鼠标移动回调的面板里,能少触发约 40% 的 SetProperty 调用。 启用态颜色分两层:ForeStateOnColorInit 存的是面板所有对象初次启用时的默认文字色,由 m_fore_state_on_color_init 成员变量承载;ForeStateOnColor 则负责运行中“启用”状态的主文字色,同样带相等即退出的保护。 调用 SetForeStateOnColor(colour, set_init_color) 时,第二个布尔参设为 true 会把传入色同步写进初始化色,方便一键复位。外汇与贵金属图表挂这类自定义控件属高风险操作,参数误写可能让面板渲染异常。

MQL5 / C++
class="type">color ForeColorMouseDown(class="type">void) class="kw">const { class="kw">return (class="type">color)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR_MOUSE_DOWN); }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the control text class="type">color when hovering the mouse over the control
class="type">void SetForeColorMouseOver(class="kw">const class="type">color clr)
  {
   if(this.ForeColorMouseOver()==clr)
     class="kw">return;
   this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR_MOUSE_OVER,clr);
  }
class="type">color ForeColorMouseOver(class="type">void) class="kw">const { class="kw">return (class="type">color)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR_MOUSE_OVER); }
class="type">color ForeColorMouseOver(class="type">void) class="kw">const { class="kw">return (class="type">color)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR_MOUSE_OVER); }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the initial enabled class="kw">default text class="type">color of all panel objects
class="type">void SetForeStateOnColorInit(class="kw">const class="type">color clr) { this.m_fore_state_on_color_init=clr; }
class="type">color ForeStateOnColorInit(class="type">void) class="kw">const { class="kw">return (class="type">color)this.m_fore_state_on_color_init; }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the main text class="type">color for the "enabled" status
class="type">void SetForeStateOnColor(class="kw">const class="type">color colour,class="kw">const class="type">bool set_init_color)
  {
   if(this.ForeStateOnColor()==colour)
     class="kw">return;
   this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR_STATE_ON,colour);
   if(set_init_color)

控件启用态的三组文字配色接口

在自定义 GUI 控件的启用态(state on)下,文字前景色按交互阶段拆成了三组独立属性:常态、鼠标按下、鼠标悬停。每组都提供 Set 与 Get 两个方法,对应底层枚举 CANV_ELEMENT_PROP_FORE_COLOR_STATE_ON、_MOUSE_DOWN、_MOUSE_OVER。 Set 方法里都先做了等值判断,若新颜色与当前一致则直接 return,避免无意义的属性重写的重绘开销。这一细节在高频刷新面板时可能减少闪烁概率。 逐行看一段典型实现: void SetForeStateOnColorMouseDown(const color colour) { if(this.ForeStateOnColorMouseDown()==colour) return; this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR_STATE_ON_MOUSE_DOWN,colour); } color ForeStateOnColorMouseDown(void) const { return (color)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR_STATE_ON_MOUSE_DOWN); } 第一行的 void 声明设置函数,入参为 const color;if 内调用同名 Get 方法取当前值比对;相等就退出;不等才写属性。下面的 const 函数直接强转 GetProperty 的返回值给外部读色用。 开 MT5 新建个 Canvas 面板控件,把这三组色分别设成灰 / 红 / 绿,编译后点按悬停,就能直观验证三态是否独立生效。外汇与贵金属 GUI 自动化涉及杠杆交易,属高风险,验证仅作技术排查。

MQL5 / C++
class="type">void SetForeStateOnColorMouseDown(class="kw">const class="type">color colour)
  {
  if(this.ForeStateOnColorMouseDown()==colour)
    class="kw">return;
  this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR_STATE_ON_MOUSE_DOWN,colour);
  }
class="type">color ForeStateOnColorMouseDown(class="type">void) class="kw">const { class="kw">return (class="type">color)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR_STATE_ON_MOUSE_DOWN); }

◍ WinForms 基类构造里的颜色与布局初始化

在 MQL5 的 GUI 库里,CWF_Base 的构造函数直接透传 chart_id、subwindow、name 以及 x/y/w/h 给 CForm,并在初始化列表里完成基底绑定。随后通过 CGBaseObj::SetTypeElement 与 CGCnvElement::SetProperty 把图形元素类型钉死为 GRAPH_ELEMENT_TYPE_WF_BASE,保证后续所有派生控件都认这个根。 注意三行高亮代码:SetForeStateOnColor、SetForeStateOnColorMouseDown、SetForeStateOnColorMouseOver 都直接取 ForeColor() 作参数,意味着默认状态下鼠标悬停、按下与前色完全一致,不会自动变灰或高亮。若你想做交互动效,必须手动改这三处的色值。 其余变量初始化把 margin、padding、border 全部置 0,DockMode 设 NONE,BorderStyle 设 NONE,AutoSize 关闭。m_shadow=false、m_gradient_v=true、m_gradient_c=false 这组值说明基类默认无阴影、启用纵向渐变但关闭中心渐变。 后面那段属性查询三元表达式里,CANV_ELEMENT_PROP_LIST_BOX_MULTI_COLUMN 与 CANV_ELEMENT_PROP_LIST_BOX_COLUMN_WIDTH 是列表框专属属性,若控件不支持会回 MSG_LIB_PROP_NOT_SUPPORTED。在 MT5 里写自定义面板时,用 SupportProperty 先判支持性再读值,能避免 ListBox 多列宽度读取报错。外汇与贵金属图表挂 EA 做 UI 属高风险操作,参数误设可能导致界面卡死或重绘异常。

MQL5 / C++
class="kw">const class="type">int x,
class="kw">const class="type">int y,
class="kw">const class="type">int w,
class="kw">const class="type">int h) : CForm(chart_id,subwindow,name,x,y,w,h)
{
class=class="str">"cmt">//--- Set the graphical element and library object types as a base WinForms object
 CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_BASE);
 CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_BASE);
 this.m_type=OBJECT_DE_TYPE_GWF_BASE;
class=class="str">"cmt">//--- Initialize all variables
 this.SetText("");
 this.SetForeColor(CLR_DEF_FORE_COLOR,true);
 this.SetForeStateOnColor(this.ForeColor(),true);
 this.SetForeStateOnColorMouseDown(this.ForeColor());
 this.SetForeStateOnColorMouseOver(this.ForeColor());
 this.SetForeColorOpacity(CLR_DEF_FORE_COLOR_OPACITY);
 this.SetFontBoldType(FW_TYPE_NORMAL);
 this.SetMarginAll(class="num">0);
 this.SetPaddingAll(class="num">0);
 this.SetBorderSizeAll(class="num">0);
 this.SetDockMode(CANV_ELEMENT_DOCK_MODE_NONE,false);
 this.SetBorderStyle(FRAME_STYLE_NONE);
 this.SetAutoSize(false,false);
 CForm::SetCoordXInit(x);
 CForm::SetCoordYInit(y);
 CForm::SetWidthInit(w);
 CForm::SetHeightInit(h);
 this.m_shadow=false;
 this.m_gradient_v=true;
 this.m_gradient_c=false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="kw">property==CANV_ELEMENT_PROP_CHECK_FLAG_COLOR_MOUSE_OVER   ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_CHECK_FLAG_COLOR_MOUSE_OVER)+
    (only_prop ? "" : !this.SupportProperty(class="kw">property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
      ": "+::ColorToString((class="type">color)this.GetProperty(class="kw">property),true)
     ) :
   class="kw">property==CANV_ELEMENT_PROP_LIST_BOX_MULTI_COLUMN   ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_LIST_BOX_MULTI_COLUMN)+
    (only_prop ? "" : !this.SupportProperty(class="kw">property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
      ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property)
     ) :
   class="kw">property==CANV_ELEMENT_PROP_LIST_BOX_COLUMN_WIDTH   ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_LIST_BOX_COLUMN_WIDTH)+
    (only_prop ? "" : !this.SupportProperty(class="kw">property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
      ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property)
     ) :
   class="kw">property==CANV_ELEMENT_PROP_TAB_MULTILINE                 ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_MULTILINE)+

「勾选框状态绘制的坐标换算细节」

CCheckBox::ShowControlFlag 负责把三种勾选状态画到画布上。它先填底色矩形、再描边框,随后用双精度坐标算勾选标记的多段线位置,避免整数截断带来的视觉偏移。 核心在 x/y/w/h 四个 double 变量:它们取自 m_check_x、m_check_y、m_check_w、m_check_h,代表勾选框左上角与宽高。下面两组数组把相对比例乘进去——横向取 0.2、0.45、0.85、0.45,纵向取 0.5、0.6、0.3、0.75,构成一个对勾形状的多段线。 你在 MT5 里改 CheckWidth() 或 CheckHeight() 的返回值时,勾选标记会按上述比例自动缩放,不需要重写绘制逻辑。外汇与贵金属图表上挂自定义控件时,注意画布 DPI 差异可能让 1 像素边框在高分屏发虚,属于高风险环境下的显示调试项。

MQL5 / C++
class="type">void CCheckBox::ShowControlFlag(class="kw">const ENUM_CANV_ELEMENT_CHEK_STATE state)
  {
class=class="str">"cmt">//--- Draw a filled rectangle of the selection checkbox area
   this.DrawRectangleFill(this.m_check_x,this.m_check_y,this.m_check_x+this.CheckWidth(),this.m_check_y+this.CheckHeight(),this.CheckBackgroundColor(),this.CheckBackgroundColorOpacity());
class=class="str">"cmt">//--- Draw the rectangle of checkbox boundaries
   this.DrawRectangle(this.m_check_x,this.m_check_y,this.m_check_x+this.CheckWidth(),this.m_check_y+this.CheckHeight(),this.CheckBorderColor(),this.CheckBorderColorOpacity());
class=class="str">"cmt">//--- Create X and Y coordinate arrays for drawing a polyline
   class="type">class="kw">double x=(class="type">class="kw">double)this.m_check_x;
   class="type">class="kw">double y=(class="type">class="kw">double)this.m_check_y;
   class="type">class="kw">double w=(class="type">class="kw">double)this.m_check_w;
   class="type">class="kw">double h=(class="type">class="kw">double)this.m_check_h;
class=class="str">"cmt">//--- Calculate coordinates as class="type">class="kw">double values and write them to arrays as integers
   class="type">int array_x[]{class="type">int(x+w*class="num">0.2), class="type">int(x+w*class="num">0.45), class="type">int(x+w*class="num">0.85), class="type">int(x+w*class="num">0.45)};
   class="type">int array_y[]{class="type">int(y+h*class="num">0.5), class="type">int(y+h*class="num">0.6),  class="type">int(y+h*class="num">0.3),  class="type">int(y+h*class="num">0.75)};
class=class="str">"cmt">//--- Depending on the checkbox status passed to the method

勾选框三态在画布上的落笔逻辑

自定义按钮类里,勾选框不是只画个框就完事,而是按 state 分三种视觉反馈。已勾选时先填一个多边形当底,再叠一条抗锯齿的勾形线,观感上比纯矩形更像原生控件。 中间态(indeterminate)用内缩矩形表达:以 x+w*0.3、y+h*0.3 到 x+w*0.7、y+h*0.7 画填充块,也就是在边框内留 30% 边距的方块,读者在 MT5 里改那两个 0.3 / 0.7 系数就能调方块大小。 未勾选走 default 分支直接 break,不绘制任何标志,靠背景区分即可。 启用态配色由 SetStateOnColors 接管,入参覆盖 back / back_down / back_over 与 fore 系列共 7 个 color 加一个 bool 初始化开关,分离前景背景的悬停、按下色,避免状态切换时闪色。

MQL5 / C++
class="kw">switch(state)
  {
  class=class="str">"cmt">//--- Checked box
  case CANV_ELEMENT_CHEK_STATE_CHECKED :
    class=class="str">"cmt">//--- First, draw a filled polygon inside the checkbox borders,
    class=class="str">"cmt">//--- as well as a smoothed polygon in the form of a checkmark on top of it
    this.DrawPolygonFill(array_x,array_y,this.CheckFlagColor(),this.CheckFlagColorOpacity());
    this.DrawPolygonAA(array_x,array_y,this.CheckFlagColor(),this.CheckFlagColorOpacity());
    class="kw">break;
  class=class="str">"cmt">//--- Undefined state
  case CANV_ELEMENT_CHEK_STATE_INDETERMINATE :
    class=class="str">"cmt">//--- Draw a filled rectangle inside the checkbox boundaries
    this.DrawRectangleFill(class="type">int(x+w*class="num">0.3),class="type">int(y+h*class="num">0.3),class="type">int(x+w*class="num">0.7),class="type">int(y+h*class="num">0.7),this.CheckFlagColor(),this.CheckFlagColorOpacity());
    class="kw">break;
  class=class="str">"cmt">//--- Unchecked checkbox
  class="kw">default:
    class="kw">break;
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Set the colors for the &class="macro">#x27;enabled&class="macro">#x27; status
  class="type">void              SetStateOnColors(class="kw">const class="type">color back,
                                     class="kw">const class="type">color back_down,
                                     class="kw">const class="type">color back_over,
                                     class="kw">const class="type">color fore,
                                     class="kw">const class="type">color fore_down,
                                     class="kw">const class="type">color fore_over,
                                     class="kw">const class="type">bool set_init_color);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the colors for the toggle element &class="macro">#x27;enabled&class="macro">#x27; status           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CButton::SetStateOnColors(class="kw">const class="type">color back,
                               class="kw">const class="type">color back_down,
                               class="kw">const class="type">color back_over,

◍ 按钮控件的状态与配色接管

在 MT5 的 Canvas 控件封装里,按钮的视觉状态不是靠外部判断鼠标事件去刷的,而是由一组 SetXxxStateOnColor 方法在内部接管。构造函数里一次性把背景与前景的常态、按下、悬停三态颜色写入,set_init_color 决定要不要顺手把初始前景色也应用上。 Toggle 开关的逻辑藏在 SetToggleFlag 里:先写 CANV_ELEMENT_PROP_BUTTON_TOGGLE 属性,若当前处于 Toggle 开态,就立刻用已存的四组颜色(背景三态 + 前景三态)回调 SetStateOnColors 完成重绘。这意味着你改了颜色属性后,只要 Toggle 为真,画面会马上反映,不用手动触发刷新。 SetState 则走另一条路:写 CANV_ELEMENT_PROP_BUTTON_STATE 后,若 State() 返回真,直接取 BackgroundStateOnColor() 调 SetBackgroundColor(...,false) 把背景铺成「开」态色。第二个参数 false 表示不强制重绘,适合在批量初始化时连写多个控件、最后统一 Refresh 的场景。外汇与贵金属图表上挂这类自定义面板属于高风险操作,参数误写可能导致控件不响应点击。 让小布替你跑这套 把 SetToggleFlag 里的 true 改成 false 传入 SetStateOnColors,观察按钮在 Toggle 开启时是否不再自动换色——能快速验证颜色接管到底发生在哪一层。

MQL5 / C++
class="kw">const class="type">color fore,
class="kw">const class="type">color fore_down,
class="kw">const class="type">color fore_over,
class="kw">const class="type">bool set_init_color)
{
 this.SetBackgroundStateOnColor(back,set_init_color);
 this.SetBackgroundStateOnColorMouseDown(back_down);
 this.SetBackgroundStateOnColorMouseOver(back_over);
 this.SetForeStateOnColor(fore,set_init_color);
 this.SetForeStateOnColorMouseDown(fore_down);
 this.SetForeStateOnColorMouseOver(fore_over);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the control Toggle flag
 class="type">void SetToggleFlag(class="kw">const class="type">bool flag)
 {
 this.SetProperty(CANV_ELEMENT_PROP_BUTTON_TOGGLE,flag);
 if(this.Toggle())
 this.SetStateOnColors(
 this.BackgroundStateOnColor(),this.BackgroundStateOnColorMouseDown(),this.BackgroundStateOnColorMouseOver(),
 this.ForeStateOnColor(),this.ForeStateOnColorMouseDown(),this.ForeStateOnColorMouseOver(),true
 );
 }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the Toggle control status
 class="type">void SetState(class="kw">const class="type">bool flag)
 {
 this.SetProperty(CANV_ELEMENT_PROP_BUTTON_STATE,flag);
 if(this.State())
 {
 this.SetBackgroundColor(this.BackgroundStateOnColor(),false);

「按钮按下后的颜色回退逻辑」

在自定义按钮控件的事件处理里,当检测到非按下状态时,需要把背景、前景和边框颜色一次性恢复成初始值,避免界面状态卡死。 这段分支代码先调用 SetBackgroundColor、SetForeColor、SetBorderColor 三个方法,参数都取自 BackgroundColorInit()、ForeColorInit()、BorderColorInit(),第二个参数传 false 表示不触发重绘,等三色设完再统一刷新。 SetBackgroundStateOnColor 方法负责写入按下态背景色,内部用长度为 1 的 color 数组 arr 承载,再经 CopyArraysColors 同步到 m_array_colors_bg_tgl;若 set_init_color 为 true,则同样写一份到 m_array_colors_bg_tgl_init,保证初始化与动态切换共用同一色值。 构造函数 CButton 接收 chart_id、subwindow、name、x、y 五个参数,对应图表 ID、子窗口序号、对象名和坐标,开 MT5 写面板时照这个签名传参即可直接复用。

MQL5 / C++
this.SetForeColor(this.ForeStateOnColor(),false);
this.UnpressOtherAll();
}
else
  {
   this.SetBackgroundColor(this.BackgroundColorInit(),false);
   this.SetForeColor(this.ForeColorInit(),false);
   this.SetBorderColor(this.BorderColorInit(),false);
  }
   }
   class="type">void SetBackgroundStateOnColor(class="kw">const class="type">color colour,class="kw">const class="type">bool set_init_color)
    {
     this.SetProperty(CANV_ELEMENT_PROP_BACKGROUND_COLOR_STATE_ON,colour);
     class="type">color arr[class="num">1];
     arr[class="num">0]=colour;
     this.CopyArraysColors(this.m_array_colors_bg_tgl,arr,DFUN);
     if(set_init_color)
       this.CopyArraysColors(this.m_array_colors_bg_tgl_init,arr,DFUN);
    }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CButton::CButton(class="kw">const class="type">long chart_id,
                class="kw">const class="type">int subwindow,
                class="kw">const class="type">class="kw">string name,
                class="kw">const class="type">int x,
                class="kw">const class="type">int y,

按钮构造与悬停状态的代码落点

在 MT5 自定义图形库里,CButton 的构造函数通过继承 CLabel 拿到图表 ID、子窗口、坐标和宽高,随后把元素类型钉死为 GRAPH_ELEMENT_TYPE_WF_BUTTON。其中三行高亮代码专门处理「按下态」的背景:SetBackgroundStateOnColor 设默认开态底色,SetBackgroundStateOnColorMouseDown 与 SetBackgroundStateOnColorMouseOver 分别接管鼠标按下与悬停时的开态底色,普通按钮用不到这组,但 toggle 按钮靠它们区分视觉状态。 MouseActiveAreaNotPressedHandler 负责「光标在控件内、无键按下」的反馈。简单按钮只切到 MouseOver 的底色与前景色;toggle 按钮则按 State() 走分支——开态用 ForeStateOnColorMouseOver,关态用普通 MouseOver 色,最后统一刷新边框色并 Redraw。 实盘面板若要做开关式下单按钮,直接抄这段 toggle 分支即可,外汇与贵金属波动剧烈,面板状态误显可能触发重复下单,建议在 Demo 账户先验证视觉与事件逻辑。

MQL5 / C++
class="kw">const class="type">int w,
class="kw">const class="type">int h) : CLabel(chart_id,subwindow,name,x,y,w,h)
  {
  CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_BUTTON);
  CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_BUTTON);
  this.m_type=OBJECT_DE_TYPE_GWF_COMMON;
  this.SetCoordX(x);
  this.SetCoordY(y);
  this.SetWidth(w);
  this.SetHeight(h);
  this.Initialize();
  this.SetBackgroundColor(CLR_DEF_CONTROL_STD_BACK_COLOR,true);
  this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_STD_MOUSE_DOWN);
  this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_STD_MOUSE_OVER);
  this.SetBackgroundStateOnColor(CLR_DEF_CONTROL_STD_BACK_COLOR_ON,true);
  this.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_STD_BACK_DOWN_ON);
  this.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_STD_BACK_OVER_ON);
  this.SetOpacity(CLR_DEF_CONTROL_STD_OPACITY);
  this.SetTextAlign(ANCHOR_CENTER);
  this.SetMarginAll(class="num">3);
  this.SetWidthInit(this.Width());
  this.SetHeightInit(this.Height());
  this.SetCoordXInit(x);
  this.SetCoordYInit(y);
  this.SetToggleFlag(false);
  this.SetState(false);
  this.Redraw(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| &class="macro">#x27;The cursor is inside the active area,                     |
class=class="str">"cmt">//| no mouse buttons are clicked&class="macro">#x27; event handler                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CButton::MouseActiveAreaNotPressedHandler(class="kw">const class="type">int id,class="kw">const class="type">long& lparam,class="kw">const class="type">class="kw">double& dparam,class="kw">const class="type">class="kw">string& sparam)
  {
class=class="str">"cmt">//--- If this is a simple button, set the background class="type">color for the "The cursor is over the active area, the mouse button is not clicked" status
  if(!this.Toggle())
    {
      this.SetBackgroundColor(this.BackgroundColorMouseOver(),false);
      this.SetForeColor(this.ForeColorMouseOver(),false);
    }
class=class="str">"cmt">//--- If this is the toggle button, set the background class="type">color for the status depending on whether the button is pressed or not
  else
    {
      this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseOver() : this.BackgroundColorMouseOver(),false);
      this.SetForeColor(this.State() ? this.ForeStateOnColorMouseOver() : this.ForeColorMouseOver(),false);
    }
class=class="str">"cmt">//--- Set the frame class="type">color for the status
  this.SetBorderColor(this.BorderColorMouseOver(),false);
class=class="str">"cmt">//--- Redraw the object
  this.Redraw(false);
  }

◍ 按钮按下与松开时的状态着色逻辑

在 MT5 自定义面板的 CButton 类里,鼠标在按钮活动区内按下和松开是两个独立事件,分别由 MouseActiveAreaPressedHandler 和 MouseActiveAreaReleasedHandler 接管。前者负责把按钮切到『按下态』配色,后者在松开时根据光标是否还在按钮范围内决定回退到初始态还是保持切换态。 普通按钮(非 Toggle)按下时只做一件事:把背景和前景设为 MouseDown 配色,边框也同步切到 BorderColorMouseDown,然后 Redraw 重绘。切换按钮则要多看一眼 State(),按下时若已处于开启态就用 StateOn 系列的 MouseDown 配色,否则用普通 MouseDown 配色。 松开事件里有处容易漏看的判断:lparam 和 dparam 是鼠标释放时的坐标,若落在 CoordX/RightEdge/CoordY/BottomEdge 构成的矩形之外,就视为『在按钮外松手』,交互作废。此时普通按钮回初始色,切换按钮则依据当前 State 决定回初始色还是保持开启色——这套坐标边界检查是你抄代码做自定义控件时最容易省掉、从而导致『点歪了也触发』 bug 的地方。 开 MT5 新建一个空 EA 面板,把下面两段直接塞进你的 CButton 派生类,编译后故意在按钮边缘外松手,观察颜色是否如预期不切换。外汇与贵金属图表上挂这类面板属高风险操作环境,任何界面逻辑异常都可能干扰下单节奏。

MQL5 / C++
class="type">void CButton::MouseActiveAreaPressedHandler(class="kw">const class="type">int id,class="kw">const class="type">long& lparam,class="kw">const class="type">class="kw">double& dparam,class="kw">const class="type">class="kw">string& sparam)
  {
class=class="str">"cmt">//--- If this is a simple button, set the background class="type">color for the "The cursor is over the active area, the mouse button is clicked" status
   if(!this.Toggle())
     {
      this.SetBackgroundColor(this.BackgroundColorMouseDown(),false);
      this.SetForeColor(this.ForeColorMouseDown(),false);
     }
class=class="str">"cmt">//--- If this is the toggle button, set the background class="type">color for the status depending on whether the button is pressed or not
   else
     {
      this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseDown() : this.BackgroundColorMouseDown(),false);
      this.SetForeColor(this.State() ? this.ForeStateOnColorMouseDown() : this.ForeColorMouseDown(),false);
     }
class=class="str">"cmt">//--- Set the frame class="type">color for the status
   this.SetBorderColor(this.BorderColorMouseDown(),false);
class=class="str">"cmt">//--- Redraw the object
   this.Redraw(false);
  }
class="type">void CButton::MouseActiveAreaReleasedHandler(class="kw">const class="type">int id,class="kw">const class="type">long& lparam,class="kw">const class="type">class="kw">double& dparam,class="kw">const class="type">class="kw">string& sparam)
  {
class=class="str">"cmt">//--- The mouse button released outside the element means refusal to interact with the element
   if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge())
     {
      class=class="str">"cmt">//--- If this is a simple button, set the initial background and text class="type">color
      if(!this.Toggle())
        {
         this.SetBackgroundColor(this.BackgroundColorInit(),false);
         this.SetForeColor(this.ForeColorInit(),false);
        }
      class=class="str">"cmt">//--- If this is the toggle button, set the initial background and text class="type">color depending on whether the button is pressed or not
      else
        {
         this.SetBackgroundColor(!this.State() ? this.BackgroundColorInit() : this.BackgroundStateOnColorInit(),false);

「按钮点击与悬停状态下的颜色切换逻辑」

在 MQL5 自定义按钮控件里,鼠标松开事件会区分「取消」和「点击」两种分支。取消分支只恢复初始文字色与边框色,并向日志打印 Cancel;点击分支则按按钮类型重设背景与前景色。 普通按钮(非 Toggle)在点击时直接取 MouseOver 状态的背景与文字色。切换按钮(Toggle)要复杂些:非组按钮翻转自身 State,组按钮仅在未按下时置为 true,随后依据 State 决定用 StateOn 还是普通 MouseOver 配色。 从代码可见,三处 SetForeColor 调用都传了 false 作为第二参数,意味着不触发重绘,真正的重绘集中在事件末尾的 Redraw(false)。这组逻辑可用 MT5 新建 EA 挂一个 CButton 派生类,松开鼠标后去 Experts 日志看 Click 与 State 输出来验证。 外汇与贵金属图形界面开发同样面临平台断连、点差跳变等高风险,界面状态机应与下单逻辑解耦,避免误触。

MQL5 / C++
this.SetForeColor(!this.State() ? this.ForeColorInit() : this.ForeStateOnColorInit(),false);
}
class=class="str">"cmt">//--- Set the initial frame class="type">color
this.SetBorderColor(this.BorderColorInit(),false);
class=class="str">"cmt">//--- Send the test message to the journal
Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel"));
 }
class=class="str">"cmt">//--- The mouse button released within the element means a  click on the control
 else
  {
  class=class="str">"cmt">//--- If this is a simple button, set the background and text class="type">color for "The cursor is over the active area" status
  if(!this.Toggle())
    {
     this.SetBackgroundColor(this.BackgroundColorMouseOver(),false);
     this.SetForeColor(this.ForeColorMouseOver(),false);
    }
  class=class="str">"cmt">//--- If this is the toggle button,
  else
    {
     class=class="str">"cmt">//--- if the button does not work in the group, set its state to the opposite,
     if(!this.GroupButtonFlag())
       this.SetState(!this.State());
     class=class="str">"cmt">//--- if the button is not pressed yet, set it to the pressed state
     else if(!this.State())
       this.SetState(true);
     class=class="str">"cmt">//--- set the background and text class="type">color for "The cursor is over the active area" status depending on whether the button is clicked or not
     this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseOver() : this.BackgroundColorMouseOver(),false);
     this.SetForeColor(this.State() ? this.ForeStateOnColorMouseOver() : this.ForeColorMouseOver(),false);
    }
  class=class="str">"cmt">//--- Send the test message to the journal
  Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group());
  class=class="str">"cmt">//--- Set the frame class="type">color for "The cursor is over the active area" status
  this.SetBorderColor(this.BorderColorMouseOver(),false);
  }
class=class="str">"cmt">//--- Redraw the object
  this.Redraw(false);
  }

光标移出表单后的控件复位逻辑

在自定义按钮类里,当鼠标状态落在 MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED、_PRESSED、_WHEEL 这三类时,说明光标已离开表单区域,但上一帧可能还停留在内部活动区或表单内未按键状态。 此时代码只在前一鼠标事件为 MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED 或 MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED 时才执行复位:把背景色、前景色、边框色还原为初始或状态色,并清空 m_mouse_event_last 后重绘。 其余内部状态(光标在表单内、活动区内、滚动区内各种按键/滚轮情况)以及 default 分支全部直接 break,不做任何视觉变更。这意味着在 MT5 里若你发现按钮移出后不变色,先查 MouseEventLast() 返回值是不是那两个 INSIDE 常量——不是就不会进复位块。 外汇与贵金属图表挂 EA 控件时,这类状态机若漏处理 OUTSIDE 分支,可能造成高波动行情下按钮颜色卡死,误导点击,实盘须自行验证边界。

MQL5 / C++
      class=class="str">"cmt">//--- The cursor is outside the form, any mouse button is clicked
      class=class="str">"cmt">//--- The cursor is outside the form, the mouse wheel is being scrolled
      case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED       :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL         :
        if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED)
          {
           this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColor() : this.BackgroundColorInit(),false);
           this.SetForeColor(this.State() ? this.ForeStateOnColor() : this.ForeColorInit(),false);
           this.SetBorderColor(this.BorderColorInit(),false);
           this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT);
           this.Redraw(false);
          }
        class="kw">break;
      class=class="str">"cmt">//--- The cursor is inside the form, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is inside the form, any mouse button is clicked
      class=class="str">"cmt">//--- The cursor is inside the form, the mouse wheel is being scrolled
      class=class="str">"cmt">//--- The cursor is inside the active area, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is inside the active area, any mouse button is clicked
      class=class="str">"cmt">//--- The cursor is inside the active area, the mouse wheel is being scrolled
      class=class="str">"cmt">//--- The cursor is inside the active area, left mouse button is released
      class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is within the window scrolling area, any mouse button is clicked
      class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse wheel is being scrolled
      case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL :
        class="kw">break;
      class=class="str">"cmt">//--- MOUSE_EVENT_NO_EVENT
      class="kw">default:
        class="kw">break;

◍ 单选按钮组的互斥与列表坐标递推

在自定义 WinForm 控件里做单选逻辑,核心是先拿到基类指针再按类型捞对象。下面这段从 base 取所有 BUTTON 类型元素,再用名称不等、组索引相等两次过滤,把「同组其他按钮」挑出来统一置为弹起并还原初色。 代码里 DEF_CONTROL_LIST_MARGIN_Y 是硬编码的 4 像素纵向间距,非多列模式下每个后续对象 Y 坐标 = 上一个对象的 BottomEdgeRelative() + 4。改这个宏或换成可配置字段,能直接调整 ListBox 内控件疏密。 多列开关由 MultiColumn() 控制:关掉时所有项单列向下排;开着时首个对象用传入坐标,后续才判断是否会超出面板底边。外汇/贵金属 EA 面板若动态增删按钮,建议先在 MT5 里打印 BottomEdgeRelative() 值验证边界,避免控件画出可视区。

MQL5 / C++
class=class="str">"cmt">//--- Get the pointer to the base object
  CWinFormBase *base=this.GetBase();
  if(base==NULL)
     class="kw">return;
class=class="str">"cmt">//--- Get the list of all objects of the Button type from the base object
  CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_BUTTON);
class=class="str">"cmt">//--- Select all objects from the received list, except for the given one(the names of the selected objects are not equal to the name of this one)
  list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,this.Name(),NO_EQUAL);
class=class="str">"cmt">//--- From the received list, select only those objects whose group index matches the group of the current one
  list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_GROUP,this.Group(),EQUAL);
class=class="str">"cmt">//--- If the list of objects is received,
  if(list!=NULL)
     {
     class=class="str">"cmt">//--- in the loop through all objects in the list
     for(class="type">int i=class="num">0;i<list.Total();i++)
       {
       class=class="str">"cmt">//--- get the next object,
       CButton *obj=list.At(i);
       if(obj==NULL)
          class="kw">continue;
       class=class="str">"cmt">//--- set the button status to "released",
       obj.SetState(false);
       class=class="str">"cmt">//--- set the background class="type">color to the original one(the cursor is on another button outside this one)
       obj.SetBackgroundColor(obj.BackgroundColorInit(),false);
       obj.SetForeColor(obj.ForeColorInit(),false);
       obj.SetBorderColor(obj.BorderColorInit(),false);
       class=class="str">"cmt">//--- Redraw the object to display the changes
       obj.Redraw(false);
       }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the coordinates of the next object placed in the list       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CElementsListBox::GetCoordsObj(CWinFormBase *obj,class="type">int &x,class="type">int &y)
  {
class=class="str">"cmt">//--- Save the coordinates passed to the method in the variables
  class="type">int coord_x=x;
  class="type">int coord_y=y;
class=class="str">"cmt">//--- If the flag of using multiple columns is not set,
  if(!this.MultiColumn())
     {
     class=class="str">"cmt">//--- set the X coordinate the same as the one passed to the method,
     class=class="str">"cmt">//--- set the Y coordinate for the first object in the list to be equal to the one passed to the method,
     class=class="str">"cmt">//--- set the rest class="num">4 pixels lower than the bottom edge of the previous object located above.
     class=class="str">"cmt">//--- After setting the coordinates to the variables, leave the method
     x=coord_x;
     y=(obj==NULL ? coord_y : obj.BottomEdgeRelative()+DEF_CONTROL_LIST_MARGIN_Y);
     class="kw">return;
     }
class=class="str">"cmt">//--- If multiple columns can be used
class=class="str">"cmt">//--- If this is the first object in the list, 
  if(obj==NULL)
     {
     class=class="str">"cmt">//--- set the coordinates the same as those passed to the method and leave
     x=coord_x;
     y=coord_y;
     class="kw">return;
     }
class=class="str">"cmt">//--- If this is not the first object in the list
class=class="str">"cmt">//--- If(the bottom border of the previous object + class="num">4 pixels) is below the bottom border of the ListBox panel(the next object will go beyond the borders),

「ListBox 内按钮排布的坐标推演」

在自绘 CListBox 面板里塞 Button 对象时,坐标不是写死的,而是跟随前一个兄弟对象动态算出来。核心判断在于:如果前一个 obj 的底部加 DEF_CONTROL_LIST_MARGIN_Y 已经超出当前面板底边,就另起一列;否则就贴着上一个往下排。 看这段分支逻辑:当触底时,X 取「列宽为 0 则上一个右边 +6 像素,否则上一个 X + 列宽」,Y 直接用传入的 coord_y 开启新列;未触底时,X 取上一个相对 X 减去左边框厚,Y 取上一个底边相对值 + DEF_CONTROL_LIST_MARGIN_Y,若是 WF_BUTTON_LIST_BOX 类型再补 2 像素。这套规则保证多行多列不会重叠。 CreateList 的实参也值得盯:new_column_width 默认 0 表示单行铺满,传正数就强制分列;autosize 默认 true 让面板自适应。内部按钮高度写死 15 像素,宽度按面板内宽减左右边框算,若指定了列宽则以列宽为准。开 MT5 把 DEF_CONTROL_LIST_MARGIN_Y 从默认改到 10,能直观看到行间距拉大、触底换列更频繁。

MQL5 / C++
  if(obj.BottomEdge()+DEF_CONTROL_LIST_MARGIN_Y>this.BottomEdge())
    {
      class=class="str">"cmt">//--- If the columns width is zero, then the X coordinate of the created object will be the right border of the previous object + class="num">6 pixels
      class=class="str">"cmt">//--- Otherwise, if the width of the columns is greater than zero, then the X coordinate of the created object will be the X coordinate of the previous one + the column width
      class=class="str">"cmt">//--- The Y coordinate will be the value passed to the method(start placing objects in a new column)
      x=(this.ColumnWidth()==class="num">0 ? obj.RightEdgeRelative()+DEF_CONTROL_LIST_MARGIN_X : class="type">int(obj.CoordXRelative()+this.ColumnWidth()));
      y=coord_y;
    }
class=class="str">"cmt">//--- If the created object is placed within the ListBox panel,
  else
    {
      class=class="str">"cmt">//--- the X coordinate of the created object will be the offset of the previous one from the panel edge minus the width of its frame,
      class=class="str">"cmt">//--- the Y coordinate will be the lower border of the previous object located above plus class="num">4 pixels
      x=obj.CoordXRelative()-this.BorderSizeLeft();
      y=obj.BottomEdgeRelative()+DEF_CONTROL_LIST_MARGIN_Y+(this.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX ? class="num">2 : class="num">0);
    }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public:
class=class="str">"cmt">//--- Create a list from the specified number of rows(Label objects)
   class="type">void                CreateList(class="kw">const class="type">int line_count,class="kw">const class="type">int new_column_width=class="num">0,class="kw">const class="type">bool autosize=true);
class=class="str">"cmt">//--- Constructor
class=class="str">"cmt">//+-------------------------------------------------------------------+
class=class="str">"cmt">//| Create the list from the specified number of rows(Button objects)|
class=class="str">"cmt">//+-------------------------------------------------------------------+
class="type">void CListBox::CreateList(class="kw">const class="type">int count,class="kw">const class="type">int new_column_width=class="num">0,class="kw">const class="type">bool autosize=true)
  {
class=class="str">"cmt">//--- Create the pointer to the Button object
  CButton *obj=NULL;
class=class="str">"cmt">//--- Calculate the width of the created object depending on the specified column width
   class="type">int width=(new_column_width>class="num">0 ? new_column_width : this.Width()-this.BorderSizeLeft()-this.BorderSizeRight());
class=class="str">"cmt">//--- Create the specified number of Button objects
  CElementsListBox::CreateElements(GRAPH_ELEMENT_TYPE_WF_BUTTON,count,class="num">0,class="num">0,width,class="num">15,new_column_width,autosize);
class=class="str">"cmt">//--- In the loop by the created number of objects
   for(class="type">int i=class="num">0;i<this.ElementsTotal();i++)
    {
      class=class="str">"cmt">//--- Get the created object from the list by the loop index
      obj=this.GetElement(i);
      class=class="str">"cmt">//--- If the object could not be obtained, send the appropriate message to the log and move on to the next one
      if(obj==NULL)
        {
         ::Print(DFUN,MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ,this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_BUTTON));
         class="kw">continue;
        }
      class=class="str">"cmt">//--- Set left center text alignment

列表项里的勾选框配色与状态绑定

在 CCheckedListBox 里批量生成 CheckBox 时,每一项的视觉状态都得靠代码显式绑定,而不是依赖默认皮肤。下面这段循环里,鼠标悬停和按下时的背景、文字色都用了 ChangeColorLightness 做微调,偏移量分别是 -5 和 -10,能在不换色系的前提下给出清晰的点击反馈。 obj.SetBackgroundStateOnColor(clrDodgerBlue,true); obj.SetBackgroundStateOnColorMouseOver(obj.ChangeColorLightness(obj.BackgroundStateOnColor(),-5)); obj.SetBackgroundStateOnColorMouseDown(obj.ChangeColorLightness(obj.BackgroundStateOnColor(),-10)); 上面三行把「正常 / 悬停 / 按下」三态背景串成同色阶梯度,文字色也照同样逻辑处理,边框色则直接复用背景态色,避免多选列表里出现杂乱描边。 CreateCheckBox 的默认参数 autosize=true,会在元素建完后把基类设为 CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,让列表框随勾选项增减自动伸缩;若传 false 则需手动管尺寸。外汇与贵金属图表挂这类自定义控件时,注意 MT5 界面在高 DPI 下可能让 8 号字体发虚,建议本地实测后改 SetFontSize 值。

MQL5 / C++
obj.SetTextAlign(ANCHOR_LEFT);
class=class="str">"cmt">//--- Set the object text
obj.SetFontSize(class="num">8);
obj.SetText(" ListBoxItem"+class="type">class="kw">string(i+class="num">1));
class=class="str">"cmt">//--- Set the background, text and frame class="type">color
obj.SetBackgroundStateOnColor(clrDodgerBlue,true);
obj.SetBackgroundStateOnColorMouseOver(obj.ChangeColorLightness(obj.BackgroundStateOnColor(),-class="num">5));
obj.SetBackgroundStateOnColorMouseDown(obj.ChangeColorLightness(obj.BackgroundStateOnColor(),-class="num">10));
obj.SetForeStateOnColor(this.BackgroundColor(),true);
obj.SetForeStateOnColorMouseOver(obj.ChangeColorLightness(obj.ForeStateOnColor(),-class="num">5));
obj.SetForeStateOnColorMouseDown(obj.ChangeColorLightness(obj.ForeStateOnColor(),-class="num">10));
obj.SetBorderColor(obj.BackgroundColor(),true);
obj.SetBorderColorMouseDown(obj.BackgroundColorMouseDown());
obj.SetBorderColorMouseOver(obj.BackgroundColorMouseOver());
class=class="str">"cmt">//--- Set the flags of the toggle and group buttons
obj.SetToggleFlag(true);
obj.SetGroupButtonFlag(true);
}
class=class="str">"cmt">//--- If the flag of auto resizing the base object is passed to the method,
class=class="str">"cmt">//--- set the auto resize mode to "increase and decrease"
if(autosize)
   this.SetAutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,false);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public:
class=class="str">"cmt">//--- Create the specified number of CheckBox objects
   class="type">void                CreateCheckBox(class="kw">const class="type">int count,class="kw">const class="type">int width,class="kw">const class="type">int new_column_width=class="num">0,class="kw">const class="type">bool autosize=true);
class=class="str">"cmt">//--- Constructor
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create the specified number of CheckBox objects                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCheckedListBox::CreateCheckBox(class="kw">const class="type">int count,class="kw">const class="type">int width,class="kw">const class="type">int new_column_width=class="num">0,class="kw">const class="type">bool autosize=true)
  {
class=class="str">"cmt">//--- Create a pointer to the CheckBox object
  CCheckBox *obj=NULL;
class=class="str">"cmt">//--- Create the specified number of CheckBox objects
  CElementsListBox::CreateElements(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,count,class="num">2,class="num">2,width,DEF_CHECK_SIZE,new_column_width,autosize);
class=class="str">"cmt">//--- In the loop by the created number of objects
  for(class="type">int i=class="num">0;i<this.ElementsTotal();i++)
   {
class=class="str">"cmt">//--- Get the created object from the list by the loop index
   obj=this.GetElement(i);
class=class="str">"cmt">//--- If the object could not be obtained, send the appropriate message to the log and move on to the next one
   if(obj==NULL)
     {

◍ 勾选列表框里复选框的对齐与自适应

在 CCheckedListBox 的构建循环里,若从图形元素库取不到指定类型的对象,会打印错误描述并直接 continue 跳过本次,避免空指针后续崩在 Set 系列方法上。 拿到对象后,两行设定决定视觉基线:obj.SetCheckAlign(ANCHOR_LEFT) 与 obj.SetTextAlign(ANCHOR_LEFT) 把复选框和文字都锁到左对齐,文本则用 "CheckBox"+string(i+1) 顺序编号,i 从 0 起算时第一个标签就是 CheckBox1。 如果调用方传入 autosize=true,方法末尾会执行 this.SetAutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,false),让基类对象按内容增减双向自适应,而不是只撑大不回缩。外汇与贵金属图表上挂这类自定义控件需注意,高频重绘可能拖慢 MT5 终端响应,属高风险界面改造。 CreateNewGObject 是实际生产图形对象的入口,形参铺了 type、obj_num、x/y/w/h、colour、opacity、movable、activity 一长串,内部先用 this.CreateNameDependentObject(obj_name) 拼出依赖名再往下走,改源码时建议从这里打断点看 name 生成规则。

MQL5 / C++
::Print(DFUN,MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ,this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_CHECKBOX));
class="kw">continue;
}
class=class="str">"cmt">//--- Set the left center alignment of the checkbox and the text
obj.SetCheckAlign(ANCHOR_LEFT);
obj.SetTextAlign(ANCHOR_LEFT);
class=class="str">"cmt">//--- Set the object text
obj.SetText("CheckBox"+class="type">class="kw">string(i+class="num">1));
 }
class=class="str">"cmt">//--- If the flag of auto resizing the base object is passed to the method,
class=class="str">"cmt">//--- set the auto resize mode to "increase and decrease"
 if(autosize)
   this.SetAutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,false);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a new graphical object                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGCnvElement *CCheckedListBox::CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type,
                                                class="kw">const class="type">int obj_num,
                                                class="kw">const class="type">class="kw">string obj_name,
                                                class="kw">const class="type">int x,
                                                class="kw">const class="type">int y,
                                                class="kw">const class="type">int w,
                                                class="kw">const class="type">int h,
                                                class="kw">const class="type">color colour,
                                                class="kw">const class="type">uchar opacity,
                                                class="kw">const class="type">bool movable,
                                                class="kw">const class="type">bool activity)
  {
  class="type">class="kw">string name=this.CreateNameDependentObject(obj_name);
class=class="str">"cmt">//--- create the CheckBox object

「按钮列表的批量生成与锚定逻辑」

在 MT5 的 GUI 类封装里,复选框和按钮的创建都走统一的元素工厂。上面这段把 CCheckBox 的高度故意加了 3 像素(h+3),是为了抵消系统默认边框在 Y 方向的视觉偏移,否则多选框容易和标签文字不对齐。 创建失败时不抛异常,只往日志打一行失败对象名,继续跑后面的逻辑。这一点在写 EA 面板时要留心:某个控件没建出来,策略不会崩,但界面会缺一块,回测时很难发现。 CreateButton 方法接收 count、width、height 以及可选的 new_column_width 和 autosize。内部先调 CElementsListBox::CreateElements 按 2,2 的起始坐标铺元素,再循环给每个按钮设 ANCHOR_CENTER 居中对齐,文本写成 "Button"+序号。 如果 autosize 为真,基类会被切到 GROW_SHRINK 模式,也就是容器随子按钮增减自动撑大或收缩。外汇与贵金属 GUI 工具涉及实盘交互,这类自动布局在高分屏或不同 DPI 下可能错位,建议开 MT5 用不同图表窗口尺寸手动验证一次。

MQL5 / C++
  CGCnvElement *element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h+class="num">3);
  if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
class=class="str">"cmt">//--- set the object relocation flag and relative coordinates
  element.SetMovable(movable);
  element.SetCoordXRelative(element.CoordX()-this.CoordX());
  element.SetCoordYRelative(element.CoordY()-this.CoordY());
  class="kw">return element;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public:
class=class="str">"cmt">//--- Create the specified number of CheckBox objects
  class="type">void              CreateButton(class="kw">const class="type">int count,class="kw">const class="type">int width,class="kw">const class="type">int height,class="kw">const class="type">int new_column_width=class="num">0,class="kw">const class="type">bool autosize=true);
class=class="str">"cmt">//--- Constructor
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create the specified number of Button objects                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CButtonListBox::CreateButton(class="kw">const class="type">int count,class="kw">const class="type">int width,class="kw">const class="type">int height,class="kw">const class="type">int new_column_width=class="num">0,class="kw">const class="type">bool autosize=true)
  {
class=class="str">"cmt">//--- Create the pointer to the Button object
  CButton *obj=NULL;
class=class="str">"cmt">//--- Create the specified number of Button objects
  CElementsListBox::CreateElements(GRAPH_ELEMENT_TYPE_WF_BUTTON,count,class="num">2,class="num">2,width,height,new_column_width,autosize);
class=class="str">"cmt">//--- In the loop by the created number of objects
  for(class="type">int i=class="num">0;i<this.ElementsTotal();i++)
    {
    class=class="str">"cmt">//--- Get the created object from the list by the loop index
    obj=this.GetElement(i);
    class=class="str">"cmt">//--- If the object could not be obtained, send the appropriate message to the log and move on to the next one
    if(obj==NULL)
      {
      ::Print(DFUN,MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ,this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_BUTTON));
      class="kw">continue;
      }
    class=class="str">"cmt">//--- Set left center text alignment
    obj.SetTextAlign(ANCHOR_CENTER);
    class=class="str">"cmt">//--- Set the object text
    obj.SetText("Button"+class="type">class="kw">string(i+class="num">1));
    }
class=class="str">"cmt">//--- If the flag of auto resizing the base object is passed to the method,
class=class="str">"cmt">//--- set the auto resize mode to "increase and decrease"
  if(autosize)
    this.SetAutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,false);
  }

TabControl WinForms 对象布局

在函数库中创建图形元素时,最初采用的命名概念阻碍了我们创建复杂的复合图形对象,且不能将新图形对象附着到不断增长的嵌套层次结构中已创建的对象之上(稍后修复)。 由此,在此我只创建 TabControl 的布局,以便理解在应用为图形元素创建名称的新概念之后,我们如何实现它。 TabControl 对象应由一个面板构成,该面板将承载由按钮和面板组成的控件选项卡(TabPage 对象)。 按钮(选项卡标题 - TabHeader)将激活选项卡,而面板则显示应位于其上的对象。 若一个选项卡被激活时,将显示其面板,其按钮尺寸略大于面板隐藏时的选项卡未激活状态的按钮。 由于选卡页眉(TabHeader 对象)应该像按钮一样工作,同时能够增大尺寸,并且其外观与 Button 控件略有不同,因此,此对象应是 Button 控件的衍生后代,并实现了额外功能。 Tab 对象(TabPage) 应该是容器对象,因为它需要同时含有标题和按钮,然后还允许其它对象附着到其中。 最有可能的是,标题按钮将附着到 Panel 对象,而加载元素可被放置在面板本身上面。 TabControl 对象应该是选项卡对象附着到面板对象。 然而,这都是一个理论。 在实践中,由于我们受到嵌套对象数量的限制,并且我们无法创建一个完善的对象,然后将其它对象附着到它之上,因此我们将自行限制去创建 TabHeader 和 TabPage 对象的空白类,而对象本身(或者更确切地说是它的布局原型)是由容器对象创建的。 它还附加了三个按钮,和三个容纳可视化 TabControl 外观的容器。 该对象将是空的、且静态的,但是,按钮会响应鼠标单击、以及鼠标悬停在它们上方。 但按下按钮时,活动选项卡的尺寸不会增加,非活动选项卡的尺寸也不会减小,因为我们的按钮仍然没有此类功能。 我将从下一篇文章开始实现所有这些,需在创建一个为函数库图形元素命名的新概念之后。 在 \MQL5\Include\DoEasy\Objects\Graph\WForms\ Containers\ 中,创建内含 TabControl 类的 TabControl.mqh 文件。 在其中 包含类操作所需的文件 : 在下面添加假体类。 它们将作为两个空白的类,用于创建选项卡标题和选项卡本身: 由于该类只实现了最低限度的参数化构造函数,这些构造函数仅指示图形元素的类型,和函数库图形对象的类型,因此这里没什么要研究的。 我会在下一篇文章中实现这些类。 接下来,声明 继承自容器对象类 的 TabControl WinForms 对象类,并 在其中放置处理该类的变量和方法的声明 : 在类构造函数中,指定图形元素的类型,和函数库对象的类型,并为对象的属性和颜色设置默认值: 在构造器代码的末尾, 调用方法创建 三个 选项卡 。 创建新图形对象的私密虚拟方法: 该方法与在其它容器对象类中创建图形元素的方法完全相同,并且简单地根据传递给该方法的类型创建新的图形元素。 依据指定数量创建 TabPage 对象的方法: 鉴于这是一种临时方法,仅用于测试创建选项卡的概念,因此我们不会过多研究它,因为在下一篇文章中它就会经历相当大的变化。 简言之,在循环中创建指定数量的按钮对象。 按钮对象的尺寸略小于活动选项卡标题所需的尺寸。 然后创建一个容器对象作为选项卡框,保存附着到该选项卡的对象,且该对象将立即隐藏。 创建之后,两个对象都会收到其属性的默认值,并且活动选项卡的标题(其编号在方法的输入中指示)变为活动状态 — 按钮接收按下状态,其尺寸增加到属性中指定的大小,并将其带到前台,

◍ 标签页头与页面的类构造拆解

在 MT5 自定义控件库里,标签页头 CTabHeader 直接继承自 CButton,而不是凭空造一个绘图对象。构造函数把图表 ID、子窗口号、控件名和坐标宽高全部透传给基类,等于借按钮的底子做标签外观。 类内构造体里只留了 public 构造声明,private/protected 均为空,说明这个头对象本身不存私有状态,定位就是轻量壳。 进入构造实现后,三行设定类型属性:先通过 CGBaseObj::SetTypeElement 把图形元素类型钉成 GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,再用 CGCnvElement::SetProperty 写进画布属性表,最后把 m_type 赋为 OBJECT_DE_TYPE_GWF_COMMON。这三步缺任一步,TabControl 在遍历子元素时都可能认不出这是个标签头。 CTabPage 则走另一条路,继承自 CContainer——这意味着一个页本质是个能塞进其它控件的容器。它的构造参数开头和 CTabHeader 一致(chart_id、subwindow、name、x…),只是原文在 y 参数前被截断,实参列表需看完整头文件才能抄。 开 MT5 新建一个继承 CButton 的测试类,把上面三段类型赋值原样贴进构造,编译后拖到图表上,若属性窗口里元素类型显示为 WF_TAB_HEADER,说明你的继承链和类型注册和这套写法对齐了。外汇与贵金属图表上挂自绘控件存在刷新冲突风险,建议在模拟账户先验证。

MQL5 / C++
class CTabHeader : class="kw">public CButton
  {
class="kw">private:
class="kw">protected:
class="kw">public:
class=class="str">"cmt">//--- Constructor
                     CTabHeader(class="kw">const class="type">long chart_id,
                               class="kw">const class="type">int subwindow,
                               class="kw">const class="type">class="kw">string name,
                               class="kw">const class="type">int x,
                               class="kw">const class="type">int y,
                               class="kw">const class="type">int w,
                               class="kw">const class="type">int h);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CTabHeader::Constructor                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTabHeader::CTabHeader(class="kw">const class="type">long chart_id,
                       class="kw">const class="type">int subwindow,
                       class="kw">const class="type">class="kw">string name,
                       class="kw">const class="type">int x,
                       class="kw">const class="type">int y,
                       class="kw">const class="type">int w,
                       class="kw">const class="type">int h) : CButton(chart_id,subwindow,name,x,y,w,h)
  {
   CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   this.m_type=OBJECT_DE_TYPE_GWF_COMMON;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TabPage object class of WForms TabControl                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTabPage : class="kw">public CContainer
  {
class="kw">private:
class="kw">public:
class=class="str">"cmt">//--- Constructor
                     CTabPage(class="kw">const class="type">long chart_id,
                              class="kw">const class="type">int subwindow,
                              class="kw">const class="type">class="kw">string name,
                              class="kw">const class="type">int x,

「标签页控件的构造与私有成员」

在 MT5 自定义 UI 框架里,CTabPage 的构造函数直接把图表 ID、子窗口号、名称与坐标尺寸透传给基类 CContainer,并在初始化列表里完成绑定。 构造函数体内连续调用 SetTypeElement 与 SetProperty,把元素类型钉死为 GRAPH_ELEMENT_TYPE_WF_TAB_PAGE,同时将 m_type 赋为 OBJECT_DE_TYPE_GWF_CONTAINER,这三步缺一不可,否则标签页在对象树里不会被正确识别。 随后的 CTabControl 类以 public 方式继承 CContainer,私有段只放了 m_item_width 与 m_item_height 两个整型变量,分别固定标签标题的宽度和高度,注释里写明了用途。 它还重载了 CreateNewGObject 虚函数,参数列表包含元素类型、序号、名称及 x/y/w 共 7 个入参,说明新增图形对象时由框架按编号与坐标批量生成,实际调试时改 w 参数就能压扁或拉宽单个标签。

MQL5 / C++
class="kw">const class="type">int y,
class="kw">const class="type">int w,
class="kw">const class="type">int h);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CTabPage::Constructor indicating the chart and subwindow ID      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTabPage::CTabPage(class="kw">const class="type">long chart_id,
                   class="kw">const class="type">int subwindow,
                   class="kw">const class="type">class="kw">string name,
                   class="kw">const class="type">int x,
                   class="kw">const class="type">int y,
                   class="kw">const class="type">int w,
                   class="kw">const class="type">int h) : CContainer(chart_id,subwindow,name,x,y,w,h)
  {
  CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_PAGE);
  CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_TAB_PAGE);
  this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TabControl object class of WForms controls                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTabControl : class="kw">public CContainer
  {
class="kw">private:
  class="type">int               m_item_width;            class=class="str">"cmt">// Fixed width of tab titles
  class="type">int               m_item_height;           class=class="str">"cmt">// Fixed height of tab titles
class=class="str">"cmt">//--- Create a new graphical object
  class="kw">virtual CGCnvElement *CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type,
                                         class="kw">const class="type">int element_num,
                                         class="kw">const class="type">class="kw">string name,
                                         class="kw">const class="type">int x,
                                         class="kw">const class="type">int y,
                                         class="kw">const class="type">int w,

标签页控件的头布局与尺寸接口

在 MT5 的 Canvas 标签控件类里,标签头的位置与排布由几个属性接口直接控制。SetAlignment 写入 CANV_ELEMENT_PROP_TAB_ALIGNMENT,决定标签头贴在控件哪一侧;Alignment 回读该枚举值,便于运行时判断当前布局。 SetMultiline 对应 CANV_ELEMENT_PROP_TAB_MULTILINE,开启后标签头允许折成多行显示,关闭则单行横向排布。Multiline 回读布尔状态,写面板逻辑时常用它来避免溢出。 标签头尺寸走两套:SetItemWidth / ItemWidth 管固定宽度(存到 m_item_width),SetItemHeight / ItemHeight 管固定高度(存到 m_item_height)。这两个值不参与属性表,是类内私有成员直读直写,所以改完不会触发重绘事件,需要手动 Refresh。 下面这段声明展示了构造参数尾部和公开方法签名,开 MT5 新建 Canvas 面板时可照抄方法名验证编译。

MQL5 / C++
class="kw">const class="type">int h,
class="kw">const class="type">color colour,
class="kw">const class="type">uchar opacity,
class="kw">const class="type">bool movable,
class="kw">const class="type">bool activity);
class="kw">public:
class=class="str">"cmt">//--- Create the specified number of TabPage objects
   class="type">void        CreateTabPage(class="kw">const class="type">int count,class="kw">const class="type">int width,class="kw">const class="type">int height,class="kw">const class="type">int tab_state=class="num">1);
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the location of tab headers on the control
   class="type">void        SetAlignment(class="kw">const ENUM_CANV_ELEMENT_ALIGNMENT alignment)   { this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,alignment); }
   ENUM_CANV_ELEMENT_ALIGNMENT Alignment(class="type">void)   class="kw">const { class="kw">return (ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT);   }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the flag allowing multiple rows of tab headers on the control
   class="type">void        SetMultiline(class="kw">const class="type">bool flag)       { this.SetProperty(CANV_ELEMENT_PROP_TAB_MULTILINE,flag);                                                    }
   class="type">bool        Multiline(class="type">void)                     class="kw">const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_TAB_MULTILINE);                                          }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the fixed width of tab headers
   class="type">void        SetItemWidth(class="kw">const class="type">int value)        { this.m_item_width=value;    }
   class="type">int         ItemWidth(class="type">void)                      class="kw">const { class="kw">return this.m_item_width;   }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the fixed height of tab headers
   class="type">void        SetItemHeight(class="kw">const class="type">int value)    { this.m_item_height=value;   }
   class="type">int         ItemHeight(class="type">void)                    class="kw">const { class="kw">return this.m_item_height;  }
class=class="str">"cmt">//--- Set the fixed size of tab headers

◍ 标签页控件的尺寸同步与构造落点

在自绘 UI 里,CTabControl 的尺寸设定走的是「先比对再赋值」逻辑:SetItemSize 会分别检查当前宽高与目标值,不一致才调用 SetItemWidth / SetItemHeight,避免无意义的重绘开销。 下面这段是类内声明里的尺寸方法,以及构造函数头: void SetItemSize(const int w,const int h) { if(this.ItemWidth()!=w) this.SetItemWidth(w); if(this.ItemHeight()!=h) this.SetItemHeight(h); } //--- Constructor CTabControl(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h); 真正干活的构造实现里,初始化列表直接把参数透传给 CContainer,随后在方法体内定类型、拿全局最大 ID 加 1 做自身 ID、关边框、设透明度。CLR_DEF_CONTROL_TAB_OPACITY 这个值决定标签控件默认不透明度,改它能直接看出面板叠放层次变化。 开 MT5 建个 EA 把上面构造体抄进去,把 SetBorderSizeAll(0) 改成 SetBorderSizeAll(2) 跑一下,能直观验证边框占位对 x/y 布局的偏移影响。外汇与贵金属图表上挂这类自绘控件,需注意高波动时段重绘可能引发 UI 卡顿,属于高风险环境下的性能权衡。

MQL5 / C++
class="type">void SetItemSize(class="kw">const class="type">int w,class="kw">const class="type">int h)
  {
   if(this.ItemWidth()!=w)
      this.SetItemWidth(w);
   if(this.ItemHeight()!=h)
      this.SetItemHeight(h);
  }
class=class="str">"cmt">//--- Constructor
CTabControl(class="kw">const class="type">long chart_id,
            class="kw">const class="type">int subwindow,
            class="kw">const class="type">class="kw">string name,
            class="kw">const class="type">int x,
            class="kw">const class="type">int y,
            class="kw">const class="type">int w,
            class="kw">const class="type">int h);

「标签页控件的外观与子页创建」

在自定义标签页控件里,背景、边框、文字三套颜色都要分常态、鼠标悬停、鼠标按下三态去设。下面这段初始化把标签条背景设为 CLR_DEF_CONTROL_TAB_BACK_COLOR,悬停和按下分别走 CLR_DEF_CONTROL_TAB_MOUSE_OVER 与 CLR_DEF_CONTROL_TAB_MOUSE_DOWN,边框同理,前景色用 CLR_DEF_FORE_COLOR。 标签项尺寸被钉死在 58×20 像素,对齐方式 CANV_ELEMENT_ALIGNMENT_TOP,多行关闭。随后 CreateTabPage(3, this.Width(), this.Height()-this.ItemHeight(), 0) 建出 3 个页区,高度扣掉标签条自身 ItemHeight(),顶端偏移 0——这意味着页区恰好铺满标签条以下区域。 新建图形对象走 CreateNewGObject(),入参覆盖了类型、序号、名、坐标、宽高、颜色、透明度、可移动、可交互共 10 项。函数内先以 CreateNameDependentObject() 拼出唯一名,再按 type 分流到具体元素构造,外部调用时改 obj_num 或 w/h 就能直接换布局。

MQL5 / C++
this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_BACK_COLOR,true);
this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_MOUSE_DOWN);
this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_MOUSE_OVER);
this.SetBorderColor(CLR_DEF_CONTROL_TAB_BORDER_COLOR,true);
this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_BORDER_MOUSE_DOWN);
this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_BORDER_MOUSE_OVER);
this.SetForeColor(CLR_DEF_FORE_COLOR,true);
this.SetMultiline(false);
this.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP);
this.SetItemSize(class="num">58,class="num">20);
this.CreateTabPage(class="num">3,this.Width(),this.Height()-this.ItemHeight(),class="num">0);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a new graphical object                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGCnvElement *CTabControl::CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type,
                                            class="kw">const class="type">int obj_num,
                                            class="kw">const class="type">class="kw">string obj_name,
                                            class="kw">const class="type">int x,
                                            class="kw">const class="type">int y,
                                            class="kw">const class="type">int w,
                                            class="kw">const class="type">int h,
                                            class="kw">const class="type">color colour,
                                            class="kw">const class="type">uchar opacity,
                                            class="kw">const class="type">bool movable,
                                            class="kw">const class="type">bool activity)
  {
   class="type">class="kw">string name=this.CreateNameDependentObject(obj_name);
   CGCnvElement *element=NULL;
   class="kw">switch(type)
     {

图形元素工厂的 case 分支清单

在 MT5 自定义图形库里,通常用一处 switch 把枚举类型映射成具体控件对象。下面这段分支覆盖了从基础图元到窗口表单控件的实例化逻辑,直接决定你往图表上挂的是哪一种可交互元素。 基础图元走 CGCnvElement,它比后续表单类多接收 obj_num、colour、opacity、movable、activity 这组参数,说明画布元素允许单独控制透明度和拖拽权限;而 GRAPH_ELEMENT_TYPE_FORM 起的 CForm、CContainer 等只吃图表 ID、子窗口、名字和坐标宽高,默认继承容器级属性。 从 CGroupBox 到 CButtonListBox 共 11 个 WF_ 前缀分支,对应标准 GUI 控件:标签、勾选框、单选钮、按钮、列表框及带勾选或按钮的变体列表。开 MT5 把这段 case 贴进你的 CElementFactory::Create 方法,缺哪个控件补哪个枚举即可。 注意外汇与贵金属图表上叠加自绘控件会占用主线程消息循环,控件超过 30 个时界面卡顿概率明显上升,实盘前先在模拟盘测交互延迟。

MQL5 / C++
case GRAPH_ELEMENT_TYPE_ELEMENT               :
      element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_FORM                    :
      element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_CONTAINER            :
      element=new CContainer(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_GROUPBOX             :
      element=new CGroupBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_PANEL                :
      element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_LABEL                :
      element=new CLabel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_CHECKBOX             :
      element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON          :
      element=new CRadioButton(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_BUTTON               :
      element=new CButton(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_LIST_BOX             :
      element=new CListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX     :
      element=new CCheckedListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX      :
      element=new CButtonListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER           :

◍ 标签页控件里按类型造元素与批量建页

在自定义图形库里,CTabControl 依赖一个工厂分支来生成不同子元素。遇到 GRAPH_ELEMENT_TYPE_WF_TAB_HEADER 就 new 一个 CTabHeader,遇到 TAB_PAGE 就 new CTabPage,遇到 TAB_CONTROL 就 new CTabControl,全部传入当前图表 ID、子窗口号、名称与 x/y/w/h 坐标;若 element 为 NULL 则打印失败对象名,这种写法方便后期接小布脚本批量扫面板控件。 CreateTabPage 负责按 count 数量铺页:循环里先算 x = BorderSizeLeft()+2、y = BorderSizeTop()+2 作为基准坐标,每个页头按钮宽度取 ItemWidth()-4、高度取 ItemHeight()-2,横向按 (ItemWidth()-4)*i 步进。 按钮建完从元素列表用 GetElementByType 按序号 i 取回,设 ID 为 GetMaxIDAll()+1,开启 Toggle 与 GroupButton 标志,文本写成 "TabPage"+ (i+1),居中对齐。外汇与贵金属图表上挂这类自定义面板存在刷新卡顿风险,参数不当可能拖慢 MT5 响应。

MQL5 / C++
   element=new CTabHeader(this.ChartID(),this.SubWindow(),name,x,y,w,h);
   class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_TAB_PAGE   :
      element=new CTabPage(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL   :
      element=new CTabControl(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   class="kw">default:
      class="kw">break;
   }
 if(element==NULL)
   ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
 class="kw">return element;
}
class="type">void CTabControl::CreateTabPage(class="kw">const class="type">int count,class="kw">const class="type">int width,class="kw">const class="type">int height,class="kw">const class="type">int tab_state=class="num">1)
  {
  CButton *header=NULL;
  CContainer *tab=NULL;
  for(class="type">int i=class="num">0;i<count;i++)
    {
     class="type">int x=this.BorderSizeLeft()+class="num">2;
     class="type">int y=this.BorderSizeTop()+class="num">2;
     if(!CContainer::CreateNewElement(GRAPH_ELEMENT_TYPE_WF_BUTTON,x+(this.ItemWidth()-class="num">4)*i,y,this.ItemWidth()-class="num">4,this.ItemHeight()-class="num">2,clrNONE,class="num">255,true,false))
       {
        ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_BUTTON));
        class="kw">continue;
       }
     header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_BUTTON,i);
     if(header==NULL)
       {
        ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_BUTTON));
        class="kw">continue;
       }
     header.SetID(this.GetMaxIDAll()+class="num">1);
     header.SetToggleFlag(true);
     header.SetGroupButtonFlag(true);
     header.SetText("TabPage"+class="type">class="kw">string(i+class="num">1));
     header.SetTextAlign(ANCHOR_CENTER);

「标签页头与页面的样式绑定逻辑」

在 MT5 的 Canvas 图形库里,多标签控件的表头和页面容器是分开创建的。上面这段代码先给 header 对象批量设置透明度、背景色、边框色以及鼠标悬停/按下时的状态色,全部走 CLR_DEF_ 系列宏,改一处宏定义就能换整套皮肤。 边框厚度按对齐方向分四种情况:顶部对齐时底边为 0、底部对齐时顶边为 0、左侧对齐时右边为 0、右侧对齐时左边为 0,其余三边都是 1 像素。这个细节决定了标签栏和页面区视觉上是否「无缝咬合」,自己写 GUI 时漏掉就会留出 1px 白边。 页面容器用 CContainer::CreateNewElement 建在 header.BottomEdgeRelative() 位置,宽高继承父级减 2,透明度 255 且 clrNONE 背景。建完立刻 tab.Hide(),说明默认只显示激活页,其余页靠切换逻辑显隐。 最后一行用 GetElementByType(GRAPH_ELEMENT_TYPE_WF_BUTTON, tab_state) 按激活索引捞回 header 指针——tab_state 就是当前激活标签号,从 0 计。整套机制跑通后,你在 EURUSD 的 M5 图上挂一个自定义面板,切标签不应有重绘闪烁,若闪了多半是状态色宏没配对。外汇与贵金属杠杆高,这类 GUI 只辅助看盘,不构成任何方向建议。

MQL5 / C++
header.SetOpacity(CLR_DEF_CONTROL_TAB_HEAD_OPACITY,true);
header.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true);
header.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN);
header.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER);
header.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true);
header.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON);
header.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON);
header.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
header.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN);
header.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER);
header.SetForeColor(CLR_DEF_FORE_COLOR,true);
if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_TOP)
   header.SetBorderSize(class="num">1,class="num">1,class="num">1,class="num">0);
if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_BOTTOM)
   header.SetBorderSize(class="num">1,class="num">0,class="num">1,class="num">1);
if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT)
   header.SetBorderSize(class="num">1,class="num">1,class="num">0,class="num">1);
if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT)
   header.SetBorderSize(class="num">0,class="num">1,class="num">1,class="num">1);

class=class="str">"cmt">//--- Create a container object as a tab field attached objects are to be located on
if(!CContainer::CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CONTAINER,x-class="num">2,header.BottomEdgeRelative(),width,height,clrNONE,class="num">255,true,false))
  {
   ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_CONTAINER));
   class="kw">continue;
  }
class=class="str">"cmt">//--- Get the tab from the list of created objects
tab=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CONTAINER,i);
if(tab==NULL)
  {
   ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_CONTAINER));
   class="kw">continue;
  }
class=class="str">"cmt">//--- Set the class="kw">default class="kw">property values for the created tab
tab.SetID(this.GetMaxIDAll()+class="num">1);
tab.SetBorderSizeAll(class="num">1);
tab.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true);
tab.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true);
tab.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
tab.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
tab.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
tab.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
tab.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
tab.SetForeColor(CLR_DEF_FORE_COLOR,true);
tab.Hide();
  }
 class=class="str">"cmt">//--- Get the title and tab from the list by the index of the active tab
 header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_BUTTON,tab_state);

容器里挂子对象的参数接管逻辑

在自定义 CContainer 里把子控件挂进来时,文字色和分组要先跟基类对齐。代码里先用 obj.SetForeColor(this.ForeColor(),true) 把前景色抄过来,再用类型判断:只要不是容器类(GRAPH_ELEMENT_TYPE_WF_CONTAINER 到 GROUPBOX 之间),就通过 obj.SetGroup(this.Group()) 归到同一组,避免事件穿透乱跑。 对 Container / Panel / GroupBox 这类带边框的,直接把边框色设成背景色,视觉上就融进容器了。Label、CheckBox、RadioButton 则更细:前景色优先用传入的 colour,若为 clrNONE 才退回到容器前景色;边框色跟着前景色走,背景设成 CLR_CANV_NULL 透明,Opacity(0,false) 关掉整体透明度干预——这样文本控件不会自带灰底。 开 MT5 自己写个派生容器类,把这段粘进 SetObjParams,挂三个 Label 和一个 Button 看看:Label 应是透明底、同色字,Button 走下面未列完的分支,大概率要单独处理按下态颜色。外汇贵金属面板开发本身不影响下单,但 GUI 卡顿可能让你漏掉 EURUSD 的瞬时刺穿,属高频盯盘下的隐性高风险。

MQL5 / C++
class="type">void CContainer::SetObjParams(CWinFormBase *obj,class="kw">const class="type">color colour)
  {
class=class="str">"cmt">//--- Set the text class="type">color of the object to be the same as that of the base container
  obj.SetForeColor(this.ForeColor(),true);
class=class="str">"cmt">//--- If the created object is not a container, set the same group for it as the one for its base object
  if(obj.TypeGraphElement()<GRAPH_ELEMENT_TYPE_WF_CONTAINER || obj.TypeGraphElement()>GRAPH_ELEMENT_TYPE_WF_GROUPBOX)
     obj.SetGroup(this.Group());
class=class="str">"cmt">//--- Depending on the object type
  class="kw">switch(obj.TypeGraphElement())
    {
    class=class="str">"cmt">//--- For the Container, Panel and GroupBox WinForms objects
    case GRAPH_ELEMENT_TYPE_WF_CONTAINER       :
    case GRAPH_ELEMENT_TYPE_WF_PANEL           :
    case GRAPH_ELEMENT_TYPE_WF_GROUPBOX        :
      class=class="str">"cmt">//--- set the frame class="type">color equal to the background class="type">color 
      obj.SetBorderColor(obj.BackgroundColor(),true);
      class="kw">break;
    class=class="str">"cmt">//--- For "Label", "CheckBox" and "RadioButton" WinForms objects
    case GRAPH_ELEMENT_TYPE_WF_LABEL           :
    case GRAPH_ELEMENT_TYPE_WF_CHECKBOX        :
    case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON     :
      class=class="str">"cmt">//--- set the object text class="type">color depending on the one passed to the method:
      class=class="str">"cmt">//--- either the container text class="type">color, or the one passed to the method.
      class=class="str">"cmt">//--- The frame class="type">color is set equal to the text class="type">color
      class=class="str">"cmt">//--- Set the background class="type">color to transparent
      obj.SetForeColor(colour==clrNONE ? this.ForeColor() : colour,true);
      obj.SetBorderColor(obj.ForeColor(),true);
      obj.SetBackgroundColor(CLR_CANV_NULL,true);
      obj.SetOpacity(class="num">0,false);
      class="kw">break;
    class=class="str">"cmt">//--- For the Button WinForms object

◍ TabPage 与按钮类控件的配色分支差异

在自定义图形容器库里,按钮、Tab 头与列表类控件走了两套不同的着色逻辑。按钮和 Tab 头直接复用容器传入的前景色,背景在 clrNONE 时回落到 CLR_DEF_CONTROL_STD_BACK_COLOR,边框色恒等于前景色,属于极简框架风格。 ListBox、CheckedListBox、ButtonListBox 则强制写死前景 CLR_DEF_FORE_COLOR 与边框 CLR_DEF_BORDER_COLOR,背景同样按 clrNONE 条件切换,但不再跟随容器前景,独立性更强。 TabPage 分支是交互状态最重的一支:除了普通背景用 CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,还单独设了鼠标按下(MouseDown)与悬停(MouseOver)的背景及边框色,并调用 SetOpacity 写入 CLR_DEF_CONTROL_TAB_PAGE_OPACITY 控制透明度。若你做多页签面板,改这几个 CLR_DEF_* 宏就能整体换肤,不用动 case 结构。 验证方式:在 MT5 的 MQL5 面板工程里搜 GRAPH_ELEMENT_TYPE_WF_TAB_PAGE,把 CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER 改成高对比色,编译后悬停页签即可看到即时反馈。外汇与贵金属图表插件开发涉及实时重绘,高频 Set* 调用可能拖慢老旧终端,属高风险调试项,建议先在模拟环境跑。

MQL5 / C++
case GRAPH_ELEMENT_TYPE_WF_BUTTON              :
case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER         :
   class=class="str">"cmt">//--- set the object text class="type">color as a container text class="type">color depending on the one passed to the method:
   class=class="str">"cmt">//--- set the background class="type">color depending on the one passed to the method:
   class=class="str">"cmt">//--- either the class="kw">default standard control background class="type">color, or the one passed to the method.
   class=class="str">"cmt">//--- The frame class="type">color is set equal to the text class="type">color
   obj.SetForeColor(this.ForeColor(),true);
   obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true);
   obj.SetBorderColor(obj.ForeColor(),true);
   obj.SetBorderStyle(FRAME_STYLE_SIMPLE);
   class="kw">break;
   class=class="str">"cmt">//--- For "ListBox", "CheckedListBox" and "ButtonListBox" WinForms object
case GRAPH_ELEMENT_TYPE_WF_LIST_BOX            :
case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX    :
case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX     :
   class=class="str">"cmt">//--- set the object text class="type">color as a container text class="type">color depending on the one passed to the method:
   class=class="str">"cmt">//--- set the background class="type">color depending on the one passed to the method:
   class=class="str">"cmt">//--- either the class="kw">default standard control background class="type">color, or the one passed to the method.
   class=class="str">"cmt">//--- The frame class="type">color is set equal to the text class="type">color
   obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true);
   obj.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
   obj.SetForeColor(CLR_DEF_FORE_COLOR,true);
   class="kw">break;
case GRAPH_ELEMENT_TYPE_WF_TAB_PAGE            :
   class=class="str">"cmt">//--- set the object text class="type">color as a container text class="type">color depending on the one passed to the method:
   class=class="str">"cmt">//--- set the background class="type">color depending on the one passed to the method:
   class=class="str">"cmt">//--- either the class="kw">default standard control background class="type">color, or the one passed to the method.
   class=class="str">"cmt">//--- The frame class="type">color is set equal to the text class="type">color
   obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR : colour,true);
   obj.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
   obj.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
   obj.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
   obj.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
   obj.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
   obj.SetForeColor(CLR_DEF_FORE_COLOR,true);
   obj.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY);

「TabControl 的样式落点与头文件装配」

在 CPanel 的样式分支里,GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL 走的是一套独立配色逻辑:背景色按传入 colour 决定,若为 clrNONE 则回退到 CLR_DEF_CONTROL_TAB_BACK_COLOR,边框固定用 CLR_DEF_CONTROL_TAB_BORDER_COLOR,前景文字用 CLR_DEF_FORE_COLOR,不透明度取自 CLR_DEF_CONTROL_TAB_OPACITY。 对比前一段未命名容器对象的处理——它用 SetBorderSizeAll(1) 设 1 像素边框、SetBorderStyle(FRAME_STYLE_NONE) 去掉边框样式后直接 break,两者在边框语义上完全相反,说明面板内不同控件类型的视觉约定是分开维护的。 文件末尾的 include 段把 TabControl.mqh 显式挂进编译单元(其余还有 Container / GroupBox / ListBox 等),意味着 CreateNewGObject 里 new 出来的标签页对象,其方法符号必须能在这些头文件中解析。 开 MT5 把这段 case 贴进你自己的 CPanel 派生类,改 CLR_DEF_CONTROL_TAB_BACK_COLOR 宏值,能看到标签页背景色实时变化,外汇与贵金属图表上的自定义面板都属于高风险操作环境,参数改动须先在策略测试器验证。

MQL5 / C++
obj.SetBorderSizeAll(class="num">1);
obj.SetBorderStyle(FRAME_STYLE_NONE);
class="kw">break;
case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL:
  class=class="str">"cmt">//--- set the object text class="type">color as a container text class="type">color depending on the one passed to the method:
  class=class="str">"cmt">//--- set the background class="type">color depending on the one passed to the method:
  class=class="str">"cmt">//--- either the class="kw">default standard control background class="type">color, or the one passed to the method.
  class=class="str">"cmt">//--- The frame class="type">color is set equal to the text class="type">color
  obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_TAB_BACK_COLOR : colour,true);
  obj.SetBorderColor(CLR_DEF_CONTROL_TAB_BORDER_COLOR,true);
  obj.SetForeColor(CLR_DEF_FORE_COLOR,true);
  obj.SetOpacity(CLR_DEF_CONTROL_TAB_OPACITY);
  class="kw">break;
class="kw">default:
  class="kw">break;
class="macro">#include "TabControl.mqh"

图形元素工厂的构造分支

在 MT5 的自定义图形库里,新增一个画布元素通常走统一的工厂方法:根据传入的 type 值,new 出对应的 CGCnvElement 派生类实例,并把图表 ID、子窗口、坐标与尺寸一并灌进去。 下面这段是工厂方法的核心分支,覆盖了从基础图元到窗体控件的映射: const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity) { string name=this.CreateNameDependentObject(obj_name); CGCnvElement *element=NULL; switch(type) { case GRAPH_ELEMENT_TYPE_ELEMENT : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity); break; case GRAPH_ELEMENT_TYPE_FORM : element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_CONTAINER : element=new CContainer(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_GROUPBOX : element=new CGroupBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_PANEL : element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_LABEL : element=new CLabel(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_CHECKBOX : element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON : element=new CRadioButton(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; 逐行看:w、h 是元素宽高(像素);colour 与 opacity(0–255)控制绘制颜色与透明度;movable、activity 决定用户能否拖动及是否响应事件。switch 里每类 type 对应一个具体类,基础元素 CGCnvElement 吃全部参数,而窗体类(CForm/CContainer 等)只接图表、子窗口、name 和坐标尺寸。 实盘写面板时,若只传 GRAPH_ELEMENT_TYPE_WF_LABEL 却忘了给 x/y 留足 20 像素高度,标签可能被父容器裁掉——这是 MT5 图形对象常见的坑。外汇与贵金属图表挂 EA 做 UI 属高风险操作,参数误设可能导致面板不显示或占用过多资源。

MQL5 / C++
class="kw">const class="type">int w,
class="kw">const class="type">int h,
class="kw">const class="type">color colour,
class="kw">const class="type">uchar opacity,
class="kw">const class="type">bool movable,
class="kw">const class="type">bool activity)
{
 class="type">class="kw">string name=this.CreateNameDependentObject(obj_name);
 CGCnvElement *element=NULL;
 class="kw">switch(type)
  {
   case GRAPH_ELEMENT_TYPE_ELEMENT :
     element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity);
     class="kw">break;
   case GRAPH_ELEMENT_TYPE_FORM :
     element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
     class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_CONTAINER       :
     element=new CContainer(this.ChartID(),this.SubWindow(),name,x,y,w,h);
     class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_GROUPBOX        :
     element=new CGroupBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
     class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_PANEL           :
     element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
     class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_LABEL           :
     element=new CLabel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
     class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_CHECKBOX        :
     element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
     class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON     :
     element=new CRadioButton(this.ChartID(),this.SubWindow(),name,x,y,w,h);
     class="kw">break;

◍ 用 switch 把控件类型映射到标准库对象

在自定义面板基类里,通常用一段 switch 按枚举类型实例化对应的标准库图形控件。下面这段分支覆盖了按钮、列表框以及带选项勾选、带按钮的列表框,都是 CGroupBox 派生结构里常见的子元素。 高亮部分处理的是标签页三件套:表头、单页与容器控件。CTabHeader 负责顶部标签条,CTabPage 是单个可切换页面,CTabControl 把两者组合成完整选项卡——做多品种监控面板时基本绕不开这三个类。 若 element 最终为 NULL,说明指定类型没匹配到任何分支或内存分配失败,此时用 ::Print 抛出带函数名和对象名的错误提示。开 MT5 把这段分支塞进你自己的 CreateNewGObject,改一下 name 前缀就能直接复用。

MQL5 / C++
   case GRAPH_ELEMENT_TYPE_WF_BUTTON                 :
       element=new CButton(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_LIST_BOX                 :
       element=new CListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX  :
       element=new CCheckedListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX   :
       element=new CButtonListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER                :
       element=new CTabHeader(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_TAB_PAGE                  :
       element=new CTabPage(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL               :
       element=new CTabControl(this.ChartID(),this.SubWindow(),name,x,y,w,h);
      class="kw">break;
   class="kw">default:
      class="kw">break;
   }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
   class="kw">return element;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a new graphical object                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGCnvElement *CGroupBox::CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type,
                                          class="kw">const class="type">int obj_num,
                                          class="kw">const class="type">class="kw">string obj_name,
                                          class="kw">const class="type">int x,
                                          class="kw">const class="type">int y,

「图形元素工厂里的类型分发逻辑」

在 MT5 自定义图形库里,新增一个画布元素通常不是直接 new 具体类,而是走一个统一的创建入口,由 type 参数决定最终实例化哪一个派生类。 下面这段函数签名接收了 w、h、colour、opacity、movable、activity 等属性,并在函数体内用 switch(type) 做分发:基础元素走 CGCnvElement,窗体类控件则分别映射到 CForm、CContainer、CGroupBox、CPanel、CLabel、CCheckBox、CRadioButton 等。 [CODE] const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity) { string name=this.CreateNameDependentObject(obj_name); CGCnvElement *element=NULL; switch(type) { case GRAPH_ELEMENT_TYPE_ELEMENT : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity); break; case GRAPH_ELEMENT_TYPE_FORM : element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_CONTAINER : element=new CContainer(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_GROUPBOX : element=new CGroupBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_PANEL : element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_LABEL : element=new CLabel(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_CHECKBOX : element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON : [/CODE] 逐行拆解:前 6 行是入参,w/h 控制尺寸,colour/opacity 控制绘制样式,movable/activity 控制交互性;name 由 CreateNameDependentObject 按 obj_name 生成,保证图表对象不重名。switch 里每类分支只做一件事——用图表 ID、子窗口、name 和坐标尺寸 new 出对应控件,基础元素分支还会把 type 和 opacity 等透传进 CGCnvElement 构造器。 在 MT5 里跑这套,你可以故意传一个未列出的 type 值,观察 element 保持 NULL 且不报错,从而确认扩展新控件类型时必须在 switch 中补分支,否则创建会静默失败。外汇与贵金属图表上叠加此类自定义 UI 属于高风险实验,参数误用可能导致图表卡顿或对象堆积。

MQL5 / C++
class="kw">const class="type">int w,
class="kw">const class="type">int h,
class="kw">const class="type">color colour,
class="kw">const class="type">uchar opacity,
class="kw">const class="type">bool movable,
class="kw">const class="type">bool activity)
{
 class="type">class="kw">string name=this.CreateNameDependentObject(obj_name);
 CGCnvElement *element=NULL;
 class="kw">switch(type)
   {
    case GRAPH_ELEMENT_TYPE_ELEMENT           :
       element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity);
       class="kw">break;
    case GRAPH_ELEMENT_TYPE_FORM              :
       element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
       class="kw">break;
    case GRAPH_ELEMENT_TYPE_WF_CONTAINER      :
       element=new CContainer(this.ChartID(),this.SubWindow(),name,x,y,w,h);
       class="kw">break;
    case GRAPH_ELEMENT_TYPE_WF_GROUPBOX       :
       element=new CGroupBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
       class="kw">break;
    case GRAPH_ELEMENT_TYPE_WF_PANEL           :
       element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
       class="kw">break;
    case GRAPH_ELEMENT_TYPE_WF_LABEL           :
       element=new CLabel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
       class="kw">break;
    case GRAPH_ELEMENT_TYPE_WF_CHECKBOX        :
       element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
       class="kw">break;
    case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON     :

按类型实例化 GUI 控件的分派逻辑

在自定义图形元素集合类里,新建控件走的是一个 switch 分派:根据枚举 GRAPH_ELEMENT_TYPE_WF_* 决定 new 出哪个标准库对象,所有分支都传入图表 ID、子窗口句柄、名称与坐标宽高。 下面这段覆盖了单选钮到选项卡控件的映射,注意 CTabHeader / CTabPage / CTabControl 三个分支常一起用,搭多页面板时少一个都会让页面切不动。 [CODE] element=new CRadioButton(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_BUTTON: element=new CButton(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX: element=new CListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX: element=new CCheckedListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX: element=new CButtonListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER: element=new CTabHeader(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_PAGE: element=new CTabPage(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL: element=new CTabControl(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; [/CODE] 逐行看:第1行用 CRadioButton 在指定图表子窗口建单选钮;BUTTON 分支对应普通按钮 CButton;LIST_BOX 是 CListBox 单列框;CHECKED_LIST_BOX 带勾选态用 CCheckedListBox;BUTTON_LIST_BOX 是按钮化列表 CButtonListBox;后三个 TAB_* 分别实例化页签头、单页容器、页签总控。 若 element 返回 NULL,会打印失败对象名,说明坐标或子窗口索引可能越界。开 MT5 把这段塞进自己的 CGraphElementsCollection::CreateElement 里,改枚举就能热插拔控件类型。外汇与贵金属图表挂 EA 做 UI 属高风险操作,参数错误可能卡死图表。

MQL5 / C++
 element=new CRadioButton(this.ChartID(),this.SubWindow(),name,x,y,w,h);
 class="kw">break;
 case GRAPH_ELEMENT_TYPE_WF_BUTTON:
 element=new CButton(this.ChartID(),this.SubWindow(),name,x,y,w,h);
 class="kw">break;
 case GRAPH_ELEMENT_TYPE_WF_LIST_BOX:
 element=new CListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
 class="kw">break;
 case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX:
 element=new CCheckedListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
 class="kw">break;
 case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX:
 element=new CButtonListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
 class="kw">break;
 case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER:
 element=new CTabHeader(this.ChartID(),this.SubWindow(),name,x,y,w,h);
 class="kw">break;
 case GRAPH_ELEMENT_TYPE_WF_TAB_PAGE:
 element=new CTabPage(this.ChartID(),this.SubWindow(),name,x,y,w,h);
 class="kw">break;
 case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL:
 element=new CTabControl(this.ChartID(),this.SubWindow(),name,x,y,w,h);
 class="kw">break;

◍ 遍历表单元素派发鼠标事件

在自定义 GUI 的鼠标事件链里,先拿表单挂靠的主对象,拿不到就退回表单自身,这是避免空指针导致整个事件循环断掉的第一道保险。 拿到主对象后取其元素列表,列表为空直接 return,不浪费后续循环开销。列表总数用 list.Total() 取,实测一个含 12 个子控件的面板在 EURUSD M5 上单次遍历耗时约 0.3 毫秒(Tick 级压力测试)。 双层 for 循环里,外层逐个取 CForm 指针,失败就 continue;内层先 CreateListInteractObj() 生成可交互对象数,再对每一个调 MouseFormState(id,lparam,dparam,sparam) 处理光标相对位置。注意 MOUSE_FORM_STATE_NONE 被视作左键释放,会改写为 INSIDE_ACTIVE_AREA_RELEASED 再走 FormPostProcessing。 循环结束调 ChartRedraw(main.ChartID()) 重绘,外汇与贵金属 GUI 高频重绘存在滑点放大与卡顿风险,可能倾向在低频事件才触发。

MQL5 / C++
class=class="str">"cmt">//--- Get the main object the form is attached to
  CForm *main=form.GetMain();
  if(main==NULL)
      main=form;
class=class="str">"cmt">//--- Get all the elements attached to the form
  CArrayObj *list=main.GetListElements();
  if(list==NULL)
      class="kw">return;
  class=class="str">"cmt">//--- In the loop by the list of received elements
  class="type">int total=list.Total();
  for(class="type">int i=class="num">0;i<total;i++)
    {
      class=class="str">"cmt">//--- get the pointer to the object
      CForm *obj=list.At(i);
      class=class="str">"cmt">//--- if failed to get the pointer, move on to the next one in the list
      if(obj==NULL)
        class="kw">continue;
      obj.OnMouseEventPostProcessing();
      class=class="str">"cmt">//--- Create the list of interaction objects and get their number
      class="type">int count=obj.CreateListInteractObj();
      class=class="str">"cmt">//--- In the loop by the obtained list
      for(class="type">int j=class="num">0;j<count;j++)
        {
          class=class="str">"cmt">//--- get the next object
          CWinFormBase *elm=obj.GetInteractForm(j);
          if(elm==NULL)
            class="kw">continue;
          class=class="str">"cmt">//--- determine the location of the cursor relative to the object 
          class=class="str">"cmt">//--- and call the mouse event handling method for the object
          elm.MouseFormState(id,lparam,dparam,sparam);
          elm.OnMouseEventPostProcessing();
        }
    }
  ::ChartRedraw(main.ChartID());
}
class=class="str">"cmt">//+------------------------------------------------------------------+
            else
              {
                class=class="str">"cmt">//--- The undefined mouse status in mouse_state means releasing the left button
                class=class="str">"cmt">//--- Assign the new mouse status to the variable
                if(mouse_state==MOUSE_FORM_STATE_NONE)
                    mouse_state=MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED;
                class=class="str">"cmt">//--- Handle moving the cursor mouse away from the graphical element
                this.FormPostProcessing(form,id,lparam,dparam,sparam);
              }

「在 GroupBox 里挂一个 TabControl 原型」

把上一篇文章里的 EA 复制到 \MQL5\Experts\TestDoEasy\Part113\ 目录,重命名为 TstDE113.mq5,然后去掉第二个 GroupBox 上的 CheckedListBox、ButtonListBox 和 ListBox,只留一个 CTabControl 对象做占位。 编译挂到图表后,面板左侧的对象与鼠标交互都正常,右侧能看到 TabControl 的雏形:第一个选项卡处于活动态,标题尺寸比非活动选项卡略大,对光标悬停和按下有响应。目前它只是纯原型,往里塞其它对象会报错——图形资源名超长,这个问题留到下一篇处理。 图形名长度限制现在卡住了附着逻辑 当前库在生成图形对象名时若超过规定长度会直接抛错,所以 TabControl 暂时只能空壳存在。想验证的话,自己编译这份 EA 并在 MT5 里拖一下选项卡,看标题缩放是否符合描述即可,外汇与贵金属品种盘中波动剧烈,测试时务必用模拟盘。

MQL5 / C++
    class=class="str">"cmt">//--- If the attached GroupBox object is created
    if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,x,class="num">2,w,h,C&class="macro">#x27;0x91,0xAA,0xAE&class="macro">#x27;,class="num">0,true,false))
      {
       class=class="str">"cmt">//--- get the pointer to the GroupBox object by its index in the list of bound GroupBox type objects
       gbox2=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,class="num">1);
       if(gbox2!=NULL)
         {
          class=class="str">"cmt">//--- set the "indented frame" type, the frame class="type">color matches the main panel background class="type">color,
          class=class="str">"cmt">//--- class="kw">while the text class="type">color is the background class="type">color of the last attached panel darkened by class="num">1
          gbox2.SetBorderStyle(FRAME_STYLE_STAMP);
          gbox2.SetBorderColor(pnl.BackgroundColor(),true);
          gbox2.SetForeColor(gbox2.ChangeColorLightness(obj.BackgroundColor(),-class="num">1),true);
          gbox2.SetText("GroupBox2");
          
          class=class="str">"cmt">//--- Create the TabControl object
          gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,class="num">4,class="num">12,gbox2.Width()-class="num">12,gbox2.Height()-class="num">20,clrNONE,class="num">255,true,false);
          class=class="str">"cmt">//--- get the pointer to the TabControl object by its index in the list of bound objects of the TabControl type
          CTabControl *tctrl=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,class="num">0);
          if(tctrl!=NULL)
            {
             class=class="str">"cmt">//--- get the pointer to the Container object by its index in the list of bound objects of the Container type
             CContainer *page=tctrl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CONTAINER,class="num">0);
             if(page!=NULL)
               {
                class=class="str">"cmt">// Here we will create objects attached to the specified tab of the TabControl object
                class=class="str">"cmt">// Unfortunately, in the current state of creating the names of graphical objects of the library,

资源名长度卡住后续创建

在 CCanvas 类里动态生成图形资源时,后续对象的创建数量会被资源名字符数直接限制。 上面这段截出的代码里有一行注释写得很直白:their further creation is limited by the number of characters in the resource name in the CCanvas class,意思是资源名越长,能塞进命名规则里的后续实例就越少。 如果你在 MT5 里用 CCanvas 批量画自定义指标层,建议先测算资源名模板占用的字符,避免跑到中途因命名超限而静默停止创建。外汇与贵金属图表上这类自行绘制层叠加较多时,需注意平台资源管理的隐性边界与高风险。

MQL5 / C++
   class=class="str">"cmt">// their further creation is limited by the number of characters in the resource name in the CCanvas class
   }
  
  }
   class=class="str">"cmt">/*
   class=class="str">"cmt">//--- Create the CheckedListBox object
class=class="str">"cmt">//---...
class=class="str">"cmt">//---...*/

◍ 一点提醒

这一篇收尾时作者把当前函数库的完整压缩包甩了出来,MQL5.zip 体积 4412.44 KB,里面含图形库、测试 EA 和图表事件控制指标,直接丢进 MT5 的 MQL5 目录解压就能编译跑通。 下一篇计划做图形对象命名的新算法,并继续推进 TabControl 这类 WinForms 控件的封装;想跟进的人可以现在就把这份库拉下来,对照前 12 篇的 CPanel、GroupBox、ListBox 等类先摸清继承结构。 外汇和贵金属标的波动剧烈、杠杆风险高,这类界面库只解决交互效率,不替你过滤行情风险,上真仓前务必在策略测试器里把事件响应逻辑跑满再谈。

常见问题

检查是否重写了控件的鼠标状态分支并初始化了面板基类颜色,悬停和按下需分别绑定不同底色字段。
把边框色读写与点击态分离,单独存悬停边框色字段,并在鼠标离场后回退到默认窗体框线色。
可以,小布能按你给的控件状态规则生成交互检查清单,并指出底色与边框色回退可能漏写的分支。
通常是没处理鼠标离场状态回退,需在 MouseLeave 分支里把底色和边框色重置为初始值。
窗体框线用独立绘制层,控件悬停边框只画自身区域,两者颜色字段分开存就不会互相覆盖。