DoEasy. 控件 (第 23 部分): 改进 TabControl 和 SplitContainer WinForms 对象·进阶篇
🛠️

DoEasy. 控件 (第 23 部分): 改进 TabControl 和 SplitContainer WinForms 对象·进阶篇

(2/3)· 当多面板对象在图表上互相串台,问题往往出在父级指代的传递时机

案例拆解新手友好 第 2/3 篇
图表上拖出两个独立面板,切换时子对象突然跑到别的容器里?这不是你操作错了,是控件构造时父级引用没绑牢。本篇把 TabControl 和 SplitContainer 的通信区域和指代逻辑拆开重做,顺手修掉几个隐藏 bug。

◍ 画布控件的基础属性 setter 与命中区偏移

在 MT5 的自定义图形库里,每个画布元素(CANV element)都靠一组 setter 把身份与布局信息写进内部属性表。下面这段接口覆盖了 ID、序号、可用状态、阴影开关,以及控制区(control area)的四向坐标与尺寸。 控制区坐标用 CANV_ELEMENT_PROP_CONTROL_AREA_X / Y / WIDTH / HEIGHT 四个属性存储,单位均为像素。比如把 SetControlAreaWidth(120) 和 SetControlAreaHeight(40) 设下去,这个元素在画布上占用的可交互矩形就是 120×40;若 X、Y 不显式设定,默认落在画布原点 (0,0),容易被其他层盖住。 除了写属性,还有只读方法取回「活跃区(active area)」相对元素边缘的内缩量:ActiveAreaLeftShift() 等四个函数分别返回左、右、上、下的偏移像素,类型强转为 int。这些值决定了鼠标命中检测时,实际可点区域比控制区边界收进了多少——做按钮热区微调时,直接读这四个返回值比瞎猜边距靠谱。 注意外汇与贵金属图表上叠加自定义画布属于高风险操作,属性设错可能导致控件不可见或事件不触发,建议在策略测试器的可视化模式里逐步验证。

MQL5 / C++
class="type">void SetID(const class="type">int id) { this.SetProperty(CANV_ELEMENT_PROP_ID,id); }
class="type">void SetNumber(const class="type">int number) { this.SetProperty(CANV_ELEMENT_PROP_NUM,number); }
class="type">void SetEnabled(const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_ENABLED,flag); }
class="type">void SetShadow(const class="type">bool flag) { this.m_shadow=flag; }

class=class="str">"cmt">//--- Set the(class="num">1) X, (class="num">2) Y coordinates, (class="num">3) width and(class="num">4) height of the element control area
class="type">void SetControlAreaX(const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X,value); }
class="type">void SetControlAreaY(const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y,value); }
class="type">void SetControlAreaWidth(const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH,value); }
class="type">void SetControlAreaHeight(const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT,value); }

class=class="str">"cmt">//--- Return the shift(class="num">1) of the left, (class="num">2) right, (class="num">3) top and(class="num">4) bottom edge of the element active area
class="type">int ActiveAreaLeftShift(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT); }
class="type">int ActiveAreaRightShift(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT); }
class="type">int ActiveAreaTopShift(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP); }

控件活动区与控制区的坐标取回

在 MT5 的 Canvas 元素类里,活动区(Active Area)和控制区(Control Area)是两套独立坐标体系。活动区决定了鼠标交互的命中范围,控制区则框定了内部子控件(比如按钮、滑块)的实际排布边界,两者经常不重合。 活动区的四条边通过 ActiveAreaLeft / Right / Top / Bottom 四个方法返回,计算方式都是基准坐标加减对应的 Shift 偏移。例如 Left = CoordX() + ActiveAreaLeftShift(),Bottom = BottomEdge() - ActiveAreaBottomShift(),说明下、右边缘用的是减法收缩。 控制区坐标则依赖 ControlAreaXShift / YShift / Width / Height 四个属性,经 GetProperty 从 CANV_ELEMENT_PROP_CONTROL_AREA_* 读入。ControlAreaLeft 直接返回 CoordX() + ControlAreaXShift(),没有做 int 强转,而活动区方法里大多显式转了 int,这种不一致在复制代码时要留意。 开 MT5 新建个 Canvas 面板,打印这几个方法的返回值,对比你鼠标实际能点中的范围和肉眼看到的控件边框,大概率会发现两者差了几个像素——这就是 Shift 偏移在起作用。

