DoEasy. 控件 (第 22 部分): SplitContainer。 修改已创建对象的属性·进阶篇
🧩

DoEasy. 控件 (第 22 部分): SplitContainer。 修改已创建对象的属性·进阶篇

(2/3)· 创建后改属性外观却不变?本篇补齐库类事件与重绘逻辑,根除隐藏隔板误显

进阶 第 2/3 篇
控件创建后改了属性,画面却纹丝不动,是很多自建 UI 的常见卡点。更隐蔽的是,移动主面板时本该隐藏的隔板突然现形,这种逻辑错很难靠肉眼排查。先理清重绘触发链,比反复调参数更省时间。

拆分容器分隔条的属性与事件钩子

在 MT5 的 Canvas 界面库里,CSplitContainer 类用一组 getter/setter 控制分隔条行为。分隔条距离通过 CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_DISTANCE 读取,返回的是 int 像素值,直接决定左右或上下两个面板的初始占比。 SetSplitterFixed(bool) 写入 CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_FIXED,置 true 后用户拖不动分隔条,适合做固定布局的盯盘面板。SplitterWidth(int, bool only_prop) 与 SetSplitterOrientation(ENUM_CANV_ELEMENT_SPLITTER_ORIENTATION, bool only_prop) 都带 only_prop 参数——传 true 时只改属性不重绘,批量初始化时能少一次界面刷新。 事件侧重载了 OnChartEvent 与 MouseActiveAreaNotPressedHandler,后者专门捕获“光标在活动区但没按鼠标”的悬停态;OnMouseEventPostProcessing 则在每轮鼠标事件末尾跑,用来做拖拽后的收尾校准。外汇与贵金属图表挂这类自定义控件时,注意高风险的实时重绘可能拖慢 tick 响应,建议先用 only_prop 攒完参数再统一刷新。

MQL5 / C++
class="type">int SplitterDistance(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_DISTANCE); }

class=class="str">"cmt">//--- (class="num">1) set and(class="num">2) class="kw">return the separator non-removability flag
class="type">void SetSplitterFixed(class="kw">const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_FIXED,flag); }
class="type">bool SplitterFixed(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_FIXED); }

class=class="str">"cmt">//--- (class="num">1) set and(class="num">2) class="kw">return the separator width
class="type">void SetSplitterWidth(class="kw">const class="type">int value,class="kw">const class="type">bool only_prop);
class="type">int SplitterWidth(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_WIDTH); }

class=class="str">"cmt">//--- (class="num">1) set and(class="num">2) class="kw">return the separator location
class="type">void SetSplitterOrientation(class="kw">const ENUM_CANV_ELEMENT_SPLITTER_ORIENTATION value,class="kw">const class="type">bool only_prop);
ENUM_CANV_ELEMENT_SPLITTER_ORIENTATION SplitterOrientation(class="type">void) class="kw">const
 { class="kw">return(ENUM_CANV_ELEMENT_SPLITTER_ORIENTATION)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION); }
