ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

QT5 Linux下读写2003 Excel:Excelformat源码修改与中文路径实践

QT5 Linux下读写2003 Excel:Excelformat源码修改与中文路径实践

简介:在Linux环境下用Qt5读写Excel 2003(xls)文件时,中文路径是许多开发者的痛点,日文、韩文等非英文路径也同理,根源往往出在wcstombs和mbstowcs两个转换函数对宽字符长度的错误计算上。这份ExcelFormat第三方库的修改版源码瞄准此问题,在源文件cpp中create和open函数内增加条件编译宏“NON_ENGLISH_PATH_FILENAME_SUPPORT”,将文件名长度按双倍字节计算后传入转换函数,从而成功支持中文、日文、韩文路径,且不影响原有表格字体、边框、对齐等样式设置能力。压缩包总大小2.61MB,包含21个文件,以C++源码、Qt工程文件、Makefile、编译中间文件为主,并附带了演示用Demo工程,包含主窗口界面和可运行样例,便于直接编译体验。资源已在统信UOS系统、aarch64和mips64架构上用gcc编译验证通过,对国产Linux发行版上的Qt项目集成Excel读写功能具有直接参考价值。目前已有392人学习下载,适合在Linux下开发办公类应用或数据导入导出功能的Qt工程师参考移植。

1. 在QT5 for Linux里处理2003的excel文件:为什么绕不开Excelformat源码修改版

当你需要在一个 QT5 for Linux 的桌面或服务端程序里支持中文路径读写 2003 的 excel 文件(也就是 .xls)时,Qt 自带的类库基本帮不上忙:QAxObject 依赖 Windows COM,QtXlsx 只支持 xlsx,剩下的只有第三方库。而 Excelformat 这个老牌 C++ 库因为代码量小、能读能写、许可证宽松,成了接入 .xls 最常被选的对象;但它在 Linux 下的中文路径、wchar_t 宽度和文件流处理上有一堆历史遗留问题,直接编译基本过不去,直接跑中文路径必翻车。本文把我改过的这套源码修改版的思路、改动位置、CMake 集成方式和验证清单完整写出来,适合正在接政务、工业、运维报表这类还依赖 .xls 场景的 Qt 工程师照着复现。

2. 把Excelformat源码先拆开看:2003的excel格式和两条读写链路

2.1 为什么最终选了Excelformat而不是libxls、libxl或者QXlsx

在 Linux 下做一个 .xls 的读写模块,常见候选有四类:libxls、libxl、QXlsx 和 Excelformat。libxls 本身读 .xls 做得不错,但写能力基本为零,你仍然要再搭一个写 .xls 的组件;libxl 读写都成熟,可它是闭源商业库,授权费按人头算,很多项目卡在采购流程上;QXlsx 只处理 xlsx,2003 的 .xls 完全不沾边。Excelformat 是唯一一个开源、C++ 编写、读写 .xls 两条腿都有的老库,SourceForge 上还留在 2016 年前后的版本,代码老但五脏俱全,正因为老,它给了我们改源码的空间。

实际工程里我见过有人同时挂三个库:libxls 读、Excelformat 写、再加一层自己写的格式转换,最后维护成本极高。我的习惯是只留 Excelformat,把读的能力补到能用为止,这样 CMake 里只多一个静态库,头文件也干净。做这个选择前先量化一下自己的需求:如果只需要把界面表格导出一个 2003 的 excel 文件,Excelformat 是性价比最高的;如果还要解析用户上传的、来源五花八门的 .xls,那必须先在读的路径上做充分测试,这个库的读取器对一些不规范的 BIFF8 记录处理得很脆。

2.2 2003的excel文件到底是什么:CFB容器加BIFF8记录

改这套源码之前,至少要弄明白 .xls 在磁盘上长什么样。一个标准的 Excel 97-2003 文件是两层结构:外层是 OLE 复合文档容器(CFB),里面是一个叫做 Workbook 的流;内层才是真正存数据的 BIFF8 记录流。CFB 负责把多个流组织在一个文件里,所以你可以用file命令看到它标记为 Composite Document File;BIFF8 记录则是连续排列的二进制块,每条记录以两个字节的 opcode 开头,跟着两个字节的长度字段和长度对应的数据。