MQL5 / C++
class="type">int ActiveAreaBottomShift(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM); }
class=class="str">"cmt">//--- Return the coordinate(class="num">1) of the left, (class="num">2) right, (class="num">3) top and(class="num">4) bottom edge of the element active area
class="type">int ActiveAreaLeft(class="type">void) const { class="kw">return class="type">int(this.CoordX()+this.ActiveAreaLeftShift()); }
class="type">int ActiveAreaRight(class="type">void) const { class="kw">return class="type">int(this.RightEdge()-this.ActiveAreaRightShift()); }
class="type">int ActiveAreaTop(class="type">void) const { class="kw">return class="type">int(this.CoordY()+this.ActiveAreaTopShift()); }
class="type">int ActiveAreaBottom(class="type">void) const { class="kw">return class="type">int(this.BottomEdge()-this.ActiveAreaBottomShift()); }
class=class="str">"cmt">//--- Return(class="num">1) X, (class="num">2) Y coordinate shift, (class="num">3) width, (class="num">4) height, (class="num">5) right and(class="num">6) lower edge of the control management area
class="type">int ControlAreaXShift(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X); }
class="type">int ControlAreaYShift(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y); }
class="type">int ControlAreaWidth(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH); }
class="type">int ControlAreaHeight(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT); }
class=class="str">"cmt">//--- Return the coordinate(class="num">1) of the left, (class="num">2) right, (class="num">3) top and(class="num">4) bottom edge of the element control area
class="type">int ControlAreaLeft(class="type">void) const { class="kw">return this.CoordX()+this.ControlAreaXShift(); }

「控件区域坐标与可见区高度的取法」

在自定义图形对象的类封装里,控件区域(control area)的四至坐标通常由基础坐标加偏移算出来。右边界等于左边界加区域宽度,下边界等于上边界加区域高度,这种写法把相对布局转成了绝对画布坐标。 下面这段是取四至与相对坐标的核心实现,注意 Relative 系列是用控件边缘减对象原点,得到的是相对于对象自身的局部偏移: [CODE] int ControlAreaRight(void) const { return this.ControlAreaLeft()+this.ControlAreaWidth(); } int ControlAreaTop(void) const { return this.CoordY()+this.ControlAreaYShift(); } int ControlAreaBottom(void) const { return this.ControlAreaTop()+this.ControlAreaHeight(); } //--- Return the relative coordinate (1) of the left, (2) right, (3) top and (4) bottom edge of the element control area int ControlAreaLeftRelative(void) const { return this.ControlAreaLeft()-this.CoordX(); } int ControlAreaRightRelative(void) const { return this.ControlAreaRight()-this.CoordX(); } int ControlAreaTopRelative(void) const { return this.ControlAreaTop()-this.CoordY(); } int ControlAreaBottomRelative(void) const { return this.ControlAreaBottom()-this.CoordY(); } //--- Return the (1) X, (2) Y coordinates, (3) width and (4) height of the element right scroll area height //--- Visibility scope height virtual int VisibleAreaHeight(void) const { return this.YSize(); } virtual bool SetVisibleAreaHeight(const int value,const bool only_prop) { ::ResetLastError();

if((!only_prop && CGBaseObj::SetYSize(value))only_prop)

