TreeView 树形控件
当你需要在界面上展示一层套一层的结构——比如文件目录、场景层级、JSON 结构——TreeView 就是你的得力助手。它自带展开/折叠、单选/多选、虚拟滚动,性能再深的树也不怕卡顿。
函数原型
namespace HX {
struct HXTreeNode {
HXString Label;
bool Expanded = true;
bool Selected = false;
std::vector<HXTreeNode> Children;
};
struct TreeViewProfile {
HXPoint Size = {200, 300};
int RowHeight = 24;
int Indent = 16;
bool MultiSelect = false;
bool Activated = false;
HXString ActivatedLabel;
HXString ActivatedPath;
bool ContextMenuRequested = false;
HXString ContextMenuLabel;
HXString ContextMenuPath;
HXPoint ContextMenuPos = {0, 0};
std::function<void(HXBufferPainter* p, const HXRect& rowRect, const HXTreeNode& node)> RenderItemCallback;
std::function<void(const HXTreeNode& node)> ItemActivatedCallback;
HXGInt ScrollbarWidth = 8;
};
void TreeView(const HXString &id, std::vector<HXTreeNode> &nodes, TreeViewProfile &profile);
}
HXTreeNode 是值类型,你直接构造一棵 std::vector<HXTreeNode> 传进去就行。控件会在交互时自动修改 Expanded 和 Selected 字段。
参数详解
HXTreeNode 节点结构
| 成员 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Label | HXString | 空 | 显示文本 |
Expanded | bool | true | 是否展开子节点 |
Selected | bool | false | 是否被选中 |
Children | std::vector<HXTreeNode> | 空 | 子节点列表 |
TreeViewProfile 配置
| 成员 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Size | HXPoint | {200, 300} | 控件宽高 |
RowHeight | int | 24 | 每行高度(像素) |
Indent | int | 16 | 每级缩进(像素) |
MultiSelect | bool | false | 是否允许多选 |
Activated | bool | false | (输出)本帧是否有节点被激活(回车或双击) |
ActivatedLabel | HXString | 空 | (输出)被激活节点的文本 |
ActivatedPath | HXString | 空 | (输出)被激活节点的路径标识 |
ContextMenuRequested | bool | false | (输出)本帧是否请求了右键菜单 |
ContextMenuLabel | HXString | 空 | (输出)右键点击的节点文本 |
ContextMenuPath | HXString | 空 | (输出)右键点击的节点路径 |
ContextMenuPos | HXPoint | {0,0} | (输出)右键菜单应弹出的位置 |
RenderItemCallback | std::function | 空 | 自定义单项渲染回调 |
ItemActivatedCallback | std::function | 空 | 节点被激活时的回调 |
ScrollbarWidth | HXGInt | 8 | 滚动条宽度 |
所谓路径(ActivatedPath / ContextMenuPath),是控件内部用 / 拼接的层级索引,例如 0/2/1。你可以用它精确定位到树中的某一个节点,而不用自己递归遍历。
返回值
TreeView 本身无返回值。所有交互结果都通过修改 profile 和 nodes 的原地状态来传达。
核心特性
层级展开与折叠
每行左侧有个小三角/箭头,点击即可展开或折叠子树。初始状态由 Expanded 决定。
HX::HXTreeNode root;
root.Label = HXStr("Assets");
root.Expanded = true; // 默认展开
HX::HXTreeNode child;
child.Label = HXStr("Textures");
root.Children.push_back(child);
单选与多选
把 MultiSelect 设为 true,用户就可以按住 Ctrl 点选多个节点,或 Shift 连选。选中的节点其 Selected 字段会被置为 true。
static HX::TreeViewProfile tvp;
tvp.MultiSelect = true; // 开启多选
虚拟滚动
TreeView 采用虚拟滚动:无论你的树有多少节点,它只渲染当前可见区域的那几行。 thousands of nodes? 没问题,帧率稳如老狗。
虚拟滚动是自动的,你不需要做任何额外配置。只要给定了 Size,滚动条就会出现并正常工作。
激活与上下文菜单
- 激活:双击节点,或选中后按回车,
Activated会被置为true。 - 右键菜单:在节点上右键单击,
ContextMenuRequested置为true,同时填充ContextMenuPos和节点信息。你通常紧接着调用HX::OpenPopupMenu弹出菜单。
if (tvp.ContextMenuRequested) {
HX::PopupMenuProfile menu;
menu.Items = {
HX::PopupMenuItem{HXStr("Rename")},
HX::PopupMenuItem{HXStr("Delete")},
};
HX::OpenPopupMenu(menu, tvp.ContextMenuPos);
}
自定义项渲染
如果你嫌弃默认样式不够骚,可以挂载 RenderItemCallback,拿到 HXBufferPainter 和行矩形,完全自己画这一行。
tvp.RenderItemCallback = [](HXBufferPainter* p, const HX::HXRect& rc, const HX::HXTreeNode& node) {
// 自己画图标 + 文字 + 高亮背景...
};
回调里绘制的坐标是相对于行矩形的,不要画出 rc 的范围,否则视觉上会重叠到别的行。
完整示例
下面是一个带文件夹结构的树,演示展开/折叠、多选、右键菜单和激活回调。
#include <include/hex.h>
#include <include/impl/EasyX/hex_impl_easyx.h>
#include <vector>
// ============================================
// 构造一棵示例树
// ============================================
static std::vector<HX::HXTreeNode> BuildDemoTree() {
std::vector<HX::HXTreeNode> roots;
HX::HXTreeNode src;
src.Label = HXStr("src");
src.Expanded = true;
{
HX::HXTreeNode cpp;
cpp.Label = HXStr("main.cpp");
src.Children.push_back(cpp);
HX::HXTreeNode h;
h.Label = HXStr("utils.h");
src.Children.push_back(h);
}
roots.push_back(src);
HX::HXTreeNode assets;
assets.Label = HXStr("assets");
assets.Expanded = false;
{
HX::HXTreeNode tex;
tex.Label = HXStr("textures");
tex.Expanded = true;
{
HX::HXTreeNode png;
png.Label = HXStr("hero.png");
tex.Children.push_back(png);
}
assets.Children.push_back(tex);
}
roots.push_back(assets);
return roots;
}
int main() {
initgraph(800, 600);
setbkcolor(WHITE);
cleardevice();
HX::HXInitForEasyX();
HX::SetBuffer(GetWorkingImage());
BeginBatchDraw();
// 树数据
static std::vector<HX::HXTreeNode> g_Nodes = BuildDemoTree();
// Profile 必须是 static / 全局,因为内部状态跨帧存活
static HX::TreeViewProfile tvp;
tvp.Size = {280, 500};
tvp.MultiSelect = true;
// 激活回调:打印到控制台
tvp.ItemActivatedCallback = [](const HX::HXTreeNode& node) {
wprintf(HXStr("Activated: %s\n"), node.Label.c_str());
};
ExMessage msg;
while (true) {
HX::HXBegin();
while (peekmessage(&msg)) {
HX::PushMessage(HX::GetHXMessage(&msg));
}
HX::Window(HXStr("TreeView Demo"), HX::WindowProfile());
HX::Text(HXStr("Project Explorer"));
HX::TreeView(HXStr("project_tree"), g_Nodes, tvp);
// 处理右键菜单
static HX::PopupMenuProfile menu;
if (tvp.ContextMenuRequested) {
menu.Items = {
HX::PopupMenuItem{HXStr("Expand")},
HX::PopupMenuItem{HXStr("Collapse")},
HX::PopupMenuItem{HXStr(""), true, true}, // 分隔线
HX::PopupMenuItem{HXStr("Delete"), true},
};
HX::OpenPopupMenu(menu, tvp.ContextMenuPos);
}
HXGInt clicked = HX::PopupMenu(menu);
if (clicked >= 0) {
// 根据 clicked 索引执行操作...
}
HX::End();
HX::Render();
FlushBatchDraw();
Sleep(16);
}
closegraph();
return 0;
}
g_Nodes 和 tvp 都声明为 static,这样它们的生命周期贯穿整个程序,控件才能记住上一帧的展开/折叠和选中状态。
常见问题
Q: 我想让树默认全部折叠,怎么办?
A: 构建 HXTreeNode 时把 Expanded 设为 false 即可。如果有深层节点,递归遍历一次全部置 false。
Q: 节点选中状态怎么持久化?
A: TreeView 已经把状态写回 node.Selected 了。你只需要保证同一棵树对象每帧都传进去(也就是别在局部重新构造),状态自然持久。
Q: 怎么给节点加图标?
A: 使用 RenderItemCallback,在文字左侧画一个小图块或者 DrawImage,标准 IMGUI 自定义渲染套路。
运行效果
截图占位符:请补充 $name 的运行效果截图。
截图占位符:请补充
树形控件 TreeView 运行效果的运行效果截图,保存为./assets/树形控件 TreeView_view.png。`n