Qt中使用QXlsx实现Excel导入导出与QTableWidget展示 📅 发布时间:2026/9/2 3:31:29 👁 浏览次数: 简介面向QT开发者这份资源旨在解决程序中高效导入导出Excel并显示在tableWidget上的问题围绕QAxObject调用Excel COM接口展开内容覆盖导入、导出、单元格批量读写以及通过多线程、异步I/O等方式避免界面卡顿的优化思路。压缩包共10个文件包含cpp/h源文件、ui界面文件、pro工程文件、可直接运行的exe和演示gif整体14.89MB便于边查看运行动效边对照源码学习。已有5079人学习下载可作为QT中操作Excel的入门参考也可改造成现成的报表导入导出工具。借助示例代码可以快速掌握动态创建Excel应用、打开/创建工作簿、读取工作表数据、填充QTableWidget以及写回并另存Excel文件的完整流程尤其适合需要批量处理Excel数据、快速搭建报表功能的桌面应用开发场景附带安装脚本也便于打包分发。 我们做Qt桌面开发尤其是接一些企业内部工具、数据管理系统的时候几乎躲不开Excel导入导出的需求。不管是把Excel里的数据批量灌进软件还是把软件里的数据导出来给客户看都是高频操作。网上搜“QT 导入导出Excel”会出来一堆帖子但大多讲得零碎要么只讲原理不贴代码要么贴了旧版Qt的写法早就不适用了。今天这篇就以QTableWidget做数据展示为载体把Excel导入导出这条链路完整捋一遍从库的选型到性能优化再到我实际踩过的坑一次说清楚。1. 库的选择和前期准备1.1 为什么不用QAxObject而是选QXlsx最早我做Excel操作时图省事直接用了QAxObject毕竟Windows下调用Excel COM接口确实方便几行代码就能打开文件、读取单元格。但用多了你会发现问题很明显第一客户机器上没装Office就直接废掉第二即使装了OfficeExcel进程经常关不干净任务管理器里一堆残留第三一旦数据量大一点COM交互慢得让人崩溃。后来我全面转向了QXlsx这是一个开源的C库核心思路是直接解析xlsx文件格式不依赖任何Office环境跨平台Windows、Linux、macOS都能跑而且对Qt原生支持极好几乎就是为Qt项目准备的。引用方式也简单把QXlsx源码加入工程或者用它的源码文件编译成静态库就能直接在项目里用没有任何额外的运行时依赖。如果你想追求更极致的写性能可以再单独引入libxlsxwriter它在写入大数据量文件时明显比QXlsx快。但这里有个取舍QXlsx能读能写一个库全搞定libxlsxwriter只写不读需要另外一个库来配合。对绝大多数桌面工具来说QXlsx的性能已经足够所以我后续的示例代码都用QXlsx演示。1.2 在pro或CMake里配置QXlsx如果你用的是qmake直接在工程文件里把QXlsx的源码路径包含进来就行。我是直接把QXlsx源码放在第三方库目录下然后这样写INCLUDEPATH $$PWD/3rdparty/QXlsx SOURCES \ $$PWD/3rdparty/QXlsx/xlsxdocument.cpp \ $$PWD/3rdparty/QXlsx/xlsxworksheet.cpp \ $$PWD/3rdparty/QXlsx/xlsxcell.cpp \ $$PWD/3rdparty/QXlsx/xlsxformat.cpp \ $$PWD/3rdparty/QXlsx/xlsxutility.cpp如果你用CMake那就更直接了add_subdirectory(QXlsx)然后target_link_libraries(你的项目 QXlsx)就能完成集成。这里提醒一句QXlsx最新版本对编译器有要求建议用支持C17的编译器另外如果你的Qt版本比较老可能会碰到一些API不兼容的编译报错碰到这种情况优先看官方仓库的README它会说明支持的Qt版本范围。2. 导入Excel从文件选择到QTableWidget显示2.1 获取文件路径并校验合法性导入的第一步自然是让用户选择要导入的Excel文件。Qt做文件对话框很简单用QFileDialog::getOpenFileName就能弹出一个系统原生对话框。核心代码如下QString filePath QFileDialog::getOpenFileName( this, tr(选择Excel文件), QDir::homePath(), tr(Excel文件 (*.xlsx *.xls)) ); if (filePath.isEmpty()) { return; // 用户取消选择 }文件路径拿到之后不要急着解析先做两个小校验第一文件是否存在第二扩展名是否符合预期。有些用户会改后缀名比如把一个txt文件直接改成.xlsx后面QXlsx解析时会直接抛错所以提前拦截更稳妥。真正的读取逻辑用QXlsx的Document接口就可以完成QXlsx::Document xlsx(filePath); if (!xlsx.load()) { QMessageBox::warning(this, 错误, Excel文件加载失败请确认文件格式正确); return; }Document对象加载成功后xlsx文件里所有工作表sheet就都处于可读取状态了。默认读第一个工作表如果有多个sheet可以用xlsx.selectSheet(Sheet1)或者通过索引访问。2.2 按行读取数据和行列边界判断xlsx文件本身是XML结构QXlsx已经把底层解析封装好了我们只需要关心行和列的循环读取。最笨的办法是双层for循环从第1行、第1列一直读到一个非常大的边界比如第10000行、第100列但这样会做大量无效读取性能极差。我当时实际测试了一下读取一个1万行、20列的文件如果用边界试探法耗时可能是精确读取法的3到5倍。所以正确做法是先拿到实际数据范围int rowCount xlsx.dimension().rowCount(); int colCount xlsx.dimension().columnCount();这里的dimension()返回的是当前sheet的有效数据区域准确又高效。拿到行列数之后再逐行读取单元格for (int row 1; row rowCount; row) { for (int col 1; col colCount; col) { QVariant value xlsx.read(row, col); // read是1-based索引 // 存储到数据结构或者直接填到表格里 } }这里有一个必须注意的点QXlsx的read方法返回的是QVariant也就是说单元格的实际类型可能是int、double、QString等读取出来之后你不能傻乎乎地直接toString()要保留它的原始类型。比如Excel里的日期读出来可能是double类型代表从1900年算起的天数如果你直接转字符串用户看到的就是一串数字而不是日期。处理方法是在写入表格前判断QVariant的type是QMetaType::Double就检查格式是QMetaType::QString就保留原值。2.3 填充QTableWidget的正确姿势QTableWidget填充数据本身不复杂但人人都会犯的一个错误就是不设置行列数就直接setItem。其实setItem会自动扩展表格行列但每次扩展都可能触发界面重绘和信号通知数据量一大就会非常卡。正确姿势是先一次性把行列设置好ui-tableWidget-clear(); ui-tableWidget-setRowCount(rowCount); ui-tableWidget-setColumnCount(colCount); ui-tableWidget-setHorizontalHeaderLabels(headers); // 可选 for (int i 0; i rowCount; i) { for (int j 0; j colCount; j) { QTableWidgetItem *item new QTableWidgetItem(data.toString()); ui-tableWidget-setItem(i, j, item); } }如果数据量特别大比如超过5万行一次性填充仍然会造成明显的卡顿。这时候有两个优化手段。第一个是批量数据填充前调用setUpdatesEnabled(false)填完再恢复ui-tableWidget-setUpdatesEnabled(false); // 批量填充 ui-tableWidget-setUpdatesEnabled(true);第二个是分页加载。最简单的方式是只读取前100行或1000行显示用户滚动到底部触发加载更多。但这样会带来一定的复杂度如果只是内部工具通常直接一次性填充加上setUpdatesEnabled(false)就够用了如果数据量超过10万行强烈建议放弃QTableWidget改用QTableViewQAbstractTableModel那个方案模型视图分离滚动流畅度不是一个量级的。2.4 耗时操作的界面不卡顿处理导入一个大文件时主线程直接执行解析界面会假死体验非常不好。我的做法是开一个QThread去做文件解析解析完之后发射信号把数据传回主线程更新表格。class ExcelImportWorker : public QThread { Q_OBJECT public: explicit ExcelImportWorker(const QString filePath, QObject *parent nullptr); signals: void importReady(const QVectorQVectorQVariant data); void importError(const QString msg); protected: void run() override; };在run()里完成文件读取和解析把结果放在QVector里通过信号一次性传回。这里要注意信号参数不要用二维数组或自定义复杂结构体容易出信号注册问题。用QVectorQVector 是安全的因为QVariant和容器都已在元类型系统里注册过。3. 导出Excel从QTableWidget到xlsx文件3.1 快速导出QTableWidget全部内容导出的逻辑是导入的逆过程。先遍历QTableWidget的行列把数据取出来然后写入QXlsx::Document对象。我在实际项目里实现了一个通用函数bool exportTableToExcel(QTableWidget *table, const QString filePath, QString *errMsg) { QXlsx::Document xlsx; int rowCount table-rowCount(); int colCount table-columnCount(); // 先写入表头 for (int col 0; col colCount; col) { QTableWidgetItem *headerItem table-horizontalHeaderItem(col); QString headerText headerItem ? headerItem-text() : QString(列%1).arg(col 1); xlsx.write(1, col 1, headerText); } // 写入数据 for (int row 0; row rowCount; row) { for (int col 0; col colCount; col) { QTableWidgetItem *item table-item(row, col); if (item) { xlsx.write(row 2, col 1, item-text()); } } } return xlsx.saveAs(filePath); }这里要特别提醒一下索引偏移的问题QTableWidget的行列索引从0开始而QXlsx的write接口行列从1开始且第1行通常留给表头。所以写入数据的行号是row 2列号是col 1。少写错这个偏移导出文件就会出现表头错乱或数据错行。3.2 导出时保留数据类型而不是全部转字符串上面这个简单版本有一个明显的毛病所有单元格都变成了文本形式。如果用户想把导出的Excel再用VLOOKUP、SUMIF这些公式做分析文本型数字会让他们非常痛苦。所以我在导出前会对单元格内容做了个原始类型缓存这样导出时能保留数字类型。每填一个单元格时都判断一下数值类型然后以对应类型写入。QVariant originalValue item-data(Qt::UserRole); // 导入时把原始值存到UserRole if (originalValue.isValid()) { xlsx.write(row 2, col 1, originalValue); } else { xlsx.write(row 2, col 1, item-text()); }这个技巧的关键是导入时除了显示文本还要保留原始值QTableWidgetItem *item new QTableWidgetItem(displayText); item-setData(Qt::UserRole, originalValue); ui-tableWidget-setItem(i, j, item);这样一来导入再导出就是一个无损回环充分保留Excel和Qt之间的数据精度。3.3 导出sheet命名和样式处理QXlsx支持一个Excel文件里写多个sheet。默认情况下Document只有一个sheet名字叫Sheet1。我推荐在导出前至少设置一个有意义的工作表名如果业务要求导出多个sheet比如汇总表、明细表、统计表可以这样操作QXlsx::Document xlsx; xlsx.addSheet(明细数据); xlsx.write(1, 1, 字段A); xlsx.addSheet(汇总数据); xlsx.write(1, 1, 汇总字段); bool ok xlsx.saveAs(filePath);另外如果客户对导出样式有要求可以使用QXlsx::Format类设置单元格格式例如加粗表头、设置背景色、设置边框、设置列宽等。我在处理带样式的导出时通常先把表头设置成灰色背景加粗字体数据区域设自动换行最后统一调整列宽到合适的大小整个文件的观感会专业很多。QXlsx::Format headerFormat; headerFormat.setFontBold(true); headerFormat.setPatternBackgroundColor(QColor(#D9E1F2)); headerFormat.setBorderStyle(QXlsx::Format::BorderThin);3.4 导出对话框和路径选择导出到一个全新的文件时用QFileDialog::getSaveFileName选择路径。这里有一个很容易忽略的坑很多用户在Linux下选择路径时输入的目录不一定存在如果目录无效xlsx.saveAs会静默失败。所以我每次导出前都会主动提取目标目录并检查可写性QFileInfo fi(filePath); QDir dir fi.absoluteDir(); if (!dir.exists()) { QMessageBox::warning(this, 错误, 目标目录不存在); return; }如果目标文件已经存在也要提前处理QXlsx默认SaveAs是会覆盖旧文件的但如果你用saveAs之后还有后续操作最好先用QFile::remove删除旧文件再执行saveAs避免一些平台上文件被占用导致的写入异常。4. 性能优化与大数据量处理4.1 大数据量Excel导入性能瓶颈分析如果你的Excel文件只有几百行、几十列那完全不用纠结性能问题随便写都能秒开。但真实场景往往不这么美好我做的一个采销管理工具客户直接扔来一个几万行的库存明细表当时的界面惨不忍睹光填充TableWidget就卡了十几秒。先分析一下瓶颈到底在哪。Excel导入的瓶颈有四处第一文件解析本身XML解压、按sheet解析单元格第二QVariant到QString的转换第三QTableWidgetItem对象的创建几万个对象一次new一个内存分配开销巨大第四界面的每行刷新。4.2 实测对比一次性填充和分批填充我专门做过一次测试对比同样一个约6万行、12列的xlsx文件在普通配置的Windows 10电脑上跑方案操作耗时界面卡顿直接循环setItem填充约8秒明显卡死窗口无响应setUpdatesEnabled(false) setItem填充约2.5秒轻微卡顿窗口可响应QThread解析 信号回传填充约2秒无卡顿界面持续响应QAbstractTableModel QTableView约1.2秒完全无卡顿滚动流畅所以在数据量超过2万行时我建议直接把QTableWidget换成QTableViewQAbstractTableModel这不是偷懒而是技术选型的根本性变化。但如果你的项目界面已经用QTableWidget搭好了短期改造不划算那至少选择第二个方案setUpdatesEnabled(false)这个优化是零成本、见效又极快。4.3 单元格创建的内存管理技巧即使你决定用QTableWidget硬扛大数据量也有一些降低内存压力的技巧。举个例子不要为每个单元格都创建全新的QTableWidgetItem如果某行数据同一列都是空值可以复用同一个空白itemQTableWidgetItem *emptyItem new QTableWidgetItem(); for (int row 0; row rowCount; row) { for (int col 0; col colCount; col) { if (data.isEmpty()) { ui-tableWidget-setItem(row, col, emptyItem); // 错误示范 } } }注意这种做法是错的因为一个item不能同时挂在多个表格位置。正确做法是为空值也单独创建item但可以在创建之前先检查是否全部为空如果整列都是空值那这一列干脆完全不填充只设置一行空表头。在实际项目中空列和空值往往占了一半以上这样优化后能省下不少内存。4.4 分批加载和懒加载思路还有一种常见的数据延迟加载方式就是分页。不把Excel全部读进来每页只显示1000行。分页方案可以这样设计导入时只读全部数据到内存里的QVector再根据当前页码取对应切片填充到QTableWidget。这样QTableWidget里的item数量永远可控界面操作也始终流畅。网上有一种“懒加载”方案是只读取前N行滚动到底部再继续读下一个N行适合读取特别大的文件。但考虑到数据在内存里已经全部存在了省去重新解析的时间直接用切片展示更简单可靠。我实操下来数据量大于10万行且交互频繁时用分页方案效果最直接。界面上可以加“首页、上一页、下一页、末页、跳转页码”这几个控件实现逻辑也不复杂本质上就是根据pageSize和当前页计算起始行号和终止行号然后从内存数据里拷贝对应切片到表格。5. 典型问题排查和避坑指南5.1 QXlsx 读取xls文件失败QXlsx从名字就能看出来它只支持xlsx格式不支持老版的xls格式Excel 97-2003。我记得第一次测试时用户给了一个.xls文件QXlsx直接返回load失败。解决方案有两个一是强行要求用户提供xlsx文件在产品里写清楚另一个是用LibreOffice命令行做格式转换把xls转成xlsx再读取但这依赖于部署环境装了LibreOffice。如果你必须在Windows环境支持xls又没有Office你还可以试试QtXlsxWriter或者单独引入POI的C移植版。但说实话绝大多数情况下提前告诉用户“请另存为xlsx格式”是最省事的。5.2 导入时日期显示成一串数字这是新手最常遇到的坑。前面提到过一次这里再展开讲。Excel内部对日期的处理是用一个序列数表示的比如2024年1月1日对应的序列数是45292。当你用QXlsx读取的时候日期单元格的类型可能是double直接被转成QString就会变成一串浮点数。解决办法是在单元格转显示文本时增加一个类型判断if (value.type() QMetaType::Double || value.type() QMetaType::QDate) { QDate date value.toDate(); if (date.isValid()) { displayText date.toString(yyyy-MM-dd); } else { displayText value.toString(); } }但这里有一个隐含陷阱即使值能转成QDate你也无法确定它原本是不是日期类型。如果用户表格里存的就是纯数字9527toDate大概率会验证失败但个别数字碰巧在合法日期范围内就会被转成日期显示。更稳妥的思路是预先读一下单元格的格式是否属于日期格式QXlsx的cellFormat()接口可以拿到格式信息这一步没那么复杂但对精确性要求高的场景值得做。5.3 导出的Excel用公式统计时显示0这个问题出现在导出的数字全是文本类型时。即便你在导出时调了xlsx.write(row, col, value)如果value是从item-text()取出来的已经在Qt侧转成了字符串那存入Excel的依然是文本。文本数字在Excel里用SUMIF或VLOOKUP匹配时经常会匹配不上甚至SUM对文本数字也会返回0。解决方式是导出前做类型判断如果QVariant是int、double、float这些数字类型就直接传原始类型的QVariant而如果已经是字符串且确定是数字文本最好先用QVariant::toDouble()转换一次再写。为了区分“数字型文本”和“普通文本”我通常用正则判断一下QRegularExpression numRegex(^-?\\d(\\.\\d)?$); bool isNumericText numRegex.match(text).hasMatch(); if (isNumericText) { xlsx.write(row 2, col 1, text.toDouble()); } else { xlsx.write(row 2, col 1, text); }这样导出的文件数字列就是Excel原生数字后续公式计算就不会出问题。5.4 大文件导入时程序崩溃这类崩溃有个典型特征Excel文件本身合法qt的界面也正常但跑到一半程序直接退出甚至没有异常输出。排查下来常见原因有三类第一一次性setRowCount过多导致内存分配失败第二QXlsx在解析超大文件时内部有索引越界等未定义行为第三Qt信号跨线程传递传入参数时发生深拷贝大对象导致栈溢出或内存峰值过高。对于第一类我已经演示过用分段填充和setUpdatesEnabled(false)来处理。对于第二类建议在使用QXlsx前升级到最新版本旧版确实有一些大文件解析的bug。对于第三类建议跨线程传递数据时改用shared_ptr或者把数据指针作为参数注册为自定义类型避免反复深拷贝。最省心的是把读取和写入都放到后台线程只在最后用队列信号把结果指针传给UI线程。5.5 界面刷新不及时或表头丢失导入数据后如果表格没刷新比如数据填完了但界面还是空白的多半是因为没有调用update()或viewport()-update()。理论上setItem会触发刷新但某些极端情况下比如使用了DeferredDelete需要手动调用一次update。表头丢失的问题则通常是你调用了clear()而不是setColumnCount(0)再重新设置列数。注意QTableWidget::clear()会清空所有item和表头但你重新设置列数时如果没有重新setHorizontalHeaderLabels表头自然就丢了。推荐固定一下流程先clear再设置行列数再设置表头标签最后填充数据。5.6 文件被Excel占用导致保存失败用户在Windows下打开导出的Excel然后回到程序里再次导出同名文件大概率保存失败。原因很简单Excel进程锁住了这个文件。要解决这个问题可以在saveAs前用QFile尝试打开写入测试如果失败就提示用户关闭Excel里的文件或者程序里自动生成带时间戳的新文件名。我的建议是默认生成带时间戳的新文件比如“导出数据_20250101_153000.xlsx”既避免占冲突也方便版本管理。6. 绕不开的一些技术细节说明6.1 Qt版本和编译器的影响实测下来Qt 5.12 和 Qt 6.x 在QXlsx的编译上几乎没差别但如果你用的是MSVC 2015这种很老的编译器可能就要注意一些C11/14特性的适配了。QTableWidget相关API在两个大版本下也完全兼容这部分代码迁移成本很低。另一个容易踩坑的是信号槽的语法。Qt 5开始推荐函数指针式连接connect(worker, ExcelImportWorker::importReady, this, MainWindow::onImportReady);这种写法在编译期就检查参数类型比旧的SIGNAL/SLOT宏安全很多。我发现很多老教程还在教宏写法新项目里强烈建议改成函数指针式。6.2 处理单元格合并和换行符Excel里如果有合并单元格QXlsx读取时只有左上角的值能取到其他区域都是空值。如果你的业务依赖合并单元格的结构读取时就要自己处理一下合并区域的坐标映射。可以在QXlsx里通过worksheet-mergedCells()拿到合并单元格列表然后对合并区域内的所有单元格统一赋值同一个值。另外Excel单元格的换行符不是\n而是\r\n。如果你把Excel里的文本导入到QTableWidget里直接显示时换行会引发表格行高异常所以更合理的做法是导入时把\r\n统一替换成\n在表格里显示时正常换行导出时再转换回\r\n保持Excel的行内换行正常。6.3 性能测试方案如果你打算在一个上线项目里做Excel导入导出不要等到客户反馈才开始优化。我的习惯是提前准备一个生成大表的测试脚本或者直接用Excel软件生成一个10万行、30列的测试文件把导入导出流程跑一遍用QElapsedTimer统计耗时并打印日志。把这个基准结果记下来后续每次调整代码都重新跑一遍谁快谁慢一目了然。QElapsedTimer timer; timer.start(); // 执行导入/导出代码 qDebug() Excel导入耗时(ms): timer.elapsed();这套基准测试方法虽然没有花哨的代码但长期维护效率工具时会特别受益。7. 实际项目中的一点建议把导入、导出、表格展示这一套功能做扎实之后你会发现绝大多数业务软件里关于Excel的需求都离不开这三个动作读、显、写。QTableWidgetQXlsx这套组合对于中小型桌面工具来说已经足够稳定、高效。但如果你对性能有极致要求或者未来可能面对百万行级别的数据一定要尽早转向QTableViewQAbstractTableModel架构。QTableWidget本质上还是一个表格控件的封装它的易用性是有代价的——几万个QTableWidgetItem对象挂在内存里任何一次排序、筛选、刷新都可能成为性能瓶颈。模型视图分离后数据存储和视图渲染解耦数据量大了也能做到秒开。还有一个建议是把“文件选择—导入/导出—结果反馈”这个流程单独抽象成一个可复用模块封装好错误弹窗、日志输出、耗时统计。这样你下个项目接到类似的Excel功能时已经不是从零开发而是直接复用这套底座只改表头映射和业务校验就够了。我后来做第二、第三个项目时Excel模块几乎没怎么改动省掉了大量重复劳动。本文还有配套的精品资源点击获取