树形视图控件基础:从节点类到鼠标指针的库底层拆解(基础篇)
(1/3)·想自己写 MT5 GUI 却卡在层级列表?先搞懂树形视图的项目结构与指针类
「在 MT5 里搭一个能展开的树形视图」
MetaTrader 5 自带的 GUI 标准控件里并没有树形视图(Tree View),要做分级折叠的菜单或品种列表,只能自己用 CCanvas 和鼠标事件拼。Anatoli Kazharski 在 2016-10-11 发布的示例里给出了一套可复用封装,原帖浏览量 3565、收藏 3,说明这类底层控件需求真实存在但资料稀缺。 核心思路是把每个节点存成结构体,父节点记录子节点索引区间,点击父节点时切换展开标志位并触发重绘。下面这段是节点结构定义和展开标志位的雏形,你在 MT5 里新建 EA 把这段代码贴进去就能跑通骨架。 树形视图的高风险点不在绘制,而在事件节流:节点多于 200 个时每次 OnChartEvent 全量重绘可能掉帧,实盘加载前先用历史品种跑一遍压力测试。
class="kw">struct TreeNode { class="type">class="kw">string m_text; class=class="str">"cmt">// 节点显示文本 class="type">int m_parent; class=class="str">"cmt">// 父节点索引,-class="num">1 表示根 class="type">int m_first_child; class=class="str">"cmt">// 首个子节点索引 class="type">int m_last_child; class=class="str">"cmt">// 末个子节点索引 class="type">bool m_expanded; class=class="str">"cmt">// 展开标志位 class="type">bool m_selected; class=class="str">"cmt">// 选中标志位 };
先看清这套树形视图要拆哪些件
做 MT5 自定义控件时,树形视图不是单个对象,而是一组需要分别实现的零件。从原文拆解看,至少要覆盖四个核心类与两类管理逻辑:承载单节点的 CTreeItem、绘制光标的 CPointer、统筹容器的 CTreeView,以及它们与库引擎的接驳层。 其余目录项暴露了可验证的工程边界:构建参数决定元件列表初始化方式,管理方法负责节点的增删查改,列表区域宽度需独立控制,页面项目还存在不同显示模式。事件处理方法和测试项则说明这套控件必须自己吞掉鼠标与重绘消息,而不是交给图表默认行为。 对你而言,开 MT5 新建一个空 EA 先把 CTreeItem 的构造函数抄出来跑通,比直接读完整库更有用——外汇与贵金属图表高频重绘,控件若卡在消息循环上,实盘拖拽就会掉帧。
◍ 树形视图与指针类要做什么
本开发库前几章已把静态控件和下拉日历做完,这一章集中攻一个更复杂但也更核心的元件——树形视图(tree view)。在多数完整的 MT5 图形界面库里,它都属于标配,因为层级数据的展示基本绕不开。 本次实现的树形视图带多种灵活设置和模式,具体参数可在代码层直接调,不写死在内部逻辑里,方便按自己的面板需求改样式和交互。 同时会顺带实现一个专门生成鼠标指针的类,并给出它嵌进树形视图的实际用法。外汇与贵金属 EA 界面开发属高风险定制,建议先在策略测试器里挂demo面板验证交互,再接实盘逻辑。 各章文末都附了当前阶段库完整版的下载位置,解压后请严格按归档内的目录层级放置文件,否则 MT5 编译会报找不到资源的错。
「用树形视图搭分层导航」
树形视图和扁平列表不一样,它允许在无限嵌套层级里按类别排布节点,每个父节点自带收起/展开控件,终端用户能逐级折叠局部列表。除了文字,节点还能挂图标标签,终节点则可以塞一组控件用来显示信息或触发动作。 这类结构很适合做分级目录,思路类似 Windows 资源管理器左侧的树。MetaTrader 5 里按 Ctrl+N 能呼出‘导航’窗口,里面账户、品种、EA 都是树状排的;MetaEditor 里按 Ctrl+D 同样能切换‘导航’窗格,脚本、指标、类文件也走同一套嵌套逻辑。 想在自建面板里复刻这种体验,核心就是递归创建 CTreeView 节点,把父节点 AddNode 到另一个节点之下,终节点再 Bind 控件。外汇与贵金属 GUI 工具开发属高风险辅助范畴,界面逻辑错误不会直接爆仓,但可能误导下单操作,建议先在策略测试器里跑通交互再上实盘。
树形视图节点类 CTreeItem 的骨架与坐标逻辑
做 MT5 自定义树形面板前,得先有节点元件。CTreeItem 就是干这个的:它和之前写过的菜单项有点像,但多了展开/收起状态、层级偏移和文字标签,必须单独成类。另一个配套元件是改宽度的鼠标指针 CPointer,留到后面再说。 节点由背景、箭头(+/-)、标签、文字描述四部分构成。新建 TreeItem.mqh,在 WndContainer.mqh 里 include 它,类里塞进库元件标准方法。外观在创建前用私有属性设:不同状态背景色、标签名、箭头图标、文字偏移、文字色。 类型枚举 ENUM_TYPE_TREE_ITEM 放 Enums.mqh,只有两项——TI_SIMPLE 终节点和 TI_HAS_ITEMS 带子节点。每个节点记总索引、层级编号、描述文字和状态。箭头偏移按 m_node_level 算,整棵树据此推位置。 对象命名拼法:程序名+元件索引+«treeitem»+部分标记+总索引+ID。CreateArrow() 里用节点级别算左边偏移;如果是 TI_SIMPLE 直接退出,但退出前先存坐标,后面建标签还用得上。 文字标签用 CEdit 而非 CLabel,原因很实:拖拽改区域宽度时,CLabel 文字可能冲出窗口边界。UpdateX() 管 X 坐标,UpdateWidth() 改宽度,UpdateY() 管显隐——节点一次性全建好,只切可见性,滚屏或收展时改 Y 就行。 颜色交给后面的 CTreeView 管,CTreeItem 留两个方法:按状态变色、鼠标掠过变色。节点建完的状态值 item_state 回传主方法,决定初始哪些展开。
class="macro">#include "TreeItem.mqh" class="macro">#include "Element.mqh" class="macro">#include "Window.mqh" class CTreeItem : class="kw">public CElement { class="kw">private: CWindow *m_wnd; class="kw">public: CTreeItem(class="type">void); ~CTreeItem(class="type">void); };
◍ 树形控件的基类与项目类字段拆解
在 MT5 自定义 GUI 里,树形控件通常从 CElement 派生。基类先通过 WindowPointer 把父窗口指针存进 m_wnd,用的是 GetPointer(object) 取实际对象地址,避免跨类操作悬空引用。 事件与生命周期由一组虚函数接管:OnEvent 处理 id/lparam/dparam/sparam 四类参数,OnEventTimer 默认空实现,Moving 负责拖拽重绘,Show/Hide/Reset/Delete 对应显隐与资源回收。Z 轴顺序用 SetZorders 与 ResetZorders 切换点击优先级,ResetColors 统一刷色。 CTreeItem 私有段把状态色全摊开了:m_item_back_color 是常态背景,_hover 与 _selected 分别对应鼠标悬停和选中。箭头图标分 on/off 与 selected 组合共 4 个 string 字段,标签图标走 m_icon_file,文字偏移由 m_label_x_gap、m_label_y_gap 两个 int 控制。 文字色同样三态:m_item_text_color、_hover、_selected。公开方法 ItemBackColor 仅一行赋值,直接写 m_item_back_color=clr,说明单项目配色可在运行时热改,不必重建控件。
class="type">void WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); } class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 事件处理函数 class="kw">virtual class="type">void OnEvent(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="kw">virtual class="type">void OnEventTimer(class="type">void) {} class=class="str">"cmt">//--- 移动元件 class="kw">virtual class="type">void Moving(const class="type">int x,const class="type">int y); class=class="str">"cmt">//--- (class="num">1) 显示, (class="num">2) 隐藏, (class="num">3) 重置, (class="num">4) 删除 class="kw">virtual class="type">void Show(class="type">void); class="kw">virtual class="type">void Hide(class="type">void); class="kw">virtual class="type">void Reset(class="type">void); class="kw">virtual class="type">void Delete(class="type">void); class=class="str">"cmt">//--- (class="num">1) 设置, (class="num">2) 重置 鼠标左键点击的优先级 class="kw">virtual class="type">void SetZorders(class="type">void); class="kw">virtual class="type">void ResetZorders(class="type">void); class=class="str">"cmt">//--- 重置颜色 class="kw">virtual class="type">void ResetColors(class="type">void); }; class CTreeItem : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 不同状态下的背景颜色 class="type">color m_item_back_color; class="type">color m_item_back_color_hover; class="type">color m_item_back_color_selected; class=class="str">"cmt">//--- 项目箭头的图标 class="type">class="kw">string m_item_arrow_file_on; class="type">class="kw">string m_item_arrow_file_off; class="type">class="kw">string m_item_arrow_selected_file_on; class="type">class="kw">string m_item_arrow_selected_file_off; class=class="str">"cmt">//--- 项目标签 class="type">class="kw">string m_icon_file; class=class="str">"cmt">//--- 文字标签的偏移 class="type">int m_label_x_gap; class="type">int m_label_y_gap; class=class="str">"cmt">//--- 项目不同状态下的文字颜色 class="type">color m_item_text_color; class="type">color m_item_text_color_hover; class="type">color m_item_text_color_selected; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 项目背景的颜色 class="type">void ItemBackColor(const class="type">color clr) { m_item_back_color=clr; }