class=class="str">"cmt">//--- Event handler
class="kw">virtual class="type">void OnChartEvent(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">//--- &class="macro">#x27;The cursor is inside the active area, the mouse buttons are not clicked&class="macro">#x27; event handler
class="kw">virtual class="type">void MouseActiveAreaNotPressedHandler(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">//--- Last mouse event handler
class="kw">virtual class="type">void OnMouseEventPostProcessing(class="type">void);

class=class="str">"cmt">//--- Constructor
 CSplitContainer(class="kw">const class="type">long chart_id,
 class="kw">const class="type">int subwindow,

「拆分容器的构造与默认外观」

在 MT5 自定义图形界面里,CSplitContainer 用来把一块画布区域拆成可拖拽的两块面板。它的构造函数接收图表 ID、子窗口号、描述文本以及 x/y/w/h 这组坐标尺寸,直接透传给基类 CContainer 并锁定元素类型为 GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER。 构造函数里把边框、内边距全置 0,外边距统一设 3 像素,背景与边框颜色都指向 CLR_CANV_NULL,等于默认完全透明无框。唯一被显式调用的是 SetSplitterFixed(false),说明分隔条初始允许自由拖动,而不是锁死比例。 想验证这套默认值,把下面代码段贴进 EA 的 include 链路,编译后在副图挂一个 CSplitContainer 实例,就能看到面板间留出了 3 像素间隙且分隔条可拖。外汇与贵金属图表加载此类界面时需注意,高频刷新可能拖累老机型 MT5 终端的响应。

MQL5 / C++
CSplitContainer::CSplitContainer(class="kw">const class="type">long chart_id,
                                 class="kw">const class="type">int subwindow,
                                 class="kw">const class="type">class="kw">string descript,
                                 class="kw">const class="type">int x,
                                 class="kw">const class="type">int y,
                                 class="kw">const class="type">int w,
                                 class="kw">const class="type">int h) : CContainer(GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER);
   this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER;
   this.SetBorderSizeAll(class="num">0);
   this.SetBorderStyle(FRAME_STYLE_NONE);
   this.SetPaddingAll(class="num">0);
   this.SetMarginAll(class="num">3);
   this.SetOpacity(class="num">0,true);
   this.SetBackgroundColor(CLR_CANV_NULL,true);
   this.SetBackgroundColorMouseDown(CLR_CANV_NULL);
   this.SetBackgroundColorMouseOver(CLR_CANV_NULL);
   this.SetBorderColor(CLR_CANV_NULL,true);
   this.SetBorderColorMouseDown(CLR_CANV_NULL);
   this.SetBorderColorMouseOver(CLR_CANV_NULL);
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetSplitterFixed(class="kw">false);
   this.CreatePanels();
  }

◍ 垂直分隔下的双面板坐标推算

在自定义分栏控件里,分隔条朝向直接决定两套面板的矩形参数怎么算。当 SplitterOrientation() 返回垂直态(CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL),两个面板是左右排布的,此时若 Panel1Collapsed() 与 Panel2Collapsed() 都为假,就走正常切分逻辑。 正常态下,panel1 从 (0,0) 起步,宽取 SplitterDistance()、高取整控件 Height();panel2 的 x 要跳过分隔条自身宽度,即 SplitterDistance()+SplitterWidth(),宽则为 Width() 减去这个起点,高同样吃满。分隔条矩形本身 x=SplitterDistance()、y=0、宽=SplitterWidth()、高=Height(),三个矩形刚好无缝拼满。 一旦任一面板被折叠,代码把 panel1 和 panel2 的坐标与尺寸强行写成同一份:x=y=0,w=Width(),h=Height(),等于两块面板重叠占满全域,分隔条仍保留 SplitterWidth() 宽但已无实际切分意义。开 MT5 把 SplitterDistance 调到 30、SplitterWidth 设 4,能在视觉上立刻验证这套坐标是否对齐。 别把折叠分支当冗余 折叠时让两面板共享同一矩形,是故意用重叠规避隐藏逻辑,而不是写错赋值;改这里前先确认你真的要「隐藏而非移除」另一块。

MQL5 / C++
class="kw">switch(this.SplitterOrientation())
  {
  class=class="str">"cmt">//--- The separator is positioned vertically
  case CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL      :
    class=class="str">"cmt">//--- If both panels are not collapsed,
    if(!this.Panel1Collapsed() && !this.Panel2Collapsed())
      {
      class=class="str">"cmt">//--- set the panel1 coordinates and size
      this.m_panel1_x=class="num">0;
      this.m_panel1_y=class="num">0;
      this.m_panel1_w=this.SplitterDistance();
      this.m_panel1_h=this.Height();
      class=class="str">"cmt">//--- set the panel2 coordinates and size
      this.m_panel2_x=this.SplitterDistance()+this.SplitterWidth();
      this.m_panel2_y=class="num">0;
      this.m_panel2_w=this.Width()-this.m_panel2_x;
      this.m_panel2_h=this.Height();
      class=class="str">"cmt">//--- write separator coordinates and size
      this.m_splitter_x=this.SplitterDistance();
      this.m_splitter_y=class="num">0;
      this.m_splitter_w=this.SplitterWidth();
      this.m_splitter_h=this.Height();
      }
    class=class="str">"cmt">//--- If panel1 or panel2 is collapsed,
    else
      {
      class=class="str">"cmt">//--- write the coordinates and sizes of panel1 and panel2
      this.m_panel2_x=this.m_panel1_x=class="num">0;
      this.m_panel2_y=this.m_panel1_y=class="num">0;
      this.m_panel2_w=this.m_panel1_w=this.Width();
      this.m_panel2_h=this.m_panel1_h=this.Height();
      class=class="str">"cmt">//--- write separator coordinates and size
      this.m_splitter_x=class="num">0;
      this.m_splitter_y=class="num">0;
      this.m_splitter_w=this.SplitterWidth();
      this.m_splitter_h=this.Height();
      }
    class="kw">break;
  class=class="str">"cmt">//--- The separator is located horizontally
  case CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL    :
    class=class="str">"cmt">//--- If both panels are not collapsed,
    if(!this.Panel1Collapsed() && !this.Panel2Collapsed())
      {

折叠状态下双面板坐标的兜底算法

当 splitter 控件处于非水平布局(即 default 之外的分支)且 panel1 或 panel2 被折叠时,代码走 else 分支做坐标兜底。此时两个面板被强制重叠为整块画布:m_panel1 与 m_panel2 的 x、y 均置 0,宽取 Width()、高取 Height(),等于丢弃了分隔条原本切分出的两块区域。 分隔条自身仍保留宽度属性 SplitterWidth(),但 y 坐标归零、高度维持 SplitterWidth(),视觉上贴边成一条线。这种写法保证折叠后控件不会留下空白计算区,后续 SetProperty 把控制区(CONTROL_AREA)直接绑到分隔条坐标上,m_splitter_x=0、m_splitter_y=0、m_splitter_w=Width()、m_splitter_h=SplitterWidth()。 在 MT5 里跑这套,你可以故意把面板折起来看坐标打印:若 m_panel1_h 等于 Height() 而非 SplitterDistance(),就说明进了 else 兜底分支。外汇与贵金属图表挂这种自定义控件时波动刷新快,坐标计算异常可能引发布局闪烁,属高风险调试项,建议先在回测图表验证。

MQL5 / C++
   class=class="str">"cmt">//--- set the panel1 coordinates and size
   this.m_panel1_x=class="num">0;
   this.m_panel1_y=class="num">0;
   this.m_panel1_w=this.Width();
   this.m_panel1_h=this.SplitterDistance();
   class=class="str">"cmt">//--- set the panel2 coordinates and size
   this.m_panel2_x=class="num">0;
   this.m_panel2_y=this.SplitterDistance()+this.SplitterWidth();
   this.m_panel2_w=this.Width();
   this.m_panel2_h=this.Height()-this.m_panel2_y;
   class=class="str">"cmt">//--- write separator coordinates and size
   this.m_splitter_x=class="num">0;
   this.m_splitter_y=this.SplitterDistance();
   this.m_splitter_w=this.Width();
   this.m_splitter_h=this.SplitterWidth();
   }
   class=class="str">"cmt">//--- If panel1 or panel2 is collapsed,
   else
     {
     class=class="str">"cmt">//--- write the coordinates and sizes of panel1 and panel2
     this.m_panel2_x=this.m_panel1_x=class="num">0;
     this.m_panel2_y=this.m_panel1_y=class="num">0;
     this.m_panel2_w=this.m_panel1_w=this.Width();
     this.m_panel2_h=this.m_panel1_h=this.Height();
     class=class="str">"cmt">//--- write separator coordinates and size
     this.m_splitter_x=class="num">0;
     this.m_splitter_y=class="num">0;
     this.m_splitter_w=this.Width();
     this.m_splitter_h=this.SplitterWidth();
     }
     class="kw">break;
   class="kw">default:
     class="kw">return class="kw">false;
     class="kw">break;
     }
class=class="str">"cmt">//--- Set the coordinates and sizes of the control area equal to the properties set by the separator
   this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X,this.m_splitter_x);
   this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y,this.m_splitter_y);
   this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH,this.m_splitter_w);
   this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT,this.m_splitter_h);
   class="kw">return true;
   }

