图形界面 XI: 表格单元中的文本编辑框和组合框 (统合构建15)·进阶篇
🧩

图形界面 XI: 表格单元中的文本编辑框和组合框 (统合构建15)·进阶篇

(2/3)· 当表格只能放按钮和复选框,参数录入就卡在半路,这篇补齐输入框与下拉选择

偏理论 第 2/3 篇
在 MQL5 自定义面板里手写表格参数录入,很多人卡在只能放静态控件。把文本框和组合框塞进单元格,窗口还能随图表拉伸,这套机制搞懂了界面才真正能用的起来。

窗口拖拽与缩放的底层枚举

在 MT5 自定义 GUI 里,CWindow 类通过 m_xy_resize_mode 这个布尔开关控制是否允许用户拉扯边框。构造函数默认把它置为 false,也就是新建窗口不可缩放,得手动调 ResizeMode(true) 才放开。 鼠标交互状态被收进 ENUM_MOUSE_STATE,从 0 到 4 分别对应未按下、窗内按下、窗外按下、标题栏内按下、边框内按下。其中 PRESSED_INSIDE_BORDER=4 是触发缩放判定的关键值,命中后才会走指针切换逻辑。 ENUM_MOUSE_POINTER 则管光标形态,MP_WINDOW_RESIZE=5 表示整窗拖拽态,MP_XY1_RESIZE=3 与 MP_XY2_RESIZE=4 是斜向拉伸,MP_X_RESIZE_RELATIVE=6 这类带 RELATIVE 的后缀用于相对坐标重算。CreateResizePointer 方法开头就判 m_xy_resize_mode,若没开直接 return(true),所以不调 ResizeMode 前这段代码形同虚设。 开 MT5 新建个面板类继承 CWindow,先 ResizeMode(true) 再断点跟 CreateResizePointer,能直接看到边框命中后指针枚举跳到 5 还是 3/4,比读文档直观。

MQL5 / C++
class="type">void ResizeMode(class="kw">const class="type">bool state) { m_xy_resize_mode=state; }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWindow::CWindow(class="type">void) : m_xy_resize_mode(class="kw">false)
  {
  ...
  }
enum ENUM_MOUSE_STATE
  {
  NOT_PRESSED           =class="num">0,
  PRESSED_INSIDE        =class="num">1,
  PRESSED_OUTSIDE       =class="num">2,
  PRESSED_INSIDE_HEADER =class="num">3,
  PRESSED_INSIDE_BORDER =class="num">4
  };
enum ENUM_MOUSE_POINTER
  {
  MP_CUSTOM             =class="num">0,
  MP_X_RESIZE           =class="num">1,
  MP_Y_RESIZE           =class="num">2,
  MP_XY1_RESIZE         =class="num">3,
  MP_XY2_RESIZE         =class="num">4,
  MP_WINDOW_RESIZE      =class="num">5,
  MP_X_RESIZE_RELATIVE  =class="num">6,
  MP_Y_RESIZE_RELATIVE  =class="num">7,
  MP_X_SCROLL           =class="num">8,
  MP_Y_SCROLL           =class="num">9,
  MP_TEXT_SELECT        =class="num">10
  };
class CWindow : class="kw">public CElement
  {
class="kw">private:
  class="type">bool CreateResizePointer(class="type">void);
  };
class="type">bool CWindow::CreateResizePointer(class="type">void)
  {
  if(!m_xy_resize_mode)
     class="kw">return(true);
  }

「可拖拽窗口的边框命中判定」

在 MT5 自定义面板里做可拖拽窗口,先要给尺寸手柄设好热区:横纵间隙 13 和 11 像素,手柄自身 23×23 像素,类型标成 MP_WINDOW_RESIZE,再调 CreatePointer 挂到图表子窗口上,返回 false 就说明创建失败。 判定鼠标落在哪条边上,靠 ResizeModeIndex 做坐标分桶。边框判定宽度写死 5 像素,上下留 15 像素盲区避开标题栏和角点;左边界 x 在 0~5 且 y 落在标题栏高+15 到总高-15 之间返回 0,右边界对称返回 1,底边 y 在总高-5~总高 且 x 在 15~总宽-15 返回 2,命中的同时把鼠标钳制区标记成边框内按下。 若左键还按着且之前已拿到过合法索引,函数直接复用 m_resize_mode_index,不再重算——这能避免拖拽中因坐标抖动跳边。拖完用 ZeroResizeVariables 把 m_point_fixed 等清零,判定里 m_point_fixed<1 就直接返回,防止下次误判成继续拖拽。 开 MT5 把这段塞进你的 CWindow 派生类,改 width=5 / offset=15 这两个值就能调出更宽或更窄的拖拽感应带,外汇与贵金属图表挂 EA 面板时尤其注意高风险的误触缩放。

