使用 C++ 进行 MS Office OLE 自动化






4.82/5 (55投票s)
使用 C++ 自动化 MS Word 和 MS Excel 的简单指南。
引言
“使用 C++ 实现 MS Office 自动化”——这是几周前我开始在网上搜索的内容,目的是通过我的程序在 Excel 表格中绘制图表。幸运的是,我从网络世界中获得了一些(实际上很少)输入,这可能是因为我在互联网上搜索能力较弱。本文是为那些仍然使用相同关键词在网上搜索的人而写。
对象链接与嵌入
早期,我曾惊叹于 Visual Basic 比其他任何编程语言都容易使用。只需在项目中包含媒体播放器组件即可创建一个媒体播放器,只需在项目中包含 Web 浏览器组件即可创建一个 Web 浏览器等。当我尝试使用 Visual C++ 做同样的事情时,我发现它不像 VB 那样容易。作为一个新手,我遇到了很多链接器错误。上面的故事发生在八年前。对象链接与嵌入,俗称 OLE,是一种基于 COM 的架构,它在开发软件应用程序时提供了灵活性和可重用性。正如我所说,如果你需要开发一个媒体播放器应用程序,代码方面就没有太多工作要做。只需包含专家已经开发的所需组件即可。使用 OLE,我们正在链接到组件并将其嵌入到我们的应用程序中。Windows 中的 OLE 无处不在,你可以将图片、视频或音乐文件复制粘贴到 Word 文档中,你可以使用 Internet Explorer 打开 PDF、Excel 或 Word 文件等等……
你可以在 HKEY_CLASSES_ROOT\CLSID\{<___CLSID___>} 下找到许多不同应用程序的注册组件,其中 {<___CLSID___>} 是变体(每个注册组件的唯一类 ID)。
COM 和接口
我在这里无法对 COM 和接口说出任何新内容。COM 对象,顾名思义,是一个组件,可以通过其接口轻松地附加到任何应用程序。COM 组件可以有任意数量的接口,应用程序不一定需要使用其所有接口。接口不过是一个纯虚类。它没有实现代码,仅用于应用程序与 COM 对象之间的通信。
使用 C++ 实现 MS Office 自动化
让我们从 Microsoft 对“使用 C++ 实现 MS Office 自动化”的说法开始
“自动化(以前称为 OLE 自动化)是一种技术,允许你利用现有程序的功能并将其整合到自己的应用程序中。”
- 使用 MFC 时,请使用 Visual C++ ClassWizard 从 Microsoft Office 类型库生成“包装类”。这些类以及其他 MFC 类,例如
COleVariant
、COleSafeArray
和COleException
,简化了自动化任务。通常建议使用此方法而不是其他方法,并且大多数 Microsoft 知识库示例都使用 MFC。 #import
是 Visual C++ 5.0 中引入的新指令,它从指定的类型库创建 VC++“智能指针”。它功能非常强大,但由于与 Microsoft Office 应用程序一起使用时通常会出现引用计数问题,因此通常不建议使用。- C/C++ 自动化要困难得多,但有时为了避免 MFC 的开销或
#import
的问题,这是必要的。基本上,你需要使用CoCreateInstance()
等 API 以及IDispatch
和IUnknown
等 COM 接口。
以上声明纯粹摘自 Microsoft 网站 Office Automation Using Visual C++。本文主要讨论上面提到的第三点,即使用 COM 接口的 C/C++ 自动化,本文仅以 MS Word 为例进行详细说明。有关类似的 MS Excel 内容,请参阅演示源代码。
初始化 MS Word 应用程序
CoInitialize(NULL);
CLSID clsid;
HRESULT hr = CLSIDFromProgID(L"Word.Application", &clsid);
// “Excel.Application” for MSExcel
IDispatch *pWApp;
if(SUCCEEDED(hr))
{
hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER,
IID_IDispatch, (void **)&pWApp);
}
调用 CoInitialize()
以初始化当前线程的 COM 库,即当前线程将加载 COM 库 DLL。之后,我们需要调用 CoUnInitialize()
以从内存中卸载已加载的 COM DLL。正如我之前提到的,所有注册组件都可以在注册表的“HKCR\CLSID\”下找到。你还可以找到组件的 PROGID(程序 ID)。使用 CLSIDFromProgID()
API 获取组件的类 ID,因为我们只能通过使用 CLSID 获取 COM 对象。对于 MS Word,“Word.Application.xx”是与版本相关的 PROGID,其中 xx 是系统中安装的 MS Word 的版本。为了方便起见,为了编写与版本无关的代码,MSWord 提供了另一个 PROGID“Word.Application”,它位于“VersionIndependentProgID
”下。使用 MS Word CLSID 调用 CoCreateInstance()
以获取 MS Word 应用程序的实例。pWApp
(IDispatch
接口) 应该接收一个有效的 MS Word 组件接口对象。
IDispatch 接口
IDispatch
是从 IUnknown
(基接口)派生而来的接口,应用程序使用它向其他应用程序(我们的程序)公开方法和属性,以利用其功能。简单来说,我们使用 CoCreateInstance()
获取的 MS Word 的 IDispatch
指针就是接口对象,它将帮助我们通过程序使用 MS Word 的方法和属性。除了 IUnknown
成员之外,IDispatch
还有四个成员函数来支持 OLE 自动化。
GetTypeInfoCount()
GetTypeInfo()
)GetIDsOfNames()
Invoke()
客户端应用程序(我们的程序)将使用 IDispatch::Invoke()
方法调用 MS Word(或任何其他组件)的方法和属性。但是,IDispatch::Invoke()
不能接收或理解 MS Word 组件的实际方法名或属性名。它只能理解 DISPID。DISPID 是一个 32 位值,表示组件的实际方法或属性。GetIDsOfName()
是我们可以用来获取组件方法或属性的 DISPID 的函数。例如,请参阅以下代码,该代码设置 MS Word 对象的“Visible
”属性
DISPID dispID;
VARIANT pvResult;
LPOLESTR ptName=_T("Visible");
hr = pWApp->GetIDsOfNames(IID_NULL, &ptName, 1, LOCALE_USER_DEFAULT, &dispID);
if(SUCCEEDED(hr))
{
VARIANT x;
x.vt = VT_I4;
x.lVal =1; // 1=visible; 0=invisible;
DISPID prop=DISPATCH_PROPERTYPUT;
DISPPARAMS dp = { NULL,NULL,0,0 };
dp.cArgs =1;
dp.rgvarg =&x;
dp.cNamedArgs=1;
dp.rgdispidNamedArgs= ∝
hr = pWApp->Invoke(dispID, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT,
&dp, &pvResult, NULL, NULL);
}
获取“Visible
”的 DISPID,使用 DISPID 调用 Invoke()
将“Visible
”属性设置为 true
。ptName
将是方法或属性的实际名称,与 GetIDsOfNames()
方法一起使用以获取等效的 DISPID。DISPPARAMS
包含 DISPID 的参数(包括方法参数或属性值),与 Invoke()
方法一起使用,该方法是方法或属性的实际调用。
为了使代码更易于使用,以下是调用 OLE 方法或设置/获取 OLE 属性的通用函数
HRESULT OLEMethod(int nType, VARIANT *pvResult,
IDispatch *pDisp,LPOLESTR ptName, int cArgs...)
{
if(!pDisp) return E_FAIL;
va_list marker;
va_start(marker, cArgs);
DISPPARAMS dp = { NULL, NULL, 0, 0 };
DISPID dispidNamed = DISPID_PROPERTYPUT;
DISPID dispID;
char szName[200];
// Convert down to ANSI
WideCharToMultiByte(CP_ACP, 0, ptName, -1, szName, 256, NULL, NULL);
// Get DISPID for name passed...
HRESULT hr= pDisp->GetIDsOfNames(IID_NULL, &ptName, 1,
LOCALE_USER_DEFAULT, &dispID);
if(FAILED(hr)) {
return hr;
}
// Allocate memory for arguments...
VARIANT *pArgs = new VARIANT[cArgs+1];
// Extract arguments...
for(int i=0; i<cArgs; i++) {
pArgs[i] = va_arg(marker, VARIANT);
}
// Build DISPPARAMS
dp.cArgs = cArgs;
dp.rgvarg = pArgs;
// Handle special-case for property-puts!
if(nType & DISPATCH_PROPERTYPUT) {
dp.cNamedArgs = 1;
dp.rgdispidNamedArgs = &dispidNamed;
}
// Make the call!
hr = pDisp->Invoke(dispID, IID_NULL, LOCALE_SYSTEM_DEFAULT,
nType, &dp, pvResult, NULL, NULL);
if(FAILED(hr)) {
return hr;
}
// End variable-argument section...
va_end(marker);
delete [] pArgs;
return hr;
}
以上函数在 Microsoft 支持文章中实际命名为 AutoWrap()
。关于该函数没有什么新的可以解释的,因为我已经解释过 GetIDsOfName()
和 Invoke()
调用,只是它们被分离到一个函数中。此外,该函数使用可变参数来处理不同方法/属性的不同数量的参数。现在,要设置 MS Word 对象的 Visible
属性,使用这个通用函数会更简单
VARIANT x;
x.vt = VT_I4;
x.lVal = 1; // 1=visible; 0=invisible;
hr=OLEMethod(DISPATCH_PROPERTYPUT, NULL, pWApp, L"Visible", 1, x);
请注意,OLEMethod
接收可变参数。也就是说,你可以根据属性/方法传递任意数量的参数。以下是 OLEMethod()
参数的摘要:
nType
– 要进行的调用类型,可以是以下任何值:DISPATCH_PROPERTYPUT
- 设置属性值DISPATCH_PROPERTYGET
- 获取属性值DISPATCH_METHOD
- 调用方法pvResult
– 调用返回的值;它可以是另一个IDispatch
对象,或一个整数值,或一个布尔值等等。pDisp
– 要进行调用的IDispatch
接口对象。ptName
– 属性或方法名称。cArgs
– 此参数后面的参数数量。- … 调用的参数按逆序排列(它可以是属性的值,或者是
IDispatch
对象方法的参数)。
方法和属性
MS Word 应用程序有许多属性和方法,这里无法一一解释。我将在这里解释几个函数;有关更多函数,请参阅源代码,因为所有方法/属性调用的代码都将类似。在本节末尾,我将告诉您如何在需要时查找方法名或属性名及其参数或值。
打开 Word 文档
HRESULT CMSWord::OpenDocument(LPCTSTR szFilename, bool bVisible)
{
if(m_pWApp==NULL)
{
if(FAILED(m_hr=Initialize(bVisible)))
return m_hr;
}
COleVariant vFname(szFilename);
VARIANT fname=vFname.Detach();
// GetDocuments
{
VARIANT result;
VariantInit(&result);
m_hr=OLEMethod(DISPATCH_PROPERTYGET, &result, m_pWApp,
L"Documents", 0);
m_pDocuments= result.pdispVal;
}
// OpenDocument
{
VARIANT result;
VariantInit(&result);
m_hr=OLEMethod(DISPATCH_METHOD, &result, m_pDocuments,
L"Open", 1, fname);
m_pActiveDocument = result.pdispVal;
}
return m_hr;
}
要打开新文档,将“Open”替换为“Add”并将参数计数更改为 0。请注意,在上面的代码中,“result
”是输出参数,它保存“Documents”和“Open”(活动文档)的 IDispatch
对象。
关闭所有打开的 Word 文档
HRESULT CMSWord::CloseDocuments()
{
if(m_pWApp==NULL) return E_FAIL;
{
VARIANT result;
VariantInit(&result);
m_hr=OLEMethod(DISPATCH_METHOD, &result, m_pDocuments,
L"Close", 0);
m_pDocuments=NULL;
m_pActiveDocument=NULL;
}
return m_hr;
}
以下代码将设置活动文档中选定文本的字体
HRESULT CMSWord::SetFont(LPCTSTR szFontName, int nSize,
bool bBold, bool bItalic,COLORREF crColor)
{
if(!m_pWApp || !m_pActiveDocument) return E_FAIL;
IDispatch *pDocApp;
{
VARIANT result;
VariantInit(&result);
OLEMethod(DISPATCH_PROPERTYGET, &result,
m_pActiveDocument, L"Application", 0);
pDocApp= result.pdispVal;
}
IDispatch *pSelection;
{
VARIANT result;
VariantInit(&result);
OLEMethod(DISPATCH_PROPERTYGET, &result,
pDocApp, L"Selection", 0);
pSelection=result.pdispVal;
}
IDispatch *pFont;
{
VARIANT result;
VariantInit(&result);
OLEMethod(DISPATCH_PROPERTYGET, &result,
pSelection, L"Font", 0);
pFont=result.pdispVal;
}
{
COleVariant oleName(szFontName);
m_hr=OLEMethod(DISPATCH_PROPERTYPUT, NULL, pFont,
L"Name", 1, oleName.Detach());
VARIANT x;
x.vt = VT_I4;
x.lVal = nSize;
m_hr=OLEMethod(DISPATCH_PROPERTYPUT, NULL, pFont, L"Size", 1, x);
x.lVal = crColor;
m_hr=OLEMethod(DISPATCH_PROPERTYPUT, NULL, pFont, L"Color", 1, x);
x.lVal = bBold?1:0;
m_hr=OLEMethod(DISPATCH_PROPERTYPUT, NULL, pFont, L"Bold", 1, x);
x.lVal = bItalic?1:0;
m_hr=OLEMethod(DISPATCH_PROPERTYPUT, NULL, pFont, L"Italic", 1, x);
}
pFont->Release();
pSelection->Release();
pDocApp->Release();
return m_hr;
}
在活动文档中插入图片
HRESULT CMSWord::InserPicture(LPCTSTR szFilename)
{
if(!m_pWApp || !m_pActiveDocument) return E_FAIL;
IDispatch *pDocApp;
{
VARIANT result;
VariantInit(&result);
OLEMethod(DISPATCH_PROPERTYGET, &result, m_pActiveDocument,
L"Application", 0);
pDocApp= result.pdispVal;
}
IDispatch *pSelection;
{
VARIANT result;
VariantInit(&result);
OLEMethod(DISPATCH_PROPERTYGET, &result, pDocApp, L"Selection", 0);
pSelection=result.pdispVal;
}
IDispatch *pInlineShapes;
{
VARIANT result;
VariantInit(&result);
OLEMethod(DISPATCH_PROPERTYGET, &result, pSelection, L"InlineShapes", 0);
pInlineShapes=result.pdispVal;
}
{
COleVariant varFile(szFilename);
COleVariant varLink((BYTE)0);
COleVariant varSave((BYTE)1);
OLEMethod(DISPATCH_METHOD,NULL,pInlineShapes,L"AddPicture",3,
varSave.Detach(),varLink.Detach(),varFile.Detach());
}
return m_hr;
}
我们如何识别 MS Word 应用程序所需的方法/属性?答案很简单,看看上面的函数,它们可以简单地解释为
Application.Documents.Open(szFilename)
Documents.Close()
ActiveDocument.Application.Selection.Font.Name=szFontName
ActiveDocument.Application.Selection.Font.Size=nSize
ActiveDocument.Application.Selection.Font.Color=crColor
ActiveDocument.Application.Selection.Font.Bold=bBold
ActiveDocument.Application.Selection.Font.Italic=bItalic
ActiveDocument.Application.Selection.InlineShapes.AddPicture(szFilename,false,true)
这是否与我们熟悉的事物相似?是的,我们在 MS Word 或 MS Excel 中创建宏时经常看到这些。这些是 VBA 脚本。所以,你难道不觉得获取应用程序所需的方法或属性名称会容易得多吗?
当你想知道如何将图片插入 Word 文档时该怎么办?
- 打开 MS Word
- 打开新文档
- 转到“工具”->“宏”->“录制新宏”
- 选择一个键盘宏并为该宏分配一个键
- 宏录制开始后,转到“插入”->“图片”->“来自文件”
- 选择一个图像文件
- 停止宏
- 转到“工具”->“宏”->“Visual Basic 编辑器 (F11)”
- 在“NewMacros”下,你可以在末尾看到你录制的宏
- 查看代码
Selection.InlineShapes.AddPicture FileName:=<your-image>,
LinkToFile:=False, SaveWithDocument=True
现在,将其与上面的 InsertPicture()
函数进行比较,以便您了解它在 C++ 中是如何编码的。
因此,无论您想通过自动化用 MS Word 完成什么任务,首先在 MS Word 本身中使用示例宏完成该任务,您将了解方法及其参数、属性名称及其值。然后使用 OLEMethod()
调用即可轻松完成任务。
注意事项
VARIANT
是一个结构体联合,字面意思是“不确定”。是的,我们不确定数据类型,所以它可以是任何类型。使用VARIANT
,我们可以获取BYTE
值或无符号长整型值,或者IUnknown
对象,或者在需要时所需的任何值。那些熟悉 COM 的人应该了解这一点。- 确保了解
OLEMethod()
或Invoke()
调用中DISPATCH_METHOD
、DISPATCH_PROPERTYPUT
和DISPATCH_PROPERTYGET
的用法。我们需要根据与OLEMethod()
一起使用的方法或属性来决定在何处使用哪一个。 - 尝试理解本文“IDispatch 接口”部分中解释的
GetIDsOfNames()
和Invoke()
,它们比此处提供的其他信息更重要。 COleVariant
是VARIANT
结构体(联合)的类版本,它使我们更容易用值初始化变体。OLEMethod()
函数接收反向的可变参数。例如,AddPicture
参数的实际顺序是 <文件名, >,而在OLEMethod
中,调用顺序是。 - 如果您的客户端应用程序关闭后,Word 或 Excel 应用程序仍然留在内存中(通过任务管理器检查),请确保您已释放所有使用的
IDispatch
对象。
结论
上面解释的所有概念对于 Excel 也同样适用。有关 Excel 的用法,请参阅演示源代码。本文的目的不是为您提供 MS Word 和 MS Excel 自动化的完整方法和属性集,而是帮助您自己进行自动化。祝您一切顺利。