SplitContainer 面板隔板:从零理解可拖拽分区控件的底层构造(基础篇)
🧩

SplitContainer 面板隔板:从零理解可拖拽分区控件的底层构造(基础篇)

(1/3)· 很多 MQL5 界面库用死了静态面板,却从没搞清隔板对象该如何接管鼠标区域与重绘

偏理论进阶 第 1/3 篇
不少交易者自己拼 UI 时把面板写死成固定比例,结果想调一下图表和信号区的空间就得改代码重编译。MQL5 没有原生光标替换接口,直接用 Windows 表单思路套过来只会卡在悬停反馈这一步。

「给面板加一块可拖拽的隔板」

在 DoEasy 控件库里,SplitContainer 的本质就是一块能上下或左右分隔两个子面板的隔板对象,用户拖动它即可实时改变两侧区域占比。 在 MT5 里这类控件常用于把图表上的信息面板和信号面板拆开,避免相互遮挡。实际验证时,可在库示例工程加载 SplitContainer 演示,拖动隔板看子面板重绘是否卡顿。 原文示例发布于 2023 年 2 月 2 日,公开阅读量约 1187,说明这类基础控件在量化交易者里需求稳定。外汇与贵金属图表加载自定义控件存在平台兼容性风险,建议先在模拟账户验证。

SplitContainer 隔板交互的取舍与实现

当前函数库已能创建 SplitContainer 控件,以两个默认参数生成静态面板对象,内部由隔板分隔两块区域。原生 MS Visual Studio 里隔板可拖动改尺寸,悬停出特征光标、按住出虚线框、移动出阴影跟随、松手重排面板。 MQL5 没有更改光标外观的接口,所以这里不去做“抓取拖动”信号,而是直接在隔板区覆盖阴影来表示可移动。算法比 VS 更短:悬停显阴影,按住阴影移动时面板尺寸立即跟随新隔板位置变化,松手且光标离开后阴影隐藏、尺寸保留。 隔板对象派生自 CWinFormBase 基类,重写了清除与重绘虚拟方法,用虚线填充整块区域。可见性由 SplitContainer 事件处理:悬停控制区显示,离开隐藏。这类区域既能包住隔板本身,也能包住最小化、最大化、关闭等按钮。 外汇与贵金属 MT5 环境属高风险,自定义控件仅用于界面交互,不预示任何交易结果。

◍ 给控件库补上窗体缩放与隔板状态底座

要在 MT5 自建 UI 里支持鼠标拖拽改窗体大小、放可移动隔板,得先给 DoEasy 底层补一组枚举与属性。原库图形元素整数属性总数到本次改动被推到 122 个,新增的隔板方向默认 0 即垂直位,这数字直接决定你后面序列化对象时文件结构是否对齐。 改动集中在 Defines.mqh、Data.mqh 与 Graph 下的几个类。鼠标状态枚举新增了窗口顶部、底部、左、右缩放区的一组 ID,事件列表里把原来的 MOUSE_EVENT_INSIDE_SCROLL_AREA_WHEEL 位置让给 MOUSE_EVENT_INSIDE_SPLITTER_AREA_WHEEL,末尾仍以 NEXT_CODE 宏收口,保证后续追加不冲突。 Form.mqh 里设置鼠标状态的方法要写一位字位:CursorInsideControlArea() 返回 true 时,把 m_mouse_state_flags 的第 10 位(bit 10)置 1,出区域则清 0。这一位就是你 EA 面板判断「光标是否在控件区」的唯一信源,没它隔板拖动逻辑跑不起来。 GCnvElement 构造函数给所有新属性默认填零,结构读写方法双向映射字段;WinFormBase 重绘前先查显示标志,不显示就跳过。下面这段是缩放区鼠标状态的枚举片段,注意注释行是新增区块起始:

MQL5 / C++
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
   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

「把鼠标状态映射成可捕获的事件枚举」

在 MT5 自定义面板开发里,光知道光标在哪儿不够,还得区分「按没按」「滚没滚」。原文把窗口交互拆成两套枚举:一套描述光标所在区域与按键状态(FORM_STATE),一套把这些状态直接定义为可监听的事件(MOUSE_EVENT)。 枚举 ENUM_MOUSE_EVENT 以 CHART_OBJ_EVENTS_NEXT_CODE 为起点赋值,保证不与图表内置对象事件冲突。它覆盖窗体外、窗体内、标题激活区、滚动区、缩放区五类空间,每类再分 NOT_PRESSED / PRESSED / WHEEL,激活区额外多一个 RELEASED 左键释放态,共 17 个枚举值。 实际写 EA 或指标时,直接在 OnChartEvent 里用 switch 接这些事件码,就能让面板响应拖拽标题、滚轮翻页。外汇与贵金属图表挂这类自定义 UI 属高风险操作环境,事件逻辑出错可能卡死图表,建议先在模拟账户 MT5 里单步验证。

