DoEasy. 控件 (第 22 部分): SplitContainer。 修改已创建对象的属性·进阶篇
(2/3)· 创建后改属性外观却不变?本篇补齐库类事件与重绘逻辑,根除隐藏隔板误显
拆分容器分隔条的属性与事件钩子
在 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 攒完参数再统一刷新。
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 终端的响应。
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,能在视觉上立刻验证这套坐标是否对齐。 别把折叠分支当冗余 折叠时让两面板共享同一矩形,是故意用重叠规避隐藏逻辑,而不是写错赋值;改这里前先确认你真的要「隐藏而非移除」另一块。
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 兜底分支。外汇与贵金属图表挂这种自定义控件时波动刷新快,坐标计算异常可能引发布局闪烁,属高风险调试项,建议先在回测图表验证。
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 等变量验证。
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 状态,调错会两个面板同时消失。
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