以文本方式查看主题

-  中文XML论坛 - 专业的XML技术讨论区  (http://bbs.xml.org.cn/index.asp)
--  『 C/C++编程思想 』  (http://bbs.xml.org.cn/list.asp?boardid=61)
----  COM组件设计与应用[分享]  (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=52269)


--  作者:一分之千
--  发布时间:9/5/2007 9:21:00 PM

--  COM组件设计与应用[分享]
COM组件设计与应用之VC6中用ATL写组件

一、前言

  1、如果你在使用 vc5.0 及以前的版本,请你升级为 vc6.0 或 vc.net 2003;

  2、如果你在使用 vc6.0 (ATL 3.0)请阅读本回内容;

  3、如果你在使用 vc.net(ATL 7.0)请阅读下回内容;(当然读读本文内容也不错)

  4、这第一个组件,除了所有 COM 组件必须的 IUnknown 接口外,我们再实现一个自己定义的接口 IFun,它有两个函数: Add()完成两个数值的加法,Cat()完成两个字符串的连接。

  5、下面......好好听讲! 开始了:-)
  二、建立 ATL 工程

  步骤2.1:建立一个工作区(WorkSpace)。

  步骤2.2:在工作区中,建立一个 ATL 工程(Project)。示例程序叫 Simple1,并选择DLL方式,见图一。



此主题相关图片如下:
按此在新窗口浏览图片

图一、建立 ATL DLL 工程

  Dynamic Link Library(DLL) 表示建立一个 DLL 的组件程序。

  Executable(EXE) 表示建立一个 EXE 的组件程序。

  Service(EXE) 表示建立一个服务程序,系统启动后就会加载并执行的程序。

  Allow merging of proxy/stub code 选择该项表示把“代理/存根”代码合并到组件程序中,否则需要单独编译,单独注册代理存根程序。代理/存根,这个是什么概念?还记得我们在上回书中介绍的吗?当调用者调用进程外或远程组件功能的时候,其实是代理/存根负责数据交换的。关于代理/存根的具体变成和操作,以后再说啦......

  Support MFC 除非有特殊的原因,我们写 ATL 程序,最好不要选择该项。你可能会说,如果没有MFC的支持,那CString怎么办呀?告诉你个秘密吧,一般人我都不告诉他,我后半辈子就靠着这个秘密活着了:

  1、你会STL吗?可以用 STL 中的 string 代替;

  2、自己写个 MyString 类,嘿嘿;

  3、悄悄地、秘密地、不要告诉别人(特别是别告诉微软),把 MFC 中的 CString 源码拿过来用;

  4、使用 CComBSTR 类,至少也能简化我们字符串操作;

  5、直接用 API 操作字符串,反正我们大家学习 C 语言的时候,都是从这里干起的。(等于没说,呵呵)

  Support MTS 支持事务处理,也就是是否支持 COM+ 功能。COM+ 也许在第 99 回介绍吧。

  三、增加 ATL 对象类

  步骤3.1:菜单 Insert\New ATL Object...(或者用鼠标右键在 ClassView 卡片中弹出菜单)并选择Object 分类,选中 Simple Object 项目。见图二。



此主题相关图片如下:
按此在新窗口浏览图片

图二、选择建立简单COM对象

  Category Object 普通组件。其中可以选择的组件对象类型很多,但本质上,就是让向导帮我们默认加上一些接口。比如我们选 "Simple Object",则向导给我们的组件加上 IUnknown 接口;我们选 "Internet Explorer Object",则向导除了加上 IUnknown 接口外,再增加一个给 IE 所使用的 IObjectWithSite 接口。当然了,我们完全可以手工增加任何接口。

  Category Controls ActiveX 控件。其中可以选择的 ActiveX 类型也很多。我们在后续的专门介绍 ActiveX 编程中再讨论。

  Category Miscellaneous 辅助杂类组件。

  Categroy Data Access 数据库类组件(我最讨厌数据库编程了,所以我也不会)。

  步骤3.2:增加自定义类 CFun(接口 IFun) ,见图三。



此主题相关图片如下:
按此在新窗口浏览图片

图三、输入类中的各项名称

   其实,我们只需要输入短名(Short Name),其它的项目会自动填写。没什么多说的,只请大家注意一下 ProgID 项,默认的 ProgID 构造方式为“工程名.短名”。

   步骤3.3:填写接口属性,见图四。


此主题相关图片如下:
按此在新窗口浏览图片

图四、接口属性

  Threading Model 选择组件支持的线程模型。COM 中的线程,我认为是最讨厌,最复杂的部分。COM 线程和公寓的概念,留待后续介绍。现在吗......大家都选 Apartment,它代表什么那?简单地说:当在线程中调用组件函数的时候,这些调用会排队进行。因此,这种模式下,我们可以暂时不用考虑同步的问题。

   Interface 接口基本类型。Dual 表示支持双接口,这个非常 非常重要,非常非常常用,但我们今天不讲。Custom 表示自定义借口。切记!切记!我们的这第一个 COM 程序中,一定要选择它!!!!(如果你选错了,请删除全部内容,重新来过。)

   Aggregation 我们写的组件,将来是否允许被别人聚合使用。Only 表示必须被聚合才能使用,有点类似 C++ 中的纯虚类,你要是总工程师,只负责设计但不亲自写代码的话,才选择它。

  Support ISupportErrorInfo 是否支持丰富信息的错误处理接口。以后就讲。

  Support Connection Points 是否支持连接点接口(事件、回调)。以后就讲。

  Free Threaded Marshaler 以后也不讲,就算打死你,我也不说!

  四、添加接口函数


此主题相关图片如下:
按此在新窗口浏览图片

图五、调出增加接口方法的菜单


此主题相关图片如下:
按此在新窗口浏览图片

图六、增加接口函数 Add



此主题相关图片如下:
按此在新窗口浏览图片

图七、增加接口函数 Cat

  请严格按照图六的方式,增加Add()函数;由于图七中增加Cat()函数的参数比较长,我没有适当的输入空格,请大家自己输入的时候注意一下。[in]表示参数方向是输入;[out]表示参数方向是输出;[out,retval]表示参数方向是输出,同时可以作为函数运算结果的返回值。一个函数中,可以有多个[in]、[out],但[retval]只能有一个,并且要和[out]组合后在最后一个位置。


此主题相关图片如下:
按此在新窗口浏览图片

图八、接口函数定义完成后的图示

  我们都知道,要想改变 C++ 中的类函数,需要修改两个地方:一是头文件(.h)中类的函数声明,二是函数体(.cpp)文件的实现处。而我们现在用 ATL 写组件程序,则还要修改一个地方,就是接口定义(IDL)文件。别着急 IDL 下次就要讨论啦。

  由于 vc6.0 的BUG,导致大家在增加完接口和接口函数后,可能不会向上图(图八)所表现的样式。解决方法:
1         关闭工程,然后重新打开 该方法常常有效
2         关闭 IDE,然后重新运行  
3         打开 IDL 文件,检查接口函数是否正确,如不正确请修改  
4         打开 IDL 文件,随便修改一下(加一个空格,再删除这个空格),然后保存 该方法常常有效
5        打开 h/cpp 文件,检查函数是否存在或是否正确,有则改之 无则嘉勉,不说完这个成语心理别扭
6        删除 IDL/H/CPP 中的接口函数,然后 再来一遍
7        重新建立工程、重新安装vc、重新安装windows、砸计算机 砸!


  五、实现接口函数

  鼠标双点图八中CFun\IFun\Add(...)就可以开始输入函数实现了:

STDMETHODIMP CFun::Add(long n1, long n2, long *pVal)
{
*pVal = n1 + n2;

return S_OK;
}  这个太简单了,不再浪费“口条”。下面我们实现字符串连接的Cat()函数:
STDMETHODIMP CFun::Cat(BSTR s1, BSTR s2, BSTR *pVal)
{
int nLen1 = ::SysStringLen( s1 ); // s1 的字符长度
int nLen2 = ::SysStringLen( s2 ); // s2 的字符长度

*pVal = ::SysAllocStringLen( s1, nLen1 + nLen2 ); // 构造新的 BSTR 同时把 s1 先保存进去
if( nLen2 )
{
  ::memcpy( *pVal + nLen1, s2, nLen2 * sizeof(WCHAR) ); // 然后把 s2 再连接进去
//  wcscat( *pVal, s2 );
}

return S_OK;
}  学生:上面的函数实现,完全是调用基本的 API 方式完成的。

  老师:是的,说实话,的确比较烦琐。

  学生:我们是用memcpy()完成连接第二个字符串功能的,那么为什么不用函数 wcscat()那?

  老师:多数情况下可以,但你需要知道:由于BSTR包含有字符串长度,因此实际的BSTR字符串内容中是可以存储L’’\0’’的,而函数 wcscat() 是以L’’\0’’作为复制结束标志,因此可能会丢失数据。明白了吗?

  学生:明白,明白。我看过《COM 组件设计与应用之数据类型》后就明白了。那么老师,有没有简单一些的方法那?

  老师:有呀,你看......
STDMETHODIMP CFun::Cat(BSTR s1, BSTR s2, BSTR *pVal)
{
CComBSTR sResult( s1 );
sResult.AppendBSTR( s2 );

*pVal = sResult.Copy();
// *pVal = sResult.Detach();

return S_OK;
}  学生:哈哈,好!使用了 CComBSTR,这个就简单多了。CComBSTR::Copy()和CComBSTR::Detach()有什么区别?

  老师:CComBSTR::Copy() 会制造一个 BSTR 的副本,另外CComBSTR::CopyTo()也有类似功能。而CComBSTR::Detach()是使对象与内部的 BSTR 指针剥离,这个函数由于没有复制过程,因此速度稍微快一点点。但要注意,一但剥离后,就不能再使用该对象啦。

  学生:老师,您讲的太牛啦,我对您的敬仰如巍巍泰山,直入云霄......

  老师:STOP,STOP!留作业啦......

   1、自己先按照今天讲的内容写出这个组件;

   2、不管你懂不懂,一定要去观察 IDL 文件,CPP 文件;

   3、编译后,看都产生了些什么文件?如果是文本的文件,就打开看看;

   4、下载本文的示例程序(vc6.0版本)编译运行,看看效果。然后预习一下示例程序中的调用方法;

  学生:知道啦,快下课吧,我要上厕所,我都憋的不行了......

  老师:下课!别忘了顶我的帖子呀......


--  作者:一分之千
--  发布时间:9/5/2007 9:25:00 PM

--  
COM组件设计与应用之VC6的IDispatch接口


一、前言
    
  终于写到了这里了,我也一直期盼着写这回的内容耶,为啥呢?因为自动化(automation)是非常常用、非常有用、非常精彩的一个 COM 功能。由于 WORD、EXCEL 等 OFFICE 软件提供了“宏”的功能,就连我们使用的VC开发环境也提供了“宏”功能,更由于 HTML、ASP、JSP 等都要依靠脚本(Script)的支持,更体现出了自动化接口的重要性。

      如果你使用 vc6.0 的开发环境,请继续阅读。

      如果你使用 vc.net 2003,请阅读下一回。

  二、IDispatch接口

      如果是编译型语言,那么我们可以让编译器在编译的时候装载类型库,也就是装载接口的描述。在第七回文章当中,我们分别使用了 #include 方法和 #import 方法来实现的。装载了类型库后,编译器就知道应该如何编译接口函数的调用了---这叫“前绑定”。但是,如果想在脚本语言中使用组件,问题就大了,因为脚本语言是解释执行的,它执行的时候不会知道具体的函数地址,怎么办?自动化接口就为此诞生了---“后绑定”。
    
  自动化组件,其实就是实现了 IDispatch 接口的组件。IDispatch 接口有4个函数,解释语言的执行器就通过这仅有的4个函数来执行组件所提供的功能。IDispatch 接口用 IDL 形式说明如下:(注1)

[
    object,
    uuid(00020400-0000-0000-C000-000000000046), // IDispatch 接口的 IID = IID_IDispatch
    pointer_default(unique)
]

interface IDispatch : IUnknown
{
    typedef [unique] IDispatch * LPDISPATCH; // 转定义 IDispatch * 为 LPDISPATCH

    HRESULT GetTypeInfoCount([out] UINT * pctinfo); // 有关类型库的这两个函数,咱们以后再说
    HRESULT GetTypeInfo([in] UINT iTInfo,[in] LCID lcid,[out] ITypeInfo ** ppTInfo);

    HRESULT GetIDsOfNames( // 根据函数名字,取得函数序号(DISPID)
                [in] REFIID riid,
                [in, size_is(cNames)] LPOLESTR * rgszNames,
                [in] UINT cNames,
                [in] LCID lcid,
                [out, size_is(cNames)] DISPID * rgDispId
            );

    [local]  // 本地版函数
    HRESULT Invoke( // 根据函数序号,解释执行函数功能
                [in] DISPID dispIdMember,
                [in] REFIID riid,
                [in] LCID lcid,
                [in] WORD wFlags,
                [in, out] DISPPARAMS * pDispParams,
                [out] VARIANT * pVarResult,
                [out] EXCEPINFO * pExcepInfo,
                [out] UINT * puArgErr
            );

    [call_as(Invoke)] // 远程版函数
    HRESULT RemoteInvoke(
                [in] DISPID dispIdMember,
                [in] REFIID riid,
                [in] LCID lcid,
                [in] DWORD dwFlags,
                [in] DISPPARAMS * pDispParams,
                [out] VARIANT * pVarResult,
                [out] EXCEPINFO * pExcepInfo,
                [out] UINT * pArgErr,
                [in] UINT cVarRef,
                [in, size_is(cVarRef)] UINT * rgVarRefIdx,  
                [in, out, size_is(cVarRef)] VARIANTARG * rgVarRef
            );
}
  以上 IDispatch 接口函数的讲解,我们留到后回中进行介绍。如何在组件程序中实现这些函数那?还好,还好,就象 IUnknown 一样,MFC 和 ATL 都帮我们已经完成了。本回我们着重介绍组件的编写,下回则介绍组件的调用方法。

[B]三、用 MFC 实现自动化组件

      我写的这整个系列文章---《COM 组件设计与应用》,多是用 ATL 写组件程序,但由于自动化非常有用,在后续的文章中,还要给大家介绍组件的“事件”功能,还要介绍如何在 MFC 的程序中象 WORD 一样支持“宏”的功能。这些都要用到 MFC,所以就给读者唠一唠下啦。
    
  3-1:建立一个工作区(Workspace)

      3-2:建立一个 MFC DLL 工程(Project),工程名称为“Simple5”

按此在新窗口浏览图片
    
  3-3:一定要选择 automation,切记!切记!

按此在新窗口浏览图片
    
  3-4:建立新类

按此在新窗口浏览图片
    
  3-5:在新建类中支持automation

按此在新窗口浏览图片

  Class information - Name
你随便写个类名子啦

  Class information - Base class 一定要从 CComTarget 派生呀,只有它才提供了 IDispatch 的支持

  Automation - None 表示不支持自动化,你要选择了它,那就白干啦

  Automation - Automation 支持自动化,但不能被直接实例化。后面在讲解多个 IDispatch 的时候就用到它了,现在先不要着急。

  Automation - Createable by type ID
一定要选择这个项目,这样我们在后面的调用中,VB就能够CreateObject(),VC就能够CreateDispatch()对组件对象实例化了。注意一点,这个 ID 其实就是组件的 ProgID 啦。

      3-6:启动 ClassWizard,选择 Automation 卡片,准备建立函数 

按此在新窗口浏览图片
    
  3-7:添加函数。我们要写一个整数加法函数Add()。

按此在新窗口浏览图片
    3-8:再增加一个转换字符串大小写的函数 Upper()。函数返回值是 BSTR,这个没有什么疑问,但参数类型怎么居然是 LPCTSTR?在 COM 中,字符串不是应该使用 BSTR 吗?是的,是应该使用 BSTR,但由于我们是用 MFC 写自动化组件,它帮我们进行 BSTR 和 LPCTSTR 之间的转换了。
按此在新窗口浏览图片
    
  3-9:好了,下面开始输入程序代码:
long CDispSimple::Add(long n1, long n2)  
{
return n1 + n2;
}

BSTR CDispSimple::Upper(LPCTSTR str)  
{
CString strResult(str);
strResult.MakeUpper();

return strResult.AllocSysString();
}
      3-10:编译注册

      如果上面的操作由于疏忽而发生了错误,那么你可以手工进行改正。

  其一、步骤<3-6>的对话窗中有“Delete”操作;

  其二、你可以打开 ODL 文件(注2)进行修改,修改时要特别小心函数的声明中,有一个[id(n)] 的函数序号,可不要乱了;

  其三、同步修改 H/CPP 中的函数声明和函数体;

  其四、在CPP文件中,根据情况也要修改 BEGIN_DISPATCH_MAP/END_DISPATCH_MAP()函数影射宏。

     正确编译后,MFC不象ATL那样会自动注册。你需要手工执行 regsvr32.exe 进行注册,或者执行菜单“Tools\Register control”
[/B]

  四、用 ATL 实现双接口组件(操作方法和步骤,请参考[URL=http://dev.yesky.com/29/2037029.shtml]《[/URL][URL=http://www.vckbase.com/document/viewdoc/?id=1497]COM 组件设计与应用(五)》[/URL])

      4-1:建立一个 ATL 工程(Project),工程名称为“Simple6”

      4-2:按默认进行。选择 DLL 类型、不合并代理和存根代码、不支持MFC、不支持MTS

      4-3:New Atl Object... 选择Simple Object

      4-4:输入名称和属性,属性按默认进行,也就是 dual(双接口)方式(注3)

按此在新窗口浏览图片

按此在新窗口浏览图片
    
  4-5:增加函数。在 ClassView 卡片中,选择接口、鼠标右键菜单、Add Method...

Add([in] VARIANT v1, [in] VARIANT v2, [out, retval] VARIANT * pVal);
Upper([in] BSTR str, [out,retval] BSTR * pVal);
    
  关于Add()函数,你依然可以使用 Add([in] long n1, [in] long n2, [out,retval] long * pVal) 方式。但这次我们没有使用 long ,而是使用了 VARIANT 做参数和返回值。这里我先卖个关子,往下看,就知道使用 VARIANT 的精彩之处了。
    
  4-6:完成代码
STDMETHODIMP CDispSimple::Add(VARIANT v1, VARIANT v2, VARIANT *pVal)
{
::VariantInit( pVal ); // 永远初始化返回值是个好习惯

CComVariant v_1( v1 );
CComVariant v_2( v2 );

if((v1.vt & VT_I4) && (v2.vt & VT_I4) ) // 如果都是整数类型
{ // 这里比较没有使用 == ,而使用了运算符 & ,你知道这是为什么吗?
  v_1.ChangeType( VT_I4 ); // 转换为整数
  v_2.ChangeType( VT_I4 ); // 转换为整数

  pVal->vt = VT_I4;
  pVal->lVal = v_1.lVal + v_2.lVal; // 加法
}
else
{
  v_1.ChangeType( VT_BSTR ); // 转换为字符串
  v_2.ChangeType( VT_BSTR ); // 转换为字符串

  CComBSTR bstr( v_1.bstrVal );
  bstr.AppendBSTR( v_2.bstrVal ); // 字符串连接

  pVal->vt = VT_BSTR;
  pVal->bstrVal = bstr.Detach();
}
return S_OK;
}

STDMETHODIMP CDispSimple::Upper(BSTR str, BSTR *pVal)
{
*pVal = NULL; // 永远初始化返回值是个好习惯

CComBSTR s(str);
s.ToUpper(); // 转换为大写

*pVal = s.Copy();

return S_OK;
}

      刚才卖的关子,现在开始揭密了......加法函数Add()不使用long类型,而使用VARIANT的好处是:函数内部动态判断参数类型,如果是整数则进行整数加法,如果是字符串,则进行字符串加法(字符串加法就是字符串连接哈)。也就是说,如果参数是VARIANT,那么我们就可以实现函数的可变参数类型呀。怪怪个咙,真爽!
五、脚本中调用举例

      打开“记事本”程序,输入脚本程序,保存为 xxx.vbs 文件。然后在资源管理器里就可以双击运行啦。

按此在新窗口浏览图片

  如果你有能力,也可以用 JScript 书写上面的程序,然后保存为 xxx.js 文件,同样也可以在资源管理器里运行。另外需要说明的一点是,脚本程序文件的图标(win 2000下)是按此在新窗口浏览图片,如果你不是这样的(有一个软件叫“XX 解霸”。写这款软件的人水平太低,它居然使用 .vbs 的扩展名文件作为它的数据流文件,破坏了系统默认的文件类型影射模式,咳......),那么需要重新设置,方法是:

按此在新窗口浏览图片

  六、WORD 中使用举例

      6-1:录制一段宏程序

按此在新窗口浏览图片

按此在新窗口浏览图片
    
  6-2:选择“键盘”,当然你也可以把这个“宏”程序放到“工具栏”上去。这里我们随便指定一个快捷键,比如Ctrl+Z

按此在新窗口浏览图片
      6-3:开始录制了,下面你随便输入点什么东东。然后点“停止”按此在新窗口浏览图片
    
  6-4:接下来,我们执行菜单,选择这个刚刚录制的宏,然后编辑它

按此在新窗口浏览图片
    
  6-5:点“编辑”按钮,输入下面的程序:

按此在新窗口浏览图片

  不做解释了,你如果会一点点 VB ,就能看懂这个东东哈。然后保存关闭 VBA 的编辑器(注4)。

      6-6:执行啦,执行啦,看看有什么效果呀......

按此在新窗口浏览图片

  然后按快捷键Ctrl+Z

按此在新窗口浏览图片
    
  你已经扩展了 MS WORD 的功能啦,我们只是举了一个简单的例子,其实这个例子并没有什么实际应用的意义,因为人家 WORD 本身就有大小写转换功能。但通过这个小例子,你可以体会出自动化组件的功能了,有够厉害吧?!


W 3 C h i n a ( since 2003 ) 旗 下 站 点
苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
7,421.875ms