跳到主要内容

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 采用开区间 APITableBegin 打头,TableEnd 收尾,中间用 TableRow 循环每一行,TableCell 填充每个单元格。这套模式和我们熟悉的 IMGUI 表格完全一致。

参数详解

TableColumn 列描述

成员类型默认值说明
HeaderHXString列头显示的文本
Widthint100列宽(像素)

TableProfile 配置

成员类型默认值说明
SizeHXPoint{400, 300}表格整体宽高
RowHeightint24每行高度
Columnsstd::vector<TableColumn>列定义数组
Resizablebooltrue是否允许拖拽调整列宽
MultiSelectboolfalse是否允许多选行
MinColumnWidthint32列宽下限
ActivatedRowint-1(输出)本帧被激活的行索引
SortCompareCallbackstd::function自定义排序比较函数
RenderCellCallbackstd::function自定义单元格渲染
RowActivatedCallbackstd::function行被激活时的回调
提示

ActivatedRow输出型字段。当用户双击某一行、或选中后按回车,这个值会被设置为对应的 rowIndex。下一帧开始时它不会自动复位,如果你需要帧级事件,建议配合 RowActivatedCallback 使用。

返回值

函数返回值含义
TableBeginvoid
TableRow(int rowIndex)bool仅当该行在可视区域时返回 true。这是虚拟滚动的核心:你仍然要在 for 循环里调用它,但只渲染返回 true 的行
TableCellvoid
TableEndvoid

核心特性

列头与网格线

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::ButtonHX::Checkbox 等。不过要注意列宽限制,别画出去了。更精细的控制请使用 RenderCellCallback

Q: 列宽调整后怎么保存?
A: 调整后新宽度已经实时写进 tp.Columns[i].Width 了。只要 tpstatic 或全局,下次打开程序它还在。如果要持久化到硬盘,自己写个 INI/JSON 读写即可。

运行效果

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

截图占位符:请补充 表格 Table 运行效果 的运行效果截图,保存为 ./assets/表格 Table_view.png。`n