【转载】COM 组件设计与应用(十四)——事件和通知(vc.net)
原文:http://vckbase.com/index.php/wv/1244.html
一、前言
我的 COM 组件运行时产生一个窗口,当用户双击该窗口的时候,我需要通知调用者;
我的 COM 组件用线程方式下载网络上的一个文件,当我完成任务后,需要通知调用者;
我的 COM 组件完成一个钟表的功能,当预定时间到达的时候,我需要通知调用者;
... ... ... ...
本回书开始话说 COM 的事件、通知、连接点......这些内容比较多,我分两次(共四回)来介绍。
二、通知的方法
当程序甲方内部发生了某个事件的时候,需要通知乙方,无非使用几个方法:
| 通知方式 | 简单说明 | 评论 | |
| 直接消息 | PostMessage() PostThreadMessage() |
向窗口或线程发个消息 | 你什么时候执行我就不管啦 |
| SendMessage() | 马上执行消息响应函数 | 不执行完消息处理函数不会返回 | |
| SendMessage(WM_COPYDATA...) | 发消息的同时,还可以带过去一些自定义的数据 | 比较常用,所以单独列了出来 | |
| 间接消息 | InvalidateRect() SetTimer() ...... |
被调用的函数会发送相关的一些消息 | 这样的函数太多了 |
| 回调函数 | GetOpenFileName()...... | 当用户改变文件选择的时候,执行回调函数 | 嗨!哥们,这是我的电话,有事就言语一声。 |
COM 的时代,以上这些方法就基本上不能玩转了,因为...您想呀 COM 组件是运行在分布式环境中的,地球另一边计算机上运行的组件,怎么可能给你的窗口发消息那?当然不能!(但话又说回来,对于 ActiveX 这样只能在本地运行的组件,当然也可以发送窗口消息的啦。)
回调函数的方式,是设计 COM 通知方法的基础。回调函数,本质上是预先把某一函数的指针告诉我,当我有必要的时候,就直接呼叫该函数了,而这个回调函数做了什么,怎么做的,我是根本不关心的。好了,问你个问题:啥是 COM 的接口?接口其实就是一组相关函数的集合(这个定义不严谨,但你可以这么理解哈)。因此,在COM中不使用“回调函数”而是使用“回调接口”(说的再清楚一些,就是使用一大堆包装好的“回调函数”集) ,回调接口,我们也叫“接收器接口”。

图一、客户端传递接收器接口指针给COM。当发生事件时,COM调用接收器接口函数完成通知
本回示例程序完成的功能是:
客户端启动组件(Simple11.IEvent1.1)并得到接口指针 IEvent1 *;
调用接口方法 IEvent1::Advise() 把客户端内部的一个接收器(sink)接口指针(ICallBack *)传递到组件服务器中;
调用 IEvent1::Add() 去计算两个整数的和;
但是计算结果并不通过该函数返回,而是通过 ICallBack::Fire_Result() 返回给客户端;
当客户端不再需要接受事件的时候,调用 IEvent1::Unadvise() 断开和组件的联系。
三、组件实现步骤
1、建立一个解决方案
2、在解决方案中,建立一个 ATL 项目。示例程序中项目名称叫 Simple12,取消“属性化”,其它接受默认选项。
3、选择项目,执行鼠标右键菜单命令“添加\添加类”。
3-1、左侧分类选择 ATL,右侧模板选择 Atl 简单对象
3-2、名称卡片中,输入组件名称。示例程序中是 Event1(注1)
3-3、选项卡片中,修改接口类型“自定义”(注2)
4、选择 IEnvent1 接口,鼠标右键菜单“添加\添加方法”

图二、增加接口函数 Add([in] long n1,[in] long n2)

图三、增加接口函数 Advise([in] ICallBack *pCallBack,[out] long *pdwCookie)

