- ·上一篇内容:使用GDI图形设备接口进行绘图
- ·下一篇内容:Visual C++之WinSock编程介绍
详解Visual C++事件编程
在 .NET 框架中,事件是很成熟的机制。任何对象都可以定义事件,并且多个对象可以侦听这些事件。.NET 中的事件使用委托来实现,委托是 .NET 中的术语,它实际上就是以前说所的回调。最重要的是,委托是类型安全的。不再使用 void* 或者 WPARAM/LPARAM。
为了用托管扩展定义一个事件,你得用 __event 关键字。例如,Windows::Forms 中的 Button 类有一个 Click 事件:
// in Button class
public:
__event EventHandler* Click;
这里 EventHandler 是某个函数的委托,该函数带有参数:Object (也就是 sender) 和 EventArgs:
public __delegate void EventHandler(
Object* sender,
EventArgs* e
);
为了接收事件,你必须用正确的签名实现处理器成员函数并创建一个委托来包装该函数,然后调用事件的 += 操作符注册你的处理器/委托。对于上面的 Click 事件,代码应该像这样:
// event handler
void CMyForm::OnAbort(Object* sender, EventArgs *e)
{
...
}
// register my handler
m_abortButton->Click += new EventHandler(this, OnAbort);
注意该处理器函数必须具备由委托定义的签名。这是托管扩展的基本原则。但是你的问题涉及的不是托管事件,你问的是本机事件——如何实现本机 C++ 事件?C++ 本身没有内建的事件机制,那么该怎么实现呢?你可以用 typedef 来定义一个回调并让客户机来提供这个回调,这种做法有些类似 qsort——但那样太老土了。更不用说处理多个事件时的繁琐。相对于静态外部函数来说,用成员函数作为事件处理器是最丑陋的做法。一种比较好的方法是创建一个定义事件的接口。那是 COM 的做法。但你不需要用 C++ 编写沉重的 COM 代码;你可以用一个简单的类。我写了一个类来做示范:CPrimeCalculator;这个类的功能是查找素数。代码如 Figure 3 所示。CPrimeCalculator::FindPrimes(n) 查找开始的 n 个素数。其工作原理是这样的,CPrimeCalculator 触发两种事件:Progress 事件和 Done 事件。这些事件都定义在 IPrimeEvents 接口中。IPrimeEvents 接口不是 .NET 和 COM 意义上的接口;它是一个纯粹的 C++ 抽象基类,它为每个事件处理器定义签名(参数和返回类型)。处理 CPrimeCalculator 的客户机必须实现 IPrimeEvents,然后调用 CPrimeCalculator::Register 来注册它们的恶接口。CPrimeCalculator 将对象/接口添加到其内部列表(list)中。由于它会对每个整数进行素数检查,CPrimeCalculator 则周期性地报告到目前为止找到了多少个素数:
// in CPrimeCalculator::FindPrimes
for (UINT p=2; p<max; p++) {
// figure out if p is prime
if (/* every now and then */)
NotifyProgress(GetNumberOfPrimes());
...
}
NotifyDone();
CPrimeCalculator 调用内部辅助函数 NotifyProgress 和 NotifyDone 来触发事件。这些函数遍历客户机对象列表,为每个客户机调用相应的事件处理器。代码如下:
void CPrimeCalculator::NotifyProgress(UINT nFound)
{
list<IPrimeEvents*>::iterator it;
for (it=m_clients.begin(); it!=m_clients.end(); it++) {
(*it)->OnProgress(nFound);
}
}
如果你对 STL 不熟悉,去看看有关迭代器反引用操作符的内容,它返回当前指向的对象,上面代码段中,for 循环里的代码等同于:
IPrimeEvents* obj = *it;
obj->OnProgress(nFound);
触发 Done 事件的 NotifyDone 函数做法类似,它没有参数,如 Figure 3 所示。你也许觉得 Done 事件是多余的,因为当 FindPrimes 返回控制时,客户机已经知道 CPrimeCalculator 完成了工作。没错——但有一种情况除外,那就是多个客户机注册接收的事件,并且调用 CPrimeCalculator::FindPrimes 的对象可能不是同一个。Figure 4 是我的测试程序 PrimeCalc。该程序为素数事件实现了两个不同的事件处理器。第一个处理器是主对话框本身,CMyDlg,它利用多继承实现 IPrimeEvents。该对话框处理 OnProgress 和 OnDone,并在对话窗口显示进度,完成后发出蜂鸣声。其它的事件处理器,如 CTracePrimeEvents 也实现了 IPrimeEvents,这个实现显示诊断(TRACE)流中的信息。如 Figure 6 所示。
Figure 5 运行中的 PrimeCalc
从使用 CPrimeCalculator 来编写应用的程序员角度看,处理事件简单而直白。从 IPrimeEvents 派生,实现处理器函数,然后调用 Register。从编写触发事件的类的程序员看来,这个过程有些冗长乏味。首先你得定迨录涌凇U獠⒚挥惺裁床缓谩5幼拍愕帽嘈?Register 和 Unregister 函数,每个 Foo 事件都得有一个相应的 NotifyFoo 函数。如果有 15 个事件的话,那就十分令人不爽了,尤其是每个 NotifyFoo 函数的模式都相同:
void CMyClass::NotifyFoo(/* args */)
{
list<IPrimeEvents*>::iterator it;
for (it=m_clients.begin(); it!=m_clients.end(); it++) {
(*it)->OnFoo(/* args */);
}
}
Figure 6 PrimeCalc 在 TraceWin 中的输出
NotifyFoo 迭代客户机列表,为每个注册的客户机调用相应的 OnFoo 处理器,并传递任何需要的参数。