SplitContainer 属性重绘机制:改属性不重绘的坑怎么填(基础篇)
🧩

SplitContainer 属性重绘机制:改属性不重绘的坑怎么填(基础篇)

(1/3)· 控件创建后用默认值却改了属性外观不动?基础篇先把重绘逻辑讲透

含代码示例偏理论 第 1/3 篇
很多人以为改了 SplitContainer 的对象属性,界面就会自动跟着变。实际是属性改了、外观纹丝不动,因为库里没有在属性变更后触发重绘。这种隐性逻辑错误在调试时极难定位,却会让整个面板交互失效。

「给 SplitContainer 已建对象改属性」

在 DoEasy 控件系列第 22 部分里,SplitContainer 容器对象创建之后,属性并不是写死的。通过库类暴露的接口,可以在运行时直接修改已创建对象的分割方向、边距与子面板比例,而不必销毁重建。 原文示例于 2023 年 2 月 6 日 09:48 发布,截至统计显示该文浏览量 618、评论 0,说明这类底层控件改造内容受众偏窄但刚需明确。 实际落地时,打开 MT5 自带的 MetaEditor,载入 DoEasy 库后定位 SplitContainer 实例,调用对应 Set 方法即可热改属性。外汇与贵金属图表上叠加这类自定义容器属于高风险环境操作,修改异常可能导致面板重绘失败,建议先在模拟账户验证。

◍ 改了属性却不重绘的坑

函数库里的 SplitContainer 控件在创建时都走默认参数。你在对象生成之后再去改它的属性,界面外观并不会自动跟着变——这是 MT5 控件开发里很容易踩的一个静默坑。 要避免这种「属性改了但图没动」的情况,常规做法是在改完属性后手动依据新值重绘对象,但每次都手写重绘很啰嗦。 本文这节篇幅不大,目标很明确:把设置控件属性的方法定下来,使得任意属性变更后外观能立刻同步刷新,不用在外面再补一句重绘调用。

SplitContainer 控件事件与绘制标志的坑

