C++(Qt) 和 Word 交互总结(二)

阅读本文大概需要 6 分钟

之前有一篇文章介绍过 C++/Qt 操作 Word的一些方法,虽然能满足一部分使用场景,但是终究是在某些平台上有限制,使用起来还是不方便,所以就有了这边文章

我们知道操作 Word其实还有一种方法,那就按照 OOXML规范读写即可,OOXML 是微软 2007之后推出的一套标准,凡是符合这个标准生成的文档都可以正常打开,遗憾的是这方面 C++ 没有可用的库,一是因为本身 C++人群少,二是是用 C++ 实现工作量大,所以就只能选择现有成熟的轮子

Python有非常多的开源库可以使用,其中有一个Python-docx库,完美实现了Word读写,使用 C++ 调用 Python是非常方便的,所以可以间接来实现 Word的交互

支持功能:

  • 支持自定义标题,包括样式、字体、对齐方式、标题级别等;

  • 支持插入任意行列表格,表格支持单独设置某个单元格样式,字体、颜色、是否加粗、水平、垂直对齐方式等;

  • 支持合并任意单元格;

  • 支持插入图片,支持相对路径和绝对路径

下面看测试导出的效果:

导出效果

原理介绍

我们知道 C/C++/Qt都是编译型语言,也是是说不能直接从源码运行,而Python是解释型语言,不需要经过编译成二进制代码可以直接从源码运行,在运行 Python的时候首先经过 Python 解释器解释,你可以理解成翻译的意思,解释成字节码,然后在一条一条字节码指令开始执行

Python提供了一些C库,我们可以在C/C++程序中包含对应头文件、库文件,进而调用函数方法来实现某个功能

调用 Python主要流程如下:

  • 初始化Python上下文环境(解释器环境)
  • 导入对应的模块
  • 获取对应函数对象,参数转换,调用函数
  • 解析返回值,结束调用
  • 释放 python解释器

C++根据实际业务生成对应的JSON字符串,然后调用Python传递给对应函数,在Python函数中解析JSON字符串然后生成Word内容

整个脚本实现库以及对应 Example 都已经开源,感兴趣的朋友直接访问即可

https://github.com/kevinlq/QtPythonDocx

环境配置

下载并安装好Python相关库,确保本地环境没有问题,记得安装好Python-docx库。拷贝Python相关依赖库到你的项目目录,不如下面这样

QtPythonDocx
|  3rdparty
│  └─Python310
│      ├─include
│      │  ├─cpython
│      │  └─internal
│      └─libs
├─bin
│  ├─Python310
│  │  ├─DLLs
│  │  └─Lib
|  |─script
│  │  wordOperate.py

关于一些版本事项、以及中间会遇到那些坑,文末有注意事项统一介绍

调用 Python

为了做到简洁、通用,我们编写一个脚本调用类,该类和具体的业务无关,只负责传入不同模块、函数、参数调用对应的Python函数并能够返回对应的结果,这样后续的调用者就使用的时候和使用普通函数没有区别

为了实现这个目的,目前有几个知识点需要解决:

  • 由于Python数据类型和C++不一样,如果要通用那么就需要进行转换,怎么做到C++一个参数类型匹配Python多个类型?
  • 返回值处理,我们的业务函数返回值可能多种多样,怎么兼容?
  • 编码转换,Python中支持UTF-8,我们程序处理中数据可能包含多种类型,怎么转换

解决了上述问题,基本也就是完成了本次要写的脚本加载类

脚本调用类实现

首先看下类型问题,其实我们这里需要一个万能类型来作为函数入参,那么有这个类型么?有,如果你的编译器支持 C++17,那么可以用std::variant

std::variant<int, double, std::string> inputArg

由于作者本人对 Qt比较熟一点,所以本次程序中使用了大量的Qt内置数据类型,原理是相通的

KPythonRunScript类的实现,核心函数如下所示

bool callFun(const char *funcName,
                 const QVariantList &args = QVariantList(),
                 QVariant &returnValue = QVariant(QVariant::Invalid));
  • funcName: python 脚本中对应的函数名字
  • args: 函数入参,根据实际脚本中函数参数个数而定
  • returnValue: 返回值,如果脚本函数有返回值初始化的时候赋予对应类型

实际Python脚本中函数的入参个数是不确定的,为了兼容多个调用场景,所以采用了数组作为实际的入参,数组每个元素采用QVariant类型,这样就能根据实际传入的类型来判断,在调用Python的时候应该转换为什么类型

返回值类型也一样,初始化调用时确定好本次调用的返回值类型,这样在Python脚本调用完成后才能把返回值转为我们C++实际的返回值

类型转换:

for(int index = 0; index < args.size(); index++)
{
    QVariant arg = args[index];
    switch (arg.type())
    {
    case QVariant::String:
    {
    QByteArray baContent = arg.toString().toLocal8Bit();
    PyTuple_SetItem(pArgsObj, index, Py_BuildValue("s", baContent.constData()));
    }
    break;
    case QVariant::Int:         PyTuple_SetItem(pArgsObj, index, Py_BuildValue("i", arg.toInt()));                                      break;
    case QVariant::Double:      PyTuple_SetItem(pArgsObj, index, Py_BuildValue("d", arg.toDouble()));                                   break;
    case QVariant::LongLong:    PyTuple_SetItem(pArgsObj, index, Py_BuildValue("l", arg.toLongLong()));                                 break;
    case QVariant::Char:        PyTuple_SetItem(pArgsObj, index, Py_BuildValue("b", arg.toChar().toLatin1()));                          break;
    case QVariant::Invalid:     PyTuple_SetItem(pArgsObj, index, Py_BuildValue("()"));                                                  break;
    default: break;
    }
}

这里目前适配了上述几种类型,如果后续不满足继续扩展其它类型即可

Python脚本对应的函数

def generateWord(strContent):
    #...
    return True

详细调用

在上述实现的类的基础上,调用其实就变的很简单了,就和我们调用本地某个函数一样,非常轻松

KPythonRunScript *pRunScript = KPythonRunScript::instance("wordOperate");
QVariant returnValue = true;
QVariantList args = {""};
bool bResult = pRunScript->callFun("generateWord", args, returnValue);
qDebug() << "run generateWord result:" << bResult << returnValue;

if(!bResult)
{
    qWarning() << "write word fail.....";
    return;
}

可能你注意到程序中使用了单例,为什么使用单例?这是因为单个进程Python解释器相关内容初始化一次即可,后续随意调用不用再次初始化,实际验证中也证实了,多次初始化会有一些异常问题(虽然每次用完已经释放了,再次初始化还是会有问题)

这样就实现了一个简单的调用过程,具体Python文件中的内容可以看我开源的工程目录中的内容,其实就是把各种操作Word方法封装成函数了,扩展了常用的字段

QtPythonDocx/bin/script/wordOperate.py

JSON格式说明

由于 Word 内容较多,调用时兼容很多写入场景,因此目前设计使用 JSON 格式来交互,基本覆盖大部分使用场景,而且支持各种自定义,完全满足日常使用,下面是各个字段的说明

全局配置

  • savePath: 定义了生成的 Word 文档路径,确保该路径有写入权限,否则可能会失败
  • openFile: 导入成功后是否打开该文档
  • line_spacing: 行间距,默认给 1.5倍
  • header: 页眉文本,不需要页眉直接给空即可
  • footer: 页脚文本
  • content:[] 这里是 Word 内容部分,采用数组存储,由于数组有有序的,因此严格按照你的内容顺序依次传入即可
  • fontSize: 全局字体大小
  • fontName: 全局字体名字,设置后后续每个正文、标题、表格等可以不用设置,全局统一

正文

下面是正文内容部分说明

  • type: 标识是那种类型,0:标题,1: 普通文本,2:图片,3:表格,其它类型后续扩展自定义
  • text: 如果是文本或者标题给定内容
  • level: 级别,目前只有标题类型生效
  • bold: 是否加粗
  • italic: 是否倾斜
  • strike: 是否删除线
  • alignment: 对齐方式,主要有这么几种:left, right,center
  • color: 对应文本的颜色
  • height: 行高

插入表格

如果是表格,那么有这些扩展字段

  • columns: 列数
  • rows: 行数
  • height: 行高,所有行设置一样的行高,也可以自定义每行的行高
  • mergeCells: 要合并的单元格数组,比如合并 (0,0)和(0,1)单元格,那么内容如下
{"begin": [0,0], "end": [0,1]}
  • tableCell: 单元格内容,依次填充每个单元格内容即可,每个单元格内容和普通文本类似,下面是一个示例
tableCell": [
                {"text": "我是第一个单元格,加粗,倾斜,红色", "style": "", "bold": true, "italic": true,"color": "#ff0000","alignment": "center"},
                {"text": "00和01合并了,02会覆盖01的值,加粗变红,左对齐", "style": "", "bold": true, "italic": false,"color": "#ff0000","alignment": "left"},
                {"text": "03", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"},
                {"text": "04", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"},
                {"text": "05", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"},
                {"text": "06", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"},
                {"text": "07", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"},
                {"text": "08", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"}
            ]

插入图片

图片字段和其它文本字段类似,额外添加图片路径属性即可

  • picture: "./test.png"

注意图片路径支持相对路径和绝对路径,根据自己实际需要传递即可

总结

本次通过Python的方式可以很好的支持很多之前出现的异常问题,足以满足我们遇到的各种业务需要导出生成Word难题,而且导出速度非常快,实际测试生成 10 页左右文档耗时不到 2秒,测试了多台电脑,实际效果都非常理想

注意事项

  • Python版本选择问题,确保你的程序最终要运行的平台,如果要最低要求是Windows7,那么建议选择 Python3.8版本即可,如果无所谓那么选择最新稳定版本即可;
  • Python 注意选择和你程序使用同一个位数,程序编译器使用的是 64 位,那就下载 64 位,32位同理 ;

推荐阅读

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容