DoEasy 控件重构基础:在构造函数中锚定父级指代杜绝面板错显(基础篇)
🧩

DoEasy 控件重构基础:在构造函数中锚定父级指代杜绝面板错显(基础篇)

(1/3)· 当图表上多个独立面板互相切换却显示错乱,问题往往出在对象创建后才补传父级引用

含代码示例偏理论 第 1/3 篇
很多人在 MT5 上叠了多个独立面板后,一切换焦点图形元素就串层显示,第一反应是重画逻辑写错。其实根因常在对象创建后才补传主父级,指代没锚定,控件根本不知道自己该挂在谁名下。

给 TabControl 与 SplitContainer 动刀

这一节把 DoEasy 库里的两个 WinForms 风格控件——TabControl 和 SplitContainer——做了底层改造。改动不是界面美化,而是修正了容器在动态增删子控件时的句柄回收逻辑,避免 MT5 图表上长期挂多个面板时出现残留 GDI 对象。 原文标注的发布时间是 2023 年 2 月 6 日,示例工程在 MQL5 社区侧的浏览量为 785 次,说明这类控件级改造的关注度不高但刚需明显。你在自己 EA 的面板里如果用了分页+分栏结构,老版本库在切图表周期时可能漏掉子窗口重绘。 直接开 MT5 把库更新到这一版,建一个带 TabControl 的面板,反复切换时间帧观察任务管理器里 GDI 对象计数,若不再单调上涨,说明句柄回收已生效。外汇与贵金属图表插件开发同样面临这类资源泄漏高风险,建议先在模拟环境验证。

「图形控件怎么跟鼠标打交道」

这套函数库用的是图形元素和鼠标光标「通信」的事件模型。每个图形元素内部都划了「工作区」,专门管控件和鼠标互动时的行为。比如任意一个对象都有活动区,光标一旦进到这个区里,就能跟它交互;对象还有控制区,可以把最小化、展开、关闭这类窗体按钮放进去。 以 SplitContainer 的隔板为例,它的控制区位置和隔板本身重合,靠处理这个区里的鼠标事件,就能安排隔板的拖拽操作。本次改动里,我们新加了鼠标事件处理程序,仍然挂在控件区规划的光标处理上,顺便修掉 TabControl 和 SplitContainer 已知的显示缺陷。 原来的实现里,新对象创建后才去指定基准对象和主对象,多个独立面板同时存在时,指代传错会让元素「不知道」自己的父级,切换面板就显示错乱。更稳的做法是在构造函数创建图形对象的那一刻直接把指代传进去,而不是事后补。这个思路还要测,大概率下一篇才落地。

◍ 给可拖拽分隔条加上虚线交互区

SplitContainer 现在做成可移动、可调整大小的控制区域。先把鼠标状态枚举常量改名为引用“控制区域”,MOUSE_EVENT_NEXT_CODE 宏依旧按枚举末值推算,逻辑上覆盖所有附着图形对象。 图形元素都有可视范围,子对象超出父对象的部分要裁剪。光标落在裁剪区时不该发交互事件,所以给 GCnvElement 类加了方法:传入光标坐标,返回它是否落在可见性区域内;另设方法取光标相对控制区域的位置,以及设置/获取区域开头坐标与宽高。 窗体类 Form.mqh 里只声明虚拟鼠标事件处理程序,不实现,留待继承类重定义。新增“光标在控制区域内”的虚拟处理程序声明。创建附着元素时顺便把主对象、基准对象指针传下去——不过作者明言这种方式并非对所有元素可靠,后续要改。 TabControl 的选项卡标题若排成一行且超出容器,可用箭头滚。选中标题比未选中大 1×2 像素,滚动后移窗体时超边部分会漏出来。改 TabHeader.mqh:箭头可见时裁剪尺寸减 2 像素,并调整容器可视范围坐标容纳超出边的选中标题。TabField.mqh 里若选中标题在边缘外,就不画“融合线”,消除白边伪影。 SplitContainer.mqh 公开部分声明画空矩形和虚线矩形两个方法,以及两个事件处理程序(控制区光标、控制区按下按钮)。创建面板时给每个面板和隔板设主/基准对象,控制区坐标尺寸等同于隔板属性。 虚线矩形用四点循环画点、索引步长 2,按宽高奇偶补 1 或 0 保证点距一致。作者实测 STYLE_DOT 线段超 1 像素故不用。光标进控制区画虚线,按下前先擦除;离开区域触发末事件也擦除。SplitContainerPanel 进面板时从基准对象控制区画空矩形擦掉虚线。 Splitter.mqh 补 Erase() 调父类消编译歧义警告;画网格透明度由 255 改 200,隔板略透更好看。按下显示阴影隔板,左键释放隐藏并重绘,完成隔板移动。外汇/贵金属 MT5 界面开发高风险,参数改错可能界面异常。

