第12章 COM组件技术操作技巧
COM是开发组件的一种方法,组件是一些小的二进制程序,它可以为操作系统或者应用程序提供服务。COM技术的发展进一步加强了程序的模块化编程的思想,使应用程序在更容易扩展与升级,具有更好的灵活性和动态性,COM支持了分部使应用程序的开发。
12.1 COM的概念和编程技巧
COM,即组件对象模型,是关于如何建立组件及如何通过组件建构应用程序的一个规范,是一种跨应用和语言共享二进制代码的方法。而C++是提倡源代码重用,源码级的复用容易产生错误,并且使得应用工程变得异常臃肿。COM组件是以Win32动态链接库(DLLs)或可执行文件(EXEs)的形式发布的可执行代码组成的。遵循COM规范编写的组件将能够满足对组件架构的所有需求。COM组件是动态链接的,COM使用DLL将组件动态链接起来,对于COM组件的封装是很容易的。COM组件按照一种标准的方式来宣布它们的存在。COM组件是一种给其他应用程序提供面向对象的API或服务的极好方法。
12.1.1 COM接口
1.接口的概念
在COM中,接口就是一组函数,它是不可变的。
任何一个组件都要包含一个IUnknown接口,客户同组件的交互都是通过这个接口完成的。在客户查询组件的其他接口时,也是通过这个接口完成的。IUnknown接口的定义包含在Win32 SDK中的UNKNOWN.H头文件中。
interface IUnknown
{
virtual HRESULT _stdcall QueryInterface(const IID& iid,void **ppv) = 0;
virtual ULONG _stdcall AddRef() = 0;
virtual ULONG _stdcall Release() = 0;
}
在IUnknown中定义了一个名为QueryInterface的函数。客户可以调用QueryInterface来决定组件是否支持某个特定的接口。
2.接口的注意事项
所有的COM接口都需要继承IUnknown。
由于所有的COM接口都继承了IUnknown,每个接口的vtbl中的前3个函数都是QueryInterface、AddRef和Release。若某个接口的vtbl中的前3个函数不是这3个,那么它将不是一个COM接口。由于所有的接口都是从IUnknown 继承的,因此所有的接口都支持QueryInterface,组件的任何一个接口都可以被客户用来获取它所支持的其他接口。
l 非虚拟继承:注意IUnknown并不是虚拟基类,所以COM接口并不能按虚拟方式继承IUnknown,这是由于会导致与COM不兼容的vtbl。若COM接口按虚拟方式继承IUnknown,那么COM接口的vtbl中的头3个函数指向的将不是IUnknown的3个成员函数。
l QuertyInterface可以用一个简单的if-then-else语句实现,但case语句是无法用的,因为接口标识符是一个结构而不是一个数。
12.1.2 CLSID和ProgID相互转换
1.问题阐述
每一个COM组件都需要指定一个 CLSID,并且不能重名,CLSID表示使用一个具有16个字节的数字,每个CLSID都在系统的注册表中被注册,它表示组件的实际路径,保证了组件路径的透明性。在Localserver32中保存了组件的路径,如图12-1所示。
图12-1 组件的路径
如果使用ATL或者其他开发环境,会自动产生一个CLSID,用以标识组件,同时,组件的标识还支持字符串方式,就是ProgID。二者都可以用来标识,只是采用了不同的表示形式。
2.实现技巧
通过上面的分析,两者之间的转换,可以通过查询注册表达得到,还可以通过函数CLSIDFromProgID和ProgIDFromCLSID完成转换,函数原型如下:
HRESULT CLSIDFromProgID(
LPCOLESTR lpszProgID, // 指向ProgID的指针
LPCLSID pclsid // 指向CLSID的指针
);
WINOLEAPI ProgIDFromCLSID(
REFCLSID clsid, // CLSID 的值,已知
LPOLESTR * lplpszProgID // 指向接收ProgID的缓冲区
);
3.实例代码
本实例演示了CLSID和ProgID之间的相互转换。首先创建一个简单的组件,然后利用一个调用者程序进行二者之间的转换。
(1)建立一个ATL工程Object,选择DLL方式,如图12-2所示。
Allow merging of proxy/stub code、Support MFC和Support MTS为默认即可。
(2)添加ATL类对象Cfun,设置其类对象的属性如图12-3所示。
图12-2 组件创建 图12-3 组件创建
从图12-3可以知道ProgID = OBJECT.Fun,默认为工程名+ShortName,单击Attributes选项卡,如图12-4所示。
图12-4 组件属性配置
这样,一个简单的COM组件就做好了,这个组件,没有任何功能实现。从这个COM组件中找出它的 CLSID,查看idl文件。其中86A70E6F-3F1C-46B5-86F9-C21DAD69C756为CLSID。
下面写一个函数,完成CLSID和ProgID的转换。
CLSID clsid = {0x86A70E6F,0x3F1C,0x46B5,{0x86,0xF9,0xC2,0x1D,
0xAD,0x69,0xC7,0x56}};
CString strClsID;
strClsID.Format("%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x",clsid.Data1,
clsid.Data2,clsid.Data3,clsid.Data4[0],clsid.Data4[1],clsid.Data4[2],
clsid.Data4[3],clsid.Data4[4],clsid.Data4[5],clsid.Data4[6],
clsid.Data4[7]);
SetDlgItemText(IDC_CLSID_ED,strClsID);
HRESULT hr;
LPOLESTR lpwProgID = NULL;
hr = ::ProgIDFromCLSID( clsid, &lpwProgID );
if ( SUCCEEDED(hr) )
{
//::MessageBoxW( NULL, lpwProgID, L"ProgID", MB_OK );
USES_CONVERSION;
LPCTSTR lpstr = OLE2CT( lpwProgID );
SetDlgItemText(IDC_PROGID_ED,lpstr);
IMalloc * pMalloc = NULL;
hr = ::CoGetMalloc( 1, &pMalloc ); // 取得 IMalloc
if ( SUCCEEDED(hr) )
{
pMalloc->Free( lpwProgID ); // 释放ProgID内存
pMalloc->Release(); // 释放IMalloc
}
}
其中OLE2CT完成了LPCOLESTR到LPCTSTR的转换,运行结果如图12-5所示。
图12-5 CLSID 转换为ProgID
12.1.3 利用IPicture接口实现显示BMP/JPG/GIF图像
1.问题阐述
在VB 中显示一个图像非常简单,只要将图像控件拖入到面板中,设置相应的属性即可。其实它的显示原理只是调用Windows的Ipicture接口,本节重点介绍了这一接口。
2.实现技巧
如何利用IPicture显示图像呢?首先了解一下这个接口的内容和作用、IPicture接口是一个COM类,操纵着图像对象及其属性。图像对象提供对位图的抽象,而Windows负责BMP、JPG和GIF位图的标准实现。IPictrue接口支持BMP、DIB、EMF、GIF、ICO、JPG、WMF格式图片的显示,但只能保存BMP和ICO格式的图片。下面是IPicture的方法描述:
l get_Handle 返回图像对象的Windows GDI句柄。
l get_Hpal 返回图像对象当前使用的调色板副本。
l get_Type返回当前图像对象的图像类型。
l get_Width 返回当前图像对象的图像宽度。
l get_Height 返回当前图像对象的图像高度。
l Render在指定的位置、指定的设备上下文上绘制指定的图像部分。
l set_Hpal设置当前图像的调色板。
l get_CurDC返回当前选中这个图像的设备上下文。
l SelectPicture将一个位图图像选入给定的设备上下文,返回选中图像的设备上下文和图像的GDI句柄。
l get_KeepOriginalForma返回图像对象KeepOriginalFormat 属性的当前值。
l put_KeepOriginalFormat设置图像对象的KeepOriginalFormat 属性。
l PictureChanged通知图像对象它的图像资源改变了。
l SaveAsFile将图像数据存储到流中,格式与存成文件格式相同。
l get_Attributes返回图像位属性当前的设置。
l IPicture接口和其他的接口不同,这个接口的实例不用CoCreateInstance来创建,而是采用一个专门的函数OleLoadPicture创建IPicture接口的实例。OleLoadPicture的声明和参数的含义如下所示:
STDAPI OleLoadPicture(
IStream * pStream,
LONG lSize,
BOOL fRunmode,
REFIID riid,
VOID ppvObj
);
第一个参数pStream指向包含有图像数据的流的指针,第二个参数lSize为从流中读取的字节数,第三个参数fRunmode为图像属性对应的初值,第四个参数riid为涉及到的接口标识描述要返回的接口指针的类型,第五个参数ppvObj为在rrid中用到的接口指针变量的地址。
3.实例代码
本实例使用IPicture接口完成图像JPG和GIF等图像的显示。建立一个基于单文档的工程,工程名为PictureRend。
在PiectureRend.h的函数InitInstance( )中初始化COM库,在程序退出的时候,关闭COM库,加载COM库的代码如下:
BOOL CPictureRendApp::InitInstance()
{
//--------------------------------------------------------------------------
AfxEnableControlContainer();
*****************************
}
关闭COM库的代码如下:
int CPictureRendApp::ExitInstance()
{
CoUninitialize();
return CWinApp::ExitInstance();
}
在PictureRendView.h中声明:
CFile m_File;
IStream* m_pStream ;
IPicture* m_pPicture ;
LONG m_nWidth;
LONG m_nHeight;
在打开菜单中,获取打开的图像的路径名,代码如下:
void CPictureRendApp::OnFileOpen()
{
CFileDialog dlg(TRUE, NULL, NULL, OFN_HIDEREADONLY
|OFN_OVERWRITEPROMPT,
"文件 (*.jpg)|*.jpg|文件 (*.gif)|*.gif|All Files (*.*)|*.*||", NULL);
if( IDOK != dlg.DoModal())
{
return;
}
m_strPicPath = dlg.GetPathName();
PostMessage(((CMainFrame*)m_pMainWnd)->GetActiveView()->m_hWnd,
WM_ON_RENDER_PIC,0,0);//一旦打开文件,消息响应
}
注意,在响应“打开”消息的时候,要对消息映射的地方加以改变,如下:
ON_COMMAND(ID_FILE_OPEN,OnFileOpen)。
在View中添加渲染图像,代码如下:
void CPictureRendView::OnRenderPic()
{
HRESULT hr;
CClientDC dc(this);
m_File.Open( theApp.m_strPicPath, CFile::modeRead | CFile::shareDenyNone );
// 读入文件内容
DWORD dwSize = m_File.GetLength();
HGLOBAL hMem = ::GlobalAlloc( GMEM_MOVEABLE, dwSize );
LPVOID lpBuf = ::GlobalLock( hMem );
m_File.ReadHuge( lpBuf, dwSize );
m_File.Close();
m_pStream = NULL;
m_pPicture = NULL;
// 由 HGLOBAL 得到 IStream,参数 TRUE 表示释放 IStream 的同时,释放内存
hr = ::CreateStreamOnHGlobal( hMem, TRUE, &m_pStream );
::GlobalUnlock( hMem );
ASSERT ( SUCCEEDED(hr) );
hr = ::OleLoadPicture(
m_pStream, dwSize, TRUE, IID_IPicture, ( LPVOID * )&m_pPicture );
ASSERT(hr==S_OK);
long nWidth,nHeight; // 宽高,MM_HIMETRIC 模式,单位是0.01毫米
m_pPicture->get_Width( &m_nWidth ); // 宽
m_pPicture->get_Height( &m_nHeight ); // 高
原大小显示//
CSize sz( m_nWidth, m_nHeight );
dc.HIMETRICtoDP( &sz );
// 转换 MM_HIMETRIC 模式单位为 MM_TEXT 像素单位
m_pPicture->Render(dc.m_hDC,0,0,sz.cx,sz.cy,
0,m_nHeight,m_nWidth,-m_nHeight,NULL);
}
在程序退出时,释放数据流和接口指针:
if ( m_pPicture ) m_pPicture->Release(); // 释放 IPicture 指针
if ( m_pStream ) m_pStream->Release(); // 释放 IStream 指针,同时释放了 hMem
运行效果如图12-6所示。
图12-6 IPicture接口的使用
12.1.4 创建/删除快捷方式的技巧
1.问题阐述
Windows的快捷方式实际上是一个带有扩展名LNK的数据文件,快捷方式中包括所指对象的大量的信息,如目标对象的路径和名称,工作目录,要传递的命令行参数,运行时的初始显示状态,图标及其快捷键等。通过在快捷方式上单击鼠标右键并在弹出菜单中选择【属性】命令可以观察该快捷方式的这些性质。
快捷方式的数据文件如果存放在C:/Windows/Desktop子目录下,这个快捷方式就会显示在桌面上,而如果存放在C:/Windows/Start Menu/Programs子目录下,这个快捷方式就会作为【开始】菜单的一个菜单项出现。
在操作系统中,通过手工操作建立这些应用程序的快捷方式并不复杂,在此不再赘述,在应用程序中如何完成上述的工作呢?
2.实现技巧
Windows外壳(Shell)的快捷方式是以OLE技术的组件对象模型COM(Component ObjectModal)为基础而设计的。利用COM模型,一个应用程序可以调用另一应用程序的某些功能。创建Windows的快捷方式比较容易,首先利用OLE通过调用CoCreateInstance()函数建立一个IID_IShellLink实例,并同时得到其接口指针。利用这个接口指针可以对其各项属性进行设置。为了使这些信息以快捷方式的数据文件(*.lnk)格式保存起来,还需要从IID_IShellLink对象取得其IID_IPersistFile接口指针,以便于调用其成员函数Save()保存前面设置的信息。
3.实例代码
本实例演示了如何利用IshellLink接口创建快捷方式。创建一个基于对话框的工程,工程名为ShortCut。
快捷方式生成代码:
void CShortCutDlg::OnCtreateBtn()
{
GetDlgItemText(IDC_EDIT2,m_strLnkPath);
if(m_strLnkPath == "")
{
MessageBox("请输入快捷方式的路径");
return ;
}
CreateShortCut((LPCTSTR)(m_strDesPath),(LPCTSTR)m_strLnkPath);
}
创建快捷方式的代码:
/************************************************************************/
/* 作用:建立块捷方式
/* 参数 lpExeName:EXE 文件全路径名
/* 参数 lpLinkPath:快捷方式文件全路径名
/*
/************************************************************************/
void CShortCutDlg::CreateShortCut(LPCTSTR lpExeName,LPCTSTR lpLinkPath)
{
IShellLink * psl = NULL;
IPersistFile * ppf = NULL;
HRESULT hr = ::CoCreateInstance(
CLSID_ShellLink,
NULL,
CLSCTX_INPROC_SERVER,
IID_IShellLink,
(LPVOID *)&psl ); // 获取接口实例
if (SUCCEEDED(hr))
{
psl->SetPath( lpExeName ); // 全路径程序名
psl->SetIconLocation("moon_ie.ico",0);
psl -> SetHotkey( MAKEWORD( 'X', HOTKEYF_SHIFT |HOTKEYF_CONTROL)) ;
psl->SetDescription("create a short cut");
hr = psl->QueryInterface( // 查找持续性文件接口指针
IID_IPersistFile, // 持续性接口 IID
(LPVOID *)&ppf
); // 得到接口指针
if ( SUCCEEDED(hr) )
{
USES_CONVERSION;
ppf->Save( T2COLE( lpLinkPath ), TRUE ); // 保存
}
}
if ( ppf )
ppf->Release();
if ( psl )
psl->Release();
}
选择快捷方式的目标文件:
void CShortCutDlg::OnFindFileBtn()
{
CFileDialog file(TRUE,".exe",".exe",OFN_HIDEREADONLY | OFN_
OVERWRITEPROMPT,"文件(*.exe)|*.exe||",NULL);
if(file.DoModal() == IDOK)
{
m_strDesPath = file.GetPathName();
SetDlgItemText(IDC_EDIT1,m_strDesPath);
}
}
12.1.5 C++类对象、DLL和 COM 的区别和联系
1.问题阐述
C++类对象、DLL及COM都是面对向对象的,它们都实现了重用,避免了程序员重复造轮子的现象的产生。那么它们之间有什么区别呢?
2.实现技巧
C++对象重用是定义在源代码级别上的,而DLL和COM是定义在二进制级别上的重用,是执行代码重用的技术。DLL和COM都实现了模块之间的通信,但是DLL对于内存的利用和数据类型使用没有一定的约束规范。而COM对数据、内存等其他的几个方面进行了规范,使得软件模块间实现调用、通信的标准。所以,COM不是接口,也不是对象,它是一种标准。符合COM标准的对象就是COM对象,其实COM对象无非是实现了很多接口的对象而已。COM对象必须实现IUnknown接口,这个接口是管理COM对象生命周期的。当COM对象不使用的时候,这个接口定义的方法负责释放内存。一个COM对象可以没有任何别的接口,但是必须要有这个接口,它也是默认实现了接口。QI,即所谓的查询接口。由于COM中有很多接口,不同的接口管理着COM的不同类型的方法,因此从一个接口可以使用的方法转到另一个接口可以使用的方法的过程称为QI,这个过程是由Idispatch接口管理的。每个组件都有一个独一无二的标识,即广泛唯一标识(GUIDs),它代表了COM的身份。一个COM对象可以有多个接口,一个接口可以被多个COM对象实现。
12.1.6 使用C++API创建COM对象
1.问题阐述
COM的设计目的,是要能实现跨语言的调用,既然是跨语言的,那么组件的接口描述就必须在任何语言环境中都要能够认识。如何实现模块之间的调用呢?
2.实现技巧
微软使用了一个新的文件格式IDL文件(接口定义描述语言)。IDL是一个文本文件,IDL经过编译,生成二进制的等价类型库文件 TLB 提供给其他语言来使用。
调用组件程序的方法可以有#include 头文件的方法和#import 类库方法。COM对象的创建由API函数CoCreateInstance完成,其函数原型声明如下:
STDAPI CoCreateInstance(
REFCLSID rclsid,
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,
REFIID riid,
LPVOID * ppv
);
第一个参数是组件的CLSID,第二个参数聚合时才能用到,第三个参数为进程内的服务,第四个参数为接口的IID,第五个参数返回接口的指针。
3.实例代码
本实例演示了如何调用COM组件。首先生成一个COM组件,然后再生成一个主程序调用COM组件。
在上面的建立的COM组件中增加两个函数,其中一个完成一个简单的加法运算,另一个完成一个字符串的连接任务,代码如下。
完成加法任务的函数代码:
STDMETHODIMP CFun::Add(long a, long b, long *retval)
{
// TODO: 在此添加相关代码
*retval = a+b;
return S_OK;
}
完成字符串连接的代码:
STDMETHODIMP CFun::CatString(BSTR str1, BSTR str2, BSTR *pretval)
{
// TODO: 在此添加相关代码
int nLen1 = ::SysStringLen(str1); //计算BSTR字符串的长度
int nLen2 = ::SysStringLen(str2);
*pretval = ::SysAllocStringLen(str1,nLen1+nLen2);
//申请nLen1+nLen2长度的内存区域,将字符串存进去
if(nLen2>0)
{
::memcpy(pretval+nLen1,str2,sizeof(WCHAR)*nLen2);
//将str2复制进申请的内存缓冲区
}
return S_OK;
}
调用组件的代码,首先将组件ID、接口ID及接口的函数集包含在工程中,代码如下:
#include "../Object/OBJECT.h"
#include "../Object/OBJECT_i.c"
调用过程如下所示:
/*将BSTR转换为CString*/
CString convert(BSTR b)
{
CString s;
if(b == NULL)
return s; // empty for NULL BSTR
#ifdef UNICODE
s = b;
#else
LPSTR p = s.GetBuffer(SysStringLen(b) + 1);
::WideCharToMultiByte(CP_ACP,
0,
b,
-1,
p,
SysStringLen(b)+1,
NULL,
NULL);
s.ReleaseBuffer();
#endif
return s;
}
void CExample1Dlg::OnExeBtn()
{
// TODO: 在此添加相关代码
::CoInitialize( NULL );
UpdateData(TRUE);
IFun* pFun = NULL;
IUnknown* pUn = NULL;
try
{
HRESULT hr = ::CoCreateInstance(
CLSID_Fun,
NULL,
CLSCTX_INPROC_SERVER,
IID_IUnknown,
(LPVOID*)&pUn
);
if(FAILED(hr))
{
MessageBox("请您注册组件");
return;
}
hr = pUn->QueryInterface(IID_IFun,(LPVOID*)&pFun);
if(FAILED(hr))
{
MessageBox("检查接口是否存在");
return;
}
pFun->Add(m_add1,m_add2,&m_add3);
BSTR bstr1 = m_strCat1.AllocSysString();
BSTR bstr2 = m_strCat2.AllocSysString();
BSTR bstr3;
pFun->CatString(bstr1,bstr2,&bstr3);
m_strCat3 = convert(bstr3);
UpdateData(FALSE);
}
catch ( LPCTSTR lpErr)
{
AfxMessageBox(lpErr);
}
pUn->Release(); //释放接口
pFun->Release(); //释放接口
::CoUninitialize();
}
使用API创建接口实例,最后不要忘记了加接口指针Realease。大部分COM的API都是以Co开头的,这个前缀是COM Object的缩写。
12.1.7 使用智能指针创建COM对象
1.问题阐述
利用COM的API创建COM对象,一切的处理工作都要程序员手动完成,比如接口指针最后的释放,这样如果在任务繁重的情况下,很容易出现忘记释放指针的情况,为了解决这个问题,COM能够提供一种自动释放的机制,于是引入了智能指针。
2.实现技巧
使用COM中的智能指针,使对象的创建工作更加简单化。而且它提供自动销毁生成的 COM的对象机制,使程序的精力转移到其他的方面。下面看一下ATL提供的两个智能指针—CcomPtr和CComQIPtr。
CcomPtr类实现客户端基本的COM引用计数模型,CComPtr有一个数据成员,它是一个未经过任何加工的COM接口指针。其类型被作为模板参数传递。
CComPtr<IUnknown> spUnk;
CComPtr<IFun> spFun;
默认的构造函数将这个原始指针数据成员初始化为NULL。
智能指针的参数要么是原始指针,要么是相同类型的智能参数。不论哪种情况,智能指针都调用AddRef控制引用。CComPtr的赋值操作符既可以处理原始指针,也可以处理智能指针,并且在调用新分配指针的AddRef之前自动释放保存的指针。最重要的是,CComPtr的析构函数释放保存的接口(如果非空)。
CComQIPtr对于CComPtr只增加了两个成员函数,CComQIPtr有两个模板参数:一个是被操纵的指针类型,另一个是对应于这个指针类型的GUID。例如,下列代码声明了操纵IDataObject和IPersist接口的智能指针:
CComQIPtr<IFun, &IID_IFun> spUnk;
CCom
CComQIPtr的优点是它有重载的构造函数和赋值操作符。同类版本(例如,接收相同类型的接口)仅仅进行AddRef右边的赋值/初始化操作,这实际上就是CComPtr的功能。异类版本(接收类型不一致的接口)正确调用QueryInterface来决定是否这个对象确实支持所请求的接口:
void f(IFun* spUnk) {
CComQIPtr<IFun, &IID_IFun> p;
// 同类赋值 - AddRef''s
p = spUnk;
CComQIPtr<IDataObject, &IID_IDataObject> do;
// 异类赋值 - QueryInterface''s
do = spUnk;
}
3.实例代码
本实例的目的借助于智能指针创建COM对象,建立一个基于对话框的工程。首先初始化应用工程的COM库,在CXXXXApp的InitInstance()中添加初始化语句:
if(AfxOleInit()==FALSE)
{
AfxMessageBox("初始化环境COM库失败!");
return FALSE;
}
引入智能指针类,引入组件的CLSID、接口的ID及接口函数集:
#include "../Object/Object.h"
#include "../Object/Object_i.c"
#include <atlbase.h>
智能指针操作代码如下:
void CExample2Dlg::OnExeBtn()
{
UpdateData(TRUE);
CComPtr<IUnknown> spUnk; //定义IUnknown的智能指针
CComPtr<IFun> spFun; //定义IFun的智能指针
try
{
HRESULT hr = spUnk.CoCreateInstance(CLSID_Fun,NULL,
CLSCTX_INPROC_SERVER); //启动组件
if(FAILED(hr))
{
MessageBox("组件没有注册!");
return ;
}
hr = spUnk.QueryInterface(&spFun); //查找IFun的接口
if(FAILED(hr))
{
MessageBox("没有接口IFun");
return;
}
spFun->Add(m_add1,m_add2,&m_add3);
CComBSTR s1(m_str1);
CComBSTR s2(m_str2);
CComBSTR s3;
spFun->CatString(s1,s2,&s3);
m_str3 = convert(s3.m_str); //将BSTR转换为CString 同上
}
catch(LPCTSTR str)
{
MessageBox(str);
}
UpdateData(FALSE);
}
上面的代码演示了使用CcomPtr智能指针,下面的代码演示了CComQIPtr的用法:
void CExample2Dlg::OnComqiBtn()
{
UpdateData(TRUE);
CComPtr<IUnknown> spUnk; //定义IUnknown的智能指针
CComQIPtr<IFun> spFun; //定义IFun的智能指针
try
{
HRESULT hr = spUnk.CoCreateInstance(CLSID_Fun,NULL,
CLSCTX_INPROC_SERVER); //启动组件
if(FAILED(hr))
{
MessageBox("组件没有注册!");
return ;
}
spFun = spUnk; //会自动调用QueryInterface查找接口
if(spFun == NULL)
{
MessageBox("没有接口!");
return;
}
spFun->Add(m_add1,m_add2,&m_add3);
CComBSTR s1(m_str1);
CComBSTR s2(m_str2);
CComBSTR s3;
spFun->CatString(s1,s2,&s3);
m_str3 = convert(s3.m_str);
}
catch(LPCTSTR str)
{
MessageBox(str);
}
UpdateData(FALSE);
}
12.1.8 使用智能指针的封装类创建COM对象
1.问题阐述
在前面就已经说过,COM之间的通讯是跨语言的,在前面都只直接包含了C接口,那么如果一个COM对象没有给C接口,应该如何调用呢?
2.实现技巧
在前面提到过,为了实现跨语言,微软提供了一个新的文件格式.idl,idl经过编译后,生成二进制的等价类型库文件 TLB提供给其他的语言使用。首先要将这个文件导入到工程中:
#import "../Object/OBJECT.tlb" no_namespace
编译后会生成.tlh和.tlh文件的智能指针包装,利用智能指针包装创建对象。
3.实例代码
本实例演示了如何使用智能包装类创建COM对象,代码如下:
void CExample3Dlg::OnExeBtn()
{
UpdateData(TRUE);
IFunPtr spFun;
HRESULT hr = spFun.CreateInstance(__uuidof(Fun));
if(FAILED(hr))
{
MessageBox("创建COM接口失败!");
return;
}
m_add3 = spFun->Add(m_add1,m_add2);
BSTR s1,s2,s3;
s1 = m_str1.AllocSysString();
s2 = m_str2.AllocSysString();
s3 = spFun->CatString(s1,s2);
m_str3 = convert(s3);
UpdateData(FALSE);
}
如果使用命名空间,程序应该改动如下:
void CExample3Dlg::OnExeBtn()
{
UpdateData(TRUE);
OBJECTLib::IFunPtr spFun;
HRESULT hr = spFun.CreateInstance(__uuidof(OBJECTLib::Fun));
if(FAILED(hr))
{
MessageBox("创建COM接口失败!");
return;
}
m_add3 = spFun->Add(m_add1,m_add2);
BSTR s1,s2,s3;
s1 = m_str1.AllocSysString();
s2 = m_str2.AllocSysString();
s3 = spFun->CatString(s1,s2);
m_str3 = convert(s3);
UpdateData(FALSE);
}
12.1.9 创建一个自动化组件的技巧
1.问题阐述
前面创建的COM对象都是定制接口对象,也就是所谓的前绑定。编译器在编译的时候装载类型库,分别使用了 #include 方法和 #import 方法来实现。装载了类型库后,编译器就知道如何编译接口函数的调用了。脚本语言是解释执行的语言,它在执行的时候不会知道具体的函数的地址,那么在脚本语言中,如何调用COM组件呢?
2.实现技巧
为了使脚本语言支持COM组件的调用,MS提供了另外一个接口,即Idispatch接口,又称为自动化接口,也被称为后绑定接口。自动化组件,即实现了Idispatch接口的组件。IDispatch接口用IDL形式说明如下:
[
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接口有4个函数,解释语言的执行器就通过仅有的4个函数来执行组件所提供的功能。
其中GetIDsOfNames将读取一个函数的名称并返回其调度ID,又称为DISPID。DISPID并不是一个GUID,而只是一个长整数,它标识的是一个函数。对于IDispatch的每一个特定的实现,DISPID是唯一的。IDispatch的每一个实现都有其自己的IID。为执行某个函数,自动化控制程序将把DISPID传给Invoke成员函数。
Invoke可以将DISPID作为函数指针数组的索引,这一点同常规COM接口是相似的。但是自动化服务并不需要按此种方式实现Invoke。一个简单的自动化服务器可以根据DISPID用一个case语句执行不同的代码。IDispatch::Invoke将实现一组按索引来访问的函数,Invoke的一个实现所实现的函数集被称为一个调度接口,而COM接口是一个指向一个函数指针数组的指针,此数组的前3个元素分别是QueryInterface、AddRef及Release。Invoke函数参数含义:第一个参数是控制程序待调用函数的DISPID;第二个参数是保留值,必须为IID_NULL;第三个参数为保存位置信息;第四个参数为所指的调用函数的类型,它的值可以是DISPATCH_METHOD、DISPATCH_PROPERTYGET和DISPATCH_PROPERTYPUT值中的一个;第五个参数是传给被调用函数的参数;第六个参数是返回值。
3.实例代码
本实例演示如何创建双接口组件。启动ATL COM AppWizard,工程名为Object。选择DLL类型、不合并代理和存根代码、不支持MFC、不支持MTS。“New Atl Object”选择“Simple Object”,输入名称DFun,属性按默认设置,增加函数Add,如图12-7所示。
选择“Attributes”选项卡,设置双接口的属性如图12-8所示。
图12-7 双接口设置向导 图12-8 设置双接口属性
在Idl中组件描述如下:
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(DAB964C5-24E5-4648-8765-F8D8AA9D6F23),
dual,
helpstring("IDFun Interface"),
pointer_default(unique)
]
interface IDFun : IDispatch
{
[id(1), helpstring("method Add")] HRESULT Add([in]long n1,[in]long
n2,[out,retval]long* pVal );
};
[
uuid(51000335-57C3-4D25-8A05-8768264350F0),
version(1.0),
helpstring("Object 1.0 Type Library")
]
library OBJECTLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(DF7E08FC-51B3-4498-9D14-63E5E704133E),
helpstring("DFun Class")
]
coclass DFun
{
[default] interface IDFun;
};
增加函数,在 ClassView 选项卡中,选择接口,单击鼠标右键菜单,在弹出菜单中添加接口函数。
Add([in] VARIANT v1, [in] VARIANT v2, [out, retval] VARIANT * pVal);
Upper([in] BSTR str, [out,retval] BSTR * pVal);
函数实现的参考代码如下:
/************************************************************************/
/* 转换为小写
/************************************************************************/
STDMETHODIMP CDFun::Lower(BSTR bstr, BSTR *pVal)
{
*pVal = NULL;
CComBSTR s(bstr);
s.ToLower(); // 转换为小写
*pVal = s.Copy();
return S_OK;
}
/************************************************************************/
/* 加法运算
/* 整数的加法运算和字符串的加法运算
/************************************************************************/
STDMETHODIMP CDFun::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;
}
最后在脚本中调用COM接口实例,创建一个记事本,更改扩展名为vbs,脚本的参考代码如下:
Set obj = CreateObject("Object.DFun")
MsgBox obj.Lower("接口函数Lower:THIS IS A TEST")
MsgBox obj.Add("1+2=" ,obj.Add(1,2))
Set obj = Nothing
运行执行结果如图12-9和图12-10所示。
图12-9 调用COM接口中的Lower函数 图12-10 调用COM接口中的Add函数
12.1.10 使用ATL创建进程外组件
1.问题阐述
在前面创建的组件均属于进程内组件,即组件对象和客户进程在同一个进程,客户进程在同一进程内调用组件对象提供的服务;进程外组件,组件对象和客户进程分属不同的进程,客户进程可以跨进程调用组件对象提供的服务。如何创建一个进程外的组件呢?
2.实现技巧
使用ATL创建组件向导创建一个进程外组件,首先使用ATL COM AppWizard创建一个工程,如图12-11所示。
单击【OK】按钮,选择组件提供服务时所用的类型。在此因为是进程外组件,故选用Executable类型,如图12-12所示。
图12-11 创建工程 图12-12 选择组件提供服务器的类型
最后添加组件提供的接口,和前面介绍的就基本一致了。
3.实例代码
本例的消费者和生产者的简单模型由COM来实现。
组件服务器具体创建步骤如下。
(1)用ATL COM Appwzard创建一个新的工程,工程名为ProcOut。
(2)选择组件提供服务时所用的类型(.dll或.exe)。
(3)在工程中插入一个对象。在ClassView选项页面,用鼠标右键单击工程名,在弹出的右键菜单中选择【New ATL Object】,在打开的对话框中选中Simple Object,如图12-13所示。
图12-13 新建组件对象
(4)单击【Next】按钮,在打开对话框的ShortName中输入 Modu,其他的按默认设置,如图12-14所示。
图12-14 组件命名
(5)定义接口函数。在接口IModu上面点击鼠标右键,在弹出的快捷菜单中选择【Add Method】命令。出现添加生产函数对话框,添加接口函数HRESULT,参数Produce([in]long nProduce),如图12-15所示。
图12-15 添加生产函数
(6)添加消费函数HRESULT Customer([in]long nProduce),如图12-16所示。
图12-16 添加消费函数
(7)添加属性,在ClassView页中用鼠标右键单击接口,选择【Add Property】命令,打开的对话框如图12-17所示。
图12-17 添加属性
(8)组件服务的代码,组件服务提供了生产和消费函数及改变属性的两个函数,其代码参考如下。
在类Cobject的头文件Object.h中添加成员变量:
int m_lNum;//表示当前的数量
生产函数的参考代码如下:
STDMETHODIMP CObject::Produce(long nProduce)
{
m_lNum += nProduce;
return S_OK;
}
消费函数的参考代码如下:
STDMETHODIMP CObject::Customer(long nCustomer)
{
m_lNum -= nCustomer;
if(m_lNum<0)
{
MessageBox(NULL,"消耗没了","提示",MB_OK);
m_lNum = 0;
}
return S_OK;
}
接口属性函数的参考代码如下:
STDMETHODIMP CObject::get_CurrentNum(long *pVal)
{
*pVal = m_lNum;
return S_OK;
}
STDMETHODIMP CObject::put_CurrentNum(long newVal)
{
m_lNum = newVal;
return S_OK;
}
(9)在客户端创建一个基于对话框的工程ProcOutTest,放置两个文本框分别表示生产或消耗的数量和当前现存的数量。放置两个按钮,用于响应生产和消费事件。参考代码如下。
在ProcOutTest的InitInstance中添加AfxOleInit( )初始化应用程序COM环境。
在stdafx.h中引入ProcOut.tlb库:
#import "ProcOut.tlb" no_namespace
在ProcOutTestDlg.h中声明接口的智能指针:
IobjectPtr m_Iobject
在ProcOutTest.cpp中的OinitDialog添加创建接口实例的代码:
HRESULT hr = m_IObject.CreateInstance(L"ProcOut.Object");
if(FAILED(hr))
{
MessageBox("创建接口实例失败!");
return FALSE;
}
m_IObject->put_CurrentNum(100);//初始化数量为100
m_IObject->get_CurrentNum(&m_Cur);
响应生产和消费两个按钮事件的代码:
void CProcOutTestDlg::OnProduceBtn()
{
UpdateData(TRUE);
m_IObject->Produce(m_Num);
m_IObject->get_CurrentNum(&m_Cur);
UpdateData(FALSE);
}
void CProcOutTestDlg::OnCustomerBtn()
{
UpdateData(TRUE);
m_IObject->Customer(m_Num);
m_IObject->get_CurrentNum(&m_Cur);
UpdateData(FALSE);
}
运行效果如图12-18所示。
图12-18 生产和消费