图形界面 X: 简单快速开发库的更新 (版本 3)·进阶篇
🧩

图形界面 X: 简单快速开发库的更新 (版本 3)·进阶篇

(2/3)· 从指标子窗口到 EA 独立面板的跨越,v3 如何重写界面挂载与消息通信逻辑

偏理论进阶 第 2/3 篇
不少交易者把 EA 面板硬塞进主图表,结果价格动作和按钮互相遮挡,操作节奏全乱。v3 之前库只支持指标子窗口挂界面,EA 想独立面板只能自己造轮子。这一版把子窗口 EA 模式补上了,但挂载常量用错就会在日志里刷满句柄错误。

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 验证。

MQL5 / C++
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静默消失,排查可能要多花数小时。

MQL5 / C++
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 像素缝。

MQL5 / C++
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 风险,未处理好坐标偏移可能导致控件错位。

MQL5 / C++
   }
  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(),这行决定了标签和窗口左边界的像素差。外汇和贵金属图表里多屏、缩放很频繁,锚点设错会让面板在高分屏上漂走,属于高概率踩坑点。

MQL5 / C++
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 线上,干扰价格行为判读,属于高风险环境下的界面瑕疵。

MQL5 / C++
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());
  }
把面板挂载诊断交给小布
这些 v3 子窗口模式的配置检查,小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 是否误用主窗口绘制。把重复劳动交给小布,你专注决策。

常见问题

从 v3 起库仅针对 MT5 开发,因 MT4 存在结构性差异与限制;紧急需要 MT4 支持可走自由职业服务找人移植,旧版库仍可独立使用。
可以,小布盯盘的品种页会读取面板挂载方式并提示异常,省去你翻日志查 EXPERT_IN_SUBWINDOW 常量状态的功夫。
需在主文件以资源形式包含占位指标 SubWindow.ex5,否则图形界面仍尝试在主图表创建并报句柄错误。
通过 ON_SUBWINDOW_CHANGE_HEIGHT 消息传递,指标借此获知开发人员设定的模式与窗口高度,避免盲绘。
没有,它仅提供零缓冲区、零尺度的纯子窗口容器,负责给 EA 界面腾出独立空间。