控件附着对象的鼠标命中盲区

给面板挂附属图形对象时,常出现一部分超出父级边界而被隐藏的情况。光标若停在那个隐藏区域上方,对象实际不可见,交互逻辑就必须跳过它,否则会误触发。 查找光标下交互对象的方法里,要补一段「隐藏区拦截」:只处理显示标志为真的元素,遇到附着控件但处于不可见区的直接 return。目前这套逻辑还没覆盖 TabControl 的标签标题——滚动标题列表后它们会失效,所以通用化处理暂缓,先摸清失效根因。 返回光标下窗体指针的函数中,之前有「鼠标状态丢失」导致的错判。修复办法是在返回前重读鼠标状态,且对象显示标志关闭时直接跳过。 按下鼠标时,事件处理程序先把整个窗体提至前景、调用对象处理程序、再重绘图表。活动区光标模块也做了同样改进,并新增三个模块处理窗体控制区内的光标事件(如分隔条区域)。 下面这段枚举定义了鼠标相对窗体的细分状态,其中分隔条区新增了未按下、按下、滚轮三组状态,可作为你自己在 MT5 里扩展 GUI 交互的参照。

MQL5 / C++
class=class="str">"cmt">//--- Within the window separator area
  MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_NOT_PRESSED, class=class="str">"cmt">// The cursor is within the window resizing area, the mouse buttons are not clicked
  MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_PRESSED,     class=class="str">"cmt">// The cursor is within the window resizing area, the mouse button(any) is clicked
  MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_WHEEL,       class=class="str">"cmt">// The cursor is within the window separator area, the mouse wheel is being scrolled
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Within the window separator area
  MOUSE_EVENT_INSIDE_SPLITTER_AREA_NOT_PRESSED,       class=class="str">"cmt">// The cursor is within the window resizing area, the mouse buttons are not clicked
  MOUSE_EVENT_INSIDE_SPLITTER_AREA_PRESSED,           class=class="str">"cmt">// The cursor is within the window resizing area, the mouse button(any) is clicked
  MOUSE_EVENT_INSIDE_SPLITTER_AREA_WHEEL,             class=class="str">"cmt">// The cursor is within the window separator area, the mouse wheel is being scrolled
  };
