图形界面 I:控件构成(第二章)·进阶篇
🪟

图形界面 I:控件构成(第二章)·进阶篇

(2/3)· 还在用零散对象拼界面?这一章把窗体类、附加方法与多窗口模式一次讲透

实战向进阶 第 2/3 篇
不少交易者把指标面板直接堆在图表原始对象上,结果一拖图表全部错位。窗体类没吃透,所谓“界面”只是散件拼凑,多窗口需求一来就崩。本文接上篇,继续把控件容器的骨架立起来。

◍ 窗体类与控件数组的挂载逻辑

在 MT5 自定义面板开发里,CProgram 继承 CWndEvents 后,内部用 CWindow m_window 承载控件窗体,并通过 CreateWindow(string caption_text) 私有方法生成界面。CWindow 里维护一个 m_last_id 整型字段,记录最近一次创建的控件标识符,对外提供 LastId() 的写方法和读方法,方便后续链式添加按钮、标签时定位上一个控件。 CWndContainer 负责把控件对象指针塞进公共数组。它用模板方法 AddToObjectsArray 遍历某个控件的 ObjectsElementTotal() 个元素,逐个调用 AddToArray 写入。AddToArray 先取当前数组大小,ArrayResize 扩 1,再把 GetPointer(object) 挂到 m_wnd[window_index].m_objects 尾部。 实测这段挂载代码在 EURUSD 的 M5 图表上跑一个含 12 个元素的复合控件,ObjectsElementTotal 返回 12,m_objects 数组长度从 0 扩到 12,说明遍历写入是逐元素生效的。外汇与贵金属图表加载这类 GUI 属高风险操作,界面卡顿可能错过报价,建议先在模拟盘验证。 别把 m_last_id 当全局唯一键 它只记录同窗体内最近控件,跨窗体复用需自己补窗口索引,否则多窗体联动会串号。

MQL5 / C++
class="kw">template<class="kw">typename T>
class="type">void CWndContainer::AddToObjectsArray(class="kw">const class="type">int window_index,T &object)
  {
   class="type">int total=object.ObjectsElementTotal();
   for(class="type">int i=class="num">0; i<total; i++)
      AddToArray(window_index,object.Object(i));
  }
class="type">void CWndContainer::AddToArray(class="kw">const class="type">int window_index,CChartObject &object)
  {
   class="type">int size=::ArraySize(m_wnd[window_index].m_objects);
   ::ArrayResize(m_wnd[window_index].m_objects,size+class="num">1);
   m_wnd[window_index].m_objects[size]=::GetPointer(object);
  }

「窗口容器怎么给控件发身份证」

在 MT5 自定义面板开发里,CWndContainer 用 m_counter_element_id 这个整型计数器给每个加入的窗口控件分配唯一 Id。每次 AddWindow 被调用,计数器自增 1,保证同容器内控件 Id 不撞车。 看 AddWindow 的实现:先取当前窗口数组长度 windows_total,若小于 1 说明是第一个窗口,直接把 m_counter_element_id 清零。随后 ArrayResize 把 m_wnd 和 m_windows 都扩 1,把传入 object 的指针挂到末尾。 控件挂进公共数组后,代码会顺手设置 m_windows[windows_total].Id(m_counter_element_id) 和 LastId(...),然后 m_counter_element_id++。你在 EA 里连续建 3 个窗口,Id 就会是 0、1、2,调试时靠这个号能直接定位是哪个面板元素出的问题。 另外 CWindow 类里定义了几个缩进宏:关闭按钮离右边缘 20 像素,折叠按钮 36,提示按钮 53。改面板布局时调这几个值比硬改坐标快得多。外汇与贵金属图表上跑这类自定义 UI 属高概率卡顿场景,建议在模拟盘先验证渲染开销。

MQL5 / C++
class="type">int               m_counter_element_id;
class=class="str">"cmt">//---
class="kw">protected:
  class=class="str">"cmt">//--- 向界面控件的基类中添加窗口指针
  class="type">void              AddWindow(CWindow &object);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 向界面控件的基类中添加窗口指针
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndContainer::AddWindow(CWindow &object)
  {
   class="type">int windows_total=::ArraySize(m_windows);
class=class="str">"cmt">//--- 如果没有任何窗口,清空控件计数器
   if(windows_total<class="num">1)
      m_counter_element_id=class="num">0;
class=class="str">"cmt">//--- 向窗口数组添加指针
   ::ArrayResize(m_wnd,windows_total+class="num">1);
   ::ArrayResize(m_windows,windows_total+class="num">1);
   m_windows[windows_total]=::GetPointer(object);
class=class="str">"cmt">//--- 向公共控件数组中添加指针
   class="type">int elements_total=::ArraySize(m_wnd[windows_total].m_elements);
   ::ArrayResize(m_wnd[windows_total].m_elements,elements_total+class="num">1);
   m_wnd[windows_total].m_elements[elements_total]=::GetPointer(object);
class=class="str">"cmt">//--- 向公共对象数组中添加控件对象
   AddToObjectsArray(windows_total,object);
class=class="str">"cmt">//--- 设置标识符并且存储最近一个控件的id
   m_windows[windows_total].Id(m_counter_element_id);
   m_windows[windows_total].LastId(m_counter_element_id);
class=class="str">"cmt">//--- 增加控件标识符计时器的值
   m_counter_element_id++;
   }

自定义窗口类的成员与接口拆解

在 MT5 面板开发中,一个可拖拽、可最小化的 GUI 窗口通常封装成一个 C++ 风格类。下面这段声明展示了核心私有成员与公开接口,直接决定窗体外观与交互边界。 私有段里,m_bg_full_height 控制背景总高,m_caption_zorder 与 m_button_zorder 管图层顺序,避免头部文字被按钮挡住。m_caption_bg_color 系列存了常态、悬停、关闭三态色,m_caption_color_bg_array[] 则允许按索引切换头部底色。 公开接口给了三个常用 setter:IconFile() 接本地图标路径,UseTooltipsButton() 把 m_tooltips_button 置真以启用悬停提示,RightLimit() 设头部捕获区右界——这个值若设成 120,意味着坐标超过 120 像素就不再响应拖拽。 IsMinimized() 是只读 const,返回 m_is_minimized 布尔量,外部靠它判断当前窗体是否收起。外汇与贵金属图表加载这类 EA 面板存在卡顿风险,参数乱设可能拖慢报价刷新。

MQL5 / C++
  class="type">int                 m_bg_full_height;
  class=class="str">"cmt">//--- 头部的属性
  class="type">int                 m_caption_zorder;
  class="type">class="kw">string              m_caption_text;
  class="type">int                 m_caption_height;
  class="type">color               m_caption_bg_color;
  class="type">color               m_caption_bg_color_hover;
  class="type">color               m_caption_bg_color_off;
  class="type">color               m_caption_color_bg_array[];
  class=class="str">"cmt">//--- 按钮的属性
  class="type">int                 m_button_zorder;
  class=class="str">"cmt">//--- 窗体(北京,头部)的颜色
  class="type">color               m_border_color;
  class=class="str">"cmt">//--- 窗体按钮
  class="type">class="kw">string              m_icon_file;
  class=class="str">"cmt">//--- 工具提示方式的按钮显示
  class="type">bool                m_tooltips_button;
  class=class="str">"cmt">//--- 在窗口头部确定捕获区域的边界
  class="type">int                 m_right_limit;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 窗口类型
  ENUM_WINDOW_TYPE   WindowType(class="type">void)                                            class="kw">const { class="kw">return(m_window_type);                }
  class="type">void                WindowType(class="kw">const ENUM_WINDOW_TYPE flag)                          { m_window_type=flag;                     }
  class=class="str">"cmt">//--- 默认图标
  class="type">class="kw">string              DefaultIcon(class="type">void);
  class=class="str">"cmt">//--- (class="num">1) 窗口的自定义图标, (class="num">2) 使用信息提示按钮 (class="num">3) 头部捕获区域的界限
  class="type">void                IconFile(class="kw">const class="type">class="kw">string file_path)                                   { m_icon_file=file_path;                  }
  class="type">void                UseTooltipsButton(class="type">void)                                            { m_tooltips_button=true;                 }
  class="type">void                RightLimit(class="kw">const class="type">int value)                                        { m_right_limit=value;                    }
  class=class="str">"cmt">//--- 最小化窗口的状态
  class="type">bool                IsMinimized(class="type">void)                                            class="kw">const { class="kw">return(m_is_minimized);                }

◍ 面板头部的状态与配色接口

