使用 Microsoft® .NET Framework 可以轻松地创建基于 Windows® 的应用程序:您只需创建窗体、添加控件,然后将窗体连接到业务逻辑,这样就可以了。但这样的应用程序并不能为用户提供真正需要的交互功能。例如,当发生重要的外部事件时,这类应用程序通常不会通知用户。并且窗口的外观相同 - 相同的战舰灰色窗体、相同的标准控件、相同的感觉。十分单调!
使用 .NET Framework 和 GDI+,可以轻松地将某些样式添加到您的应用程序中。您可以使用透明度、形状不规则的窗口、通知图标、弹出窗口、不同配色方案等元素。通过精心运用这些设计元素,就可以在应用程序和用户之间建立起更吸引人的交互体验。
装点您的应用程序
您可以应用各种简单的技术来美化应用程序。其中一种技术是使用通知窗口警报用户发生重要事件,突出这些事件,并使用干扰较少的方法通知用户优先级较低的事件。许多应用程序都有效地应用了此技术。例如,MSN® Messenger 在任务栏区域上使用弹出窗口,以在朋友已登录或尝试联系您时通知您;Outlook® 2003 通过桌面警报通知提示收到新的电子邮件。(这些警报还可以使用户在项到达时删除、标记或打开项。)添加具有吸引力的窗体和窗口(用户可以使用它们进行交互)还会增强用户对应用程序的体验。例如,使用 Windows Media Player 可以应用自己的外观,使用非标准矩形形状的窗口。显然,您不该对每个应用程序使用所有这些技术,但是通过明智地应用其中某些技术,可以创建增强用户体验的应用程序。
我将通过修改简单的事件监视器应用程序来介绍如何以及何时应用这些技术。初始应用程序显示所选城市的最新的天气数据(请参阅图 1)。此典型的基于 .NET 桌面应用程序检索 Web 服务的数据,并将结果显示在使用标准 Windows 控件的窗体中。我使用的服务以及许多其他 Web 服务可以在 http://www.xmethods.net/(英文)中找到。
![](http://www.microsoft.com/china/MSDN/library/netFramework/netframework/art/WindowsFormsfig01.gif)
图 1 标准应用程序窗口
这不是一个特别友好的应用程序。您需要对天气报告使用专用的屏幕区域,然后定期按 Update(更新)按钮。我将在本文其余部分增强该应用程序以说明如何创建更吸引人的用户体验。最后,您将拥有一个持续运行的天气警报中心。它将监视特定的城市并在出现重要的天气变化时通知您,而且它将打破那些单调的灰色应用程序的模式。您可以修改应用程序以作为任务栏中的 Notify(通知)图标运行、提供事件发生时的自定义弹出信息、激活自身以及使用具有自定义背景的自定义窗口形状显示其信息。
删除该主窗口依赖关系
首要任务是修改应用程序,以便它不再依赖主窗体,而是在启动时创建 Notify(通知)图标。由于存在适当数量的启动代码,因此最好创建一个新类来处理应用程序级别的任务。您将检查应用程序的多个副本、创建 Notify(通知)图标、添加菜单和处理程序、启动计时器、启动消息循环以及处理一些会话事件。
请务必在应用程序作为 Notify(通知)图标运行时执行这些小任务,因为如果不正确清理,在结束时,任务栏通知区域中很容易出现 Notify(通知)图标的多个副本。这使得使用应用程序变得很难,而且会使那些尝试确定哪个 Notify(通知)图标属于来自应用程序的哪个会话的用户感到迷惑。
要缓解该问题,请首先防止运行应用程序的多个副本。通常的做法是创建一个指明应用程序正在运行的全局 mutex(有关此问题的详细介绍,请参阅本期 MSDN®Magazine 中的 .NET Matters 专栏)。第一个实例获得 mutex 上的一个锁并将在应用程序持续期间保存该锁。后续副本尝试获得该锁,但失败了,然后退出了,如下所示:
Public Shared Sub Main() Dim appSingleton As New System.Threading.Mutex(False, _ "SingleInstance WeatherAlert") If appSingleton.WaitOne(0, False) Then Application.Run() appSingleton.Close() End Sub
接下来,启用应用程序中的 Windows XP 主题。毫无疑问,您肯定听说过 EnableVisualStyles,但使用 .NET Framework 1.x 时,您必须添加一些用于启用向后兼容的额外代码(此代码对于 .NET Framework 2.0 不再是必需的)。在以前的操作系统中运行时调用 EnableVisualStyles 会终止程序,因为用户将丢失 uxtheme.dll。您必须在应用主题之前先测试较新的操作系统,如下所示:
Private Shared Sub InitThemes() If (((Environment.OSVersion.Platform = PlatformID.Win32NT) _ AndAlso (Environment.OSVersion.Version.Major >= 5)) _ AndAlso (Environment.OSVersion.Version.Minor > 0)) Then If OSFeature.Feature.IsPresent(OSFeature.Themes) Then Application.EnableVisualStyles() End If Application.DoEvents() End If End Sub
主应用程序类也创建 Notify(通知)图标并运行主消息循环。运行没有主窗口的应用程序与运行具有主窗口的应用程序基本相同 - 只需进行两处更改。必须将调用 Application.Run(FormName) 改为调用 Application.Run(),以便启动没有任何主窗体的消息循环。如果用户从未创建主窗口,则不需要创建不可见状态的窗口;根本就不要创建主窗口。第二处更改是处理 SessionEnded 事件。如果用户在应用程序运行时注销 Windows,则 Windows 将通过引发 SessionEnded 事件要求退出应用程序。如果不响应此事件,Windows 将假定应用程序刚刚出错。Windows 将通知您的用户您的应用程序未响应,并要求终止应用程序。要解决此问题,只需处理该事件并关闭应用程序。将处理程序附加到主应用程序,如下所示:
AddHandler Microsoft.Win32.SystemEvents.SessionEnded, _ New Microsoft.Win32.SessionEndedEventHandler(AddressOf _ ApplicationMain.SystemEvents_SessionEnded) 将在该处理程序中进行正确清理: Private Shared Sub SystemEvents_SessionEnded(ByVal sender As Object, _ ByVal e As Microsoft.Win32.SessionEndedEventArgs) If (Not _mainWindow Is Nothing) Then _mainWindow.Close() _icon.Visible = False Application.Exit() End Sub
退出代码中有一些项与标准 Windows 窗体应用程序不同。首先,您需要检查主窗口是否是在您关闭它之前创建的。用户甚至可能未打开主窗口。然后,您需要隐藏 Notify(通知)图标。您是否曾使一个作为任务栏图标运行的程序消失并留下灰显的任务栏图标直到您试用并激活它?一些开发人员忘记清理任务栏了。在退出程序之前需要隐藏 Notify(通知)图标。对于 Notify(通知)图标,没有 Close 方法;隐藏 Notify(通知)图标时,该图标将自我清理。
通知用户
用户将应用程序作为图标运行时就是向用户提供反馈的时间。您可以通过创建弹出窗口实现此目的。我不喜欢标准提示通知 - 我更喜欢其他样式的通知,例如 Outlook 消息通知或 MSN Messenger 联系人登录通知。我希望对通知加以控制以通知用户,而不影响正常的任务。
Outlook 2003 经适当配置后,在您收到电子邮件时,屏幕右下角将淡显一个小窗口,其中显示发件人和邮件主题。您可以单击此处以阅读邮件、立即删除邮件或设置后续标记。如果您很忙而邮件不紧急,您可以忽略该弹出窗口,允许它逐渐消失,将该邮件保留为未读状态,以稍后进行阅读。
为天气警报应用程序创建一个类似的窗口将会使用户更轻松地接收天气信息更新。通知窗口本身会很简短地显示,如果用户忽略该窗口,则该窗口将消失。不要创建那种需要用户强制忽略的通知窗体。如果用户不在附近,或者用户认为邮件不够重要无需中断当前任务,则该通知会消失。
![](http://www.microsoft.com/china/MSDN/library/netFramework/netframework/art/WindowsFormsfig02.gif)
图 2 通知框
我将窗口放在桌面的右下角处,通知图标的正上方。弹出窗口应该逐渐显示、显示一会儿,然后逐渐消失。为了使该窗口更易于使用,如果用户将鼠标移动到窗口上,它将立即变得不透明,而且不会逐渐消失,除非用户离开该窗口。如果用户希望打开主应用程序,可以单击弹出窗口,应用程序将显示主窗口。最后,若要添加一些样式,可以使用渐变画笔绘制背景,然后隐藏所有常见的窗口边界(请参阅图 2)。GDI+ 可以使此操作变得相当简单。
使用 GDI+
创建透明、非矩形窗口需要几个简单的步骤。首先,将设计器中的 Form Border Style(窗体边界样式)更改为 None(无)。该操作将删除弹出窗口中的所有正常的非客户端区域对象。没有关闭框、系统菜单或尺寸柄。您还应该关闭“在任务栏中显示”设置。不希望弹出窗口显示在任务栏中。
使用一种称为“色度键控”的技术可以实现透明度。色度键控图像将一种颜色指定为透明色。在绘制窗口时,不绘制与透明色匹配的所有像素。请务必选择一种不可能在正常应用程序中使用的颜色。显然,白色、黑色和灰色不是最好的选择,因为它们的使用频率很高。酸橙绿或浅黄绿色可能会好一些。使用 Visual Studio® .NET 中的 Windows 窗体属性可以设置通过 TransparencyKey 属性呈现透明的颜色。有关色度键控的详细信息,请参阅“色度键控、Alpha 值混合处理和透明度”提要栏。
设置透明度后,必须添加代码才能绘制背景。要使用 GDI+ 实现的圆角矩形功能,可以使用图形路径。事实上,使用图形路径可以创建所需的任何不规则形状。图 3 中的代码说明了用于创建圆角矩形路径的方法。
背景利用另一个新的 GDI+ 功能 - 渐变画笔。我使用线性渐变画笔创建了所需的背景效果。首先,我用酸橙绿透明色填充了整个背景矩形,然后用线性渐变画笔(从钢青色到浅蓝色)填充了内部圆角矩形。我是使用 OnPaintBackground 方法的替代方法完成所有这些工作的。使用替代方法比将事件处理程序附加到相关事件更为有效。
要完成绘制代码,必须添加绘制文本的绘制处理程序。它将在其构造函数中绘制传递到弹出窗口的消息。然后,您希望将字体设置为与系统定义的链接颜色和活动链接一致。当用户进入弹出窗口时,将窗口字体更改为带有下划线的字体。当用户离开该窗口时,还原为原来的字体。这会给用户留下一种弹出窗口中的文本是一个链接的印象。
逐渐显示和逐渐消失
下一步是添加计时器和处理程序以更新弹出窗口的不透明度。在设计器中,添加计时器并将弹出窗口的初始不透明度设置为 0%。计时器事件引发时,增加不透明度(我选择了增加 0.05)。当不透明度达到 1 时,重置计时器以在 3 秒内引发,以便用户有时间查看弹出窗口。然后,将计时器设置为较小的时间间隔,降低每次引发时的不透明度。不透明度达到 0 后,关闭窗口。
必须添加处理程序才能在用户将鼠标移到弹出窗口时停止逐渐显示/逐渐消失进程。当然,用户离开该窗口时要重新启动逐渐消失进程。这里只有一个要注意的问题。关闭框(请参阅图 2)是子窗口。当用户将鼠标移到关闭框上时,主窗口将获得 MouseLeave 消息。
除了弹出窗口的鼠标进入和鼠标离开方法,还必须为弹出窗口中的所有子窗口添加事件处理程序。虽然您的确会在用户在关闭框和父级弹出窗口之间移动时获取进入和离开事件,但在获取其他 MouseEnter 事件之前不会从一个窗口获取 MouseLeave 消息。这意味着您从一个窗口移到另一个窗口时不能使状态出错。用户与弹出窗口交互时,该窗口不会消失。
在您离开弹出窗口执行之前,必须定义该窗口在您收到多个更新时的行为。在此示例中,我采取了一种简单的方式:如果弹出窗口显示时发生第二个通知,只需忽略。商业应用程序采用不同的方法。Outlook 2003 创建了一系列弹出窗口,每个电子邮件一个窗口。MSN Messenger 将弹出窗口堆栈在屏幕上,直到达到桌面的顶端。此示例并不会生成达到该程度的足够的有趣通知。
设计应用程序的外观
现在该将一些样式应用到主窗口了(请参阅图 4)。要达到效果,需要创建背景图像。任何一个您希望透明的位置都应以透明色绘制。在我的示例中,我又将背景色选择为酸橙绿。将背景图像设置为新的位图,将窗体边框样式设置为 None(无),这样就基本完成了。问题是当位图颜色深度与监视器颜色深度不一致时,有一些图形卡阻止透明度起作用(有关详细信息,请参阅知识库文章 822495 BUG:The TransparencyKey Property Is Not Effective for Microsoft Windows Forms If the Color Depth of the Monitor Is Set to a Value That Is Greater Than 24-Bit(英文)。要解决此问题,必须将背景位图设置为透明。下面的代码中显示的 OnLoad 替代方法可以实现此操作。
Protected Overrides Sub OnLoad(ByVal e As EventArgs) Dim bitmapBG As Bitmap = _CType (BackgroundImage,Bitmap) bitmapBG.MakeTransparent(bitmapBG.GetPixel(0, 0)) TransparencyKey = bitmapBG.GetPixel(0, 0) MyBase.OnLoad(e) End Sub
更新图像后,还必须更新透明度键以便与像素的已更改值匹配。
![](http://www.microsoft.com/china/MSDN/library/netFramework/netframework/art/WindowsFormsfig03.gif)
图 4 向主窗口添加样式
然后,需要修改控件,以便它们处于透明状态,更好地显示在背景中。我选择了黄色字体以区别于背景图像。您可以在设计器或代码中修改背景色,将其设置为 System.Drawing.Color.Transparent。背景图像将通过控件显示(请参阅图 5)。
![](http://www.microsoft.com/china/MSDN/library/netFramework/netframework/art/WindowsFormsfig04.gif)
图 5 背景图像显示方式
最后,它会帮助用户添加一种在屏幕周围移动主窗口的机制。由于不再存在标题栏,您可以让用户移动窗口,方法是在可见窗体的任意位置按鼠标左键并将窗口从一个位置拖到另一个位置。如果用户拖动窗口,MouseMove 方法将移动该窗口:
Protected Overrides Sub OnMouseMove(ByVal e As MouseEventArgs) If _isMoving Then Location = New Point(Location.X + e.X - XOffset, Location.Y + e.Y - YOffset) End If MyBase.OnMouseMove(e) End Sub
该方法很有用,除非用户碰巧在其中一个子窗口(创建的用于显示天气信息的窗口)中单击了鼠标。要使其发挥作用,必须创建一个按下鼠标处理程序,指明用户在某个子窗口中单击鼠标按钮时的移动操作。但这并非对所有子窗口都有用 - 您仍然需要链接按钮和关闭框的默认行为。同一方法可处理任何其他子控件的所有 MouseDown 事件,如下所示:
Private Sub Child_MouseDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) Handles _ labelAirPressure.MouseDown, labelDewPoint.MouseDown, _ labelHumidity.MouseDown, labelLocation.MouseDown, _ labelSkyConditions.MouseDown, labelTemperature.MouseDown, _ labelVisibility.MouseDown, labelWind.MouseDown If (e.Button = MouseButtons.Left) Then _isMoving = True XOffset = e.X YOffset = e.Y End If End Sub
添加关闭框和链接按钮以获取最新天气信息,这样就完成了。
小结
创建需要以不同方式与用户进行交互的应用程序时,突出它们,但不要过度。由于标准外观已为用户所熟悉,因此如果您打破了该模式,则理由必须充分。由于过多的应用程序将占用系统任务栏的空间,因此不要进行该操作,除非它确实是应用程序与用户交互的最佳方法。否则,请使用其他方法宣传您的应用程序。
Bill Wagner 是 SRT Solutions, Inc. 的创始人之一,还是 Microsoft 在密歇根州的区域总监。Bill 是 Effective C# (Addison-Wesley, 2004) 的作者。要与他联系,请发送电子邮件至 wwagner@srtsolutions.com,或加入位于 www.srtsolutions.com/public/blog/20574(英文)的 Blog 论坛。