BIFF8 记录类型很多,但对读写最关键的就那么几个:BOF/EOF 标记工作簿边界、SST 存共享字符串表、LABELSST 引用 SST 里的字符串索引、NUMBER 存浮点数、FORMULA 存公式结果。Excelformat 源码里的 ExcelFormat 部分就是在做这些记录的编码和解码,BasicExcel 部分做的是 CFB 层和对外 API。理解这个结构对排查很重要:中文乱码和路径无关时,问题多半出在 SST 或者 LABELSST 的索引对不上;文件打不开时,往往在 CFB 层的目录扇区解析上。

2.3 从源码里找读写两条链路的入口:先画清楚再动手

我拿到一套老库源码,第一步不是急着改,而是把它的调用链在纸上画出来。以 Excelformat 最常见的接口为例,写文件的调用链是:BasicExcel::New创建空工作簿,Worksheet(idx)拿到指定工作表,Cell(row, col)拿单元格指针,然后调SetString/SetWString/SetDouble写入内容,最后SaveAs(filename)把内存里组好的整个 BIFF8 记录块交给 CFB 封装并落盘。读文件则是Load(filename)先解析 CFB 目录,取出 Workbook 流,再逐条跳读 BIFF8 记录,遇到 SST 就加载共享字符串,遇到 LABELSST 就按索引取字符串,最后把值填回给Cell(row, col)由外部读取。

中文路径的问题恰恰卡在两条链路的端口上:Load和SaveAs的 filename 参数在 Windows 版源码里是按窄字符或 TCHAR 处理的,移植到 Linux 后它默认把入参当作 ASCII,中文路径一旦传进来,文件流打不开,或者打开了但 CFB 内部记录定位出错。真正要修改的不是 BIFF8 解析逻辑,而是文件打开层、路径编码层和 wchar_t 适配层。

3. 源码修改版的核心改动:文件层、编码层、编译兼容三件事

3.1 改动主线:只动 IO 与编码,不碰 BIFF8 解析

这套源码修改版能否稳定的关键,是控制改动面。BIFF8 记录解析逻辑是经过大量测试的,乱动会引入玄学问题;而文件打开、路径转换和 wchar_t 处理这三层在 Linux 下是确定会出问题的,改动收益最高。我先明确三条主线:第一条,把所有文件打开入口统一收口到 Qt 的 QFile / QSaveFile,让路径编码交给 Qt 的本地编码机制;第二条,文件流强制二进制模式,堵住老库在读写时可能使用文本模式的隐患;第三条,写一层 wchar_t 与 QString 的双向转换,屏蔽 Linux 和 Windows 在 wchar_t 位宽上的差异。

定位改动点时,在源码里搜几个关键词能快速收敛:文件名参数出现的地方、fopen/_wfopen/ifstream打开文件的位置、MultiByteToWideChar这类 Windows API 调用点、以及所有直接按字节长度操作 wchar_t 的代码。老库在这几处通常写得比较随意,我见过的一个版本甚至把 wchar_t* 用strlen求长度,在 Linux 的 4 字节 wchar_t 下直接多读一倍数据。

3.2 修改点A:Load入口把文件交给QFile,路径编码走本地规则

第一处必须改的是BasicExcel::Load的文件打开方式。常见的老代码是用fopen(filename, "rb")或者ifstream(filename, ios::binary)直接打开,这在 Linux 的 UTF-8 路径下其实大多能通,但一旦 locale 被设置成 C/POSIX,或者文件名里带空格和中文混排,就会偶发失败。我一般会让 Load 先走一遍QFile::decodeName把外部传入的字节路径还原成 QString,再用 QFile 读成 QByteArray,最后把这段内存数据交给原来的 BIFF8 解析流程。