MQL5 / C++
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 header 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_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
  };

拆分区鼠标事件与图形元素类型枚举

在自定义指标或 EA 的 GUI 开发中,先要把鼠标在窗口分隔条(splitter)上的状态拆清楚。MT5 内部用一组枚举值区分光标是否落在分隔区、是否按下、是否滚动滚轮:未按下、按下任意键、滚轮滚动,分别对应三个独立事件码。 为了不让后续事件码冲突,工程里习惯用宏把“最后一个鼠标事件码 +1”定义为 MOUSE_EVENT_NEXT_CODE。例如本段中 MOUSE_EVENT_INSIDE_SPLITTER_AREA_WHEEL 加 1,得到下一个可用事件起点,方便自己扩展触摸或右键事件。 图形元素则归到 ENUM_GRAPH_ELEMENT_TYPE 里。基础层有 STANDARD、SHADOW_OBJ、ELEMENT、FORM、WINDOW;往上叠 WinForms 仿真层,从 WF_BASE 到 WF_CONTAINER、WF_PANEL、WF_TAB_CONTROL、WF_SPLIT_CONTAINER,再到具体控件 WF_LABEL、WF_BUTTON、WF_CHECKBOX、WF_RADIOBUTTON,共 20 个类型常量。 开 MT5 新建一个空 EA,把下面枚举贴进头文件,编译后能在调试器里看到这些常量。做贵金属或外汇面板时,高风险在于误把 WF_SPLIT_CONTAINER 当普通 WINDOW 处理,可能导致拖拽分隔条时事件丢失。

MQL5 / C++
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 graphical element types                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_STANDARD,                        class=class="str">"cmt">// Standard graphical object
   GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,               class=class="str">"cmt">// Extended standard graphical object
   GRAPH_ELEMENT_TYPE_SHADOW_OBJ,                      class=class="str">"cmt">// Shadow object
   GRAPH_ELEMENT_TYPE_ELEMENT,                         class=class="str">"cmt">// Element
   GRAPH_ELEMENT_TYPE_FORM,                            class=class="str">"cmt">// Form
   GRAPH_ELEMENT_TYPE_WINDOW,                          class=class="str">"cmt">// Window
   class=class="str">"cmt">//--- WinForms
   GRAPH_ELEMENT_TYPE_WF_UNDERLAY,                     class=class="str">"cmt">// Panel object underlay
   GRAPH_ELEMENT_TYPE_WF_BASE,                         class=class="str">"cmt">// Windows Forms Base
   class=class="str">"cmt">//--- &class="macro">#x27;Container&class="macro">#x27; object types are to be set below
   GRAPH_ELEMENT_TYPE_WF_CONTAINER,                    class=class="str">"cmt">// Windows Forms container base object
   GRAPH_ELEMENT_TYPE_WF_PANEL,                        class=class="str">"cmt">// Windows Forms Panel
   GRAPH_ELEMENT_TYPE_WF_GROUPBOX,                     class=class="str">"cmt">// Windows Forms GroupBox
   GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,                  class=class="str">"cmt">// Windows Forms TabControl
   GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER,              class=class="str">"cmt">// Windows Forms SplitContainer
   class=class="str">"cmt">//--- &class="macro">#x27;Standard control&class="macro">#x27; object types are to be set below
   GRAPH_ELEMENT_TYPE_WF_COMMON_BASE,                  class=class="str">"cmt">// Windows Forms base standard control
   GRAPH_ELEMENT_TYPE_WF_LABEL,                        class=class="str">"cmt">// Windows Forms Label
   GRAPH_ELEMENT_TYPE_WF_BUTTON,                       class=class="str">"cmt">// Windows Forms Button
   GRAPH_ELEMENT_TYPE_WF_CHECKBOX,                     class=class="str">"cmt">// Windows Forms CheckBox
   GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,                  class=class="str">"cmt">// Windows Forms RadioButton

◍ WinForms 图形元素与控件事件枚举的收尾定义

