图形界面 I: 在MetaTrader 4终端中使用不同类型的程序来测试界面库 (第五章)·进阶篇
(2/3)· 只靠EA在MT5里验证界面库远远不够,指标子窗口异常和MT4兼容性才是真正分水岭
◍ 子窗口排他逻辑与窗口宽度热改
在自定义指标类里,子窗口编号的识别必须放在构造函数中第一时间完成。若程序类型不是指标(PROGRAM_TYPE != PROGRAM_INDICATOR),直接 return,避免在非指标环境下误调用 ChartWindowFind 导致无意义报错。 DetermineSubwindow 方法通过 ChartWindowFind 拿编号,若返回值小于 0 说明识别失败,打印错误码并退出;若编号大于 0 表示处于子窗口,再用 ChartIndicatorsTotal 取该子窗口指标总数。当 total != 1 时,说明子窗口已被占用,代码会取最后一个指标名并调用 ChartIndicatorDelete 把自己删掉——这保证了同一子窗口只挂一个指标实例。 窗口宽度支持运行时修改。ChangeWindowWidth 先比对当前背景宽度,无变化就退出;否则同步更新 CElement 基类、背景 m_bg 及标题栏 m_caption_bg 的 XSize 与 X_Size。实测中改 width 参数后,界面重绘会立刻反映新尺寸,不用重启指标。 外汇与贵金属图表加载这类自定义指标存在高波动风险,子窗口排他只是避免资源冲突,不预示任何价格方向。
class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { program.ChartEvent(id,lparam,dparam,sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+ class CWndEvents : class="kw">public CWndContainer { class="kw">private: class=class="str">"cmt">//--- 识别子窗口编号 class="type">void DetermineSubwindow(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 识别子窗口编号 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::DetermineSubwindow(class="type">void) { class=class="str">"cmt">//--- 如果程序类型不是指标, 退出 if(PROGRAM_TYPE!=PROGRAM_INDICATOR) class="kw">return; class=class="str">"cmt">//--- 重置最后的错误代码 ::ResetLastError(); class=class="str">"cmt">//--- 识别指标窗口编号 m_subwin=::ChartWindowFind(); class=class="str">"cmt">//--- 如果识别编号出错, 退出 if(m_subwin<class="num">0) { ::Print(__FUNCTION__," > 识别子窗口编号错误: ",::GetLastError()); class="kw">return; } class=class="str">"cmt">//--- 如果这不是图表的主窗口 if(m_subwin>class="num">0) { class=class="str">"cmt">//--- 取得指定子窗口中指标的总数 class="type">int total=::ChartIndicatorsTotal(m_chart_id,m_subwin); class=class="str">"cmt">//--- 取得列表中最后一个指标的短名称 class="type">class="kw">string indicator_name=::ChartIndicatorName(m_chart_id,m_subwin,total-class="num">1); class=class="str">"cmt">//--- 如果子窗口中已经有了一个指标, 从图表中删除程序 if(total!=class="num">1) { ::Print(__FUNCTION__," > 这个子窗口中已经包含了一个指标."); ::ChartIndicatorDelete(m_chart_id,m_subwin,indicator_name); class="kw">return; } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CWndEvents::CWndEvents(class="type">void) { class=class="str">"cmt">//--- 识别子窗口编号 DetermineSubwindow(); } class CWindow : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 修改窗口的宽度 class="type">void ChangeWindowWidth(class="kw">const class="type">int width); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 修改窗口的宽度 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWindow::ChangeWindowWidth(class="kw">const class="type">int width) { class=class="str">"cmt">//--- 如果宽度没有改变, 就退出 if(width==m_bg.XSize()) class="kw">return; class=class="str">"cmt">//--- 更新背景和表头的宽度 CElement::XSize(width); m_bg.XSize(width); m_bg.X_Size(width); m_caption_bg.XSize(width); m_caption_bg.X_Size(width);
面板按钮坐标与窗口自适应逻辑
在自定义控件库里,标题栏右侧的关闭、最大化、最小化以及提示按钮都不是写死位置的,而是每次根据元素右边界减去各自偏移量来重算 X 坐标。下面这段就是刷新边界的典型写法,三个按钮依次贴着右边排布,提示按钮仅在启用时才参与布局。
class=class="str">"cmt">//--- 关闭按钮 class="type">int x=CElement::X2()-CLOSE_BUTTON_OFFSET; m_button_close.X(x); m_button_close.XGap(x-m_x); m_button_close.X_Distance(x); class=class="str">"cmt">//--- 最大化按钮 x=CElement::X2()-ROLL_BUTTON_OFFSET; m_button_unroll.X(x); m_button_unroll.XGap(x-m_x); m_button_unroll.X_Distance(x); class=class="str">"cmt">//--- 最小化按钮 m_button_rollup.X(x); m_button_rollup.XGap(x-m_x); m_button_rollup.X_Distance(x); class=class="str">"cmt">//--- 工具提示按钮 (如果已启用) if(m_tooltips_button) { x=CElement::X2()-TOOLTIP_BUTTON_OFFSET; m_button_tooltip.X(x); m_button_tooltip.XGap(x-m_x); m_button_tooltip.X_Distance(x); } }
CElement::X2() 减关闭偏移定 x,再把 x 同时赋给按钮的 X(绝对坐标)、XGap(与锚点间距)、X_Distance(绘制距离),保证拖动窗口时按钮始终吸附右上角。最大化按钮复用同一套减法,只是偏移常量换成 ROLL_BUTTON_OFFSET;最小化按钮直接沿用上一步的 x,三者紧挨。
图表缩放时靠 OnEvent 接 CHARTEVENT_CHART_CHANGE 重排。若 WindowMode==FULL,表单宽度被设成 m_chart.WidthInPixels()-2;若 RIGHT,则 X 坐标等于图表像素宽减去表单自身宽加 1,实现右贴边。
CreateWindow 里给出了具体尺寸数据:非全屏模式下 x_size 固定 205 像素,y_size 为 243 像素;全屏则宽度跟随图表。外汇与贵金属图表加载此类 EA 面板存在脚本冲突导致卡顿的高风险,建议在 MT5 策略测试器外的实盘图表先单窗口验证。
class=class="str">"cmt">//--- 更新坐标和所有按钮的边界: class=class="str">"cmt">//--- 关闭按钮 class="type">int x=CElement::X2()-CLOSE_BUTTON_OFFSET; m_button_close.X(x); m_button_close.XGap(x-m_x); m_button_close.X_Distance(x); class=class="str">"cmt">//--- 最大化按钮 x=CElement::X2()-ROLL_BUTTON_OFFSET; m_button_unroll.X(x); m_button_unroll.XGap(x-m_x); m_button_unroll.X_Distance(x); class=class="str">"cmt">//--- 最小化按钮 m_button_rollup.X(x); m_button_rollup.XGap(x-m_x); m_button_rollup.X_Distance(x); class=class="str">"cmt">//--- 工具提示按钮 (如果已启用) if(m_tooltips_button) { x=CElement::X2()-TOOLTIP_BUTTON_OFFSET; m_button_tooltip.X(x); m_button_tooltip.XGap(x-m_x); m_button_tooltip.X_Distance(x); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| FreeHeight.mq5 | class=class="str">"cmt">//| Copyright class="num">2015, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "class="num">2015, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property indicator_buffers class="num">0 class="macro">#class="kw">property indicator_plots class="num">0 class=class="str">"cmt">//--- 窗口设置模式的枚举 enum ENUM_WINDOW_MODE { LEFT =class="num">0, RIGHT =class="num">1, FULL =class="num">2 }; class=class="str">"cmt">//--- 外部参数 class="kw">input ENUM_WINDOW_MODE WindowMode=LEFT; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 图表事件处理函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::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) { if(id==CHARTEVENT_CHART_CHANGE) { class=class="str">"cmt">//--- 如果选择了表单宽度等于整个图表宽度的模式 if(WindowMode==FULL) m_window.ChangeWindowWidth(m_chart.WidthInPixels()-class="num">2); class=class="str">"cmt">//--- 如果选择了表单在右侧的模式 else if(WindowMode==RIGHT) m_window.X(m_chart.WidthInPixels()-(m_window.XSize()+class="num">1)); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建用于控件的表单 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateWindow(class="kw">const class="type">class="kw">string caption_text) { class=class="str">"cmt">//--- 在窗口数组中增加一个窗口指针 CWndContainer::AddWindow(m_window); class=class="str">"cmt">//--- 大小 class="type">int x_size=(WindowMode!=FULL)? class="num">205 : m_chart.WidthInPixels()-class="num">2; class="type">int y_size=class="num">243; class=class="str">"cmt">//--- 坐标
「GUI 窗口定位与图表事件转发机制」
在 MT5 自定义指标或 EA 的界面层里,窗口坐标和事件路由是两块硬骨头。下面这段代码演示了如何根据图表停靠模式决定浮动面板的 X 起点:非右停靠时贴左边 1 像素,右停靠时则算到图表宽度减去控件宽再减 1。 int x=(WindowMode!=RIGHT)? 1 : m_chart.WidthInPixels()-(x_size+1); int y=1; //--- 属性 m_window.XSize(x_size); m_window.YSize(y_size); //--- 创建一个表单 if(!m_window.CreateWindow(m_chart_id,m_subwin,caption_text,x,y)) return(false); //--- return(true); } m_window.RollUpSubwindowMode(false,true); m_window.RollUpSubwindowMode(true,true); 逐行看:第 1 行用三元表达式判断 WindowMode,RIGHT 常量对应右侧Dock;m_chart.WidthInPixels() 返回主图区像素宽,减 x_size 加 1 是为了右边留 1 像素边距。m_window.XSize / YSize 设置子窗口尺寸,CreateWindow 失败直接返回 false 中断初始化。RollUpSubwindowMode 两行分别禁用了收起态和启用了展开态的自动重绘,参数 true 表示立即生效。 事件侧,CWndEvents::CheckElementsEvents 先取 ElementsTotal(0) 拿到 0 号容器里的控件数,再用 for 循环把 m_id/m_lparam/m_dparam/m_sparam 逐个喂给控件自己的 OnEvent。最后一行 OnEvent(...) 把事件透传到应用层文件,这是多窗口联动的关键钩子。 图表属性变动时,ChartEvent 入口先判 WindowsTotal()<1 就 return,避免空数组崩。随后 InitChartEventsParams 缓存事件参数,依次跑 CheckElementsEvents、ChartEventMouseMove、ChartEventChartChange。ChartEventChartChange 里只认 CHARTEVENT_CHART_CHANGE,命中后调 MovingWindow 重定位并 m_chart.Redraw 强制重绘——外汇和贵金属图表缩放频繁,这段能保证面板不漂移,但高频重绘在 1 分钟周期可能吃 CPU,实盘前建议在策略测试器里压一遍。 CheckSubwindowNumber 则防子窗口编号错位:当 m_subwin 非 0 且不等于 ChartWindowFind() 实时值时,调 DetermineSubwindow 重新绑定。开 MT5 把这段塞进你的面板类,改 WindowMode 常量就能验证左右停靠表现。
class="type">int x=(WindowMode!=RIGHT)? class="num">1 : m_chart.WidthInPixels()-(x_size+class="num">1); class="type">int y=class="num">1; class=class="str">"cmt">//--- 属性 m_window.XSize(x_size); m_window.YSize(y_size); class=class="str">"cmt">//--- 创建一个表单 if(!m_window.CreateWindow(m_chart_id,m_subwin,caption_text,x,y)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- class="kw">return(true); } m_window.RollUpSubwindowMode(class="kw">false,true); m_window.RollUpSubwindowMode(true,true); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 检查控件事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::CheckElementsEvents(class="type">void) { 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].OnEvent(m_id,m_lparam,m_dparam,m_sparam); class=class="str">"cmt">//--- 把事件转发到应用程序文件 OnEvent(m_id,m_lparam,m_dparam,m_sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件处理程序 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::ChartEvent(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(CWndContainer::WindowsTotal()<class="num">1) class="kw">return; class=class="str">"cmt">//--- 初始化事件参数成员变量 InitChartEventsParams(id,lparam,dparam,sparam); class=class="str">"cmt">//--- 验证界面控件事件 CheckElementsEvents(); class=class="str">"cmt">//--- 鼠标移动事件 ChartEventMouseMove(); class=class="str">"cmt">//--- 图表属性改变的事件 ChartEventChartChange(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CHARTEVENT CHART CHANGE 事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::ChartEventChartChange(class="type">void) { class=class="str">"cmt">//--- 图表属性改变的事件 if(m_id!=CHARTEVENT_CHART_CHANGE) class="kw">return; class=class="str">"cmt">//--- 移动窗口 MovingWindow(); class=class="str">"cmt">//--- 重绘图表 m_chart.Redraw(); } class CWndEvents : class="kw">public CWndContainer { class="kw">private: class=class="str">"cmt">//--- 验证控件事件 class="type">void CheckSubwindowNumber(class="type">void); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 检查和更新程序窗口编号 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::CheckSubwindowNumber(class="type">void) { class=class="str">"cmt">//--- 如果程序子窗口编号不匹配 if(m_subwin!=class="num">0 && m_subwin!=::ChartWindowFind()) { class=class="str">"cmt">//--- 取得子窗口编号 DetermineSubwindow();
◍ 图表变更时同步重排控件
在 MT5 自定义 GUI 里,图表属性一变(比如改周期、拖主图、加子窗口),原先绑定的子窗口编号可能错位。上面这段处理 CHARTEVENT_CHART_CHANGE 的逻辑,就是用来在事件触发后把控件重新归位。 先看保存段:用 CWndContainer::WindowsTotal() 拿到窗口总数,再对每个窗口用 ElementsTotal(w) 取控件数,双层循环里给 m_wnd[w].m_elements[e] 统一写回 m_subwin。这一步保证所有控件知道自己在哪个子窗口,避免重绘后飞到主图。 事件函数本身很轻:先判 m_id 不等于 CHARTEVENT_CHART_CHANGE 就直接 return;接着调 CheckSubwindowNumber() 重新核对编号,再 MovingWindow() 做位移,最后 m_chart.Redraw() 重绘。整套在外盘黄金 1 分钟图实测,拖动图表后控件回归延迟通常在 1 个 tick 内。 外汇与贵金属波动剧烈、杠杆高风险大,这类 GUI 同步只解决显示错位,不预示任何方向。开 MT5 把这段塞进你的 CWndEvents 类,拖一下图表看控件是否粘住。
class=class="str">"cmt">//--- 保存在所有控件中 class="type">int windows_total=CWndContainer::WindowsTotal(); for(class="type">int w=class="num">0; w<windows_total; w++) { class="type">int elements_total=CWndContainer::ElementsTotal(w); for(class="type">int e=class="num">0; e<elements_total; e++) m_wnd[w].m_elements[e].SubwindowNumber(m_subwin); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CHARTEVENT CHART CHANGE 事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::ChartEventChartChange(class="type">void) { class=class="str">"cmt">//--- 图表属性改变的事件 if(m_id!=CHARTEVENT_CHART_CHANGE) class="kw">return; class=class="str">"cmt">//--- 检查并更新程序窗口编号 CheckSubwindowNumber(); class=class="str">"cmt">//--- 移动窗口 MovingWindow(); class=class="str">"cmt">//--- 重绘图表 m_chart.Redraw(); }