// BasicExcel.cpp 中 Load 入口的修改版 bool BasicExcel::Load(const char* path) { if (!path || !*path) { return false; } #ifdef Q_OS_LINUX // Linux 下路径的字节编码就是 UTF-8,QFile::decodeName 按本地 8 位编码还原 const QString nativePath = QFile::decodeName(path); if (nativePath.isEmpty()) { return false; } QFile file(nativePath); if (!file.exists() || !file.open(QIODevice::ReadOnly)) { return false; } const QByteArray raw = file.readAll(); file.close(); if (raw.isEmpty()) { return false; } // 后续所有 BIFF8 解析逻辑不变,只把“文件流读取”替换成“内存块读取” return readFromMemory(reinterpret_cast<const unsigned char*>(raw.constData()), static_cast<size_t>(raw.size())); #else // Windows / macOS 沿用原实现 // ... #endif }

这段代码里有两个参数值得注意。file.readAll()会把整个 .xls 一次性读进内存,对于 2003 格式单文件常见几 MB 的量级完全没问题,但你如果被要求处理上百 MB 的超大 Excel,则需要改成read()分块喂给解析层。readFromMemory是我在原库基础上抽出来的一个静态函数,内部不做任何格式转换,只是把原来从 ifstream 里read字节的逻辑替换成了从指针读。这样 Load 的改动对后续解析完全透明。

3.3 修改点B:SaveAs换成QSaveFile,顺带解决半截文件问题

写文件的入口同样要收口。Linux 下如果沿用老库的ofstream(filename, ios::out),一旦漏写ios::binary,遇到 0x0A 字节就可能被做换行转换,整个 BIFF8 结构被破坏;更稳妥的做法是换成QSaveFile,它先把数据写到临时文件,最后commit()时原子替换目标文件,写一半断电也不会留下一个损坏的 .xls。

// BasicExcel.cpp 中 SaveAs 入口的修改版 bool BasicExcel::SaveAs(const char* path) { if (!path || !*path) { return false; } #ifdef Q_OS_LINUX const QString nativePath = QFile::decodeName(path); QSaveFile outFile(nativePath); if (!outFile.open(QIODevice::WriteOnly)) { return false; } // m_data 是内存里已经组好的整块 CFB + BIFF8 数据 qint64 written = outFile.write(m_data.constData(), m_data.size()); if (written != static_cast<qint64>(m_data.size())) { outFile.cancelWriting(); return false; } return outFile.commit(); #else // Windows / macOS 沿用原实现 // ... #endif }

这里有个容易踩的细节:QSaveFile::open()只是创建临时文件,如果目标文件的父目录不存在,open 会直接失败。调用方需要先保证目录存在,我一般会在save()之前用QDir().mkpath(QFileInfo(path).absolutePath())兜底。另外commit()失败时不能直接返回成功,务必在write返回值不等于数据长度时调用cancelWriting(),否则残留的临时文件会一直堆在目录里。

3.4 修改点C:wchar_t位宽差异的适配,这是中文乱码的真正来源

在 Linux 上,wchar_t 是 4 字节;在 Windows 上,wchar_t 是 2 字节。Excelformat 内部从 BIFF8 读取字符串时,会把 UTF-16LE 的字节序列转换成 wchar_t 数组交给上层;写出时反向转换。如果不做适配,代码里所有len * sizeof(wchar_t)的计算在 Linux 下都会出错,轻则读出的字符串后面带一堆空字符,重则越界崩溃。我的做法是封装两个静态函数,把转换集中到一处,外部统一使用 QString,源码里其他位置不再直接碰 wchar_t。

对于读取方向,把 wchar_t 数组按 UTF-32 语义交给QString::fromUtf32就行。因为 Linux 的 wchar_t 宽度正好是 32 位,这既不需要逐字符转码,也不会丢数据。

