Search code examples
c#.netwinformsconsole-applicationnotifyicon

Adding MenuItems to Contextmenu for a TrayIcon in a Console app


I made a little console app which locks the Mouse to the first screen.
Now I want to create a TrayIcon with a ContextMenu to close the application.

In debug-mode, I can see that the ContextMenu has two Items, just like it should, but it doesn't display the ContextMenu.

My code:

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
}

Solution

  • To make the NotifyIcon work, you have to start a Message Loop, usually calling Application.Run(). The calling method is also usually marked as single-threaded ([STAThread]).

    That's more or less just it.
    ▶ Of course you need to dispose of the objects you created. In this case, the NotifyIcon object and the ContextMenu. You can also call Dispose() on the Icon object, in case it's just set to null in the internal NativeWindow.

    In the example here, the ConsoleNotifyIcon class object is used to run the Message Loop and receive the ContextMenu items mouse events.
    In this case, the Exit click handler signals the main Thread that an exit request has been queued. It also removes the NotifyIcon from the Notification Area.

    The Main Thread can then acknowledge the request and terminate.
    It also makes sure, before exiting, that the NotifyIcon has been disposed.

    ▶ You can use Environment.Exit() in the CloseRequest event handler.
    Here, the AppDomain.ProcessExit event is handled to respond to Environment.Exit() and SetConsoleCtrlHandler handles other exit causes (see the notes in code).
    In any case, the CleanUp() method is called, to remove remaining event handlers and dispose of the resources allocated by the NotifyIcon object.

    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 Handler):

    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 STA Thread
        // In this simple case, you could also 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;
        }
    }