将应用程序最小化到通知区域






4.61/5 (12投票s)
如何编写一个用户关闭时会转到系统托盘的应用程序

引言
本文是我之前的一篇文章《用 VB.NET 创建系统托盘应用程序》[^] 的延伸。如果你不熟悉如何为通知区域编写应用程序,那将是一个不错的起点。
背景
我最近得到一个 BitTorrent 下载器,它做了一些非常有趣的事情:当应用程序关闭时,它会切换到后台模式并最小化到通知区域的一个图标。右键单击该图标会弹出一个菜单,显示我还有多少个种子正在下载,并允许我恢复到常规窗口或完全退出应用程序。这看起来是个很酷的功能,所以我开始学习如何编写能够实现相同功能的应用程序。本文就是结果。
像往常一样,我用 VB.NET 编写了它,因为这是我最熟悉的语言。将其翻译成 C# 应该 pretty easy:只需记住其中一些实现是 VB 特有的。
代码和演示使用 Visual Studio 2008 和 3.5 Framework 进行测试;它应该可以与 .NET 4.0 甚至 2.0 一起使用。如果不行,请留言。
上下文决定一切
有效地,这项技术创建了一个可能拥有或不拥有可见用户界面的通知区域应用程序。我们首先创建一个 `System.Windows.Forms.ApplicationContext` 的派生类。
Public Class AppContext
Inherits ApplicationContext
#Region " Constructor "
Public Sub New(ByVal StartClosed As Boolean)
InitializeApp()
If StartClosed Then
Notify.Visible = True
Else
AppForm.Show()
End If
End Sub
#End Region
#Region " Event handlers "
Private Sub AppContext_ThreadExit(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles Me.ThreadExit
'Guarantees that the icon will not linger.
Notify.Visible = False
End Sub
#End Region
End Class
当创建 `AppContext` 时,它会接受一个参数,指示应用程序是立即启动到通知区域还是显示一个窗口。在初始化一切之后,应用程序线程将通过在系统托盘中显示图标或显示应用程序的窗体来启动。当应用程序线程结束时——无论是用户退出、程序管理器取消、Windows 正在关闭,等等——`ThreadExit` 事件会被调用,这允许你执行任何必要的清理工作,例如关闭文件和保存应用程序状态。我这里所做的只是确保图标已从通知区域移除。
幕后代码
下一步是创建 `MainModule` 来处理应用程序的实际管理。使用 `WithEvents` 关键字声明组件,以便你可以拦截组件的事件。
#Region " Global storage "
Public WithEvents AppForm As MainForm
Public WithEvents Notify As NotifyIcon
Public WithEvents MainMenu As ContextMenuStrip
Public WithEvents mnuShow As ToolStripMenuItem
Public WithEvents mnuSep1 As ToolStripSeparator
Public WithEvents mnuExit As ToolStripMenuItem
Public TimerMenu As ToolStripMenuItem
#End Region
然后创建 `Sub Main` 方法,它将成为你应用程序的入口点。
#Region " Sub Main "
Public Sub Main(ByVal cmdArgs() As String)
Application.EnableVisualStyles()
Dim StartClosed As Boolean = False
For Each Cmd As String In cmdArgs
If Cmd.ToLower = "/c" Then
StartClosed = True
Exit For
End If
Next
Application.Run(New AppContext(StartClosed))
End Sub
#End Region
我使用了传递命令行参数的变体。在这个演示中,应用程序通常会以一个常规窗口启动;命令行参数 **/c** 表示应用程序启动时是关闭的。该方法重新开启了视觉样式(稍后你会看到为什么它被禁用;我认为这是一个 VB 特有的步骤,C# 程序员可以忽略它),确定应用程序如何启动,然后通过调用 `Application.Run` 和 `AppContext` 的新实例来启动应用程序。
当创建 `AppContext` 时,`InitializeApp` 方法会被调用。
Public Sub InitializeApp()
'Initialize the menus
TimerMenu = New ToolStripMenuItem
mnuShow = New ToolStripMenuItem("Show application")
mnuSep1 = New ToolStripSeparator()
mnuExit = New ToolStripMenuItem("Exit application")
MainMenu = New ContextMenuStrip
MainMenu.Items.AddRange(New ToolStripItem() _
{TimerMenu, mnuShow, mnuSep1, mnuExit})
'Initialize the notification icon
Notify = New NotifyIcon
Notify.Icon = My.Resources.MainIcon
Notify.ContextMenuStrip = MainMenu
Notify.Text = "Formless tray application"
'Create the application form
AppForm = New MainForm
AppForm.Icon = My.Resources.MainIcon
End Sub
上下文菜单被构建,图标本身被实例化和初始化,应用程序窗体被创建并设置好。
`MainModule` 的其余部分是改变应用程序状态并处理图标上下文菜单单击事件的代码。
Public Sub CloseApp()
Notify.Visible = True
AppForm.Visible = False
End Sub
Public Sub ExitApp()
Application.Exit()
End Sub
Public Sub MinimizeApp()
AppForm.WindowState = FormWindowState.Minimized
End Sub
Public Sub RestoreApp()
Notify.Visible = False
AppForm.Visible = True
End Sub
Private Sub mnuExit_Click(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles mnuExit.Click
ExitApp()
End Sub
Private Sub mnuShow_Click(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles mnuShow.Click
RestoreApp()
End Sub
当应用程序关闭时,图标将保留在通知区域。右键单击它会弹出一个菜单,显示应用程序反馈(在此演示中是当前的日期和时间)以及显示应用程序窗体或完全退出应用程序的选项。
你会发现 `RestoreApp` 没有改变 `AppForm.WindowState`。如果窗体在关闭时是最大化或最小化状态,它将以最大化或最小化状态恢复。
请注意,设置 `AppForm.Visible = False` 仅隐藏了窗体;它不会停止其代码的执行。你的应用程序将继续在后台运行,而不会在任务栏上造成混乱。如果你的应用程序需要在“活动”和“后台”模式之间进行区分,你可以使用 `CloseApp` 和 `RestoreApp` 来切换状态。
窗体
演示的窗体非常基础。它有一个 `MenuStrip`,包含最小化、关闭和退出应用程序的选项,一个 `Timer`,一个名为 `TimeLabel` 的 `Label` 和一个 `TextBox`。单击菜单项将调用 `MainModule` 中的 `MinimizeApp`、`CloseApp` 或 `ExitApp` 方法。在窗体的构造函数中,`TimeLabel` 被设置为显示当前的日期和时间,并且计时器设置为每秒触发一次。当计时器响起时,`TimeLabel` 和 `TimerMenu` 都会更新为当前的日期和时间。

Public Class MainForm
#Region " Constructors "
Public Sub New()
InitializeComponent()
TimeLabel.Text = DateTime.Now.ToString
TimerMenu.Text = TimeLabel.Text
Timer1.Interval = 1000
Timer1.Start()
End Sub
#End Region
#Region " Event handlers "
Private Sub MainForm_FormClosing(ByVal sender As Object, _
ByVal e As System.Windows.Forms.FormClosingEventArgs) _
Handles Me.FormClosing
If e.CloseReason = CloseReason.UserClosing Then
e.Cancel = True
CloseApp()
End If
End Sub
Private Sub mnuFormClose_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles mnuFormClose.Click
CloseApp()
End Sub
Private Sub mnuFormExit_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles mnuFormExit.Click
ExitApp()
End Sub
Private Sub mnuFormMinimize_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles mnuFormMinimize.Click
MinimizeApp()
End Sub
#End Region
Private Sub Timer1_Tick(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Timer1.Tick
TimeLabel.Text = DateTime.Now.ToString
TimerMenu.Text = TimeLabel.Text
End Sub
End Class
让这一切工作的是 `FormClosing` 事件。`FormClosingEventArgs` 参数允许我们查看窗体关闭的原因。如果它是由于用户操作而关闭——也就是说,用户单击了右上角的“关闭窗体”按钮或选择了应用程序任务栏上下文菜单中的“关闭”——我们可以取消关闭,而是调用 `CloseApp`。
项目设置
由于 Visual Basic 代码的编译方式,VB 程序员在开始工作之前需要做一些额外的事情。进入“项目设置”窗体(在 Visual Studio 主菜单中选择“项目”,然后在底部选择“项目名称 属性”),然后选择“应用程序”选项卡。取消选中“启用应用程序框架”;这将告诉编译器你将自己管理基础架构。此操作的一个副作用是视觉样式会被禁用,这就是为什么我们在 `Sub Main` 中手动重新启用它的原因。在“启动对象”下拉列表中,选择 `Sub Main`。
最后说明
我希望你觉得这篇文章很有用。一如既往,我欢迎关于代码中错误和其他问题的评论,以及任何反馈。如果有什么有才华的人愿意将此代码翻译成 C# 并将其发布为一篇文章,请给我署名。
文章历史
- 版本 1 - 2010 年 8 月 25 日 - 初始发布