理解 COM 事件处理






4.91/5 (94投票s)
通过一个C++模板类,了解COM事件处理的基本原理,该类允许对dispinterface COM事件进行通用处理。
文章标题更改
请注意,我已将本文的标题从“TEventHandler - A C++ COM Event Handler For IDispatch-Based Events”更改为当前标题“Understanding COM Event Handling”。后者我认为是一个更好的标题,更能准确地反映本文的预期主题,即仔细阐述COM事件处理背后的内部机制。
引言
如果您曾经进行过涉及使用COM对象的开发工作,那么您很可能遇到过COM对象事件处理的需求。Visual Basic用户知道连接COM(或ActiveX)对象的事件接口是多么简单。VB IDE为用户提供了方便的事件处理函数代码。用户只需填写事件处理函数的详细信息。
对于Visual C++用户来说,这并不总是那么直接。如果您的COM对象恰好是一个ActiveX控件,并且您正在使用MFC,那么,Visual C++ IDE提供了向导,可以帮助您生成事件处理函数存根。所有必要的代码(例如,插入事件接收映射和事件条目宏)都会为您自动完成。
但是,如果您的COM对象不是ActiveX控件呢?如果您正在使用也触发事件的普通COM对象,并且需要处理这些事件呢?
如果您是MFC用户,您可能会想尝试各种MFC宏,看看是否可以通过手动或通过向导将事件处理函数集成到您的代码中。我个人认为这是可能的。但您需要对MFC及其生成的宏有深入的了解。
如果您不使用MFC,您可能想尝试ATL代码(例如IDispEventImpl
、BEGIN_SINK_MAP
、SINK_ENTRY_EX
等)来执行事件处理。ATL宏代码肯定不简单,但它们在MSDN中有很好的文档记录,并且它们确实提供了标准的处理机制。
在本文中,我将回归基础,并试图解释COM中事件处理的基本原理。我还将提供一个C++类,作为COM对象事件处理的一个基本且简单的(至少在代码开销方面)辅助工具。
我通过一个名为TEventHandler
的自定义开发的特殊模板类来实现这一点,该类已在我许多项目中得到使用。该类利用COM基本原理和原始概念,避免使用复杂的宏。接下来的章节将详细阐述该类。我假设读者对C++、ATL和模板类的概念有充分的了解。但是,在我们开始讨论TEventHandler
类之前,让我们先探讨COM中事件处理的基本原理。
COM中的事件处理
传入接口
当我们开发常规的COM对象时,我们为自己编写的或已提供的接口(在IDL文件中定义)提供实现。这些实现构成了所谓的“传入”接口。通过“传入”,我们意味着对象“监听”其客户端。也就是说,客户端调用接口的方法,并以这种方式与对象“对话”。
参考下图,我们可以说ISomeInterface
是由右侧COM对象提供的“传入”接口。

传出接口
正如**Kraig Brockschmidt**在他的书《Inside OLE》中所言,许多COM对象本身也有有用的信息要传达给它们的客户端。客户端也可能想监听COM对象。如果需要这种双向对话,就需要一个所谓的“传出”接口。“传出”一词是相对于COM对象而言的。从COM对象的角度来看,它是传出的。设想一下“说话者”和“倾听者”的角色颠倒了,如下图所示。

