Qt5生成Word格式报告

时间:2022-01-15 18:59:00

引言

项目中需要生成word格式的报告文件,初探了Qt5通过word模板生成报告的方法,整理了使用时的环境配置、子线程中使用时的注意事项以及常用的操作方法,于此记录。

环境:vs2012+Qt5.2+word2016

一、使用ActiveQt模块

注意:ActiveQt只适用于windows平台下,linux和macOS版本的Qt中是没有这个库的

首先需要添加库文件,可以直接在VS2012菜单栏Qt5->Qt Project Setting->勾选Active Qt
Qt5生成Word格式报告
勾选之后再次查看Qt Project Setting,发现自动勾上了Active Qt server,同时“项目属性->配置属性->链接器->输入->附加依赖性”中自动加入了Qt5AxContainerd.lib;Qt5AxBased.lib
此后便可成功include头文件

#include <QAxWidget>
#include <QAxObject>

二、子线程中使用

在使用过程中发现调用word过程比较耗时,会阻塞GUI线程,于是将保存报告操作移到子线程中.
不过在子线程中使用QAxWidget会报错ASSERT failure in QWidget: "Widgets must be created in the GUI thread."这是由于线程里面不能创建GUI对象。
解决方案是用 QAxObjec取代QAxWidget,初始化过程如下:

bool Report::Open(QString Dir)
{
// 新建一个word应用程序,并设置为不可见
//m_WordFile = new QAxWidget("Word.Application", 0, Qt::MSWindowsOwnDC);
m_WordFile = new QAxObject();//取代QAxWidget,使其在子线程中可用
bool bFlag = m_WordFile->setControl( "word.Application" );
if(NULL == m_WordFile)
{
// 尝试用wps打开
bFlag = m_WordFile->setControl( "kwps.Application" );
if(!bFlag)
{
return false;
}
}
m_WordFile->setProperty("Visible", false);
// 获取所有的工作文档
QAxObject *Documents = m_WordFile->querySubObject("Documents");
if(NULL == Documents)
{
return false;
}
// 以文件template.dot为模版新建一个文档
Documents->dynamicCall("Add(QString)", Dir);

// 获取当前激活的文档
m_Document = m_WordFile->querySubObject("ActiveDocument");
if(NULL == m_Document)
{
return false;
}
m_bInit = true;
return true;
}

同时由于在QApplication的主线程中,会自动初始化COM库,而新开辟的子线程不会自动初始化COM库,所以需要我们手动来初始化,方法如下:
添加头文件:

#include <windows.h> 

构造函数中初始化COM库:

Report::Report(QObject *parent)
: QObject(parent)
{
HRESULT result = OleInitialize(0);

if (result != S_OK && result != S_FALSE)
{
qDebug()<<QString("Could not initialize OLE (error %x)").arg((unsigned int)result);
}
//moveToThread方法产生线程
this->moveToThread(&m_thread);
m_thread.start();
}

析构函数中释放:

Report::~Report()
{
OleUninitialize();
m_thread.quit();
m_thread.wait();
}

三、准备word模板

在word文档中手动添加书签(bookmark)后保存为dot格式
Qt5生成Word格式报告

四、代码

1.插入书签位置

QString outFileName = QFileDialog::getSaveFileName(this, QStringLiteral("请输入要保存的名字:"),".", "Microsoft Word 97-2003(*.doc);;Microsoft Word 2007-2013(*.docx)");
if (outFileName.isEmpty()) {
QMessageBox::warning(this, tr("警告"),tr("输入的文件名为空!"),QMessageBox::Ok);
return ;
}

// 新建一个word应用程序,并设置为不可见
QAxWidget *word=new QAxWidget("Word.Application", 0, Qt::MSWindowsOwnDC);
word->setProperty("Visible", false);
// 获取所有的工作文档
QAxObject * documents = word->querySubObject("Documents");
// 以文件testTemplate.dot为模版新建一个文档,注意这里的路径为绝对路径
QDir dir(".");
documents->dynamicCall("Add(QString)",QString("%1/testTemplate.dot").arg(dir.absolutePath()));
// 获取当前激活的文档
QAxObject *document=word->querySubObject("ActiveDocument");
// 获取文档中名字为TSName_1_1的标签
QString bookmakrName="TSName_1_1";
QAxObject*bookmark_text=document->querySubObject(QString("Bookmarks(%1)").arg(bookmakrName).toLocal8Bit().data());
// 选中标签,将字符插入到标签位置
if(!bookmark_text->isNull())
{
bookmark_text->dynamicCall("Select(void)");
bookmark_text->querySubObject("Range")->setProperty("Text",QStringLiteral("测试输入"));
}

