DoEasy 控件重构基础:在构造函数中锚定父级指代杜绝面板错显(基础篇)
(1/3)· 当图表上多个独立面板互相切换却显示错乱,问题往往出在对象创建后才补传父级引用
给 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 交互的参照。
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,就能验证事件通道没被占用。外汇与贵金属图表挂这类自定义面板,仍属高风险环境,参数误触可能下错单。
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 做自定义面板属高风险操作,参数误设可能导致图表卡死或订单误触。
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); }