在Windows环境下,在Qt里面操作Office文档,可以通过AxContainer模块调用Office全家桶的Com组件。
完成这个操作,需要使用到AxContainer模块中的QAxObject。
使用这种方法操作Office文件,好处是可以非常精确,精确到Word文档的每一个字符、PowerPoint的每一个图片以及Excel文档的每一行一列……那种程度。
但是麻烦的是,太精确了,就没有那么简便。当我们想实现简单地预览、统计、输出等只读功能的时候,会比较复杂。而且,雪上加霜的是,同样一个小需求,对不同类型的文档,都需要使用不同的类,不同的方法,不同的属性。
下面进入正题,先说说如何使用。
AxContainer模块
首先,确保安装了Qt的AxContainer模块。这个模块只有在Windows下的Qt开发环境才有,Linux等系统中没有,当然也不需要。
在qt的安装过程里,选择模块的那一页,其中有一个复现框内容是QtActive,就是我们需要的模块。确保安装qt环境的时候,选择了QtActive。
其次,需要把它的头文件包含到项目中,另外连接它的库文件。它的CMake文件并不叫QtActive,而是叫AxContainer:
FIND_PACKAGE(Qt6AxContainer REQUIRED)
INCLUDE_DIRECTORIES(${Qt6AxContainer_INCLUDE_DIRES})
QAxObject类
把Qt6Container的头文件目录加入编译环境之后,就可以直接包含QtAxContainer这个头文件,使用QtAxContainer中的类QAxObject了。
#include <QtAxContainer>
QAxObject最常用的构造函数是:
QAxObject(const QString &c, QObject *parent = nullptr);
其中,c指的是Com的名称。比如,要操作Word文档,就传入Word.Application。
如:
auto application = new QAxObject("Word.Application");
QAxObject继承自QAxBase,QAxBase的方法主要有dynamicCall与querySubObject。
dynamicCall用于执行方法调用,它的原型如下:
QVariant dynamicCall(const char *function, const QVariant &var1 = QVariant(), const QVariant &var2 = QVariant(), const QVariant &var3 = QVariant(), const QVariant &var4 = QVariant(), const QVariant &var5 = QVariant(), const QVariant &var6 = QVariant(), const QVariant &var7 = QVariant(), const QVariant &var8 = QVariant())
QVariant dynamicCall(const char *function, QList<QVariant> &vars)
querySubObject用于返回对象的子对象,它的原型如下:
QAxObject * querySubObject(const char *name, const QVariant &var1 = QVariant(), const QVariant &var2 = QVariant(), const QVariant &var3 = QVariant(), const QVariant &var4 = QVariant(), const QVariant &var5 = QVariant(), const QVariant &var6 = QVariant(), const QVariant &var7 = QVariant(), const QVariant &var8 = QVariant())
QAxObject * querySubObject(const char *name, QList<QVariant> &vars)
这两个方法的第一个参数,都是字符串,dynamicCall需要写需要执行的VBA函数原型,而querySubObject需要写子对象名字。
如:
selection->dynamicCall ("GoTo(int, int, int, const QVariant&)", 1, 1, 1, page->property ("Start"));
调用了Office VBA的Goto方法。
Office VBA中,Word文档的Selection对象的Goto方法原型为:
Syntax
expression. GoTo( _What_ , _Which_ , _Count_ , _Name_ )
expression Required. A variable that represents a Selection object.
可以对照着上面,体会QAxObject到Office VBA的关系。
基本上,掌握了QAxObject的调用方法,再结合Office VBA的参考手册就可以随心所欲地做很多事情了。
下面尝试一下操作Word文档。
操作Word文档
操作Word文档,需要使用"Word.Application"这个Com组件,使用QAxObject构造这个组件之后,使用组件之中的Documents对象来打开Word文档对象Document。
所以,从开头到能够操作一个具体的Word文档,需要经过三个“东西”:Word.Application、Documents、Document。
而使用Documents打开Word文档可以任选两个API其中之一,分别是Open和OpenNoRepairDialog:
expression.Open (FileName, ConfirmConversions, ReadOnly, AddToRecentFiles, PasswordDocument, PasswordTemplate, Revert, WritePasswordDocument, WritePasswordTemplate, Format, Encoding, Visible, OpenConflictDocument, OpenAndRepair, DocumentDirection, NoEncodingDialog)
expression.OpenNoRepairDialog (FileName, ConfirmConversions, ReadOnly, AddToRecentFiles, PasswordDocument, PasswordTemplate, Revert, WritePasswordDocument, WritePasswordTemplate, Format, Encoding, Visible, OpenAndRepair, DocumentDirection, NoEncodingDialog, XMLTransform)
使用Open的话,如果文档没有正常关闭,而且OpenAndRepai参数又没有置为假的话,会弹出来一个是否修复的对话框,使用OpenNoRepairDialog就可以简单地避免这个困扰。
但是,PowerPoint与Excel的Com里却并没有这个函数,只能使用复杂的Open函数,而且传入的参数还不一样。
Document对象
把上面的内容合在一起,实现一个打开一个Word文档的函数:
// 为演示方便,下述代码没有进行错误处理以及资源清理
QVariant
loadWord (const QString &filename)
{
auto application = new QAxObject ("Word.Application");
application->setProperty ("Visible", false);
auto documents = application->querySubObject ("Documents");
auto doc = documents->querySubObject (
"OpenNoRepairDialog(const QString&, bool, bool, bool)", qname, false,
true, false);
return doc;
}
取得Range对象
Range是Document的子对象,有多种方法构造,通过Document的Content方法是其中之一。
QVariant
wordContent (QVariant doc)
{
auto range = doc->querySubObject ("Content");
return range;
}
Range对象有很多方法,我们先使用Information方法,查询一下这个Word文档有多少页:
int
wordPageCount (QVariant doc)
{
auto range = doc->querySubObject ("Content");
auto pages = range->dynamicCall ("Information(wdNumberOfPagesInDocument)");
return pages.toInt ();
}
下面,我们把上面的东西再组合起来,把Word文档的其中一页复制到剪贴板里。
// 注意,还是没有资源清理等操作 !!!
bool
wordPageCopy (QVariant doc, int page_num)
{
// 跳转到第page_num页。
auto pageRange = doc->querySubObject ("GoTo(int, int, int, const QVariant&)",
1, 1, page_num);
// 跳转到第page_num的下一页,以便找到page_num的结尾。
auto endRange = mDoc->querySubObject ("GoTo(int, int, int, const QVariant&)",
1, 1, page_num + 1);
int endPosition;
// 如果page_num的下一页没有,表示文档到了末尾,使用Document的Content的结尾
if (endRange->property ("Start").toInt () == 1)
{
auto content = doc->querySubObject ("Content");
endPosition = content->property ("End").toInt ();
}
else
{
endPosition = endRange->property ("Start").toInt () - 1;
}
pageRange->setProperty ("End", endPosition);
// 执行复制方法
pageRange->dynamicCall ("Copy()");
// 还可以执行CopyAsPicture方法,这个方法可以把当前的页面保存成图片
// pageRange->dynamicCall ("CopyAsPicture()");
}
之后,剪贴板里面就有了这篇Word文档的内容,可以在支持粘贴的应用中粘贴了。