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