MQL5 / C++
m_xy_resize.XGap(class="num">13);
m_xy_resize.YGap(class="num">11);
m_xy_resize.XSize(class="num">23);
m_xy_resize.YSize(class="num">23);
m_xy_resize.Id(CElementBase::Id());
m_xy_resize.Type(MP_WINDOW_RESIZE);
class=class="str">"cmt">//--- 创建控件
if(!m_xy_resize.CreatePointer(m_chart_id,m_subwin))
   class="kw">return(class="kw">false);
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_resize_mode_index;
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 返回窗口大小调整模式的索引
  class="type">int               ResizeModeIndex(class="kw">const class="type">int x,class="kw">const class="type">int y);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回窗口大小调整模式的索引                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CWindow::ResizeModeIndex(class="kw">const class="type">int x,class="kw">const class="type">int y)
  {
class=class="str">"cmt">//--- 如果已拖动, 返回边框索引
  if(m_resize_mode_index!=WRONG_VALUE && m_mouse.LeftButtonState())
     class="kw">return(m_resize_mode_index);
class=class="str">"cmt">//--- 边框的宽度, 偏移量和索引
  class="type">int width  =class="num">5;
  class="type">int offset =class="num">15;
  class="type">int index  =WRONG_VALUE;
class=class="str">"cmt">//--- 检查左边界的焦点
  if(x>class="num">0 && x<width && y>m_caption_height+offset && y<m_y_size-offset)
     index=class="num">0;
class=class="str">"cmt">//--- 检查右边界的焦点
  else if(x>m_x_size-width && x<m_x_size && y>m_caption_height+offset && y<m_y_size-offset)
     index=class="num">1;
class=class="str">"cmt">//--- 检查底部边框的焦点
  else if(y>m_y_size-width && y<m_y_size && x>offset && x<m_x_size-offset)
     index=class="num">2;
class=class="str">"cmt">//--- 如果获得索引, 则标记点击的区域
  if(index!=WRONG_VALUE)
     m_clamping_area_mouse=PRESSED_INSIDE_BORDER;
class=class="str">"cmt">//--- 返回区域索引
  class="kw">return(index);
  }