对于写入方向,先把 QString 转成 UTF-32 的数组,再逐个放进 wchar_t。注意这里不能用text.toStdWString()直接返回,因为标准库的 wstring 在 Linux 上底层就是 32 位字符,但它不保证与 Excelformat 内部期望的 UTF-16LE 长度一致,转换必须先统一到 UTF-32 再落到 wchar_t 中。

// WinCompat.h:统一 wchar_t 与 QString 的双向转换 #include <QString> #include <QVector> #include <string> static QString FromBiffWString(const wchar_t* src, size_t len) { if (!src || len == 0) { return QString(); } #ifdef Q_OS_LINUX // Linux: wchar_t 是 32 位,按 UTF-32 构建 QString return QString::fromUtf32(reinterpret_cast<const uint*>(src), int(len)); #else return QString::fromWCharArray(src, int(len)); #endif } static std::wstring ToBiffWString(const QString& text) { if (text.isEmpty()) { return std::wstring(); } #ifdef Q_OS_LINUX const QVector<uint> ucs4 = text.toUcs4(); std::wstring result; result.resize(ucs4.size()); for (int i = 0; i < ucs4.size(); ++i) { result[i] = static_cast<wchar_t>(ucs4[i]); } return result; #else return text.toStdWString(); #endif }

这个适配层改了之后,只需要保证所有单元格字符串的读写都走这两个函数,中文内容就不会再乱码。我之前遇到过一种情况:路径用 QString 处理好了,文件也打开了,但单元格里写进去的中文在 LibreOffice 里看是乱码,查到最后是调用SetString而不是SetWString,导致文本被按单字节 Latin-1 处理。所以调用侧也要遵守一条规则:中文必须走 WString 系列接口,不能用 SetString。

3.5 编译兼容:Linux下要补的宏和头文件

老库在 Windows 上编译没问题的代码,在 Linux 下会先死在一批 Windows CRT 函数上。最常见的是strcmpi、_tcslen、_tcsicmp这些,Linux 的 libc 没有这个命名。我一般会加一个PlatformCompat.h,在#ifdef Q_OS_LINUX分支里做宏映射,而不是去逐行改源码。

// PlatformCompat.h #ifndef PLATFORM_COMPAT_H #define PLATFORM_COMPAT_H #include <strings.h> #include <string.h> #ifdef Q_OS_LINUX #define _tcslen strlen #define _tcsicmp strcasecmp #define _tcsncpy strncpy #define _tcsnicmp strncasecmp #define strcmpi strcasecmp #define stricmp strcasecmp #endif #endif

同时,在编译 Excelformat 库的 CMake 配置里,我建议加上-fno-strict-aliasing,因为这个库老到用了大量reinterpret_cast和类型双关,高优化等级下容易被编译器优化出怪问题。把这层兼容头文件 include 进 BasicExcel.cpp 和 ExcelFormat.cpp 的最前面,比在 IDE 里改全局定义要可移植得多。

4. 在QT5工程里集成修改版:CMake构建和两个封装类

4.1 把修改版编译成独立静态库,避免污染主工程

集成时我不会把 Excelformat 的 .cpp 直接丢进主工程,而是单独编一个静态库。这样有几个优点:老库的警告噪音不会刷屏主工程;后续机器迁移时只需要带上这个 .a 文件;编译参数可以单独控制,不影响 Qt 业务代码的编译标准。CMake 里的一段配置如下。

cmake_minimum_required(VERSION 3.16) project(xls2003_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets) # 修改版 Excelformat 静态库 add_library(excelformat_mod STATIC third_party/excelformat/BasicExcel.cpp third_party/excelformat/ExcelFormat.cpp third_party/excelformat/PlatformCompat.cpp ) target_include_directories(excelformat_mod PUBLIC third_party/excelformat ) target_compile_definitions(excelformat_mod PRIVATE NO_OLDNAMES=1 ) target_compile_options(excelformat_mod PRIVATE -fno-strict-aliasing -w ) # 主程序 add_executable(demo main.cpp Xls2003Writer.cpp ) target_link_libraries(demo PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets excelformat_mod )

