对于许多使用 Microsoft .NET Framework 的开发人员来说,他们印象中认为这类应用程序与可以在该应用程序中使用的库相关联,比如,控制台应用程序不能使用 Windows Forms 类。
对于许多使用 Microsoftreg; .NET Framework 的开发人员来说,他们印象中认为这类应用程序与可以在该应用程序中使用的库相关联,比如,控制台应用程序不能使用 Windowsreg; Forms 类。其实,这些概念是独立的。当您在 Visual Studioreg; 中创建新的控制台应用程序时,它将应用程序输出类型设置为“控制台应用程序”(等同于给 csc.exe 的 /t:exe 命令行参数)。当您在 Visual Studio 中创建新的 Windows 窗体应用程序时,它将应用程序输出类型设置为“Windows 应用程序”(等同于 /t:winexe),同时添加引用至 System.Windows.Forms.dll。但是很多人没有意识到的是,在控制台应用程序中可以添加对 System.Windows.Forms.dll 的引用,而且与之相反,您可以更改 Windows 窗体项目并编译为“控制台应用程序”。应用程序类型实际只规定是否将为进程启动默认的控制台窗口。
所有这些的意思是,在控制台应用程序中,您可以使用来自 System.Windows.Forms 的类帮助您达成目标。我将创建一个依赖 Windows 窗体类的类使之变得很容易,将用由 winmm.dll 导出的 mciSendString 函数作为此类的测试案例,因为它提供的功能与您在使用时存在困难的 API 相似。同样的解决方案,但是会让您的控制台应用程序能处理各种 Windows 消息。例如,为了利用 Windows Vista™ 中提供的一些新的电源管理功能,您可能希望处理 WM_POWERBROADCAST 消息。本专栏中提供的类可以帮您解决这个问题。
我将从我正在进行的设计开始。从 System.dll 导出的 Microsoft.Win32.SystemEvents 类提供了大量静态事件,用于监视各种系统状况。其中一些由广播消息引发。例如,DisplaySettingsChanging 和 DisplaySettingsChanged 事件是由 WM_DISPLAYCHANGE 消息引发。
当 SystemEvents 在用户交互式应用程序中初始化时,如果引发 SystemEvents 初始化的应用程序线程被标记为 STA(单线程单元,即 Windows 窗体应用程序的默认设置),SystemEvents 将预期此线程有一个消息循环,以便 SystemEvents 附带发生。(因此,如果您错误地将某控制台应用程序标记为 STA,而且没有人工弹出消息,就无法引发 SystemEvents 上任何基于消息的事件。)但是,如果初始化线程是 MTA(多线程单元,即控制台应用程序中的默认设置),SystemEvents 将生成自己的线程。该线程将执行一个消息循环。在任何一种情况下,SystemEvents 都会创建一个隐藏窗口,与运行消息循环的线程相关联。此窗口的窗口过程负责处理相关消息和引发相关事件。
由于我的实现不会像 SystemEvents 那么复杂,因此一个与 SystemEvents 类似的基本设计将使一切顺利开始工作。我的 MessageEvents 类提供了三个公共和静态成员:
public static class MessageEvents
{
public static void WatchMessage(int message);
public static event EventHandler
MessageReceived;
public static IntPtr WindowHandle;
...
}
WatchMessage 方法用来通知 MessageEvents 类有关(我作为使用者实际关心的)消息。(相比较而言,SystemEvents 已将这些硬编码到它的实现中。)当收到其中一条消息时,就会引发 MessageReceived 事件;此事件携带一个 MessageReceivedEventArgs 实例,并进而关联到代表所接收消息的相关 System.Windows.Forms.Message 结构。最后,如果 API 的目标是将向其发送消息的特定窗口,MesssageEvents 类将为 MessageEvents 用于接收和处理事件的底层窗口公开句柄。(通常那些使用窗口消息进行通知的 API 也能与 HWND_BROADCAST 或 0xFFFF 配合使用,如果使用后者的话,可以导致通知消息到达我们的底层窗口。)
该底层窗口由 MessageEvents 创建。SystemEvents 能够智能化地决定是否创建一个附加窗口,如果不必这么做就节省资源。与之不同的是,我采用的是总是创建新线程和窗口的简单方法。如果您通过测量数据发现这损害了应用程序的性能,请放心使用必要的附加逻辑适当地扩展类型。
MessageWindow 是 MessageEvents 中的一种私有嵌套类型。它由窗体派生而来,因此也继承了所有控件实例可用的底层 NativeWindow 功能(我可以将 MessageWindow 重新编写得精简一些,并直接操作 NativeWindow,但我还是把它留给您作为练习,如果您觉得合适的话)。MessageWindow 提供了一个公共成员 RegisterEventForMessage,它所做的仅仅将提供的 messageID 存入一个由 Dictionary 表示的消息集。该类也会覆盖 WndProc 方法,后者是对接收消息的窗口做出响应时执行的方法。这种覆盖会检查接收的消息是否在消息集内,并因此判断它是否已是为要引发的事件所注册的消息。如果是,WndProc 引发 MessageReceived 事件。不管如何,所有消息将传递到基本类的 WndProc 实现。
关于如何引发事件,有几个有趣的地方值得注意。首先,MessageReceived 是 MessageEvents 上的一个事件,而非 MessageWindow 的。通常,一个类不能通过直接访问引发另一个类上的事件。这是 C# 编译器玩的一个小技巧。当我声明 MessageReceived 事件时,实际上我在做两件事:声明一个公共事件和声明一个私有委托。公共事件提供 add 和 remove 访问器,这就是为什么任何类都能用事件注册和注销处理程序。私有委托是在我“引发事件”时实际所调用的内容,即使在 C# 源代码中看上去像是我直接调用了事件(有关详细信息,请参见 msdn.microsoft.com/msdnmag/issues/06/11/NETMatters)。由于委托是私有的,因此其他类应该无法对其进行访问。但是,由于 MessageWindow 是 MessageEvents 中的嵌套类,因此 MessageWindow 确实可以访问 MessageEvents 私有实现的详细信息。在这种情况下,MessageWindow 可以访问私有的 MessageReceived 委托,允许它被 MessageWindow 直接调用。
需要注意的另外一点是,事件不会在执行 WndProc 的同一线程上引发。相反,WndProc 在初始化后会使用 MessageEvents 获取的 SynchronizationContext 实例(稍后将详细讨论此问题)。SynchronizationContext 用来在应用程序的同步环境中(不管是什么),通过异步 Post(而不是同步 Send)引发事件。在控制台应用程序中,SynchronizationContext.Post 所做的仅仅是执行 ThreadPool 线程上的委托。在 Windows 窗体应用程序中,WindowsFormsSynchronizationContext.Post 通过将调用封送处理回 UI 线程来执行委托。
您可能对 [Visual Studio.NET] 的这些文章也感兴趣: