图形界面表格交互基础:文本框组合框与窗体缩放的函数库实现(基础篇)
📐

图形界面表格交互基础:文本框组合框与窗体缩放的函数库实现(基础篇)

(1/3)·从零搭建可编辑表格控件,先吃透窗体缩放与单元嵌入的文本框、组合框底层逻辑

含代码示例 第 1/3 篇
不少人在 MQL5 自定义面板里硬塞输入框,却忽略窗体缩放时控件坐标错位,运行时点不动也拖不开。表格单元直接塞文本框和组合框若不做尺寸管理,往往一最大化就糊成一团。先把基础控件和窗口逻辑理清,后面才不会反复返工。

在表格单元里塞进可编辑控件

MetaTrader 5 的图形界面库到统合构建 15 这一版,已经能把文本编辑框和组合框直接嵌进表格的单元格里,而不只是早期版本那种只能静态显示文字的死表。这个改动对做自定义面板的人很关键:你能在同一张表里既看报价又改参数,不用再弹独立窗口。 作者 Anatoli Kazharski 在 2017 年 10 月 2 日的示例里给出了一套可跑的框架,原帖数据层面有 3181 次浏览、33 条评论,说明当时这套做法在 MQL5 社区算高频需求。它覆盖四个落点:窗口缩放自适应、单元格内文本框、单元格内组合框、以及应用层测试。 高风险提醒:外汇和贵金属市场杠杆高、滑点大,任何自定义界面只解决操作效率,不解决策略盈亏;在 MT5 里加载这类面板前,先开模拟账户验证控件响应是否正常。 直接动作:开 MT5 新建 EA 面板工程,把表格控件和 CEdit / CComboBox 做单元格父级绑定,拖一下窗口边框看控件是否跟着重排,能重排就说明你拿到统合构建 15 的能力了。

◍ 这套界面库从哪接、往哪改

首篇《图形界面 I:函数库结构的准备》已经把这套界面函数库的骨架拆开讲过一遍,每篇文章末尾都附了当时阶段的完整库文件,解压后必须保持存档里的目录层级不变,否则 MT5 里 include 路径会直接报找不到文件。 下一版更新的重心落在表格控件(CTable 类)。上一版只能在单元格里塞复选框和按钮,这次要把文本框和组合框也加进单元格控件的行列。 另外新增了一项实用能力:在 EA 或指标运行期间动态调整窗口尺寸,不用停掉程序改资源再重编译。外汇和贵金属图表加载这类界面时波动大,实盘前建议在策略测试器里先跑一遍确认布局不溢出。

「窗体全屏与拖拽改尺寸的底层实现」