几个参数值得解释。NO_OLDNAMES=1是 Windows 系代码常见的开关,让老库不要依赖微软风格的旧命名,在 Linux 下可以减少一些头文件冲突;-fno-strict-aliasing前面说过了,是给老代码的保守选项;-w是关掉警告,因为老库的警告实在太多,留着会影响真正错误的排查。如果你有洁癖,可以去掉-w但要做好被几百条 warning 刷屏的心理准备。

4.2 封装Xls2003Writer:对外只暴露四个方法

直接在主工程里调 BasicExcel 的 API 会很痛苦,尤其是老库的命名和返回值不够直观。我习惯做一层薄封装,让它只暴露四个方法:创建文件、设置工作表名、写单元格、保存。其他细节全部收进实现里。

// Xls2003Writer.h #pragma once #include <QString> class BasicExcel; class Xls2003Writer { public: Xls2003Writer(); ~Xls2003Writer(); bool create(const QString& filePath, int sheetCount = 1); void setSheetName(int sheetIndex, const QString& sheetName); void setCell(int sheetIndex, int row, int col, const QString& value); bool save(); private: BasicExcel* m_excel; QString m_filePath; };

实现时有一条需要特别留意:改工作表名之前要先确认当前工作表的 Status 满足库的要求,不同小版本的枚举名不一样,但基本都要先解除锁定再改名。否则 SetSheetName 会被静默忽略,生成的文件里工作表仍叫 Sheet1,用户在 Excel 里手动改能成功,但程序改不生效。我在代码里统一先调一个unlockSheet的内部方法,再设置名称。

// Xls2003Writer.cpp 关键实现 bool Xls2003Writer::create(const QString& filePath, int sheetCount) { if (m_excel) { delete m_excel; m_excel = nullptr; } m_excel = new BasicExcel(); m_excel->New(sheetCount); m_filePath = filePath; return true; } void Xls2003Writer::setCell(int sheetIndex, int row, int col, const QString& value) { BasicExcel::Worksheet* ws = m_excel->Worksheet(sheetIndex); if (!ws) return; // 注意:老库的接口在不同版本里叫 Cell(row,col) 或 GetCell(row,col) // 编译报错时把下面的调用换成对应版本即可 BasicExcel::Cell* cell = ws->Cell(row, col); if (!cell) return; const std::wstring wstr = ToBiffWString(value); cell->SetWString(wstr.c_str(), static_cast<int>(wstr.size())); } bool Xls2003Writer::save() { if (!m_excel) return false; return m_excel->SaveAs(m_filePath.toStdString().c_str()); }

这里最关键的参数是SetWString的第二个参数。它接收的是字符个数,不是字节数;在 Linux 下如果用wcslen * sizeof(wchar_t)传进去,长度会翻四倍,后续的 BIFF8 记录长度字段就会全部错位,生成的文件打不开。ToBiffWString返回的 wstring 用wstr.size()取的是字符个数,正好符合要求。

4.3 封装Xls2003Reader:读取单元格值并转回QString

读取方向也要做一层封装,核心是把 BasicExcel 的 Cell 类型翻译成 QString。读取时同样只走 WString 接口,不要用 GetString,因为那套接口在 Linux 的 wchar_t 下不可靠。

// Xls2003Reader.cpp 关键实现 QString Xls2003Reader::cellText(int sheetIndex, int row, int col) { BasicExcel::Worksheet* ws = m_excel->Worksheet(sheetIndex); if (!ws) return QString(); BasicExcel::Cell* cell = ws->Cell(row, col); if (!cell) return QString(); if (cell->Type() == BasicExcel::Cell::WSTRING) { const wchar_t* text = cell->GetWString(); int len = cell->GetWStringLength(); return FromBiffWString(text, static_cast<size_t>(len)); } if (cell->Type() == BasicExcel::Cell::STRING) { const char* text = cell->GetString(); int len = cell->GetStringLength(); return QString::fromUtf8(text, len); } if (cell->Type() == BasicExcel::Cell::DOUBLE) { return QString::number(cell->GetDouble(), 'g', 15); } return QString(); }

