当前位置: 首页 > news >正文

QT Word模板 + QuaZIP + LibreOffice,跨平台方案实现导出.docx文件后再转为.pdf文件

最近在研究QT如何导出.docx文件,然后再将.docx文件转为.pdf文件;需要是跨平台的;

然而,在这方面,QT没有自带的库支持处理,第三方库也支持的比较少;

网上介绍的库有:OpenOffice、Liboffice、DuckX、docx和minidocx等,有兴趣的可以自行去了解一下;

在这里,我们不是通过代码去生成.docx文件,而是提前准备模板,通过替换模板中的文本,再导出.docx文件的方式去处理;然后如果需要,可以通过libreoffice库的命令将.docx转为.pdf。

简单介绍处理流程:

  1. 准备.docx模板,在模板上填写上需要替换的占位字符串;
  2. 通过QuaZIP将.docx文件解压缩;
  3. 修改解压缩出来的.xml文件,替换文本、图片等;
  4. 将解压出来的全部内容压缩为.docx;
  5. 通过LibreOffice库的命令将.docx转为.pdf。

(.docx 等于 .xml + zip) 我们熟悉的.docx文档,其实就是一个一个.xml文档,然后将其压缩后,就形成了。

1 准备工作

1.1 介绍使用到的工具

1.1.1 Word模板

模板就是你期望导出什么样.docx文件,先提前自己编写好,然后在还不确定填写文本的地方,使用占位字符串填写上,后面就可以通过替换占位字符串将目标文本替换上去,从而达到预期效果;

1.1.2 QuaZIP

QuaZIP 是一个基于 zlib 的跨平台 C++ 库,专为 Qt 框架设计,提供 ZIP 文件的读写功能。

这里将在项目中导入QuaZIP和ZLIB的源码,不编译成库使用,方便项目跨平台切换;

博主之前也学习QuaZIP库时,使用其简单做了一个加密压缩和解压缩zip的小案例,有兴趣的可以去看看:QT 引入Quazip和Zlib源码工程到项目中,无需编译成库,跨平台,加密压缩,带有压缩进度

1.1.3 LibreOffice

LibreOffice 是一款功能强大的办公软件,默认使用开放文档格式 (OpenDocument Format , ODF), 并支持 *.docx, *.xlsx, *.pptx 等其他格式。

它包含了 Writer, Calc, Impress, Draw, Base 以及 Math 等组件,可用于处理文本文档、电子表格、演示文稿、绘图以及公式编辑。

它可以运行于 Windows, GNU/Linux 以及 macOS 等操作系统上,并具有一致的用户体验。

对,没错,LibreOffice是一款办公软件,与Microsoft Office 和 WPS 一样,都可以处理Wrod、Excel文档等;
最最最重要的是,LibreOffice是免费的、开源的、跨平台的,并且提供了命令!所以在项目中,我们就可以使用LibreOffice的命令将.docx转为.pdf了。
当然也支持其他更多格式的转换。

1.2 项目依赖准备

1.2.1 Word模板

自己手动新建一个.docx文件,定义自己的模板;

例如,本教程将使用如下模板:
在这里插入图片描述

说明:
模板中,占位字符串用到了:
${FileHead}、${Name}、${Age}、${Description} ...等等

后面将会在代码中,将这些占位字符串替换为我们自己的字符串;
图片也可以插入然后替换,但是,文档内的图片建议(必须)是统一后缀的,例如都是.png格式,才在代码中更加方便的替换;
文档中对文本的加粗、斜体、设置颜色、背景颜色、设置字体等,替换字符串后,均使用原来设置好的样式;

对于表格,有固定行数的,也有不固定行数的(表格数据是动态的),本博客也有讲解如何处理不固定行数的表格新增问题;

1.2.2 QuaZIP

上面提到了,项目中将使用QuaZIP的源码,这里也会提供;是一个文件夹,内部包含了所有的代码;
在这里插入图片描述
将其放在项目的根路径下,即与mian.cpp文件同级路径下,然后在.pro文件中包含进来:

# 源码方式使用需要设置为静态库
DEFINES +=   QUAZIP_STATIC
include($$PWD/quazip/3rdparty/zlib.pri)
include($$PWD/quazip/quazip.pri)

1.2.3 LibreOffice 安装教程

LibreOffice非常庞大,如果使用源码安装的话,会出现各种依赖库的问题,非常麻烦,所以这里将使用官方编译好的程序进行安装。