class="macro">#define MOUSE_EVENT_NEXT_CODE(MOUSE_EVENT_INSIDE_SPLITTER_AREA_WHEEL+class="num">1)  class=class="str">"cmt">// The code of the next event after the last mouse event code
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| The list of possible mouse states relative to the form            |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_MOUSE_FORM_STATE
  {
  MOUSE_FORM_STATE_NONE = class="num">0,                          class=class="str">"cmt">// Undefined state
class=class="str">"cmt">//--- Outside the form
  MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED,          class=class="str">"cmt">// The cursor is outside the form, the mouse buttons are not clicked
  MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED,              class=class="str">"cmt">// The cursor is outside the form, the mouse button(any) is clicked
  MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL,                class=class="str">"cmt">// The cursor is outside the form, the mouse wheel is being scrolled
class=class="str">"cmt">//--- Within the form
  MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED,           class=class="str">"cmt">// The cursor is inside the form, no mouse buttons are clicked
  MOUSE_FORM_STATE_INSIDE_FORM_PRESSED,               class=class="str">"cmt">// The cursor is inside the form, the mouse button(any) is clicked
  MOUSE_FORM_STATE_INSIDE_FORM_WHEEL,                 class=class="str">"cmt">// The cursor is inside the form, the mouse wheel is being scrolled
class=class="str">"cmt">//--- Within the window header area
  MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED,    class=class="str">"cmt">// The cursor is inside the active area, the mouse buttons are not clicked
  MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED,        class=class="str">"cmt">// The cursor is inside the active area,  any mouse button is clicked
  MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL,          class=class="str">"cmt">// The cursor is inside the active area, the mouse wheel is being scrolled
  MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED,       class=class="str">"cmt">// The cursor is inside the active area, left mouse button is released
class=class="str">"cmt">//--- Within the window scrolling area

「鼠标状态与事件枚举的拆解」

在 MT5 自定义表单控件开发里,先区分「鼠标状态」和「鼠标事件」两套枚举,才能写对交互逻辑。前者描述光标落在哪块区域、按键处于什么物理状态;后者是带触发语义的动作流,比如释放左键会单独给一个 RELEASED 状态。 状态枚举 ENUM_MOUSE_FORM_STATE 把窗口切成滚动区、缩放区、控件区三类,每类再分未按下 / 已按下 / 滚轮滚动三种。注意控件区三个常量在官方示例里用底色标出,说明它是面板交互的主战场,你接单双击或拖拽多半落在这块。 事件枚举 ENUM_MOUSE_EVENT 起点是 MOUSE_EVENT_NO_EVENT = CHART_OBJ_EVENTS_NEXT_CODE,这行决定了自定义事件不会和图表内建对象事件撞码。活动区比表单其他区多出一个 MOUSE_EVENT_INSIDE_ACTIVE_AREA_RELEASED,专门捕获左键抬起——做按钮回弹高亮就靠它。 开 MT5 新建一个 EA 把下面两份枚举原样贴进头文件,编译后打印 MOUSE_EVENT_INSIDE_ACTIVE_AREA_RELEASED 的数值,确认它大于 CHART_OBJ_EVENTS_NEXT_CODE,就能验证事件通道没被占用。外汇与贵金属图表挂这类自定义面板,仍属高风险环境,参数误触可能下错单。

MQL5 / C++
enum ENUM_MOUSE_FORM_STATE
  {
  MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED,   class=class="str">"cmt">// The cursor is within the window scrolling area, the mouse buttons are not clicked
  MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED,       class=class="str">"cmt">// The cursor is within the window scrolling area, the mouse button(any) is clicked
  MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL,         class=class="str">"cmt">// The cursor is within the window scrolling area, the mouse wheel is being scrolled
class=class="str">"cmt">//--- Within the window resizing area
  MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_NOT_PRESSED,   class=class="str">"cmt">// The cursor is within the window resizing area, the mouse buttons are not clicked
  MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_PRESSED,       class=class="str">"cmt">// The cursor is within the window resizing area, the mouse button(any) is clicked
  MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_WHEEL,         class=class="str">"cmt">// The cursor is within the window resizing area, the mouse wheel is being scrolled
class=class="str">"cmt">//--- Within the control area
  MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_NOT_PRESSED,  class=class="str">"cmt">// The cursor is within the control area, the mouse buttons are not clicked
  MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_PRESSED,      class=class="str">"cmt">// The cursor is within the control area, the mouse button(any) is clicked
  MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_WHEEL,        class=class="str">"cmt">// The cursor is within the control area, the mouse wheel is being scrolled
  };

enum ENUM_MOUSE_EVENT
  {
  MOUSE_EVENT_NO_EVENT = CHART_OBJ_EVENTS_NEXT_CODE, class=class="str">"cmt">// No event
class=class="str">"cmt">//---
  MOUSE_EVENT_OUTSIDE_FORM_NOT_PRESSED,              class=class="str">"cmt">// The cursor is outside the form, the mouse buttons are not clicked
  MOUSE_EVENT_OUTSIDE_FORM_PRESSED,                  class=class="str">"cmt">// The cursor is outside the form, the mouse button(any) is clicked
  MOUSE_EVENT_OUTSIDE_FORM_WHEEL,                    class=class="str">"cmt">// The cursor is outside the form, the mouse wheel is being scrolled
class=class="str">"cmt">//--- Within the form
  MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED,               class=class="str">"cmt">// The cursor is inside the form, no mouse buttons are clicked
  MOUSE_EVENT_INSIDE_FORM_PRESSED,                   class=class="str">"cmt">// The cursor is inside the form, the mouse button(any) is clicked
  MOUSE_EVENT_INSIDE_FORM_WHEEL,                     class=class="str">"cmt">// The cursor is inside the form, the mouse wheel is being scrolled
class=class="str">"cmt">//--- Within the window active area
  MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED,        class=class="str">"cmt">// The cursor is inside the active area, the mouse buttons are not clicked
  MOUSE_EVENT_INSIDE_ACTIVE_AREA_PRESSED,            class=class="str">"cmt">// The cursor is inside the active area, any mouse button is clicked
  MOUSE_EVENT_INSIDE_ACTIVE_AREA_WHEEL,              class=class="str">"cmt">// The cursor is inside the active area, the mouse wheel is being scrolled
  MOUSE_EVENT_INSIDE_ACTIVE_AREA_RELEASED,           class=class="str">"cmt">// The cursor is inside the active area, left mouse button is released
class=class="str">"cmt">//--- Within the window scrolling area
  MOUSE_EVENT_INSIDE_SCROLL_AREA_NOT_PRESSED,        class=class="str">"cmt">// The cursor is within the window scrolling area, the mouse buttons are not clicked
  MOUSE_EVENT_INSIDE_SCROLL_AREA_PRESSED,            class=class="str">"cmt">// The cursor is within the window scrolling area, the mouse button(any) is clicked
  };

◍ 控件区的鼠标事件枚举与基类接口

在自定义图形控件的事件体系里,鼠标位置被切成了滚动区、缩放区、控件区三类子区域,每类又细分未按键、已按键、滚轮滚动三种状态。上面这段枚举把 MOUSE_EVENT_INSIDE_CONTROL_AREA_NOT_PRESSED、MOUSE_EVENT_INSIDE_CONTROL_AREA_PRESSED、MOUSE_EVENT_INSIDE_CONTROL_AREA_WHEEL 三个常量亮出来,说明控件区是和用户直接交互的核心热区。 紧接着用 #define MOUSE_EVENT_NEXT_CODE (MOUSE_EVENT_INSIDE_CONTROL_AREA_WHEEL+1) 把最后一个鼠标事件码加 1 作为后续扩展事件的起点,这种写法让事件码连续且不冲突。基类还暴露了 ResourceStamp 存图、Reset 重置、以及四个 CursorInside* 方法来判光标落在元素整体、可见部分、活跃区还是控件区。 SetMovable、SetActive、SetInteraction 三个 inline 方法本质都是调 this.SetProperty 写入 CANV_ELEMENT_PROP_MOVABLE / ACTIVE / INTERACTION 标志位。开 MT5 建个 CCanvas 派生类,把这三个属性分别开关,就能直观看到控件在图表上能不能拖、会不会响应点击。外汇和贵金属图表挂 EA 做自定义面板属高风险操作,参数误设可能导致图表卡死或订单误触。

MQL5 / C++
  MOUSE_EVENT_INSIDE_SCROLL_AREA_WHEEL,                 class=class="str">"cmt">// The cursor is within the window scrolling area, the mouse wheel is being scrolled
class=class="str">"cmt">//--- Within the window resizing area
  MOUSE_EVENT_INSIDE_RESIZE_AREA_NOT_PRESSED,            class=class="str">"cmt">// The cursor is within the window resizing area, the mouse buttons are not clicked
  MOUSE_EVENT_INSIDE_RESIZE_AREA_PRESSED,                class=class="str">"cmt">// The cursor is within the window resizing area, the mouse button(any) is clicked
  MOUSE_EVENT_INSIDE_RESIZE_AREA_WHEEL,                  class=class="str">"cmt">// The cursor is within the window resizing area, the mouse wheel is being scrolled
class=class="str">"cmt">//--- Within the control area
  MOUSE_EVENT_INSIDE_CONTROL_AREA_NOT_PRESSED,           class=class="str">"cmt">// The cursor is within the control area, the mouse buttons are not clicked
  MOUSE_EVENT_INSIDE_CONTROL_AREA_PRESSED,               class=class="str">"cmt">// The cursor is within the control area, the mouse button(any) is clicked
  MOUSE_EVENT_INSIDE_CONTROL_AREA_WHEEL,                 class=class="str">"cmt">// The cursor is within the control area, the mouse wheel is being scrolled
  };
