DoEasy. 控件 (第 22 部分): SplitContainer。 修改已创建对象的属性·综合运用
(3/3)· 创建后改属性外观却不变?本篇补全库类方法,杜绝隐藏隔板误显的逻辑坑
「分隔条距离如何驱动双面板重排」
在自绘分栏容器里,分隔条距离(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 需注意高刷新率下的重绘开销,属于高风险定制开发。
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 差值。
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 就能定位。
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 把这段抄进自己的容器类,拖一下分隔条就能看到面板不会被拖穿。
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 策略测试器里跑一遍。
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() 是否返回了非空指针。
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,用于光标移出后对旧激活表单做清理,外汇与贵金属图表上的悬浮面板高风险操作建议在此统一回收资源。
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() 这层过滤。外汇与贵金属图表插件开发属高风险环境,未显示对象参与事件可能造成点差外的隐性逻辑错误。
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 编译,重点看第三、第四选项卡隔板状态是否符合预期。
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 界面卡顿风险偏高,建议先在模拟环境验证渲染开销。
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 可见。
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 多行标题看拉伸适配是否如预期。 外汇与贵金属标的波动剧烈、杠杆风险高,任何控件验证请在模拟盘完成,别拿实盘测交互延迟。