「折叠面板1时的坐标与显隐切换」

在自定义分栏容器里,SetPanel1Collapsed(int flag) 负责控制左侧面板是收起还是展开。flag 非 0 时走收起分支,为 0 则走展开分支,两个分支互斥。 方法先通过 SetProperty 把传入 flag 写进 CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED 属性,再取回 Panel1、Panel2 指针;任一为空直接 return,避免空指针崩在 Move/Resize 里。 收起分支中,若 Panel1 能移动到 (CoordX()+m_panel1_x, CoordY()+m_panel1_y) 且 Resize 成功(第三个参数 false 表示不重绘),就把它的坐标改为相对容器的偏移,然后调 CollapsePanel1() 隐藏自己;同时把 Panel2 的 collapsed 属性置 false,移动并 Resize 为 (m_panel2_w, m_panel2_h, true) 后 ExpandPanel2() 显示。 展开分支逻辑相反:先把 Panel2 标为 collapsed 并隐藏,再把 Panel1 移回原位并 Resize 第三个参数 true 显示。外汇与贵金属图表上挂这类自定义控件需注意,Canvas 坐标计算错误可能导致面板飞出可视区,属高风险调试项,建议在 MT5 策略测试器用 Print 打出 m_panel1_x 等变量验证。

MQL5 / C++
class="type">void CSplitContainer::SetPanel1Collapsed(class="kw">const class="type">int flag)
  {
class=class="str">"cmt">//--- Set the flag, passed to the method, to the object class="kw">property
   this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED,flag);
   CSplitContainerPanel *p1=this.GetPanel1();
   CSplitContainerPanel *p2=this.GetPanel2();
   if(p1==NULL || p2==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Set the parameters of the panels and the separator
   this.SetsPanelParams();
class=class="str">"cmt">//--- If panel1 should be collapsed
   if(this.Panel1Collapsed())
     {
      class=class="str">"cmt">//--- If panel1 is shifted to new coordinates and its size is changed, hide panel1
      if(p1.Move(this.CoordX()+this.m_panel1_x,this.CoordY()+this.m_panel1_y) && p1.Resize(this.m_panel1_w,this.m_panel1_h,class="kw">false))
       {
        p1.SetCoordXRelative(p1.CoordX()-this.CoordX());
        p1.SetCoordYRelative(p1.CoordY()-this.CoordY());
        this.CollapsePanel1();
       }
      class=class="str">"cmt">//--- set the expanded flag for panel2
      this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED,class="kw">false);
      class=class="str">"cmt">//--- If panel2 is shifted to new coordinates and its size is changed, display panel2
      if(p2.Move(this.CoordX()+this.m_panel2_x,this.CoordY()+this.m_panel2_y) && p2.Resize(this.m_panel2_w,this.m_panel2_h,true))
       {
        p2.SetCoordXRelative(p2.CoordX()-this.CoordX());
        p2.SetCoordYRelative(p2.CoordY()-this.CoordY());
        this.ExpandPanel2();
       }
     }
