图形界面 X: 简单快速开发库的更新 (版本 3)·综合运用
🧩

图形界面 X: 简单快速开发库的更新 (版本 3)·综合运用

(3/3)· 从占位指标到消息联动,v3 最后一块拼图教你绕开 MT4 结构坑,跑通 EA 子窗口界面

案例拆解新手友好 第 3/3 篇
很多人在 MT5 里硬套 MT4 的子窗口思路,结果 EA 面板要么挤主图要么报错退坑。v3 用占位指标换了一种挂法,旧经验反而会变成新坑。

「表格控件的窗口指针与可视区裁剪」

做 MT5 自定义面板时,CWindow 作为表单基类,把关闭、收起、展开、提示这几个按钮的指针通过 GetCloseButtonPointer 等方法暴露出来。实际写 EA 或指标界面,拿不到这些指针就没法给按钮挂事件,所以继承后第一件事就是确认 m_wnd 非空。 CTable::CreateTable 里有一段很关键:当可视行数大于总行数时,直接把 m_visible_rows_total 裁成 m_rows_total,列方向同理。这个裁剪避免了对不存在单元格的循环创建,在行数动态变化的行情面板里能少建几十个 Edit 对象。 坐标计算上,m_y_size 用 m_row_y_size*m_visible_rows_total-(m_visible_rows_total-1)+2 得出,也就是行高乘可见行数再减去行间隔再加 2 像素边框。若 m_auto_xresize_mode 开启,m_x_size 会按窗口右锚点重算,挂右侧自动拉伸的盘口表时常踩这个坑。 最后若所属窗口是对话框或已最小化,表格直接 Hide(),避免在不可见容器里白跑创建逻辑。外汇与贵金属图表加载这类界面时波动大,建议先在模拟盘验证渲染开销。