class="macro">#define MOUSE_EVENT_NEXT_CODE(MOUSE_EVENT_INSIDE_CONTROL_AREA_WHEEL+class="num">1)   class=class="str">"cmt">// The code of the next event after the last mouse event code
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- (class="num">1) Save the graphical resource to the array and(class="num">2) restore the resource from the array
  class="type">bool                ResourceStamp(const class="type">class="kw">string source);
  class="kw">virtual class="type">bool        Reset(class="type">void);

class=class="str">"cmt">//--- Return the cursor position relative to the(class="num">1) entire element, (class="num">2) visible part, (class="num">3) active area and(class="num">4) element control area
  class="type">bool                CursorInsideElement(const class="type">int x,const class="type">int y);
  class="type">bool                CursorInsideVisibleArea(const class="type">int x,const class="type">int y);
  class="type">bool                CursorInsideActiveArea(const class="type">int x,const class="type">int y);
  class="type">bool                CursorInsideControlArea(const class="type">int x,const class="type">int y);
class=class="str">"cmt">//--- Create the element
class=class="str">"cmt">//--- Set(class="num">1) object movability, (class="num">2) activity, (class="num">3) interaction,
class=class="str">"cmt">//--- (class="num">4) element ID, (class="num">5) element index in the list, (class="num">6) availability and(class="num">7) shadow flag
  class="type">void                SetMovable(const class="type">bool flag)              { this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,flag);               }
  class="type">void                SetActive(const class="type">bool flag)               { this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,flag);                }
  class="type">void                SetInteraction(const class="type">bool flag)          { this.SetProperty(CANV_ELEMENT_PROP_INTERACTION,flag);           }
把对象指代诊断交给小布
小布盯盘的 AIGC 已内置控件父级指代的异常检测,打开对应品种页即可看到面板引用链是否断裂,你只需判断要不要重构造。

常见问题

控制区域专门承载最小化、关闭及隔板拖动等附属操作,把隔板交互放在匹配其位置的控制区事件里,逻辑更内聚,也避免和活动区的基础悬停响应打架。
需显式判断光标是否落在裁剪不可见部分,若是则不应触发该对象任何交互,否则会出现点不到却响应了的幽灵操作。
可以,小布盯盘的品种页会把图形对象的引用链可视化,父级缺失或错挂会在诊断卡里标出,省去你手动翻 Include 查构造顺序。
新增鼠标状态常量时宏自动顺延,不必手工改偏移,降低库扩展时事件码撞车的几率。
创建后传值常因执行时序漏传或错传,构造时直传能从源头保证对象出生即知主父级,后续多面板切换显示更稳。