ActiveX控件和它的容器
1.COM基础
2.ActiveX控件及实现
3.ActiveX控件容器及实现
4.总结
1.COM基础
COM是一种组件开发技术, 它实际上是一种在二进制层上兼容的软件开发方法的规范. COM技术是与具体的编程语言无关的技术, 只要是支持COM开发的开发工具都可以用来进行COM应用开发, 而它们在二进制上兼容的要求由各个开发工具来实现, 绝大部分是由编译器实现的.
COM的基础概念有以下几部分组成,1)接口的定义及实现, 2)IUnknown接口, 3)GUID (COM中所涉及的概念还有很多,具体的可以参阅其他资料 ). 下面分别简单的介绍它们.
1).接口的定义及实现
一个接口实际上就是一组定义了具体的功能的函数的集合, 这些定义没有具体的实现. 接口的定义类似于C++中的纯虚类定义, 它定义了接口函数的返回类型、参数个数及函数的功能, COM组件就是靠这些接口相互进行通信. 一个简单的例子如下.(MFC为我们提供了许多方便的宏用来定义接口, 而且在一般情况下, 我们是使用IDL或者是ODL来定义接口, 而不是使用下面这种形式).
interface IStack:IUnknown {
virtual void Pop(int* pvalue) = 0;
virtual void Push(int value) = 0;
};
上面定义的就是一个简单的接口IStack. 它定义了两个方法,并且描述了这两个方法的返回类型 (void), 参数个数和类型, 两个函数都用纯虚函数实现,在定义接口的文件里并不需要这两个函数的具体实现. 一般情况下接口的实现可以通过对接口的继承来完成, 但在一个组件实现了多个接口的情况下MFC采用了嵌套子类的实现方法, 具体情况可以参阅其他文档. ]
2).IUnknown接口
在上面的例子中, IStack从一个叫做IUnknown的接口继承而来, 那么IUnknown接口是一个什么样的接口呢? 再COM规范中要求, 任何一个COM组件必须实现IUnknown接口, IUnknown接口的主要作用是用来维护COM组件的引用计数和对COM组件实现的接口进行查询, 先让我们看一下IUnknown接口的定义.
interface IUnknown {
virtual void QueryInterface(REFIID riid, void** ppvObject) = 0;
virtual HRESULT AddRef() = 0;
virtual HRESULT Release() = 0;
};
在上面IUnknown接口中, AddRef和Release是用来维护引用计数的. 因为一个COM组件可以同时为多个应用程序服务, 如果没有一种适当的机制来维护COM组件的生存期的话, 那么当一个使用COM组件的应用程序结束时, 这个组件也会被同时释放掉, 那么其他使用这个组件的应用程序就会出现要求访问的组件不存在的错误.所以COM子系统就是用引用计数来解决这个问题, 当一个应用程序要求使用某个组件时, 它就增加这个组件的引用计数, 当这个应用程序结束时, 它就减少这个组件的引用计数, 当一个组件的引用计数为0时, COM子系统就会释放这个组件.
QueryInterface方法是用来在COM组件中查询一个接口是否被实现的方法, 因为每一个接口都拥有一个能唯一标识它自己的一个ID, 称为IID, 通过传递这IID, 我们就可以查询一个接口是否被该 COM组件实现, 如果该组件实现了该接口,我们就可以利用QueryInterface方法的第二个参数传回的值来使用这个接口的方法.
3).GUID
上面提到, 每个接口都由一个唯一标识自己的ID, IID, 同样每个实现了某个接口的C++类也有一个ID, 称为CLSID, 在OLE Automation中, 广泛使用了一种称为类型库的技术, 一个类型库包含了一个COM组件中所有的类型信息, 包括它实现的接口, 枚举类型, 接口的方法, 及接口参数等一些相关的信息, 同样类型库也是用一个表示自己的ID, LIBID. COM子统为了能在众多的COM技术中尽快的找的某个类型的COM组件, 又对COM组件进行了分类管理, 而每个类又有一个类别ID,CATID, 实际上我们可以利用这个CATID来列出系统中的所有的控件(CATID_Control). 上面说的所有这些ID, 实际上是一种类型, GUID. 它们只不过是GUID的不同的typedef.
GUID是一种利用系统时间和网卡具有的唯一编号的特性生成的一个具有128位的数字. 这个数字在时间和空间上保证了它的唯一性. 所以接口及相关的一些概念都利用GUID来进行区分, 而不是利用它们的名字.
2.ActiveX控件及实现
ActiveX控件的最早原型应该是随着VB出现的VBX控件, 由于VBX控件的16位结构并不能适应32位操作系统的要求,于是就诞生了OCX控件, OCX控件是一种32位的自包含的简单应用, 它实际上是一组完成指定的功能函数集合.它实际上是DLL的另外一种表现形式. OCX控件可以有自己的界面,也可以没有界面, 它拥有属性, 方法, 而且一个OCX控件可以触发出某种类型的事件, 用来通知容器它的状态的改变或者是某种外部状态的改变或事件的发生, 实现一个OCX控件必须实现一系列既定的接口, 这使得OCX控件显得有些庞大和冗余, 因为有些控件只需要实现这些接口的一部分, 而且对于Internet 来说, 实现这些多余的接口无疑增加了控件的体积.所以在1996年PDC大会上, 微软提出了它的 Activate Internet的概念, 并把它的一些技术改称为ActiveX技术, ActiveX控件就在原先的OCX控件上经过对要实现的接口的削减而诞生了, 现在只要一个COM组件实现IUnknown接口就可以被称为 ActiveX控件. 所以可以说一个ActiveX控件就是一个实现了IUnknown接口并且支持自注册的简单的 COM组件.
但是实现一个IUnknown接口的控件显然是没有实际用处的, 所以真正的ActiveX控件还是要实现原先OCX控件定义的一些接口, 用来和它的容器进行交互操作. 下面简要的说明一下一个真正的 ActiveX控件的实现.除了IUnkown接口外, 一个ActiveX控件一般要实现下面接口中的一部分. IOleObject,IOleInPlaceObject,IOleInPlaceActiveObject,IOleControl, IDataObject,IViewObject2, IDispatch, IConnectionPointContainer, ProviderClassInfo[2], ISpecifyPropertyPages, IPerPropertyBrowsing, IPersistStream, IPersistStreamInit,IPersistMemory, IPersistStorage, IPersistMoniker, IPersistPropertyBag,IOleCache[2],IExternalConnection,IRunnableObject, IClassFactory实现要求可以查看MSDN.
一个ActiveX控件通常具有一些属性和事件.控件的属性一般情况下是通过IDispatch接口实现的.在定义相应的控件属性时, 有一个被称为DISPID的值,这个值是用来被其他使用该控件的容器调用属性时使用的, 因为它们必须通过IDispatch接口的Invoke方法来调用相应的属性.IDispach的方法 Invoke是用来调用响应的属性的关键方法,但是这个方法在调用控件的属性时, 并不是用属性的名字, 而是被称为DISPID的ID值. 在一般情况下, 一个控件通常有它自己的类型库, 容器通过查询控件的类型库得到相应的属性和方法及事件的列表, 并取得它们的DISPID,然后就可以通过Invoke方法来操作它们.
一个ActiveX控件一般具有三种属性, 固有属性(stock property), 环境属性(ambient property), 自定义属性(custom property). 固有属性是大部分ActiveX控件具有的属性, 比如前景色, 字体等, 环境属性是控件处于容器中时, 有容器提供的一些属性, 如LocaleID, UserMode. 这些属性具有固定的DISPID值, 在控件中可以通过GetAmbientxxxx方法得到这些属性的值. 自定义属性是一个控件要实现自己的某些特定的功能特征时,定义的一些属性, 在容器中这些属性可以通过类型库来得到, 通过对IDispatch接口的调用来处理.
控件的事件是由控件触发的一个消息或通知, 如果一个控件支持事件, 它必须实现 IConnectionPointContainer和IConnectionPoint接口, 然后控件定义自己的出接口, 这个接口一般是通过用dispinterface声明, 在容器对控件进行事件响应时, 必须使用IDispatch接口的Invoke 方法进行处理, 根据Invoke调用传进来的DISPID我们就可以知道是控件触发了哪一个事件, 根据其他信息, 我们就可以对这个事件进行处理.
下面简单介绍一下如何利用MFC来进行ActiveX控件的开发. 首先我们使用AppWizard来生成 ActiveX控件的框架, 实际上这个框架已经是一个完整的控件, 在向导的帮助下这个控件已经实现了上面提到的ActiveX控件要实现的接口的一部分重要的接口, 象对事件的基本支持,属性的支持.我们可以在这个框架的帮助下添加我们自己要实现的功能, 为这个控件添加属性方法和事件.VC中的 ClassWizard在这方面提供大量的方便的操作, 在ClassWizard的AcitveX Automation页提供了对 ActiveX控件的属性事件方法的添加.
对于一个ActiveX控件来说你需要首先弄清楚哪些是要在控件中完成,哪些是要在容器中实现.那么,需要控件完成的你就要考虑用属性或者是方法来实现,而需要容器来完成的你只需将参数通过事件触发传递给容器,在容器端来实现.
另外,一个比较实际的问题是你的控件将是什么样子.比较简单的方法是在ClassWizard的时候指定控件将继承自那个类,从而拥有该类的外观.但这种方法不够灵活.如果你想定做控件的外观,那么最好的方法还是你自己手绘控件,或者是通过在控件内部添加一些控件形成组合控件.你可以在OnDraw (CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)中来绘制控件.该函数负责控件的绘制,其中pdc是当前系统用的环境设备,rcBounds是当前控件的rect范围,你可以用它来定位.绘制控件还是比较简单的,但前提是你必须要了解Windows的绘图机制.主要是会使用CBrush,CDC,CFont等 MFC的基本绘图类.
实际上对于ActiveX Control来说,在对它编程完全可以像是对一般的程序一样使用各种MFC的类,但是很多的类将不得不动态的创建,因此你必须掌握好定位.主要是掌握好对各种子类的重绘和刷新的时机和方法.
关于属性表的创建,属性表允许控件显示它的各种属性,以供察看和编辑.属性表通常以表的对话框的形式实现.你可以在这里改变一些控件的属性.对于大多数的控件来说这已经足够了.
下面我们来看看VC的AppWinzard Control都为我们做了些什么.用VC的AppWinzard Control你可以快速生成一个ActiveX Control在这里VC自动为我们声称了两个接口:一个用来负责属性和方法. 另一个用来负责事件.这个控件可以在容器运行,但是它什么也不做.并且它的外观也非常简陋.首先让我们来重新绘制它的外观,这在OnDraw中完成.
// 设置当前的字体,并保留原字体
CFont* pOldfont;
pOldfont = SelectFontObject(pdc,m_customfont);
// 得到当前的各种颜色.其中TranslateColor是为了把OLE_COLOR转换成COLORREF.
COLORREF textbkcolor = ::GetSysColor(COLOR_BTNFACE);
COLORREF textforecolor = this->TranslateColor(this->GetForeColor());
COLORREF edgebkcolor = ::GetSysColor(COLOR_3DFACE);
COLORREF edgeforecolor = ::GetSysColor(COLOR_3DFACE);
COLORREF oldbkcolor = pdc->SetBkColor(textbkcolor);
COLORREF oldforecolor = pdc->SetTextColor(textforecolor);
if(m_brush.m_hObject = NULL)
m_brush.CreateSolidBrush(textbkcolor);
CBrush* pOldbrush = pdc->SelectObject(&m_brush);
pdc->Rectangle(&rcBounds);
CSize osize = pdc->GetTextExtent(m_cstrCaption);
m_size = osize;
pdc->ExtTextOut((rcBounds.right-osize.cx)/2,
(rcBounds.bottom-osize.cy)/2,
ETO_CLIPPED|ETO_OPAQUE,
rcBounds,
m_cstrCaption,
m_cstrCaption.GetLength(),
NULL);
UINT borderstyle = EDGE_RAISED;
UINT borderflags = BF_RECT;
//画边框
pdc->SetBkColor(edgebkcolor);
pdc->SetTextColor(edgeforecolor);
pdc->DrawEdge((LPRECT)(LPCRECT)rcBounds,borderstyle,borderflags);
// 恢复设置
pdc->SetBkColor(oldbkcolor);
pdc->SetTextColor(oldforecolor);
pdc->SelectObject(pOldfont);
pdc->SelectObject(pOldbrush);
以上代码将为控件绘制一个比较好的外观,当然你可以任意改变直到你满意为止.以后你就可以根据需要来添加一些属性和方法并写出相对的实现.大部分的定义都是由CLASS WINZARD来维护的,所以你可以轻松的添加它们. 一些建议:在控件的属性页的编写过程中,需要将属性页上的标准控件与ActiveX 控件的属性相联系,这样当你在动态的改变标准控件的值时,ActiveX控件的属性会随之改变.但问题是如果你使用别的方法来动态改变属性页上的标准控件的值,则ActiveX控件的属性不会随之改变. 原因很简单,ActiveX控件的属性不知道自己已经发生改变,所以没有接受从标准控件传来的值.这一过程是在DoDataExchange()中的DD_P函数来完成的.由于你手动的改变了标准控件的值,所以你需要使用SetModified()来通知ActiveX控件的属性发生改变,这样DD_P函数就会有效了.另外,在ActiveX控件的属性中的数据类型有一些是OLE_XXX类型,这些类型实际上是一些LONG型的值,并且COLECONTROL 中有一些函数用来转换它们.在类型转换过程中尽量不要使用强制转换,这可能会带来一些意想不到的错误,鼓励使用缓冲区机制.
关于控件部分其实还有很多东西,可以参阅MFC或其他的文档来了解.
3.ActiveX控件容器及实现
ActiveX控件的容器实际上是ActiveX控件的客户端, 它使用ActiveX控件提供的各种功能.但是它也同时为控件提供了一些属性和其他的特征, 使得控件可以更好的和它进行交互和操作. ActiveX 控件的容器实际上是一个OLE容器,然后在实现了相应的接口来支持ActiveX控件后成为ActiveX控件的容器.
除了IUnknown外,容器程序需要用到下列接口的一部分: IOleInplaceFrame, IOleInPlaceUIWindow, IOleClientSite,IOleInPlaceSite, IAdviseSink, IOleControlSite, IOleControlSite, IDispatch, IProperytNotifySink, IStorage, IOleContainer接口的具体定义请参照MSDN. 在MFC附带的例子中有一个很好的例子, 就是VC中附带的工具ActiveX Control Test Container.
下面就以这个例子来解释一个ActiveX控件容器的实现及对某些问题的处理. 在这个例子中,使用了VC的向导来生成一个具有Container支持的应用程序, 在生成的类中有一个用来包装每一个嵌入到问档中的OLE对象的类, 一般被称为xxxCntrItem, 在这个例子中被改名CTestContainer98Item. 创建每一个ActiveX控件时都是通过这个类来直接生成,这个类维护了ActiveX控件的一些属性特征.而且这个类支持序列化,
这样我们就可以通过序列化来保存控件的属性状态等信息. 1).动态创建控件. 这应该是一个ActiveX控件容器最重要的任务. 为了能管理容器中的控件, 首先它必须能动态的创建控件. 因为每一个COM组件都具有一个唯一的ID, CLSID, ActiveX控件也不例外, 但是针对系统中成百上千的COM对象, 我们如何确定哪一个是ActiveX控件呢? 在COM基础中我们提到了为了能更快的定位COM组件并加载它,COM子系统对COM组件实行了分类别管理即利用CATID来分类各种不同的COM组件, ActiveX 控件的CATID是CATID_Control,所以我们可以通过这个信息来找到所有在系统中注册的控件, 一般情况下我们是通过生成一个列表来表示所有这些控件. 下面是经过改写的
CInsertControlDlg::RefreshControlList()函数
CArray m_aImplementedCategories;
CListBox m_lbControls;
ICatInformationPtr m_pCatInfo;
CList m_lControls;
void CInsertControlDlg::RefreshControlList()
{
BOOL bDone;
HRESULT hResult;
IEnumGUIDPtr pEnum;
ULONG nImplementCategories;
CATID* pcatidImpl;
CLSID clsid;
LPOLESTR pszName;
CString strName;
ULONG iCategory;
int iItem;
POSITION posControl;
CString strServerPath;
CString strString;
// 首先,清空列表框,并用m_aImplementCategories的数据填充pcatidImpl, 作为m_pCatInfo函数
// EnumClassedOfCategories的第二个参数,来获取CLSID的枚举器
m_lbControls.ResetContent();
nImplementCategories = m_aImplementCategories.GetSize();
if (nImplementCategories == 0)
{
nImplementCategories = (ULONG)-1;
pcatidImpl = NULL;
}
else
{
// 为pcatidImpl分配内存,将m_aImplementCategories数据传给pcatidImpl
pcatidImpl = (CATID*)_alloca(nImplementCategories * sizeof(CATID));
for ( iCategory = 0; iCategory < nimplementcategories; iCategory++ )
pcatidImpl[iCategory]="m_aImplementCategories[iCategory]; // 获取CLSID的枚举器
hResult = m_pCatInfo->EnumClassesOfCategories(nImplementCategories, pcatidImpl, 0, NULL, &pEnum);
if (FAILED(hResult)) return;
//然后通过枚举器枚举所有ActiveX Control的CLSID, 并取得相应的用户类型名称,加入到列表框中.
bDone = FALSE;
while (!bDone)
{
hResult = pEnum->Next(1, &clsid, NULL); // 获得下一个ActiveX Control的CLSID
if (hResult == S_OK)
{
pszName = NULL;
hResult = OleRegGetUserType(clsid, USERCLASSTYPE_FULL, &pszName);//得到相应的用户类型名称
if (SUCCEEDED(hResult))
{
strName = pszName;
CoTaskMemFree(pszName);
pszName = NULL;
iItem = m_lbControls.AddString(strName);
posControl = m_lControls.AddTail(clsid);
m_lbControls.SetItemDataPtr(iItem, posControl);
}
}
else
{
bDone = TRUE;
}
}
OnControlsSelChange();
}
上面这个函数演示了如何从众多的COM组件中提取ActiveX控件并把它们添加到一个列表框中,同时保留了它们的CLSID. 其中m_pCatInfo在InitDialog中调用CreateInstance创建自己的实例.
在我们的到了某个ActiveX控件的CLSID以后, 我们就可以利用CoCreateInstanse函数来生成该控件的实例. 如上面所说的, 每一个ActiveX控件都有一个包装类, 我们在创建控件的时候, 实际上都是通过这个包装类来进行,下面我们看一下, 在这个包装类中创建控件的代码(删除了一些不是很重要的代码).
BOOL CActiveXContainerCntrItem::CreateControl(REFCLSID clsid)
{
IUnknown* pUnknown;
// 1. 创建控件自己的实例, 在下面的步骤将对控件的一些状态进行初始化
HRESULT hResult = CoCreateInstance(clsid, NULL, CLSCTX_INPROC|CLSCTX_SERVER,
IID_IUnknown, (void**)&pUnknown);
if (FAILED(hResult))
return FALSE;
// 2. 在控件中请求IOleObject接口,
hResult = pUnknown->QueryInterface(IID_IOleObject, (void**)&m_lpObject);
if (FAILED(hResult))
{
pUnknown->Release();
return FALSE;
}
pUnknown->Release();
CString strUserType;
GetUserType(USERCLASSTYPE_SHORT, strUserType);
// 3. 创建一个唯一的名称, 用来维护每个控件实例的唯一性
GetDocument()->CreateUniqueItemName(this, strUserType, m_strDisplayName);
// 4. 初始化控件的某些基本信息.
InitControlInfo();
BOOL bQuickActivate = FALSE;
// 5. 如果控件支持IQuickActivate接口, 利用IQuickActive接口激活控件
bQuickActivate = QuickActivate();
if (!bQuickActivate)
{
// 6. 如果控件不支持IQuickActiveX接口, 通过IOleObject接口设置控件的ClientSite.
m_lpObject->GetMiscStatus(DVASPECT_CONTENT, &m_dwMiscStatus);
if (m_dwMiscStatus & OLEMISC_SETCLIENTSITEFIRST)
hResult = m_lpObject->SetClientSite(GetClientSite());
if (FAILED(hResult))
TRACE0("Can't SetClientSite for the Control"n");
}
if (SUCCEEDED(hResult))
{
// 7. 支持IQuickActivate接口的控件必须使用下面的步骤.
IPersistStreamInitPtr pPersistStreamInit;
IPersistStoragePtr pPersistStorage;
pPersistStreamInit = m_lpObject;
if (pPersistStreamInit != NULL)
{
hResult = pPersistStreamInit->InitNew();
if (hResult == E_NOTIMPL)
hResult = S_OK;
}
else
{
pPersistStorage = m_lpObject;
if (pPersistStorage != NULL)
{
hResult = pPersistStorage->InitNew(m_lpStorage);
}
else
{
hResult = S_OK;
}
}
}
return FinishCreate(hResult); // 8. 在此处设置对控件的事件处理和属性处理信息
}
下面针对上面注释中提到的一些内容进行说明,
注释2请求IOleObject接口是用来对后面的一些设置做准备, 因为这个接口要在很多的地方使用, 所以被保存在一个成员变量中.
注释3是用来区别一个控件的多个实例, 这个方法被文档类实现, 它根据控件的名称和一个数字来维护同一种控件的实例.
注释4是用来初始化控件的一些基本信息,这些信息是通过读取控件类型库将控件的属性和事件放在各自的列表中, 以后好用来对控件的属性变化和事件进行响应.
注释5是针对QuickActivate()方法的, QuickActivate()方法首先向控件请求IQuickActivate 接口, 如果控件不支持该接口, 返回FALSE, 如果控件支持该接口, 则初始化两个结构QACONTAINER 和QACONTROL, 然后用这两个结构调用IQuickActivate接口的QuickActivate方法, IQuickActivate接口是为了提高ActiveX控件的加载速度而设计的.在调用了IQuickActivate接口的 QuickActivate()方法之后, IPersist*::Init和IPersist*::InitNew方法必须被调用, 控件应该在QuickActivate方法中建立它的连接点与容器的接收器之间的连接, 如果没有调用 IPersist*::Init和IPersist*::InitNew, 那么这些连接就不会生效.
到这里控件已经被建立了, 但是这里并没有涉及到与容器有关的内容. 下面讲述具体的容器的实现. 我们在例子中可以看到, 程序的文档类继承自COleDocument, 在COleDocument中文档为我们实现了作为容器所必须实现的一个接口, IOleContainer, 我们在程序中可以通过GetStartPosition (), GetNextItem()等方法来使用这个接口, 这个接口的主要作用是用来遍历容器中的控件或其他的 OLE对象.另外还有一些必须实现的接口实际上已经在MFC中实现, 我们在一般情况下只要简单的使用这些经过封装的函数就可以了, 这里主要讲述一些与控件的属性和事件处理相关的一些问题, 在 ActiveX控件及实现中我们提到, 控件的属性和事件一般是通过IDispatch来实现, 在Test Container中我们可以看到下面的一段用来实现接口映射的代码.
BEGIN_INTERFACE_MAP( CTestContainer98Item, COleClientItem )
INTERFACE_PART( CTestContainer98Item, IID_IServiceProvider, ServiceProvider )
INTERFACE_PART( CTestContainer98Item, IID_IPropertyNotifySink, PropertyNotifySink )
INTERFACE_PART( CTestContainer98Item, IID_IDispatch, AmbientProperties )
INTERFACE_PART( CTestContainer98Item, IID_IOleControlSite, OleControlSite )
// INTERFACE_PART( CTestContainer98Item, IID_IOleInPlaceSiteEx, OleInPlaceSiteWindowless )
// INTERFACE_PART( CTestContainer98Item, IID_IOleInPlaceSiteWindowless, OleInPlaceSiteWindowless )
END_INTERFACE_MAP()
我们可以看到, 在上面的接口映射中一共出现了6个接口, 但是有两个入口是被注释的. 下面我们逐一解释这些接口:
第一个IServiceProvider在这里主要是用来提供IBindHost接口的. 实际上在实现一个容器的时候, 这个接口并不是必须的.
第二个IPropertyNotifySink是用来实现控件的属性变化通知的接收器. 如果希望你的容器能在其中的控件的属性改变时得到相应的通知, 就要实现这个接口,在这个接口的OnChange方法中你可以得到相应的被改编的属性的DISPID, 有了这个DISPID,你就可以更进一步的控制控件的某些属性特征了.
第三个接口是用来为控件提供环境属性的. 为控件提供环境属性这个功能是由IDispatch接口实现的, 每一个环境属性都具有特定的DISPID, 所以当控件调用GetAmbientxxx方法时, 控件就会要求容器提供相应的属性的实现,这些属性都是被IDispatch接口实现的.
第四个接口是IOleControlSite. 这个接口的主要作用是提供一些在容器内部的Site对象对内嵌在其中的控件的管理. 实现这个接口是可选的.
在上面的接口映射中, 我们并没有看到对控件的事件的处理的接口映射, 在Test Container的代码中我们可以看到下面这段代码.
BEGIN_INTERFACE_PART( EventHandler, IDispatch )
STDMETHOD( GetIDsOfNames )( REFIID iid, LPOLESTR* ppszNames, UINT nNames, LCID lcid, DISPID* pDispIDs );
STDMETHOD( GetTypeInfo )( UINT iTypeInfo, LCID lcid, ITypeInfo** ppTypeInfo );
STDMETHOD( GetTypeInfoCount )( UINT* pnInfoCount );
STDMETHOD( Invoke )( DISPID dispidMember, REFIID iid, LCID lcid, WORD wFlags, DISPPARAMS* pdpParams,
VARIANT* pvarResult, EXCEPINFO* pExceptionInfo, UINT* piArgError );
END_INTERFACE_PART( EventHandler )
很显然这段代码是用来处理事件的, 但是为什么在接口的映射部分没有它呢? 如果你查看 CTestContainer98Item类的代码时你会发现一个叫做GetInterfaceHook()的方法, 这个方法有一个类型为const void*的参数pv, 这个参数实际上是一个IID类型的指针,看看下面的代码:
piid = (const IID*)pv;
if( *piid == m_infoEvents.GetIID() )
{
return( &m_xEventHandler );
}
现在我们知道了控件的事件是怎么处理的, GetInterfaceHook()方法是CCmdTarget的一个方法, 但是在MSDN中却并没有文档说明.在这个方法中同样也实现了其他几个接口的映射关系.
到这里我们已经可以了解到要实现一个ActiveX控件的容器所需要实现的接口及相关的一些问题了, MFC的类库为我们做了许多的工作, 它们实现了一些作为控件容器所必须实现的接口, 使我们在开发这类应用程序的时候有了很好的起点.
4.总结
上面所谈到的只是一些基本的概念及简单的实现, 开发一个ActiveX控件或者是它的容器都需要很多的知识和技术, 因为COM本身就是一项十分庞大的技术规范, 它涉及了很多方面的知识, 而这些又往往是其它基于COM的技术的基础.