{ [/CODE] 逐行拆解:ControlAreaRight 取左边界加宽度得右边界;ControlAreaTop 用对象 Y 坐标加纵向偏移量;ControlAreaBottom 由上边界加高度推出。Relative 四个函数都减掉 CoordX/CoordY,把绝对坐标折回对象内部坐标系,方便做命中测试和子控件排版。 VisibleAreaHeight 直接返回 YSize(),也就是对象声明的纵向尺寸;SetVisibleAreaHeight 先清错误码,再按 only_prop 决定只改属性还是连底层 CGBaseObj 的 Y 尺寸一起设。外汇和贵金属图表挂这类自定义控件属高风险操作,参数设错可能导致面板绘制异常。开 MT5 把这段塞进你的对象类里,改改 ControlAreaYShift 看子区域会不会按预期错位。

MQL5 / C++
class="type">int ControlAreaRight(class="type">void) const { class="kw">return this.ControlAreaLeft()+this.ControlAreaWidth(); }
class="type">int ControlAreaTop(class="type">void) const { class="kw">return this.CoordY()+this.ControlAreaYShift(); }
class="type">int ControlAreaBottom(class="type">void) const { class="kw">return this.ControlAreaTop()+this.ControlAreaHeight(); }
class=class="str">"cmt">//--- Return the relative coordinate(class="num">1) of the left, (class="num">2) right, (class="num">3) top and(class="num">4) bottom edge of the element control area
class="type">int ControlAreaLeftRelative(class="type">void) const { class="kw">return this.ControlAreaLeft()-this.CoordX(); }
class="type">int ControlAreaRightRelative(class="type">void) const { class="kw">return this.ControlAreaRight()-this.CoordX(); }
class="type">int ControlAreaTopRelative(class="type">void) const { class="kw">return this.ControlAreaTop()-this.CoordY(); }
class="type">int ControlAreaBottomRelative(class="type">void) const { class="kw">return this.ControlAreaBottom()-this.CoordY(); }

class=class="str">"cmt">//--- Return the(class="num">1) X, (class="num">2) Y coordinates, (class="num">3) width and(class="num">4) height of the element right scroll area height
class=class="str">"cmt">//--- Visibility scope height
class="kw">virtual class="type">int VisibleAreaHeight(class="type">void) const { class="kw">return this.YSize(); }
class="kw">virtual class="type">bool SetVisibleAreaHeight(const class="type">int value,const class="type">bool only_prop)
   {
   ::ResetLastError();
   if((!only_prop && CGBaseObj::SetYSize(value)) || only_prop)
      {

◍ 可见区域与光标命中判定的底层写法

在自定义图形控件里,可见区域(visible area)是一块逻辑视口:它决定了哪部分画布内容真正参与鼠标交互与重绘。上面这段实现把坐标和尺寸拆成四个独立 setter,每个都通过 SetProperty 写入 CANV_ELEMENT_PROP_VISIBLE_AREA_* 属性,失败就走 CMessage::ToLog 报错误码并返回 false。 SetVisibleArea(x,y,w,h) 只是四个 setter 的批量封装,最后一个参数统一传 false,意味着本次不触发重绘——调用方可以在一次逻辑里连续改四项后再手动刷新,避免四次冗余绘制。ResetVisibleArea() 则把可见区域重置为 (0,0,Width(),Height()),即控件全尺寸,适合初始化或退出局部缩放时调用。 CursorInsideVisibleArea(x,y) 用四个边界做闭区间判断:x 在左界与右界之间、y 在上界与下界之间才返回 true。实际做 MT5 面板时,若你的控件支持滚动视口,光标命中测试必须走这套边界而不是控件整体矩形,否则滚动后点击会错位。外汇与贵金属图表上的自定义面板属于高风险交互场景,坐标算错可能误触下单逻辑,建议在策略测试器里先用静态坐标验证命中函数再接事件。

MQL5 / C++
this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,value);
class="kw">return true;
}
else
   CMessage::ToLog(DFUN,::GetLastError(),true);
class="kw">return false;
}
class=class="str">"cmt">//--- Set relative coordinates and size of the visible area
class="type">void SetVisibleArea(const class="type">int x,const class="type">int y,const class="type">int w,const class="type">int h)
   {
   this.SetVisibleAreaX(x,false);
   this.SetVisibleAreaY(y,false);
   this.SetVisibleAreaWidth(w,false);
   this.SetVisibleAreaHeight(h,false);
   }
