跳转到内容
新建笔记

QAbstractTableModel:为文件元数据建立一致的行和表头

自定义表头只需要重写 headerData(),但一个可用的表格模型还必须让行数、行数据和索引规则一致。原例把目录和文件都放入 fileInfos,另一个 fileListData 却只加入成功读取的文件;遇到子目录或读取失败时,两组数据会错位,甚至在 at(index.row()) 处越界。

下面把一个已读取文件与它的四个字段放入同一个 Row。保留原来的四列表头和内容:上限频率、下限频率、线数、测量值。文件名放在提示文本中,完整路径通过 Qt::UserRole 提供;如果要显示文件名,可再显式增加一列,不能只修改注释。

  • rowCount() 与 columnCount() 对有效的父索引返回 0,因为这是平面表格,没有子行。
  • data() 检查索引是否属于本模型、行列是否在范围内、角色是否支持。
  • headerData() 区分横向字段名与纵向行号,并检查范围。
  • 替换整批数据时,先准备新数据,再用 beginResetModel() / endResetModel() 包住实际替换。

表格的根是无效的 QModelIndex()。原来的 getRootIndex() 返回首行首格,它不是文件目录树的根;给表格设置目录路径和给树模型设置根索引是不同操作。Qt:QAbstractTableModel

程序读取指定目录下的普通文件,不递归进入子目录。每行按第一个 ASCII 冒号 : 分隔,首尾空白会被清理;值中的后续冒号保留。重复的已知字段以后一次为准,缺少字段显示“未知”,未知字段和没有分隔符的行忽略。频率中的 Hz 暂时作为文本保留,不把它误当成可直接参与计算的数值。

示例最多处理 1000 个候选文件,每个文件最多 64 KiB,使用 UTF-8 解码。每个文件是一个完整输入,解码器使用 Stateless,使文件末尾不完整的多字节序列也报告错误。Qt:QStringConverter 标志Qt:QStringConverter 标志打不开、读取失败、超限或含无效 UTF-8 的文件跳过,并记录原因;行数只计算真正加入的数据行。无效目录或文件数量超限时保留上一批模型数据,并返回失败。

这是面向小目录的同步示例。大量文件解析应移到工作线程,完成后在模型所属线程应用结果;不能在后台线程直接调用连接到界面的模型更新方法。

完整程序与实际文件检查

跳转到“完整程序与实际文件检查”

普通启动传入目录路径;--check 会创建独立临时目录,写入正常文件、缺字段文件、超限文件、无效 UTF-8 文件和末尾截断的 UTF-8 文件,再使用 Qt 自带的模型检查器验证接口。测试文件随临时目录对象销毁而清理。