MQL5 / C++
class CWindow : class="kw">public CElement
  {
class="kw">public:
   class=class="str">"cmt">//--- 返回表单按钮指针的方法
   CBmpLabel       *GetCloseButtonPointer(class="type">void)                                              { class="kw">return(::GetPointer(m_button_close));   }
   CBmpLabel       *GetRollUpButtonPointer(class="type">void)                                             { class="kw">return(::GetPointer(m_button_unroll));  }
   CBmpLabel       *GetUnrollButtonPointer(class="type">void)                                             { class="kw">return(::GetPointer(m_button_rollup));  }
   CBmpLabel       *GetTooltipButtonPointer(class="type">void)                                            { class="kw">return(::GetPointer(m_button_tooltip)); }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建编辑框型表格                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTable::CreateTable(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">int x,class="kw">const class="type">int y)
  {
class=class="str">"cmt">//--- 如果没有表单指针
   if(!CElement::CheckWindowPointer(::CheckPointer(m_wnd)))
      class="kw">return(false);
class=class="str">"cmt">//--- 可见部分不再需要通用
   m_visible_rows_total    =(m_visible_rows_total>m_rows_total)? m_rows_total : m_visible_rows_total;
   m_visible_columns_total =(m_visible_columns_total>m_columns_total)? m_columns_total : m_visible_columns_total;
class=class="str">"cmt">//--- 初始化变量
   m_id      =m_wnd.LastId()+class="num">1;
   m_chart_id =chart_id;
   m_subwin  =subwin;
   m_x       =x;
   m_y       =y;
   m_x_size  =(m_x_size<class="num">1 || m_auto_xresize_mode)? (m_anchor_right_window_side)? m_wnd.X2()-m_x+m_x_size-(m_wnd.X2()-m_x)+class="num">1-m_auto_xresize_right_offset : m_wnd.X2()-m_x-m_auto_xresize_right_offset : m_x_size;
   m_y_size  =m_row_y_size*m_visible_rows_total-(m_visible_rows_total-class="num">1)+class="num">2;
class=class="str">"cmt">//--- 到边缘的距离
   CElement::XGap((m_anchor_right_window_side)? m_x : m_x-m_wnd.X());
   CElement::YGap((m_anchor_bottom_window_side)? m_y : m_y-m_wnd.Y());
class=class="str">"cmt">//--- 创建表格
   if(!CreateArea())
      class="kw">return(false);
   if(!CreateCells())
      class="kw">return(false);
   if(!CreateScrollV())
      class="kw">return(false);
   if(!CreateScrollH())
      class="kw">return(false);
class=class="str">"cmt">//--- 如果窗口是对话框或者是最小化状态,隐藏元件
   if(m_wnd.WindowType()==W_DIALOG || m_wnd.IsMinimized())
      Hide();
class=class="str">"cmt">//---
   class="kw">return(true);
  }

改一版能演示自适应界面的测试EA

把前一篇的 MQL 应用稍作改动,就能在 SubWindow 指标子窗口里挂一个 EA,用来验证本文的界面逻辑。主窗口尺寸会跟着子窗口自动变,子窗口高度还能手动拖——关键在调用 CWindow::RollUpSubwindowMode() 时传 false,关掉卷起模式。 第一页所有控件以表单右侧为锚:表单宽度一变,它们距右边界的距离不动。创建 CSimpleButton 后只要调 AnchorRightWindowSide(true),按钮就钉在右边,不用自己算坐标。 第二页只放一个 CCanvasTable 绘制表格,子窗口宽高变化时表格跟着撑满。要让它听话,得先开 AutoXResizeMode() 和 AutoYResizeMode(),再用 AutoXResizeRightOffset()、AutoYResizeBottomOffset() 设偏移——样例里右偏移 1 像素、底偏移 25 像素。 第三页的 CLineGraph 线形图同样自主贴合表单边。第四、五页则堆了其他库控件演示右锚效果,具体形态可下测试程序自己翻。外汇与贵金属图表上跑这类 EA 属高风险操作,界面逻辑出错可能干扰下单,先在模拟盘验证。

MQL5 / C++
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=class="num">1;
   class="type">int y=class="num">1;
class=class="str">"cmt">//--- 属性
   m_window.Movable(false);
   m_window.UseRollButton();
   m_window.AutoXResizeMode(true);
   m_window.AutoYResizeMode(true);
   m_window.RollUpSubwindowMode(false,false);
class=class="str">"cmt">//--- 创建表单
   if(!m_window.CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
      class="kw">return(false);
class=class="str">"cmt">//--- 设置工具提示
   m_window.GetCloseButtonPointer().Tooltip("Close program");
   m_window.GetUnrollButtonPointer().Tooltip("Unroll");
   m_window.GetRollUpButtonPointer().Tooltip("Roll up");
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建简单按钮 class="num">1                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateSimpleButton1(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap,class="type">class="kw">string button_text)
  {
class=class="str">"cmt">//--- 保存窗口指针
   m_simple_button1.WindowPointer(m_window);
class=class="str">"cmt">//--- 附加到第一个页面
   m_tabs.AddToElementsArray(class="num">0,m_simple_button1);
class=class="str">"cmt">//--- 坐标
   class="type">int x=m_window.X()+x_gap;
   class="type">int y=m_window.Y()+y_gap;
class=class="str">"cmt">//--- 在创建之前设置属性
   m_simple_button1.ButtonXSize(class="num">140);
   m_simple_button1.BackColor(C&class="macro">#x27;class="num">255,class="num">140,class="num">140&class="macro">#x27;);
   m_simple_button1.BackColorHover(C&class="macro">#x27;class="num">255,class="num">180,class="num">180&class="macro">#x27;);
   m_simple_button1.BackColorPressed(C&class="macro">#x27;class="num">255,class="num">120,class="num">120&class="macro">#x27;);
   m_simple_button1.AnchorRightWindowSide(true);
class=class="str">"cmt">//--- 创建一个按钮
   if(!m_simple_button1.CreateSimpleButton(m_chart_id,m_subwin,button_text,x,y))
      class="kw">return(false);
class=class="str">"cmt">//--- 把元件指针加到库中
   CWndContainer::AddToElementsArray(class="num">0,m_simple_button1);
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建一个绘制型表格                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateCanvasTable(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap)
  {
class="macro">#define COLUMNS3_TOTAL class="num">15
class="macro">#define ROWS3_TOTAL    class="num">30
class=class="str">"cmt">//--- 保存表单的指针
   m_canvas_table.WindowPointer(m_window);
class=class="str">"cmt">//--- 附加到第二个页面
   m_tabs.AddToElementsArray(class="num">1,m_canvas_table);
class=class="str">"cmt">//--- 坐标
   class="type">int x=m_window.X()+x_gap;

◍ 给画布表格套上自适应外框

在 MT5 自定义指标里画表格,最烦的是窗口一拉伸表格就错位。下面这段直接把表格设成随子窗口自动伸缩,省去手动重算坐标的麻烦。 核心是先给列宽和对齐方式填数组:默认列宽 70 像素,第 0、1 列加到 100 和 90,对齐上第 0、1 列右对齐、第 2 列左对齐,其余居中。表格本体锁死 400×200 像素、列数行数走宏定义。 自适应关键在两对调用:AutoXResizeMode(true) 与 AutoYResizeMode(true) 打开双向跟随;AutoXResizeRightOffset(1) 和 AutoYResizeBottomOffset(25) 留出右边 1 像素、底部 25 像素的余量,避免贴边被吞。外汇与贵金属图表波动剧烈、多品种切换频繁,这类自适应能降低界面重绘出错概率。 数据循环用双重 for 把 '列:行' 写进每个单元格做占位,再 CreateTable 按子窗口 ID 和坐标落盘;失败直接返 false。最后 AddToElementsArray 挂进容器数组,返 true 即就绪,开 MT5 把这段塞进 CWndContainer 派生类就能看到效果。

MQL5 / C++
  class="type">int y=m_window.Y()+y_gap;
class=class="str">"cmt">//--- 表格列宽度的数组
  class="type">int width[COLUMNS3_TOTAL];
  ::ArrayInitialize(width,class="num">70);
  width[class="num">0]=class="num">100;
  width[class="num">1]=class="num">90;
class=class="str">"cmt">//--- 在列中文字对齐方式的数组
  ENUM_ALIGN_MODE align[COLUMNS3_TOTAL];
  ::ArrayInitialize(align,ALIGN_CENTER);
  align[class="num">0]=ALIGN_RIGHT;
  align[class="num">1]=ALIGN_RIGHT;
  align[class="num">2]=ALIGN_LEFT;
class=class="str">"cmt">//--- 在创建之前设置属性
  m_canvas_table.XSize(class="num">400);
  m_canvas_table.YSize(class="num">200);
  m_canvas_table.TableSize(COLUMNS3_TOTAL,ROWS3_TOTAL);
  m_canvas_table.TextAlign(align);
  m_canvas_table.ColumnsWidth(width);
  m_canvas_table.GridColor(clrLightGray);
  m_canvas_table.AutoXResizeMode(true);
  m_canvas_table.AutoYResizeMode(true);
  m_canvas_table.AutoXResizeRightOffset(class="num">1);
  m_canvas_table.AutoYResizeBottomOffset(class="num">25);
class=class="str">"cmt">//--- 发布表格数据
  for(class="type">int c=class="num">0;c<COLUMNS3_TOTAL; c++)
    for(class="type">int r=class="num">0; r<ROWS3_TOTAL; r++)
      m_canvas_table.SetValue(c,r,class="type">class="kw">string(c)+":"+class="type">class="kw">string(r));
class=class="str">"cmt">//--- 创建控件
  if(!m_canvas_table.CreateTable(m_chart_id,m_subwin,x,y))
    class="kw">return(false);
class=class="str">"cmt">//--- 把对象加到对象组的通用数组中
  CWndContainer::AddToElementsArray(class="num">0,m_canvas_table);
  class="kw">return(true);
}

「把这条线请下神坛」

这一版图形界面开发库的结构已经收敛成图 9 所示的分层形态,控件基类 CElement 把重复方法下沉后,CPU 负载比早期版本有明显下降。压缩包 easyandfastgui_mql5.zip 体积 465.56 KB,解压后可直接丢进 MT5 的 MQL5/Include 目录验证。 有用户反馈在 2016 年 10 月解压后 Menuitem.mqh 第 290 行附近出现双引号损坏,手动清掉约 700 个异常字符才能编译;原作者随后联系编辑修复并重传,说明第三方库落地时先本地 diff 一遍再引用是必要动作。 后续版本会继续扩控件并加功能,但这类封装本质只是降低 MQL5 写 GUI 的门槛,不等于策略自动变强。开 MT5 把库挂上跑一个空面板,确认消息循环和重绘不卡,再谈往上叠逻辑。

把界面联调交给小布
这些子窗口模式下的句柄报错和消息常量,小布盯盘的 AIGC 诊断已内置,打开对应品种页即可看到编译期提示,你只管调布局。

常见问题

MT4 在子窗口与资源包含上有本质结构差异和限制,维护双平台成本过高;如有紧急需要可在自由职业服务发布请求找人移植。
必须在 MQL 应用主文件里以资源形式包含 SubWindow.ex5 占位指标,否则图形界面不会在子窗口创建。
可以,小布盯盘的 AIGC 已内置常见编译与句柄错误诊断,对应品种页会标出资源缺失和常量配置冲突。
它用来从 EA 向占位指标传递是否固定高度及像素值,指标本身无法主动获知,需靠这条消息同步绘制参数。