判断单元格类型再取值这一步不能省。老库的枚举在不同版本里叫法略有差异,但基本都有 STRING、WSTRING、DOUBLE 这几个分支。数字类型用'g', 15格式化是为了避免把 123456789012345 这种 15 位以上数字变成科学计数法或丢精度,如果你对精度要求更高,可以改成'f', 2之类的固定小数位。

4.4 一个带中文路径的最小调用示例

把上面两段串起来,就是一个可以直接跑的示例。这个例子完整复现了标题里的需求:路径是中文目录加中文文件名,工作表名是中文,单元格内容也是中文。

// main.cpp #include <QCoreApplication> #include <QDir> #include <QFileInfo> #include "Xls2003Writer.h" int main(int argc, char* argv[]) { QCoreApplication app(argc, argv); const QString outPath = QStringLiteral("/home/张三/报表/2024年度-设备台账.xls"); QDir().mkpath(QFileInfo(outPath).absolutePath()); Xls2003Writer writer; if (!writer.create(outPath, 2)) { return 1; } writer.setSheetName(0, QStringLiteral("设备清单")); writer.setCell(0, 0, 0, QStringLiteral("设备编号")); writer.setCell(0, 0, 1, QStringLiteral("设备名称")); writer.setCell(0, 1, 0, QStringLiteral("E-1001")); writer.setCell(0, 1, 1, QStringLiteral("循环水泵")); if (!writer.save()) { return 2; } return 0; }

这个示例里的mkpath是在 SaveAs 之前主动创建目录,因为我前面说过 QSaveFile 不会自动创建父目录;如果这个动作忘了做,中文路径的报错往往不是“路径不存在”,而是 open 返回 false,排查起来很迷惑。跑通这段之后,再往 Writer 里加样式、合并单元格、列宽这些能力时,方向就清晰了。

5. 中文路径读写2003的excel常见故障:五个坑的定位与修复

5.1 定位工具先行:编译过但打不开,先用strace看文件层

在逐个列坑之前,先说一个通用排查手段。中文路径问题有时发生在库解析之前,程序和文件系统之间就断了,这时靠打日志效率很低,直接上 strace 看系统调用最干净。命令很简单:

strace -e trace=openat,read,close -o open_trace.log ./demo

然后进日志里搜中文路径,看 openat 的返回码。如果返回 ENOENT 或 EACCES,说明路径本身或权限有问题,问题不在 Excelformat;如果 openat 成功但后续 read 返回长度为 0,再回来查代码里的文件打开参数。用这个命令把问题定位到“文件层之上”还是“文件层之下”,能省下大量试错时间。这也是我在 Linux 上做任何文件兼容问题时第一个要跑的命令。

5.2 编译报错:strcmpi未定义,老库的Windows函数残留

现象:在 Linux 下编译 BasicExcel.cpp 或 ExcelFormat.cpp 时,报strcmpi或_tcsicmp未定义,有时还伴随__int64未声明。

原因:这套源码最初是按 Windows CRT 写的,Linux 的 libc 不提供这些非标准命名。strcmpi在 Windows 里是大小写不敏感字符串比较,Linux 等价的函数是strcasecmp。

解决:不要全局去改源码里的每个调用点,用我在第 3 章写的 PlatformCompat.h 统一映射,在 include 老库源码前先 include 它。如果你不想额外加文件,也可以在 CMake 里直接定义宏映射,但可读性差很多。改完再编译,这类报错会一次性清掉。

5.3 路径编码:中文目录能建文件,但程序自己的Load打不开

现象:同样的路径,在文件管理器里能看到文件,file 中文目录/报表.xls也能正常识别,但程序里Load("/home/张三/报表/2024年度-设备台账.xls")返回 false。

