简介:一份面向QT开发者的Excel高效导入导出与表格显示实现方案,解决在QTableWidget中快速读写Excel数据的常见需求。资源包含完整工程源码、界面设计文件、资源文件以及可直接运行的exe程序,辅以loading.gif动态演示,方便对照学习与二次开发。包内共10个文件,涵盖2个cpp、1个h、1个ui、1个pro、1个qrc等典型QT项目结构,压缩包仅14.89MB,轻量易用。已有5079人学习下载。方案基于QAxObject调用Excel COM接口,给出导入、导出、批量读写、异步与多线程优化等关键代码思路,并配有界面操作演示,适合希望快速掌握QT与Excel交互的中初级开发者参考。 做QT桌面程序开发的,十有八九躲不过表格数据的导入导出。尤其是当你面对一堆Excel文件,要么导进程序展示,要么把界面上的数据导出去给第三方,这个需求几乎每个项目都会遇到。我最近在做的一个数据管理工具,就实打实地把"QT + Excel + tableWidget"这条路从选型到踩坑完整走了一遍,今天把整个过程整理出来,希望能帮你少走点弯路。
先说结论:如果你和我一样,需要的是"跨平台、开源免费、不依赖目标机器安装Office"的Excel读写方案,那主选QXlsx,备选QAxObject。下面我会详细拆解为什么这么做,以及每一步的具体实现。
1. 为什么我最终选了QXlsx而不是QAxObject
最开始接到需求时,我脑子里第一个冒出来的是QAxObject——QT自带的ActiveX方案,因为网上教程多、代码一看就会。但真要落地才发现问题:QAxObject的本质是通过COM接口调用本机安装的Microsoft Excel,也就是说目标机器上必须装了Office才能跑。我这边开发机器装了Office当然没问题,可交付给客户后,客户没装Office,程序一启动就崩。
当时我做了个简单的选型对比,这里整理成表格供你参考:
| 方案 | 跨平台 | 依赖Office | 开源免费 | 读写.xlsx | 学习成本 |
|---|---|---|---|---|---|
| QAxObject | 仅Windows | 必须安装 | 是 | 支持 | 低 |
| QXlsx | 是 | 不依赖 | 是 | 支持 | 中 |
| libxl | 是 | 不依赖 | 商业授权 | 支持 | 低 |
| 直接读写CSV | 是 | 不依赖 | 是 | 不支持 | 低 |
最终我选了QXlsx——它在GitHub上开源免费,纯C++实现,直接编译进工程就能用,没有运行时依赖,Linux、Windows都能跑。而且它读写.xlsx文件的能力很完整,支持单元格格式、合并单元格、公式等常见功能,对绝大多数桌面应用场景足够用了。
这个选择背后的逻辑其实很清晰:桌面工具的分发场景里,你永远无法控制目标机器装了什么东西。选择不依赖外部环境的方案,等于断掉了一根最容易出问题的电源线。
2. 工程集成:先把QXlsx跑起来
确定方案后,第一步是把QXlsx集成进QT工程。这步看似简单,但细节处理不当会在后面读写时冒出各种莫名奇妙的报错。
2.1 源码集成方式
推荐直接去GitHub把QXlsx的源码拉下来,然后拷贝到你工程的子目录里。我的工程结构大概是这样:
MyProject/ ├── MyProject.pro ├── src/ │ ├── main.cpp │ ├── mainwindow.h │ ├── mainwindow.cpp │ └── ... └── third_party/ └── QXlsx/ ├── xlsxdocument.h ├── xlsxdocument.cpp ├── xlsxcell.h ├── xlsxcell.cpp └── ...在.pro文件里这样配置:
# QXlsx 源码集成 INCLUDEPATH += $$PWD/third_party/QXlsx SOURCES += \ third_party/QXlsx/xlsxdocument.cpp \ third_party/QXlsx/xlsxcell.cpp \ third_party/QXlsx/xlsxworksheet.cpp \ # ... 其他需要编译的cpp文件 HEADERS += \ third_party/QXlsx/xlsxdocument.h \ third_party/QXlsx/xlsxcell.h \ third_party/QXlsx/xlsxworksheet.h \ # ... 其他需要包含的头文件说实话,逐个列源文件的方式确实有点啰嗦,好处是升级某个文件时不会牵连到别的工程。如果你愿意,也可以用QXlsx提供的QXlsx.pri文件,直接一句include($$PWD/third_party/QXlsx/QXlsx.pri)搞定,二选一即可。我习惯用后者,省事。
2.2 跑通一个最小读写Demo
集成完成后,先不要急着接界面,写个最基础的读写Demo验证环境。下面是读取Excel中第一个sheet的A1、B2单元格并打印的代码:
#include "xlsxdocument.h" #include <QDebug> void testQXlsx() { // 打开已有文件 QXlsx::Document doc("D:/test/example.xlsx"); if (!doc.load()) { qDebug() << "load failed"; return; } QXlsx::Cell* cellA1 = doc.cellAt("A1"); QXlsx::Cell* cellB2 = doc.cellAt("B2"); if (cellA1) { qDebug() << "A1:" << cellA1->value(); } if (cellB2) { qDebug() << "B2:" << cellB2->value(); } }写文件更简单:
QXlsx::Document doc; doc.write("A1", "Hello QT"); doc.write("B2", 12345); doc.saveAs("D:/test/out.xlsx");如果这个Demo能跑通,说明环境没问题,正式开发中遇到问题可以快速定位到是环境问题还是业务逻辑问题。
2.3 版本和编译器的坑
这里提醒一句:QXlsx对编译器版本有要求,尽量用较新的QT版本,比如5.15+或6.x,配MSVC2019/2022或GCC 9以上。我之前在QT 5.12 + GCC 7的环境下编译,遇到了一堆C++标准不兼容的问题。排查半天发现是编译器太老、对C++17支持不完整导致的,升级编译器后就正常了。
3. 把Excel吃进tableWidget:导入实现的完整拆解
环境跑通后,正式进入核心逻辑:从Excel读取数据,填充到QTableWidget控件中。
3.1 文件选择与格式校验
用户点击"导入"按钮后,先用QFileDialog弹出文件选择对话框,只允许选择.xlsx和.xls结尾的文件:
void MainWindow::onImportButtonClicked() { QString filePath = QFileDialog::getOpenFileName( this, tr("选择Excel文件"), QDir::homePath(), tr("Excel文件 (*.xlsx *.xls)") // 这里注意.xls不是QXlsx的强项,见下文 ); if (filePath.isEmpty()) { return; // 用户取消 } // 额外做一次后缀校验,防止用户手动输入非法路径 if (!filePath.endsWith(".xlsx", Qt::CaseInsensitive) && !filePath.endsWith(".xls", Qt::CaseInsensitive)) { QMessageBox::warning(this, tr("格式错误"), tr("请选择Excel文件")); return; } importExcel(filePath); }提示:QXlsx对.xlsx支持最好,对老旧的.xls支持有限。如果遇到.xls文件,建议让用户另存为.xlsx格式再导入,或者程序内部调用LibreOffice转换。这一点在对接老客户时经常踩到。
3.2 逐行读取与类型识别
核心读取逻辑:先获取工作表行列数,然后遍历单元格,把数据读到QVariant列表中暂存,最后统一填充到tableWidget。
这里有个容易忽略的点:QXlsx中dimension()返回的是表的有效区域,不要硬编码固定行列,否则文件末尾有空行时会报错或读错。示例代码:
void MainWindow::importExcel(const QString& filePath) { QXlsx::Document doc(filePath); if (!doc.load()) { QMessageBox::critical(this, tr("错误"), tr("无法加载Excel文件")); return; } QXlsx::Worksheet* sheet = doc.workbook()->activeWorksheet(); if (!sheet) { QMessageBox::warning(this, tr("提示"), tr("工作簿中没有可用的工作表")); return; } QXlsx::CellRange range = sheet->dimension(); // 有效数据区域 int rowCount = range.rowCount(); int colCount = range.columnCount(); // 保存读取的数据,待会再填充到tableWidget QVector<QVector<QVariant>> data; data.reserve(rowCount); for (int row = 0; row < rowCount; ++row) { QVector<QVariant> rowData; rowData.reserve(colCount); for (int col = 0; col < colCount; ++col) { QXlsx::Cell* cell = sheet->cellAt(row + 1, col + 1); // QXlsx行列从1开始 if (cell) { QVariant value = cell->value(); rowData.append(value); } else { rowData.append(QVariant()); // 空单元格 } } data.append(rowData); } populateTableWidget(data); }读取阶段要留意类型:Excel单元格的值可能是数字、字符串、日期、布尔值。QXlsx的cell->value()会把这几种情况转成对应的QVariant类型。日期在底层会被转成QDateTime或double,如果你直接拿去显示,会看到一串数字,这一点等会儿在第5章的踩坑汇总里细说。
3.3 填充tableWidget时的三个性能陷阱
拿到DATA后,填充到QTableWidget看似简单——两层循环setItem就行了。但数据量一上来,两个最典型的性能陷阱就会出现。
陷阱一:setRowCount和setColumnCount一次性设置好再填数据,不要每写一个单元格就自动增加行列。后者会让容器的内存不断重新分配,10万行数据直接卡死。
陷阱二:关闭tableWidget的排序。如果界面上开启了setSortingEnabled(true),每插入一个item都会触发一次排序,复杂度直接翻倍。正确做法是先setSortingEnabled(false),全部插入完再恢复。
陷阱三:填充过程中不要频繁触发界面刷新。QTableWidget没有像QTableView那样直接的setUpdatesEnabled控制,但你可以先清空、再填充、最后一次性repaint(),避免反复重绘造成的闪烁。
下面是我最终使用的填充函数:
void MainWindow::populateTableWidget(const QVector<QVector<QVariant>>& data) { ui->tableWidget->setSortingEnabled(false); // 插入前关闭排序 ui->tableWidget->clearContents(); // 清空旧数据 int rowCount = data.size(); int colCount = 0; for (const auto& row : data) { colCount = qMax(colCount, row.size()); } ui->tableWidget->setRowCount(rowCount); ui->tableWidget->setColumnCount(colCount); for (int row = 0; row < rowCount; ++row) { for (int col = 0; col < data[row].size(); ++col) { QVariant value = data[row][col]; if (!value.isValid() || value.isNull()) { continue; // 空单元格不创建item,节省开销 } QString text = value.toString(); QTableWidgetItem* item = new QTableWidgetItem(text); // 数字右对齐,看起来更专业 if (value.typeId() == QMetaType::Int || value.typeId() == QMetaType::Double) { item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); } ui->tableWidget->setItem(row, col, item); } } // 填充完再设置表头和数据 ui->tableWidget->setSortingEnabled(true); // 恢复排序 ui->tableWidget->resizeColumnsToContents(); // 自动调整列宽 ui->statusBar->showMessage(tr("导入完成,共 %1 行 %2 列").arg(rowCount).arg(colCount)); }注意,如果数据量极大(比如超过5万行),resizeColumnsToContents()本身会卡顿一下。我会把列宽调整放到一个延时操作中,或者只在用户双击某个列头时才触发。
4. 数据上桌之后的显示调优:把tableWidget做得像样
数据填充只是第一步,真正让用户觉得"好用"的,是表格的显示细节。很多程序员忽略了这一步,结果导入的数据都在,但界面乱糟糟,用户一行都看不下去。
4.1 表头、只读与列宽:这是基本体面
表头是用户在表格上第一眼看到的东西。从Excel导入后,默认设置为A、B、C这样的列名毫无意义,正确做法是读取Excel第一行作为表头:
void MainWindow::setTableHeaders(int colCount) { QStringList headers; headers.reserve(colCount); for (int col = 0; col < colCount; ++col) { QTableWidgetItem* headerItem = ui->tableWidget->horizontalHeaderItem(col); if (headerItem && !headerItem->text().isEmpty()) { headers << headerItem->text(); } else { headers << QString("列%1").arg(col + 1); } } ui->tableWidget->setHorizontalHeaderLabels(headers); }同时,表格默认是可以双击编辑的,这不符合"展示导入数据"的定位。要把表格设置为只读模式,但注意要允许用户复制单元格内容:
// 只读模式 ui->tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); // 允许复制单元格内容 ui->tableWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); ui->tableWidget->setSelectionBehavior(QAbstractItemView::SelectItems);4.2 大数据量的异步加载与用户体验
如果你要导入的Excel动辄几万行,一次性填充会让界面卡住几秒甚至十几秒,这对用户来说体验极差。我的做法是分两档处理:
- 少于5000行:直接同步填充,速度可接受。
- 大于5000行:先填充前100行让用户看到"有反应",再放到QThread后台线程里继续填充剩余数据,每填充2000行发一个信号刷新界面。
这边给出一个简单的异步思路(完整代码篇幅太长,只贴关键骨架):
class ImportWorker : public QObject { Q_OBJECT public slots: void doImport(const QString& filePath) { // 这里执行Excel读取和item生成 // 完成后 emit importFinished(data); } signals: void importFinished(const QVector<QVector<QVariant>>& data); }; // 在MainWindow里 void MainWindow::startImport(const QString& filePath) { QThread* thread = new QThread; ImportWorker* worker = new ImportWorker; worker->moveToThread(thread); connect(thread, &QThread::started, worker, [worker, filePath]() { worker->doImport(filePath); }); connect(worker, &ImportWorker::importFinished, this, &MainWindow::onImportFinished); connect(worker, &ImportWorker::importFinished, thread, &QThread::quit); connect(thread, &QThread::finished, worker, &ImportWorker::deleteLater); connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); } void MainWindow::onImportFinished(const QVector<QVector<QVariant>>& data) { populateTableWidget(data); }注意:Excel读取用到了QXlsx的Document对象,它在后台线程创建和加载没问题,但千万别把QTableWidget等界面对象直接塞进子线程。正确做法是子线程只负责解析数据,返回纯数据容器,主线程再负责填充UI。
4.3 视觉增强:交替色、行高与冻结表头
为了让表格更好看、更易读,我追加了几个小调整:
- 开启交替行背景色:
ui->tableWidget->setAlternatingRowColors(true); - 默认行高设小一点(如果数据量大):
ui->tableWidget->verticalHeader()->setDefaultSectionSize(24);- 冻结首行,让表头始终可见:
ui->tableWidget->resizeRowsToContents();这一步实际上并不能冻结,真正冻结表头需要配合QTableView的setViewportMargins或自定义代理,考虑到篇幅这里不展开。实际上QT 5.15+的QTableView没有原生冻结功能,业界常用的方案是双表头联动,或者直接按"表头固定"的视觉效果来处理:保持首行数据在滚动时不上移。如果只是想让列头(垂直头)始终显示,Qt已经在滚动时自动做到了。
这些细节提升的是整体使用感受,尤其是用户需要较长滚动浏览时,交替色和合理行高能明显降低眼睛的疲劳感,对我们这种天天盯屏幕的人来说很重要。
5. 反向操作:从tableWidget一键回写Excel
导入做好了,导出通常也不会少。这里的核心需求是:用户可能在tableWidget里做了排序、筛选、修改,甚至手动添加了几行数据,导出时要把界面上的这些改动原样写到Excel。
5.1 导出路径选择
导出前让用户选择保存路径和文件名:
QString filePath = QFileDialog::getSaveFileName( this, tr("导出Excel"), QDir::homePath() + "/export.xlsx", tr("Excel文件 (*.xlsx)") );5.2 遍历item写出的核心代码
我按行遍历tableWidget的item,逐格写入QXlsx的Document对象。这里要注意:tableWidget的单元格可能没有建item(空单元格),不要因此错位。
void MainWindow::exportToExcel(const QString& filePath) { QXlsx::Document doc; int rowCount = ui->tableWidget->rowCount(); int colCount = ui->tableWidget->columnCount(); // 写入表头 for (int col = 0; col < colCount; ++col) { QTableWidgetItem* headerItem = ui->tableWidget->horizontalHeaderItem(col); QString headerText = headerItem ? headerItem->text() : QString("列%1").arg(col + 1); doc.write(1, col + 1, headerText); } // 写入数据 for (int row = 0; row < rowCount; ++row) { for (int col = 0; col < colCount; ++col) { QTableWidgetItem* item = ui->tableWidget->item(row, col); if (!item) { continue; // 空单元格跳过 } QString text = item->text(); // 尝试把纯数字字符串转成数字,这样Excel里可以做计算 bool ok = false; double num = text.toDouble(&ok); if (ok) { // 判断是否整数,是整数就存为整数,否则存为double if (num == qint64(num)) { doc.write(row + 2, col + 1, qint64(num)); // Excel行从1开始,数据从第2行开始 } else { doc.write(row + 2, col + 1, num); } } else { doc.write(row + 2, col + 1, text); } } } // 顺手设置列宽,导出的表格也好看一点 for (int col = 0; col < colCount; ++col) { int maxWidth = 10; for (int row = 0; row < rowCount; ++row) { QTableWidgetItem* item = ui->tableWidget->item(row, col); if (item) { maxWidth = qMax(maxWidth, item->text().length()); } } doc.setColumnWidth(col + 1, maxWidth + 4); } if (doc.saveAs(filePath)) { QMessageBox::information(this, tr("提示"), tr("导出成功")); } else { QMessageBox::critical(this, tr("错误"), tr("导出失败,请检查路径或文件权限")); } }提示:上面这段代码里故意做了"数字与字符串"的区分处理,目的是让导出的Excel里的数字列能够参与SUM、AVERAGE等公式计算。如果你全部存QString,用户后续在Excel里求和会得到0,这是很容易被忽略的隐藏bug。
5.3 导出时的冻结窗格与工作表命名
如果你导出的表格有几十列,用户在Excel里查看时,首行表头不能固定不说,往下翻几行就不知道每一列是什么了。虽然Excel自带"冻结窗格"功能,但更好的做法是在导出时就替用户设置好:
// 冻结首行 doc.worksheet()->freezePanes(2, 1); // 第2行以上冻结,第1列向左冻结 // 或者用更直观的方式:冻结到B2单元格上方和左侧 doc.worksheet()->freezePanes("B2");同时,把sheet名字从默认的"Sheet1"改成更有业务含义的名字,比如"数据清单":
doc.worksheet()->setName("数据清单");这样导出的文件打开后,默认在"数据清单"工作表,首行固定,用户一看就明白内容是什么,不需要额外操作。
6. 从崩溃到流畅:我踩过的几个坑和优化心得
这一章算是我个人最想分享的部分。很多问题不是看文档能看到的,只能在实际跑数据时踩过一次才记住。
6.1 微软Excel没装,QAxObject直接崩
前文提到我最开始用QAxObject,但发现目标机器没装Office后,程序运行到setControl("Excel.Application")这行就崩溃了。这个崩溃还不是那种可以捕获的异常,而是直接抛出 COM 错误。
解决方案就是换成QXlsx。但还要提醒一句:如果你的程序无论如何都要操作Excel的宏(.xlsm)或某些高级格式,那QXlsx做不到,只能回到QAxObject,但必须对"目标机器未安装Office"做提前预判和友好提示。
6.2 中文路径和文件名乱码
Windows下中文文件路径是老大难。QXlsx底层用的是QFile、文件流等方式,理论上是支持UTF-8的中文路径的。但我在某些Windows设置下确实遇到过中文路径打不开的情况。稳妥做法是:
- 在打开文件前把路径统一转成标准格式:
QString normalizedPath = QFileInfo(filePath).absoluteFilePath(); normalizedPath.replace("/", QDir::separator()); // 转换成系统分隔符- 在程序启动时设置统一编码(对QT 6来说默认就是UTF-8,基本不用管;QT 5建议加一句):
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));6.3 数字变成科学计数法
Excel里输入长数字(比如身份证号、订单号)时,默认会显示成科学计数法,导入QT后你读到的可能是类似1.23457e+17的字符串,直接显示在表格里用户根本看不懂。
QXlsx的cell->value()对数字类型返回的是double,要正确处理这种大整数,需要在读取时判断:
QVariant value = cell->value(); if (value.typeId() == QMetaType::Double) { double d = value.toDouble(); // 如果接近整数且位数很大,转成整数或字符串 if (d > 1e15) { // 用 qlonglong 存储,但注意长度超过19位会溢出 qlonglong ll = static_cast<qlonglong>(d); value = QString::number(ll); } }建议统一把这种长数字按文本处理,否则后续导入数据库时会因为类型转换丢失精度。
6.4 十万行数据卡死的定位过程
有次客户给了一个将近10万行的Excel,我直接用最初的同步版本导入,程序直接卡了十几秒,期间窗口无响应,标题栏出现"未响应"。我一开始以为是填充QTableWidget太慢,后来用QElapsedTimer分别统计了"读取Excel耗时"和"填充表格耗时",发现读取只要几百毫秒,填充阶段占了大头。
进一步分析发现:填充阶段频繁地触发表格的信号、排序和重绘。我把setSortingEnabled(false)加上,并在填充前调用setUpdatesEnabled(false),填充完成后再恢复重绘,性能立刻提升了一个数量级。再配合分页或后台线程方案,10万行的文件也能在2-3秒内导入并且不卡界面。
6.5 从"能跑"到"好用"的几个小细节
最后记录几个我每次都会打开的小功能,它们看起来无关紧要,但对用户体感的提升是质变级的:
- 导入完成后提示成功的行数、列数,并把状态栏清空,让用户明确知道导入完成。
- 表格列宽支持拉伸后自动保存到配置文件里,下次打开程序还能用。
- 导出时如果目标文件已存在,QXlsx的
saveAs是默认覆盖的,我习惯在覆盖前弹窗询问一下,避免用户手滑覆盖掉重要文件。
这些细节单独拎出来都不难,但串在一起就能让工具从"能用"进阶为"好用",也是我眼中专业工具和业余脚本的分水岭。
本文还有配套的精品资源,点击获取