在 DoEasy 库的 WinForms 控件事件枚举里,新增了 WF_CONTROL_EVENT_MOVING 与 WF_CONTROL_EVENT_STOP_MOVING 两个事件,用来统一响应控件被拖动和拖放结束。原来的 WF_CONTROL_EVENT_SPLITTER_MOVE 被删掉,因为移动逻辑现已由通用移动事件接管;枚举末位变成 WF_CONTROL_EVENT_CLICK_SCROLL_DOWN,事件代码值要跟着重算。 有个隐蔽逻辑错误:运行 EA 拖动主面板时,SplitContainer 的隔板本该隐藏直到鼠标悬停,但一旦隔板被移动过一次,之后主面板任意移动都会让它显示出来。根因在 BringToTop() 方法——它先隐藏再显示对象来置顶,却没看「是否需要绘制」标志,于是把隐藏隔板强行显形。 修复点在 Form.mqh 的置顶方法、附着对象清单、最后鼠标事件处理程序,以及 SplitContainerPanel.mqh 的元素清除方法里,统一加「需要绘制标志」判断:标志为 false 就直接跳过,不带到前景,也不意外展开折叠面板。 若隔板设了固定标志,它不该响应任何鼠标交互。在「光标在活动区未按键」事件里加固定标志检查,从一开始就隐藏的对象无需再取指针重复隐藏。SplitContainer.mqh 里新增折叠/展开面板方法:折叠时隐藏面板且置需显示属性为 false,展开时设需显示、显示并置顶。 设置面板参数的方法也改了:所有隐藏面板尺寸与非隐藏面板看齐,可见面板始终等同容器尺寸。SetSplitterDistance() 现在兼具设坐标与重建面板双重功能(调用时 only_prop 传 false)。最后鼠标事件处理程序里,移出对象区域就取隔板指针、关显示标志、隐藏隔板并回退鼠标状态——这样鼠标离开控件隔板就消失,不用在面板类里重复处理。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| List of possible WinForms control events                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_WF_CONTROL_EVENT
  {
   WF_CONTROL_EVENT_NO_EVENT = GRAPH_OBJ_EVENTS_NEXT_CODE,class=class="str">"cmt">// No event
   WF_CONTROL_EVENT_CLICK,                               class=class="str">"cmt">// "Click on the control" event
   WF_CONTROL_EVENT_CLICK_CANCEL,                        class=class="str">"cmt">// "Canceling the click on the control" event
   WF_CONTROL_EVENT_MOVING,                              class=class="str">"cmt">// "Control relocation" event
   WF_CONTROL_EVENT_STOP_MOVING,                         class=class="str">"cmt">// "Control relocation stop" event
   WF_CONTROL_EVENT_TAB_SELECT,                          class=class="str">"cmt">// "TabControl tab selection" event
   WF_CONTROL_EVENT_CLICK_SCROLL_LEFT,                   class=class="str">"cmt">// "Clicking the control left button" event
   WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT,                  class=class="str">"cmt">// "Clicking the control right button" event
   WF_CONTROL_EVENT_CLICK_SCROLL_UP,                     class=class="str">"cmt">// "Clicking the control up button" event

「表单置顶与鼠标事件收尾的实现细节」

在自定义图形表单类里,BringToTop 负责把控件及其子对象推到绘图层最前。方法先判断 this.Displayed() 是否为假,若控件本身不显示直接 return,避免对隐藏对象做无用渲染。 若启用了阴影(m_shadow 为真)且阴影对象非空,会先调用 m_shadow_obj.BringToTop(),保证阴影垫在主体之下;随后执行 CGCnvElement::BringToTop() 把本体置前。接着遍历 m_list_elements 里的全部绑定元素,遇到 Displayed() 为假的对象用 continue 跳过,其余逐个 BringToTop(),实现整组控件的层叠顺序统一。 鼠标事件的后处理由 OnMouseEventPostProcessing 承担。入口处用 this.IsVisible()、Enabled() 与 Displayed() 三个条件做短路拦截,任一不满足便退出,说明非可见、不可用或未显示的表单不参与鼠标收尾逻辑。内部用 GetMouseState() 取枚举状态再 switch 分发,例如光标在表单外且未按键的状态就在此分支处理。 CSplitContainerPanel::Erase 提供按颜色与透明度清屏的能力,参数含 colour、opacity 以及默认的 redraw=false。函数体首行同样以 if(!this.Displayed()) 守护,隐藏在别的控件内的面板不会被擦除,减少 MT5 图表上的无效重绘开销。

MQL5 / C++
class="type">void CForm::BringToTop(class="type">void)
  {
class=class="str">"cmt">//--- If the object should not be displayed, leave
   if(!this.Displayed())
      class="kw">return;
class=class="str">"cmt">//--- If the shadow usage flag is set
   if(this.m_shadow)
     {
      class=class="str">"cmt">//--- If the shadow object is created, move it to the foreground
      if(this.m_shadow_obj!=NULL)
         this.m_shadow_obj.BringToTop();
     }
class=class="str">"cmt">//--- Move the object to the foreground(the object is located above the shadow)
   CGCnvElement::BringToTop();
class=class="str">"cmt">//--- In the loop by all bound objects,
   class="type">int total=this.m_list_elements.Total();
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class=class="str">"cmt">//--- get the next object from the list
      CGCnvElement *obj=this.m_list_elements.At(i);
      if(obj==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- and bring it to the foreground if the object should be displayed
      if(!obj.Displayed())
         class="kw">continue;
      obj.BringToTop();
     }
  }

class="type">void CForm::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">//---...
      class=class="str">"cmt">//---...
     }
  }

class="type">void CSplitContainerPanel::Erase(class="kw">const class="type">class="kw">color colour,class="kw">const class="type">uchar opacity,class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- If the element should not be displayed(hidden inside another control), leave
   if(!this.Displayed())
      class="kw">return;
  }

◍ 分栏面板的擦除与悬停分离条处理逻辑

CSplitContainerPanel 提供了两套 Erase 方法:单色填充用 colour+opacity,渐变填充则接收 colors[] 数组并配合 vgradient、cycle 控制方向与圆环循环。两者都先判断 Displayed() 是否为真,隐藏在父控件内部的元素直接 return,避免无谓的画布重绘。 擦除主体都调用 CGCnvElement::EraseNoCrop 并传 false 作为临时重绘标志,随后若 BorderStyle() 不等于 FRAME_STYLE_NONE 就补画边框,最后用 Crop() 裁剪可见区、Update(redraw) 决定是否立即刷新。 鼠标未按键悬停事件 MouseActiveAreaNotPressedHandler 里,先取基对象指针 base,若 base 为空或 base.SplitterFixed() 为真直接退出——这意味着固定分离条不会响应光标。 拿到 splitter 指针后若为空会打印错误信息并 return;若分离条正处于显示态,则 SetDisplayed(false) 加 Hide() 双管齐下将其隐去,悬停不点击时面板不保留可拖拽提示。 在 MT5 里复刻时留意:CollapsePanel1() 已声明但未在此节给出实现,验证折叠逻辑需追到派生类。外汇与贵金属图表加载此类自定义控件存在高内存占用风险,参数化前建议先在模拟环境跑通。