class CWindow : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 与调整窗口大小相关的变量
  class="type">int               m_x_fixed;
  class="type">int               m_size_fixed;
  class="type">int               m_point_fixed;
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 变量清零
  class="type">void              ZeroResizeVariables(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 重置与调整窗口大小相关的变量                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::ZeroResizeVariables(class="type">void)
  {
class=class="str">"cmt">//--- 如果已经为零, 离开
  if(m_point_fixed<class="num">1)

◍ 窗口缩放光标的判定与切换逻辑

在 MT5 自定义 GUI 里,CWindow 类通过 CheckResizePointer() 把鼠标位置翻译成边框索引,再决定要不要显示拉伸光标。核心先看 m_resize_mode_index=ResizeModeIndex(x,y); 这一步把屏幕坐标映射到 0/1/2 这类边框代号,WRONG_VALUE 表示不在任何可拉伸边上。 当光标对象 m_xy_resize 当前不可见且边框已命中时,代码按索引分配图标:垂直边(0 或 1)用 index=0,水平边(2)用 index=1,随后 ChangeImage(0,index) 换图、Moving() 跟手、Update(true) 重绘并显示。若边框未命中则什么都不做,返回 false。 光标已显示的分支里,只要边框有效就持续 Moving 跟随鼠标;一旦离开边框且左键没按住,立刻 Hide() 并调 ZeroResizeVariables() 清状态,最后 m_chart.Redraw() 刷新。外汇与贵金属图表挂这类面板属高风险操作环境,实盘前应在模拟盘验证光标响应是否卡顿。 把下面这段塞进你的 CWindow 派生类就能直接看效果:命中垂直边走图标 0、水平边走图标 1,没命中且松手就隐藏。开 MT5 用鼠标贴边移动,若图标不切换,优先查 ResizeModeIndex 的命中容差是不是设得过小。

MQL5 / C++
class="type">bool CWindow::CheckResizePointer(class="kw">const class="type">int x,class="kw">const class="type">int y)
  {
class=class="str">"cmt">//--- 确定当前的边框索引
   m_resize_mode_index=ResizeModeIndex(x,y);
class=class="str">"cmt">//--- 如果光标被隐藏
   if(!m_xy_resize.IsVisible())
     {
      class=class="str">"cmt">//--- 如果边框已定义
      if(m_resize_mode_index!=WRONG_VALUE)
        {
         class=class="str">"cmt">//--- 为了判断鼠标指针的显示图标索引
         class="type">int index=WRONG_VALUE;
         class=class="str">"cmt">//--- 如果在垂直边框
         if(m_resize_mode_index==class="num">0 || m_resize_mode_index==class="num">1)
            index=class="num">0;
         class=class="str">"cmt">//--- 如果在水平边框
         else if(m_resize_mode_index==class="num">2)
            index=class="num">1;
         class=class="str">"cmt">//--- 更改图标
         m_xy_resize.ChangeImage(class="num">0,index);
         class=class="str">"cmt">//--- 移动, 重绘和显示
         m_xy_resize.Moving(m_mouse.X(),m_mouse.Y());
         m_xy_resize.Update(true);
         m_xy_resize.Reset();
         class="kw">return(true);
        }
     }
   else
     {
      class=class="str">"cmt">//--- 移动指针
      if(m_resize_mode_index!=WRONG_VALUE)
         m_xy_resize.Moving(m_mouse.X(),m_mouse.Y());
      class=class="str">"cmt">//--- 隐藏光标
      else if(!m_mouse.LeftButtonState())
        {
         class=class="str">"cmt">//--- 隐藏指针并重置变量
         m_xy_resize.Hide();
         ZeroResizeVariables();
        }
      class=class="str">"cmt">//--- 刷新图表
      m_chart.Redraw();
      class="kw">return(true);
     }
class=class="str">"cmt">//---
   class="kw">return(class="kw">false);
  }

拖拽边框时的距离计算与窗口重算

在自定义面板里拖动窗口边框,核心是先由 CheckDragWindowBorder 算出鼠标相对锚点的位移。该函数用 m_resize_mode_index 区分三边:0 为左边框、1 为右边框、2 为底边,分别按 X/Y 轴取差值的整数像素距离。 首次进入(m_point_fixed<1)时会把当前坐标和尺寸锁进 m_x_fixed / m_size_fixed / m_point_fixed,并广播 ON_SET_AVAILABLE 与 ON_CHANGE_GUI 两个自定义事件,通知图表重算可用控件与刷新界面,随后直接 return(0) 不返回位移。 真正拖动中,左边框 distance = 鼠标X - m_x_fixed,右边框 distance = 传入x - m_point_fixed,底边 distance = 传入y - m_point_fixed,返回整型像素值供上层调用。 CalculateAndResizeWindow 拿到 distance 后做实际改尺寸:左边框模式下 new_x = m_x_fixed+distance-m_point_fixed,new_x_size = m_size_fixed-distance+m_point_fixed;若 new_x<1 或 new_x_size 不足 m_minimum_x_size 则直接 return,避免窗口缩到不可见。外汇与贵金属图表挂 EA 做这类 GUI 拖动属高风险操作,参数越界可能造成面板消失,建议在 MT5 策略测试器外先用脚本单图验证。

MQL5 / C++
class="type">int CheckDragWindowBorder(class="kw">const class="type">int x,class="kw">const class="type">int y);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查拖动窗口边框                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CWindow::CheckDragWindowBorder(class="kw">const class="type">int x,class="kw">const class="type">int y)
  {
class=class="str">"cmt">//--- 确定位移距离
   class="type">int distance=class="num">0;
class=class="str">"cmt">//--- 如果边框未被拖动
   if(m_point_fixed<class="num">1)
     {
      class=class="str">"cmt">//--- 如果沿 X 轴调整大小
      if(m_resize_mode_index==class="num">0 || m_resize_mode_index==class="num">1)
        {
         m_x_fixed     =m_x;
         m_size_fixed  =m_x_size;
         m_point_fixed =x;
        }
      class=class="str">"cmt">//--- 如果沿 Y 轴调整大小
      else if(m_resize_mode_index==class="num">2)
        {
         m_size_fixed  =m_y_size;
         m_point_fixed =y;
        }
      class=class="str">"cmt">//--- 发送消息来检测可用控件
      ::EventChartCustom(m_chart_id,ON_SET_AVAILABLE,CElementBase::Id(),class="num">0,"");
      class=class="str">"cmt">//--- 发送有关图形界面变化的消息
      ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),class="num">0,"");
      class="kw">return(class="num">0);
     }
class=class="str">"cmt">//--- 如果这是左边框
   if(m_resize_mode_index==class="num">0)
     distance=m_mouse.X()-m_x_fixed;
class=class="str">"cmt">//--- 如果这是右边框
   else if(m_resize_mode_index==class="num">1)
     distance=x-m_point_fixed;
class=class="str">"cmt">//--- 如果这是底端边框
   else if(m_resize_mode_index==class="num">2)
     distance=y-m_point_fixed;
class=class="str">"cmt">//---返回距离
   class="kw">return(distance);
  }