原因:程序内部在某处提前把 UTF-8 的路径字符串用QString::fromLocal8Bit转了一次,在 locale 为 C/POSIX 的服务器环境里,fromLocal8Bit 会按 ASCII 解析中文,导致字节被截断,传给文件层的路径已经不是原来的 UTF-8 字节流。开发者本机 locale 通常是 UTF-8 所以测不出来,一部署到 docker 容器就原形毕露。

解决:所有从外部传入的路径,统一用QFile::decodeName和QFile::encodeName做本地编码转换,不要手动选择 fromLocal8Bit 还是 fromUtf8。我在这套修改版里所有文件入口都改成了这个组合,部署到不同环境的差异就消失了。

5.4 单元格乱码:路径通了,但中文内容在Excel里是乱码

现象:文件能生成,用 LibreOffice 打开路径正常,但单元格里的中文显示成类似“䏿–‡”的乱码。

原因:写入时调用了SetString而不是SetWString。老库的 SetString 会把 char* 按当前 locale 或直接按字节塞进 BIFF8 的窄字符串记录,而 2003 格式内部对 Unicode 文本用的是 UTF-16LE;中文的 UTF-8 字节被逐字节当 Latin-1 处理后就变成了乱码。读取方向同理,GetString 出来的内容不能直接 QString::fromUtf8。

解决:单元格写入统一走ToBiffWString加SetWString,读取统一走GetWString加FromBiffWString。这一步做完,中文路径和中文内容是两套独立机制,别再混在一起排查。

5.5 保存出来的xls打不开:文本模式或长度计算错误

现象:保存成功,返回 true,文件大小也不是 0,但用 Excel 打开提示“文件格式和扩展名不匹配”,用 LibreOffice 打开直接是乱码。

原因:常见两处。第一处是文件流没按二进制模式打开,老库的ofstream忘了ios::binary,在 Linux 下写入时把 0x0A 当换行转换;第二处是SetWString的长度参数按字节数传了,BIFF8 记录里的字符串长度字段和实际数据对不上,整个记录流错位。

解决:写入统一走 QSaveFile,它默认就是二进制;长度参数则按字符个数传,我在这套修改版里已经把这两个点都封死了,你在自己的项目里改时,记住这两条就是最容易做错的位置。

6. 验收这套源码修改版:三条交叉检验路径和一份复测清单

验证一个 .xls 读写模块,单靠“程序没崩”完全不够,必须做独立于 Excelformat 的交叉检查。我会按三条路径来验收。先用file命令确认文件的 CFB 容器标识,再用 LibreOffice 无头模式转换 PDF 验证可读性,最后用 Python 的 xlrd 读取单元格并和写入值做比对。

# 第一条:确认文件是合法的 OLE 复合文档 file "/home/张三/报表/2024年度-设备台账.xls" # 预期输出包含 Composite Document File V2 Document # 第二条:用 LibreOffice 无头模式转换 PDF,退出码为 0 说明能正常解析 soffice --headless --convert-to pdf --outdir out "/home/张三/报表/2024年度-设备台账.xls" echo $?

第三条用 Python 的 xlrd 读取,注意装 1.2.0 这个最后的支持 .xls 的版本。读的时候按工作表索引拿值,和 Qt 侧写入的二维表逐项比对,这一条能直接发现 BIFF8 记录索引错位问题。

import xlrd wb = xlrd.open_workbook("/home/张三/报表/2024年度-设备台账.xls") ws = wb.sheet_by_index(0) for r in range(ws.nrows): for c in range(ws.ncols): print(r, c, ws.cell_value(r, c))

三条都过了之后,我会跑一遍复测清单。必测项包括:路径含中文、文件名含空格、工作表名含中文和特殊符号、单个工作表写入到 65535 行、单元格内容超过 255 个字符、文件名不带扩展名。2003 格式的行数上限是 65536,所以写入前最好在 Writer 里做一个行数检查,超过直接返回错误,否则库内部可能写出一个自己都读不回来的文件。把这套修改版固化成静态库放进工程后,我遇到的大部分问题都集中在第一次编译和第一次中文写入,之后的迭代基本不会再碰这些底层坑。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进