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

COM 简介 - 它是什么以及如何使用它。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (506投票s)

2000年7月2日

viewsIcon

2338802

downloadIcon

15733

一篇为 COM 新手程序员准备的教程,解释了如何重用现有的 COM 组件,例如 Windows shell 中的组件。

  • 下载演示源代码 - 7 Kb
  • 本文目的

    我为刚开始接触 COM 并需要一些帮助来理解基础知识的程序员写了这篇教程。本文简要介绍了 COM 规范,然后解释了一些 COM 术语,并描述了如何重用现有的 COM 组件。本文涉及编写您自己的 COM 对象或接口。

    更新

    2000 年 7 月 22 日:增加了几段关于使用 Release() 和卸载 DLL 的内容。还增加了关于 HRESULT 的章节

    引言

    COM(Component Object Model,组件对象模型)是如今在 Windows 世界中似乎无处不在的流行三字母缩写词(TLA)。不断有大量新技术涌现,它们都基于 COM。文档中充斥着诸如COM 对象接口服务器等术语,但都假设您已经熟悉 COM 的工作原理和使用方法。

    本文从头开始介绍 COM,描述其底层机制,并展示如何使用由他人(特别是 Windows shell)提供的 COM 对象。读完本文后,您将能够使用 Windows 内置以及第三方提供的 COM 对象。

    本文假设您精通 C++。我在示例代码中使用了一点 MFC 和 ATL,但我会详细解释代码,所以即使您不熟悉 MFC 或 ATL,也应该能跟上。本文的章节包括:

    COM - 它究竟是什么? - 快速介绍 COM 标准,以及它旨在解决的问题。您不需要了解这些才能使用 COM,但我仍建议阅读它,以理解为什么 COM 中的事情要这样做。

    基本元素的定义 - COM 术语以及这些术语所代表内容的描述。

    使用 COM 对象 - 关于如何创建、使用和销毁 COM 对象的概述。

    基础接口 - IUnknown - 对基础接口 IUnknown 中方法的描述。

    请密切注意 - 字符串处理 - 如何在 COM 代码中处理字符串。

    融会贯通 - 示例代码 - 两组示例代码,用以说明本文讨论的所有概念。

    处理 HRESULT - 对 HRESULT 类型的描述,以及如何测试错误和成功代码。

    参考资料 - 如果您的雇主允许,您应该报销的那些书。:)

    COM - 它究竟是什么?

    简而言之,COM 是一种在不同应用程序和语言之间共享二进制代码的方法。这与 C++ 的方法不同,C++ 提倡重用源代码。ATL 就是一个完美的例子。虽然源代码级别的重用效果很好,但它只适用于 C++。它还可能引入命名冲突的问题,更不用说项目中存在多份代码副本导致的臃肿。

    Windows 允许您使用 DLL 在二进制级别共享代码。毕竟,Windows 应用程序就是这样运作的——重用 kernel32.dll、user32.dll 等。但是由于这些 DLL 是按照 C 接口编写的,它们只能被 C 或理解 C 调用约定的语言使用。这将共享的负担放在了编程语言实现者身上,而不是 DLL 本身。

    MFC 引入了另一种二进制共享机制,即 MFC 扩展 DLL。但这些限制更多——您只能从 MFC 应用程序中使用它们。

    COM 通过定义一个二进制标准解决了所有这些问题,这意味着 COM 规定二进制模块(DLL 和 EXE)必须被编译成符合特定结构的格式。该标准还精确规定了 COM 对象在内存中必须如何组织。这些二进制文件也必须不依赖于任何编程语言的特性(例如 C++ 中的名称修饰)。一旦做到这一点,这些模块就可以从任何编程语言中轻松访问。二进制标准将兼容性的负担放在了生成二进制文件的编译器上,这使得后来需要使用这些二进制文件的人们变得容易得多。

    COM 对象在内存中的结构恰好与 C++ 虚函数使用的结构相同,这就是为什么很多 COM 代码使用 C++ 编写的原因。但请记住,模块是用什么语言编写的并不重要,因为最终生成的二进制文件对所有语言都是可用的。

    顺便说一下,COM 并非 Win32 特有。理论上,它可以被移植到 Unix 或任何其他操作系统。然而,我从未在 Windows 世界之外见过 COM 的提及。

    基本元素的定义

    让我们从底层向上看。一个接口(interface)只是一组函数。这些函数被称为方法(method)。接口名称以 I 开头,例如 IShellLink。在 C++ 中,接口被编写为一个只包含纯虚函数的抽象基类。

    接口可以从其他接口继承(inherit)。继承的工作方式就像 C++ 中的单继承。接口不允许进行多重继承。

    一个 coclasscomponent object class 的缩写)包含在 DLL 或 EXE 中,并包含一个或多个接口背后的代码。这个 coclass 被称为实现(implement)了那些接口。一个 COM 对象(COM object)是 coclass 在内存中的一个实例。请注意,COM 中的“class”与 C++ 中的“class”不同,尽管通常 COM 类的实现就是一个 C++ 类。

    一个 COM 服务器(COM server)是一个包含一个或多个 coclass 的二进制文件(DLL 或 EXE)。

    注册(Registration)是创建注册表条目的过程,这些条目告诉 Windows COM 服务器位于何处。反注册(Unregistration)则相反——移除那些注册表条目。

    一个 GUID(发音与 "fluid" 押韵,代表globally unique identifier,全局唯一标识符)是一个 128 位的数字。GUID 是 COM 用来标识事物的、与语言无关的方式。每个接口和 coclass 都有一个 GUID。由于 GUID 在全世界范围内都是唯一的,因此避免了名称冲突(只要您使用 COM API 来创建它们)。您有时也会看到术语 UUID(代表universally unique identifier,通用唯一标识符)。UUID 和 GUID 在所有实际用途上是相同的。

    一个类 ID(class ID),或称 CLSID,是命名一个 coclass 的 GUID。一个接口 ID(interface ID),或称 IID,是命名一个接口的 GUID。

    COM 广泛使用 GUID 有两个原因:

    1. GUID 在底层只是数字,任何编程语言都可以处理它们。
    2. 任何人在任何机器上正确创建的每个 GUID 都是唯一的。因此,COM 开发者可以自行创建 GUID,而不会有两个开发者选择相同 GUID 的风险。这消除了需要一个中央机构来颁发 GUID 的必要性。

    HRESULT 是 COM 用来返回错误和成功代码的一种整型类型。尽管有 H 前缀,但它并不是任何东西的“句柄”(handle)。稍后我将详细介绍 HRESULT 以及如何测试它们。

    最后,COM 库(COM library)是操作系统的一部分,当您处理 COM 相关事务时,您会与之交互。通常,COM 库被简称为“COM”,但为了避免混淆,我在这里不会这样做。

    使用 COM 对象

    每种语言都有自己处理对象的方式。例如,在 C++ 中,您可以在栈上创建它们,或者使用 new 来动态分配它们。由于 COM 必须是语言中立的,COM 库提供了自己的对象管理例程。下面是 COM 和 C++ 对象管理的比较:

    创建一个新对象

    • 在 C++ 中,使用 operator new 或在栈上创建一个对象。
    • 在 COM 中,调用 COM 库中的一个 API。

    删除对象

    • 在 C++ 中,使用 operator delete 或让栈对象离开作用域。
    • 在 COM 中,所有对象都维护自己的引用计数。调用者必须在用完对象后告知该对象。当引用计数达到 0 时,COM 对象会自行从内存中释放。

    现在,在创建和销毁对象的这两个阶段之间,您实际上需要使用它。当您创建一个 COM 对象时,您告诉 COM 库您需要哪个接口。如果对象创建成功,COM 库会返回一个指向所请求接口的指针。然后您可以通过该指针调用方法,就像它是一个指向常规 C++ 对象的指针一样。

    创建 COM 对象

    要创建一个 COM 对象并从该对象获取一个接口,您需要调用 COM 库的 API CoCreateInstance()CoCreateInstance() 的原型是:

    HRESULT CoCreateInstance (
        REFCLSID  rclsid,
        LPUNKNOWN pUnkOuter,
        DWORD     dwClsContext,
        REFIID    riid,
        LPVOID*   ppv );

    参数如下:

    rclsid
    coclass 的 CLSID。例如,您可以传入 CLSID_ShellLink 来创建一个用于创建快捷方式的 COM 对象。
    pUnkOuter
    这仅在聚合 COM 对象时使用,这是一种获取现有 coclass 并为其添加新方法的方式。对于我们的目的,我们只需传递 NULL,表示不使用聚合。
    dwClsContext
    指示我们想要使用哪种类型的 COM 服务器。在本文中,我们将始终使用最简单的服务器类型,即进程内 DLL,因此我们将传递 CLSCTX_INPROC_SERVER。一个告诫:您不应使用 CLSCTX_ALL(这是 ATL 中的默认值),因为它在未安装 DCOM 的 Windows 95 系统上会失败。
    riid
    您希望返回的接口的 IID。例如,您可以传递 IID_IShellLink 来获取一个指向 IShellLink 接口的指针。
    ppv
    一个接口指针的地址。COM 库通过此参数返回所请求的接口。

    当您调用 CoCreateInstance() 时,它会处理在注册表中查找 CLSID、读取服务器位置、将服务器加载到内存中,以及创建您请求的 coclass 的实例。

    这里有一个示例调用,它实例化一个 CLSID_ShellLink 对象,并请求一个指向该 COM 对象的 IShellLink 接口指针。

    HRESULT     hr;
    IShellLink* pISL;
    
    hr = CoCreateInstance ( CLSID_ShellLink,         // CLSID of coclass
                            NULL,                    // not used - aggregation
                            CLSCTX_INPROC_SERVER,    // type of server
                            IID_IShellLink,          // IID of interface
                            (void**) &pISL );        // Pointer to our interface pointer
    
        if ( SUCCEEDED ( hr ) )
            {
            // Call methods using pISL here.
            }
        else
            {
            // Couldn't create the COM object.  hr holds the error code.
            }

    首先,我们声明一个 HRESULT 来保存 CoCreateInstance() 的返回值和一个 IShellLink 指针。我们调用 CoCreateInstance() 来创建一个新的 COM 对象。如果 hr 持有一个表示成功的代码,SUCCEEDED 宏返回 TRUE;如果 hr 表示失败,则返回 FALSE。还有一个对应的宏 FAILED 用于测试失败代码。

    删除 COM 对象

    如前所述,您不会释放 COM 对象,您只是告诉它们您已经用完了。每个 COM 对象都实现的 IUnknown 接口有一个方法 Release()。您调用此方法来告诉 COM 对象您不再需要它。一旦调用了 Release(),您就不能再使用该接口指针了,因为 COM 对象可能随时从内存中消失。

    如果您的应用程序使用许多不同的 COM 对象,那么在每次用完接口后调用 Release() 是至关重要的。如果您不释放接口,COM 对象(以及包含代码的 DLL)将保留在内存中,不必要地增加您应用程序的工作集。如果您的应用程序将长时间运行,您应该在空闲处理期间调用 CoFreeUnusedLibraries() API。此 API 会卸载所有没有未完成引用的 COM 服务器,从而也减少了您应用程序的内存使用。

    继续上面的例子,这里是如何使用 Release() 的:

        // Create COM object as above.  Then...
    
        if ( SUCCEEDED ( hr ) )
            {
            // Call methods using pISL here.
    
            // Tell the COM object that we're done with it.
            pISL->Release();
            }

    下一节将完整解释 IUnknown 接口。

    基础接口 - IUnknown

    每个 COM 接口都派生自 IUnknown。这个名字有点误导,因为它并不是一个未知的接口。这个名字的含义是,如果您有一个指向某个 COM 对象的 IUnknown 指针,您并不知道底层的对象是什么,因为每个 COM 对象都实现了 IUnknown

    IUnknown 有三个方法:

    1. AddRef() - 告诉 COM 对象增加其引用计数。如果您创建了一个接口指针的副本,并且原始指针和副本都仍在使用,您就会使用此方法。在本文中,我们不需要使用 AddRef()
    2. Release() - 告诉 COM 对象减少其引用计数。请参考前面的例子中的代码片段来演示 Release() 的用法。
    3. QueryInterface() - 从 COM 对象请求一个接口指针。当一个 coclass 实现多个接口时,您会使用此方法。

    我们已经见识了 Release() 的用法,那么 QueryInterface() 呢?当您用 CoCreateInstance() 创建一个 COM 对象时,您会得到一个接口指针。如果这个 COM 对象实现了多个接口(不包括 IUnknown),您可以使用 QueryInterface() 来获取您需要的任何其他接口指针。QueryInterface() 的原型是:

    HRESULT IUnknown::QueryInterface (
        REFIID iid,
        void** ppv );

    参数如下:

    iid
    您正在请求的接口的 IID。
    ppv
    接口指针的地址。如果成功,QueryInterface() 通过此参数返回接口。

    让我们继续我们的 shell link 例子。用于创建 shell link 的 coclass 实现了 IShellLinkIPersistFile。如果您已经有了一个 IShellLink 指针 pISL,您可以用如下代码从 COM 对象请求一个 IPersistFile 接口:

    HRESULT hr;
    IPersistFile* pIPF;
    
        hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );

    然后,您使用 SUCCEEDED 宏测试 hr 来判断 QueryInterface() 是否成功。如果成功,您就可以像使用其他任何接口一样使用新的接口指针 pIPF。您还必须调用 pIPF->Release() 来告诉 COM 对象您已经用完这个接口了。

    请密切注意 - 字符串处理

    我需要暂时绕个弯,讨论一下如何在 COM 代码中处理字符串。如果您熟悉 Unicode 和 ANSI 字符串的工作原理,并且知道如何在两者之间进行转换,那么您可以跳过本节。否则,请继续阅读。

    每当 COM 方法返回一个字符串时,该字符串都将是 Unicode 格式。(嗯,所有遵循 COM 规范编写的方法都是如此!)Unicode 是一种像 ASCII 一样的字符编码方案,只是所有字符都是 2 字节长。如果您想将字符串转换成更易于管理的状态,您应该将其转换为 TCHAR 字符串。

    TCHAR_t 函数(例如 _tcscpy())旨在让您用同一套源代码处理 Unicode 和 ANSI 字符串。在大多数情况下,您将编写使用 ANSI 字符串和 ANSI Windows API 的代码,因此为了简单起见,本文的其余部分我将使用 char 而不是 TCHAR。不过,您绝对应该阅读有关 TCHAR 类型的内容,以便在遇到他人编写的代码时有所了解。

    当您从 COM 方法获得一个 Unicode 字符串时,您可以通过以下几种方式将其转换为 char 字符串:

    1. 调用 WideCharToMultiByte() API。
    2. 调用 CRT 函数 wcstombs()
    3. 使用 CString 构造函数或赋值运算符(仅限 MFC)。
    4. 使用 ATL 字符串转换宏。

    WideCharToMultiByte()

    您可以使用 WideCharToMultiByte() API 将 Unicode 字符串转换为 ANSI 字符串。这个 API 的原型是:

    int WideCharToMultiByte (
        UINT    CodePage,
        DWORD   dwFlags,
        LPCWSTR lpWideCharStr,
        int     cchWideChar,
        LPSTR   lpMultiByteStr,
        int     cbMultiByte,
        LPCSTR  lpDefaultChar,
        LPBOOL  lpUsedDefaultChar );

    参数如下:

    CodePage
    用于转换 Unicode 字符的代码页。您可以传递 CP_ACP 来使用当前的 ANSI 代码页。代码页是包含 256 个字符的集合。字符 0-127 始终与 ASCII 编码相同。字符 128-255 则不同,可能包含图形或带变音符号的字母。每种语言或地区都有自己的代码页,因此使用正确的代码页以正确显示重音字符非常重要。
    dwFlags
    dwFlags 决定了 Windows 如何处理“组合”Unicode 字符,即一个字母后跟一个变音符号。组合字符的一个例子是 è。如果这个字符在 CodePage 中指定的代码页里,那么什么特别的事情都不会发生。但是,如果它不在代码页里,Windows 就必须将其转换为其他东西。
    传递 WC_COMPOSITECHECK 会使 API 检查无法映射的组合字符。传递 WC_SEPCHARS 会使 Windows 将字符分解为两部分,即字母后跟变音符号,例如 e`。传递 WC_DISCARDNS 会使 Windows 丢弃变音符号。传递 WC_DEFAULTCHAR 会使 Windows 用 lpDefaultChar 参数中指定的“默认”字符替换组合字符。默认行为是 WC_SEPCHARS
    lpWideCharStr
    要转换的 Unicode 字符串。
    cchWideChar
    lpWideCharStr 的长度,以 Unicode 字符为单位。通常您会传递 -1,表示该字符串是以零结尾的。
    lpMultiByteStr
    一个 char 缓冲区,将存放转换后的字符串。
    cbMultiByte
    lpMultiByteStr 的大小,以字节为单位。
    lpDefaultChar
    可选 - 一个包含单个字符的 ANSI 字符串,当 dwFlags 包含 WC_COMPOSITECHECK | WC_DEFAULTCHAR 且某个 Unicode 字符无法映射到等效的 ANSI 字符时,将插入此“默认”字符。您可以传递 NULL,让 API 使用系统默认字符(截至本文撰写时,是一个问号)。
    lpUsedDefaultChar
    可选 - 指向一个 BOOL 变量的指针,该变量将被设置为指示默认字符是否曾被插入到 ANSI 字符串中。如果您不关心此信息,可以传递 NULL。

    呼,好多无聊的细节!和往常一样,文档让它看起来比实际复杂得多。这里有一个例子,展示如何使用这个 API:

    // Assuming we already have a Unicode string wszSomeString...
    char szANSIString [MAX_PATH];
    
        WideCharToMultiByte ( CP_ACP,                // ANSI code page
                              WC_COMPOSITECHECK,     // Check for accented characters
                              wszSomeString,         // Source Unicode string
                              -1,                    // -1 means string is zero-terminated
                              szANSIString,          // Destination char string
                              sizeof(szANSIString),  // Size of buffer
                              NULL,                  // No default character
                              NULL );                // Don't care about this flag

    此调用之后,szANSIString 将包含该 Unicode 字符串的 ANSI 版本。

    wcstombs()

    CRT 函数 wcstombs() 更简单一些,但它最终只是调用了 WideCharToMultiByte(),所以结果是一样的。wcstombs() 的原型是:

    size_t wcstombs (
        char*          mbstr,
        const wchar_t* wcstr,
        size_t         count );

    参数如下:

    mbstr
    一个用于存放结果 ANSI 字符串的 char 缓冲区。
    wcstr
    要转换的 Unicode 字符串。
    计数
    mbstr 缓冲区的大小,以字节为单位。

    wcstombs() 在其对 WideCharToMultiByte() 的调用中使用了 WC_COMPOSITECHECK | WC_SEPCHARS 标志。重用前面的例子,您可以用这样的代码来转换一个 Unicode 字符串:

        wcstombs ( szANSIString, wszSomeString, sizeof(szANSIString) );

    CString

    MFC 的 CString 类包含接受 Unicode 字符串的构造函数和赋值运算符,所以您可以让 CString 为您完成转换工作。例如:

    // Assuming we already have wszSomeString...
    
    CString str1 ( wszSomeString );    // Convert with a constructor.
    CString str2;
    
        str2 = wszSomeString;          // Convert with an assignment operator.

    ATL 宏

    ATL 有一套方便的宏用于字符串转换。要将 Unicode 字符串转换为 ANSI,请使用 W2A() 宏(助记符为“wide to ANSI”)。实际上,更准确地说,您应该使用 OLE2A(),其中“OLE”表示该字符串来自 COM 或 OLE 源。不管怎样,这里有一个如何使用这些宏的例子。

    #include <atlconv.h>
    
    // Again assuming we have wszSomeString...
    
    {
    char szANSIString [MAX_PATH];
    USES_CONVERSION;  // Declare local variable used by the macros.
    
        lstrcpy ( szANSIString, OLE2A(wszSomeString) );
    }

    OLE2A() 宏“返回”一个指向转换后字符串的指针,但转换后的字符串存储在一个临时的栈变量中,所以我们需要用 lstrcpy() 创建我们自己的副本。您应该研究的其他宏包括 W2T()(Unicode 到 TCHAR)和 W2CT()(Unicode 字符串到 const TCHAR 字符串)。

    还有一个 OLE2CA() 宏(Unicode 字符串转为 const char 字符串),我们可以在上面的代码片段中使用它。OLE2CA() 实际上是那种情况下的正确宏,因为 lstrcpy() 的第二个参数是 const char*,但我不想一次性给您灌输太多东西。

    坚持使用 Unicode

    另一方面,如果您不会对字符串进行任何复杂的操作,也可以直接保持字符串为 Unicode 格式。如果您正在编写一个控制台应用程序,您可以使用 std::wcout 全局变量来打印 Unicode 字符串,例如:

        wcout << wszSomeString;

    但请记住,wcout 期望所有字符串都是 Unicode 格式,所以如果您有任何“普通”字符串,您仍然需要使用 std::cout 来输出它们。如果您有字符串字面量,请在它们前面加上 L 使其成为 Unicode,例如:

        wcout << L"The Oracle says..." << endl << wszOracleResponse;

    如果您保持字符串为 Unicode 格式,有几条限制:

    • 您必须对 Unicode 字符串使用 wcsXXX() 字符串函数,例如 wcslen()
    • 除了极少数例外,您不能在 Windows 9x 上将 Unicode 字符串传递给 Windows API。要编写能在 9x 和 NT 上无需修改即可运行的代码,您需要使用 TCHAR 类型,如 MSDN 中所述。

    融会贯通 - 示例代码

    以下是两个例子,用来说明本文中涉及的 COM 概念。代码也包含在文章的示例项目中。

    使用具有单个接口的 COM 对象

    第一个例子展示了如何使用一个公开单个接口的 COM 对象。这是您会遇到的最简单的情况。代码使用 shell 中包含的 Active Desktop coclass 来检索当前壁纸的文件名。您需要安装 Active Desktop 才能使此代码工作。

    涉及的步骤是:

    1. 初始化 COM 库。
    2. 创建一个用于与 Active Desktop 交互的 COM 对象,并获取一个 IActiveDesktop 接口。
    3. 调用 COM 对象的 GetWallpaper() 方法。
    4. 如果 GetWallpaper() 成功,打印壁纸的文件名。
    5. 释放接口。
    6. 反初始化 COM 库。

    WCHAR   wszWallpaper [MAX_PATH];
    CString strPath;
    HRESULT hr;
    IActiveDesktop* pIAD;
    
        // 1. Initialize the COM library (make Windows load the DLLs). Normally you would
        // call this in your InitInstance() or other startup code.  In MFC apps, use
        //  AfxOleInit() instead.
        CoInitialize ( NULL );
    
        // 2. Create a COM object, using the Active Desktop coclass provided by the shell.
        // The 4th parameter tells COM what interface we want (IActiveDesktop).
        hr = CoCreateInstance ( CLSID_ActiveDesktop,
                                NULL,
                                CLSCTX_INPROC_SERVER,
                                IID_IActiveDesktop,
                                (void**) &pIAD );
    
        if ( SUCCEEDED(hr) )
            {
            // 3. If the COM object was created, call its GetWallpaper() method.
            hr = pIAD->GetWallpaper ( wszWallpaper, MAX_PATH, 0 );
    
            if ( SUCCEEDED(hr) )
                {
                // 4. If GetWallpaper() succeeded, print the filename it returned.
                // Note that I'm using wcout to display the Unicode string wszWallpaper.
                // wcout is the Unicode equivalent of cout.
                wcout << L"Wallpaper path is:\n    " << wszWallpaper << endl << endl;
                }
            else
                {
                cout << _T("GetWallpaper() failed.") << endl << endl;
                }
    
            // 5. Release the interface.
            pIAD->Release();
            }
        else
            {
            cout << _T("CoCreateInstance() failed.") << endl << endl;
            }
    
        // 6. Uninit the COM library.  In MFC apps, this is not necessary since MFC does
        // it for us.
        CoUninitialize();

    在这个示例中,我使用了 std::wcout 来显示 Unicode 字符串 wszWallpaper

    使用具有多个接口的 COM 对象

    第二个例子展示了如何在一个公开多个接口的 COM 对象上使用 QueryInterface()。代码使用 shell 中包含的 Shell Link coclass,为我们在上一个例子中检索到的壁纸文件创建一个快捷方式。

    涉及的步骤是:

    1. 初始化 COM 库。
    2. 创建一个用于创建快捷方式的 COM 对象,并获取一个 IShellLink 接口。
    3. 调用 IShellLink 接口的 SetPath() 方法。
    4. 在 COM 对象上调用 QueryInterface() 并获取一个 IPersistFile 接口。
    5. 调用 IPersistFile 接口的 Save() 方法。
    6. 释放接口。
    7. 反初始化 COM 库。
    CString       sWallpaper = wszWallpaper;  // Convert the wallpaper path to ANSI
    IShellLink*   pISL;
    IPersistFile* pIPF;
    
        // 1. Initialize the COM library (make Windows load the DLLs). Normally you would
        // call this in your InitInstance() or other startup code.  In MFC apps, use
        // AfxOleInit() instead.
        CoInitialize ( NULL );
    
        2. Create a COM object, using the Shell Link coclass provided by the shell.
        // The 4th parameter tells COM what interface we want (IShellLink).
        hr = CoCreateInstance ( CLSID_ShellLink,
                                NULL,
                                CLSCTX_INPROC_SERVER,
                                IID_IShellLink,
                                (void**) &pISL );
    
        if ( SUCCEEDED(hr) )
            {
            // 3. Set the path of the shortcut's target (the wallpaper file).
            hr = pISL->SetPath ( sWallpaper );
    
            if ( SUCCEEDED(hr) )
                {
                // 4. Get a second interface (IPersistFile) from the COM object.
                hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );
    
                if ( SUCCEEDED(hr) )
                    {
                    // 5. Call the Save() method to save the shortcut to a file.  The
                    // first parameter is a Unicode string.
                    hr = pIPF->Save ( L"C:\\wallpaper.lnk", FALSE );
    
                    // 6a. Release the IPersistFile interface.
                    pIPF->Release();
                    }
                }
    
            // 6b. Release the IShellLink interface.
            pISL->Release();
            }
    
        // Printing of error messages omitted here.
    
        // 7. Uninit the COM library.  In MFC apps, this is not necessary since MFC
        // does it for us.
        CoUninitialize();
    

    处理 HRESULT

    我已经展示了一些简单的错误处理,使用了 SUCCEEDEDFAILED 宏。现在我将提供更多关于如何处理从 COM 方法返回的 HRESULT 的细节。

    一个 HRESULT 是一个 32 位的有符号整数,非负值表示成功,负值表示失败。一个 HRESULT 有三个字段:严重性位(用于指示成功或失败)、功能代码和状态代码。“功能”(facility)指示 HRESULT 来自哪个组件或程序。微软为各种组件分配功能代码,例如 COM 有一个,任务计划程序有一个,等等。“代码”是一个 16 位的字段,本身没有内在含义;这些代码只是数字和意义之间的任意关联,就像 GetLastError() 返回的值一样。

    如果您在 winerror.h 文件中查找错误代码,您会看到列出了很多 HRESULT,其命名约定为 [功能]_[严重性]_[描述]。任何组件都可能返回的通用 HRESULT(如 E_OUTOFMEMORY)的名称中没有功能部分。例子:

    • REGDB_E_READREGDB:功能 = REGDB,代表“注册表数据库”;E = 错误;READREGDB 是对错误的描述(无法读取数据库)。
    • S_OK:功能 = 通用;S = 成功;OK 是对状态的描述(一切正常)。

    幸运的是,有比翻阅 winerror.h 更简单的方法来确定一个 HRESULT 的含义。对于内置功能,可以使用错误查找(Error Lookup)工具来查找 HRESULT。例如,假设您在调用 CoCreateInstance() 之前忘记了调用 CoInitialize()CoCreateInstance() 将返回一个值 0x800401F0。您可以将该值输入到错误查找工具中,您将看到描述:“CoInitialize has not been called.”(CoInitialize 尚未被调用。)

     [Error Lookup screen shot - 7K]

    您也可以在调试器中查找 HRESULT 的描述。如果您有一个名为 hresHRESULT 变量,您可以在监视(Watch)窗口中通过输入“hres,hr”作为要监视的值来查看其描述。“,hr”告诉 VC 将该值显示为 HRESULT 的描述。

     [Watch window - 4K]

    参考文献

    《Essential COM》作者 Don Box,ISBN 0-201-63446-5。关于 COM 规范和 IDL(接口定义语言),你想知道的一切都在这里。前两章深入探讨了 COM 规范及其旨在解决的问题。

    《MFC Internals》作者 George Shepherd 和 Scot Wingo,ISBN 0-201-40721-3。深入探讨了 MFC 对 COM 的支持。

    《Beginning ATL 3 COM Programming》作者 Richard Grimes 等人,ISBN 1-861001-20-7。这本书深入讲解了如何使用 ATL 编写自己的 COM 组件。

    © . All rights reserved.