图形界面 XI: 表格单元中的文本编辑框和组合框 (统合构建15)·进阶篇
(2/3)· 当表格只能放按钮和复选框,参数录入就卡在半路,这篇补齐输入框与下拉选择
窗口拖拽与缩放的底层枚举
在 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,比读文档直观。
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 面板时尤其注意高风险的误触缩放。
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 的命中容差是不是设得过小。
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 策略测试器外先用脚本单图验证。
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 可据此重排控件。
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 面板属高风险操作,参数改动前建议在模拟账户验证。
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); }