参考上图,我们可以说ITalkBackInterface
是由右侧COM对象**支持**的“传出”接口。COM对象调用ITalkBackInterface
的方法,而**客户端**实现ITalkBackInterface
的方法。支持一个或多个传出接口的COM对象称为**可连接对象**或**源**。可连接对象可以支持任意数量的传出接口。传出接口的每个方法都代表一个单独的**事件**或**请求**。所有COM对象,无论是非可视化COM对象还是ActiveX控件(手动或通过MFC或ATL生成),都使用相同的机制(**可连接性**)向其客户端触发事件。
事件和请求
事件用于告知客户端对象中发生了感兴趣的事情 - 属性已更改或用户已单击某个按钮。事件对COM控件尤其重要。事件由COM对象触发,不期望客户端有任何响应。换句话说,它们是简单的通知。另一方面,请求是COM对象向客户端提问并期望得到答复的方式。事件和请求类似于Windows消息,其中一些消息会告知窗口某个事件(例如WM_MOVE
),而另一些则会向窗口索取信息(例如WM_QUERYENDSESSION
)。
接收器 (Sinks)
在这两种情况下,COM对象的客户端都必须监听对象所说的话,然后适当地使用这些信息。因此,是客户端实现了也称为**接收器**(我真的很不喜欢这个名字,但它在COM和.NET领域已变得普遍且无处不在)的传出接口。从接收器的角度来看,这个传出接口实际上是**传入**的。接收器通过它监听COM对象。在这种情况下,可连接的COM对象扮演客户端的角色。
事物如何联系在一起
让我们从高处审视整个通信情况。有三个参与者:
- COM对象本身
- COM对象的客户端
- 接收器
客户端像往常一样通过对象的传入接口与COM对象通信。为了让COM对象在另一个方向与客户端通信,COM对象必须以某种方式获取指向**某个地方**(即客户端)实现的传出接口的指针。通过此指针,COM对象将向客户端发送事件和请求。这个**某个地方**就是接收器。让我们用一个简单的图来说明上述情况。