点击链接进入到官网:主页| LibreOffice 简体中文官方网站 - 自由免费的办公套件

然后点击下载进入到下载页面:
在这里插入图片描述
有两个选项,下载LibreOffice和国内下载镜像,如果你是Windows和Linux用户,那么可以点击国内下载镜像去下载,下载速度会很快;
因为我用的是统信UOS系统ARM架构,只能点击下载LibreOffice项去下载,下载速度会很慢;

然后点击downloadarchive
在这里插入图片描述
然后会进入到很多旧版本下载的页面,Windows用户和Linux用户可以根据自己情况下载相应版本;
但是,ARM架构的用户,只能选择最新的测试版本,因为只有最新的测试版本才提供了ARM架构的安装文件,其他正式版本都没有,我不知道为什么,这里的版本我几乎都点来看过了,都没有;有知道的朋友请告知一下,谢谢。
在这里插入图片描述
点击进来后,如果你是Linux用户和ARM架构用户,点击deb包下载,也方便安装;
如果你是Windows用户,点击win下载.msi安装文件;
在这里插入图片描述
点击进来后,如果你是Linux用户,点击x86_64;如果你是ARM架构用户,点击aarch64;
在这里插入图片描述

Windows用户根据情况选择32位系统安装包和64位系统安装包;
在这里插入图片描述
然后,不管你是点击了 aarch64 还是 x86_64 还是 x86 ,在进入的下载页面中,都点击第一个包进行下载:
ARMLinux
Windows
之后就是漫长的等待下载的过程了…

1.2.3.1 Linux和ARM

下载完成后,Linux和ARM架构用户,将压缩包解压出来,进入到全是.deb包的文件路径,打开终端,输入命令:sudo dpkg -i *.deb 开始安装;
在这里插入图片描述
程序一般安装在 /opt 文件夹下,里面会有一个libreoffice25.8的文件夹,因为我安装的是25.8版本的,所以文件夹名字是libreoffice25.8;
libreoffice25.8文件夹内,还会有一个 program/ 文件夹,这个文件夹里面就有我们要用到的程序 soffice ;
在这里插入图片描述
通过命令: /opt/libreoffice25.8/program/soffice --version 就可以查看安装的版本了;
在这里插入图片描述
然后,请记住这个这个路径,例如我的是:/opt/libreoffice25.8/program/soffice ; 在代码中需要用到!
注意,我安装的最新测试版本,soffice 是一个.sh脚本,不知道其他正式版本是不是;

1.2.3.2 Windows

双击 .msi 文件进行安装;
选择自定义安装,才可以选择安装路径;
在这里插入图片描述
然后修改安装路径,不介安装在系统盘符的,可以不用改;
在这里插入图片描述
然后继续点击下一步,直到装成功就可以了!
最后在系统菜单栏里,就可以看到相应的菜单项了,点击后就可以打开wrod或者excel文档了;

不过,请记住自己的安装路径,在安装路径下的 program/ 文件夹内,有一个 soffice.exe 可执行程序,双击后也可以打开;请记住这个路径,例如我的是:D:\libreoffice\program\soffice.exe ; 在代码中需要用到!
在这里插入图片描述

2 解压docx文档后的文件介绍

将.docx解压后,会得到一个文件夹,文件夹内部全都是.xml文件;

其中,在wrod文件夹下的document.xml文件,这个文件就相当主页面了,我们操作替换的文件也是它;
在这里插入图片描述
如下图所示:
在这里插入图片描述

在media/ 文件中,存储着Word文件插入的图片;
细心观察,图片名字为image1.png ~ image5.png;而且图片名字的序号与文档中的图片位置是一致的;
如果将文档中第三张图片删除掉,再解压出来,那么顺序就变成了image1.png ~ image4.png;文档会自动对文件进行排序命名的;

所以,根据此原理,只需要替换掉文件夹中的图片,即可实现文档的图片替换了!
在这里插入图片描述
那么图片是如何与主页面的.xml文档关联上的呢?

在 word/_rels/ 文件夹中,有一个document.xml.rels文件,文件内会有标识图片的Id值,通过这个Id值,在主页面的.xml文件中是可以搜索得到的,有兴趣的可以去搜一下!
就是这样就可以关联上了。
在这里插入图片描述

其他的我也不太懂了,就不介绍了。有兴趣的可自行了解。

