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 的技术的基础 .

你可能感兴趣的:(ActiveX)