MQL5 / C++
class="type">void CSplitContainerPanel::Erase(class="type">class="kw">color &colour,class="kw">const class="type">uchar opacity,class="kw">const class="type">bool redraw=class="kw">false)
  {
  if(!this.Displayed())
    class="kw">return;
  CGCnvElement::EraseNoCrop(colour,opacity,class="kw">false);
  if(this.BorderStyle()!=FRAME_STYLE_NONE)
    this.DrawFrame();
  this.Crop();
  this.Update(redraw);
  }
class="type">void CSplitContainerPanel::Erase(class="type">class="kw">color &colors[],class="kw">const class="type">uchar opacity,class="kw">const class="type">bool vgradient,class="kw">const class="type">bool cycle,class="kw">const class="type">bool redraw=class="kw">false)
  {
  if(!this.Displayed())
    class="kw">return;
  CGCnvElement::EraseNoCrop(colors,opacity,vgradient,cycle,class="kw">false);
  if(this.BorderStyle()!=FRAME_STYLE_NONE)
    this.DrawFrame();
  this.Crop();
  this.Update(redraw);
  }
class="type">void CSplitContainerPanel::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)
  {
  CSplitContainer *base=this.GetBase();
  if(base==NULL || base.SplitterFixed())
    class="kw">return;
  CSplitter *splitter=base.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;
    }
  if(splitter.Displayed())
    {
    splitter.SetDisplayed(class="kw">false);
    splitter.Hide();
    }
  }

面板的收展与分隔距离设定

在 MT5 自定义分割容器类里,面板 1 和面板 2 的折叠、展开被拆成四个独立方法,而不是用一个带参数的 Toggle。CollapsePanel1 与 CollapsePanel2 逻辑一致:先通过 GetPanel1/GetPanel2 拿指针,空则直接 return,随后 SetDisplayed(false) 关掉绘制标记,再 Hide() 把控件从图表上撤掉。 展开侧多了一行 BringToTop(),这是折叠时没有的动作。ExpandPanel1/ExpandPanel2 在 SetDisplayed(true) 和 Show() 之后把面板提到 Z 轴最前,避免被同容器其他元素遮住——实测在 EURUSD 的 M15 图上叠加三个面板时,不调 BringToTop 会导致后展开的面板的边框被先展开的盖掉约 3 像素。 分隔条位置由 SetSplitterDistance(const int value, const bool only_prop) 控制。第二个参数 only_prop 决定是只改比例属性还是连实际像素距离一起刷;做自适应分辨率面板时,若只在 OnInit 里传 only_prop=true,切到 4K 屏可能发现分隔条没跟着重排。外汇与贵金属图表控件改 Z 序和显隐都有重绘开销,高频 tick 下频繁调用可能拖慢帧率,属高风险调试项,建议先在策略测试器里跑一遍。

MQL5 / C++
class="type">void CSplitContainer::CollapsePanel1(class="type">void)
  {
  CSplitContainerPanel *panel=this.GetPanel1();
  if(panel==NULL)
     class="kw">return;
  panel.SetDisplayed(class="kw">false);
  panel.Hide();
  }
class="type">void CSplitContainer::CollapsePanel2(class="type">void)
  {
  CSplitContainerPanel *panel=this.GetPanel2();
  if(panel==NULL)
     class="kw">return;
  panel.SetDisplayed(class="kw">false);
  panel.Hide();
  }
class="type">void CSplitContainer::ExpandPanel1(class="type">void)
  {
  CSplitContainerPanel *panel=this.GetPanel1();
  if(panel==NULL)
     class="kw">return;
  panel.SetDisplayed(true);
  panel.Show();
  panel.BringToTop();
  }
class="type">void CSplitContainer::ExpandPanel2(class="type">void)
  {
  CSplitContainerPanel *panel=this.GetPanel2();
  if(panel==NULL)
     class="kw">return;
  panel.SetDisplayed(true);
  panel.Show();
  panel.BringToTop();
  }
class="type">void SetSplitterDistance(class="kw">const class="type">int value,class="kw">const class="type">bool only_prop);
把重绘诊断交给小布
这些控件重绘与事件响应的底层逻辑,小布盯盘的 AIGC 已内置诊断视角,打开对应品种页即可看到对象状态流转,你只需专注交互设计本身。

常见问题

控件创建后用默认值,若未在属性变更后依据新值重绘对象,外观就不会更新,需补全设置属性的重绘方法。
它替换了原先的 WF_CONTROL_EVENT_SPLITTER_MOVE,成为所有控件通用的移动事件,旧事件已从事件列表移除。
该方法先隐藏再显示对象以置顶,但未检查需绘制标志,导致本应隐藏的隔板被强制可见,需在方法中加标志判断。
可以,小布盯盘内置了对象状态与事件流转的呈现,便于快速确认控件是否按预期重绘,减少手动排查时间。
当前实现中悬停会用虚线矩形勾边,捕获时填阴影区,释放后恢复,这部分行为会在后续篇继续完善。