// 将文件另存为outFileName,关闭工作文档,退出应用程序
document->dynamicCall("SaveAs (const QString&)", outFileName);
document->dynamicCall("Close (boolean)", true); //关闭文本窗口
word->dynamicCall("Quit(void)"); //退出word

delete bookmark_text;
delete document;
delete documents;
delete word;

2.批量插入

QString outFileName = QFileDialog::getSaveFileName(this, QStringLiteral("请输入要保存的名字:"),".", "Microsoft Word 97-2003(*.doc);;Microsoft Word 2007-2013(*.docx)");
if (outFileName.isEmpty()) {
QMessageBox::warning(this, tr("警告"),tr("输入的文件名为空!"),QMessageBox::Ok);
return ;
}
word = new QAxWidget("Word.Application", 0, Qt::MSWindowsOwnDC);
word->setProperty("Visible", false);
word->setProperty("DisplayAlerts", true);

QAxObject *docs = word->querySubObject("Documents");
if (!docs) {
QMessageBox::warning(this, tr("警告"), tr("无法获得Documents对象!"),QMessageBox::Ok);
return ;
}

QStringList items;
QStringList sometexts;
//items按顺序依次是“待匹配标签名”和“插入的内容”
items<<"TSName_1_2"<<"TSName222"<<"TSName_1_1"<<"TSName111"<<"555";
sometexts<<"111"<<"222"<<"333"<<"444"<<"555";

editBookMarks(docs, sometexts, items, outFileName);

word->dynamicCall("Quit(boolean)", true);
delete word;
void testword::editBookMarks(QAxObject *docs, QStringList sometexts, QStringList &itemList, QString outFileName)
{
QDir dir(".");
docs->dynamicCall("Add(QString)", QString("%1/testTemplate.dot").arg(dir.absolutePath()));

QAxObject *currentDoc = word->querySubObject("ActiveDocument");
if(!currentDoc){
QMessageBox::warning(this, QStringLiteral("警告"), QStringLiteral("无法获取当前打开文件对象!"),QMessageBox::Ok);
return;
}
QAxObject *allBookmarks = currentDoc->querySubObject("Bookmarks");
if (!allBookmarks) {
QMessageBox::warning(this, QStringLiteral("警告"), QStringLiteral("无法获取模板中的书签,请先插入书签!"), QMessageBox::Ok);
return ;
}

int count = allBookmarks->property("Count").toInt();

/* 填写模板中的书签 */
for (int i = count; i > 0; --i) {
QAxObject *bookmark = allBookmarks->querySubObject("Item(QVariant)", i);
QString name= bookmark->property("Name").toString();
int j=0;
foreach(QString itemName , itemList){
if (name == itemName) {
QAxObject *curBM = currentDoc->querySubObject("Bookmarks(QString)", name);
curBM->querySubObject("Range")->setProperty("Text", itemList.at(j+1));
break;
}
j++;
}
if (j == itemList.length()) {//如果遍历itemList,未找到匹配的书签,提示输入
QString text = QInputDialog::getText(this, QStringLiteral("请输入"), QStringLiteral("%1").arg(name));
bookmark->querySubObject("Range")->setProperty("Text", text);
itemList.append(name);
itemList.append(text);
}
}
//依次插入sometexts中内容
while(!sometexts.isEmpty()){
QAxObject *currentRange = currentDoc->querySubObject("Range()");
int rangeEnd = currentRange->property("End").toInt();
currentRange->dynamicCall("setRange(QVariant, QVariant)", rangeEnd, rangeEnd);
currentRange->dynamicCall("InsertAfter(QString)", QStringLiteral("\n%1-%3\n").arg(sometexts[0]).arg(1));

sometexts.removeAt(0);
}

currentDoc->dynamicCall("SaveAs(QString&)", outFileName);
currentDoc->dynamicCall("Close()");
}