class CWindow : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 计算并调整窗口大小
   class="type">void              CalculateAndResizeWindow(class="kw">const class="type">int distance);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计算并调整窗口大小                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::CalculateAndResizeWindow(class="kw">const class="type">int distance)
  {
class=class="str">"cmt">//--- 左边框
   if(m_resize_mode_index==class="num">0)
     {
      class="type">int new_x       =m_x_fixed+distance-m_point_fixed;
      class="type">int new_x_size =m_size_fixed-distance+m_point_fixed;
      class=class="str">"cmt">//--- 超过限, 离开
      if(new_x<class="num">1 || new_x_size<=m_minimum_x_size)
        class="kw">return;
      class=class="str">"cmt">//--- 坐标
      CElementBase::X(new_x);

「拖拽边框改窗口尺寸的实现细节」

在 MT5 自定义面板里,窗口三向缩放靠的是对 m_resize_mode_index 的判断:0 为左边框、1 为右边框、2 为下边框。右拖时先用 gap_x2 = m_chart_width - m_mouse.X() - (m_size_fixed - m_point_fixed) 算鼠标到图表右缘的剩余像素,new_x_size 小于 m_minimum_x_size 或 gap_x2<1 就直接 return,避免把面板拖没。 下边框逻辑类似,但阈值更松:gap_y2<2 才拦截,说明纵向至少留 2 像素边距。实际写库时这个 1 和 2 的差别容易忽略,却决定了小窗在图表边缘会不会卡死。 UpdateSize 是统一入口:左键没按就 ZeroResizeVariables 清零退出;CheckDragWindowBorder 返回 0 表示没抓到边也退出。抓到后调 CalculateAndResizeWindow(distance) 改画布,再 EventChartCustom 发 ON_WINDOW_CHANGE_XSIZE / YSIZE 自定义事件,外部 EA 可据此重排控件。

MQL5 / C++
   m_canvas.X_Distance(new_x);
   class=class="str">"cmt">//--- 设置和存储大小
   CElementBase::XSize(new_x_size);
   m_canvas.XSize(new_x_size);
   m_canvas.Resize(new_x_size,m_canvas.YSize());
    }
class=class="str">"cmt">//--- 右边框
   else if(m_resize_mode_index==class="num">1)
    {
     class="type">int gap_x2     =m_chart_width-m_mouse.X()-(m_size_fixed-m_point_fixed);
     class="type">int new_x_size =m_size_fixed+distance;
     class=class="str">"cmt">//--- 超过限, 离开
     if(gap_x2<class="num">1 || new_x_size<=m_minimum_x_size)
        class="kw">return;
     class=class="str">"cmt">//--- 设置和存储大小
     CElementBase::XSize(new_x_size);
     m_canvas.XSize(new_x_size);
     m_canvas.Resize(new_x_size,m_canvas.YSize());
    }
class=class="str">"cmt">//--- 低边框
   else if(m_resize_mode_index==class="num">2)
    {
     class="type">int gap_y2=m_chart_height-m_mouse.Y()-(m_size_fixed-m_point_fixed);
     class="type">int new_y_size=m_size_fixed+distance;
     class=class="str">"cmt">//--- 超过限, 离开
     if(gap_y2<class="num">2 || new_y_size<=m_minimum_y_size)
        class="kw">return;
     class=class="str">"cmt">//--- 设置和存储大小
     m_full_height=new_y_size;
     CElementBase::YSize(new_y_size);
     m_canvas.YSize(new_y_size);
     m_canvas.Resize(m_canvas.XSize(),new_y_size);
    }
   }
class CWindow : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 更新窗口大小
   class="type">void              UpdateSize(class="kw">const class="type">int x,class="kw">const class="type">int y);
   };
