将 MenuItem 添加到控制台应用程序中 TrayIcon 的上下文菜单

Adding MenuItems to Contextmenu for a TrayIcon in a Console app

我制作了一个小的控制台应用程序,它将鼠标锁定在第一个屏幕上。
现在我想创建一个带有 ContextMenu 的 TrayIcon 来关闭应用程序。

在调试模式下,我可以看到 ContextMenu 有两个 Item,就像它应该的那样,但它不显示 ContextMenu。

我的代码:

static void GenerateTrayIcon()
{
    ContextMenu trayiconmenu = new ContextMenu();
    trayiconmenu.MenuItems.Add(0, new MenuItem("Show", new EventHandler(Show_Click)));
    trayiconmenu.MenuItems.Add(1, new MenuItem("Exit", new EventHandler(Exit_Click)));

    NotifyIcon TrayIcon = new NotifyIcon();
    TrayIcon.Icon = new Icon("Path to .ico");
    TrayIcon.Text = "Cursor is locked to primary screen";
    TrayIcon.Visible = true;
    TrayIcon.ContextMenu = trayiconmenu;
}

static void Exit_Click(object sender, EventArgs e)
{
    Environment.Exit(0);
}

static void Show_Click(object sender, EventArgs e)
{
    // Do something
}

制作NotifyIcon work, you have to start a Message Loop, usually calling Application.Run()。调用方法通常也标记为单线程([STAThread]).

大致就是这样。
▶ 当然你需要处理你创建的对象。在本例中,NotifyIcon 对象和 ContextMenu。您还可以在 Icon 对象上调用 Dispose(),以防它在内部 NativeWindow 中设置为 null

在此处的示例中,ConsoleNotifyIcon class 对象用于 运行 消息循环并接收 ContextMenu 项目鼠标事件。
在这种情况下,退出点击处理程序 向主线程发出信号 退出请求已排队。它还从通知区域中删除了 NotifyIcon。

然后主线程可以确认请求并终止。
它还确保在退出之前,NotifyIcon 已被释放。

▶ 您可以在 CloseRequest 事件处理程序中使用 Environment.Exit()
在这里,AppDomain.ProcessExit event is handled to respond to Environment.Exit() and SetConsoleCtrlHandler 处理其他退出原因(请参阅代码中的注释)。
在任何情况下,都会调用 CleanUp() 方法,以删除剩余的事件处理程序并处理 NotifyIcon 对象分配的资源。

private static readonly object closeLocker = new object();
private static ConsoleEventDelegate closeHandler;
private delegate bool ConsoleEventDelegate(ExitReason closeReason);

private enum ExitReason
{
    ControlC = 0,
    ControlBreak = 1,
    UserClose = 2,
    UserLogoff = 5,
    SystemShutdown = 6
}

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetConsoleCtrlHandler(ConsoleEventDelegate HandlerRoutine, bool Add);

[STAThread]
static void Main(string[] args)
{
    // Handles Close Button, CTRL-C, CTRL-Break, Logoff and ShutDown
    closeHandler = new ConsoleEventDelegate(ConsoleEventHandler);
    SetConsoleCtrlHandler(closeHandler, true);

    // Handles Environment.Exit()
    AppDomain.CurrentDomain.ProcessExit += OnProcessExit;

    // Add a handler to the NotifyIcon CloseRequest event
    ConsoleNotifyIcon.CloseRequest += NotifyIconCloseRequest;

    // Create the NotifyIcon Icon in the Tray Notification Area
    ConsoleNotifyIcon.GenerateTrayIcon();

    // [...]
    // Other processes

    Console.ReadLine();
    // Raises the ProcessExit event
    Environment.Exit(0);
}


// Event raised by the NotifyIcon's Exit routine.
// Causes OnProcessExit to fire
private static void NotifyIconCloseRequest(object sender, EventArgs e) => Environment.Exit(0);

// Fires when Environment.Exit(0) is called
private static void OnProcessExit(object sender, EventArgs e) => CleanUp();

// Handles - Console Close Button, Control-C, Control-Break 
//         - System Log-off event, System ShutDown event
static bool ConsoleEventHandler(ExitReason reason)
{
    SetConsoleCtrlHandler(closeHandler, false);
    CleanUp();
    return true;
}

// All Console Exit reasons end up here
private static void CleanUp()
{
    // This is called from a different Thread
    lock (closeLocker) {
        AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
        ConsoleNotifyIcon.CloseRequest -= NotifyIconCloseRequest;
        if (!ConsoleNotifyIcon.IsDisposed) {
            ConsoleNotifyIcon.Dispose();
        }
    }
}

ConsoleNotifyIcon class(NotifyIcon 处理程序):

using System.Threading.Tasks;
using System.Windows.Forms;

public class ConsoleNotifyIcon
{
    public static event EventHandler<EventArgs> CloseRequest;
    // Store these objects as private Fields
    private static NotifyIcon trayIcon;
    private static ContextMenu trayContextMenu;

    // The main public method starts a new Thread, in case a STA Thread is needed
    // If not, you can just Task.Run() it
    public static void GenerateTrayIcon()
    {
        var thread = new Thread(StartTrayIcon);
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
    }

    [STAThread]  // Reminder
    private static void StartTrayIcon() {
        trayContextMenu = new ContextMenu();
        trayContextMenu.MenuItems.Add(0, new MenuItem("Show", Show_Click));
        trayContextMenu.MenuItems.Add(1, new MenuItem("Exit", Exit_Click));

        trayIcon = new NotifyIcon() {
            ContextMenu = trayContextMenu
            Icon = [Some Icon],  // Possibly, use an Icon Resource
            Text = "Cursor is locked to primary screen",
            Visible = true,
        };

        // Setup completed. Starts the Message Loop
        Application.Run();
    }

    public static bool IsAppCloseRequest { get; private set; }

    public static bool IsDisposed { get; private set; }

    static void Exit_Click(object sender, EventArgs e) {

        // Sets the public property, it can be used to check the status
        IsAppCloseRequest = true;
        // Signals the Exit request, raising the CloseRequest event.  
        // The application may decide to delay the exit process, so calling Dispose()  
        // is handled by the subscribers of the event, as shown in the Console code 
        CloseRequest?.Invoke(null, EventArgs.Empty);
    }

    static void Show_Click(object sender, EventArgs e) {
        // Do something
    }

    public static void Dispose() {
        if (IsDisposed) return;
        Application.ExitThread();
        trayIcon?.Icon?.Dispose();
        trayIcon?.Dispose();
        trayContextMenu?.Dispose();
        IsDisposed = true;
    }
}