在自定义面板库的类型体系末尾,补上了 Windows Forms 风格控件的细分元素枚举,从基础列表对象一直列到方向箭头与分隔条。其中 GRAPH_ELEMENT_TYPE_WF_SPLITTER 负责切分容器面板的可拖拽分隔区,是布局类 EA 面板常碰到的元素。 紧随其后的 ENUM_WF_CONTROL_EVENT 把用户能触发的动作码定死了:基础点击、点击取消、Tab 切换自不必说,四个方向的滚动箭头点击也单独成事件。WF_CONTROL_EVENT_SPLITTER_MOVE 专门捕获分隔条被拖动的位置变更,配合前面的 SPLITTER 元素类型,就能在 MT5 里做可拖拽调宽的信息栏。 开 MT5 新建一个 include 把这两段枚举贴进去,编译后挂到图表,用 OnChartEvent 打印事件码,就能验证分隔条拖动是否真的回传了 WF_CONTROL_EVENT_SPLITTER_MOVE。外汇与贵金属图表上跑这类自定义 UI 仍属高风险操作,事件逻辑出错可能卡死图表。

MQL5 / C++
  GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX,              class=class="str">"cmt">// Base list object of Windows Forms elements
  GRAPH_ELEMENT_TYPE_WF_LIST_BOX,                          class=class="str">"cmt">// Windows Forms ListBox
  GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,                  class=class="str">"cmt">// Windows Forms CheckedListBox
  GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,                   class=class="str">"cmt">// Windows Forms ButtonListBox
  class=class="str">"cmt">//--- Auxiliary elements of WinForms objects
  GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM,                     class=class="str">"cmt">// Windows Forms ListBoxItem
  GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,                        class=class="str">"cmt">// Windows Forms TabHeader
  GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,                         class=class="str">"cmt">// Windows Forms TabField
  GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL,             class=class="str">"cmt">// Windows Forms SplitContainerPanel
  GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON,                      class=class="str">"cmt">// Windows Forms ArrowButton
  GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP,                   class=class="str">"cmt">// Windows Forms UpArrowButton
  GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN,                 class=class="str">"cmt">// Windows Forms DownArrowButton
  GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT,                 class=class="str">"cmt">// Windows Forms LeftArrowButton
  GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT,                class=class="str">"cmt">// Windows Forms RightArrowButton
  GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,              class=class="str">"cmt">// Windows Forms UpDownArrowButtonsBox
  GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,              class=class="str">"cmt">// Windows Forms LeftRightArrowButtonsBox
  GRAPH_ELEMENT_TYPE_WF_SPLITTER,                          class=class="str">"cmt">// Windows Forms Splitter
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| List of possible WinForms control events                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_WF_CONTROL_EVENT
  {
  WF_CONTROL_EVENT_NO_EVENT = GRAPH_OBJ_EVENTS_NEXT_CODE,class=class="str">"cmt">// No event
  WF_CONTROL_EVENT_CLICK,                                class=class="str">"cmt">// "Click on the control" event
  WF_CONTROL_EVENT_CLICK_CANCEL,                         class=class="str">"cmt">// "Canceling the click on the control" event
  WF_CONTROL_EVENT_TAB_SELECT,                           class=class="str">"cmt">// "TabControl tab selection" event
  WF_CONTROL_EVENT_CLICK_SCROLL_LEFT,                    class=class="str">"cmt">// "Clicking the control left button" event
  WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT,                   class=class="str">"cmt">// "Clicking the control right button" event
  WF_CONTROL_EVENT_CLICK_SCROLL_UP,                      class=class="str">"cmt">// "Clicking the control up button" event
  WF_CONTROL_EVENT_CLICK_SCROLL_DOWN,                    class=class="str">"cmt">// "Clicking the control down button" event
  WF_CONTROL_EVENT_SPLITTER_MOVE,                        class=class="str">"cmt">// "Control separator relocation" event
  };
让小布替你跑这套
这些控件坐标与鼠标状态的基础判定逻辑,小布盯盘的 AIGC 已内置到品种页的面板诊断里,打开对应页就能看到区域命中是否按预期工作,不必自己逐行打点。

常见问题

基类提供了清除与重绘的虚拟方法钩子,重写后才能让隔板区域跟随控件事件统一隐藏和显示,避免散落在各处的硬绘制约后续扩展。
目前小布内置的是对现有面板区域命中与状态的诊断视图,不输出完整控件源码,但可对照它标出的鼠标区域偏差来修自己的库。
原文用覆盖在隔板区的阴影区域代替抓取光标,悬停即显阴影、拖完移出即隐,是一种不依赖系统光标变更的折中方案。
它们为顶部、底部、左、右等窗口调整区域预留状态标识,当前虽未接拖拽缩放,但先定义好 ID 能让后续事件处理不破结构。
省去了虚线矩形与跟随阴影的中间态,悬停显阴影、拖动即改尺寸、移出即定型,牺牲了部分视觉过渡换取实现轻量。