图形界面 I: 在MetaTrader 4终端中使用不同类型的程序来测试界面库 (第五章)·进阶篇
🧩

图形界面 I: 在MetaTrader 4终端中使用不同类型的程序来测试界面库 (第五章)·进阶篇

(2/3)· 只靠EA在MT5里验证界面库远远不够,指标子窗口异常和MT4兼容性才是真正分水岭

进阶 第 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 参数后,界面重绘会立刻反映新尺寸,不用重启指标。 外汇与贵金属图表加载这类自定义指标存在高波动风险,子窗口排他只是避免资源冲突,不预示任何价格方向。

MQL5 / C++
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 坐标。下面这段就是刷新边界的典型写法,三个按钮依次贴着右边排布,提示按钮仅在启用时才参与布局。

MQL5 / C++
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,三者紧挨。 图表缩放时靠 OnEventCHARTEVENT_CHART_CHANGE 重排。若 WindowMode==FULL,表单宽度被设成 m_chart.WidthInPixels()-2;若 RIGHT,则 X 坐标等于图表像素宽减去表单自身宽加 1,实现右贴边。 CreateWindow 里给出了具体尺寸数据:非全屏模式下 x_size 固定 205 像素,y_size 为 243 像素;全屏则宽度跟随图表。外汇与贵金属图表加载此类 EA 面板存在脚本冲突导致卡顿的高风险,建议在 MT5 策略测试器外的实盘图表先单窗口验证。

MQL5 / C++
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 常量就能验证左右停靠表现。

MQL5 / C++
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 类,拖一下图表看控件是否粘住。

MQL5 / C++
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();
   }
交给小布盯盘看跨平台表现
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到不同程序类型下的界面加载状态,你专注决策而非重复编译验证。

常见问题

当前库默认按主窗口坐标体系放置表单,子窗口的偏移与句柄未传入窗口管理逻辑,需要改写创建流程以感知所属窗口。
小布盯盘内置了跨程序类型的加载诊断,可显示指标或脚本在MT4中的界面初始化结果,省去手动逐终端编译。
脚本一次性执行完即退出,表单需在处理函数内显式维持消息循环,否则控件无法持续响应光标与按钮事件。
若库本身按跨平台设计,多数核心类可直接复用,主要调整在于平台相关的窗口事件映射与资源路径,不必重写控件逻辑。