65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (15投票s)

2017 年 3 月 17 日

CPOL

4分钟阅读

viewsIcon

77354

downloadIcon

5555

如何在 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 接口)传递给它。

然后,它将 ViewManagerOnStatusChange 方法连接到 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 日:首次发布
© . All rights reserved.