Table 表格控件
列表千千万,Table 最耐看。如果你要展示行列数据——比如日志、配置表、数据库查询结果——Table 提供了列头、网格线、虚拟滚动、列宽拖拽、行选择、排序回调,开箱即用。
函数原型
namespace HX {
struct TableColumn {
HXString Header;
int Width = 100;
};
struct TableProfile {
HXPoint Size = {400, 300};
int RowHeight = 24;
std::vector<TableColumn> Columns;
bool Resizable = true;
bool MultiSelect = false;
int MinColumnWidth = 32;
int ActivatedRow = -1;
std::function<int(const HXString& a, const HXString& b, int columnIdx)> SortCompareCallback;
std::function<void(HXBufferPainter* p, const HXRect& cellRect, int row, int col)> RenderCellCallback;
std::function<void(int rowIdx)> RowActivatedCallback;
};
void TableBegin(const HXString &id, TableProfile &profile);
bool TableRow(int rowIndex);
void TableCell(const HXString &text);
void TableEnd();
}
Table 采用开区间 API:TableBegin 打头,TableEnd 收尾,中间用 TableRow 循环每一行,TableCell 填充每个单元格。这套模式和我们熟悉的 IMGUI 表格完全一致。
参数详解
TableColumn 列描述
| 成员 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Header | HXString | 空 | 列头显示的文本 |
Width | int | 100 | 列宽(像素) |
TableProfile 配置
| 成员 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Size | HXPoint | {400, 300} | 表格整体宽高 |
RowHeight | int | 24 | 每行高度 |
Columns | std::vector<TableColumn> | 空 | 列定义数组 |
Resizable | bool | true | 是否允许拖拽调整列宽 |
MultiSelect | bool | false | 是否允许多选行 |
MinColumnWidth | int | 32 | 列宽下限 |
ActivatedRow | int | -1 | (输出)本帧被激活的行索引 |
SortCompareCallback | std::function | 空 | 自定义排序比较函数 |
RenderCellCallback | std::function | 空 | 自定义单元格渲染 |
RowActivatedCallback | std::function | 空 | 行被激活时的回调 |
ActivatedRow 是输出型字段。当用户双击某一行、或选中后按回车,这个值会被设置为对应的 rowIndex。下一帧开始时它不会自动复位,如果你需要帧级事件,建议配合 RowActivatedCallback 使用。
返回值
| 函数 | 返回值 | 含义 |
|---|---|---|
TableBegin | void | 无 |
TableRow(int rowIndex) | bool | 仅当该行在可视区域时返回 true。这是虚拟滚动的核心:你仍然要在 for 循环里调用它,但只渲染返回 true 的行 |
TableCell | void | 无 |
TableEnd | void | 无 |
核心特性
列头与网格线
TableBegin 会自动渲染一行表头,背景色取自当前主题。数据区则有细网格线分割,清晰利落。
static HX::TableProfile tp;
tp.Columns = {
{HXStr("Name"), 140},
{HXStr("Type"), 100},
{HXStr("Size"), 80},
};
虚拟滚动
HX::TableBegin(HXStr("my_table"), tp);
for (int i = 0; i < (int)g_Data.size(); ++i) {
if (HX::TableRow(i)) { // 只有可见行返回 true
HX::TableCell(g_Data[i].name);
HX::TableCell(g_Data[i].type);
HX::TableCell(g_Data[i].size);
}
}
HX::TableEnd();
就算你有十万行,TableRow 也只会让屏幕内的那二三十行真正执行绘制。这就是虚拟滚动的威力——CPU 和 GPU 都表示感谢。
列宽拖拽调整
开启 Resizable = true 后,把鼠标放到列头右侧边界,光标会变成拖拽样式,按住左键左右拖动即可调整列宽。松开自动保存新宽度到 tp.Columns[i].Width。
tp.Resizable = true;
tp.MinColumnWidth = 48; // 别让列窄到看不见字
拖拽过程中列宽是实时更新的,但不要把 TableProfile 设成局部变量——否则每帧初始化回默认值,用户刚拖好的宽度下一秒就复原了。务必 static 或全局!
行选择持久化
行选中状态由控件内部通过 HXStatePool 持久化,也就是说不需要你自己维护一个 selected 数组。只要 id 不变,选中的行就会一直高亮。
tp.MultiSelect = true; // 按住 Ctrl 多选,Shift 连选
排序回调
如果你点击了列头,SortCompareCallback 会被调用,你返回比较结果即可。
tp.SortCompareCallback = [](const HXString& a, const HXString& b, int col) -> int {
if (col == 2) {
// 第三列按数字排
int ia = _wtoi(a.c_str());
int ib = _wtoi(b.c_str());
return (ia < ib) ? -1 : (ia > ib) ? 1 : 0;
}
return wcscmp(a.c_str(), b.c_str());
};
排序回调只负责比较逻辑,不负责对数据源排序。你可以在回调里设置一个标记,帧末对 g_Data 执行 std::sort。
完整示例
下面是一个带 1000 行假数据的表格,演示虚拟滚动、多选、列宽调整和行激活。
#include <include/hex.h>
#include <include/impl/EasyX/hex_impl_easyx.h>
#include <vector>
#include <string>
struct FileInfo {
HXString name;
HXString type;
HXString size;
};
static std::vector<FileInfo> BuildData() {
std::vector<FileInfo> v;
for (int i = 0; i < 1000; ++i) {
FileInfo f;
f.name = HXStr("file_") + std::to_wstring(i) + HXStr(".cpp");
f.type = (i % 3 == 0) ? HXStr("Source") : HXStr("Header");
f.size = std::to_wstring((i + 1) * 1024) + HXStr(" B");
v.push_back(f);
}
return v;
}
int main() {
initgraph(900, 600);
setbkcolor(WHITE);
cleardevice();
HX::HXInitForEasyX();
HX::SetBuffer(GetWorkingImage());
BeginBatchDraw();
static std::vector<FileInfo> g_Data = BuildData();
static HX::TableProfile tp;
tp.Size = {700, 500};
tp.Columns = {
{HXStr("File Name"), 220},
{HXStr("Type"), 120},
{HXStr("Size"), 100},
};
tp.Resizable = true;
tp.MultiSelect = true;
tp.RowActivatedCallback = [](int row) {
wprintf(HXStr("Activated row: %d\n"), row);
};
ExMessage msg;
while (true) {
HX::HXBegin();
while (peekmessage(&msg)) {
HX::PushMessage(HX::GetHXMessage(&msg));
}
HX::Window(HXStr("Table Demo"), HX::WindowProfile());
HX::Text(HXStr("File List (1000 rows, virtual scrolling)"));
HX::TableBegin(HXStr("file_table"), tp);
for (int i = 0; i < (int)g_Data.size(); ++i) {
if (HX::TableRow(i)) {
HX::TableCell(g_Data[i].name);
HX::TableCell(g_Data[i].type);
HX::TableCell(g_Data[i].size);
}
}
HX::TableEnd();
if (tp.ActivatedRow >= 0) {
HX::Text(HXStr("Last activated: ") + std::to_wstring(tp.ActivatedRow));
}
HX::End();
HX::Render();
FlushBatchDraw();
Sleep(16);
}
closegraph();
return 0;
}
注意 tp.ActivatedRow 不会自动变回 -1。如果你只想在激活那帧响应一次,可以在读取后手动置 -1,或者在 RowActivatedCallback 里处理一次性逻辑。
常见问题
Q: 为什么 TableRow 要对所有行调用,而不是只遍历可见的?
A: 因为控件内部需要知道总行数才能正确计算滚动条位置和高度。遍历成本本身很低(就是一个 if 判断),真正昂贵的绘制操作已经被跳过了。
Q: 可以给单元格加按钮或自定义控件吗?
A: 在 TableRow 返回 true 的分支里,你完全可以继续调用 HX::Button、HX::Checkbox 等。不过要注意列宽限制,别画出去了。更精细的控制请使用 RenderCellCallback。
Q: 列宽调整后怎么保存?
A: 调整后新宽度已经实时写进 tp.Columns[i].Width 了。只要 tp 是 static 或全局,下次打开程序它还在。如果要持久化到硬盘,自己写个 INI/JSON 读写即可。
运行效果
截图占位符:请补充 $name 的运行效果截图。
截图占位符:请补充
表格 Table 运行效果的运行效果截图,保存为./assets/表格 Table_view.png。`n