3 编码

提前准备好需要替换的图片,命名可随意,但必须要与文档中的图片后缀一样!!!
例如我准备的模板文档中插入的5张图片都是.png格式,这里我也准备了5张不同内容的.png格式照片用于替换;
在这里插入图片描述

包含头文件:

#include <QFile>
#include <QDomDocument>
#include <QProcess>
#include <QDir>
#include <QDebug>
#include <QTemporaryDir>
#include <QMap>
#include <QDesktopServices>
#include <QDirIterator>
#include <QUrl>#include "quazipfile.h"// 存储表格数据的
struct TableValue {QString document;       // 文档QString description;    // 描述QString explain;        // 说明QString kind;           // 种类、类别
};// 使用 RAII 包装器确保资源清理
class QuaZipRAII {
public:QuaZipRAII(QuaZip* zip) : m_zip(zip) {}~QuaZipRAII() {if(m_zip && m_zip->isOpen()) {m_zip->close();}delete m_zip;}operator QuaZip*() { return m_zip; }QuaZip* operator->() { return m_zip; }QuaZip* get() { return m_zip; }
private:QuaZip* m_zip;Q_DISABLE_COPY(QuaZipRAII)
};

3.1 解压缩

这里使用的是QuaZIP去处理解压缩,并没有使用JlCompress去处理,使用JlCompress去解压缩会出问题;

// zipPath 参数传.docx文档路径	targetDir 参数传解压路径
bool Widget::extractFile(const QString &zipPath, const QString &targetDir)
{QuaZipRAII zip(new QuaZip(zipPath));if (!zip->open(QuaZip::mdUnzip)) {qWarning() << "Failed to open ZIP file:" << zipPath<< "Error:" << zip->getZipError();return false;}QDir targetDirObj(targetDir);if (!targetDirObj.exists() && !targetDirObj.mkpath(".")) {qWarning() << "Failed to create target directory:" << targetDir;return false;}const int blockSize = 65536;  // 64KB 块大小QuaZipFileInfo fileInfo;QuaZipFile file(zip);for (bool more = zip->goToFirstFile(); more; more = zip->goToNextFile()) {if (!zip->getCurrentFileInfo(&fileInfo)) {qWarning() << "Failed to get file info, skipping. Error:" << zip->getZipError();continue;}QString absPath = targetDirObj.absoluteFilePath(fileInfo.name);QFileInfo fi(absPath);// 创建目录结构if (!QDir().mkpath(fi.path())) {qWarning() << "Failed to create path:" << fi.path();continue;}// 处理目录if (fileInfo.name.endsWith('/')) {QDir dir(absPath);if (!dir.exists() && !dir.mkpath(".")) {qWarning() << "Failed to create directory:" << absPath;}continue;}// 打开文件if (!file.open(QIODevice::ReadOnly)) {qWarning() << "Failed to open ZIP entry:" << fileInfo.name<< "Error:" << file.getZipError();continue;}// 创建目标文件QFile outFile(absPath);if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {qWarning() << "Failed to open output file:" << absPath;file.close();continue;}// 分块读写char buffer[blockSize];qint64 totalBytes = 0;while (!file.atEnd()) {qint64 bytesRead = file.read(buffer, blockSize);if (bytesRead <= 0) break;qint64 bytesWritten = outFile.write(buffer, bytesRead);if (bytesWritten != bytesRead) {qWarning() << "Write error for:" << absPath<< "Expected:" << bytesRead << "Actual:" << bytesWritten;break;}totalBytes += bytesWritten;}outFile.close();file.close();// 设置文件权限
//        if (fileInfo.externalAttr != 0) {
//            QFile::setPermissions(absPath,
//                static_cast<QFile::Permissions>(fileInfo.externalAttr >> 16));
//        }// 设置文件时间戳QDateTime dt;dt.setDate(QDate(fileInfo.dateTime.date().year() + 1900,fileInfo.dateTime.date().month() + 1,fileInfo.dateTime.date().day()));dt.setTime(QTime(fileInfo.dateTime.time().hour(),fileInfo.dateTime.time().minute(),fileInfo.dateTime.time().second()));QFile(absPath).setFileTime(dt, QFileDevice::FileModificationTime);if (totalBytes != fileInfo.uncompressedSize) {qWarning() << "Size mismatch for:" << absPath<< "Expected:" << fileInfo.uncompressedSize<< "Actual:" << totalBytes;}}return zip->getZipError() == UNZ_OK;
}