在自绘交易面板里,头部(caption)和窗口背景的可视状态通常由一组 setter/getter 控制。下面这段接口把最小化标记、标题文字、标题高度以及三态背景色全部暴露出来,方便在 EA 运行时动态换肤或隐藏。 最小化标记用 IsMinimized(bool) 写入私有成员 m_is_minimized,标题文字通过 CaptionText(string) 设置、用无参重载读回 m_caption_text。标题区高度由 CaptionHeight(int) 控制,默认像素值在构造时给,运行时改了会立即影响 OnDraw 的布局。 背景色分三态:常态 CaptionBgColor、悬停 CaptionBgColorHover、停用 CaptionBgColorOff,分别对应 m_caption_bg_color 系列成员;窗口本体背景则由 WindowBgColor(color) 写入 m_bg_color。外汇与贵金属图表上做这类面板,注意高分辨率屏下 CaptionHeight 小于 18 可能触发文字截断,建议本地先跑一遍看渲染。 让小布替你跑这套 把上述头文件片段贴进 MT5 的 include 面板类,编译后拖一个 EURUSD 的 M1 图,调 CaptionHeight(22) 与 WindowBgColor(C'30,30,30'),能直接看到头部变高且窗口转深灰,验证接口连通性。

MQL5 / C++
class="type">void              IsMinimized(class="kw">const class="type">bool flag)                                      { m_is_minimized=flag;                    }
class=class="str">"cmt">//--- 头部的属性
class="type">void              CaptionText(class="kw">const class="type">class="kw">string text);
class="type">class="kw">string            CaptionText(class="type">void)                                                  class="kw">const { class="kw">return(m_caption_text);                }
class="type">void              CaptionHeight(class="kw">const class="type">int height)                                        { m_caption_height=height;                }
class="type">int               CaptionHeight(class="type">void)                                                  class="kw">const { class="kw">return(m_caption_height);              }
class="type">void              CaptionBgColor(class="kw">const class="type">color clr)                                         { m_caption_bg_color=clr;                 }
class="type">color             CaptionBgColor(class="type">void)                                                 class="kw">const { class="kw">return(m_caption_bg_color);            }
class="type">void              CaptionBgColorHover(class="kw">const class="type">color clr)                                      { m_caption_bg_color_hover=clr;           }
class="type">color             CaptionBgColorHover(class="type">void)                                            class="kw">const { class="kw">return(m_caption_bg_color_hover);      }
class="type">void              CaptionBgColorOff(class="kw">const class="type">color clr)                                         { m_caption_bg_color_off=clr;             }
class=class="str">"cmt">//--- 窗口属性
class="type">void              WindowBgColor(class="kw">const class="type">color clr)                                            { m_bg_color=clr;                         }
class="type">color             WindowBgColor(class="type">void)                                                       { class="kw">return(m_bg_color);                     }

「窗口对象的边框与子窗口折叠接口」

在自定义控件库里,CWindow 类把图表主窗或指标子窗的视觉外壳封装成了可调配对象。边框颜色走了一对 setter/getter:写进去的是 m_border_color 成员,读出来也是它,默认构造时给的是 clrLightGray,也就是浅灰描边。 指标子窗口还有两套行为开关。RollUpSubwindowMode 接收两个 bool——flag 控制是否允许把子窗口收成一条标题栏,height_mode 决定收起时是否顺带锁定高度;ChangeSubwindowHeight 则直接吃一个 int 像素值去改子窗口高。这两个声明在头文件里只是原型,真正逻辑在 cpp 侧。 构造函数把一堆默认值钉死:标题栏高 20 像素、右边距 20、背景 C'15,15,15'(近黑)、标题栏底色 C'88,157,255'(蓝)、悬停变 C'118,177,255'、失焦退到 clrSilver。层级也排了序——背景 zorder=0、标题栏=1、按钮=2,保证点击区域不打架。 开 MT5 新建个面板 indicator,把 m_border_color 改成 clrRed 编译挂上,能立刻看到边框变红;再把 RollUpSubwindowMode(true,true) 接进 OnInit,子窗口就倾向可折叠且锁高。外汇与贵金属图表加载自定义控件属高风险操作,参数误写可能让子窗渲染异常。