class=class="str">"cmt">//--- If panel1 should be expanded,
   else
     {
      class=class="str">"cmt">//--- set the collapsed flag for panel2
      this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED,true);
      class=class="str">"cmt">//--- If panel2 is shifted to new coordinates and its size is changed, hide panel2
      if(p2.Move(this.CoordX()+this.m_panel2_x,this.CoordY()+this.m_panel2_y) && p2.Resize(this.m_panel2_w,this.m_panel2_h,class="kw">false))
       {
        p2.SetCoordXRelative(p2.CoordX()-this.CoordX());
        p2.SetCoordYRelative(p2.CoordY()-this.CoordY());
        this.CollapsePanel2();
       }
      class=class="str">"cmt">//--- If panel1 is shifted to new coordinates and its size is changed, display panel1
      if(p1.Move(this.CoordX()+this.m_panel1_x,this.CoordY()+this.m_panel1_y) && p1.Resize(this.m_panel1_w,this.m_panel1_h,true))
       {
        p1.SetCoordXRelative(p1.CoordX()-this.CoordX());
        p1.SetCoordYRelative(p1.CoordY()-this.CoordY());

◍ 折叠面板的互斥显示逻辑

CSplitContainer 的 SetPanel2Collapsed 方法处理的是左右(或上下)两个面板的互斥显隐:传 1 进去就收起面板2、撑开面板1,传 0 则反过来。 核心先通过 SetProperty 把 CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED 写进对象属性,再取 p1、p2 指针;任一为空直接 return,避免空指针把画布渲染搞崩。 收起面板2时,先判 Panel2Collapsed() 为真,用 p2.Move 配合 p2.Resize(w,h,false) 把面板2挪到偏移坐标并缩到 m_panel2_w / m_panel2_h,成功后调 CollapsePanel2 隐藏;同时给面板1清掉 collapsed 标记,Move+Resize(w,h,true) 成功后再 ExpandPanel1。 反过来走 else 分支:面板1 收起、面板2 展开,坐标偏移量都来自 m_panel1_x / m_panel1_y 与 m_panel2_x / m_panel2_y 这类成员,相对坐标用 CoordX()-this.CoordX() 重算。 在 MT5 自建 UI 里接这套,重点验证偏移量符号——若面板2 藏在容器右下,m_panel2_x 可能为负,Resize 的第三参 false/true 直接决定 Visible 状态,调错会两个面板同时消失。

MQL5 / C++
class="type">void CSplitContainer::SetPanel2Collapsed(class="kw">const class="type">int flag)
  {
class=class="str">"cmt">//--- Set the flag, passed to the method, to the object class="kw">property
   this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED,flag);
   CSplitContainerPanel *p1=this.GetPanel1();
   CSplitContainerPanel *p2=this.GetPanel2();
   if(p1==NULL || p2==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Set the parameters of the panels and the separator
   this.SetsPanelParams();
class=class="str">"cmt">//--- If panel2 should be collapsed,
   if(this.Panel2Collapsed())
     {
      class=class="str">"cmt">//--- If panel2 is shifted to new coordinates and its size is changed, hide panel2
      if(p2.Move(this.CoordX()+this.m_panel2_x,this.CoordY()+this.m_panel2_y) && p2.Resize(this.m_panel2_w,this.m_panel2_h,class="kw">false))
        {
         p2.SetCoordXRelative(p2.CoordX()-this.CoordX());
         p2.SetCoordYRelative(p2.CoordY()-this.CoordY());
         this.CollapsePanel2();
        }
      class=class="str">"cmt">//--- set the expanded flag for panel1
      this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED,class="kw">false);
      class=class="str">"cmt">//--- If panel1 is shifted to new coordinates and its size is changed, display panel1
      if(p1.Move(this.CoordX()+this.m_panel1_x,this.CoordY()+this.m_panel1_y) && p1.Resize(this.m_panel1_w,this.m_panel1_h,true))
        {
         p1.SetCoordXRelative(p1.CoordX()-this.CoordX());
         p1.SetCoordYRelative(p1.CoordY()-this.CoordY());
         this.ExpandPanel1();
        }
     }
class=class="str">"cmt">//--- If panel2 should be expanded,
   else
     {
      class=class="str">"cmt">//--- set the collapsed flag for panel1
      this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED,true);
      class=class="str">"cmt">//--- If panel1 is shifted to new coordinates and its size is changed, hide panel1
      if(p1.Move(this.CoordX()+this.m_panel1_x,this.CoordY()+this.m_panel1_y) && p1.Resize(this.m_panel1_w,this.m_panel1_h,class="kw">false))
        {
         p1.SetCoordXRelative(p1.CoordX()-this.CoordX());
         p1.SetCoordYRelative(p1.CoordY()-this.CoordY());
         this.CollapsePanel1();
        }
      class=class="str">"cmt">//--- If panel2 is shifted to new coordinates and its size is changed, display panel2
把重绘逻辑交给小布盯盘
这些控件事件与重绘诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到调用链提示,你只需判断要不要改库类结构。

常见问题

默认创建用初始值,属性变更后需主动按新值重绘对象,否则画面保持原状。
由 WF_CONTROL_EVENT_MOVING 替代,它对全部控件通用,旧事件已从列表移除。
BringToTop 方法先隐后显没检查绘制标志,需在置顶与附着清单里跳过不需绘制的元素。
可以,小布盯盘内置了事件与重绘链路提示,打开品种页能直接看调用关系,减少手动翻库类的时间。
在移动与释放事件中切外观状态,悬停画虚线矩形,捕获时填阴影区,释放恢复初始样式。