3.2 压缩

这里使用的是QuaZIP去处理解压缩,并没有使用JlCompress去处理,使用JlCompress去解压缩会出问题;

// zipPath 参数传目标 .docx全路径		sourceDir 参数传需要压缩的文件夹路径
bool Widget::compressFile(const QString &zipPath, const QString &sourceDir)
{// 使用 RAII 管理资源QuaZipRAII newZip(new QuaZip(zipPath));if (!newZip->open(QuaZip::mdCreate)) {qWarning() << "Failed to create ZIP file:" << zipPath<< "Error:" << newZip->getZipError();return false;}QDir sourceDirObj(sourceDir);QSet<QString> addedDirs;const int blockSize = 65536;  // 64KB 块大小// 递归处理目录QDirIterator it(sourceDir, QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot,QDirIterator::Subdirectories);while (it.hasNext()) {QString filePath = it.next();QFileInfo fi(filePath);QString relativePath = sourceDirObj.relativeFilePath(filePath);// 处理目录if (fi.isDir()) {QString zipDirPath = relativePath + '/';if (!addedDirs.contains(zipDirPath)) {QuaZipFile zipFile(newZip);QuaZipNewInfo newInfo(zipDirPath);
//                newInfo.externalAttr = (fi.permissions() | 0x10) << 16; // 保留权限+目录标志if (!zipFile.open(QIODevice::WriteOnly, newInfo)) {qWarning() << "Failed to create directory entry:" << zipDirPath<< "Error:" << zipFile.getZipError();continue;}zipFile.close();addedDirs.insert(zipDirPath);}continue;}// 处理文件QFile sourceFile(filePath);if (!sourceFile.open(QIODevice::ReadOnly)) {qWarning() << "Failed to open source file:" << filePath;continue;}QuaZipFile zipFile(newZip);QuaZipNewInfo newInfo(relativePath, filePath);
//        newInfo.externalAttr = fi.permissions() << 16; // 保留文件权限if (!zipFile.open(QIODevice::WriteOnly, newInfo)) {qWarning() << "Failed to open ZIP entry:" << relativePath<< "Error:" << zipFile.getZipError();sourceFile.close();continue;}// 分块读写提高大文件处理效率qint64 totalBytes = 0;char buffer[blockSize];while (!sourceFile.atEnd()) {qint64 bytesRead = sourceFile.read(buffer, blockSize);if (bytesRead <= 0) break;qint64 bytesWritten = zipFile.write(buffer, bytesRead);if (bytesWritten != bytesRead) {qWarning() << "Write error for:" << relativePath<< "Expected:" << bytesRead << "Actual:" << bytesWritten;break;}totalBytes += bytesWritten;}zipFile.close();sourceFile.close();if (totalBytes != fi.size()) {qWarning() << "File size mismatch:" << relativePath<< "Original:" << fi.size() << "Compressed:" << totalBytes;}}return newZip->getZipError() == UNZ_OK;
}

3.3 docx转pdf

转换时,使用安装好的libreoffice命令,将docx转为pdf文件;

bool Widget::convertDocxToPdf(const QString &docxPath, const QString &pdfOutputDir)
{QProcess process;QString libreOfficeCmd = "";// 根据自己的安装路径设置,一定要绝对路径,相对路径不行,会找不到soffice
#ifdef Q_OS_UNIXlibreOfficeCmd = "/opt/libreoffice25.8/program/soffice"; // Linux路径
#elselibreOfficeCmd = "D:/libreoffice/program/soffice.exe" // Windows路径
#endif// 测试命令: 
// /opt/libreoffice25.8/program/soffice  --headless  -convert-to  pdf  --outdir  pdf文件输出路径   xxx.docx输入路径QStringList args = {"--headless","--convert-to", "pdf","--outdir", pdfOutputDir,docxPath};process.start(libreOfficeCmd, args);if (!process.waitForFinished(15000)) {qWarning() << "PDF转换超时:" << process.errorString();return false;}return process.exitCode() == 0;
}

3.4 替换占位符内容 和 图片

起始就是读取文件的内容后,通过QString::replace函数实现内容替换即可!