在 MT5 自定义控件库里,窗体类 CWindow 把「全屏」和「拖边框改尺寸」都封装成了事件驱动逻辑,而不是靠终端自带的窗口管理。 全屏按钮默认禁用,要用 CButton::FullscreenButtonIsUsed(true) 打开;它只在主窗体出现。最小尺寸在初始化时被锁死,当前版本硬下限是 200×200 像素,由 CWindow::InitializeProperties() 控制,改小会被拦。 点全屏按钮走 CWindow::OnClickFullScreenButton():未最大化时存下原坐标、尺寸、自动缩放模式,启用自动缩放并贴到左上角铺满图表;已最大化则还原之前状态。双击标题也能触发,事件是 ON_DOUBLE_CLICK,不用碰按钮。 拖边框改尺寸靠 CWindow::ResizeWindow() 串起一串方法:ResizeMode() 开模式,鼠标在边框按下对应 PRESSED_INSIDE_BORDER 状态,光标换成 MP_WINDOW_RESIZE 图标。CheckResizePointer() 管图标显隐,CheckDragWindowBorder() 记起点和计算位移,CalculateAndResizeWindow() 算新坐标和宽高。鼠标移动事件 CHARTEVENT_MOUSE_MOVE 一到就调 ResizeWindow(),左键松开即清零变量。 别把 200×200 当可突破的软限制 InitializeProperties() 里写死的下限不会因图表分辨率变化而松动,想在小于此值的面板里塞表格,只会得到裁剪或报错,调参前先确认控件最小占用。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 控件的窗体类                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CWindow : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 创建窗体的对象
   CButton            m_button_fullscreen;
   class=class="str">"cmt">//--- 存在将窗口最大化为全屏模式的按钮
   class="type">bool               m_fullscreen_button;
   class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 返回指向窗体按钮的指针
   CButton            *GetFullscreenButtonPointer(class="type">void)            { class="kw">return(::GetPointer(m_button_fullscreen)); }
   class=class="str">"cmt">//--- 使用全屏按钮
   class="type">void               FullscreenButtonIsUsed(class="kw">const class="type">bool state)    { m_fullscreen_button=state;            }
   class="type">bool               FullscreenButtonIsUsed(class="type">void)          class="kw">const { class="kw">return(m_fullscreen_button);     }
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在窗体上创建按钮                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\Images\EasyAndFastGUI\Controls\full_screen.bmp"
class="macro">#resource "\Images\EasyAndFastGUI\Controls\minimize_to_window.bmp"
class=class="str">"cmt">//---
class="type">bool CWindow::CreateButtons(class="type">void)
  {
class=class="str">"cmt">//--- 如果程序类型是脚本, 离开

窗口类里的最小尺寸与全屏按钮逻辑

在 MT5 自定义 GUI 库里,CWindow 类通过两个私有成员 m_minimum_x_size 与 m_minimum_y_size 约束窗口缩放下限,对外只暴露 MinimumXSize() / MinimumYSize() 两个 setter,避免运行期被随意改小导致控件溢出。 初始化时若外部没传最小宽高,代码会用三元表达式兜底:m_minimum_x_size 小于 200 就直接取当前 m_x_size,等于给窗口留了 200 像素的硬下限。外汇与贵金属图表上挂这类面板,缩放失控可能挡住 K 线,属高风险交互缺陷。 创建按钮的循环固定跑 buttons_total=4 次,其中 b==1 分支专门挂全屏按钮:先判 m_fullscreen_button 是否存在且窗口类型不是 W_DIALOG,否则 continue 跳过;通过 GetPointer 拿到按钮对象并指定 bmp 图标路径 Images\EasyAndFastGUI\Controls\full_screen.bmp。 开 MT5 把这段抄进自己的面板类,改 buttons_total 和图标路径就能验证按钮阵列的渲染顺序。

MQL5 / C++
if(CElementBase::ProgramType()==PROGRAM_SCRIPT)
    class="kw">return(true);
class=class="str">"cmt">//--- 计数器, 大小, 数量
  class="type">int i=class="num">0,x_size=class="num">20;
  class="type">int buttons_total=class="num">4;
class=class="str">"cmt">//--- 文件的路径
  class="type">class="kw">string icon_file="";
class=class="str">"cmt">//--- 捕获区域中的异常
  m_right_limit=class="num">0;
class=class="str">"cmt">//---
  CButton *button_obj=NULL;
class=class="str">"cmt">//---
  for(class="type">int b=class="num">0; b<buttons_total; b++)
    {
    ...
    else if(b==class="num">1)
      {
      m_button_fullscreen.MainPointer(this);
      class=class="str">"cmt">//--- 如果 (class="num">1) 按钮未启用或 (class="num">2) 它是一个对话框, 离开
      if(!m_fullscreen_button || m_window_type==W_DIALOG)
        class="kw">continue;
      class=class="str">"cmt">//---
      button_obj=::GetPointer(m_button_fullscreen);
      icon_file="Images\\EasyAndFastGUI\\Controls\\full_screen.bmp";
      }
    ...
    }
class=class="str">"cmt">//---
  class="kw">return(true);
  }
