新书推介:《语义网技术体系》
作者:瞿裕忠,胡伟,程龚
   XML论坛     W3CHINA.ORG讨论区     计算机科学论坛     SOAChina论坛     Blog     开放翻译计划     新浪微博  
 
  • 首页
  • 登录
  • 注册
  • 软件下载
  • 资料下载
  • 核心成员
  • 帮助
  •   Add to Google

    >> 本版讨论高级C/C++编程、代码重构(Refactoring)、极限编程(XP)、泛型编程等话题
    [返回] 计算机科学论坛计算机技术与应用『 C/C++编程思想 』 → COM 组件设计与应用(十一、十二)- IDispatch 及双接口的调用/错误与异常处理 查看新帖用户列表

      发表一个新主题  发表一个新投票  回复主题  (订阅本版) 您是本帖的第 7674 个阅读者浏览上一篇主题  刷新本主题   树形显示贴子 浏览下一篇主题
     * 贴子主题: COM 组件设计与应用(十一、十二)- IDispatch 及双接口的调用/错误与异常处理 举报  打印  推荐  IE收藏夹 
       本主题类别:     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客楼主
    发贴心情 COM 组件设计与应用(十一、十二)- IDispatch 及双接口的调用/错误与异常处理

    一、前言
        前段时间,由于工作比较忙,没有能及时地写作。其间收到了很多网友的来信询问和鼓励,在此一并表示感谢。咳......我也需要工作来养家糊口呀......
        上回书介绍了两种方法来写自动化(IDispatch)接口的组件程序,一是用 MFC 方式编写“纯粹”的 IDispatch 接口;二是用 ATL 方式编写“双接口”的组件。

    二、IDispatch 接口和双接口
        使用者要想调用普通的 COM 组件功能,必须要加载这个组件的类型库(Type library)文件 tlb(比如在 VC 中使用 #import)。然而,在脚本程序中,由于脚本是被解释执行的,所以无法使用加载类型库的方式进行预编译。那么脚本解释器如何使用 COM 组件那?这就是自动化(IDispatch)组件大显身手的地方了。IDispatch 接口需要实现4个函数,调用者只通过这4个函数,就能实现调用自动化组件中所有的函数。这4个函数功能如下:


    此主题相关图片如下:
    按此在新窗口浏览图片
    从 Invoke() 函数的实现就可以看出,使用 IDispatch 接口的程序,其执行效率是比较低的。ATL 从效率出发,实现了一种叫“双接口(dual)”的接口模式。下面我们来看看,到底什么是双接口:

    按此在新窗口浏览图片
    图一、双接口(dual) 结构示意图

        从上图中可以看出,所谓双接口,其实是在一个 VTAB 的虚函数表中容纳了三个接口(因为任何接口都是从 IUnknown 派生的,所以就不强调 IUnknown 了,叫做双接口)。我们如果从任意一个接口中调用 QueryInterface()得到另外的接口指针的话,其实,得到的指针地址都是同一个。双接口有什么好处那?答:好呀,多好呀,特别好呀......

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

    示例一、IDispatch 调用原理篇

    void demo()
    {
     ::CoInitialize( NULL );  // COM 初始化

     CLSID clsid;    // 通过 ProgID 得到 CLSID
     HRESULT hr = ::CLSIDFromProgID( L"Simple8.DispSimple.1", &clsid );
     ASSERT( SUCCEEDED( hr ) ); // 如果失败,说明没有注册组件

     IDispatch * pDisp = NULL; // 由 CLSID 启动组件,并得到 IDispatch 指针
     hr = ::CoCreateInstance( clsid, NULL, CLSCTX_ALL, IID_IDispatch, (LPVOID *)&pDisp );
     ASSERT( SUCCEEDED( hr ) ); // 如果失败,说明没有初始化 COM

     LPOLESTR pwFunName = L"Add"; // 准备取得 Add 函数的序号 DispID
     DISPID dispID;     // 取得的序号,准备保存到这里
     hr = pDisp->GetIDsOfNames(  // 根据函数名,取得序号的函数
      IID_NULL,
      &pwFunName,     // 函数名称的数组
      1,       // 函数名称数组中的元素个数
      LOCALE_SYSTEM_DEFAULT,  // 使用系统默认的语言环境
      &dispID );     // 返回值
     ASSERT( SUCCEEDED( hr ) );  // 如果失败,说明组件根本就没有 ADD 函数

     VARIANTARG v[2];     // 调用 Add(1,2) 函数所需要的参数
     v[0].vt = VT_I4; v[0].lVal = 2; // 第二个参数,整数2
     v[1].vt = VT_I4; v[1].lVal = 1; // 第一个参数,整数1

     DISPPARAMS dispParams = { v, NULL, 2, 0 }; // 把参数包装在这个结构中
     VARIANT vResult;   // 函数返回的计算结果

     hr = pDisp->Invoke(   // 调用函数
      dispID,     // 函数由 dispID 指定
      IID_NULL,
      LOCALE_SYSTEM_DEFAULT, // 使用系统默认的语言环境
      DISPATCH_METHOD,  // 调用的是方法,不是属性
      &dispParams,   // 参数
      &vResult,    // 返回值
      NULL,     // 不考虑异常处理
      NULL);     // 不考虑错误处理
     ASSERT( SUCCEEDED( hr ) ); // 如果失败,说明参数传递错误

     CString str;   // 显示一下结果
     str.Format("1 + 2 = %d", vResult.lVal );
     AfxMessageBox( str );

     pDisp->Release();  // 释放接口指针
     ::CoUninitialize();  // 释放 COM
    }
    示例二、CComDispatchDriver 智能指针包装类的使用方法
    void demo()
    {
     // 已经进行过了 COM 初始化

     CLSID clsid;    // 通过 ProgID 取得组件的 CLSID
     HRESULT hr = ::CLSIDFromProgID( L"Simple8.DispSimple.1", &clsid );
     ASSERT( SUCCEEDED( hr ) ); // 如果失败,说明没有注册组件

     CComPtr < IUnknown > spUnk; // 由 CLSID 启动组件,并取得 IUnknown 指针
     hr = ::CoCreateInstance( clsid, NULL, CLSCTX_ALL, IID_IUnknown, (LPVOID *)&spUnk );
     ASSERT( SUCCEEDED( hr ) );

     CComDispatchDriver spDisp( spUnk ); // 构造只能指针
     CComVariant v1(1), v2(2), vResult; // 参数
     hr = spDisp.Invoke2( // 调用2个参数的函数
      L"Add",    // 函数名是 Add
      &v1,    // 第一个参数,值为整数1
      &v2,    // 第二个参数,值为整数2
      &vResult);   // 返回值
     ASSERT( SUCCEEDED( hr ) ); // 如果失败,说明或者没有 ADD 函数,或者参数错误

     CString str;   // 显示一下结果
     str.Format("1 + 2 = %d", vResult.lVal );
     AfxMessageBox( str );
    }
    示例程序中使用了 Invoke2()函数,其实你根据不同的函数,还可以使用 Invoke0()、Invoke1()、InvokeN()、PutProperty()、GetProperty()......等等等,的确很方便。

    示例三、加载类型库,产生包装类来使用
        这个方法使用更简单一些,如果你观察 MFC 帮你产生的包装类的实现,你就会发现,其实它调用的是 IDispatch 接口函数。使用 vc6.0 的朋友,步骤如下:
    1、建立一个 MFC 的应用程序
    2、开启 ClassWizard,执行 Add Class,选择 From a type library

    按此在新窗口浏览图片
    图二、加载类型库

    3、然后找到你要使用的组件文件 simple6.dll(tlb 文件也可以),选择接口后确认

    按此在新窗口浏览图片
    图三、选择类型库中需要包装的接口

    4、在适当的地方输入调用代码
    #include "simple6.h" // 包装类的头文件

    void demo()
    {
     // 已经进行过了 COM 初始化

     IDispSimple spDisp;  // 包装类的对象

     spDisp.CreateDispatch( _T("Simple6.DispSimple.1") ) //启动组件
     spDisp.xxx(...); // 调用函数

     spDisp.ReleaseDispatch(); // 释放接口
    }
        使用 vc.net 的朋友,步骤如下:
    1、建立一个 MFC 的应用程序
    2、执行菜单“添加\添加类”,选择 MFC 分类中的“类型库中的MFC类”

    按此在新窗口浏览图片
    图四、添加类型库中的MFC类

    3、选择组件文件 simple8.dll(或 tlb 文件),并选择需要包装的接口

    按此在新窗口浏览图片
    图五、选择文件和接口

    4、在适当的位置输入调用代码
    #include "CDispSimple.h" // 包装类的头文件

    void demo()
    {
     // 已经进行过了 COM 初始化

     CDispSimple spDisp; // 包装类的对象
     spDisp.CreateDispatch( _T("Simple8.DispSimple.1") ) // 启动组件
     spDisp.xxx(...); // 调用函数

     spDisp.ReleaseDispatch(); // 释放接口
    }
    示例四、使用 #import 方式调用组件
        #import 方式在第七回中已经作过介绍,这里就不多罗嗦了。大家下载本回的示例程序后,自己去看吧。并且一定要掌握这个方法,因为它的运行效率是最快的呀。

    四、小结
        留作业啦。在我们以前所实现的所有组件程序中,只添加了接口方法(函数),而没有添加接口属性(变量),你自己练习一下吧,很简单的,然后写个程序调用看看。其实对于 VC 来说,调用属性和调用方法没有太大的区别(vc 把属性包装为 GetXXX()/PutXXX()或getXXX()/putXXX()的函数方式),但在另外一些语言中(比如脚本语言)则更方便,设置属性值是:对象.属性 = 变量或常量,获取属性值是:变量 = 对象.属性。
        本回书至此做一了断,更多组件设计和使用的知识,且听下回分解......
    --------------------------------------------------------------------------------
    注1:多个自动化接口的实现方法,我们以后再说。
    注2:将来介绍 ITypeLib::GetTypeInfo() 的时候,大家再回味 IDispatch::GetTypeInfo()吧。
    注3:在后面介绍“事件”的时候,我们会自己真正去实现一个 IDispatch::Invoke() 函数。
    注4:介绍多个双接口实现的时候,会谈到这个问题。


       收藏   分享  
    顶(0)
      




    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/25 8:19:00
     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客2
    发贴心情 
    一、前言[/B]
      程序设计中,错误处理必不可少,而且通常要占用很大的篇幅。本回书着落在 COM 中的错误(异常)的处理方法。
      在组件程序中,如果遇到错误,一般有两个方式进行处理。

    二、简单返回
      对于比较简单的错误,直接返回表示错误原因的 HRESULT。比如下面几个就是常见的错误值:



    此主题相关图片如下:
    按此在新窗口浏览图片
    另外,你还可以返回自己构造 HRESULT 错误值。方法是使用宏 MAKE_HRESULT(sev,fac,code)



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

    调用者得到返回的 HRESULT 值后,也可以使用宏 HRESULT_SEVERITY()、HRESULT_FACILITY()、HRESULT_CODE() 来取得sev错误程度、fac设备信息和 code 错误代码。


    三、错误信息接口
      既然 COM 是靠各种各样的接口来提供服务的,于是很自然地就会想到,是否有一个接口能够提供更丰富的错误信息报告那?答案是:ISupportErrorInfo。下面这段代码是使用 ISupportErrorInfo 的一般方法:
    STDMETHODIMP Cxxx::fun()
    {
     ... ... ... ...

     CComQIPtr< ICreateErrorInfo> spCEI;
     ::CreateErrorInfo( &spCEI );

     spCEI->SetGUID( IID_Ixxx );  // 发生错误的接口IID
      
     spCEI->SetSource( L"xxx.xxx" ); // ProgID

     // 如果你的组件同时提供了帮助文件,那么就可以:
     spCEI->SetHelpContext( 0 );  // 设置帮助文件的主题号
     spCEI->SetHelpFile( L"xxx.hlp" ); // 设置帮助文件的文件名

     spCEI->SetDescription( L"错误描述信息" );

     CComQIPtr < IErrorInfo > spErrInfo = spCEI;
     if( spErrInfo )
       ::SetErrorInfo( 0, spErrInfo ); // 这时调用者就可以得到错误信息了

     return E_FAIL;
    }
      上面是原理性代码,在我们写的程序中,不用这么麻烦。因为 ATL 已经把上述的代码给我们包装成 CComCoClass::Error() 的6个重载函数了。如此,我们可以非常简单的改写为:
    STDMETHODIMP Cxxx::fun()
    {
     ... ... ... ...

     return Error( L"错误描述信息" );
    }
    四、关于 try/catch
      学习了 C++ 后,很多人都喜欢使用 try/catch 的异常处理结构。如果你使用 vc6.0 的ATL,编译器默认是不支持异常处理的,编译后会报告“warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify -GX”,解决方法是手工加上编译开关:

    按此在新窗口浏览图片
    图一、加上编译开关,支持C++的异常处理结构

      在vc.net 2003 中,编译器默认是支持异常处理结构的,所以不用特别进行设置。如果想减小目标文件的尺寸,你也可以决定不使用 C++ 异常处理,那么在项目属性中

    按此在新窗口浏览图片
    图二、在vc.net中修改是否支持C++异常结构的编译开关


    五、客户端接收组件的错误信息

      1、如果使用 API 方式调用组件,接收错误的方法是:
    HRESULT hr = spXXX->fun() // 调用组件功能
    if( FAILED( hr ) ) // 如果发生了错误
    {
     CComQIPtr < ISupportErrorInfo > spSEI = spXXX; // 组件是否提供了 ISupportErrorInfo 接口?
     if( spSEI ) // 如果支持,那么
     {
      hr = spSEI->InterfaceSupportsErrorInfo( IID_Ixxx ); // 是否支持 Ixxx 接口的错误处理?
      if( SUCCEEDED( hr ) )
      { // 支持,太好了。取出错误信息
       CComQIPtr < IErrorInfo > spErrInfo;  // 声明 IErrorInfo 接口
       hr = ::GetErrorInfo( 0, &spErrInfo ); // 取得接口
       if( SUCCEEDED( hr ) )
       {
        CComBSTR bstrDes;
        spErrInfo->GetDescription( &bstrDes ); // 取得错误描述
        ...... // 还可以取得其它的信息
       }
      }
     }
    }
      2、如果使用 #import 等包装方式调用组件,接收错误的方法是:
    try
    {
     ...... // 调用组件功能
    }
    catch( _com_error &e )
    {
     e.Description(); // 取得错误描述信息
     ...... // 还可以调用 _com_error 函数取得其它信息
    }
    六、编写支持错误处理的组件程序
      非常简单,只要在增加 ATL 组件对象的时候选中 ISupportErrorInfo 即可。

    按此在新窗口浏览图片
    图三、vc6.0 中,选中组件支持错误处理接口

    按此在新窗口浏览图片
    图四、vc.net 2003 中,选中组件支持错误处理接口

    七、小结
      阅读文章后,请下载本回的示例程序。示例程序中演示了三种错误处理方法和三种接收错误的方法,同时程序中也有比较详细的注释。[B]


    [此贴子已经被作者于2007-10-30 9:51:08编辑过]

    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/25 8:21:00
     
     GoogleAdSense
      
      
      等级:大一新生
      文章:1
      积分:50
      门派:无门无派
      院校:未填写
      注册:2007-01-01
    给Google AdSense发送一个短消息 把Google AdSense加入好友 查看Google AdSense的个人资料 搜索Google AdSense在『 C/C++编程思想 』的所有贴子 访问Google AdSense的主页 引用回复这个贴子 回复这个贴子 查看Google AdSense的博客广告
    2024/4/30 21:40:11

    本主题贴数2,分页: [1]

    管理选项修改tag | 锁定 | 解锁 | 提升 | 删除 | 移动 | 固顶 | 总固顶 | 奖励 | 惩罚 | 发布公告
    W3C Contributing Supporter! W 3 C h i n a ( since 2003 ) 旗 下 站 点
    苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
    78.125ms