使用 WPF 窗体创建的 C# 系统托盘应用程序





5.00/5 (15投票s)
如何在 C# 和 WPF 中创建基本的系统托盘应用程序
引言
本文介绍了一个使用 C# 和 WPF 编写的简单系统托盘应用程序,该应用程序演示了系统托盘应用程序的典型功能。
示例代码控制一个模拟设备,该设备响应用户的菜单命令,在运行和非运行状态之间切换。
系统托盘应用程序实现了以下功能:
- 出现在系统托盘中的图标
- 用户右键/左键单击图标时显示的弹出菜单
- 由菜单命令启动的一组视图
- 设备状态更改时出现在系统托盘上方的气球文本
- 工具提示
- 根据设备状态更改的图标
菜单包含一组基本命令
- 显示应用程序信息
- 显示状态信息
- 启动模拟设备
- 停止模拟设备
- 退出系统托盘应用程序
代码提供了一个基本框架,您可以轻松修改以满足自己的需求,例如控制连接到 USB 端口的硬件设备。
架构故意保持简单,对象数量少,职责划分清晰。
- 应用程序上下文对象除了初始化应用程序外,几乎不做其他事情。
- 设备管理器对象封装了(模拟的)设备,并实现了一个允许客户端对象控制设备的接口。将接口与实现分离有很多好处,包括减少组件之间的耦合,轻松地在实现之间进行切换,以及允许客户端使用虚拟接口进行测试,而无需依赖实现。
- 视图管理器对象,负责管理用户界面。它拥有一个
NotifyIcon
对象,以及各种菜单和视图。它通过设备管理器接口来控制设备。 - 关于和状态视图使用 WPF 实现,遵循视图和视图模型模式,其中 UI 在 XAML 视图中描述,视图中显示的数据存储在视图模型对象中。在实际应用程序中,您通常会添加一个模型来包含遵循 MVVM 模式的源数据。
背景
要理解本文,您需要了解 .NET 和 WPF。
.NET 的 NotifyIcon
类使创建系统托盘应用程序变得容易,但它与 WPF 不兼容。因此,基于 NotifyIcon
类的系统托盘应用程序通常使用 WinForms 来实现视图和对话框。本文采用的替代方案是将 WPF 窗体放入单独的程序集中。
如果您愿意,可以替换 WPF 窗体为 WinForms,但我建议不要这样做:WPF 为用户界面提供了更丰富、更富有成效的开发环境。
代码
Main
函数首先检查应用程序是否已有正在运行的实例,如果有,则终止,因为一次只能运行一个实例。它通过创建一个固定名称的命名互斥体来检测另一个实例的存在。如果该互斥体已存在,则必须已有另一个实例正在运行。互斥体名称是程序集的 GUID,应避免与系统中其他命名互斥体发生冲突。
// Use the assembly GUID as the name of the mutex which we use to detect
// if an application instance is already running
bool createdNew = false;
string mutexName = System.Reflection.Assembly.GetExecutingAssembly().GetType().GUID.ToString();
using (System.Threading.Mutex mutex = new System.Threading.Mutex(false, mutexName, out createdNew))
{
if (!createdNew)
{
// Only allow one instance
return;
}
下一步是创建应用程序上下文实例。通常,应用程序会创建其主窗口对象并将其传递给 Application Run
方法。但是,我们不需要主窗口,因此我们改用应用程序上下文。
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
try
{
STAApplicationContext context = new STAApplicationContext();
Application.Run(context);
}
catch (Exception exc)
{
MessageBox.Show(exc.Message, "Error");
}
应用程序上下文继承自 ApplicationContext
类,负责初始化系统。它只有两个属性。
private ViewManager _viewManager;
private DeviceManager _deviceManager;
ViewManager
对象管理用户界面,并通过 IDeviceManager
接口与设备进行交互。
DeviceManager
类管理模拟设备。它实现了 IDeviceManager
接口。
应用程序上下文在其构造函数中初始化系统。
public STAApplicationContext()
{
_deviceManager = new DeviceManager();
_viewManager = new ViewManager(_deviceManager);
_deviceManager.OnStatusChange += _viewManager.OnStatusChange;
_deviceManager.Initialise();
}
它创建一个 DeviceManager
类的实例,然后创建一个 ViewManager
类的实例,并将 DeviceManager
类(因此是 IDeviceManager
接口)传递给它。
然后,它将 ViewManager
的 OnStatusChange
方法连接到 DeviceManager
实例公开的 OnStatusChange
事件。每当设备状态更改时,都会触发此事件。
IDeviceManager
接口定义了一组简单的命令和属性来控制(模拟的)设备。
public interface IDeviceManager
{
string DeviceName { get; }
DeviceStatus Status { get; }
List<KeyValuePair<string, bool>> StatusFlags { get; }
void Initialize();
void Start();
void Stop();
void Terminate();
}
上述接口由 DeviceManager
类实现。有关 DeviceManager
类的更多详细信息,请参阅示例代码。总而言之,它不过是一个模拟真实设备的空壳。
ViewManager
类在其构造函数中创建并初始化一个 NotifyIcon
实例。
public ViewManager(IDeviceManager deviceManager)
{
System.Diagnostics.Debug.Assert(deviceManager != null);
_deviceManager = deviceManager;
_components = new System.ComponentModel.Container();
_notifyIcon = new System.Windows.Forms.NotifyIcon(_components)
{
ContextMenuStrip = new ContextMenuStrip(),
Icon = SystemTrayApp.Properties.Resources.NotReadyIcon,
Text = "System Tray App: Device Not Present",
Visible = true,
};
_notifyIcon.ContextMenuStrip.Opening += ContextMenuStrip_Opening;
_notifyIcon.DoubleClick += notifyIcon_DoubleClick;
_notifyIcon.MouseUp += notifyIcon_MouseUp;
_aboutViewModel = new WpfFormLibrary.ViewModel.AboutViewModel();
_statusViewModel = new WpfFormLibrary.ViewModel.StatusViewModel();
_statusViewModel.Icon = AppIcon;
_aboutViewModel.Icon = _statusViewModel.Icon;
_hiddenWindow = new System.Windows.Window();
_hiddenWindow.Hide();
}
.NET 的 NotifyIcon
类实现了系统托盘图标。
上面的代码安装了系统托盘事件处理程序,用于上下文菜单打开、双击和鼠标向上事件。它还为两个视图(即关于视图和状态视图)创建了视图模型的实例。
ContextMenuStrip_Opening
方法在上下文菜单不存在时创建它,然后根据需要启用/禁用菜单项。
private void ContextMenuStrip_Opening(object sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel = false;
if (_notifyIcon.ContextMenuStrip.Items.Count == 0)
{
_startDeviceMenuItem = ToolStripMenuItemWithHandler(
"Start Device",
"Starts the device",
startStopReaderItem_Click);
_notifyIcon.ContextMenuStrip.Items.Add(_startDeviceMenuItem);
_stopDeviceMenuItem = ToolStripMenuItemWithHandler(
"Stop Device",
"Stops the device",
startStopReaderItem_Click);
_notifyIcon.ContextMenuStrip.Items.Add(_stopDeviceMenuItem);
_notifyIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator());
_notifyIcon.ContextMenuStrip.Items.Add(ToolStripMenuItemWithHandler
("Device S&tatus", "Shows the device status dialog", showStatusItem_Click));
_notifyIcon.ContextMenuStrip.Items.Add(ToolStripMenuItemWithHandler
("&About", "Shows the About dialog", showHelpItem_Click));
_notifyIcon.ContextMenuStrip.Items.Add(ToolStripMenuItemWithHandler
("Code Project &Web Site", "Navigates to the Code Project Web Site", showWebSite_Click));
_notifyIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator());
_exitMenuItem = ToolStripMenuItemWithHandler
("&Exit", "Exits System Tray App", exitItem_Click);
_notifyIcon.ContextMenuStrip.Items.Add(_exitMenuItem);
}
SetMenuItems();
}
当用户选择“设备状态”命令时,系统会调用 showStatusItem_Click
方法。
private void showStatusItem_Click(object sender, EventArgs e)
{
ShowStatusView();
}
private void ShowStatusView()
{
if (_statusView == null)
{
_statusView = new WpfFormLibrary.View.StatusView();
_statusView.DataContext = _statusViewModel;
_statusView.Closing += ((arg_1, arg_2) => _statusView = null);
_statusView.WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen;
_statusView.Show();
UpdateStatusView();
}
else
{
_statusView.Activate();
}
_statusView.Icon = AppIcon;
}
如果视图存在,代码只需激活它并设置图标。否则,它会创建一个状态视图,并对其进行初始化,包括添加对 Closing
事件的处理程序以及更新内容。
关于视图的代码非常相似,包含在示例代码中。
构建示例代码
示例代码是一个 Microsoft Visual Studio 2013 解决方案。
历史
- 2017 年 3 月 17 日:首次发布