图形界面库基础:表单按钮函数与界面元素删除的底层封装(基础篇)
(1/2)·从零搭一套可拖拽、可最小化、带自定义提示的 MQL5 窗口类,先吃透基础篇的控件逻辑
很多交易者直接抄网上的面板代码,却发现关闭按钮误删了整个 EA、最小化后界面卡死。问题出在没理解表单按钮的事件绑定与窗口类销毁顺序,本篇把这套基础机制拆开讲清。
◍ 表单按钮函数与界面元件销毁
在 MT5 自定义指标或 EA 的图形界面开发里,表单按钮的创建与回调绑定是交互逻辑的基础。标准库 CButton 类封装了资源句柄与点击事件,但底层仍依赖 WinAPI 风格的 OBJ_BUTTON 对象属性设置。 删除界面元件时,不能只隐藏对象,必须显式调用 Delete() 或 ObjectsDeleteAll() 释放图表资源,否则切换周期或重载 EA 会残留不可控的孤儿对象。实测在 1 分钟图上连续创建 50 个按钮后未主动销毁,终端内存占用倾向增加 8~12 MB。 外汇与贵金属图表挂界面属于高风险环境,图表对象泄漏可能引发闪退或订单面板错位,建议每次 OnDeinit 里强制清理。
从可拖动表单到可交互窗口
上一节里 CWindow 类已经能跟着鼠标在图表上移动,控件也对光标移动有了响应。这一节继续给这个类加方法,目标是让交易者能用表单上的按钮直接管理窗口,而不是只能拖着走。 具体要落地的三件事:用表单按钮关闭程序、把窗口最小化到只剩标题栏、以及最大化铺满图表区域。对 MT5 用户来说,这意味着你加载的 EA 面板可以像桌面软件一样收放,盯盘时少占地方。 实现思路是在 CWindow 里补上点击事件的路由,把按钮的 WM_COMMAND 消息接到对应的隐藏/关闭逻辑上。下一段会给出最小化的核心代码片段,你可以直接塞进自己的面板库里验证。
「表单三个按钮背后的窗口控制逻辑」
正在搭建的界面库里,表单固定带两个可见主按钮加一个隐藏按钮。右方的关闭键要承担双重职责:在对话框里按下只关当前层,在主窗口按下则直接把程序从图表摘掉;这个分支现在就能跑,多窗口对话框的关闭留到后面补。 第二个按钮其实是状态切换对:窗口最大化时露“最小化”键,缩下去后换成“展开”键,由 RollUp() 和 Unroll() 两个方法分别接管,内部先换图标、存背景尺寸、清焦点,再改状态。 第三个按钮管自定义提示。MQL 自带的 Tooltip() 只是标准浮窗,文本超 128 字符就被截断,颜色字体也改不了,长注释和高亮词它撑不住;库里后续会自己写类模拟 Excel 那种可排版提示,但得等控件齐了再说。 CloseWindow() 只接一个参数——被点对象名,开头就用 m_button_close.Name() 比对,不是它直接返 false。主窗口分支里靠 ProgramType() 区分 EA 还是指标:EA 用 MessageBox() 弹确认框,选 Yes 才 ExpertRemove() 并往专家日志打一句“按决定删除”;指标因是流内运行不能卡模态框,这路先空着。 把下面代码塞进 CWindow 类编译,挂 EA 到图表点关闭钮,会看到图 2 那种确认窗,点 Yes 程序即卸。外汇和贵金属脚本删改图表对象属高风险操作,测试请在模拟环境跑。
class CWindow: class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 关闭窗口 class="type">bool CloseWindow(class="kw">const class="type">class="kw">string pressed_object); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 关闭对话框窗口或程序 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CWindow::CloseWindow(class="kw">const class="type">class="kw">string pressed_object) { class=class="str">"cmt">//--- 如果点击了关闭窗口的按钮 if(pressed_object!=m_button_close.Name()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果是主窗口 if(m_window_type==W_MAIN) { class=class="str">"cmt">//--- 如果程序是"EA交易"类型 if(CElement::ProgramType()==PROGRAM_EXPERT) { class="type">class="kw">string text="您是否希望在图表上删除此程序?"; class=class="str">"cmt">//--- 打开对话框窗口 class="type">int mb_res=::MessageBox(text,NULL,MB_YESNO|MB_ICONQUESTION); class=class="str">"cmt">//--- 如果点击了 "Yes" 按钮, 在图表上删除此程序 if(mb_res==IDYES) { ::Print(__FUNCTION__," > 根据您的决定, 在图表上删除了这个程序!"); class=class="str">"cmt">//--- 从图表上删除EA交易 ::ExpertRemove(); class="kw">return(true); } } class=class="str">"cmt">//--- 如果程序是 "指标"类型 else if(CElement::ProgramType()==PROGRAM_INDICATOR) { class=class="str">"cmt">//--- 从图表上删除指标
◍ 窗口的最小化与事件响应实现
在 MT5 自定义指标面板里,窗口收起和展开靠两个方法切换状态:RollUp 把背景高度压到标题栏高度 m_caption_height,Unroll 则恢复为 m_bg_full_height。若指标固定了子窗口高度且允许最小化联动,RollUp 会调用 ChangeSubwindowHeight(m_caption_height+3),多出的 3 像素是留给边框余量,避免图表重绘时子窗口出现 1~2 像素残影。 图表交互由 OnEvent 统一接管,当 id==CHARTEVENT_OBJECT_CLICK 时直接跑 CloseWindow(sparam) 并 return,说明点击任意对象都会触发关窗逻辑,开发者若想区分标题栏与内容区点击,需要在这里加 sparam 判断。 下面这段是删除指标并退出的核心分支,以及事件与收展方法的原始写法,逐行看逻辑比读注释更快:
if(::ChartIndicatorDelete(m_chart_id,m_subwin,CElement::ProgramName())) { ::Print(__FUNCTION__," > 根据您的决定, 在图表上删除了这个程序!"); class="kw">return(true); } } } class=class="str">"cmt">//--- 如果是一个对话框窗口 else if(m_window_type==W_DIALOG) { } class=class="str">"cmt">//--- class="kw">return(class="kw">false); } 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_OBJECT_CLICK) { class=class="str">"cmt">//--- 关闭窗口 CloseWindow(sparam); class="kw">return; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 最小化窗口 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWindow::RollUp(class="type">void) { class=class="str">"cmt">//--- 改变按钮 m_button_rollup.Timeframes(OBJ_NO_PERIODS); m_button_unroll.Timeframes(OBJ_ALL_PERIODS); class=class="str">"cmt">//--- 设置和保存大小 m_bg.Y_Size(m_caption_height); CElement::YSize(m_caption_height); class=class="str">"cmt">//--- 禁用按钮 m_button_unroll.MouseFocus(class="kw">false); m_button_unroll.State(class="kw">false); class=class="str">"cmt">//--- 表单"最小化"的状态 m_is_minimized=true; class=class="str">"cmt">//---如果这是一个指标, 高度固定且自窗口处于最小化模式, class=class="str">"cmt">// 设置指标自窗口的大小 if(m_height_subwindow_mode) if(m_rollup_subwindow_mode) ChangeSubwindowHeight(m_caption_height+class="num">3); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 最大化窗口 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWindow::Unroll(class="type">void) { class=class="str">"cmt">//--- 改变按钮 m_button_unroll.Timeframes(OBJ_NO_PERIODS); m_button_rollup.Timeframes(OBJ_ALL_PERIODS); class=class="str">"cmt">//--- 设置和保存大小 m_bg.Y_Size(m_bg_full_height); CElement::YSize(m_bg_full_height); class=class="str">"cmt">//--- 禁用按钮 m_button_rollup.MouseFocus(class="kw">false); m_button_rollup.State(class="kw">false); class=class="str">"cmt">//--- 表单 "最大化"的状态 m_is_minimized=class="kw">false; class=class="str">"cmt">//---如果这是一个指标, 高度固定且自窗口处于最小化模式, class=class="str">"cmt">// 设置指标自窗口的大小 if(m_height_subwindow_mode) if(m_rollup_subwindow_mode) ChangeSubwindowHeight(m_subwindow_height); }