bool Widget::replaceInDocx(const QString &templatePath,const QString &outputPath,const QMap<QString, QString> &textReplacements,const QMap<QString, QString> &imageReplacements)
{QTemporaryDir tempDir;if (!tempDir.isValid()) {qWarning() << "无法创建临时目录";return false;}// 1 解压 docxQString unzipPath = tempDir.path();if (!extractFile(templatePath, unzipPath)) {qWarning() << "解压失败!";return false;}qWarning() << unzipPath;// 2 替换文本内容QString docXmlPath = unzipPath + "/word/document.xml";QFile file(docXmlPath);if (!file.open(QIODevice::ReadOnly)) {qWarning() << "无法打开document.xml";return false;}QDomDocument doc;if (!doc.setContent(&file)) {file.close();qWarning() << "解析XML失败";return false;}file.close();// 文本替换QString xml = doc.toString();for (auto it = textReplacements.begin(); it != textReplacements.end(); ++it) {QString key = QString("${%1}").arg(it.key());QString value = it.value();xml.replace(key, value);}// 不想使用循环,也可以一个一个的替换xml.replace("${FileHead}", "我是表头");if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {qWarning() << "无法写入document.xml";return false;}file.write(xml.toUtf8());file.close();// 3 替换图片if (!imageReplacements.isEmpty()) {QString mediaPath = unzipPath + "/word/media/";QDir mediaDir(mediaPath);// 确保media目录存在if (!mediaDir.exists()) {mediaDir.mkpath(".");}// 处理图片替换for (auto it = imageReplacements.constBegin(); it != imageReplacements.constEnd(); ++it) {QString placeholder = it.key();       // 占位符名称,如 "logo"QString imagePath = it.value();       // 新图片路径// 目标图片路径QString destPath = mediaPath + placeholder;// 复制图片到media目录if (!copyWithOverwrite(imagePath, destPath)) {qWarning() << "图片复制失败:" << imagePath << "->" << destPath;continue;}}}// 准备表格数据QList<TableValue> tableValueList = {{ "文档1", "描述1", "说明1", "种类1" },{ "文档2", "描述2", "说明1", "种类2" },{ "文档3", "描述3", "说明1", "种类3" },{ "文档4", "描述4", "说明1", "种类4" },{ "文档5", "描述5", "说明1", "种类5" },{ "文档6", "描述6", "说明1", "种类6" },{ "末尾1", "末尾2", "末尾3", "末尾4" },};// 4 动态新增替换表格if (!replayTableIndex(docXmlPath, tableValueList)) {qWarning() << "表格数据替换失败!";return false;}// 5 压缩回 docxif (!compressFile(outputPath, unzipPath)) {qWarning() << "压缩失败!";return false;}return true;
}

3.5 动态往表格插入数据

动态往表格中插入数据,就不能使用固定好写好占位符内容了,因为不知道需要插入多少行数据;

即使知道如果是要插入512行数据,那么是不是就要定义好512个占位符内容呢?这显然不现实;

因为我们操作的对象是.xml文档,所以其实是可以通过复制一行表格内容再粘贴,即可实现插入效果的;

如果复制的是占位符内容,那么就可以先复制,再替换内容,再粘贴的方式,实现动态插入的效果;

知识点:

  • <w:tbl> 表示整个表格。
  • <w:tr> 表示表格的一行(table row)。
  • <w:tc> 是表格的一个单元格(table cell)。
  • <w:t> 是里面的文本内容。

操作流程:

在 Word 中插入一个表格(比如 4 列),写一行示例数据:
${Value1} | ${Value2} | ${Value3} | ${Value4}

在代码里找到这段表格所在的 XML,然后:

  1. 复制这一整行(<w:tr> 标签)
  2. 替换占位符
  3. 重复插入你需要的行数

表格 XML 示例(简化后的):

<w:tbl><w:tr><w:tc><w:p><w:r><w:t>${Value1}</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>${Value2}</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>${Value3}</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>${Value4}</w:t></w:r></w:p></w:tc></w:tr>
</w:tbl>

只需要:

  • 在 XML 中找到相应的表格 <w:tbl>
  • 在<w:tbl> 表格内找到占位符内容的行 <w:tr>
  • 在代码里复制这个 <w:tr>节点
  • 在<w:tr>一行节点内,循环遍历内部的所有子节点,即循环遍历一行内的所有单元格 然后在逐步替换单元格内的文本即可

