DoEasy. 控件 (第 22 部分): SplitContainer。 修改已创建对象的属性·综合运用
🧩

DoEasy. 控件 (第 22 部分): SplitContainer。 修改已创建对象的属性·综合运用

(3/3)· 创建后改属性外观却不变?本篇补全库类方法,杜绝隐藏隔板误显的逻辑坑

新手友好 第 3/3 篇
改了 SplitContainer 对象属性,画面却纹丝不动,是很多刚碰库控件的人的日常卡点。更隐蔽的是,移动一次主面板后,本该隐藏的隔板突然常驻显示——这不是你操作错,是重绘逻辑漏了标志位检查。

「分隔条距离如何驱动双面板重排」

在自绘分栏容器里,分隔条距离(SplitterDistance)不是单纯画一条线,而是直接改写两个面板的可控区域坐标。垂直方向下,控制区 X 被设为距离值、Y 置 0;水平方向则反过来,X 为 0、Y 取距离值。 SetSplitterDistance 方法先写对象属性,再根据方向分支设定控制区。若 only_prop 为真则直接返回,不触发面板与分隔条的重绘——这在批量初始化多个容器时能省掉不必要的图形刷新。 当 only_prop 为假且三个子对象(p1、p2、sp)均非空,才进入实际布局:先 SetsPanelParams 统一参数,再对分隔条做 Resize(false) 不重绘背景,随后 Move 到绝对坐标并回写相对坐标。面板 1 成功 Resize 后,面板 2 才会接着被重定位。 想验证这套逻辑,可在 MT5 里给 CSplitContainer 派生类加一行 Print(SplitterDistance()),拖动分隔条看控制区 X/Y 是否按上面分支跳变;外汇与贵金属图表上叠加此类自绘 UI 需注意高刷新率下的重绘开销,属于高风险定制开发。

MQL5 / C++
class="type">void CSplitContainer::SetSplitterDistance(class="kw">const class="type">int value,class="kw">const class="type">bool only_prop)
  {
class=class="str">"cmt">//--- Set the value, passed to the method, to the object class="kw">property
   this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_DISTANCE,value);
class=class="str">"cmt">//--- Depending on the direction of the separator(vertical or horizontal),
class=class="str">"cmt">//--- set the values to the coordinates of the object control area
   class="kw">switch(this.SplitterOrientation())
     {
      case CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL :
         this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X,this.SplitterDistance());
         this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y,class="num">0);
         class="kw">break;
      class=class="str">"cmt">//---CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL
      class="kw">default:
         this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X,class="num">0);
         this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y,this.SplitterDistance());
         class="kw">break;
     }
class=class="str">"cmt">//--- If only setting the class="kw">property, leave
   if(only_prop)
      class="kw">return;
