简介:一套适配 Qt6 的 QtXlsx 源码包,面向需要在 Qt6 环境下读写 Excel 文件的 C++ 开发者。原版 QtXlsxWriter 在 GitHub 上停留在 2020 年 3 月版本,直接编译常会遇到 Qt6 接口变更导致的错误;本包在此基础上针对 Qt6 逐一修改,并在 Deepin20 的 Qt6 环境编译通过,可扫清大多数迁移障碍。压缩包仅 561KB,共 187 个文件,以 67 个 cpp 源文件、47 个 h 头文件和 44 个 pro 工程文件为主,另有 14 个 qdoc 文档可辅助查找类与接口说明,并附带样式配置与示例图片;cpp 文件实现具体读写逻辑,头文件暴露类接口,pro 文件组织模块构建,整体结构完整,涵盖 document、worksheet、styles、chart 等核心模块,这些代码既可直接复用,也便于按需裁剪或二次封装。已有四百一十二位开发者学习或下载,适合希望快速获得可编译 Qt6 版 QtXlsx 的开发者;既能直接集成到项目生成和解析 .xlsx 文件,也能对照源码与配套博文逐项整理 Qt6 适配思路。
1. 弄一份能编过的 QtXlsx:Qt6 下的 Excel 读写组件
做 Qt6 桌面项目遇到导出 Excel 的需求,第一个想到的往往是 QXlsx 或者 QtXlsx。但 QtXlsxWriter 这个老牌库有个尴尬的现实:官方仓库停留在 Qt5 时代,最后一次活跃更新还在 2020 年前后。直接把这个源码拖进 Qt6 工程,编译错误能列一屏——QStringRef 没了、QRegExp 被删除、连 qrand 都被 Qt6 扫地出门。这份 QtXlsxWriter-qt6.zip 就是干这个事的:把 dbzhang800 仓库 2020-03-19 版本的源码逐一改到能在 Qt6 下编译通过,实测环境是 Deepin 20 的 Qt6。适合谁?正在用 Qt 6.x 做 C++ 桌面开发、被 Excel 导出逼疯、又不想换语言换库的人。它解决的是最现实的问题:一个能编过的 QtXlsx 源码,以及看懂它改了什么的能力。
2. 为什么 Qt6 编译不过:QtXlsx 的老代码踩了哪些 API 红线
2.1 QStringRef 的移除,这是第一个硬伤
Qt6 把 QStringRef 彻底删掉了。在 Qt5 里,QStringRef 是一个轻量级的字符串引用,用来避免不必要的拷贝。QtXlsx 源码里大量用了这个类型,比如在 xlsxworksheet.cpp 和 xlsxformat.cpp 的解析逻辑里,很多地方用 QStringRef 指向 QXmlStreamReader 读到的字符数据。到了 Qt6,这个类直接消失,编译器报错就是“QStringRef: No such file or directory”。
常规做法是替换成 QStringView。但这里有个坑:QStringView 不持有数据,它只是视图,生命周期比 QStringRef 更短,而且 QStringView 不能直接隐式转换成 QString。我在适配时对每一处 QStringRef 都做了逐行检查,确认原始数据源的存活范围,再把QStringRef改成QStringView,构造函数和.toString()的调用点同步调整。具体改法是在所有出现QStringRef的头文件和实现文件里做替换:
// 修改前(Qt5 风格) QStringRef text = streamReader.text(); QString value = text.toString(); // 修改后(Qt6 风格) QStringView text = streamReader.text(); QString value = text.toString();这里的逻辑不复杂:QXmlStreamReader::text()在 Qt6 返回的就是QStringView,所以把局部变量的类型从QStringRef改成QStringView后,后续调用.toString()的行为完全一致。但要注意,如果你的代码里出现类似QStringRef(&str, start, len)这种显式构造,改成QStringView(str).mid(start, len)不是等价的,因为QStringView::mid()返回的是视图,而原来可能期望一个持有数据的对象。我在改的时候遇到这种情况就直接截成QString,省得后续悬垂引用。
另外一个隐性问题是QStringRef的toInt()、toDouble()这类转换函数,在QStringView上是有的,但行为细节略有差异。Qt6 的QStringView::toInt(bool *ok = nullptr)不会像 Qt5 的QStringRef那样有各种重载版本,如果原代码传了base参数,需要先转QString再调用完整版转换函数。
2.2 QRegExp 被删除,正则表达式的迁移成本
Qt6 里QRegExp整个类都被移除了,替代者是QRegularExpression。QtXlsx 里 QRegExp 用得不算多,但一旦用了,改起来就麻烦。主要是在处理单元格引用、区域表达式(比如 "A1:B10" 这种)以及解析工作表名称时出现。QRegExp 的语法是 POSIX 风格,而 QRegularExpression 是 PCRE 风格,直接改类名不一定能跑。
我遇到的具体问题在解析区域引用时,QtXlsx 的 xlsxutility.cpp 里有类似这样的逻辑,用来拆分区域字符串:
// 修改前(Qt5) QRegExp rx("\\$?([A-Za-z]{1,3})\\$?(\\d+)"); rx.indexIn(str); QString col = rx.cap(1); QString row = rx.cap(2); // 修改后(Qt6) QRegularExpression rx("\\$?([A-Za-z]{1,3})\\$?(\\d+)"); QRegularExpressionMatch match = rx.match(str); QString col = match.captured(1); QString row = match.captured(2);这段改动的核心不只是 API 替换:indexIn+cap的组合换成了match+captured,匹配失败时的行为也要确认。QRegularExpression 默认不做部分匹配,如果原来依赖indexIn的返回值做判断,比如if (rx.indexIn(str) != -1),改成if (rx.match(str).hasMatch())才等价。另外,如果原 QRegExp 用了setCaseSensitivity,改成 QRegularExpression 时要构造QRegularExpression::CaseInsensitiveOption选项。
改完这处之后要检查所有用到QRegExp的地方,一条条找出来挨个替换。我发现至少有三类:单元格引用解析、共享字符串索引处理、条件格式规则解析。每一处都要确认正则里的转义是否在 PCRE 下语义一致。
2.3 qrand、qsrand 这类全局函数的消失
Qt6 把qrand()和qsrand()删了,替代方案是<QRandomGenerator>。QtXlsx 里用随机数的地方不多,主要是生成临时图表相关的内容。修改方式很简单:
// 修改前(Qt5) qsrand(QTime::currentTime().msec()); int randValue = qrand() % 100; // 修改后(Qt6) int randValue = QRandomGenerator::global()->bounded(100);bounded(100)生成 0-99 的随机数,和qrand() % 100行为等价。但需要注意QRandomGenerator::global()是线程安全的,每次调用会获取全局生成器,如果在一个循环里高频调用,性能略差。QtXlsx 里这种场景不多,直接全局调用就行,不用自己维护局部生成器。
2.4 QStringList 的 join 和 split 行为变化
这个问题比较隐蔽。Qt5 的QString::split()有多个重载版本,其中一个接受QString::SkipEmptyParts枚举值。Qt6 里这些枚举被移到了Qt::SplitBehavior,而且QString::SkipEmptyParts被标记为废弃。编译时不会直接报错,但会有一堆警告。更严重的是,如果代码里写了QString::SkipEmptyParts而没有包含对应头文件,某些编译器版本下会直接编译失败。
QtXlsx 的 xlsxutility.cpp 里在解析公式引用时用了 split,我全部改成Qt::SkipEmptyParts。同时要注意 Qt6 的QString::split默认行为是保留空字符串,行为和 Qt5 一致,但类型变了,确保用的是新枚举,别混着写。
2.5 源码包里的实际文件构成
这份 zip 解压后的核心文件在项目正文里列得很清楚:xlsxdocument.cpp、xlsxworkbook.cpp、xlsxworksheet.cpp、xlsxformat.cpp、xlsxstyles.cpp、xlsxchart.cpp、xlsxconditionalformatting.cpp、xlsxdrawinganchor.cpp。这些文件就是 QtXlsx 的核心实现,覆盖了文档对象、工作簿、工作表、单元格格式、样式表、图表、条件格式和绘图锚点。适配 Qt6 的修改主要就分布在这几个文件里,而不是改头文件。
我拿到压缩包后第一件事不是直接编译,而且是先用 diff 工具对比一下这些文件和原始版本的差异。建议你也这样做——这样你能清楚地知道这份源码到底改了哪里,而不是稀里糊涂编过了就以为万事大吉。后面专门用一节讲怎么读懂这份 diff,那是理解 Qt6 迁移成本的最好教材。
3. 编译与集成:在 Qt6 工程里把 QtXlsx 用起来
3.1 构建之前的环境准备
这份源码标注的验证环境是 Deepin 20 的 Qt6。Deepin 是基于 Debian 的发行版,Qt6 的安装方式和其他 Linux 发行版类似。确保你已经安装了完整的 Qt6 开发包,包括 qmake 或 cmake、Qt6 核心模块、以及 Qt6 的 XML 模块(QXmlStreamReader 依赖)。
如果你用的是 Qt6 的离线安装包安装的,注意安装时勾选 Qt Core 模块和 Qt XML 模块。有些精简安装包默认不装 XML 模块,编译时会报找不到QXmlStreamReader的头文件。这个原因很蠢,但实际遇到的人不少。我在 Deepin 20 上用的是系统包管理器装的 qt6-base-dev 和 libqt6xml6,如果你是 Ubuntu 系,类似这样的安装命令:
sudo apt update sudo apt install qt6-base-dev libqt6xml6QtXlsx 还依赖 Qt5Compat 模块,因为源码里有QTextCodec的用法。Qt6 把 QTextCodec 挪到了 Qt5Compat 模块里,如果没有这个模块,链接会报QTextCodec未定义。安装方式:
sudo apt install qt6-5compat-dev这里有个细节:qmake 工程文件里要加上QT += 5compat,否则即使装了库也链接不通过。如果你是用 CMake 构建,需要在 CMakeLists.txt 里加上find_package(Qt6 REQUIRED COMPONENTS Core Xml 5Compat)。
3.2 用 qmake 构建静态库
QtXlsx 原生提供了 qmake 工程文件,这份源码保留了 qmake 的支持。进入源码目录后,构建方式很直接:
cd QtXlsxWriter-qt6 mkdir build && cd build qmake6 ../QtXlsx.pro make -j$(nproc)整个过程如果顺利,会在build目录下生成libqxlsx.a或libqxlsx.so。但这里我要提醒几个常见问题。第一,qmake6这个命令名在有些发行版里是qmake,你需要确认你调用的是 Qt6 版本的 qmake,而不是 Qt5 的;第二,如果编译报错找不到Qt6Xml之类的模块,说明 qmake 的模块探测路径有问题,通常是没安装对应的 dev 包;第三,make -j$(nproc)会把编译线程开到和 CPU 核数一样多,如果内存紧张,建议用make -j2,实测 8G 内存下-j$(nproc)有概率 OOM。
构建成功后,把libqxlsx.a(或.so)和源码里的include目录一起拷贝到你的项目里。头文件路径在源码的src/xlsx目录下,核心头文件是xlsxdocument.h、xlsxformat.h、xlsxworkbook.h等。
3.3 用 CMake 集成到现有工程
现在 Qt6 的新项目基本都是 CMake 了。我一般不用 qmake 构建 QtXlsx 的源码,而是直接把它作为子目录或者预编译库引入。如果你想把 QtXlsx 源码编成静态库后链接,CMakeLists.txt 的关键配置是这样的:
cmake_minimum_required(VERSION 3.16) project(ExcelDemo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt6 REQUIRED COMPONENTS Core Xml 5Compat) add_library(qxlsx STATIC ${QXLSX_SOURCE_DIR}/xlsxdocument.cpp ${QXLSX_SOURCE_DIR}/xlsxworkbook.cpp ${QXLSX_SOURCE_DIR}/xlsxworksheet.cpp ${QXLSX_SOURCE_DIR}/xlsxformat.cpp ${QXLSX_SOURCE_DIR}/xlsxstyles.cpp ${QXLSX_SOURCE_DIR}/xlsxchart.cpp ${QXLSX_SOURCE_DIR}/xlsxconditionalformatting.cpp ${QXLSX_SOURCE_DIR}/xlsxdrawinganchor.cpp ${QXLSX_SOURCE_DIR}/xlsxutility.cpp ${QXLSX_SOURCE_DIR}/xlsxcell.cpp ${QXLSX_SOURCE_DIR}/xlsxcellrange.cpp ${QXLSX_SOURCE_DIR}/xlsxcolor.cpp ${QXLSX_SOURCE_DIR}/xlsxdatavalidation.cpp ${QXLSX_SOURCE_DIR}/xlsxrichstring.cpp ${QXLSX_SOURCE_DIR}/xlsxcellreference.cpp ) target_include_directories(qxlsx PUBLIC ${QXLSX_SOURCE_DIR}) target_link_libraries(qxlsx PUBLIC Qt6::Core Qt6::Xml Qt6::5Compat) add_executable(ExcelDemo main.cpp) target_link_libraries(ExcelDemo PRIVATE qxlsx)这里列出的源文件是常见的最小集合,实际编译时如果提示有未定义的符号,再对照源码目录把缺失的.cpp加进来。QtXlsx 的源文件之间有依赖关系,比如xlsxcell.cpp会引用xlsxcellrange.cpp里的实现,所以一个偷懒的办法是直接把/src/xlsx目录下所有.cpp文件都加入目标。我后来就是这么干的,省得一个个排查依赖。
3.4 第一个能跑通的写入例程
编过之后最重要的事是验证:能不能真正写出一个 Excel 文件。我建议新建一个最小工程,只做一件事——创建一个 xlsx 文件,写入几行数据,然后用 Excel 或 WPS 打开验证。下面这个例程是完整可编译的,直接放在main.cpp里:
#include <QGuiApplication> #include <QDebug> #include "xlsxdocument.h" #include "xlsxformat.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QXlsx::Document xlsx; QXlsx::Format titleFormat; titleFormat.setFontSize(14); titleFormat.setFontBold(true); // 写入标题行:第 1 行,A、B、C 三列 xlsx.write("A1", "产品名称", titleFormat); xlsx.write("B1", "销量", titleFormat); xlsx.write("C1", "单价", titleFormat); // 写入数据行 xlsx.write("A2", "机械键盘"); xlsx.write("B2", 120); xlsx.write("C2", 399.0); xlsx.write("A3", "游戏鼠标"); xlsx.write("B3", 86); xlsx.write("C3", 199.0); // 保存到当前目录 if (xlsx.saveAs("demo.xlsx")) { qDebug() << "写入成功: demo.xlsx"; } else { qDebug() << "写入失败"; return 1; } return 0; }这个例程里有几个参数需要说明。QXlsx::Document是无参构造的空白文档,在内存中维护一张默认的 sheet,命名为 "Sheet1"。write()方法的第一个参数是单元格引用("A1" 这种),第二个是值,第三个是可选格式。值的类型支持 QString、int、double 和 QDateTime,内部会做类型推断并写入对应的 Excel 单元格类型。saveAs()的参数是输出文件路径,注意路径里如果包含中文,在 Linux 下要确保文件名编码正确,否则保存后文件名可能是乱码。
编译这个例程,如果之前已经把 qxlsx 编成了静态库:
g++ main.cpp -o demo -I/path/to/qxlsx/src \ -L/path/to/build -lqxlsx \ $(pkg-config --cflags --libs Qt6Core Qt6Xml Qt65Compat)更省心的方式还是用 CMake,把上面那个最小工程建出来,target_link_libraries加上 qxlsx,直接cmake --build完事。运行生成的demo可执行文件,当前目录下出现demo.xlsx就是成了。
3.5 深色模式下的一个细节:单元格样式写入顺序
QtXlsx 写入样式时有一个细节和小坑。样式是通过QXlsx::Format对象设置的,但写入单元格时格式是拷贝到文档内部的样式表里。如果你复用了同一个Format对象,修改它的某个属性再写入另一个单元格,前一个单元格的样式不会跟着变,因为写入时做的是值拷贝。这其实是正常行为,但容易让人误以为 Format 是引用语义。
我在做报表时喜欢把表头、正文、合计行各建一个 Format 对象,避免混用。这算是一个小习惯,但它能省掉不少排查样式错乱的调试时间。
4. 常用 API 与参数细节:从写入到读取的完整闭环
4.1 单元格写入的类型映射与边界
QtXlsx 的write()方法有两个重载版本,一个是传入单元格坐标字符串("A1"),一个是传入行列号(行从 1 开始,列从 1 开始)。如果你用write(1, 1, "内容"),其实等价于write("A1", "内容")。源码里内部会通过XlsxCellReference把坐标字符串转换成行列号,这一转换逻辑在xlsxcellreference.cpp里,适配 Qt6 时这个地方的正则替换就是前面提到的 QRegularExpression 迁移点之一。
写入日期时间时有个容易踩的坑:直接写入QDateTime对象,QtXlsx 会把它转成 Excel 的序列号。但如果你写入的 QDateTime 带有时区信息,转换结果可能和你本地看到的 Excel 值不一致。我一般建议写入前先统一转成QDateTime::currentDateTime().toUTC(),或者写入字符串。例如:
#include <QDateTime> QXlsx::Format dateFormat; dateFormat.setNumberFormat("yyyy-mm-dd"); QDateTime dt = QDateTime::currentDateTime(); xlsx.write("A1", dt, dateFormat); // 带本地时区如果你想统一格式,用字符串写入反而是可控的:
xlsx.write("A1", dt.toString("yyyy-MM-dd HH:mm:ss"));这种方式写入的是文本,不是日期类型。Excel 里排序、筛选没问题,但无法参与日期运算。具体怎么取舍看业务需求,我的习惯是:如果要对日期做计算就传 QDateTime,如果只是展示就写字符串。
4.2 合并单元格与列宽设置
报表场景里合并单元格是高频操作。QtXlsx 提供了mergeCells()方法,参数是区域字符串:
xlsx.mergeCells("A1:F1"); // 合并第一行 A 到 F xlsx.write("A1", "季度销售汇总", titleFormat);这里要注意:合并操作必须在写入内容之前或之后都行,QgXlsx 内部会在写入时检查单元格是否属于某个合并区域。如果先写内容再合并,内容会保留在区域左上角的单元格。如果在合并区域内的非左上角单元格写入内容,QtXlsx 的行为是忽略这次写入还是抛异常,取决于版本实现——实测这一版的源码是静默忽略,不报错不崩溃,数据会丢。排查起来很费劲,所以我建议你在业务逻辑里先保证不往合并区域的非锚点单元格写数据。
列宽设置在 QtXlsx 里是通过setColumnWidth()方法。注意它的单位不是像素,而是 Excel 的字符宽度单位。默认列宽是 8.43 个字符左右,设置中文内容时,一个中文字符相当于两个英文字符宽度。我的经验值是:中文文本按长度乘以 2.2 再加 2 做缓冲,英文数字按长度加 2:
xlsx.setColumnWidth(1, 20); // 第 1 列宽 20 字符 xlsx.setColumnWidth(2, 12); xlsx.setColumnWidth(3, 12);如果列宽不生效,检查是否在写入数据之后又调用了setColumnWidth。QtXlsx 内部把列宽存到工作表的列属性里,如果单元格数据先写入再设列宽,理论上也生效,但如果同一个单元格被重复写入,列设置可能在内部被覆盖掉。遇到列宽莫名其妙变回默认的情况,把setColumnWidth移到saveAs()之前最后一次调用。
4.3 读取已有 Excel 文件的常用模式
QtXlsx 支持读取,但读取能力比写入弱一些,尤其是在条件格式、图表这类复杂对象上会有信息丢失。读取操作的基本流程是:
QXlsx::Document doc("existing.xlsx"); if (!doc.load()) { qDebug() << "加载失败"; return 1; } // 读取 A1 单元格的值 QVariant val = doc.read("A1"); if (val.isValid()) { qDebug() << "A1:" << val.toString(); } // 遍历某个区域 int rowCount = doc.dimension().rowCount(); int colCount = doc.dimension().columnCount(); for (int row = 1; row <= rowCount; ++row) { for (int col = 1; col <= colCount; ++col) { QVariant cellVal = doc.read(row, col); if (cellVal.isValid()) { qDebug() << row << "," << col << ":" << cellVal.toString(); } } }dimension()返回的是文档的实际数据区域。但这里有一个坑:如果某个单元格被设置过格式但没填值,dimension()的行列数可能比实际数据大。判断单元格是否真正有值,可以用read()返回的 QVariant 是否isValid()。但注意,写入空字符串的单元格返回的 QVariant 是合法的空字符串,和没有写入的单元格不同。这在遍历时会导致误判,需要结合业务决定。
读取性能方面,如果大文件(几千行),read()单格读取有可感知的延迟。更好的方式是先用doc.worksheetAt(0)拿到 worksheet,再用内部接口批量读取,但这对普通业务场景来说优化意义不大,几千行的报表几百毫秒能读完。
4.4 处理合并区域与单元格样式读取的坑
读取时如果遇到合并单元格,你会发现在非锚点位置read()返回空 QVariant。这是 QtXlsx 的读取限制,它不会像 Excel 那样自动返回合并区域的值。解决方法是读之前先获取合并区域列表:
QList<QXlsx::CellRange> mergedCells = doc.mergedCells(); for (const QXlsx::CellRange &range : mergedCells) { qDebug() << "合并区域:" << range.toString(); }拿到合并区域列表后,再对区域内所有单元格做映射,统一返回锚点的值。这是一个很实用的补丁思路,我在做表格逆向解析时就是这么处理的。如果你想读单元格的背景色、字体这些样式信息,QtXlsx 的read()只返回值,不返回格式。格式要通过低层接口cellAt()拿到QXlsx::Cell对象再取格式,但这部分源码在 Qt6 适配后表现如何,我没有在复杂场景下做过全面验证,建议不要依赖太深的格式读取逻辑。
5. 避坑指南:我在 Qt6 适配和编译中遇到的五个真实问题
5.1 编译时报错找不到 QTextCodec
现象:编译 xlsxdocument.cpp 时,报错QTextCodec: No such file or directory,或者链接时报QTextCodec::codecForName未定义引用。
原因:Qt6 把 QTextCodec 从 QtCore 模块移到了 Qt5Compat 模块。QtXlsx 源码在读取和写入 CSV、处理编码时用到了 QTextCodec,但没有在工程文件里为 Qt6 声明对应模块。
解决:qmake 项目文件里加QT += 5compat,CMake 里调用find_package(Qt6 REQUIRED COMPONENTS 5Compat)并链接Qt6::5Compat。安装对应的系统包,Ubuntu/Debian 系是libqt6-5compat-dev。
5.2 QStringRef 替换成 QStringView 之后出现数据错乱
现象:编译通过后,写入 Excel 时某些中文字段变成乱码,或者在某些单元格里内容丢失。
原因:花时间排查看代码,我的结论是——在某个解析循环里,原来的 QStringRef 指向的数据源是循环体内的临时 QString,改成 QStringView 后视图引用的临时对象在循环迭代结束时已经被销毁,数据悬垂。
解决:这类场景不要用 QStringView,直接改成 QString 局部变量。QStringView适合引用的生命周期明显长于视图的场景,比如引用一个成员变量。在解析循环里,稳妥第一:
// 不建议:临时对象转视图 QStringView text = streamReader.text(); // streamReader.text() 返回临时 QStringView // 稳妥做法:先转成 QString QString text = streamReader.text().toString();从那以后我处理 Qt6 迁移时定了一条规矩:凡是原代码用QStringRef的地方,逐行确认引用源的存活范围,拿不准就直接用QString,不做无谓的性能优化。
5.3 qmake6 和 qmake 版本混乱导致编译的是 Qt5 模块
现象:运行时崩溃,或者在编译时出现大量 “Qt5 和 Qt6 头文件混用” 的错误,比如#error "Qt 6 requires C++17",但代码明明写的 C++17。
原因:系统同时装了 Qt5 和 Qt6,qmake命令指向 Qt5 的 qmake,用这个 qmake 解析工程文件后,整个构建链用的是 Qt5 模块。
解决:确认qmake --version输出版本号。如果输出的是 qmake version 3.1 且包含 Qt 5.15 字样,说明是 Qt5 的 qmake,应该改用qmake6。或者直接用 CMake,因为 CMake 的find_package(Qt6)不会产生版本混淆。也可以用which qmake看路径,Ubuntu 上 Qt5 的 qmake 通常在/usr/lib/qt5/bin/qmake,Qt6 的在/usr/lib/qt6/bin/qmake6。
5.4 生成的 xlsx 文件 Excel 打不开,提示文件损坏
现象:程序运行成功,demo.xlsx文件存在,但用 Excel 或 WPS 打开时提示文件损坏。用文本编辑器打开能看到 XML 内容但不完整。
原因:这个问题的典型原因是保存路径写错,QtXlsx 在写入时如果目标目录没有写权限,会返回失败。但有一种隐蔽的情况:saveAs()传入的是相对路径,而程序的工作目录不是你预期的目录。文件可能被写到了别的位置,你打开的是一个被部分写入的临时文件。
解决:保存时用绝对路径,并在saveAs()之后检查返回值。深入排查时,把保存路径也打印出来。如果你用QGuiApplication而非QCoreApplication,在某些 Linux 桌面上工作目录可能是家目录,这会导致路径判断失误。
5.5 读取包含图片或图表的 xlsx 文件时崩溃
现象:用本库读取一个包含图片或图表的 xlsx 文件,程序直接崩溃或崩溃,但读取纯数据文件没有问题。
原因:QtXlsx 对 OOXML 的图片绘制和图表锚点解析支持不完整。源码里的xlsxdrawinganchor.cpp和xlsxchart.cpp在处理复杂锚点时,缺少对某些 XML 元素的处理分支。Qt6 适配过程中这些部分的改动较少,保留了原版的局限。
解决:如果读取的 Excel 文件可能包含图片,读取前做一次文件级检查,或者用unzip -l看压缩包里是否有xl/media/目录——有说明含图片。对这种文件不要用 QtXlsx 读,可以先用 LibreOffice 转换成纯数据格式,再读取。这是一个可行的绕行方案。
6. 进阶玩法:把这份适配源码变成你迁移旧库的教材
6.1 用 diff 看清 Qt5 到 Qt6 的全部修改点
这份源码最有价值的部分不是能编译通过,而是那些修改痕迹。拿原始的 QtXlsxWriter 源码和这份适配后的源码做一次 diff,你得到的就是一份浓缩的 Qt5-to-Qt6 迁移清单。具体做法:
git clone https://github.com/dbzhang800/QtXlsxWriter.git origin_src diff -ru origin_src/src/xlsx QtXlsxWriter-qt6/src/xlsx > qt6_migration.patch然后逐个查看 diff 里的 hunk。我建议你重点关注三类修改:QStringRef相关的改动、QRegExp相关的改动、以及QTextCodec相关的改动。这三类几乎覆盖了大多数老 Qt 库从 5 迁到 6 时会遇到的全部典型问题。把这几类修改变成你自己的模式识别能力,遇到其他老库时就知道去哪里查、怎么改。
6.2 从单个库的适配中提炼迁移方法论
我的做法是把整个 diff 按模式分类,整理成一个清单,每次迁移一个老库时就拿这套清单逐项排查。第一类是字符串类:QStringRef 是否被使用,QString 的隐含共享变化是否影响性能假设;第二类是正则类:QRegExp 出现的位置和改写方式;第三类是编码处理:QTextCodec 的使用点;第四类是随机数和全局函数的替换;第五类是模块拆分:QDesktopServices 是否从 QtCore 挪到了 QtGui,QTextCodec 是否挪到了 Qt5Compat。
这套方法论不依赖具体库,是一种可复用的迁移技能。对 QtXlsx 来说,这份源码把你需要处理的问题全部暴露了一遍,改过一遍之后,再去迁移其他 Qt5 库,你会更有底气。
6.3 数据验证:写入后用程序自动校验
已经编译通过并生成了 demo.xlsx,怎么验证内容是对的?除了用 Excel 打开肉眼检查,我建议你在程序里做一轮自校验——保存后用 QtXlsx 重新读取,并断言关键单元格的值:
QXlsx::Document verify("demo.xlsx"); if (!verify.load()) { qDebug() << "校验失败: 文件无法加载"; return 1; } QString name = verify.read("A2").toString(); int count = verify.read("B2").toInt(); double price = verify.read("C2").toDouble(); if (name == "机械键盘" && count == 120 && price == 399.0) { qDebug() << "校验通过"; } else { qDebug() << "校验失败: 数据不一致"; }这轮自校验不只是验证写入正确,也是在验证读取路径没问题。后续在真实业务里,这份代码既是写入模块又是读取模块,写读自洽非常重要。从那以后,每次改完 QtXlsx 相关的代码,我都强制走一遍写入→读取→断言的流程,省下了不少在 Excel 界面里手工翻数据的力气。希望帮到你。
本文还有配套的精品资源,点击获取