#include <QAbstractItemModelTester>
#include <QAbstractTableModel>
#include <QApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QModelIndex>
#include <QStringConverter>
#include <QStringList>
#include <QTableView>
#include <QTemporaryDir>
#include <QVariant>
#include <QVector>
#include <array>
#include <cassert>
#include <utility>
class TaskFileModel final : public QAbstractTableModel {
public:
explicit TaskFileModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {}
static constexpr qint64 maxFileBytes = 64 * 1024;
static constexpr qsizetype maxFiles = 1000;
QString rootPath() const { return root; }
QString lastError() const { return error; }
QStringList skippedFiles() const { return skipped; }
bool reload() { return setRootPath(root); }
bool setRootPath(const QString &path) {
error.clear();
QDir dir(path);
if (path.trimmed().isEmpty() || !dir.exists()) {
error = QStringLiteral("Directory does not exist");
return false;
}
const QFileInfoList entries = dir.entryInfoList(QDir::Files, QDir::Name);
if (entries.size() > maxFiles) {
error = QStringLiteral("Too many files for this synchronous example");
return false;
}
QVector<Row> next;
QStringList nextSkipped;
for (const QFileInfo &info : entries) {
QFile file(info.absoluteFilePath());
if (!file.open(QIODevice::ReadOnly)) {
nextSkipped.append(info.fileName() + ": " + file.errorString());
continue;
}
const QByteArray bytes = file.read(maxFileBytes + 1);
if (file.error() != QFileDevice::NoError || bytes.size() > maxFileBytes) {
nextSkipped.append(info.fileName() + ": read failed or file too large");
continue;
}
QStringDecoder decoder(QStringDecoder::Utf8, QStringConverter::Flag::Stateless);
const QString text = decoder(bytes);
if (decoder.hasError()) {
nextSkipped.append(info.fileName() + ": invalid UTF-8");
continue;
}
Row row;
row.fileName = info.fileName();
row.path = info.absoluteFilePath();
row.fields.fill(QStringLiteral("未知"));
for (const QString &line : text.split('\n')) {
const qsizetype separator = line.indexOf(':');
if (separator < 0) continue;
const QString key = line.left(separator).trimmed();
const QString value = line.mid(separator + 1).trimmed();
for (int column = 0; column < 4; ++column)
if (key == fieldName(column)) row.fields[column] = value;
}
next.append(std::move(row));
}
beginResetModel();
root = dir.absolutePath();
rows = std::move(next);
skipped = std::move(nextSkipped);
endResetModel();
return true;
}
int rowCount(const QModelIndex &parent = {}) const override {
return parent.isValid() ? 0 : static_cast<int>(rows.size());
}
int columnCount(const QModelIndex &parent = {}) const override {
return parent.isValid() ? 0 : 4;
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid() || index.model() != this || index.row() < 0
|| index.row() >= rowCount() || index.column() < 0 || index.column() >= 4)
return {};
const Row &row = rows.at(index.row());
if (role == Qt::DisplayRole) return row.fields[index.column()];
if (role == Qt::ToolTipRole) return row.fileName;
if (role == Qt::UserRole) return row.path;
return {};
}
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override {
if (role != Qt::DisplayRole || section < 0) return {};
if (orientation == Qt::Horizontal)
return section < 4 ? QVariant(fieldName(section)) : QVariant{};
return section < rowCount() ? QVariant(section + 1) : QVariant{};
}
private:
struct Row {
QString fileName;
QString path;
std::array<QString, 4> fields;
};
static QString fieldName(int column) {
static const std::array<QString, 4> names{
QStringLiteral("上限频率"), QStringLiteral("下限频率"),
QStringLiteral("线数"), QStringLiteral("测量值")};
return names.at(static_cast<std::size_t>(column));
}
QString root;
QString error;
QStringList skipped;
QVector<Row> rows;
};
static bool writeFixture(const QString &path, const QByteArray &bytes) {
QFile file(path);
return file.open(QIODevice::WriteOnly) && file.write(bytes) == bytes.size();
}
static void checkModel() {
QTemporaryDir temporary;
assert(temporary.isValid());
const QDir dir(temporary.path());
assert(dir.mkdir("subdirectory"));
const QByteArray normal = QStringLiteral(
"上限频率: 800Hz\n下限频率: 20Hz\n线数: 10\n测量值: 123.45\n").toUtf8();
assert(writeFixture(dir.filePath("a.txt"), normal));
assert(writeFixture(dir.filePath("b.txt"), QStringLiteral(
"上限频率: 400Hz\n上限频率: 500Hz\r\n测量值: 1:2\n").toUtf8()));
assert(writeFixture(dir.filePath("oversize.txt"),
QByteArray(TaskFileModel::maxFileBytes + 1, 'x')));
assert(writeFixture(dir.filePath("invalid.txt"), QByteArray(1, char(0xff))));
assert(writeFixture(dir.filePath("truncated.txt"),
normal + QByteArray::fromHex("e4b8")));
TaskFileModel model;
QAbstractItemModelTester tester(&model,
QAbstractItemModelTester::FailureReportingMode::Fatal);
assert(model.rowCount() == 0 && model.columnCount() == 4);
assert(model.setRootPath(temporary.path()));
assert(model.rowCount() == 2 && model.skippedFiles().size() == 3);
assert(model.data(model.index(0, 0)).toString() == QStringLiteral("800Hz"));
assert(model.data(model.index(0, 1)).toString() == QStringLiteral("20Hz"));
assert(model.data(model.index(0, 2)).toString() == QStringLiteral("10"));
assert(model.data(model.index(0, 3)).toString() == QStringLiteral("123.45"));
assert(model.data(model.index(1, 0)).toString() == QStringLiteral("500Hz"));
assert(model.data(model.index(1, 2)).toString() == QStringLiteral("未知"));
assert(model.data(model.index(1, 3)).toString() == QStringLiteral("1:2"));
assert(model.data(model.index(0, 0), Qt::ToolTipRole).toString() == "a.txt");
assert(model.data(model.index(0, 0), Qt::UserRole).toString() == dir.filePath("a.txt"));
assert(model.headerData(0, Qt::Horizontal).toString() == QStringLiteral("上限频率"));
assert(model.headerData(1, Qt::Vertical).toInt() == 2);
assert(!model.headerData(4, Qt::Horizontal).isValid());
assert(!model.data({}).isValid() && !model.index(2, 0).isValid());
assert(!model.data(model.index(0, 0), Qt::DecorationRole).isValid());
assert(model.rowCount(model.index(0, 0)) == 0);
assert(model.columnCount(model.index(0, 0)) == 0);
TaskFileModel another;
assert(another.setRootPath(temporary.path()));
assert(!model.data(another.index(0, 0)).isValid());
const QString rootBeforeFailure = model.rootPath();
assert(!model.setRootPath(dir.filePath("missing")));
assert(!model.lastError().isEmpty() && model.rootPath() == rootBeforeFailure);
assert(model.rowCount() == 2);
assert(QFile::remove(dir.filePath("a.txt")));
assert(model.reload() && model.rowCount() == 1);
assert(model.data(model.index(0, 0), Qt::ToolTipRole).toString() == "b.txt");
assert(model.setRootPath(dir.filePath("subdirectory")) && model.rowCount() == 0);
}
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
if (app.arguments().contains("--check")) {
checkModel();
return 0;
}
if (app.arguments().size() != 2) return 1; // 传入一个目录路径
QTableView view;
auto *model = new TaskFileModel(&view);
if (!model->setRootPath(app.arguments().at(1))) return 2;
view.setModel(model);
view.resize(640, 360);
view.show();
return app.exec();
}
cmake_minimum_required(VERSION 3.20)
project(FileTableModel LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets Test)
add_executable(file_table main.cpp)
target_link_libraries(file_table PRIVATE Qt6::Widgets Qt6::Test)

Debug 构建保留检查中的断言。Qt Test 模块用于 QAbstractItemModelTester;它可以发现多种模型契约错误,但不会替应用决定字段语义、解析规则或文件错误策略。Qt:模型检查器

原示例目录 D:/shawei/temp/jpbalance/temp/TaskTemplates 只是历史工程路径;使用时传入自己的模板目录。模型对象以视图为父对象,窗口销毁时一并清理。重载目录采用模型重置,之前保存的索引和选择可能失效;需要跨次定位某个文件时,保留稳定的文件标识并在新模型中重新查找,不长期保存旧的普通 QModelIndex。