注意,一个文档内可能会有多个表格,所以在定位表格的时候,可以获取当前表格内的所有文本,然后通过字符串判断方式是否包含指定的替换文本,从而得知当前表格是不是需要操作的表格;
例如通过明确定位 ${Value1} 即可知道表格,因为文档中的占位符唯一;

bool Widget::replayTableIndex(const QString &templatePath, QList<Widget::TableValue> TableData)
{QString docXmlPath = templatePath;QFile file(docXmlPath);if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return false;QDomDocument doc;if (!doc.setContent(&file)) {file.close();return false;}file.close();// 查找所有表格QDomNodeList tables = doc.elementsByTagName("w:tbl");if (tables.isEmpty()) return false;QDomElement table = { };for (int j = 0; j < tables.count(); j++) {table = tables.at(j).toElement();   // 获取表格QString str = table.text();         // 获取表格内的所有文本// 通过判断字符串内是否包含占位字符串,从而得知当前表格是否是需要替换文本的表格if (str.contains("${Value1}")) {    // 可以判断是占位字符串里面任意,因为他们理论上都是唯一的break;} else {table = { };}}// 判断是否找到了匹配的表格if (table.isNull()) {qWarning() << "没有找到匹配的表格!";return false;}// 查找表格内的所有行QDomNodeList rows = table.elementsByTagName("w:tr");if (rows.size() < 2) return false;QDomNode templateRow = { }; // 占位模板行for (int j = 0; j < rows.count(); ++j) {templateRow = rows.at(j);           // 获得占位模板行QString str = templateRow.toElement().text();         // 获取一行表格内的所有文本// 通过判断字符串内是否包含占位字符串,从而得知当前行是否是占位模板行if (str.contains("${Value1}")) {    // 可以判断是占位字符串里面任意,因为他们理论上都是唯一的break;} else {templateRow = { };}}// 判断是否找到了 占位模板行if (table.isNull()) {qWarning() << "没有找到 占位模板行!";return false;}// 表格动态插入行和文本替换for (int i = 0; i < TableData.size(); ++i) {// 克隆一个新的占位模板行,相当于插入了一行占位模板行QDomNode newRow = templateRow.cloneNode(true);// 替换文本//QString xmlString = newRow.toElement().ownerDocument().toString();// 临时存储站位模板行,用于动态插入newRow = newRow.toElement();// 查找一行表格内的所有单元格QDomNodeList cells = newRow.toElement().elementsByTagName("w:t");for (int j = 0; j < cells.count(); ++j) {QDomElement cell = cells.at(j).toElement();QString text = cell.text(); // 获得单元格内的文本if (text.contains("${Value1}")) {			// text == "${Value1}"text = TableData[i].document;} else if (text.contains("${Value2}")) {	// text == "${Value2}"text = TableData[i].description;} else if (text.contains("${Value3}")) {	// text == "${Value3}"text = TableData[i].explain;} else if (text.contains("${Value4}")) {	// text == "${Value4}"text = TableData[i].kind;}// 重新设置单元格里面的文本,相当于替换cell.firstChild().setNodeValue(text);}// 在末尾插入新的一行 占位模板行
//        table.appendChild(newRow);table.insertAfter(newRow, table.lastChild());}table.removeChild(templateRow); // 删除原始模板行// 保存 document.xmlif (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) return false;QTextStream out(&file);doc.save(out, 2);file.close();return true;
}

3.6 使用示例

void Widget::generateReport()
{// 文本替换数据QMap<QString, QString> textData {{"Name", "Jtom"},{"Age", "111"},{"Description", "这是一段描述信息ABCdef123"},{"Table1", "固定表格的数据1"},{"Table2", "其他其他"},{"Table3", "啦啦啦"},{"Special1", "我很特别"},{"Special2", "俺也一样"},{"Special3", "我知道"}};// 图片替换数据 (Word中的图片名字 -> 图片路径)QMap<QString, QString> imageData {{"image1.png", "Image/1.png"},{"image2.png", "Image/2.png"},{"image3.png", "Image/3.png"},{"image4.png", "Image/图片2.png"},{"image5.png", "Image/图片3.png"}};QString currentPath = QCoreApplication::applicationDirPath();QString templatePath = currentPath + "/input.docx";		// 输入.docx路径QString outputDocx = currentPath + "/report.docx";		// 输出.docx路径QString outputPdf = currentPath + "/report.pdf";		// 输出.pdf路径if (replaceInDocx(templatePath, outputDocx, textData, imageData)) {qDebug() << "DOCX 创建成功: " << outputDocx;if (convertDocxToPdf(outputDocx, QFileInfo(outputPdf).absolutePath())) {qDebug() << "PDF 创建成功: " << outputPdf;// 打开生成的PDFQDesktopServices::openUrl(QUrl::fromLocalFile(outputPdf));} else {qWarning() << "PDF 生成失败";}} else {qWarning() << "报告生成失败";}
}