请注意,接收器本身就是一个对象。接收器为所有传出接口提供了实现。它通常也与其他客户端代码部分紧密关联,因为实现接收器的整个想法是为了让客户端代码能够响应事件和/或响应COM对象的某些请求。
COM对象如何连接到接收器?
但是,COM对象首先如何连接到接收器?这就是连接点和连接点容器的概念所在。我们将在下一节中详细探讨这一点。
连接点和连接点容器
对于COM对象支持的每个传出接口(请注意“支持”一词;COM对象本身不**实现**此接口,而是**调用**它),COM对象会公开一个称为**连接点**的小对象。这个连接点对象实现了IConnectionPoint
接口。
客户端就是通过这个IConnectionPoint
接口将它的接收器的传出接口实现传递给COM对象。这些IConnectionPoint
对象的引用计数由客户端和COM对象本身维护,以确保双向通信的生命周期。
从客户端角度调用以建立与COM对象的事件通信的方法是IConnectionPoint::Advise()
。Advise()
的反向操作是IConnectionPoint::Unadvise()
,它终止连接。有关这些方法的更多详细信息,请参阅MSDN文档。
因此,通过IConnectionPoint
接口方法调用,客户端可以开始监听COM对象的一系列事件。另请注意,由于COM对象为它支持的每个传出接口维护一个单独的IConnectionPoint
接口,因此客户端必须能够为它实现的每个接收器使用正确的连接点对象。
那么,客户端如何选择适合接收器的连接点呢?这时就轮到**连接点容器**了。一个可连接对象也必须是一个连接点容器。也就是说,它必须实现IConnectionPointContainer
接口。通过此接口,客户端可以请求传出接口的相应连接点对象。
当客户端想要将接收器连接到连接点时,它会向连接点容器请求实现该接收器的传出接口的连接点对象。当它收到相应的连接点对象后,客户端会将接收器的接口指针传递给该连接点。IConnectionPointContainer
接口指针本身可以通过对COM对象进行QueryInterface()
轻松获得。没有什么比下面的示例代码更能说明问题了。
void Sink::SetupConnectionPoint(ISomeInterface* pISomeInterface)
{
IConnectionPointContainer* pIConnectionPointContainerTemp = NULL;
IUnknown* pIUnknown = NULL;
/*QI this object itself for its IUnknown pointer which will be used */
/*later to connect to the Connection Point of the ISomeInterface object.*/
this -> QueryInterface(IID_IUnknown, (void**)&pIUnknown);
if (pIUnknown)
{
/* QI pISomeInterface for its connection point.*/
pISomeInterface -> QueryInterface (IID_IConnectionPointContainer,
(void**)&pIConnectionPointContainerTemp);
if (pIConnectionPointContainerTemp)
{
pIConnectionPointContainerTemp ->
FindConnectionPoint(__uuidof(ISomeEventInterface),
&m_pIConnectionPoint);
pIConnectionPointContainerTemp -> Release();
pIConnectionPointContainerTemp = NULL;
}
if (m_pIConnectionPoint)
{
m_pIConnectionPoint -> Advise(pIUnknown, &m_dwEventCookie);
}
pIUnknown -> Release();
pIUnknown = NULL;
}
}
上述示例函数描述了Sink
类的一个SetupConnectionPoint()
方法。Sink
类实现了传出接口ISomeEventInterface
的方法。SetupConnectionPoint()
方法接受一个参数,该参数是指向名为ISomeInterface
的接口的指针。假定ISomeInterface
背后的COM对象是一个连接点容器。以下是该函数逻辑的概述:
- 我们首先对
Sink
对象本身进行QueryInterface()
以获取其IUnknown
接口指针。稍后将在调用IConnectionPoint::Advise()
时使用此IUnknown
指针。 - 成功获取
IUnknown
指针后,我们接下来对pISomeInterface
背后的对象进行QueryInterface()
以获取其IConnectionPointContainer
接口。 - 成功获取
IConnectionPointContainer
接口指针后,我们使用它来查找传出接口ISomeEventInterface
的相应连接点对象。 - 如果我们能够获得此连接点对象(它将由
m_pIConnectionPoint
表示),我们将继续调用其Advise()
方法。 - 从这里开始,每当
pISomeInterface
背后的COM对象向接收器触发事件(通过调用ISomeEventInterface
的某个方法)时,Sink
对象中的相应方法实现将被调用。
我当然希望上面关于COM中事件处理、连接点和连接点容器的介绍性部分能为读者提供对事件处理基础知识的清晰理解。值得再次提到的是,上述原理无论涉及何种类型的COM对象(例如,普通COM对象、ActiveX控件等)都适用。在充分解释了基础知识之后,我们将继续阐述示例源代码,特别是TEventHandler
模板类。
示例源代码
我将尝试通过运行一些示例代码来解释TEventHandler
。请参考包含在TEventHandler_src.zip文件中的源代码。在示例代码集中,我提供了两套项目:
- EventFiringObject
- TestClient
EventFiringObject
EventFiringObject项目包含一个简单的COM对象代码,该对象实现了一个名为IEventFiringObject
的接口。该COM对象还是一个连接点容器,它识别_IEventFiringObjectEvents
连接点。为了讨论方便,下面列出了该COM对象的相关IDL构造:
[
object,
uuid(8E396CC0-A266-481E-B6B4-0CB564DAA3BC),
dual,
helpstring("IEventFiringObject Interface"),
pointer_default(unique)
]
interface IEventFiringObject : IDispatch
{
[id(1), helpstring("method TestFunction")]
HRESULT TestFunction([in] long lValue);
};
[
uuid(32F2B52C-1C07-43BC-879B-04C70A7FA148),
helpstring("_IEventFiringObjectEvents Interface")
]
dispinterface _IEventFiringObjectEvents
{
properties:
methods:
[id(1), helpstring("method Event1")] HRESULT Event1([in] long lValue);
};
[
uuid(A17BC235-A924-4FFE-8D96-22068CEA9959),
helpstring("EventFiringObject Class")
]
coclass EventFiringObject
{
[default] interface IEventFiringObject;
[default, source] dispinterface _IEventFiringObjectEvents;
};
在EventFiringObject项目中,我们实现了一个名为CEventFiringObject
的C++ ATL类,它实现了coclass
EventFiringObject
的规范。CEventFiringObject
提供了TestFunction()
方法的简单实现。它只是触发_IEventFiringObjectEvents
中指定的Event1
。
STDMETHODIMP CEventFiringObject::TestFunction(long lValue)
{
/* TODO: Add your implementation code here */
Fire_Event1(lValue);
return S_OK;
}
TestClient
TestClient是一个简单的测试应用程序:一个基于MFC对话框的应用程序,它实例化EventFiringObject
COM对象。它还尝试处理从EventFiringObject
触发的Event1
事件。我将更详细地介绍TestClient代码,以解释事件处理过程。客户端代码围绕CTestClientDlg
类展开,该类继承自CDialog
。在TestClientDlg.h中,请注意我们声明了一个智能指针对象的实例,该对象将绑定到实现IEventFiringObject
的COM对象。
/* ***** Declare an instance of a IEventFiringObject smart pointer. ***** */
IEventFiringObjectPtr m_spIEventFiringObject;
然后,在CTestClientDlg::OnInitDialog()
函数中,我们实例化m_spIEventFiringObject
。
/* ***** Create an instance of an object
which implements IEventFiringObject. ***** */
m_spIEventFiringObject.CreateInstance(__uuidof(EventFiringObject));
我们还在简单的对话框中创建了一个标记为“Call Test Function”的按钮。在该按钮的单击处理程序中,我们调用m_spIEventFiringObject
的TestFunction()
。
/* ***** Call the IEventFiringObject.TestFunction(). ***** */
/* ***** This will cause the object which implements ***** */
/* ***** IEventFiringObject to fire Event1. ***** */
m_spIEventFiringObject -> TestFunction(456);
到目前为止,我们处理的主要是典型的COM客户端代码。当调用TestFunction()
时,有趣的就开始了。我们知道TestFunction()
将导致m_spIEventFiringObject
COM对象触发Event1
事件。这就是真正的行动开始的地方。
TEventHandler 类
通用设计目标和示例用例
TEventHandler
类在TEventHandler.h中提供。它的工作原理如下:
- 它在客户端中充当接收器的角色。
- 它通用地处理一个事件接口,该接口必须是dispinterface(即继承自
IDispatch
)。 - 在接收到COM对象触发的事件后,
TEventHandler
将调用其客户端的一个预定义方法。这就是TEventHandler
客户端如何被通知COM对象触发的事件。
客户端的预定义方法必须使用以下签名定义:
typedef HRESULT (event_handler_class::*parent_on_invoke)
(
TEventHandler<event_handler_class, device_interface,
device_event_interface>* pthis,
DISPID dispidMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS* pdispparams,
VARIANT* pvarResult,
EXCEPINFO* pexcepinfo,
UINT* puArgErr
);
请注意,预定义方法的参数与IDispatch::Invoke()
的参数匹配,只是增加了一个参数(位于列表的第一个位置),该参数是指向TEventHandler
实例本身的指针。提供此参数是因为它可能对客户端代码有用。在我们的示例代码的上下文中,我们对TEventHandler
的使用可以用下图表示。