图四、增加接口函数 Unadvise([in] long dwCookie)
你应该注意到了,在Add()函数中,并没有[out]、[retval] 这样的 IDL 属性,嘿嘿,因为我们本来就不打算通过 Add() 函数直接得到计算结果。不然怎么演示回调接口呀:-) 另外,在函数 Advise()中,需要返回一个整数 dwCookie,这是干什么?道理很简单,因为我们的组件想同时支持多个对象的回调连接。因此当客户端传递一个接口给我们组件的时候,我返回给它唯一的一个 cookie 号码来表示身份,将来断开连接的时候 Unadvise(),它需要把这个 cookie 身份号再给我,这样我就知道是谁想断开了。
5、增加回调接口 ICallBack 的 IDL 定义。打开 IDL 文件并手工输入(黑体字部分为手工输入的) ,然后保存:
import "oaidl.idl";
import "ocidl.idl"; [
object,
uuid(DB72DF86-70E9-4ABC-B2F8-5E04062D3B2E), // 这个 IID 可以用 GUDIGEN.EXE 产生
helpstring("ICallBack 接口"),
pointer_default(unique)
]
interface ICallBack : IUnknown
{ }; [
object, // 以下内容同示例程序,当然如果是你自己生成的程序就肯定有差别的啦
uuid(DB72DF85-70E9-4ABC-B2F8-5E04062D3B2E), helpstring("IEvent1 Interface"),
pointer_default(unique)
]
interface IEvent1 : IUnknown
{
[helpstring("method Add")] HRESULT Add([in] long n1, [in] long n2);
[helpstring("method Advise")] HRESULT Advise([in] ICallBack * pCallBack, [out] long * pdwCookie);
[helpstring("method Unadvise")] HRESULT Unadvise([in] long dwCookie);
}; [
uuid(FBA1E0F0-49CD-4B77-B9B1-4DC066AF8A8E),
version(1.0),
helpstring("Simple12 1.0 类型库")
]
library SIMPLE11Lib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb"); [
uuid(53E00126-B1A0-4510-B9BC-75ED87CE2DB7),
helpstring("Event1 Class")
]
coclass Event1
{
[default] interface IEvent1;
// 需要手工输入,据说 VB 使用的话,不能有 [source,default] 属性 [source, default] interface ICallBack; };
};
6、增加回调接口函数

图五、增加回调接口函数
其实和以前的方法一样,只要注意别选错了接口就好。

图六、增加接口函数 Fire_Result([in] long nResult)
我们计算整数和,得到结果后,就是要靠这个回调接口函数去反馈给客户端呀。
7、添加组件内部保存回调接口指针的数组
刚才已经说过,我们这个组件打算支持多个对象的回调连接,因此我们要使用一个数组来保存。由于 vc.net 无法用向导来添加数组形式的成员变量,我们还是打开 CEvent1 类的头文件,手工输入吧:
......
private:
ICallBack * m_pCallBack[10];
......
保存一个数组可以有多种方式。示例程序比较简单,定义了一个10个元素空间的成员数组变量。如果你已经学会了使用 STL,那么你也可以用 vector 等容器来实现。注意!注意!注意!在构造函数中别忘了初始化数组元素为 NULL。
8、好了,下面开始完成所有代码
STDMETHODIMP CEvent1::Add(long n1, long n2)
{
long nResult = n1 + n2;
for( int i=0; i<10; i++)
{
if( m_pCallBack[i] ) // 如果回调接口有效
m_pCallBack[i]->Fire_Result( nResult ); // 则发出事件/通知
} return S_OK;
} STDMETHODIMP CEvent1::Advise(ICallBack *pCallBack, long *pdwCookie)
{
if( NULL == pCallBack ) // 居然给我一个空指针?!
return E_INVALIDARG; for( int i=0; i<10; i++) // 寻找一个保存该接口指针的位置
{
if( NULL == m_pCallBack[i] ) // 找到了
{
m_pCallBack[i] = pCallBack; // 保存到数组中
m_pCallBack[i]->AddRef(); // 指针计数器 +1 *pdwCookie = i + 1; // cookie 就是数组下标
// +1 的目的是避免使用0,因为0表示无效 return S_OK;
}
}
return E_OUTOFMEMORY; // 超过10个连接,内存不够用啦
} STDMETHODIMP CEvent1::Unadvise(long dwCookie)
{
if( dwCookie<1 || dwCookie>10 ) // 这是谁干的呀?乱给参数
return E_INVALIDARG; if( NULL == m_pCallBack[ dwCookie - 1 ] ) // 参数错误,或该接口指针已经无效了
return E_INVALIDARG; m_pCallBack[ dwCookie -1 ]->Release(); // 指针计数器 -1
m_pCallBack[ dwCookie -1 ] = NULL; // 空出该下标的数组元素 return S_OK;
}
四、客户端实现步骤
大家下载示例程序后,去浏览客户端的实现程序吧。这里我只说明一下关于接收器是如何构造的:

图七、从 ICallBack 派生接收器类 CSink
这里 ICallBack 是 COM 接口,因此 CSink 是不能事例化的,如果你去编译,会得到一坨一坨(注3)的错误,报告说你没有实现 virtual 函数。然后,我们可以按照错误报告,去实现所有的虚函数:
// STDMETHODIMP 是宏,等价于 long __stdcall
STDMETHODIMP CSink::QueryInterface(const struct _GUID &iid,void ** ppv)
{
*ppv=this; // 不管想得到什么接口,其实都是对象本身
return S_OK;
} ULONG __stdcall CSink::AddRef(void)
{ return 1; }// 做个假的就可以,因为反正这个对象在程序结束前是不会退出的 ULONG __stdcall CSink::Release(void)
{ return 0; }// 做个假的就可以,因为反正这个对象在程序结束前是不会退出的 STDMETHODIMP CSink::raw_Fire_Result(long nResult)
{
... ... // 把计算结果显示在窗口中
return S_OK;
}
五、小结
COM 组件实现事件、通知这样的功能有两个基本方法。今天介绍的回调接口方式非常好,速度快、结构清晰、实现也不复杂;下回书介绍连接点方式(Support Connection Points),连接点方法其实并不太好,速度慢(如果是远程DCOM方式,要谨慎选择它)、结构复杂、唯一的好处就是 ATL 对它进行了包装,所以实现起来反而比较简单。不介绍又不行,因为微软绝大数支持事件的组件都是用连接点实现的,咳......讨厌的微软(注4)。
注1:本来设想多举几个例子,因此第一个叫 Event1,可写完后,感觉程序已经比较复杂了,就没继续再做了。
注2:当然,你选择使用双接口 Dual 也没有问题。但要注意到在下面的步骤,增加回调接口修改 IDL 文件的时候,我们是要使用 Custom(从IUnknown派生,而不是从IDispatch派生)的。
注3:一坨一坨经常用来形容一堆一堆的狗屎。
注4:微软的同志们,玩笑话不要当真呀!我还靠着你来吃饭那。
【转载】COM 组件设计与应用(十四)——事件和通知(vc.net)的更多相关文章
- AngularJs的UI组件ui-Bootstrap分享(十四)——Carousel
Carousel指令是用于图片轮播的控件,引入ngTouch模块后可以在移动端使用滑动的方式使用轮播控件. <!DOCTYPE html> <html ng-app="ui ...
- 【转载】COM 组件设计与应用(四)——简单调用组件
原文:http://vckbase.com/index.php/wv/1211.html 一.前言 同志们.朋友们.各位领导,大家好. VCKBASE 不得了, 网友众多文章好. 组件设计怎么学? 知 ...
- HT图形组件设计之道(四)
在<HT图形组件设计之道(二)>我们展示了HT在2D图形矢量的数据绑定功能,这种机制不仅可用于2D图形,HT的通用组件甚至3D引擎都具备这种数据绑定机制,此篇我们将构建一个3D飞机模型,展 ...
- Kafka设计解析(十四)Kafka producer介绍
转载自 huxihx,原文链接 Kafka producer介绍 Kafka 0.9版本正式使用Java版本的producer替换了原Scala版本的producer.本文着重讨论新版本produce ...
- xmlplus 组件设计系列之十 - 网格(DataGrid)
这一章我们要实现是一个网格组件,该组件除了最基本的数据展示功能外,还提供排序以及数据过滤功能. 数据源 为了测试我们即将编写好网格组件,我们采用如下格式的数据源.此数据源包含两部分的内容,分别是表头数 ...
- Windows Phone 十四、磁贴通知
磁贴(Tile) Windows Phone 磁贴种类: 小尺寸 SmallLogo:71x71: Square71x71 中等 Logo:150x150: Square150x150 宽 WideL ...
- spring学习 十四 注解AOP 通知传递参数
我们在对切点进行增强时,不建议对切点进行任何修改,因此不加以使用@PointCut注解打在切点上,尽量只在Advice上打注解(Before,After等),如果要在通知中接受切点的参数,可以使用Jo ...
- AngularJs的UI组件ui-Bootstrap分享(十二)——Rating
Rating是一个用于打分或排名的控件.看一个最简单的例子: <!DOCTYPE html> <html ng-app="ui.bootstrap.demo" x ...
- AngularJs的UI组件ui-Bootstrap分享(十)——Model
Model是用来创建模态窗口的,但是实际上,并没有Model指令,而只有$uibModal服务,创建模态窗口是使用$uibModal.open()方法. 创建模态窗口时,要有一个模态窗口的模板和对应的 ...
随机推荐
- leetcode 刷题
176:第二高的薪水 offset ) as secondhighestsalary; ---去掉第一个,再从第一个开始 177:第N高的薪水 ------相关子查询:子查询中引用了外层查询所引用表的 ...
- Html.Partial()传值的问题
@Html.Partial("Test", Model, new ViewDataDictionary { { "a", "b" } }); ...
- [翻译] FBNetworkReachability
FBNetworkReachability You can use FBNetworkReachabilty class to get network reachability on iOS devi ...
- Entity Framework的基本操作
一.使用基本的方法进行增删改查 二.使用状态进行增删改查,即使用基类对象进行操作 三.多个表同时进行添加 添加数据后获取自动增长 ...
- 自制年月选择插件 jquery.MyDatePicker v1.0beta
参数: el: null, //默认值和位置从哪个文本框取,传入jquery对象 ,默认为this 用于按钮引发文本框的focus事件来弹出 viewtype: 'month', //日期控件模式 默 ...
- ali验证码推送接口调用
/** * 发送接口 * @param $info 发送信息的某些参数 * @return bool */ public function send($info) { $config = target ...
- BZOJ2281:[SDOI2011]黑白棋(博弈论,组合数学,DP)
Description 小A和小B又想到了一个新的游戏. 这个游戏是在一个1*n的棋盘上进行的,棋盘上有k个棋子,一半是黑色,一半是白色. 最左边是白色棋子,最右边是黑色棋子,相邻的棋子颜色不同. 小 ...
- Codeforces 1118 F2. Tree Cutting (Hard Version) 优先队列+树形dp
题目要求将树分为k个部分,并且每种颜色恰好在同一个部分内,问有多少种方案. 第一步显然我们需要知道哪些点一定是要在一个部分内的,也就是说要求每一个最小的将所有颜色i的点连通的子树. 这一步我们可以将所 ...
- mysql测试数据创建
用存储过程方式创建几十几百万条测试数据,2核4G里插入1万条,约8.5秒,也就是24小时可以加大约1亿条记录. //创建库,用户create database dbTest;create user ' ...
- 关于HTML Button点击自动刷新页面的问题解决
原因 button,input type=button按钮在IE和w3c,firefox浏览器区别: 1.当在IE浏览器下面时,button标签按钮,input标签type属性为button的按钮是一 ...