class=class="str">"cmt">//--- Sets the size of the visible area equal to the entire object
class="type">void ResetVisibleArea(class="type">void) { this.SetVisibleArea(class="num">0,class="num">0,this.Width(),this.Height()); }

class=class="str">"cmt">//--- Return the(class="num">1) X coordinate, (class="num">2) right border, (class="num">3) Y coordinate, (class="num">4) bottom border of the visible area
class=class="str">"cmt">//+-----------------------------------------------------------------------------+
class=class="str">"cmt">//|Return the position of the cursor relative to the visible area of the element|
class=class="str">"cmt">//+-----------------------------------------------------------------------------+
class="type">bool CGCnvElement::CursorInsideVisibleArea(const class="type">int x,const class="type">int y)
  {
   class="kw">return(x>=this.CoordXVisibleArea() && x<=this.RightEdgeVisibleArea() && y>=this.CoordYVisibleArea() && y<=this.BottomEdgeVisibleArea());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Return the cursor position relative to the element control area   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CGCnvElement::CursorInsideControlArea(const class="type">int x,const class="type">int y)
  {

控件区鼠标事件与元素创建的虚函数接口

在自定义表单类里,控件区(ControlArea)和滚动区(ScrollArea)的鼠标交互被拆成了六类虚函数,方便子类重写各自逻辑。 判断光标是否落在控件矩形内,靠一句坐标边界与运算:x 和 y 必须同时处于 ControlAreaLeft/Right/Top/Bottom 返回的区间,返回布尔值供消息路由使用。 具体事件处理器包括:滚动区内按下任意键、滚轮滚动;控件区内无按键悬停、按下任意键、滚轮滚动。这些全声明为 virtual void,参数统一是 MQL5 事件系统的 id、lparam、dparam、sparam 四件套。 公开方法 CreateNewElement 负责在指定坐标 (x,y) 与尺寸 (w,h) 上新建图形元素,并接收 colour 与 opacity(0~255 的 uchar)控制外观。实际写面板时,opacity 设 255 为不透明,设 120 左右会看到明显半透明层叠效果。 把这些虚函数留空还是重写,直接决定你的 MT5 面板能否响应点击和滚轮——开 MT5 新建 EA 继承 CForm 试一下便知。

MQL5 / C++
class="kw">return(x>=this.ControlAreaLeft() && x<=this.ControlAreaRight() && y>=this.ControlAreaTop() && y<=this.ControlAreaBottom());
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the window scrolling area, a mouse button is clicked(any)&class="macro">#x27; event handler
  class="kw">virtual class="type">void        MouseScrollAreaPressedHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam);
class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the window scrolling area, the mouse wheel is being scrolled&class="macro">#x27; event handler
  class="kw">virtual class="type">void        MouseScrollAreaWhellHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam);
class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the control area, no mouse buttons are clicked&class="macro">#x27; event handler
  class="kw">virtual class="type">void        MouseControlAreaNotPressedHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam);
class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the control area, a mouse button is clicked(any)&class="macro">#x27; event handler
  class="kw">virtual class="type">void        MouseControlAreaPressedHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam);
class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the control area, the mouse wheel is being scrolled&class="macro">#x27; event handler
  class="kw">virtual class="type">void        MouseControlAreaWhellHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam);
class=class="str">"cmt">//--- Send a message about the event
  class="kw">virtual class="type">bool        SendEvent(const class="type">long chart_id,const class="type">class="kw">ushort event_id);
class="kw">public:
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a new attached element                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CForm::CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                             const class="type">int x,
                             const class="type">int y,
                             const class="type">int w,
                             const class="type">int h,
                             const class="type">class="kw">color colour,
                             const class="type">uchar opacity,

「图形对象创建与鼠标状态位映射」

