图形界面表格交互基础:文本框组合框与窗体缩放的函数库实现(基础篇)
(1/3)·从零搭建可编辑表格控件,先吃透窗体缩放与单元嵌入的文本框、组合框底层逻辑
在表格单元里塞进可编辑控件
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() 里写死的下限不会因图表分辨率变化而松动,想在小于此值的面板里塞表格,只会得到裁剪或报错,调参前先确认控件最小占用。
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 和图标路径就能验证按钮阵列的渲染顺序。
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 策略测试器以外用实盘图表手动点一次全屏按钮验证。
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 把这段接进你的面板类,双击标题栏试一下退出还原是否丢坐标。
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); }