图形界面 X: 简单快速开发库的更新 (版本 3)·进阶篇
(2/3)· 从指标子窗口到 EA 独立面板的跨越,v3 如何重写界面挂载与消息通信逻辑
EA子窗口编号的运行时校正
在 MT5 的 GUI 类库里,EA 挂载到图表子窗口后,用户手动增删指标会导致窗口序号漂移。CWndEvents::CheckExpertSubwindowNumber 就是专门兜底这种扰动的:每次被调用先比对 ChartGetInteger(CHART_WINDOWS_TOTAL) 的返回值与缓存的 m_subwindows_total,相等就直接 return,避免无谓遍历。 如果子窗口总数变了,代码用两层 for 循环从 0 扫到 subwindows_total-1,再对单个窗口内 ChartIndicatorsTotal 得到的指标数做内循环,用 ChartIndicatorName 取出的短名称去匹配 m_subwindow_shortname。一旦命中且 sw!=m_subwin,就说明 EA 的子窗口被挤到了新位置,此时必须刷新所有控件的所属窗口编号。 下面这段是首次把 EA 子窗口挂上去时的登记逻辑,注意它把 subwindows_total 作为新编号写入 m_subwin,并把 m_subwindows_total 自增 1,失败则打印 GetLastError 编号——外汇与贵金属图表多品种多周期切换频繁,这种自检缺失可能让面板控件画到错误窗口,实盘属于高风险操作环境,建议开 MT5 自己挂个 EA 验证。
if(::ChartIndicatorAdd(m_chart_id,subwindows_total,m_subwindow_handle)) { class=class="str">"cmt">//--- 保存子窗口编号和图表上当前子窗口的数量 m_subwin =subwindows_total; m_subwindows_total =subwindows_total+class="num">1; class=class="str">"cmt">//--- 取得和保存EA交易子窗口的短名称 m_subwindow_shortname=::ChartIndicatorName(m_chart_id,m_subwin,class="num">0); } class=class="str">"cmt">//--- 如果子窗口没有设置 else ::Print(__FUNCTION__,"> 设置EA交易子窗口错误!错误编号: ",::GetLastError()); }
「子窗口丢失时EA如何自删与高度联动」
EA挂载在独立子窗口时,若用户误删该子窗口,代码里用 is_subwindow 标记判断;一旦为假就调 ExpertRemove() 终止自身,并在终端打印「删除了EA交易的子窗口导致需要删除EA交易!」。外汇/贵金属图表操作频繁,这种自清理能避免空指针报错,但高频重绘场景仍属高风险。 窗口尺寸变化靠两个自定义事件宏区分:ON_WINDOW_CHANGE_XSIZE 值为 3,ON_WINDOW_CHANGE_YSIZE 值为 4。CWindow::ChangeWindowHeight 在高度真变化且未最小化时,改写背景尺寸并广播 YSIZE 事件,外部可据此重排控件。 控件基类 CElement 留了 m_auto_yresize_mode 开关,AutoYResizeMode() 读写该标志。开启后子控件可随父窗口高度自动伸缩,省去手动遍历。 别让自删逻辑吞掉调试信息 生产环境若把 ExpertRemove() 前的 Print 注释掉,子窗口被删时EA静默消失,排查可能要多花数小时。
m_subwin=sw; class=class="str">"cmt">//--- 在界面中主表单的所有控件中保存 class="type">int elements_total=CWndContainer::ElementsTotal(class="num">0); for(class="type">int e=class="num">0; e<elements_total; e++) m_wnd[class="num">0].m_elements[e].SubwindowNumber(m_subwin); } uclass=class="str">"cmt">//--- class="kw">break; } } class=class="str">"cmt">//--- 如果没有找到EA交易子窗口,删除EA交易 if(!is_subwindow) { ::Print(__FUNCTION__," > 删除了EA交易的子窗口导致需要删除EA交易!"); class=class="str">"cmt">//--- 在图表中删除EA交易 ::ExpertRemove(); } } class="macro">#define ON_WINDOW_CHANGE_XSIZE(class="num">3) class=class="str">"cmt">// 在X轴上改变了窗口的大小 class="macro">#define ON_WINDOW_CHANGE_YSIZE(class="num">4) class=class="str">"cmt">// 在Y轴上改变了窗口的大小 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于创建控件的表单类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CWindow : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 管理大小 class="type">void ChangeWindowHeight(class="kw">const class="type">int height); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 改变窗口的高度 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWindow::ChangeWindowHeight(class="kw">const class="type">int height) { class=class="str">"cmt">//--- 如果高度没有改变,退出 if(height==m_bg.YSize()) class="kw">return; class=class="str">"cmt">//--- 如果窗口是最小化的,退出 if(m_is_minimized) class="kw">return; class=class="str">"cmt">//--- 更新背景的高度 CElement::YSize(height); m_bg.YSize(height); m_bg.Y_Size(height); m_bg_full_height=height; class=class="str">"cmt">//--- 生成窗口大小已经改变的消息 ::EventChartCustom(m_chart_id,ON_WINDOW_CHANGE_YSIZE,(class="type">long)CElement::Id(),class="num">0,""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 控件基类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CElement { class="kw">protected: class=class="str">"cmt">//--- 控件自动改变大小的模式 class="type">bool m_auto_yresize_mode; uclass=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- (class="num">1) 控件自动改变高度的模式 class="type">bool AutoYResizeMode(class="type">void) class="kw">const { class="kw">return(m_auto_yresize_mode); } class="type">void AutoYResizeMode(class="kw">const class="type">bool flag) { m_auto_yresize_mode=flag; } }; class=class="str">"cmt">//--- 图表属性改变事件 if(id==CHARTEVENT_CHART_CHANGE)
◍ 跟随图表底边自适应表格高度
在 MT5 自定义控件里,让表格随主图窗口底边收缩是个高频需求。核心入口是 CCanvasTable::ChangeHeightByBottomWindowSide,它先判断 m_anchor_bottom_window_side 锚点是否开启——若已开启则直接 return,避免和底部锚定逻辑打架。 自适应高度的计算靠两行:x_size 在自动扩宽模式下取窗口右边界减区域左界再减右侧偏移,y_size 取窗口底界减区域上界再减底部偏移。代码里写了硬下限——y_size 小于 60 像素就退出,不强行重绘,防止图表极小时控件畸变。 滚动条的重算也在这函数内完成。水平滚动条 Y 坐标落到区域底减自身宽度,垂直滚动条根据是否共存水平条给 0 或 2 像素偏移;若根本不需要垂直滚动条,就 Hide 掉并把水平条宽度铺满区域。开 MT5 把 m_auto_yresize_bottom_offset 设成 3,拖拽图表窗口高度,能看到表格底边贴着窗口底留 3 像素缝。
class="type">void CCanvasTable::ChangeHeightByBottomWindowSide(class="type">void) { class=class="str">"cmt">//--- 如果启用了表单底部锚点的模式,退出 if(m_anchor_bottom_window_side) class="kw">return; class=class="str">"cmt">//--- 坐标 class="type">int y=class="num">0; class=class="str">"cmt">//--- 大小 class="type">int x_size=(m_auto_xresize_mode)? m_wnd.X2()-m_area.X()-m_auto_xresize_right_offset : m_x_size; class="type">int y_size=m_wnd.Y2()-m_area.Y()-m_auto_yresize_bottom_offset; class=class="str">"cmt">//--- 如果大小小于指定值,退出 if(y_size<class="num">60) class="kw">return; class=class="str">"cmt">//--- 设置表格背景新的大小 ChangeMainSize(x_size,y_size); class=class="str">"cmt">//--- 计算表格大小 CalculateTableSize(); class=class="str">"cmt">//--- 检查滚动条是否存在 class="type">bool is_scrollh=!(m_table_visible_x_size>=m_table_x_size); class="type">bool is_scrollv=!(m_table_visible_y_size>=m_table_y_size); class=class="str">"cmt">//--- 相对于滚动条存在时的距离 class="type">int offset=(is_scrollh || (!is_scrollh && !is_scrollv))? class="num">0 : class="num">2; class=class="str">"cmt">//--- 计算和设置水平滚动条新的坐标 y=m_area.Y2()-m_scrollh.ScrollWidth(); m_scrollh.YDistance(y); class=class="str">"cmt">//--- 为新的大小初始化水平滚动条 m_scrollh.Reinit(m_table_x_size,m_table_visible_x_size); class=class="str">"cmt">//--- 计算和设置垂直滚动条的高度 m_scrollv.Reinit(m_table_y_size,m_table_visible_y_size-offset); m_scrollv.ChangeYSize((is_scrollh)? m_table_visible_y_size+class="num">2 : m_table_visible_y_size); class=class="str">"cmt">//--- 如果不需要垂直滚动条 if(!is_scrollv) { class=class="str">"cmt">//--- 隐藏垂直滚动条 m_scrollv.Hide(); class=class="str">"cmt">//--- 改变水平滚动条的宽度 m_scrollh.ChangeXSize(m_area.XSize());
窗口拉伸时控件怎样跟着重排
在自定义 GUI 框架里,窗口尺寸变化不是只改个背景就完事,内部控件必须按规则重排。上面这段逻辑先处理滚动条:垂直滚动条可见时,水平滚动条的宽度要扣掉竖条占用的 ScrollWidth()+1 像素,避免两者重叠。
事件类 CWndEvents 把 X/Y 两个方向的拉伸拆成两个独立处理函数。以 OnWindowChangeXSize 为例,它先校验自定义事件号是否为 CHARTEVENT_CUSTOM+ON_WINDOW_CHANGE_XSIZE,再比对 m_lparam 与当前活动窗口 ID,不匹配直接 return,防止误改别的窗口。
真正改宽度时,代码遍历该窗口下全部元素(ElementsTotal(awi) 个),遇到类名为 "CWindow" 的跳过,其余若开了 AutoXResizeMode() 就调用 ChangeWidthByRightWindowSide() 贴右边界自适应。这一套跑下来,面板里的按钮、编辑框才可能随窗体拖拽平滑变形。
开 MT5 新建个带 CWindow 容器的指标,给子控件设 AutoXResizeMode(true),拖一下窗口右侧就能验证上面的重排是否生效;外汇与贵金属图表插件开发同样涉及这类 UI 风险,未处理好坐标偏移可能导致控件错位。
} else { class=class="str">"cmt">//--- 显示垂直滚动条 if(CElement::IsVisible()) m_scrollv.Show(); class=class="str">"cmt">//--- 计算和改变水平滚动条的宽度 m_scrollh.ChangeXSize(m_area.XSize()-m_scrollv.ScrollWidth()+class="num">1); } class=class="str">"cmt">//--- 管理水平滚动条是否可见 if(CElement::IsVisible()) { if(!is_scrollh) m_scrollh.Hide(); else m_scrollh.Show(); } class=class="str">"cmt">//--- 改变表格的大小 ChangeTableSize(m_table_x_size,m_table_y_size,m_table_visible_x_size,m_table_visible_y_size-offset); class=class="str">"cmt">//--- 绘制表格 DrawTable(); class=class="str">"cmt">//--- 更新对象的位置 Moving(m_wnd.X(),m_wnd.Y()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于事件处理的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CWndEvents : class="kw">public CWndContainer { class="kw">private: class=class="str">"cmt">//--- 处理改变窗口大小 class="type">bool OnWindowChangeXSize(class="type">void); class="type">bool OnWindowChangeYSize(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ON_WINDOW_CHANGE_XSIZE 事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CWndEvents::OnWindowChangeXSize(class="type">void) { class=class="str">"cmt">//--- 如果信号是 "改变控件大小" if(m_id!=CHARTEVENT_CUSTOM+ON_WINDOW_CHANGE_XSIZE) class="kw">return(false); class=class="str">"cmt">//--- 活动窗口的索引 class="type">int awi=m_active_window_index; class=class="str">"cmt">//--- 如果窗口ID匹配 if(m_lparam!=m_windows[awi].Id()) class="kw">return(true); class=class="str">"cmt">//--- 改变所有控件的大小,表单除外 class="type">int elements_total=CWndContainer::ElementsTotal(awi); for(class="type">int e=class="num">0; e<elements_total; e++) { class=class="str">"cmt">//--- 如果它是窗口,就转到下一个 if(m_wnd[awi].m_elements[e].ClassName()=="CWindow") class="kw">continue; class=class="str">"cmt">//--- 如果启用了模式,调整宽度 if(m_wnd[awi].m_elements[e].AutoXResizeMode()) m_wnd[awi].m_elements[e].ChangeWidthByRightWindowSide(); } uclass=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ON_WINDOW_CHANGE_YSIZE 事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CWndEvents::OnWindowChangeYSize(class="type">void) { class=class="str">"cmt">//--- 如果信号是 "改变控件大小" if(m_id!=CHARTEVENT_CUSTOM+ON_WINDOW_CHANGE_YSIZE) class="kw">return(false); class=class="str">"cmt">//--- 活动窗口的索引 class="type">int awi=m_active_window_index; class=class="str">"cmt">//--- 如果窗口ID匹配 if(m_lparam!=m_windows[awi].Id()) class="kw">return(true); class=class="str">"cmt">//--- 改变所有控件的大小,表单除外 class="type">int elements_total=CWndContainer::ElementsTotal(awi); for(class="type">int e=class="num">0; e<elements_total; e++) { class=class="str">"cmt">//--- 如果它是窗口,就转到下一个 if(m_wnd[awi].m_elements[e].ClassName()=="CWindow")
「锚点决定了控件贴边方式」
在 MT5 自定义面板库里,控件是否跟随窗口右侧或底部边界,由 CElement 基类里的两个布尔成员控制:m_anchor_right_window_side 与 m_anchor_bottom_window_side。它们通过 AnchorRightWindowSide() / AnchorBottomWindowSide() 读写,直接决定重绘时坐标是相对左上还是相对右下计算。 CWndEvents::ChartEventCustom() 中拦截了 OnWindowChangeXSize() 与 OnWindowChangeYSize() 两个自定义事件,一旦收到信号就 return,说明 X/Y 轴尺寸变更是独立通道处理的,不会和最小化、上下文菜单等信号混在一起。 复选框标签的落点最能看出锚点作用:当右锚开启,标签 x 取 m_x - m_label_x_gap;底锚开启,y 取 m_y - m_label_y_gap。也就是说勾上锚点后,控件是往窗口内缩,而不是往外扩。 XGap 的赋值也分了叉——右锚时直接写 x,非右锚时写 x - m_wnd.X(),这行决定了标签和窗口左边界的像素差。外汇和贵金属图表里多屏、缩放很频繁,锚点设错会让面板在高分屏上漂走,属于高概率踩坑点。
class="type">void CWndEvents::ChartEventCustom(class="type">void) { class=class="str">"cmt">//--- 如果信号是在X轴上改变控件的大小 if(OnWindowChangeXSize()) class="kw">return; class=class="str">"cmt">//--- 如果信号是在Y轴上改变控件的大小 if(OnWindowChangeYSize()) class="kw">return; } class CElement { class="kw">protected: class="type">bool m_anchor_right_window_side; class="type">bool m_anchor_bottom_window_side; class="kw">public: class="type">bool AnchorRightWindowSide(class="type">void) class="kw">const { class="kw">return(m_anchor_right_window_side); } class="type">void AnchorRightWindowSide(class="kw">const class="type">bool flag) { m_anchor_right_window_side=flag; } class="type">bool AnchorBottomWindowSide(class="type">void) class="kw">const { class="kw">return(m_anchor_bottom_window_side); } class="type">void AnchorBottomWindowSide(class="kw">const class="type">bool flag) { m_anchor_bottom_window_side=flag; } }; class="type">bool CCheckBox::CreateLabel(class="type">void) { class="type">class="kw">string name=CElement::ProgramName()+"_checkbox_lable_"+(class="type">class="kw">string)CElement::Id(); class=class="str">"cmt">//--- 坐标 class="type">int x =(m_anchor_right_window_side)? m_x-m_label_x_gap : m_x+m_label_x_gap; class="type">int y =(m_anchor_bottom_window_side)? m_y-m_label_y_gap : m_y+m_label_y_gap; class=class="str">"cmt">//--- 到边缘的距离 m_label.XGap((m_anchor_right_window_side)? x : x-m_wnd.X());
◍ 复选框与按钮的锚点定位逻辑
MT5 自定义控件里,复选框 CCheckBox 的 Moving() 方法把「锚定到右 / 底边缘」和「自由坐标」分两套写法。右锚时所有子对象 X 坐标统一用 m_wnd.X2() 减去各自的 XGap(),底锚时 Y 用 m_wnd.Y2() 减 YGap();非锚定时直接拿传入的 x、y 加偏移量。这样窗口缩放后控件能贴边不漂,读者开 MT5 把 m_anchor_right_window_side 改成 true 就能看到贴右效果。 Show() 里有个细节容易漏:按钮可见性置位后必须再调一次 Moving(m_wnd.X(),m_wnd.Y()) 重算坐标,否则第一次显示可能落在旧位置。源码里宏 ON_CLICK_TAB 定义为 27,代表分页切换事件 ID,写面板交互时可直接复用这个编号。 实际调参时建议把 XGap / YGap 打印出来核对,外汇与贵金属图表高波动下窗口频繁重绘,锚点算错会让控件叠到 K 线上,干扰价格行为判读,属于高风险环境下的界面瑕疵。
m_label.YGap((m_anchor_bottom_window_side)? y : y-m_wnd.Y()); class=class="str">"cmt">//--- 初始化渐变色数组 CElement::InitColorArray(label_color,m_label_color_hover,m_label_color_array); class=class="str">"cmt">//--- 保存对象指针 CElement::AddToArray(m_label); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 移动控件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CCheckBox::Moving(class="kw">const class="type">int x,class="kw">const class="type">int y) { class=class="str">"cmt">//--- 如果元件是隐藏的, 退出 if(!CElement::IsVisible()) class="kw">return; class=class="str">"cmt">//--- 如果锚点在右边 if(m_anchor_right_window_side) { class=class="str">"cmt">//--- 在元件栏位中保存坐标 CElement::X(m_wnd.X2()-XGap()); class=class="str">"cmt">//--- 在对象栏位中保存坐标 m_area.X(m_wnd.X2()-m_area.XGap()); m_check.X(m_wnd.X2()-m_check.XGap()); m_label.X(m_wnd.X2()-m_label.XGap()); } else { class=class="str">"cmt">//--- 在对象栏位中保存坐标 CElement::X(x+XGap()); class=class="str">"cmt">//--- 在对象栏位中保存坐标 m_area.X(x+m_area.XGap()); m_check.X(x+m_check.XGap()); m_label.X(x+m_label.XGap()); } class=class="str">"cmt">//--- 如果锚点在底部 if(m_anchor_bottom_window_side) { class=class="str">"cmt">//--- 在元件栏位中保存坐标 CElement::Y(m_wnd.Y2()-YGap()); class=class="str">"cmt">//--- 在对象栏位中保存坐标 m_area.Y(m_wnd.Y2()-m_area.YGap()); m_check.Y(m_wnd.Y2()-m_check.YGap()); m_label.Y(m_wnd.Y2()-m_label.YGap()); } else { class=class="str">"cmt">//--- 在对象栏位中保存坐标 CElement::Y(y+YGap()); class=class="str">"cmt">//--- 在对象栏位中保存坐标 m_area.Y(y+m_area.YGap()); m_check.Y(y+m_check.YGap()); m_label.Y(y+m_label.YGap()); } class=class="str">"cmt">//--- 更新图形对象的坐标 m_area.X_Distance(m_area.X()); m_area.Y_Distance(m_area.Y()); m_check.X_Distance(m_check.X()); m_check.Y_Distance(m_check.Y()); m_label.X_Distance(m_label.X()); m_label.Y_Distance(m_label.Y()); } class="macro">#define ON_CLICK_TAB(class="num">27) class=class="str">"cmt">// 切换页面 class="type">void CSimpleButton::Show(class="type">void) { class=class="str">"cmt">//--- 如果元件已经可见, 退出 if(CElement::IsVisible()) class="kw">return; class=class="str">"cmt">//--- 使得所有对象可见 m_button.Timeframes(OBJ_ALL_PERIODS); class=class="str">"cmt">//--- 可见状态 CElement::IsVisible(true); class=class="str">"cmt">//--- 更新对象的位置 Moving(m_wnd.X(),m_wnd.Y()); }