-
什么是
COM,
如何使用
COM
本文的目的是为刚刚接触
COM
p>
的程序员提供编程指南,并帮助他们理
解
C
OM
的基本概念。内容包括
COM
规范
简介,重要的
COM
术语以及如何重用现有的
< br>COM
组
件。本文不包括如何编写自己的
COM
对象和接口。
COM
即组件对象模型,是
Component Object Model
取前三个字母的缩写
,这三个字母在当
今
Windows
的
世界中随处可见。
随时涌现出来的大把大把的新技术都以
COM
为基础。
各种文档
中也充斥着诸如
p>
COM
对象、接口、服务器之类的术语。因此,对于一个程序员来说
,不仅要掌
握使用
COM
的方法,而且
还要彻底熟悉
COM
的所有一切。
本文由浅入深描述
COM
的内在运行机制,
教你如何使用第三方提供的<
/p>
COM
对象
(以
Windows
外壳组件
Shell
为例)
。读完本文后,你就能掌握如何使用
Windows
p>
操作系统中内建的组件和第
三方提供的
CO
M
对象。
本文假设你精通
C++
语言。在例子代
码中使用了一点
MFC
和
ATL
,
如果你不熟悉
MFC
和
ATL
也没关系,本文会对这些代码进行完全透彻的解释。
本文包括以下几个部分:
COM<
/p>
——到底是什么?——
COM
标准的要点
介绍,它被设计用来解决什么问题
基本元素的定义——
COM
术语以及这些术语的含义
使用和处理
COM<
/p>
对象——如何创建、使用和销毁
COM
对
象
基本接口——描述
IUnknown
基本接口及其方法
掌握串的处理——在
COM
代码中如何处理串
应用
COM
技术——例子代码,举例说明本文所讨论的所有概念
处理
HR
ESULT
——
HRESULT
类型描
述,如何监测错误及成功代码
COM
——到底是什么
简单地说,
COM
是一种跨应用和语言共享二进制代码的方法。与
C++
不同,它提倡源代码
重用。
ATL
p>
便是一个很好的例证。源码级重用虽然好,但只能用于
C++
。它还带来了名字冲突的
可能性,更不用说不断拷贝重用代码而导致工
程膨胀和臃肿。
Windows
使用
DLLs
在二进制级共享代码。这也是
Windows
程序运行的关键—
—重用
,
等。
但
DLLs
是针对
C
接口而写的,<
/p>
它们只能被
C
或理解
C
调用规范的语言
使用。由编程语言来负责实现共享代码,
而不是由
DLLs
本身。这样的话
DL
Ls
的使用受到限制。
MFC
引入了另外一种
MFC
扩展
< br>DLLs
二进制共享机制。但它的使用仍受限制——只能在
MFC
程
序中使用。
COM
通
过定义二进制标准解决了这些问题,即
COM
明确指出二进制模
块(
DLLs
和
EXEs
)
必须被编译成与指定的结构匹配。
这个标准也确切
规定了在内存中如何组织
COM
对象。
COM
定
义的二进制标准还必须独立于任何编程语言(如
C++
中的命名修饰)
。一旦满足了这些条件
,就
可以轻松地从任何编程语言中存取这些模块。
由编译器负责
所产生的二进制代码与标准兼容。
这
样使后来的人就能更容易地
使用这些二进
制代码。
在内存中,
COM
对象的这种标准形式在
C++
虚函数中偶尔用到,
所以这就是为什么许多
COM
代码使用
C++
的原因。但是记住,编写模块所用的
语言是无关的,因为结果二进制代码为所有语
言可用。
此外,
C
OM
不是
Win32
特有的。从理论上
讲,它可以被移植到
Unix
或其它操作系统。但是
我好像还从来没有在
Windows
以外的地方听
说过
COM
。
基本元素的定义
我们从
下往上看。
接口只不过是一组函数。
这些函数被称为方法。
p>
接口名字以大写的
I
开头,
例如
C++
中的
IShell
Link
,接口被设计成一个抽象基类,其中只有纯粹的虚拟函数。
接口可以从其它接口继承,
这里所说的继承的原理就好像
C++
中
的单继承。
接口是不允许多
继承的。
coclass
(简称组件对象类——
component object class
)被包含在
D
LL
或
EXE
中,并且包含着一个
p>
或者多个接口的代码。组件对象类(
coclasss
)实现这些接口。
COM
对象在内存中表现为组件对
象类(
coclasss
)的一个实例
。注意
COM
“类”和
C++
“类”是不相同的,尽管常常
COM
类实现
p>
的就是一个
C++
类。
COM
服务器是包含了一个或多个
coclass
的二进制(
p>
DLL
或
EXE
)
。
注册<
/p>
(
Registration
)是创建注
册表入口的一个过程,告诉
Windows
操作系统
COM
服务器放在什
么位置。取消注册(
Unregistration
)则相反——从注册表删除这些注册入
口。
GUID
(谐音为
“
fluid
”
,
意思是全球唯一标示符——
globally
unique identifier
)
是个
< br>128
位的数字。
它是一种独立于
COM
编程语言的标示方法。
每一个接口和
< br>coclass
有一个
GUID
。
因为每一个
GUID
都是全球唯一的
,所以避免了名字冲突(只要你用
COM API
创建它们)<
/p>
。有时你还会碰到另一个
术语
UUID<
/p>
(意思也是全球唯一标示符——
universally
unique
identifier
)
。
UUIDs
和
GUIDs
在实际
使用时的用途是一样的。
类
ID
< br>或者
CLSID
是命名
cocl
ass
的
GUID
。接口
ID
或者
IID
是命名接口
的
GUID
。
在
COM
中
广泛地使用
GUID
有两个理由:
GUIDs
只是简单的数字,任何编
程语言都可以对之进行处理;
GU
IDs
可以在任何机器上被任何人创建,一旦完成创建,它就是唯一的。因此,
COM
开发人员
可以创建自己特有的
GUIDs
而不会与其它开发人员所创建的
GUI
Ds
有冲突。这样就消除了集中
授权发布
GUIDs
的必要。
HRESULT
< br>是
COM
用来返回错误和成功代码的整型数字。除此之外
,别无它意,虽然以
H
作
前缀,但没有
句柄之意。下文会对它有更多的讨论。
最后,
COM
库是在你使用
COM
时与你交互的操作系统的一部分,它常常
指的就是
COM
本
身。但是为了避免混
淆才分开描述的。
使用和处理
p>
COM
对象
每一种语言都有其自己处理对象的
方式。例如,
C++
是在栈中创建对象,或者用
new
动态分
配。因
p>
为
COM
必须独立于语言,所以
COM
库为自己提供对象管理例程。下面
是对
p>
COM
对象管理和
C++
< br>对象管理所做的一个比较:
创建一个新对象
< br>C++
中,用
new
操作符,或
者在栈中创建对象。
COM
中,调用
COM
库中的
API
< br>。
删除对象
C++
中,用
delete
操作符,或
将栈对象踢出。
COM
中,所有的对
象保持它们自己的引用计数。调用者必须通知对象什么时候用完这个对象。
当引用计数为
零时,
COM
对象将自己从内存中释放。
由此可见,对象处理的两个阶
段:创建和销毁,缺一不可。当创建
COM
对象时要通知
COM
库使用哪一个接口。如果这个对象创建成功,
< br>COM
库返回所请求接口的指针。然后通过这个指
针调用
方法,就像使用常规
C++
对象指针一样。
创建
COM
< br>对象
为了创建
COM
对象并从这个对象获得接口,必须调用
COM
库的
API
函数,
CoCreateInstance()
。
其原型如下:<
/p>
HRESULT CoCreateInstance (
REFCLSID
rclsid,
LPUNKNOWN pUnkOuter,
DWORD
dwClsContext,
REFIID
riid,
LPVOID*
ppv );
以下是参数解释:
rclsid
p>
:
coclass
的
CLSID
,例如,可以传递
CLSID_ShellLin
k
创建一个
COM
对象
来建立快捷方式。
pUnkOuter
:这个参数只用于
COM
对象的聚合,利用它向现有
的
coclass
添加新方法。参数值为
null
表示不使用聚合。
dwC
lsContext
:表示所使用
COM
服务器的种类。本文使用的是最简单的
COM
服务器,一个进
程
内(
in-process
)
DLL
,
所以传
递的参数值为
CLSCTX_INPROC_SERVER
。注
意这里不要随意使用
CLSCTX_ALL
(在
ATL
中,它是个缺省值)
,
p>
因为在没有安装
DCOM
的
Windows95
系统上会导致失败。
riid
:请求接口的
IID
。例如,可以传递
IID_IShellLink
获得
p>
IShellLink
接口指针。
ppv
:接口指针的地址。
COM
库通过这个参数返回请求的接口。
p>
当你调用
CoCreateInstance()
< br>时,它负责在注册表中查找
COM
服务器的位置,将服务
器加载
到内存,并创建你所请求的
coclass
实例。以下是一个调用的例子,创建一个
CLSID_ShellLink<
/p>
对
象的实例并请求指向这个对象
IShe
llLink
接口指针。
HRESULT
hr;
IShellLink* pISL;
hr = CoCreateInstance (
CLSID_ShellLink,
// coclass
的
CLSID
NULL,
//
不是用聚合
CLSCTX_INPROC_SERVER,
//
服务器类型
IID_IShellLink,
//
接口的
IID
(void**) &pISL );
//
指向接口的指针
if ( SUCCEEDED ( hr ) )
{
//
用
pISL
调用方法
}
else
{
//
不能创建
< br>COM
对象,
hr
为出错代码
}
首
p>
先
声
明
一
个
接
受
CoCreat
eInstance()
返
回
值
的
HRESULT
和
IShellLink
指
针
。
调
用
CoCreateInstance()<
/p>
来创建新的
COM
对象。如果
hr
接
受到一
个表示成功的代码,
则
SUCCEEDED
宏返回
TRUE
,
否则返回
FALSE
。
FAILED
< br>是一个与
SUCCEEDED
对应的宏用来检查失败代码
。
删除
COM
对象
前面说过,
你不用释放
COM
对象,
只要告诉它们你已经用完对象。
IUnknown
是每一个
COM
对象必须实现的接口,
它有一个方法,
Relea
se()
。
调用这个方法通知
COM<
/p>
对象你不再需要对象。
一旦调用了这个方法之后,就不能再次使用
这个接口,因为这个
COM
对象可能从此就从内存中
消失了。
如果你的应用程序使用许多不同的
COM
对象,
因此在用完某个接口后调用
Release()
就显得
非常重要。如果你不释放接口,这个
COM
对象
(包含代码的
DLLs
)将保留在内存中,这会增加
不必要的开销。如果你的应用程序要长时间运行,就应该在应用程序处于空闲期间调用
CoFreeUnusedLibraries() API
。
这个
API
将卸载任何没有明显引用的
COM
服务器,
因此这也降低了
应用程序使用的内存开销。
继续用上面的例子来说明如何
使用
Release()
:
//
像上面一样创建
COM
对象,
然后,
if ( SUCCEEDED ( hr ) )
{
//
用
pISL
调用方法
//
通知
COM
对象不再使用它
pISL->Release();
}
接下来将详细讨论
IUnknown
接口。
基本接口——
IUnknown
每一个
C
OM
接口都派生于
IUnknown
。
这个名字有点误导人,其中没有未知(
Unknown
)接
p>
口的意思。它的原意是如果有一个指向某
COM
对象的
IUnknown
指针,就不用知道潜在的对象
p>
是什么,因为每个
COM
对象都实现
IUnknown
。
IUnknown
有三个方法:
AddRef()
——
通知
COM
对象增加它的引用计数。如果你进行了
一次接口指针的拷贝,就必须
调用一次这个方法,
并且原始的值
和拷贝的值两者都要用到。
在本文的例子中没有用到
AddRe
f()
方法;
Release()
——
通知
COM
对象减少它的引用计数。参见前面的
Release()
示例代码段;
QueryInterface()
——
从
CO
M
对象请求一个接口指针。当
coclass
< br>实现一个以上的接口时,就要
用到这个方法;
前面已
经看到了
Release()
的使用,但如何使用
QueryInterface()
呢
?
当你用
CoCreateInstance()
创
建对象的时候,你得到一个返回的接口指针。如果这个
COM
对
象实现一个以上的接口(不包
括
IUnknown
)
,你就必须用
QueryInterface()
方法来获得任何你需要的附加的接口指针。
QueryInte
rface()
的原型如下:
HRESULT
IUnknown::QueryInterface (
REFIID iid,
void** ppv );
以下
是参数解释:
iid
:所请求的接口的
IID
。
ppv
:接口指针的地址,
QueryInterface()
通
过这个参数在成功时返回这个接口。
让我们继续外壳链接的例子。它实现了
IShellLink
和
IPersistFile
接口。如
果你已经有一个
IShellLink
指针,
< br>pISL
,可以从
COM
对象请
求
IPersistFile
接口:
HRESULT hr;
IPersistFile* pI
PF;
hr = pISL->QueryInterface (
IID_IPersistFile, (void**) &pIPF );
然后使
用
SUCCEEDED
宏检查
hr
p>
的值以确定
QueryInterface()
的调用情况,
如果成功的话你就
可以象使用其它接口指针那
样使用新的接口指针,
pIPF
。但必须记住调用
pIPF->Release()
通知
COM
对象已经用完这个接口。
仔细做好串处理
这一部分将花点时间来讨论如何在
COM
代码中处理串。
如果你熟悉
p>
Unicode
和
ANSI
,
并知
道如何对它们进行转换的话,你就可以跳过这
一部分,否则还是读一下这一部分的内容。
不管什么时候,只要
COM
方法返回一个串,这个串都是
Unicode
串
(这里指的是写入
COM
规范的所有方法)
。
Unicode
是一种字符编码集,类似
ASCII
,但用两个字节表示一个字符。如果
你想更
好地控制或操作串的话,应该将它转换成
TCHAR
类型串。<
/p>
TCHA
R
和以
_t
开头的函数
(如
_tcscpy()
)
被
设计用来让你用相同的源代码处理
Unicode
和
ANSI
串。在大多数情况下编写的代码都是用来处理
ANSI
串和
ANSI
Wind
owsAPIs
,所以在下文中,除
非另外说明,我所说的字符
/
串都是指
TCHAR
类型。你应该熟练掌握
TCHAR
类型,
尤其是当你阅
读其他人写的有关代码时,要特别注意
TCHAR
类型。
当你从某个
COM
< br>方法返回得到一个
Unicode
串时,
可以用下列几种方法之一将它转换成
char
类型串:
调用
WideCharToMultiByte()
API
;
调用
CRT
函数
wcstombs()
;
使用
CString
构造器或赋值操
作
(
仅用于
MFC
)
;
使用
ATL
串转换宏;
WideCharToMultiByte()
你可以用
WideCharToMultiByte()
< br>将一个
Unicode
串转换成一个
ANSI
串。此函数的原型如下:
int
WideCharToMultiByte (
UINT
CodePage,
DWORD
dwFlags,
LPCWSTR lpWideCharStr,
int
cchWideChar,
LPSTR
lpMultiByteStr,
int
cbMultiByte,
LPCSTR
lpDefaultChar,
LPBOOL
lpUsedDefaultChar );
以下是参数解释:
CodePage
:
Unicode
< br>字符转换成的代码页。你可以传递
CP_ACP
来使用当
前的
ANSI
代码页。代码
页是
256
个字符集。
字符
0
——
127
与
ANSI
编码一样。
字符
128<
/p>
——
255
与
A
NSI
字符不同,
它可
以包含图形字符
或者读音符号。
每一种语言或地区都有其自己的代码页,
所以使
用正确的代码页
对于正确地显示重音字符很重要。
dwFlags
:
dwFlags <
/p>
确定
Windows
如何处理“复合”<
/p>
Unicode
字符,它是一种后面带
读音符号的
字符。
如è就是一个复合
字符。如果这些字符在
CodePage
参数指定的代码页中,
不会出什么事。
否则,
Window
s
必须对之进行转换。
传递
WC_CO
MPOSITECHECK
使得这个
API
检查非映射复合字
符。
传递
p>
WC_SEPCHARS
使得
Window
s
将字符分为两段,即字符加读音,如
e`
。