运行后的PDF如下图所示:
在这里插入图片描述

3.7 背景知识

.docx = ZIP 压缩包 + XML 文件

.docx 文件其实是 ZIP 压缩文件,里面的正文存在 word/document.xml 文件里。

表格结构(简化):

<w:tbl>            ← 表格开始<w:tr>           ← 表格的一行(Table Row)<w:tc>         ← 单元格(Table Cell)<w:p><w:r><w:t>内容</w:t></w:r></w:p></w:tc>...</w:tr><w:tr>...        ← 第二行</w:tr>
</w:tbl>
  • <w:tr> 表示表格的 一整行(新增需要复制这一整段,删除也需要移除这一整段)。

  • <w:tc> 表示每个单元格。

  • <w:t> 是最终的文本内容,你的 ${} 占位符一定出现在 <w:t> 标签中。

Word中插入的图片,如果插入的图片后缀都是一样的,例如都是.png格式,那么Word会依据文档中的图片顺序,将图片重新命名为:image1.png,…,imagen.png;

即使将中间某一张图片删除掉了,Wrod也会重新排序命名的;

所以,只要确保插入的图片格式一样,就可以实现图片的简单替换!

4 总结

其实这种方案也算是取巧的一种,需要很多库的支持,并且处理起来也挺繁琐;
但是也是一种可行的方案,目前Qt、C++方面对Word的支持还是很少的!

按照教程,安装LibreOffice,准备好.docx模板,QuaZIP库源码在下面工程代码中提供了。
剩下的就是编码处理流程了,我提供的模板也只是测试模板,更加复杂的样式模板可以自行去测试一下,应该也是没有问题的!

项目源码:https://gitee.com/ygt777/Qt_Wrod_QuaZIP_LibreOffice.git

最后,学习参考:C++/Qt导出动态数据生成Word、PDF报表文件

http://www.dtcms.com/a/308518.html

相关文章:

  • 安全月报 | 傲盾DDoS攻击防御2025年7月简报
  • 功能强大编辑器
  • [Agent开发平台] 可观测性(追踪与指标) | 依赖注入模式 | Wire声明式配置
  • 量子安全:微算法科技(MLGO)基于比特币的非对称共识链算法引领数字经济未来
  • Linux安装AnythingLLM
  • 【ad-hoc构造】P10033 「Cfz Round 3」Sum of Permutation|普及+
  • langchain--1--prompt、output格式、LCEL示例
  • 2025年7月最新一区SCI-基尔霍夫定律优化算法Kirchhoff’s law algorithm-附Matlab免费代码
  • FastGPT + Kymo AI生态创新平台,搭建企业智能化知识管理
  • XPATH选择器常用语法
  • langchain从入门到精通(四十二)——全面剖析之Memory
  • 机器学习①【机器学习的定义以及核心思想、数据集:机器学习的“燃料”(组成和获取)】
  • 深度学习基础—2
  • [人工智能-综述-17]:AI革命:重塑职业版图,开启文明新篇
  • day066-内容分发网络(CDN)与web应用防火墙(WAF)
  • 大模型+垂直场景:技术纵深、场景适配与合规治理全景图
  • Rust × WebAssembly 项目脚手架详解
  • Linux服务器性能检测与调优指南
  • 深入解析LLM层归一化:稳定训练的关键
  • 【04】大恒相机SDK C++发开——调试千兆网相机心跳超时设备掉线
  • 50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | FeedbackUiDesign(评价反馈组件)
  • 工程项目管理软件选型对比:主流平台功能与适用场景深度测评
  • [12月考试] F
  • 用el-table实现的可编辑的动态表格组件
  • 微信小程序中进行参数传递的方法
  • 【Linux】的起源 and 3秒学习11个基本指令
  • JSX语法
  • 关于AI的使用感想
  • Maven模块化开发与设计笔记
  • 深入解析 Spring AI 系列:剖析OpenAI接口接入组件