在 MT5 自定义 GUI 里,新增一个画布元素时先走 CreateAndAddNewElement 拿到指针,若返回 NULL 直接退出,避免空指针把整个表单绘制打断。 拿到对象后有两行关键绑定:SetMain 会把当前表单或上层对象设为主容器,SetBase 继承基底坐标参考;这两步漏掉,子元素在鼠标命中测试时坐标会整体偏移。 鼠标状态用 ushort 的位段表达,最低 0~5 位分别对应左键(1)、右键(2)、SHIFT(4)、CTRL(8)、中键(16)、附加键(32)。位 4 的十进制 16 即十六进制 0x10,写事件过滤时直接用按位与就能判键。 外汇与贵金属图表挂这类 UI 属高风险操作环境,实盘前请在模拟账户跑通鼠标交互逻辑,防止事件阻塞导致下单延迟。

MQL5 / C++
const class="type">bool activity,
const class="type">bool redraw)
  {
class=class="str">"cmt">//--- Create a new graphical element
   CGCnvElement *obj=this.CreateAndAddNewElement(element_type,x,y,w,h,colour,opacity,activity);
class=class="str">"cmt">//--- If the object has been created, draw the added object and class="kw">return &class="macro">#x27;true&class="macro">#x27;
   if(obj==NULL)
      class="kw">return false;
   obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain());
   obj.SetBase(this.GetBase());
   obj.Erase(colour,opacity,redraw);
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set and get the mouse status relative to the form                |
class=class="str">"cmt">//+------------------------------------------------------------------+
ENUM_MOUSE_FORM_STATE CForm::MouseFormState(const class="type">int id,const class="type">long lparam,const class="type">class="kw">double dparam,const class="type">class="kw">string sparam)
  {
class=class="str">"cmt">//--- Data location in the class="type">class="kw">ushort value of the button status
   class=class="str">"cmt">//---------------------------------------------------------------------------
   class=class="str">"cmt">//  bit     |    byte   |                 state                |   dec    |  hex  |
   class=class="str">"cmt">//---------------------------------------------------------------------------
   class=class="str">"cmt">//   class="num">0      |     class="num">0    | left mouse button                    |    class="num">1     |   class="num">1   |
   class=class="str">"cmt">//---------------------------------------------------------------------------
   class=class="str">"cmt">//   class="num">1      |     class="num">0    | right mouse button                   |    class="num">2     |   class="num">2   |
   class=class="str">"cmt">//---------------------------------------------------------------------------
   class=class="str">"cmt">//   class="num">2      |     class="num">0    | SHIFT key                            |    class="num">4     |   class="num">4   |
   class=class="str">"cmt">//---------------------------------------------------------------------------
   class=class="str">"cmt">//   class="num">3      |     class="num">0    | CTRL key                             |    class="num">8     |   class="num">8   |
   class=class="str">"cmt">//---------------------------------------------------------------------------
   class=class="str">"cmt">//   class="num">4      |     class="num">0    | middle mouse button                  |   class="num">16     |  class="num">10   |
   class=class="str">"cmt">//---------------------------------------------------------------------------
   class=class="str">"cmt">//   class="num">5      |     class="num">0    | class="num">1 add. mouse button                 |   class="num">32     |  class="num">20   |
   class=class="str">"cmt">//---------------------------------------------------------------------------
把重复劳动交给小布
这类控件区域的事件绑定和裁剪判定,小布盯盘的后台诊断已内置样例,打开对应品种页就能直接比对你的实现是否漏了控制区注册。

常见问题

原实现把隔板操作事件漏在活动区,需在控件区域的光标处理里补控制区事件,并在枚举常量改名后统一引用控制区域状态。
可以,小布盯盘的品种页内置了图形元素依附关系诊断,能标出哪些对象在构造时没拿到正确主父级,省去手动翻日志。
根因是对象创建后传主父级常传错,导致元素不知自己归属;本篇建议改到构造函数直接传指代,下篇会落地。
新增鼠标状态常量后宏自动顺延,不必手改偏移,避免事件码重叠引发控件误响应。