TEventHandler 的设计目的
为什么我们要重新包装IDispatch::Invoke()
方法?如果您的回调函数仍然必须处理IDispatch::Invoke()
调用的所有参数,那么TEventHandler
还能有什么价值?
答案是,TEventHandler
类主要不是为了简化事件方法参数的处理(尽管这可能是可能的,请参阅本节后面关于此的评论)。
它的设计目的是成为一个**接收器**对象。它旨在为客户端提供一个现成的接收器,该接收器可以连接到COM对象的dispinterface事件,并自动调用客户端的镜像Invoke()
方法。
请注意,仅凭C++模板,无法提前预测事件方法的返回值和参数列表。这需要向导的工作,它们可以从与可连接COM对象关联的类型库中读取所有这些信息,然后生成与事件方法签名匹配的函数代码。
TEventHandler
不是一个向导。它是一个C++模板(这使其在某种程度上成为一个真正的代码生成器,但它无法完成所有事情,例如读取类型库并根据其中找到的信息生成代码...)。它还旨在成为一个事件接口的接收器,该接口必须继承自IDispatch
,因此TEventHandler
实现了IDispatch
(有关这方面原因的更多信息,请参阅下一节**“为什么TEventHandler是基于Dispinterface的?”**)。
为什么TEventHandler是基于Dispinterface的?
为了使TEventHandler
成为ISomeEventInterface
的接收器,它必须继承自ISomeEventInterface
,并且必须实现该接口的方法。我选择让TEventHandler
继承自IDispatch
,从而使其成为也继承自IDispatch
的事件接口的接收器。
继承自IDispatch
的接口也称为**dispinterfaces**(代表**disp**atch **interfaces**)。一般的接口(不仅仅是事件接口)是dispinterfaces的,这是非常常见的。它们之所以常见,是因为COM长期以来需要支持Visual Basic。
IDispatch
接口是Visual Basic实现**后期绑定**的基础,后期绑定意味着在**运行时**以编程方式构造函数调用(包括参数和接收返回值)。它可能是所有COM接口中最通用、最灵活的。
请注意,我在这里以通用方式使用后期绑定(我不是指C++的vtable概念,vtable是后期绑定的另一种实现)。IDispatch
可用于定义一个**虚拟接口**,该接口的方法通过IDispatch::Invoke()
方法调用。这种运行时函数调用系统称为**自动化**(以前称为OLE自动化)。
为了区分一个IDispatch
虚拟接口与另一个,会使用一个称为派发接口ID(DIID)的东西。从编程上讲,这与普通的接口ID(IID)没有区别。请看以下代码片段:
pIConnectionPointContainerTemp -> FindConnectionPoint
(
__uuidof(ISomeEventInterface),
&m_pIConnectionPoint
);
在这里,我们要求连接点容器返回一个支持由DIID标识的传出接口的连接点,该DIID等同于__uuidof(ISomeEventInterface)
的结果。
Visual Basic应用程序只能通过基于dispinterface的接收器来处理ActiveX对象的事件。这很自然,因为Visual Basic无法处理非dispinterface的事件,并且还需要处理从任何和所有ActiveX对象触发的事件。
其核心在于,虽然事件接口不一定是dispinterface(其方法可以是任何签名,唯一的规定是事件接口也必须继承自IUnknown
),但Visual Basic无法在内部预测这些自定义事件接口的设计并为其生成接收器。
此外,方法返回值和参数的类型必须限于Visual Basic能够理解和内部处理的类型。因此,标准化处理事件接口的唯一方法是要求它们继承自IDispatch
,并且返回值和参数类型必须来自广泛但有限的集合。这个集合称为**自动化兼容类型**。
与Visual Basic一样,我们无法设计TEventHandler
模板类来作为自定义事件接口的接收器。请记住,我们必须实际实现TEventHandler
要作为接收器的事件接口的方法。因此,我决定TEventHandler
只处理dispinterface事件。而且,许多COM对象源支持它们。
代码详情
TEventHandler
模板类(总结)定义如下:
template <class event_handler_class, typename device_interface,
typename device_event_interface>
class TEventHandler : IDispatch
{
...
...
...
}
请注意,TEventHandler
继承自IDispatch
。但是,请注意,它不必实现IDispatch
的所有方法。最低限度只需要基本的AddRef()
、Release()
、QueryInterface()
和Invoke()
方法。TEventHandler
接受三个模板参数,它们是:
event_handler_class
device_interface
device_event_interface
event_handler_class
参数指示TEventHandler
包含当COM对象触发传出接口事件时将被调用的预定义方法的类的名称。在我们的示例用例中,此参数将是**CTestClientDlg
**类。CTestClientDlg
类将包含调用函数:
HRESULT CTestClientDlg::OnEventFiringObjectInvoke
(
IEventFiringObjectEventHandler* pEventHandler,
DISPID dispidMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS* pdispparams,
VARIANT* pvarResult,
EXCEPINFO* pexcepinfo,
UINT* puArgErr
)
在这个函数中,CTestClientDlg
将处理从EventFiringObject
COM对象触发的事件。device_interface
参数是指我们要接收事件的COM对象的接口类型。在我们的示例用例中,这将是**IEventFiringObject
**。
最后一个模板参数是device_event_interface
,它指示COM对象支持的传出接口的接口类型。这将是Sink对象必须实现的接口。在我们的示例用例中,这将是**_IEventFiringObjectEvents
**。请注意,由于_IEventFiringObjectEvents
本质上继承自IDispatch
,因此我们的Sink对象(即TEventHandler
)也继承自IDispatch
。
所有上述模板参数都用于使VC++编译器能够生成一个C++类,该类已定制以包含与**CTestClientDlg
**、**IEventFiringObject
**和**_IEventFiringObjectEvents
**的**类型**相匹配的方法、属性和参数。因此,最终将创建一个定制的类供代码进一步使用。
用法
TEventHandler
类的使用简单明了。让我们通过TestClient中的一些示例代码来讲解:
- 我们需要定义一个基于
TEventHandler
的特定类类型:// ***** Declare an event handling class using the TEventHandler template. ***** typedef TEventHandler<CTestClientDlg, IEventFiringObject, _IEventFiringObjectEvents> IEventFiringObjectEventHandler;
请注意,我们此时并未实例化一个对象!我们只是通过使用
TEventHandler
来定义一个C++类。我们将这个新的C++类称为IEventFiringObjectEventHandler
。IEventFiringObjectEventHandler
就是我们之前提到的定制类。这个IEventFiringObjectEventHandler
是EventFiringObject
COM对象支持的传出接口_IEventFiringObjectEvents
的**接收器**。 - 在我们的
CTestClientDlg
类中,我们将定义一个指向IEventFiringObjectEventHandler
类实例的指针:/* Declare a pointer to a TEventHandler class */ /* which is specially tailored */ /* to receiving events from the _IEventFiringObjectEvents */ /* events of an IEventFiringObject object. */ IEventFiringObjectEventHandler* m_pIEventFiringObjectEventHandler;
- 我们需要定义
IEventFiringObjectEventHandler
类对象调用EventFiringObject
在基于_IEventFiringObjectEvents
传出接口触发事件时被调用的invoke方法。我们之前见过这个方法,它定义为:HRESULT CTestClientDlg::OnEventFiringObjectInvoke ( IEventFiringObjectEventHandler* pEventHandler, DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr );
请注意,读者必须熟悉
IDispatch::Invoke()
方法才能解释此方法各个参数中包含的值。请参阅OnEventFiringObjectInvoke()
本身的示例代码,并参考MSDN文档获取更多详细信息。 - 在
CTestClientDlg::OnInitDialog()
函数中,我们实例化m_pIEventFiringObjectEventHandler
:/* Instantiate an IEventFiringObjectEventHandler object. */ m_pIEventFiringObjectEventHandler = new IEventFiringObjectEventHandler (*this, m_spIEventFiringObject, &CTestClientDlg::OnEventFiringObjectInvoke );
在这里,我们使用根据
TEventHandler
定义的构造函数参数进行实例化。第一个参数是对CTestClientDlg
对象的引用。第二个是智能指针对象m_spIEventFiringObject
。这将转换为IEventFiringObject
接口指针,因此m_spIEventFiringObject
中的内部指针将传递给构造函数。最后一个参数是指向
CTEstClientDlg
类的方法的指针,该方法符合parent_on_invoke
方法签名。您会注意到在TEventHandler
构造函数中,一旦创建了IEventFiringObjectEventHandler
的实例,就会调用SetupConnectionPoint()
方法。此方法将 duly执行所有必需的连接点协议,以与EventFiringObject
COM对象建立事件连接。 - 当我们不再希望与
EventFiringObject
COM对象保持事件连接时,我们在CTestClientDlg::OnDestroy()
方法中关闭连接点:void CTestClientDlg::OnDestroy() { CDialog::OnDestroy(); /* When the program is terminating, make sure that we instruct our */ /* Event Handler to disconnect from the connection point of the */ /* object which implemented the IEventFiringObject interface. */ /* We also needs to Release() it (instead of deleting it). */ if (m_pIEventFiringObjectEventHandler) { m_pIEventFiringObjectEventHandler -> ShutdownConnectionPoint(); m_pIEventFiringObjectEventHandler -> Release(); m_pIEventFiringObjectEventHandler = NULL; } }
- 为了触发事件处理代码,我在
CTestClientDlg
对话框中包含了一个按钮,该按钮的处理程序将调用EventFiringObject
的TestFunction()
方法,该方法将内部触发Event1
。这将导致IEventFiringObjectEventHandler
类对象调用CTestClientDlg::OnEventFiringObjectInvoke()
。HRESULT CTestClientDlg::OnEventFiringObjectInvoke ( IEventFiringObjectEventHandler* pEventHandler, DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr ) { if (dispidMember == 0x01) // Event1 event. { // 1st param : [in] long lValue. VARIANT varlValue; long lValue = 0; VariantInit(&varlValue); VariantClear(&varlValue); varlValue = (pdispparams -> rgvarg)[0]; lValue = V_I4(&varlValue); TCHAR szMessage[256]; sprintf (szMessage, "Event 1 is fired with value : %d.", lValue); ::MessageBox (NULL, szMessage, "Event", MB_OK); } return S_OK; }
结论
我当然希望您能发现TEventHandler
类很有用。C++模板在生成通用代码方面确实非常出色。已经有传言说C#将提供模板功能。我真的很期待能亲自尝试一下。如果您对TEventHandler
有任何意见,关于如何进一步改进它,请随时给我发电子邮件。