Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

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.

An EventHandler shouldn't be needed from what I've read so far.

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
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
188 views
Welcome To Ask or Share your Answers For Others

1 Answer

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 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;
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...