class=class="str">"cmt">//--- If there are no panels or separator, leave
   CSplitContainerPanel *p1=this.GetPanel1();
   CSplitContainerPanel *p2=this.GetPanel2();
   CSplitter *sp=this.GetSplitter();
   if(p1==NULL || p2==NULL || sp==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Set the parameters of the panels and the separator
   this.SetsPanelParams();
class=class="str">"cmt">//--- If the size of the separator object has been successfully changed
   if(sp.Resize(this.m_splitter_w,this.m_splitter_h,class="kw">false))
     {
      class=class="str">"cmt">//--- Shift the separator
      if(sp.Move(this.CoordX()+this.m_splitter_x,this.CoordY()+this.m_splitter_y))
        {
         class=class="str">"cmt">//--- Set new relative separator coordinates
         sp.SetCoordXRelative(sp.CoordX()-this.CoordX());
         sp.SetCoordYRelative(sp.CoordY()-this.CoordY());
         class=class="str">"cmt">//--- If panel class="num">1 is resized successfully
         if(p1.Resize(this.m_panel1_w,this.m_panel1_h,true))
           {
            class=class="str">"cmt">//--- If panel class="num">2 coordinates are changed to new ones

分隔条宽度如何驱动面板重排

在 CSplitContainer 里,SetSplitterWidth 不只改一个数值,它会顺着方向重新分配控件区域。竖向分隔条时,控件区宽等于分隔条宽、高等于容器高;横向时反过来,控件区宽等于容器宽、高等于分隔条宽。 传入 only_prop=true 可只写属性不触发布局,适合初始化阶段批量设参;传 false 才会往下拿 panel1、panel2、splitter 三个指针,任一为空直接 return,避免空对象崩在 Resize 里。 真正联动发生在 SetsPanelParams() 之后:splitter 先以 m_splitter_w、m_splitter_h 做 Resize(false),成功才把自身 Move 到新坐标,随后两个面板按相对坐标重算位置与尺寸。下面这段就是面板2跟随分隔条移动的核⼼片段,改 m_panel2_x / m_panel2_y 就能验证偏移量。 外汇与贵金属图表上挂这类自定义容器,需注意高分屏下坐标像素可能取整误差,调试时建议打印 CoordX/Y 差值。

MQL5 / C++
if(p2.Move(this.CoordX()+this.m_panel2_x,this.CoordY()+this.m_panel2_y,true))
  {
   class=class="str">"cmt">//--- if panel class="num">2 has been successfully resized,
   if(p2.Resize(this.m_panel2_w,this.m_panel2_h,true))
     {
      class=class="str">"cmt">//--- set new relative coordinates of panel class="num">2
      p2.SetCoordXRelative(p2.CoordX()-this.CoordX());
      p2.SetCoordYRelative(p2.CoordY()-this.CoordY());
     }
  }

◍ 拖完分隔条后面板怎么重排

在 CSplitContainer 里,分隔条被拖动后并不是只改自己坐标,而是连带着把两个子面板一起重算。先看移动分隔条这段:若 sp.Move 成功,就把它的坐标转成相对容器的偏移,再依次尝试 p1 和 p2 的 Resize 与 Move,任何一步失败就静默退出,不会抛异常。 [CODE]if(sp.Move(this.CoordX()+this.m_splitter_x,this.CoordY()+this.m_splitter_y)) { //--- Set new relative separator coordinates sp.SetCoordXRelative(sp.CoordX()-this.CoordX()); sp.SetCoordYRelative(sp.CoordY()-this.CoordY()); //--- If panel 1 is resized successfully if(p1.Resize(this.m_panel1_w,this.m_panel1_h,true)) { //--- If panel 2 coordinates are changed to new ones if(p2.Move(this.CoordX()+this.m_panel2_x,this.CoordY()+this.m_panel2_y,true)) { //--- if panel 2 has been successfully resized, if(p2.Resize(this.m_panel2_w,this.m_panel2_h,true)) { //--- set new relative coordinates of panel 2 p2.SetCoordXRelative(p2.CoordX()-this.CoordX()); p2.SetCoordYRelative(p2.CoordY()-this.CoordY()); } } } }[/CODE] 逐行拆解:第1行用容器坐标加偏移量移动分隔条;第4–5行把绝对坐标减容器原点,存成相对坐标,方便后续容器整体移动时子件不漂;第7行先重设面板1尺寸,第三个参数 true 表示重绘;第10行把面板2挪到预设的 m_panel2_x / m_panel2_y;第13行再Resize面板2,成功后才在第16–17行写它的相对坐标。 切换分隔条方向走的是另一个入口 SetSplitterOrientation。only_prop 为 true 时只写属性不重排,省一次绘制;为 false 时先抓 p1、p2、sp 三个指针,任一为空直接 return,随后调 SetsPanelParams 重算布局参数,再走一遍 p1.Resize → p2.Move → p2.Resize 的流程。实盘里若你发现拖不动分隔条,八成是 p1 或 p2 的 Resize 返回了 false,开 MT5 在这两个 if 里打 Print 就能定位。

MQL5 / C++
if(sp.Move(this.CoordX()+this.m_splitter_x,this.CoordY()+this.m_splitter_y))
  {
   class=class="str">"cmt">//--- Set new relative separator coordinates
   sp.SetCoordXRelative(sp.CoordX()-this.CoordX());
   sp.SetCoordYRelative(sp.CoordY()-this.CoordY());
   class=class="str">"cmt">//--- If panel class="num">1 is resized successfully
   if(p1.Resize(this.m_panel1_w,this.m_panel1_h,true))
     {
      class=class="str">"cmt">//--- If panel class="num">2 coordinates are changed to new ones
      if(p2.Move(this.CoordX()+this.m_panel2_x,this.CoordY()+this.m_panel2_y,true))
        {
         class=class="str">"cmt">//--- if panel class="num">2 has been successfully resized,
         if(p2.Resize(this.m_panel2_w,this.m_panel2_h,true))
           {
            class=class="str">"cmt">//--- set new relative coordinates of panel class="num">2
            p2.SetCoordXRelative(p2.CoordX()-this.CoordX());
            p2.SetCoordYRelative(p2.CoordY()-this.CoordY());
           }
        }
     }
  }

class="type">void CSplitContainer::SetSplitterOrientation(class="kw">const ENUM_CANV_ELEMENT_SPLITTER_ORIENTATION value,class="kw">const class="type">bool only_prop)
  {
   this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION,value);
   if(only_prop)
      class="kw">return;
   CSplitContainerPanel *p1=this.GetPanel1();
   CSplitContainerPanel *p2=this.GetPanel2();
   CSplitter *sp=this.GetSplitter();
   if(p1==NULL || p2==NULL || sp==NULL)
      class="kw">return;
   class=class="str">"cmt">//--- Set the parameters of the panels and the separator
   this.SetsPanelParams();
   class=class="str">"cmt">//--- If panel class="num">1 is resized successfully
   if(p1.Resize(this.m_panel1_w,this.m_panel1_h,true))
     {
      class=class="str">"cmt">//--- If panel class="num">2 coordinates are changed to new ones
      if(p2.Move(this.CoordX()+this.m_panel2_x,this.CoordY()+this.m_panel2_y,true))
        {
         class=class="str">"cmt">//--- if panel class="num">2 has been successfully resized,
         if(p2.Resize(this.m_panel2_w,this.m_panel2_h,true))

「分隔条拖拽时的越界钳制逻辑」

在 CSplitContainer 的 OnChartEvent 里,分隔条被拖动时并不是放任坐标自由落体。事件 ID 命中 WF_CONTROL_EVENT_MOVING 后,先通过 GetSplitter() 拿指针,若指针为空或 SplitterFixed() 为真,直接 return 不做任何处理。 坐标来自 lparam / dparam 强转的 int x、int y。竖向分隔条(CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL)会把 y 强制对齐控件本身的 CoordY(),只动 x;随后用 Panel1MinSize() 和 Panel2MinSize() 加 SplitterWidth() 做边界钳制,保证两面板剩余宽度不为负。 具体阈值写在两个 if 里:x 小于 CoordX()+Panel1MinSize() 就吸到左下限;大于 CoordX()+Width()-Panel2MinSize()-SplitterWidth() 就卡到右侧上限。开 MT5 把这段抄进自己的容器类,拖一下分隔条就能看到面板不会被拖穿。

MQL5 / C++
class="type">void CSplitContainer::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">//--- Adjust subwindow Y shift
   CGCnvElement::OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- If the event ID is moving the separator
   if(id==WF_CONTROL_EVENT_MOVING)
     {
     class=class="str">"cmt">//--- Get the pointer to the separator object
      CSplitter *splitter=this.GetSplitter();
     class=class="str">"cmt">//--- 若分隔条指针无效或分隔条被固定,直接退出不处理
      if(splitter==NULL || this.SplitterFixed())
        class="kw">return;
     class=class="str">"cmt">//--- Declare the variables for separator coordinates
      class="type">int x=(class="type">int)lparam;
      class="type">int y=(class="type">int)dparam;
     class=class="str">"cmt">//--- Depending on the separator direction,
      class="kw">switch(this.SplitterOrientation())
        {
        class=class="str">"cmt">//--- vertical position
         case CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL :
           class=class="str">"cmt">//--- Set the Y coordinate equal to the Y coordinate of the control element
            y=this.CoordY();
           class=class="str">"cmt">//--- Adjust the X coordinate so that the separator does not go beyond the control element
           class=class="str">"cmt">//--- taking into account the resulting minimum width of the panels
            if(x<this.CoordX()+this.Panel1MinSize())
               x=this.CoordX()+this.Panel1MinSize();
            if(x>this.CoordX()+this.Width()-this.Panel2MinSize()-this.SplitterWidth())
               x=this.CoordX()+this.Width()-this.Panel2MinSize()-this.SplitterWidth();
           class="kw">break;
        class=class="str">"cmt">//---CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL
        class=class="str">"cmt">//--- horizontal position of the separator

分隔条拖动时的坐标夹取与悬停显隐

在 CSplitContainer 的拖动逻辑里,default 分支先把分隔条的 X 坐标锁死成容器自身的 CoordX(),只留 Y 方向可动。Y 不是随便放的:下限被压在 CoordY()+Panel1MinSize(),上限则是 CoordY()+Height()-Panel2MinSize()-SplitterWidth(),超出就拉回边界,保证两个子面板都不会被挤没。 Move() 返回 true 后,代码把分隔条绝对坐标减掉容器坐标,转成相对坐标,再按方向写回 SetSplitterDistance():横向容器用 X 差,纵向容器用 Y 差,第二个参数传 false 表示不触发重绘连锁。 鼠标进入活动区且没按键时,MouseActiveAreaNotPressedHandler() 先判 SplitterFixed(),锁死的直接 return。没锁的话取分隔条指针,若对象为空就 Print 报错并返回;若分隔条当前隐藏,则 SetDisplayed(true) 后 Show()+Redraw(true) 把它亮出来。外汇与贵金属面板控件改这类坐标逻辑时波动剧烈,回测与实盘都属高风险,参数边界最好先在 MT5 策略测试器里跑一遍。

MQL5 / C++
      class="kw">default:
         class=class="str">"cmt">//--- Set the X coordinate equal to the X coordinate of the control element
         x=this.CoordX();
         class=class="str">"cmt">//--- Adjust the Y coordinate so that the separator does not go beyond the control element
         class=class="str">"cmt">//--- taking into account the resulting minimum height of the panels
         if(y<this.CoordY()+this.Panel1MinSize())
            y=this.CoordY()+this.Panel1MinSize();
         if(y>this.CoordY()+this.Height()-this.Panel2MinSize()-this.SplitterWidth())
            y=this.CoordY()+this.Height()-this.Panel2MinSize()-this.SplitterWidth();
         class="kw">break;
         }
   class=class="str">"cmt">//--- If the separator is shifted by the calculated coordinates,
   if(splitter.Move(x,y,true))
      {
      class=class="str">"cmt">//--- set the separator relative coordinates
      splitter.SetCoordXRelative(splitter.CoordX()-this.CoordX());
      splitter.SetCoordYRelative(splitter.CoordY()-this.CoordY());
      class=class="str">"cmt">//--- Depending on the direction of the separator, set its new coordinates
      this.SetSplitterDistance(!this.SplitterOrientation() ? splitter.CoordX()-this.CoordX() : splitter.CoordY()-this.CoordY(),class="kw">false);
      }
   }
class="type">void CSplitContainer::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">//--- If the separator is non-movable, leave
   if(this.SplitterFixed())
      class="kw">return;
class=class="str">"cmt">//--- Get the pointer to the separator
   CSplitter *splitter=this.GetSplitter();
   if(splitter==NULL)
      {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER));
      class="kw">return;
      }
class=class="str">"cmt">//--- If the separator is not displayed
   if(!splitter.Displayed())
      {
      class=class="str">"cmt">//--- Enable the display of the separator, show and redraw it
      splitter.SetDisplayed(true);
      splitter.Show();
      splitter.Redraw(true);
      }
   }

◍ 鼠标离开后的分隔条清理逻辑

在 MT5 自定义分割容器控件里,鼠标事件的后处理决定了界面元素何时该彻底隐身。CSplitContainer::OnMouseEventPostProcessing 就是干这个的:当控件不可见、被禁用或未处于显示状态,函数直接 return,不做任何多余计算。 关键分支落在光标离开窗体(或全局无事件)时。此时若上一次鼠标事件记录是『未按下/按下/滚轮』等若干种非交互态之一,代码会取出指向分隔条的 CSplitter 指针,把它设为不显示并 Hide(),同时把内部记录 m_mouse_event_last 重置为无事件态。 这套动作的实际意义是:用户把鼠标移出分割区后,拖动用的高亮分隔条应当立刻消失,避免界面残留。外汇与贵金属图表挂这类自定义控件时,HighGUI 层若漏了这段后处理,分隔条可能卡在屏幕上——属低风险显示 bug,但会干扰盯盘。 让小布替你跑这套 把下面代码贴进 MT5 的 Include 对应类文件,编译后在图表上移动鼠标越过分割容器边缘,观察分隔条是否在光标离开后消失;若没消失,检查 GetSplitter() 是否返回了非空指针。

MQL5 / C++
class="type">void CSplitContainer::OnMouseEventPostProcessing(class="type">void)
  {
   if(!this.IsVisible() || !this.Enabled() || !this.Displayed())
      class="kw">return;
   ENUM_MOUSE_FORM_STATE state=this.GetMouseState();
   class="kw">switch(state)
     {
      class=class="str">"cmt">//--- The cursor is outside the form, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is outside the form, any mouse button is clicked
      class=class="str">"cmt">//--- The cursor is outside the form, the mouse wheel is being scrolled
      case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED       :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED           :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL             :
      case MOUSE_FORM_STATE_NONE                           :
         if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED   ||
            this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED          ||
            this.MouseEventLast()==MOUSE_EVENT_OUTSIDE_FORM_NOT_PRESSED         ||
            this.MouseEventLast()==MOUSE_EVENT_INSIDE_SPLITTER_AREA_NOT_PRESSED ||
            this.MouseEventLast()==MOUSE_EVENT_INSIDE_SPLITTER_AREA_PRESSED     ||
            this.MouseEventLast()==MOUSE_EVENT_INSIDE_SPLITTER_AREA_WHEEL       ||
            this.MouseEventLast()==MOUSE_EVENT_NO_EVENT)
           {
            class=class="str">"cmt">//--- Get the pointer to the separator
            CSplitter *splitter=this.GetSplitter();
            if(splitter==NULL)
              {
               ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER));
               class="kw">return;
              }
            splitter.SetDisplayed(class="kw">false);
            splitter.Hide();
            this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT);
           }
         class="kw">break;
      class=class="str">"cmt">//--- The cursor is inside the form, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is inside the form, any mouse button is clicked
      class=class="str">"cmt">//--- The cursor is inside the form, the mouse wheel is being scrolled
      class=class="str">"cmt">//--- The cursor is inside the active area, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is inside the active area, any mouse button is clicked

「鼠标状态枚举的空处理分支」

在自定义表单的鼠标事件分发里,有一组 MOUSE_FORM_STATE_* 枚举被统一归到 break,不做任何业务处理。覆盖的区域包括激活区、滚动区、缩放区、分隔条区,以及表单内部按下/释放/滚轮等共 19 种状态。 这些分支当前是占位性质:光标在窗口内移动、滚轮滚动、按键但无具体交互逻辑时,引擎直接跳出,避免进入默认分支误触发。若你要做拖拽缩放或滚轮翻页,应从这里接管而非写在 default 里。 下方代码展示了该 switch 的收尾片段与后续后置处理函数头。注意 FormPostProcessing 接收 CForm* 与 id/lparam/dparam/sparam,用于光标移出后对旧激活表单做清理,外汇与贵金属图表上的悬浮面板高风险操作建议在此统一回收资源。

MQL5 / C++
      class=class="str">"cmt">//--- The cursor is inside the active area, the mouse wheel is being scrolled
      class=class="str">"cmt">//--- The cursor is inside the active area, left mouse button is released
      class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is within the window scrolling area, any mouse button is clicked
      class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse wheel is being scrolled
      class=class="str">"cmt">//--- The cursor is within the window resizing area, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is within the window resizing area, the mouse button(any) is clicked
      class=class="str">"cmt">//--- The cursor is within the window resizing area, the mouse wheel is being scrolled
      class=class="str">"cmt">//--- The cursor is within the window resizing area, the mouse buttons are not clicked
      class=class="str">"cmt">//--- The cursor is within the window resizing area, the mouse button(any) is clicked
      class=class="str">"cmt">//--- The cursor is within the window separator area, the mouse wheel is being scrolled
      case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED           :
      case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED               :
      case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL                :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED   :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED       :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL         :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED      :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED   :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED       :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL         :
      case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_NOT_PRESSED   :
      case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_PRESSED       :
      case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_WHEEL         :
      case MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_NOT_PRESSED:
      case MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_PRESSED     :
      case MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_WHEEL       :
         class="kw">break;
      class=class="str">"cmt">//--- MOUSE_EVENT_NO_EVENT
      class="kw">default:
         class="kw">break;
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Post-processing of the former active form under the cursor       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGraphElementsCollection::FormPostProcessing(CForm *form,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">//--- Get the main object the form is attached to
  CForm *main=form.GetMain();
  if(main==NULL)
     main=form;
class=class="str">"cmt">//--- Get all the elements attached to the form
  CArrayObj *list=main.GetListElements();
  if(list==NULL)
     class="kw">return;
   class=class="str">"cmt">//--- In the loop by the list of received elements
   class="type">int total=list.Total();

遍历表单对象时的可见性过滤

在 MT5 自定义控件库里,鼠标事件后处理阶段要先扫一遍所有已注册的表单对象,再决定哪些真正参与交互。下面这段循环就是干这个活的:外层按 total 数量走,内层对每个表单再展开可交互子对象。 过滤条件里除了常见的空指针、IsVisible()、Enabled(),还多了 Displayed() 判断——高亮那两行说明对象即便逻辑上启用,若未实际绘制到图表上也直接跳过,避免对隐藏 Tab 页里的控件发事件。 遇到 GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL 类型时,代码会把 elm 替换成当前选中的 Tab 页表单,意味着只有用户正看的那个页签接收鼠标状态,其余页签的子控件本轮不处理。 跑完所有对象后调用 ChartRedraw 重绘主图表。你在写自己的 GUI 框架时,若发现隐藏页控件误触发,优先检查是不是漏了 Displayed() 这层过滤。外汇与贵金属图表插件开发属高风险环境,未显示对象参与事件可能造成点差外的隐性逻辑错误。

MQL5 / C++
for(class="type">int i=class="num">0;i<total;i++)
  {
  class=class="str">"cmt">//--- get the pointer to the object
  CForm *obj=list.At(i);
  class=class="str">"cmt">//--- if failed to get the pointer, move on to the next one in the list
  if(obj==NULL || !obj.IsVisible() || !obj.Enabled() || !obj.Displayed())
    class="kw">continue;
  obj.OnMouseEventPostProcessing();
  class=class="str">"cmt">//--- Create the list of interaction objects and get their number
  class="type">int count=obj.CreateListInteractObj();
  class=class="str">"cmt">//--- In the loop by the obtained list
  for(class="type">int j=class="num">0;j<count;j++)
    {
    class=class="str">"cmt">//--- get the next object
    CWinFormBase *elm=obj.GetInteractForm(j);
    if(elm==NULL || !elm.IsVisible() || !elm.Enabled() || !elm.Displayed())
      class="kw">continue;
    if(elm.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL)
      {
      CTabControl *tab_ctrl=elm;
      CForm *selected=tab_ctrl.SelectedTabPage();
      if(selected!=NULL)
        elm=selected;
      }
    class=class="str">"cmt">//--- determine the location of the cursor relative to the object 
    class=class="str">"cmt">//--- and call the mouse event handling method for the object
    elm.MouseFormState(id,lparam,dparam,sparam);
    elm.OnMouseEventPostProcessing();
    }
  }
 ::ChartRedraw(main.ChartID());
}

◍ 在 MT5 里跑通 SplitContainer 面板原型

沿用前一篇的 EA 框架,把文件放到 \MQL5\Experts\TestDoEasy\Part122\ 下命名 TestDoEasy122.mq5,就能直接在图表上拉起带 TabControl 的面板。前五个选项卡各放一个 SplitContainer:偶数索引走垂直隔板,奇数索引走水平隔板,第三个选项卡固定隔板,第四、第五分别折叠 panel2 与 panel1。 编译后启动 EA,对象创建后的新属性都会正确反映到外观上。一个肉眼可见的瑕疵是:鼠标从隔板移开后,隐藏隔板的模糊触发器还会闪一下,这点和 MS Visual Studio 的原生行为不一致,后续会改显示逻辑来对齐。 OnInit() 里先给渐变填充色数组填两档暗蓝(C'26,100,128' 与 C'35,133,169'),再用 engine.CreateWFPanel() 建一个 410×200 的面板,Padding 设 3, relocation 与 auto size 标志从输入参数读。面板内建 TabControl 时预留了左边距 30、上边距 40 的排版余量,HeaderPadding 设成 (6,0)。外汇与贵金属图表加载此类自定义控件属高风险操作,参数没对齐可能让面板渲染异常。 想验证就照上面路径丢进 MT5 编译,重点看第三、第四选项卡隔板状态是否符合预期。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Set EA global variables
   ArrayResize(array_clr,class="num">2);             class=class="str">"cmt">// Array of gradient filling colors
   array_clr[class="num">0]=C&class="macro">#x27;class="num">26,class="num">100,class="num">128&class="macro">#x27;;           class=class="str">"cmt">// Original ≈Dark-azure class="type">class="kw">color
   array_clr[class="num">1]=C&class="macro">#x27;class="num">35,class="num">133,class="num">169&class="macro">#x27;;           class=class="str">"cmt">// Lightened original class="type">class="kw">color
class=class="str">"cmt">//--- Create the array with the current symbol and set it to be used in the library
   class="type">class="kw">string array[class="num">1]={Symbol()};
   engine.SetUsedSymbols(array);
   class=class="str">"cmt">//--- Create the timeseries object for the current symbol and period, and show its description in the journal
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(class="kw">false); class=class="str">"cmt">// Short descriptions
class=class="str">"cmt">//--- Create the required number of WinForms Panel objects
   CPanel *pnl=NULL;
   for(class="type">int i=class="num">0;i<class="num">1;i++)
     {
      pnl=engine.CreateWFPanel("WinForms Panel"+(class="type">class="kw">string)i,(i==class="num">0 ? class="num">50 : class="num">70),(i==class="num">0 ? class="num">50 : class="num">70),class="num">410,class="num">200,array_clr,class="num">200,true,true,class="kw">false,-class="num">1,FRAME_STYLE_BEVEL,true,class="kw">false);
      if(pnl!=NULL)
        {
         pnl.Hide();
         Print(DFUN,"Panel description: ",pnl.Description(),", Type and name: ",pnl.TypeElementDescription()," ",pnl.Name());
         class=class="str">"cmt">//--- Set Padding to class="num">4
         pnl.SetPaddingAll(class="num">3);
         class=class="str">"cmt">//--- Set the flags of relocation, auto resizing and auto changing mode from the inputs
         pnl.SetMovable(InpMovable);
         pnl.SetAutoSize(InpAutoSize,class="kw">false);
         pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,class="kw">false);

         class=class="str">"cmt">//--- Create TabControl
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,InpTabControlX,InpTabControlY,pnl.Width()-class="num">30,pnl.Height()-class="num">40,clrNONE,class="num">255,true,class="kw">false);
         CTabControl *tc=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,class="num">0);
         if(tc!=NULL)
           {
            tc.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode);
            tc.SetAlignment((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment);
            tc.SetMultiline(InpTabCtrlMultiline);
            tc.SetHeaderPadding(class="num">6,class="num">0);

「在分页控件里铺标签与分割容器」

这段逻辑演示了如何在一个 15 页的标签页控件(tc)里,先批量生成每页的文字标签,再给前 5 页各自挂一个 SplitContainer 分割容器。前 5 页标签文本留空,第 6 页起才显示 "TabPage" 加页码,避免初期页面信息过载。 标签循环里用 tc.CreateNewElement 在每页坐标 (322,120) 处建一个宽 80 高 20 的标签,蓝色不透明;拿不到指针就 continue 跳过,保证单页渲染失败不影响整体。 分割容器只建在 n=0~4 这 5 页,尺寸跟随标签页宽度减 22、高度减 22,避免贴边。偶数页用垂直分隔条、奇数页用水平分隔条;分隔距离统一锁在 50 像素。 分隔条宽度按 4+2*n 递增,第 0 页 4 像素、第 4 页 12 像素,视觉层级随页序拉开。索引 2 的页把分隔条设成固定(SetSplitterFixed(true)),其余可拖拽,适合做对照布局。外汇与贵金属图表叠加这类自定义控件时,MT5 界面卡顿风险偏高,建议先在模拟环境验证渲染开销。

MQL5 / C++
tc.CreateTabPages(class="num">15,class="num">0,class="num">56,class="num">20,TextByLanguage("Вкладка","TabPage"));
class=class="str">"cmt">//--- Create a text label with a tab description on each tab
for(class="type">int j=class="num">0;j<tc.TabPages();j++)
  {
   tc.CreateNewElement(j,GRAPH_ELEMENT_TYPE_WF_LABEL,class="num">322,class="num">120,class="num">80,class="num">20,clrDodgerBlue,class="num">255,true,class="kw">false);
   CLabel *label=tc.GetTabElement(j,class="num">0);
   if(label==NULL)
     class="kw">continue;
   class=class="str">"cmt">//--- If this is the very first tab, then there will be no text
   label.SetText(j<class="num">5 ? "" : "TabPage"+class="type">class="kw">string(j+class="num">1));
   }
for(class="type">int n=class="num">0;n<class="num">5;n++)
  {
   class=class="str">"cmt">//--- Create a SplitContainer control on each tab
   tc.CreateNewElement(n,GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER,class="num">10,class="num">10,tc.Width()-class="num">22,tc.GetTabField(class="num">0).Height()-class="num">22,clrNONE,class="num">255,true,class="kw">false);
   class=class="str">"cmt">//--- Get the SplitContainer control from each tab
   CSplitContainer *split_container=tc.GetTabElementByType(n,GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER,class="num">0);
   if(split_container!=NULL)
     {
      class=class="str">"cmt">//--- The separator will be vertical for each even tab and horizontal for each odd one
      split_container.SetSplitterOrientation(n%class="num">2==class="num">0 ? CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL : CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL,true);
      class=class="str">"cmt">//--- The separator distance on each tab will be class="num">50 pixels
      split_container.SetSplitterDistance(class="num">50,true);
      class=class="str">"cmt">//--- The width of the separator on each subsequent tab will increase by class="num">2 pixels
      split_container.SetSplitterWidth(class="num">4+class="num">2*n,class="kw">false);
      class=class="str">"cmt">//--- Make a fixed separator for the tab with index class="num">2, and a movable one for the rest
      split_container.SetSplitterFixed(n==class="num">2 ? true : class="kw">false);
      class=class="str">"cmt">//--- For a tab with index class="num">3, the second panel will be in a collapsed state(only the first one is visible)

按标签页序号折叠面板并填充标签

在 CSplitContainer 的标签切换逻辑里,索引 3 和 4 被用来演示单面板可见状态:n==3 时折叠第二面板,n==4 时折叠第一面板,其余标签保持双面板展开。 随后用 for(j=0;j<2;j++) 遍历两个控制面板,先通过 GetPanel(j) 取指针,空指针就 continue 跳过;拿到面板后调用 CreateNewElement 以 4,4 为左上偏移、宽高各减 8 像素的尺寸建一个 WF_LABEL 文本标签,颜色 clrDodgerBlue、透明度 255、可点击不可拖动。 标签建好后用 GetPanelElementByType 取回 CLabel 对象,设 ANCHOR_CENTER 居中对齐,文本写成「Panel」加面板序号(j+1)。这一套跑完,切到不同标签就能看到面板折叠差异和对应的中文/俄文面板名。 最后只轮询 i<1 也就是名为 Panel0 的顶层面板,Show() 加 Redraw(true) 强制重绘,Init 返回 INIT_SUCCEEDED。开 MT5 把这段塞进 OnInit,切到第 4 个标签应只留 Panel1 可见。

MQL5 / C++
if(n==class="num">3)
   split_container.SetPanel2Collapsed(true);
class=class="str">"cmt">//--- For a tab with index class="num">4, the first panel will be in a collapsed state(only the second one is visible)
if(n==class="num">4)
   split_container.SetPanel1Collapsed(true);
class=class="str">"cmt">//--- On each of the control panels...
for(class="type">int j=class="num">0;j<class="num">2;j++)
   {
    CSplitContainerPanel *panel=split_container.GetPanel(j);
    if(panel==NULL)
       class="kw">continue;
    class=class="str">"cmt">//--- ...create a text label with the panel name
    if(split_container.CreateNewElement(j,GRAPH_ELEMENT_TYPE_WF_LABEL,class="num">4,class="num">4,panel.Width()-class="num">8,panel.Height()-class="num">8,clrDodgerBlue,class="num">255,true,class="kw">false))
       {
        CLabel *label=split_container.GetPanelElementByType(j,GRAPH_ELEMENT_TYPE_WF_LABEL,class="num">0);
        if(label==NULL)
           class="kw">continue;
        label.SetTextAlign(ANCHOR_CENTER);
        label.SetText(TextByLanguage("Панель","Panel")+class="type">class="kw">string(j+class="num">1));
       }
   }
class=class="str">"cmt">//--- Display and redraw all created panels
 for(class="type">int i=class="num">0;i<class="num">1;i++)
  {
   pnl=engine.GetWFPanelByName("Panel"+(class="type">class="kw">string)i);
   if(pnl!=NULL)
     {
      pnl.Show();
      pnl.Redraw(true);
     }
  }
class=class="str">"cmt">//---
 class="kw">return(INIT_SUCCEEDED);
}

◍ 下一篇接着拆 SplitContainer

这一节把当前函数库版本、测试 EA 与图表事件控制指标的安装包一次性放出,压缩包体积 4480.1 KB,直接在 MT5 里解压就能跑通已有控件逻辑。 下一篇会把 SplitContainer 的面板隔板交互继续往下写,同时补上工作区边界的鼠标事件。你要验证手感,现在就可以把 ZIP 里的指标挂上图表,拖动 TabControl 多行标题看拉伸适配是否如预期。 外汇与贵金属标的波动剧烈、杠杆风险高,任何控件验证请在模拟盘完成,别拿实盘测交互延迟。

把重绘检查交给小布盯盘
这些控件状态诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到对象层级与绘制标志,省去你逐行翻库类的功夫。

常见问题

控件按默认值创建,改属性只写了值没触发依据新值重绘,需在属性变更后主动重绘对象。
BringToTop 方法先隐后显把对象置顶,但没看绘制标志,导致本不显示的元素被强制可见,需在置顶与附着清单里加标志检查。
可以,小布盯盘的品种页把对象层级和绘制状态做了可视化,不用自己查库类源码就能定位这类显示 bug。
它被通用的 WF_CONTROL_EVENT_MOVING 替代,所有控件移动都能用同一事件响应,列表末尾事件也随之重算代码值。
悬停用虚线矩形勾边,鼠标捕获填阴影区,释放恢复原本外观,配合移动事件实时切换绘制状态。