在 Visual C++ 客户端中处理 VB ActiveX 事件






4.92/5 (16投票s)
2001 年 5 月 9 日
9分钟阅读

279860

2541
本文展示了如何在 Visual C++ 客户端中处理 VB ActiveX 组件中生成的自定义事件。
引言
本教程是我关于构建用于 VB ActiveX DLL 的 VC 客户端一文的扩展。阅读了该文章后,读者自然会想到处理事件的问题。
在这里,我将向您展示如何在 Visual C++ 客户端中处理 VB ActiveX 组件中生成的自定义事件。首先,我们将构建一个 MFC 客户端,然后转向创建一个 ATL 客户端。正如您将看到的,所有这些工作一点也不难。像 MFC 这样的框架使程序员非常容易从 ActiveX 代码组件接收事件通知。在继续之前,我假设读者熟悉 VB ActiveX 技术、自动化以及 MFC COM 和 IDL(接口定义语言)。
VB ActiveX 组件
VB ActiveX 组件是遵循 COM 规范以提供对象的代码单元。它通过一个或多个接口公开其大部分功能。这些软件组件具有高可重用性。
由于组件需要与客户端通信,因此它们可以实现为进程内(即 DLL)组件和进程外(即 EXE)组件。
在进程内组件(ActiveX DLL 组件)中,服务器和客户端之间的通信是在客户端应用程序的地址空间中实现的。尽管这使得它们比需要加载到自己地址空间中的 ActiveX EXE 组件更快,但最大的缺点是错误的 DLL 会使客户端崩溃,进而使对象崩溃。这往往会使所有人都崩溃。:-)
VB 事件
事件可以简单地定义为在应用程序生命周期内发生的事情。事件允许类与其他类和程序通信。组件会引发事件以通知客户端某个任务的完成。客户端可以捕获此事件,并且客户端可以根据需要响应事件。
自定义事件为类和组件提供事件处理功能。生成事件的对象称为事件源,响应事件的对象称为事件接收器。首先,我们将从 VB ActiveX DLL 中触发一个自定义事件,并在 MFC 客户端中处理通知。换句话说,我们必须在 MFC 客户端中构建一个事件接收器,以响应由充当事件源的 VB ActiveX 组件生成的事件。事件接收器中的代码在事件触发时执行。
要在 VB 中声明一个名为 evtTaskDone
的自定义事件,请在类模块的通用声明部分使用 Event
关键字,例如
Public Event evtTaskDone()
然后可以使用 RaiseEvent
关键字触发此自定义事件,例如
RaiseEvent evtTaskDone
考虑到所有这些,我们现在卷起袖子,深入研究一些代码。首先,我们将构建一个作为事件源的 VB ActiveX DLL。
构建事件源
启动 VB,并在“新建项目”对话框中,选择ActiveX DLL,然后单击打开。VB 会创建一个名为 Project1
的新 DLL 组件项目,其中包含一个类 Class1
。转到“项目”->“属性”,并将项目名称设置为 VBEvents
。在“项目资源管理器”视图中,右键单击 Class1
,然后选择将其从项目中删除。
注意:我们也可以选择使用这个类,但那样我们就看不到 VB 类生成器了。
现在再次右键单击“项目资源管理器”视图,然后向项目中添加一个类模块。在“添加类模块”对话框中,选择VB 类生成器,然后单击“打开”。
在类生成器中,转到文件->新建->类,然后添加一个名为 clsEventSrc
的新类。接受默认值并单击“确定”。接下来,转到文件->新建->事件,然后向此类添加一个名为 evtNotify
的事件。将所有更改更新到项目并关闭类生成器窗口。
接下来,单击工具->添加过程,然后向您刚刚创建的 clsEventSrc
类添加一个名为 prcFireEvent
的新过程,例如
Public Sub prcFireEvent()
RaiseEvent evtNotify
End Sub
该过程只是触发我们的事件。保存所有内容,然后转到文件->生成以构建 VBEvents.dll 并注册组件。
构建 MFC 客户端
我们的 MFC 客户端是一个普通的 AppWizard 生成的基于对话框的应用程序,具有额外的自动化支持。像往常一样,打开 VC++ 6.0 并创建一个名为 MFCClient
的新 MFC Appwizard
EXE 项目。点击“构建”以构建项目,然后从所有辛苦工作中休息一下!
OLE/COM 对象查看器是 Visual C++ 6.0 附带的一个方便的小工具。它将帮助我们为 DLL 组件生成 IDL 文件。转到工具->OLE/COM 对象查看器并打开此工具。接下来,在 OLE/COM 对象查看器中,单击文件->查看类型库并导航到我们之前构建的 VBEvent.dll 文件。准备好迎接一些魔术了吗?单击“打开”并打开ITypeLib 查看器。您能查看 IDL 文件吗?哇!通过文件->另存为将文件保存为 VBEvents.IDL 并关闭工具。我们目前不需要它。
接下来在我们的 VC++ 项目中,将此 IDL 文件添加到项目中。在 FileView
中,右键单击 IDL 文件并选择设置。在MIDL选项卡中,将输出头文件名设置为 VBEvents.h,将UUID 文件名设置为 VBEvents_i.c。此外,取消选择MkTyplib 兼容选项。
保存所有内容,然后在 FileView
中,右键单击 VBEvents.IDL 文件并选择编译。这将构建 typelibrary
并生成必要的文件。
检查 MIDL 生成的 VBEvents_i.c 文件。它包含客户端可用于构建接收器对象的所有 UUID 定义。在 VBEvents.h 中,注意双接口 _clsEventSrc
。组件的 dispinterface
__clsEventSrc
由 DIID___clsEvent
标识。这是我们自定义事件的事件源。
下一步是添加一个接收器对象以连接到源事件。幸运的是,对于我们来说,MFC 使构建事件接收器变得像 1-2-3(以及 4-5-6)一样容易。通过几个 MFC 宏,您可以将构建接收器所涉及的许多复杂性委托给 MFC。首先,向项目添加一个名为 MFCSink
的新 CCmdTarget
派生类。在 ClassWizard
中,选择自动化选项。这是我们具有自动化支持的接收器对象。
然后使用 #import
在客户端中导入服务器的 typelib
。如果您还没有阅读我的上一篇文章,请在此处阅读。否则,请直接使用如下代码
#import "VBEvents.dll" rename_namespace("MFCClient")
using namespace MFCClient;
这段代码没有什么新东西。当您在 stdafx.h 中时,还要 #include
afxctl.h 文件
接下来,打开 MFCSink.cpp 并修改 INTERFACE_PART
宏,以便第二个参数 (IID
) 是事件源的 IID,在我们的例子中是 DIID___clsEventSrc
。您的接口映射应如下所示
BEGIN_INTERFACE_MAP(MFCSink, CCmdTarget)
INTERFACE_PART(MFCSink, DIID___clsEventSrc, Dispatch)
END_INTERFACE_MAP()
接下来,在 MFCSink
类的 DISPATCH
映射中,为要处理的源接口中定义的每个事件添加一个 DISP_FUNCTION_ID
宏。我的 DISPATCH
映射如下所示
BEGIN_DISPATCH_MAP(MFCSink, CCmdTarget)
//{{AFX_DISPATCH_MAP(MFCSink)
DISP_FUNCTION_ID(MFCSink, "evtNotify",1,evtNotify, VT_EMPTY, VTS_NONE)
//}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()
在 Classview
中,右键单击 IMFCSink
接口并添加一个方法 evtNotify()
。请注意,根据我们的 DISPATCH
映射,此方法不带参数并返回 void
。我们此方法的实现显示一个简单的 MessageBox
,看起来像
void MFCSink::evtNotify()
{
// TODO: Add your dispatch handler code here
AfxMessageBox("Event notification handled in MFC client");
}
剩下我们要做的就是在客户端代码中适当地连接和终止连接。MFC 使用 AfxConnectionAdvise()
和相应的 AfxConnectionUnadvise()
使这项工作变得非常容易。如果您不熟悉这些函数,现在是查看其文档的好时机。
继续,在对话框类头文件中声明三个变量,如下所示
_clsEventSrc *m_pSrc;
MFCSink *m_pSink;
DWORD m_dwCookie;
第一个是指向我们将通过其触发事件的接口的指针。第二个是指向接收器对象的指针。最后,m_dwCookie
变量是一个 cookie,它存储已建立的连接数。当我们想要断开与事件源的连接时,我们需要它。在我们的例子中,我们在对话框类构造函数中将其设置为 1
。不要忘记 #include
VBEvents_i.c 文件。在对话框的 OnInitDialog()
成员函数中建立连接的代码如下所示
CoInitialize(NULL); /*Initialize COM system*/
m_pSink=new MFCSink; /*create an instance of the sink object*/
/*create source object*/
HRESULT hr=CoCreateInstance(CLSID_clsEventSrc,NULL,CLSCTX_INPROC_SERVER,IID_IDispatch,(void**) &m_pSrc);
if(SUCCEEDED(hr))
LPUNKNOWN m_pUnk=m_pSink->GetIDispatch(FALSE);
if(SUCCEEDED(hr))
{
/*establish the connection*/
if (AfxConnectionAdvise(m_pSrc,DIID___clsEventSrc,m_pUnk,FALSE,&m_dwCookie))
return TRUE;
else
return FALSE;
}
else
return FALSE;
由于我们已建立连接,因此当对话框被销毁时,我们还需要断开连接。我们可以在对话框的 OnDestroy()
中执行此操作。在 ClassView
中,右键单击对话框类并添加一个 Windows 消息处理程序来处理 WM_DESTROY
消息。在处理程序中,添加以下代码以成功断开连接。
LPUNKNOWN m_pUnk=m_pSink->GetIDispatch(FALSE);
AfxConnectionUnadvise(m_pSrc,DIID___clsEventSrc,m_pUnk,FALSE,m_dwCookie);
if(m_pSink!=NULL)
{
delete m_pSink; /*the sink destructor must be public or compiler will complain*/
m_pSink=NULL;
m_pSrc=NULL;
}
一切就绪后,我们现在需要触发事件。只需调用
m_pSrc->prcFireEvent();
在您想要触发事件的代码中的任何位置。
ATL 客户端
构建纯 ATL 客户端比 MFC 客户端需要更多的打字。但这比在原始 C++ 中创建可连接对象要容易得多。请记住,接收器必须支持 IDispatch
。这意味着至少需要实现 7 个方法(IUnknown
的 3 个,IDispatch
的 4 个)。让我们欣慰的是,ATL 提供了 IDispEventSimpleImpl<>
和 IDispEventImpl<>
模板类,可帮助我们快速创建 dispinterface
接收器对象。有大量关于为基于 dispinterface
的源对象创建 ATL 接收器的信息和代码,您可能需要查找。相关 Microsoft KB 文章 Q:181277、Q:181845 和 Q:194179
回到手头的任务,为了使我们的客户端非常高效,我们将使用 IDispEventSimpleImpl
派生类。首先,创建一个新的 ATL/COM AppWizard
生成的 EXE 项目,名为 ATLClient
。为此,添加一个名为 ATLClientDlg
的对话框。对话框有两个按钮,一个用于建立连接,另一个用于触发事件。接下来,如上文 MFC 客户端部分所述,使用 #import
导入服务器的 typelib
。转到接收器对象,声明如下
#define IDC_SRCOBJ 1
static _ATL_FUNC_INFO OnEventInfo = {CC_STDCALL, VT_EMPTY, VT_NULL};
class CSinkObj : public IDispEventSimpleImpl<IDC_SRCOBJ, CSinkObj, &__uuidof(__clsEventSrc)>
{
public:
HWND m_hWndList;
CSinkObj(HWND hWnd = NULL) : m_hWndList(hWnd)
{
}
BEGIN_SINK_MAP(CSinkObj)
//Make sure the Event Handlers have __stdcall calling convention
SINK_ENTRY_INFO(IDC_SRCOBJ, __uuidof(__clsEventSrc), 1, evtNotify, &OnEventInfo)
END_SINK_MAP()
// Event handler
HRESULT __stdcall evtNotify()
{
// output string to list box
TCHAR buf[80];
wsprintf(buf, "Sink : Notification Event Received");
AtlTrace("\n%s",buf);
return S_OK;
}
};
我所做的只是向 IDispEventSimpleImpl
派生类添加了一个接收器映射,然后为我想要处理的源接口的每个事件添加了一个接收器条目。ATL_FUNC_INFO
结构帮助我们将参数传递给事件处理程序。然而,在我们的事件处理程序中,我们没有做任何花哨的事情。一个简单的调试消息就足够了。
在对话框类中,添加变量
private:
CSinkObj* m_pSink;
_clsEventSrc *pEvent;
对话框类的 OnConnect()
如下所示
LRESULT OnConnect(UINT,WORD,HWND hWndCtrl,BOOL& bHandled)
{
m_pSink=new CSinkObj(hWndCtrl);
HRESULT hr=CoCreateInstance(CLSID_clsEventSrc,NULL,CLSCTX_INPROC_SERVER,
__uuidof(_clsEventSrc),(void**)&pEvent);
if(SUCCEEDED(hr))
{
m_pSink->DispEventAdvise(pEvent);
}
return hr;
}
和以前一样,调用
pEvent->prcFireEvent();
当您想要触发事件时。不要忘记在对话框销毁时使用 DispEventUnadvise()
断开连接。
就是这样!我们已经构建了 MFC 和 ATL 客户端,它们都响应由 VB ActiveX DLL 代码组件生成的事件。代码和项目文件是使用 Visual C++ 6.0 SP3 在 Win95 下构建的。
我包含了另一个项目 VBTimer
,它由一个 VB ActiveX DLL 和相应的 ATL 客户端项目文件组成。这个项目比我们的第一个 VB DLL 更复杂一些,后者在没有任何参数的情况下触发事件。ActiveX DLL 实现了一个 VB Timer,它每隔 1 秒触发一个带有一个参数(计时器计数)的事件。此事件由 ATL 客户端捕获,并在输出窗口中显示计时器计数。
参考文献
- NIIT 技术参考
- Microsoft KB 文章 Q181845、Q181277 和 Q194179
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。