简介:面向Qt桌面应用开发者,这份封装与Demo解决在表格控件中读取并展示Excel数据的常见痛点。RAR压缩包共八个文件,核心逻辑由三个cpp源文件和两个头文件承载,同时附带界面定义、工程配置与用户配置,整体仅八KB,结构轻巧清晰,正式工程或学习Demo均可直接使用。目前已有近三千人学习或下载,作者说明在Qt5环境下测试可用,按照工程默认配置即可顺利运行,无需额外复杂依赖。封装内包含独立的Excel读取引擎与显示界面,演示了从打开文件、解析工作表到刷新表格控件的完整流程,可直接引入Qt工程,并保留清晰的模块边界,便于二次开发。对需要实现Excel导入功能的中初级开发者很有参考价值,还可以依此扩展多表切换、格式校验等功能,适合数据导入工具或报表管理类项目。
1. 为什么Qt的Excel读取到QTableWidget需要封装:从一次凌晨的交付事故说起
凌晨两点被测试喊醒,说导入向导一跑就崩。过去一看,是同事在按钮槽里直接 new 了一个 Excel 对象,界面转圈,接着 COM 组件报错,整个程序退出。用 Qt 把 Excel 读取并显示到 QTableWidget 这件事,零散地写在槽函数里并不难,难的是把它做成封装:换掉一个从网上抄来的片段,接上自己的业务代码,还要经得起目标机器没有 Office、数据有两万行、中文路径满天飞这些现实问题。这篇笔记把我近期一直在用的封装方案拆开讲,从选型到实现,从参数配置到坑位排查,目标是让照着做的人能交差,让已经在做的人少绕几个弯。
2. Excel读取方案选型:为什么我放弃QAxObject而主推QXlsx
2.1 三条常见路线:QAxObject、QXlsx、CSV中间层
拿到这个需求,我先问自己三个问题:目标机器能不能装 Excel?要读的是 xlsx 还是老 xls?数据量大致多少?这三个问题能直接筛掉一半方案。常见的读取路线有三条:Windows 下的 QAxObject、跨平台的 QXlsx、以及最朴素的 CSV 中间层。
QAxObject 是 Qt 自带 ActiveX 容器在 Windows 上调用 Excel COM 接口的方案。写起来很短,本质是让 Excel 进程帮你打开文件,再逐个单元格取值。优点是能顺手调用 Excel 的公式重算、宏、图表导出能力;缺点是目标机器必须装 Office,而且每次打开都要启动一个 Excel 进程,首次读取要等好几秒。它适合需要 Excel 计算能力的场景,而不是单纯的“把格子搬到界面上”。
QXlsx 是直接把 xlsx 当作 zip 包解析、读取内部 XML 的 C++ 库。把它编进 Qt 工程后,读取过程不依赖外部程序,Windows 和 Linux 上都能跑。它的缺点是 API 覆盖不到 Excel 的完整功能,比如复杂图表、图片资源读取都不太全,但对“读取单元格文本”来说已经足够。它只支持 xlsx,遇到老 xls 要另想办法。
CSV 中间层则是让上游先把 Excel 导出成 CSV,你用 QTextStream 自己按行解析。它最轻,性能也最好,但会丢掉多 sheet、合并单元格、公式结果类型这些信息,还要处理逗号转义和编码。当数据源已经能给出 CSV 时,这确实是一种可靠的兜底方案。
| 路线 | 依赖 | 支持格式 | 开表速度 | 部署依赖 | 适合场景 |
|---|---|---|---|---|---|
| QAxObject | Windows + 本机 Excel | xls/xlsx/csv | 慢 | 每台机器装 Office | 需要 Excel 计算能力 |
| QXlsx | Qt + 库源码 | xlsx | 快 | 无 | 数据导入、表格预览 |
| CSV 中间层 | 无 | csv | 最快 | 无 | 数据交换、接口对接 |
2.2 为什么我最终选了QXlsx:跨平台、无Office依赖、线程安全
如果项目场景固定在 Windows 内网,并且 Office 一定齐全,那 QAxObject 确实省事。但这几年我做过的桌面工具,交付环境经常是某个老工控机,系统是裁剪过的嵌入式 Windows,别说 Office,连 VC 运行库都要打包进去。在这种环境里让用户去装一套完整 Office,只为了把几十行数据读进 QTableWidget,交付成本完全不可接受。
另一个更关键的选型理由是线程安全。QAxObject 创建的 COM 对象必须在创建它的线程里使用,想放到后台线程预读 Excel,就得在那个线程里做 CoInitialize 和 CoUninitialize,生命周期管理稍乱就会偶发崩溃。QXlsx 只是文件解析,不碰 COM、不起进程,放到 QtConcurrent 后台线程里非常稳。这一点对封装来说几乎是决定性的,因为数据量一上来,界面卡死的问题必然要解决。
如果你的业务里确实要读老 xls,常见做法是两条路:一是先让服务器或工具把 xls 批量转成 xlsx,二是保留一个 QAxObject 分支只处理 xls,其他格式全走 QXlsx。我一般倾向后者,因为转换环节要引入外部命令调度,交付更重,破绽也更多。老文件不多时,甚至可以直接提示用户“请另存为 xlsx”。
2.3 统一数据流设计:从工作簿到表格模型,封装边界画在哪
封装之前先想清楚数据流:Excel 文件 -> 二维数据 -> QTableWidget。这里最要紧的是把“读文件”和“填表格”两个动作分开。我喜欢把二维数据统一存成QVector<QVector<QString>>,所有单元格先转成显示字符串,再做 UI 绑定。这样做是因为 Excel 单元格类型太杂:日期是序列号,数字可能带科学计数法,百分比单元格读出来可能是 0.15,直接用原始 QVariant 显示会遇到一堆格式问题。
统一成 QString 虽然丢弃了类型信息,但 QTableWidget 本来就是个“显示字符串”的控件,显示层需要类型时,可以在按列处理时单独转换。这是一次刻意的取舍,换来的是 Reader 层接口极简、跨库复用方便。数据从 Excel 换成数据库或接口时,只需要替换 Reader 层,TableBinder 完全不用动。
封装边界我会画在接口上,不把任何三方库类型泄漏给调用方。外面能看到的就是一个ExcelReader类,加上一个显示选项结构体:
// excelreader.h 预览 #include <QString> #include <QVector> struct ExcelDisplayOptions { bool useFirstRowAsHeader = true; int startRow = 1; int startColumn = 0; QString nullPlaceholder = ""; bool autoResizeColumn = true; }; class ExcelReader { public: bool openFile(const QString &filePath, QString *outError); int rowCount() const; int columnCount() const; QString data(int row, int col) const; bool fillTable(QTableWidget *table, const ExcelDisplayOptions &options, QString *outError); private: QVector<QVector<QString>> m_rows; };这个接口刻意不暴露 QXlsx 的任何类型,调用方完全不知道背后用的是什么库。以后想换读 xls 的实现,或者改成读 CSV,业务代码都不需要动。这才是封装真正的价值:把最容易变的部分关在门里。
3. 封装实现:一个ExcelReader类把读取和绑定tablewidget收干净
3.1 头文件设计与核心成员
完整的头文件要比预览多一个 QObject 继承关系。把ExcelReader定义为QObject子类,是为了将来需要发进度信号、接线程信号时不用再改头文件。实际上我的项目里这个类并不直接做异步,异步的工作交给调用方的QFutureWatcher,ExcelReader只保留“同步读取 + 填充表格”的纯函数职责。
// excelreader.h #ifndef EXCELREADER_H #define EXCELREADER_H #include <QObject> #include <QString> #include <QVector> class QTableWidget; struct ExcelDisplayOptions { bool useFirstRowAsHeader = true; // 第一行作为表头 int startRow = 1; // 数据起始行,从1开始 int startColumn = 0; // 起始列,从0开始 QString nullPlaceholder = ""; // 空单元格占位符 bool autoResizeColumn = true; // 填充后自适应列宽 }; class ExcelReader : public QObject { Q_OBJECT public: explicit ExcelReader(QObject *parent = nullptr); bool openFile(const QString &filePath, QString *outError); int rowCount() const; int columnCount() const; QString data(int row, int col) const; bool fillTable(QTableWidget *table, const ExcelDisplayOptions &options, QString *outError); private: QVector<QVector<QString>> m_rows; }; #endif // EXCELREADER_H逻辑说明:m_rows是一个行优先的二维容器,下标 0 对应 Excel 第 1 行。结构体里的startRow和useFirstRowAsHeader有优先级关系,我会在实现里约定:第一行作为表头时,startRow被忽略。startColumn保持从 0 开始,和 QTableWidget 的列号对齐,免得调用方来回转换时晕。
有人会问为什么不用QStandardItemModel直接做中间层。QStandardItemModel 确实能直接喂给 QTableView,但我们的目标是 QTableWidget,它有自己的 QTableWidgetItem 体系,硬塞 Model 反而要再做一层映射。二维 QString 容器最朴素,打印调试也最直观。
3.2 读取xlsx并填充QTableWidget的最小代码
读取部分使用 QXlsx,假定你已经在 pro 文件里加入了 QXlsx 源码路径。这里给出一个经过裁剪的核心实现,注释写在关键位置上:
// excelreader.cpp 读取核心 #include "excelreader.h" #include <QTableWidget> #include <QTableWidgetItem> #include <QDebug> #include <xlsxdocument.h> #include <xlsxcell.h> bool ExcelReader::openFile(const QString &filePath, QString *outError) { QXlsx::Document doc(filePath); if (!doc.load()) { if (outError) *outError = "xlsx文件无法解析,请确认不是老版xls"; return false; } QXlsx::Worksheet *sheet = doc.currentWorksheet(); if (!sheet) { if (outError) *outError = "工作簿里没有可用的工作表"; return false; } int rowCount = sheet->dimension().rowCount(); int colCount = sheet->dimension().columnCount(); if (rowCount <= 0 || colCount <= 0) { if (outError) *outError = "工作表内容是空的"; return false; } m_rows.clear(); m_rows.reserve(rowCount); for (int row = 1; row <= rowCount; ++row) { QVector<QString> line; line.reserve(colCount); for (int col = 1; col <= colCount; ++col) { QXlsx::Cell *cell = sheet->cellAt(row, col); if (cell) { line.push_back(cell->value().toString()); } else { line.push_back(QString()); } } m_rows.push_back(line); } qDebug() << "ExcelReader rowCount" << rowCount << "colCount" << colCount; return true; }逻辑说明:doc.load()在部分 QXlsx 版本里叫isLoadPackage(),如果编译不过可以去源码里看公共接口名。读取循环里cellAt(row, col)的索引从 1 开始,返回nullptr表示这个单元格为空,我用空字符串占位,确保每一行的line长度都等于源表列数,这是后面填表格不错位的根基。
这里把日期、数字全部toString()成字符串,等于把格式问题统一推迟到显示层处理。代价是如果你要按数值排序、求和,后面需要单独对列做转换,但大多数“把 Excel 读进来显示”的需求,字符串已经够用了。
填充 QTableWidget 的部分也在同一个类里,但只依赖m_rows,不碰任何 QXlsx 类型:
// excelreader.cpp 填充表格 bool ExcelReader::fillTable(QTableWidget *table, const ExcelDisplayOptions &options, QString *outError) { if (!table) { if (outError) *outError = "tableWidget 为空"; return false; } int startRow = options.startRow; if (options.useFirstRowAsHeader) startRow = 1; int startCol = options.startColumn; int maxCol = 0; for (int r = startRow; r < m_rows.size(); ++r) maxCol = qMax(maxCol, m_rows.at(r).size()); table->clear(); table->setColumnCount(qMax(0, maxCol - startCol)); table->setRowCount(qMax(0, m_rows.size() - startRow)); if (options.useFirstRowAsHeader) { QStringList labels; const QVector<QString> &header = m_rows.at(0); for (int c = startCol; c < header.size(); ++c) labels << header.at(c); table->setHorizontalHeaderLabels(labels); } table->setUpdatesEnabled(false); for (int r = startRow; r < m_rows.size(); ++r) { int rowItem = r - startRow; const QVector<QString> &line = m_rows.at(r); for (int c = startCol; c < line.size(); ++c) { QString text = line.at(c); if (text.isEmpty()) text = options.nullPlaceholder; table->setItem(rowItem, c - startCol, new QTableWidgetItem(text)); } } table->setUpdatesEnabled(true); if (options.autoResizeColumn) table->resizeColumnsToContents(); return true; }逻辑说明:clear()会把原表数据、表头全部清掉,调用前如果担心用户已编辑的内容丢失,需要在上层自行确认。setUpdatesEnabled(false)在批量填充时非常有效,可以避免每 setItem 一次就重绘一次界面,哪怕只有几千行,这个开关也能让肉眼可见的卡顿消失很大一部分。列数取所有数据行里的最大值,再减去startCol,避免某些行前面有空列导致表格缺列。
3.3 躲开界面卡死:后台线程读、主线程填表
openFile是纯文件解析,适合放到后台线程;fillTable要操作 QTableWidget,必须在主线程。我一般用QtConcurrent::run加QFutureWatcher,代码量最少,也足够稳。下面是调用方的一种典型写法:
// 调用方代码,典型异步读取 #include <QtConcurrent/QtConcurrent> #include <QFutureWatcher> void MainWindow::startReadExcel(const QString &filePath) { ExcelReader *reader = new ExcelReader(this); QFutureWatcher<QString> *watcher = new QFutureWatcher<QString>(this); connect(watcher, &QFutureWatcher<QString>::finished, this, [this, watcher, reader]() { QString err = watcher->result(); if (err.isEmpty()) { ExcelDisplayOptions opt; opt.useFirstRowAsHeader = true; reader->fillTable(ui->tableWidget, opt, &err); } if (!err.isEmpty()) statusBar()->showMessage(err, 5000); watcher->deleteLater(); reader->deleteLater(); }); watcher->setFuture(QtConcurrent::run([reader, filePath]() { QString err; reader->openFile(filePath, &err); return err; })); }逻辑说明:reader在 lambda 里被捕获,任务完成后在 finished 槽里用deleteLater回收。这里有个容易忽略的点:filePath是值拷贝进 lambda 的,不会出现主线程改了目录而子线程读到一半路径变化的问题。openFile写进m_rows,fillTable读m_rows,两段代码都跑在同一个reader上,一个在子线程写,一个在主线程读,严格说存在一个数据竞争点——但任务完成后才触发 finished,Qt 的信号槽 happens-before 关系保证了这里安全。如果你不放心,可以把 reader 也作为 lambda 局部对象,等任务结束再传回结果,但那样代码会长很多。
3.4 调用方式与信号槽设计
ExcelReader不主动弹文件对话框,也不持有任何窗口指针,这样它能在不同的窗口、不同的线程模式下复用。文件选择是上层业务,我通常会这样接:
// mainwindow.cpp 文件选择与触发 void MainWindow::on_openExcel_clicked() { QString file = QFileDialog::getOpenFileName( this, "选择 Excel", QString(), "Excel 文件 (*.xlsx);;所有文件 (*.*)"); if (file.isEmpty()) return; startReadExcel(file); }说明:把文件选择放在按钮槽里,把读取放进上面定义好的startReadExcel,中间不掺杂逻辑。如果项目里需要打开最近文件、拖放文件,只需要再生成一个 filePath 调同一个入口就行。这也算封装的一部分:入口统一,后续维护的人只需要维护一个读取链路。
4. 用Demo工程打通读取链路:最小配置与五个必调参数
4.1 最小工程文件:pro、主窗口和按钮
一个可跑的最小 demo,我需要四个文件:main.cpp、mainwindow.h/cpp、excelreader.h/cpp,以及一个 pro 文件。pro 文件里除了常规 Qt 模块,还需要打开 concurrent 和 QXlsx 的路径:
# ExcelReadDemo.pro QT += core gui widgets concurrent TARGET = ExcelReadDemo TEMPLATE = app SOURCES += main.cpp \ mainwindow.cpp \ excelreader.cpp HEADERS += mainwindow.h \ excelreader.h # QXlsx 源码路径,按实际放的位置调整 INCLUDEPATH += $$PWD/3rdparty/qxlsx/src说明:QT += concurrent是为了用QtConcurrent::run;如果不用异步读取,可以不加,但我建议 demo 从一开始就带异步,免得后面单独补线程时又要改代码结构。QXlsx 的引入方式一般有两种:把源码放进3rdparty/qxlsx/src然后用 INCLUDEPATH,或者直接用它的 pri 文件。pri 文件会连带编译源码,比较省事。
主窗口里只放一个按钮和一个 QTableWidget,选择文件后填充。构造函数里给表格设两个常用属性,避免用户误编辑被写进内存里的假数据:
// mainwindow.cpp 构造函数 ui->tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); ui->tableWidget->setAlternatingRowColors(true);NoEditTriggers让表格只读,AlternatingRowColors让隔行变色,读起来更省眼。这两个属性不是必须,但演示导入场景时很合适。
4.2 五个必调参数:编码、表头、空值、合并单元格、列宽
我总结了在真实项目里经常要调的五个参数,它们都来自同一种需求:Excel 源文件长什么样,用户希望表格长什么样。
| 参数 | 推荐值 | 作用 | 常见坑 |
|---|---|---|---|
useFirstRowAsHeader | true | 第一行作为表头 | 第一行是标题合并单元格时会错位 |
startColumn | 0 | 从第几列开始填充 | 源表前两列是序号和说明,UI 不想显示 |
nullPlaceholder | "" 或 "--" | 空单元格显示什么 | 空字符串会导致 item 外形上有空白,看不清行列 |
autoResizeColumn | true | 填充后自适应列宽 | 大数据量时 resize 很慢,建议按需关闭 |
| 文件编码 | UTF-8/GBK | 影响中文读取 | xlsx 内部是 UTF-8,CSV 才会踩 GBK 的坑 |
useFirstRowAsHeader和startRow的优先级问题,我在实现里让表头优先。如果第一行和第二行之间有标题行,直接把useFirstRowAsHeader关掉,把startRow改成 2,数据开始位置就跳过了标题。startColumn主要应对源表左侧有业务字段、但表格预览不需要显示的情况。
nullPlaceholder是很多人忽略的参数。Excel 里的空单元格填进 QTableWidget 后,item 存在但文本为空,用户在屏幕上看到一整块空白,视觉上会怀疑是不是读丢了数据。设置成 "--" 后,空缺一眼可见,也方便后续写断言校验。
4.3 验证读取结果:从打印日志到边界数据检查
demo 跑通之后,别急着接业务,先用三种方式验证读取结果是不是对的。第一种是看日志:在openFile里打印rowCount和colCount,再打印首行前五个单元格和最后一行后五个单元格,和 Excel 源文件肉眼对比。第二种是看表格维度:Excel 原始数据 101 行 5 列,useFirstRowAsHeader=true时,QTableWidget 应该显示 100 行 5 列;如果列数对不上,问题一定出在dimension().columnCount()或隐藏列上。
第三种是对边界数据做专项检查:在测试 Excel 里放一个合并单元格、一个空单元格、一列日期、一列以 0 开头的文本号,比如00123。合并单元格只会保留左上角的值,空单元格会按占位符显示,日期会显示成序号或 QDateTime,文本号如果被 Excel 当数字读,00123会变成123。这些现象不是封装 Bug,而是 Excel 数据模型本身的特点,提前知道就不会到现场再去查。
这里还有一个边界:QXlsx 读取合并单元格时,除了左上角,其他被合并的位置cellAt都返回空指针。如果你的业务要求被合并区域都显示同一个值,需要在openFile之后补一个“合并区域展开”的步骤,遍历每个 cell 的 merged 范围,把左上角值抄到其他位置。更简单的做法是让 Excel 导出方不要合并单元格,但这往往不现实,所以封装里最好预留这个能力。
5. 避坑指南:Excel读取到QTableWidget的五个高发问题与排查顺序
5.1 现象:文件路径没错却打不开,程序直接崩溃或解析失败
最常见的原因是 QXlsx 库版本和 Qt 版本不匹配,尤其当你从网上下载源码直接编进工程时,有的版本load()接口已经改名,有的版本头文件还依赖 C++17,而你的工程还开着 C++14。其次是中文路径问题,某些老版本 QXlsx 对本地编码路径处理不好,文件能打开但内部 XML 定位失败。排查时我先把测试文件复制到纯英文路径,比如D:/test.xlsx,如果英文路径下能跑通,说明问题集中在路径或编码上;如果英文路径下也崩溃,则优先查库接口和源码版本。解决办法是把 QXlsx 固定成一个经过验证的版本,不要每次新建工程都去拉最新源码;同时用QDir::toNativeSeparators统一路径格式,并在openFile入口打印filePath.toUtf8(),确认 QString 没有被截断。
5.2 现象:中文表头显示正常,但第二行开始数据整体错位
错位最常见的原因是 Excel 源文件里有隐藏列或者合并列。你在 Excel 里看到的A、B、C三列,实际上文件里可能是五列,中间两列被隐藏了,dimension().columnCount()返回的是实际最大列号,但你在显示端只看到可见的三列数据,于是从第二行开始感觉是内容右移或左移。另一个原因是每行末尾有空列时,如果读取循环只读到非空单元格就不补位,那行与行之间的列号就会对不齐。解决方法是严格按dimension().columnCount()遍历每一列,空单元格一律补空字符串,保证每一行列长一样。然后再检查 Excel 源文件的隐藏列,把那些列在 UI 上通过setColumnHidden隐藏,而不是读取时跳过。
5.3 现象:两万行数据一填就卡死,UI失去响应
卡死通常不在读取,而在fillTable的写入循环。QTableWidget 每 setItem 一次,即使不触发重绘,内部也要维护 item 的结构;如果你在循环里每写几个单元格就调用一次resizeColumnsToContents,那复杂度就更离谱了,因为列宽自适应需要扫描每一行数据,两万行乘几十列,UI 线程直接冻住。我的处理习惯是三步:先setUpdatesEnabled(false),再批量创建 QTableWidgetItem 并setItem,结束后setUpdatesEnabled(true),最后只调一次resizeColumnsToContents。如果数据量超过五万行,QTableWidget 本身也不合适了,应该换QTableView + QAbstractTableModel,把数据放到 model 里按需显示,但那是另一个方案,这里先不展开。
5.4 现象:关闭界面时偶发崩溃,错误堆栈停在QAxObject
如果你用的是 QAxObject 路线,这个崩溃几乎必现。原因是 COM 对象在创建它的线程里最后释放,而退出窗口时消息循环已经部分销毁,Excel 进程还没退出,QAxObject析构时再去调 COM 接口就崩了。另一个原因是频繁开关文件后,Excel 进程残留了很多实例,每个实例都占着一份内存,最终把系统资源耗光。我的建议很直接:如果只是读数据,不要用 QAxObject,改用 QXlsx 可以根除这个问题。如果确实需要 Excel 计算能力,避坑要点是不要让QAxObject作为局部变量散落在按钮槽里,而是收进一个单例管理器,在closeEvent里显式clear(),必要时用taskkill兜底清理残留进程。
5.5 现象:日期在Excel里是43xxx,显示成数字
这是 Excel 的日期存储模型导致的。Excel 把日期存成从 1900 年起的序列号,43101 这类数字实际代表 2018 年某一天,但 QXlsx 读出来的原始值类型取决于单元格格式,有时候是 double,有时候是 QDateTime。直接toString()时,double 自然就变成了一串数字。解决方法是别信单元格的“显示格式”,而是独立维护一张列类型映射表,比如第 3 列约定是日期列,就在fillTable之前把该列所有单元格统一格式化成yyyy/MM/dd。如果不想维护映射表,也可以用 QXlsx 读到的cell->value().userType()做运行时判断,遇到QMetaType::QDateTime再格式化,但这种方式在数字型日期上并不可靠,所以我自己只把它当辅助手段。
6. 进阶技巧:把封装做成能直接交付的Excel预览组件
到这个阶段,你已经有了一个稳定的 Reader 和一套可调的显示参数。如果再往下走,我通常会补一个后处理函数,把表格变成更适合交付的预览组件:首行加粗、自动调整列宽、冻结首行、隐藏空列。下面这个函数可以放在任何用到 QTableWidget 的地方,不依赖具体数据来源:
void beautifyTable(QTableWidget *table) { QHeaderView *h = table->horizontalHeader(); h->setSectionResizeMode(QHeaderView::Interactive); h->setStretchLastSection(true); QFont font = table->horizontalHeader()->font(); font.setBold(true); table->horizontalHeader()->setFont(font); table->verticalHeader()->setVisible(false); table->setSelectionBehavior(QAbstractItemView::SelectRows); if (table->rowCount() > 0) { table->setCurrentCell(-1, -1); table->setEditTriggers(QAbstractItemView::NoEditTriggers); } }我自己的交付习惯是写一个“读取结果自检”函数:读取完成后,断言rowCount和columnCount与 Excel 源文件一致,检查每个表头都不是空字符串,再随机抽三条数据行和源文件手工对比。这套自检不用做得太重,但每次改封装、换库版本时跑一遍,能省掉大量现场排查时间。尤其是升级 QXlsx 之后,接口变化很小,但日期读取行为可能变,不跑一遍直接上生产,很容易在某一个文件上翻车。
还有一个教训想提一句:封装最怕越做越大。早期我把 Excel 样式、合并单元格、公式计算全塞进ExcelReader,结果每次改需求都要重新编译、重新测,后来把读取、填表、美化拆成三段,改动范围一下就明了。以后你接手类似需求,先想清楚哪些逻辑是“数据从文件来”的部分,哪些是“数据往表格去”的部分,边界画得越早,后面踩坑越少。希望帮到你。
本文还有配套的精品资源,点击获取