.NET 5 托盘图标在 C# Windows 服务中的使用

.NET 5 Tray Icon Usage in C# Windows Service

我搜索了在 .NET 环境中处理 windows 系统/托盘图标的当前最佳实践,但没有找到任何最新信息。

考虑通常的 .NET 5 项目配置:

<PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

使用以下代码 (Program.cs):

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<TrayIconService>();
    })
    .Build()
    .Run();

class TrayIconService : IHostedService, IAsyncDisposable
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // what is the recommended way to create a windows tray icon in .NET 5?
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        await this.DisposeAsync();
    }

    public async ValueTask DisposeAsync()
    {
        // and how do I close the tray icon and dispose related resources afterwards?
    }
}

你能帮我在 C# 中实现一个简单的 'Hello World' 上下文菜单 windows 系统托盘图标 and/or 给我一些关于最先进的尝试图标用法的文档?

IHostedService 的实施是否是最好的考虑?我如何引用 windows API?我需要一个 net5.0-windows 项目吗?

提前致谢!

我相信你现在已经找到了答案,但是没有“解决方案”,而且有很多观点,所以让我给出一个如何解决这个问题的选项。

这实际上是一个很常见的架构,windows 就像您使用新的 Windows 11 MAUI API 其他技术一样,但是,用户并没有跳上“磁贴”设计和时间线充满点击诱饵,不是与用户“交谈”的可靠方式。

有几种方法可以做到这一点,您可以在服务代码中启动托盘图标,使其成为 windows 唯一服务

基本上,您正在考虑将您的服务与 System.Windows.Forms.NotifyIcon 和 TrayIcon.Visible 属性

相结合

使用 NotifyIcon,您可以执行以下操作:

class MyTray:IDisposable
{
   NotifyIcon ni;//needs disposed
    public ()
    {
       ni= new NotifyIcon()
       //use a helper class to generate a context menu for the tray icon if there is a menu like start, stop ...
       ni.ContextMenuStrip = new MyContextMenus(menuDataContext).Create();
     }

}

然后当你调用它时:

ni.Icon = Resources.Icon_red;
ni.Text = "Some ballon Text";

//使托盘图标可见 ni.Visible = 真; 用户可以与托盘菜单上的图标进行交互 这里是 MyContextMenu(backingField) 创建的示例:

public ContextMenuStrip Create()
{
    // Add the default menu options.
    menu = new ContextMenuStrip();
    ToolStripMenuItem item;
    ToolStripSeparator sep;

    item = new ToolStripMenuItem
    {
        Text = "License",
        Image = Resources.contract
    };
    item.Click += new EventHandler(License_Click);
    menu.Items.Add(item);


    // About.
    item = new ToolStripMenuItem
    {
        Text = "Service Status",
        Image = Resources.data_green1
    };
    item.Click += new EventHandler(Status_Click);

    menu.Items.Add(item);

    // Separator.
    sep = new ToolStripSeparator();
    menu.Items.Add(sep);

    //rule engine editor
    item = new ToolStripMenuItem
    {
        Text = "Rule Engine Editor",
        Image = Resources.data_edit1
    };

    item.Click += new System.EventHandler(Editor_Click);

    menu.Items.Add(item);

    // Separator.
    sep = new ToolStripSeparator();
    menu.Items.Add(sep);
    // Exit.
    item = new ToolStripMenuItem
    {
        Name = "mnuClose",
        Text = "Close",
        Image = Resources.data_down
    };
    item.Click += new EventHandler(Exit_Click);

    menu.Items.Add(item);
    return menu;
}

或解耦它,就像在这个示例中一样,服务可以在任何支持 .net 的 OS 上,并通过 ProtoBuf、Sockets WCF 或命名管道等协议进行通信。

也许是一种“更好”的方法

看看this article

此示例使用名称管道(网络连接)与使用 NuGet 包和 WPF 作为表示平台的应用程序进行交互。

服务器像这样与任何收听 Pipe 的人交谈:

using H.Pipes;
using H.Pipes.Args;
using NamedPipesSample.Common;

namespace NamedPipesSample.WindowsService
{
    public class NamedPipesServer : IDisposable
    {
        const string PIPE_NAME = "samplepipe";

        private PipeServer<PipeMessage> server;

        public async Task InitializeAsync()
        {
            server = new PipeServer<PipeMessage>(PIPE_NAME);

            server.ClientConnected += async (o, args) => await OnClientConnectedAsync(args);
            server.ClientDisconnected += (o, args) => OnClientDisconnected(args);
            server.MessageReceived += (sender, args) => OnMessageReceived(args.Message);
            server.ExceptionOccurred += (o, args) => OnExceptionOccurred(args.Exception);

            await server.StartAsync();
        }

        private void OnClientConnected(ConnectionEventArgs<PipeMessage> args)
        {
            Console.WriteLine($"Client {args.Connection.Id} is now connected!");

            await args.Connection.WriteAsync(new PipeMessage
            {
                Action = ActionType.SendText,
                Text = "Hi from server"
            });
        }

        private void OnClientDisconnected(ConnectionEventArgs<PipeMessage> args)
        {
            Console.WriteLine($"Client {args.Connection.Id} disconnected");
        }

        //...
    }
}

如果您按照示例进行操作,充当托盘图标的 WPF 应用程序将像这样“下降”:

public async Task InitializeAsync()
{
    if (client != null && client.IsConnected)
        return;

    client = new PipeClient<PipeMessage>(pipeName);
    client.MessageReceived += (sender, args) => OnMessageReceived(args.Message);
    client.Disconnected += (o, args) => MessageBox.Show("Disconnected from server");
    client.Connected += (o, args) => MessageBox.Show("Connected to server");
    client.ExceptionOccurred += (o, args) => OnExceptionOccurred(args.Exception);

    await client.ConnectAsync();

    await client.WriteAsync(new PipeMessage
    {
        Action = ActionType.SendText,
        Text = "Hello from client",
    });
}