65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (55投票s)

2009年4月5日

CPOL

9分钟阅读

viewsIcon

307325

downloadIcon

18297

使用 C++ 自动化 MS Word 和 MS Excel 的简单指南。

引言

“使用 C++ 实现 MS Office 自动化”——这是几周前我开始在网上搜索的内容,目的是通过我的程序在 Excel 表格中绘制图表。幸运的是,我从网络世界中获得了一些(实际上很少)输入,这可能是因为我在互联网上搜索能力较弱。本文是为那些仍然使用相同关键词在网上搜索的人而写。

OLEAutoWord.JPG

OLEAutoExcel.JPG

对象链接与嵌入

早期,我曾惊叹于 Visual Basic 比其他任何编程语言都容易使用。只需在项目中包含媒体播放器组件即可创建一个媒体播放器,只需在项目中包含 Web 浏览器组件即可创建一个 Web 浏览器等。当我尝试使用 Visual C++ 做同样的事情时,我发现它不像 VB 那样容易。作为一个新手,我遇到了很多链接器错误。上面的故事发生在八年前。对象链接与嵌入,俗称 OLE,是一种基于 COM 的架构,它在开发软件应用程序时提供了灵活性和可重用性。正如我所说,如果你需要开发一个媒体播放器应用程序,代码方面就没有太多工作要做。只需包含专家已经开发的所需组件即可。使用 OLE,我们正在链接到组件并将其嵌入到我们的应用程序中。Windows 中的 OLE 无处不在,你可以将图片、视频或音乐文件复制粘贴到 Word 文档中,你可以使用 Internet Explorer 打开 PDF、Excel 或 Word 文件等等……

ComponentesInterfacesClients.JPG

你可以在 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 类,例如 COleVariantCOleSafeArrayCOleException,简化了自动化任务。通常建议使用此方法而不是其他方法,并且大多数 Microsoft 知识库示例都使用 MFC。
  • #import 是 Visual C++ 5.0 中引入的新指令,它从指定的类型库创建 VC++“智能指针”。它功能非常强大,但由于与 Microsoft Office 应用程序一起使用时通常会出现引用计数问题,因此通常不建议使用。
  • C/C++ 自动化要困难得多,但有时为了避免 MFC 的开销或 #import 的问题,这是必要的。基本上,你需要使用 CoCreateInstance() 等 API 以及 IDispatchIUnknown 等 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”属性设置为 trueptName 将是方法或属性的实际名称,与 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_METHODDISPATCH_PROPERTYPUTDISPATCH_PROPERTYGET 的用法。我们需要根据与 OLEMethod() 一起使用的方法或属性来决定在何处使用哪一个。
  • 尝试理解本文“IDispatch 接口”部分中解释的 GetIDsOfNames()Invoke(),它们比此处提供的其他信息更重要。
  • COleVariantVARIANT 结构体(联合)的类版本,它使我们更容易用值初始化变体。
  • OLEMethod() 函数接收反向的可变参数。例如,AddPicture 参数的实际顺序是 <文件名, >,而在 OLEMethod 中,调用顺序是
  • 如果您的客户端应用程序关闭后,Word 或 Excel 应用程序仍然留在内存中(通过任务管理器检查),请确保您已释放所有使用的 IDispatch 对象。

结论

上面解释的所有概念对于 Excel 也同样适用。有关 Excel 的用法,请参阅演示源代码。本文的目的不是为您提供 MS Word 和 MS Excel 自动化的完整方法和属性集,而是帮助您自己进行自动化。祝您一切顺利。

© . All rights reserved.