效果如下:
Qt5生成Word格式报告

3.插入表格

方法一:利用Range对象定位后插入表格

/******************************************************************************
* 函数:intsertTable
* 功能:创建表格
* 参数:nStart 开始位置; nEnd 结束位置; row hang; column 列
* 返回值: void
*****************************************************************************/

void WordEngine::intsertTable(int nStart, int nEnd, int row, int column)
{
QAxObject* ptst = m_wordDocuments->querySubObject( "Range( Long, Long )",
nStart, nEnd );
QAxObject* pTable = m_wordDocuments->querySubObject( "Tables" );
QVariantList params;
params.append(ptst->asVariant());
params.append(row);
params.append(column);
if( pTable )
{
pTable->dynamicCall( "Add(QAxObject*, Long ,Long )",params);
}
// QAxObject* table = selection->querySubObject("Tables(1)");
// table->setProperty("Style", "网格型");
}

方法二:利用bookmark定位后插入表格

QAxObject *WordEngine::insertTable(QString sLabel, int row, int column)  
{
QAxObject *bookmark = m_pWorkDocument->querySubObject("Bookmarks(QVariant)", sLabel);
if(bookmark)
{
bookmark->dynamicCall("Select(void)");
QAxObject *selection = m_pWord->querySubObject("Selection");

selection->dynamicCall("InsertAfter(QString&)", "\n");
//selection->dynamicCall("MoveLeft(int)", 1);
selection->querySubObject("ParagraphFormat")->dynamicCall("Alignment", "wdAlignParagraphCenter");
//selection->dynamicCall("TypeText(QString&)", "Table Test");//设置标题

QAxObject *range = selection->querySubObject("Range");
QAxObject *tables = m_pWorkDocument->querySubObject("Tables");
QAxObject *table = tables->querySubObject("Add(QVariant,int,int)",range->asVariant(),row,column);

for(int i=1;i<=6;i++)
{
QString str = QString("Borders(-%1)").arg(i);
QAxObject *borders = table->querySubObject(str.toAscii().constData());
borders->dynamicCall("SetLineStyle(int)",1);
}
return table;
}
}

插入表格,修改列宽等是后来看到的,可参考这个github项目中的WordEngine实现

五、其他

用以下方法往bookmark插入内容:

QString bookmakrName="TSName1_1_2";//假设dot文件中并没有这个bookmark
QAxObject* bookmark_text=document->querySubObject("Bookmarks(const QString&)", bookmakrName);
if(NULL == bookmark_text)//注意这个判断不可少,否则下面调用isNull()时会出错
{
return;
}
// 选中标签,将字符插入到标签位置
if(!bookmark_text->isNull()) //如果没有匹配到对应的bookmark,直接判断会出错,所以要提前返回
{
bookmark_text->dynamicCall("Select(void)");
bookmark_text->querySubObject("Range")->setProperty("Text",QStringLiteral("测试输入"));
}

如果dot文件中并没有这个bookmark,会报如下错误:

QAxBase: Error calling IDispatch member Bookmarks: Exception thrown by server
Code : 5941
Source : Microsoft Word
Description: ????????????
Help : wdmain11.chm [25421]
Connect to the exception(int,QString,QString,QString) signal to catch this exception

这时判断NULL == bookmark_text返回即可,如果要避免匹配不存在的bookmark,可以在前面处理,比如上文“2.批量插入”中所示的先利用QAxObject *allBookmarks = currentDoc->querySubObject("Bookmarks");获取所有Bookmarks,然后在进行处理。

最后附上相关源码:demo-Qt5生成Word格式报告(demo是最开始写的,未包含插入表格,多线程等方法实现,比较简单)

参考

Qt利用ActiveX生成Word文档
qt中如何使用ActiveX读写word
github-试卷自动生成系统
github-QTScada
QT在子线程中使用QAxWidget需要初始化COM的问题