class CWindow : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 最小窗口大小
  class="type">int                m_minimum_x_size;
  class="type">int                m_minimum_y_size;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 设置最小窗口大小
  class="type">void               MinimumXSize(class="kw">const class="type">int x_size)            { m_minimum_x_size=x_size;            }
  class="type">void               MinimumYSize(class="kw">const class="type">int y_size)            { m_minimum_y_size=y_size;            }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWindow::CWindow(class="type">void) : m_minimum_x_size(class="num">0),
                         m_minimum_y_size(class="num">0)
  {
...
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 初始化属性                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::InitializeProperties(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">class="kw">string caption_text,class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap)
  {
...
  m_x_size       =(m_x_size<class="num">1)? class="num">200 : m_x_size;
  m_y_size       =(m_y_size<class="num">1)? class="num">200 : m_y_size;
...
  m_minimum_x_size =(m_minimum_x_size<class="num">200)? m_x_size : m_minimum_x_size;

◍ 窗口全屏切换的状态存储逻辑

在自定义 GUI 框架里,CWindow 类用一组私有变量记住进入全屏前的坐标与尺寸:m_last_x、m_last_y 存左上角,m_last_x_size、m_last_y_size 存宽高,m_last_auto_xresize / m_last_auto_yresize 存原来的自适应开关。这样从全屏退回时才能还原到用户之前的布局,而不是丢到默认位置。 切换动作由 OnClickFullScreenButton() 触发。函数先比对按钮的 id 与 index,若不是全屏按钮发来的消息直接返回 false,避免误触。 进入全屏分支时,先把当前 m_x / m_y / m_x_size / m_full_height 以及两个自适应模式写入 m_last_* 变量,再把 m_auto_xresize_mode 和 m_auto_yresize_mode 都置为 true,随后用 Chart 的像素宽高减 2、减 3 做边距把窗口拉满,并把 m_x、m_y 设成 1 后 Moving 到位。外汇与贵金属图表加载此类 EA 有卡顿或坐标错乱风险,建议在 MT5 策略测试器以外用实盘图表手动点一次全屏按钮验证。

MQL5 / C++
class="type">bool CWindow::OnClickFullScreenButton(class="kw">const class="type">int id=WRONG_VALUE,class="kw">const class="type">int index=WRONG_VALUE)
  {
class=class="str">"cmt">//--- 如果有外部调用, 择检查控件的标识符和索引
   class="type">int check_id     =(id!=WRONG_VALUE)? id : CElementBase::Id();
   class="type">int check_index =(index!=WRONG_VALUE)? index : CElementBase::Index();
class=class="str">"cmt">//--- 如果索引不匹配, 离开
   if(check_id!=m_button_fullscreen.Id() || check_index!=m_button_fullscreen.Index())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 如果窗口非全屏
   if(!m_is_fullscreen)
     {
      class=class="str">"cmt">//--- 切换到全屏
      m_is_fullscreen=true;
      class=class="str">"cmt">//--- 获取图表窗口的当前维度
      SetWindowProperties();
      class=class="str">"cmt">//--- 存储窗体的当前坐标和尺寸
      m_last_x           =m_x;
      m_last_y           =m_y;
      m_last_x_size      =m_x_size;
      m_last_y_size      =m_full_height;
      m_last_auto_xresize =m_auto_xresize_mode;
      m_last_auto_yresize =m_auto_yresize_mode;
      class=class="str">"cmt">//--- 启用窗体的自动大小调整
      m_auto_xresize_mode=true;
      m_auto_yresize_mode=true;
      class=class="str">"cmt">//--- 窗体最大化到整个图表
      ChangeWindowWidth(m_chart.WidthInPixels()-class="num">2);
      ChangeWindowHeight(m_chart.HeightInPixels(m_subwin)-class="num">3);
      class=class="str">"cmt">//--- 更新位置
      m_x=m_y=class="num">1;
      Moving(m_x,m_y);
      class=class="str">"cmt">//--- 更换按钮图标
      m_button_fullscreen.IconFile("Images\\EasyAndFastGUI\\Controls\\minimize_to_window.bmp");
      m_button_fullscreen.IconFileLocked("Images\\EasyAndFastGUI\\Controls\\minimize_to_window.bmp");
     }
class=class="str">"cmt">//--- 如果窗口处于全屏
   else
     {

「退出全屏时如何还原窗口状态」

在自定义 GUI 里做全屏切换,难点不在放大,而在退出时能把窗口原样还回去。下面这段逻辑先把全屏标记关掉,再把自动缩放模式恢复成进入全屏前存好的值,避免退出后窗口还被强制拉伸。 m_is_fullscreen=false; m_auto_xresize_mode=m_last_auto_xresize; m_auto_yresize_mode=m_last_auto_yresize; 若自动缩放被禁用,就调用 ChangeWindowWidth(m_last_x_size) 和 ChangeWindowHeight(m_last_y_size) 回到前次尺寸;随后用 Moving(m_x,m_y) 把窗口挪回 m_last_x、m_last_y 记录的旧坐标。 按钮图标也要换回普通全屏图:m_button_fullscreen.IconFile("Images\\EasyAndFastGUI\\Controls\\full_screen.bmp") 连同锁定态一起设,否则界面会停在退出态的视觉上。最后 MouseFocus(false) 并 Update(true) 把焦点从按钮移走,返回 true 完成退出。 事件层上,CHARTEVENT_CUSTOM+ON_CLICK_BUTTON 里若 lparam/dparam 命中全屏按钮就走 OnClickFullScreenButton 提前 return;另在 CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK 中,只要 CursorInsideCaption 判定光标在标题栏内,就直接拿 m_button_fullscreen.Id() 和 Index() 触发同一函数——等于双击标题栏也能退出全屏。 CWindow 类用私有 bool m_xy_resize_mode 记录是否允许拖拽改尺寸,对外暴露 ResizeMode() 常量方法返回它。开 MT5 把这段接进你的面板类,双击标题栏试一下退出还原是否丢坐标。

MQL5 / C++
   class=class="str">"cmt">//--- 切换到上一次窗口大小
   m_is_fullscreen=class="kw">false;
   class=class="str">"cmt">//--- 禁用自动大小调整
   m_auto_xresize_mode=m_last_auto_xresize;
   m_auto_yresize_mode=m_last_auto_yresize;
   class=class="str">"cmt">//--- 如果禁用该模式, 设置为前次的大小
   if(!m_auto_xresize_mode)
      ChangeWindowWidth(m_last_x_size);
   if(!m_auto_yresize_mode)
      ChangeWindowHeight(m_last_y_size);
   class=class="str">"cmt">//--- 更新位置
   m_x=m_last_x;
   m_y=m_last_y;
   Moving(m_x,m_y);
   class=class="str">"cmt">//--- 更换按钮图标
   m_button_fullscreen.IconFile("Images\\EasyAndFastGUI\\Controls\\full_screen.bmp");
   m_button_fullscreen.IconFileLocked("Images\\EasyAndFastGUI\\Controls\\full_screen.bmp");
   }
class=class="str">"cmt">//--- 从按钮中移除焦点
   m_button_fullscreen.MouseFocus(class="kw">false);
   m_button_fullscreen.Update(true);
   class="kw">return(true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 图表事件处理程序                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::OnEvent(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(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
    {
    ...
    class=class="str">"cmt">//--- 检查全屏模式
    if(OnClickFullScreenButton((class="type">uint)lparam,(class="type">uint)dparam))
       class="kw">return;
    ...
    class=class="str">"cmt">//---
    class="kw">return;
    }
  }
class="type">void CWindow::OnEvent(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(id==CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK)
    {
    class=class="str">"cmt">//--- 如果事件在窗口标题中生成
    if(CursorInsideCaption(m_mouse.X(),m_mouse.Y()))
       OnClickFullScreenButton(m_button_fullscreen.Id(),m_button_fullscreen.Index());
    class=class="str">"cmt">//---
    class="kw">return;
    }
  }
class CWindow : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 窗口调整大小模式
   class="type">bool            m_xy_resize_mode;
   class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 调整窗口大小的能力
   class="type">bool            ResizeMode(class="type">void)                class="kw">const { class="kw">return(m_xy_resize_mode); }
交给小布盯盘看面板原型
这些控件结构和尺寸约束小布盯盘的 AIGC 已内置,打开对应品种页即可看到类似可交互面板,你专注策略逻辑而非重复造轮子。

常见问题

本篇在表格控件基础上扩展,文本框和组合框作为单元子控件加入,具体声明与创建见后续技术小节的函数库代码。
小布盯盘内置了同类交互面板原型,可对照查看控件布局与缩放表现,便于你照猫画虎写自己的 EA 界面。
用 CButton::FullscreenButtonIsUsed() 启用全屏按钮,最小尺寸在当前版固定不小于 200x200 像素,由 InitializeProperties 控制。
切换全屏前会把尺寸、坐标和自动调整模式存到私有字段,再次双击或点按钮即按保存值还原到前一次状态。
全屏要填满主图且在图表尺寸变化时跟着变,所以 OnClickFullScreenButton 里会强制启用自动大小调整模式。