跳到主要内容

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> 传进去就行。控件会在交互时自动修改 ExpandedSelected 字段。

参数详解

HXTreeNode 节点结构

成员类型默认值说明
LabelHXString显示文本
Expandedbooltrue是否展开子节点
Selectedboolfalse是否被选中
Childrenstd::vector<HXTreeNode>子节点列表

TreeViewProfile 配置

成员类型默认值说明
SizeHXPoint{200, 300}控件宽高
RowHeightint24每行高度(像素)
Indentint16每级缩进(像素)
MultiSelectboolfalse是否允许多选
Activatedboolfalse(输出)本帧是否有节点被激活(回车或双击)
ActivatedLabelHXString(输出)被激活节点的文本
ActivatedPathHXString(输出)被激活节点的路径标识
ContextMenuRequestedboolfalse(输出)本帧是否请求了右键菜单
ContextMenuLabelHXString(输出)右键点击的节点文本
ContextMenuPathHXString(输出)右键点击的节点路径
ContextMenuPosHXPoint{0,0}(输出)右键菜单应弹出的位置
RenderItemCallbackstd::function自定义单项渲染回调
ItemActivatedCallbackstd::function节点被激活时的回调
ScrollbarWidthHXGInt8滚动条宽度
提示

所谓路径ActivatedPath / ContextMenuPath),是控件内部用 / 拼接的层级索引,例如 0/2/1。你可以用它精确定位到树中的某一个节点,而不用自己递归遍历。

返回值

TreeView 本身无返回值。所有交互结果都通过修改 profilenodes 的原地状态来传达。

核心特性

层级展开与折叠

每行左侧有个小三角/箭头,点击即可展开或折叠子树。初始状态由 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_Nodestvp 都声明为 static,这样它们的生命周期贯穿整个程序,控件才能记住上一帧的展开/折叠和选中状态。

常见问题

Q: 我想让树默认全部折叠,怎么办?
A: 构建 HXTreeNode 时把 Expanded 设为 false 即可。如果有深层节点,递归遍历一次全部置 false

Q: 节点选中状态怎么持久化?
A: TreeView 已经把状态写回 node.Selected 了。你只需要保证同一棵树对象每帧都传进去(也就是别在局部重新构造),状态自然持久。

Q: 怎么给节点加图标?
A: 使用 RenderItemCallback,在文字左侧画一个小图块或者 DrawImage,标准 IMGUI 自定义渲染套路。

运行效果

截图占位符:请补充 $name 的运行效果截图。

截图占位符:请补充 树形控件 TreeView 运行效果 的运行效果截图,保存为 ./assets/树形控件 TreeView_view.png。`n