class="type">void CWindow::UpdateSize(class="kw">const class="type">int x,class="kw">const class="type">int y)
  {
class=class="str">"cmt">//--- 如果完成并释放鼠标左键, 重置数值
   if(!m_mouse.LeftButtonState())
    {
     ZeroResizeVariables();
     class="kw">return;
    }
class=class="str">"cmt">//--- 如果边框的捕获和移动尚未开始, 离开
   class="type">int distance=class="num">0;
   if((distance=CheckDragWindowBorder(x,y))==class="num">0)
     class="kw">return;
class=class="str">"cmt">//--- 计算并调整窗口大小
   CalculateAndResizeWindow(distance);
class=class="str">"cmt">//--- 重绘窗口
   Update(true);
class=class="str">"cmt">//--- 更新对象的位置
   Moving(m_x,m_y);
class=class="str">"cmt">//--- 窗口大小已更改的消息
   if(m_resize_mode_index==class="num">2)
     ::EventChartCustom(m_chart_id,ON_WINDOW_CHANGE_YSIZE,(class="type">long)CElementBase::Id(),class="num">0,"");
   else
     ::EventChartCustom(m_chart_id,ON_WINDOW_CHANGE_XSIZE,(class="type">long)CElementBase::Id(),class="num">0,"");
   }
class CWindow : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 控制窗口大小
   class="type">void        ResizeWindow(class="type">void);

◍ 窗体拖拽改尺寸的退出条件与坐标判定

CWindow::ResizeWindow 在 MT5 自定义面板里负责鼠标拖动改变窗口尺寸,但函数开头一连串 guard 决定了绝大多数交互根本进不到实际计算。 先判断 IsAvailable() 与鼠标钳制区:若窗口不可用,或 m_clamping_area_mouse 既不是 PRESSED_INSIDE_BORDER 也不是 NOT_PRESSED,直接 return,不处理任何缩放。 接着若 m_xy_resize_mode 为 false、或窗口正处于全屏 m_is_fullscreen、或已最小化 m_is_minimized,也立即退出——这三类状态下强行改尺寸会引发界面抖动。 通过前筛后才取相对坐标 x=m_mouse.RelativeX(m_canvas)、y=m_mouse.RelativeY(m_canvas);随后 CheckResizePointer(x,y) 返回 false 说明指针不在可拉伸边缘,同样离开。 唯一落点就是 UpdateSize(x,y) 按当前指针位置重算宽高。实盘加载这类面板时,若发现拖边框没反应,优先查 m_xy_resize_mode 是否被误关,以及是否意外进了全屏。外汇与贵金属图表挂 EA 面板属高风险操作,参数改动前建议在模拟账户验证。

MQL5 / C++
class="type">void CWindow::ResizeWindow(class="type">void)
  {
class=class="str">"cmt">//--- 如果窗口不可用, 离开
   if(!IsAvailable())
      class="kw">return;
class=class="str">"cmt">//--- 如果鼠标按钮没有在窗体按钮上按下, 离开
   if(m_clamping_area_mouse!=PRESSED_INSIDE_BORDER && m_clamping_area_mouse!=NOT_PRESSED)
      class="kw">return;
class=class="str">"cmt">//--- 如果 (class="num">1) 窗口调整大小模式被禁用或 
class=class="str">"cmt">//    (class="num">2) 窗口处于全屏或 (class="num">3) 窗口最小化, 离开
   if(!m_xy_resize_mode || m_is_fullscreen || m_is_minimized)
      class="kw">return;
class=class="str">"cmt">//--- 坐标
   class="type">int x =m_mouse.RelativeX(m_canvas);
   class="type">int y =m_mouse.RelativeY(m_canvas);
class=class="str">"cmt">//--- 检查是否改变列表的宽度
   if(!CheckResizePointer(x,y))
      class="kw">return;
class=class="str">"cmt">//--- 更新窗口大小
   UpdateSize(x,y);
   }
交给小布盯盘看盘口
这些控件布局与窗口自适应的思路,小布盯盘的 AIGC 已内置到品种页的交互面板里,打开就能直接调参,不必自己从头搭。

常见问题

需要在表格控件初始化后,针对目标单元创建 CEdit 类实例并绑定到单元坐标,由窗口消息循环统一派发绘制与输入事件。
可以,小布盯盘的品种页交互面板已封装类似单元格内编辑与下拉选择,省去自己写 CTable 扩展的成本。
窗体基类里该按钮省缺关闭,需用 CButton::FullscreenButtonIsUsed() 显式启用,且只能在主窗口创建时才有效。
由 CWindow::InitializeProperties() 在控件属性初始化阶段强制下限,避免极小尺寸导致布局错乱与绘制异常。
切换全屏前会把当前尺寸、坐标与自动调整模式存入私有字段,再次双击即从这些字段还原窗体参数。