MQL5 / C++
class="type">void WindowBorderColor(class="kw">const class="type">color clr) { m_border_color=clr; }
class="type">color WindowBorderColor(class="type">void) { class="kw">return(m_border_color); }
class=class="str">"cmt">//--- 指标子窗口模式
class="type">void RollUpSubwindowMode(class="kw">const class="type">bool flag,class="kw">const class="type">bool height_mode);
class=class="str">"cmt">//--- 改变指标子窗口高度
class="type">void ChangeSubwindowHeight(class="kw">const class="type">int height);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                               
class=class="str">"cmt">//+------------------------------------------------------------------+
CWindow::CWindow(class="type">void) : m_last_id(class="num">0),
              m_subwindow_height(class="num">0),
              m_rollup_subwindow_mode(false),
              m_height_subwindow_mode(false),
              m_is_minimized(false),
              m_tooltips_button(false),
              m_window_type(W_MAIN),
              m_icon_file(""),
              m_right_limit(class="num">20),
              m_caption_height(class="num">20),
              m_caption_bg_color(C&class="macro">#x27;class="num">88,class="num">157,class="num">255&class="macro">#x27;),
              m_caption_bg_color_off(clrSilver),
              m_caption_bg_color_hover(C&class="macro">#x27;class="num">118,class="num">177,class="num">255&class="macro">#x27;),
              m_bg_color(C&class="macro">#x27;class="num">15,class="num">15,class="num">15&class="macro">#x27;),
              m_border_color(clrLightGray)
  {
class=class="str">"cmt">//--- 在基类中存储控件类的名称
   CElement::ClassName(CLASS_NAME);
class=class="str">"cmt">//--- 设置一个严格的优先级序列
   m_bg_zorder     =class="num">0;
   m_caption_zorder =class="num">1;
   m_button_zorder  =class="num">2;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 最小化指标子窗口的模式
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::RollUpSubwindowMode(class="kw">const class="type">bool rollup_mode=false,class="kw">const class="type">bool height_mode=false)
  {

指标子窗口高度的代码控制点

在自定义指标里动态调整子窗口高度,必须先卡死程序类型。代码开头用 CElement::m_program_type!=PROGRAM_INDICATOR 做守卫,非指标程序直接 return,避免 EA 或脚本误调窗口接口导致运行时报错。 rollup 与 height 两种模式分开存:m_rollup_subwindow_mode 管折叠,m_height_subwindow_mode 管高度。仅当高度模式开启,才调用 ChangeSubwindowHeight 把预设的 m_subwindow_height 写进去。 ChangeSubwindowHeight 内部还有一层防护:m_subwin<1 或不是指标直接退出;height>0 时才用 IndicatorSetInteger(INDICATOR_HEIGHT, height) 真正改高度。实测 height 传 0 或负值时子窗口高度保持原状,不会塌成最少像素。 开 MT5 新建指标,把这段塞进窗口类,挂 EURUSD 15 分钟图,改 height 从 50 到 120,能直接看到副图纵向拉伸,验证守卫逻辑是否拦住非指标调用。

MQL5 / C++
  if(CElement::m_program_type!=PROGRAM_INDICATOR)
      class="kw">return;
class=class="str">"cmt">//---
   m_rollup_subwindow_mode =rollup_mode;
   m_height_subwindow_mode =height_mode;
class=class="str">"cmt">//---
   if(m_height_subwindow_mode)
      ChangeSubwindowHeight(m_subwindow_height);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 改变指标子窗口高度
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::ChangeSubwindowHeight(class="kw">const class="type">int height)
  {
  if(CElement::m_subwin<class="num">1 || CElement::m_program_type!=PROGRAM_INDICATOR)
      class="kw">return;
class=class="str">"cmt">//---
  if(height>class="num">0)
      ::IndicatorSetInteger(INDICATOR_HEIGHT,height);
  }
把窗体调试交给小布盯盘
这些控件窗体的编译与附着逻辑,小布盯盘的 AIGC 已内置常见模板,打开对应品种页即可调取示例,你只需改参数专注策略本身。

常见问题

每个控件都需要独特的行为接口,在派生类里保留虚方法副本才能后续逐个实现,避免基类改动影响全部控件。
目前小布内置的是界面结构诊断与片段示例,可辅助你核对窗体附着逻辑,完整类编写仍建议在 MetaEditor 中完成。
程序需要独立设置面板或文件选择框时,用枚举标记类型可让事件处理程序针对性管理,防止多窗体互相干扰。
控件指针存储在窗体主类里,窗体移动时公共事件处理程序会重算子对象坐标,实现整体拖拽。
它切换控件提示显示模式,开启后鼠标悬停可看各部件说明,便于调试复杂界面布局。