I am trying to make a WinUI3 app which includes a tray icon. When I close the app, it should go to the system tray and run in the background. When I right-click on the tray icon, two menu options should appear- 'Open' and 'Exit'. When 'Open' is clicked, the app should re-open and when 'Exit' is clicked, the app should close.
I used Win32 APIs, and my app creates a system tray icon, and right-click makes the menu appear. However, the menu items are not responsive and nothing happens when I click on the menu items 'Open' and 'Exit'. My tray Icon
MainWindow.xaml.cs
public sealed partial class MainWindow : Window
{
private const uint WM_LBUTTONUP = 0x0202; // Left button up
private const uint WM_COMMAND = 0x0111; // Left button up
private const int WM_SYSCOMMAND = 0x0112; //
private const int WM_APP = 0x8000; // Custom Windows message
private const uint NIM_ADD = 0x00000000;
private const uint NIM_DELETE = 0x00000002;
private const uint NIF_MESSAGE = 0x00000001;
private const uint NIF_ICON = 0x00000002;
private const uint NIF_TIP = 0x00000004;
public const int LR_LOADFROMFILE = 0x00000010;
private const int ID_TRAY_EXIT = 0x2000;
private const int ID_TRAY_OPEN = 0x2001;
private const int WS_MAXIMIZEBOX = 0x00010000;
private const int GWL_STYLE = -16;
public const int IMAGE_ICON = 1;
private IntPtr _prevWndProc;
private delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
private WndProcDelegate _wndProcDelegate;
private IntPtr _hwnd;
[DllImport("user32.dll")]
private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, WndProcDelegate newProc);
[DllImport("user32.dll")]
private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, int newProc);
[DllImport("user32.dll")]
private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr CreatePopupMenu();
[DllImport("user32.dll")]
private static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool AppendMenu(IntPtr hMenu, uint uFlags, uint uIDNewItem, string lpNewItem);
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowLongPtr(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool DestroyMenu(IntPtr hMenu);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool TrackPopupMenu(IntPtr hMenu, uint uFlags, int x, int y, int nReserved, IntPtr hWnd, IntPtr prcRect);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowLongPtr(HandleRef hWnd, int nIndex, IntPtr dwNewLong);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern IntPtr LoadImage(IntPtr hInst, string lpszName, UInt32 uType, int cxDesired, int cyDesired, UInt32 fuLoad);
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
private static extern bool Shell_NotifyIcon(uint dwMessage, [In] ref NOTIFYICONDATA lpdata);
public MainWindow()
{
this.InitializeComponent();
_wndProcDelegate = new WndProcDelegate(WndProc);
_hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
_prevWndProc = SetWindowLongPtr(_hwnd, GWLP_WNDPROC, _wndProcDelegate); // GWLP_WNDPROC
CreateNotifyIcon();
this.Closed += MainWindow_Closed;
}
private const int GWLP_WNDPROC = -4;
private void myButton_Click(object sender, RoutedEventArgs e)
{
HiddenButton.Content = "Clicked";
}
private void CreateNotifyIcon()
{
NOTIFYICONDATA notifyIconData = new NOTIFYICONDATA
{
cbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONDATA)),
hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this), // Correctly obtain the window handle
uID = 1,
uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP,
uCallbackMessage = WM_APP + 1, // Custom message ID
szTip = "WinUI 3 System Tray Sample",
hIcon = LoadImage(IntPtr.Zero, @".\Properties\Icon\logo.ico", IMAGE_ICON, 32, 32, LR_LOADFROMFILE) // Extracting icon from the executable
};
Shell_NotifyIcon(NIM_ADD, ref notifyIconData); // NIM_ADD to add the icon
}
private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
Debug.WriteLine($"msg: {msg}, lParam: {lParam}, wParam: {wParam & 0xFFFF}");
if( msg == WM_APP +1)
{
if (lParam.ToInt32() == WM_LBUTTONUP)
{
NotifyIcon_Click(this, EventArgs.Empty);
}
else if (lParam.ToInt32() == 0x0205) // WM_RBUTTONUP (right button up)
{
ShowContextMenu();
}
Debug.WriteLine($"msg: {msg}, lParam: {lParam.ToInt32()}");
return IntPtr.Zero;
}
else if(msg == WM_COMMAND)
{
int commandID = wParam.ToInt32() & 0xFFFF;
if(commandID == ID_TRAY_OPEN)
{
NotifyIcon_Click(this, EventArgs.Empty);
}
else if (commandID == ID_TRAY_EXIT) {
Application.Current.Exit();
}
Debug.WriteLine($"msg: {msg}, lParam: {lParam}, wParam: {wParam.ToInt32() & 0xFFFF}");
}
else if(msg == WM_SYSCOMMAND)
{
int commandID = wParam.ToInt32() & 0xFFFF;
if(commandID == ID_TRAY_OPEN)
{
NotifyIcon_Click(this, EventArgs.Empty);
}
else if (commandID == ID_TRAY_EXIT) {
Application.Current.Exit();
}
Debug.WriteLine($"msg: {msg}, lParam: {lParam}, wParam: {wParam.ToInt32() & 0xFFFF}");
}
return CallWindowProc(_prevWndProc, hWnd, msg, wParam, lParam);
}
private void ShowContextMenu()
{
// Get the current cursor position
if (GetCursorPos(out POINT cursorPos))
{
IntPtr hMenu = CreatePopupMenu();
AppendMenu(hMenu, 0x0, ID_TRAY_OPEN, "Open");
AppendMenu(hMenu, 0x0, ID_TRAY_EXIT, "Exit");
SetForegroundWindow(hMenu);
// Show the menu at the current cursor position
TrackPopupMenu(hMenu, 0x100, cursorPos.X, cursorPos.Y, 0, _hwnd, IntPtr.Zero);
DestroyMenu(hMenu);
}
}
private void MainWindow_Closed(object sender, WindowEventArgs e)
{
e.Handled = true;
this.AppWindow.Hide();
// Show notification icon in the system tray
}
private void NotifyIcon_Click(object sender, EventArgs e)
{
this.AppWindow.Show();
this.Activate();
}
public void ShowWindow()
{
this.AppWindow.Show();
this.Activate();
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct NOTIFYICONDATA
{
public uint cbSize;
public IntPtr hWnd;
public uint uID;
public uint uFlags;
public uint uCallbackMessage;
public IntPtr hIcon;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string szTip;
public uint dwState;
public uint dwStateMask;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string szInfo;
public uint uTimeoutOrVersion;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string szInfoTitle;
public uint dwInfoFlags;
public Guid guidItem;
public IntPtr hBalloonIcon;
}
}
In the WndProc method, I never receive the message WM_COMMAND and WM_SYSCOMMAND when dealing with the tray and the menu items. I also expect the menu disappear when I click anywhere else and need to set an icon inside the tray Icon (Currently it is empty). I could not run H.NotifyIcon and currently do not want to use that as it is incomplete.
Can anyone help me with how I can make a proper system tray icon in WinUI3?
According to the TrackPopupMenu document
If you specify TPM_RETURNCMD(100) in the uFlags parameter, the return value is the menu-item identifier of the item that the user selected.
So you can handle the user's click results in your ShowContextMenu()
.
"I also expect the menu disappear when I click anywhere"
This is also explained in this document
To display a context menu for a notification icon, the current window must be the foreground window before the application calls TrackPopupMenu or TrackPopupMenuEx. Otherwise, the menu will not disappear when the user clicks outside of the menu or the window that created the menu (if it is visible). If the current window is a child window, you must set the (top-level) parent window as the foreground window.
SetForegroundWindow(_hwnd);
uint nCmd = TrackPopupMenu(hMenu, 0x100, cursorPos.X, cursorPos.Y, 0, _hwnd, IntPtr.Zero);
PostMessage(_hwnd, 0x0000, IntPtr.Zero, IntPtr.Zero);
"else and need to set an icon inside the tray Icon (Currently it is empty)."
Please right-click your image, select Properties, and check whether the "Build Action" is "Content".
Please check whether your image path is correct.
[DllImport("user32.dll", SetLastError = true)]
private static extern uint TrackPopupMenu(IntPtr hMenu, uint uFlags, int x, int y, int nReserved, IntPtr hWnd, IntPtr prcRect);
private void CreateNotifyIcon()
{
NOTIFYICONDATA notifyIconData = new NOTIFYICONDATA
{
cbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONDATA)),
hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this), // Correctly obtain the window handle
uID = 1,
uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP,
uCallbackMessage = WM_APP + 1, // Custom message ID
szTip = "WinUI 3 System Tray Sample",
hIcon = LoadImage(IntPtr.Zero, @"Assets\GalleryIcon.ico", IMAGE_ICON, 32, 32, LR_LOADFROMFILE) // Extracting icon from the executable
};;
Shell_NotifyIcon(NIM_ADD, ref notifyIconData); // NIM_ADD to add the icon
}
private void ShowContextMenu()
{
// Get the current cursor position
if (GetCursorPos(out POINT cursorPos))
{
IntPtr hMenu = CreatePopupMenu();
AppendMenu(hMenu, 0x0, ID_TRAY_OPEN, "Open");
AppendMenu(hMenu, 0x0, ID_TRAY_EXIT, "Exit");
SetForegroundWindow(_hwnd);
// Show the menu at the current cursor position
//TPM_RETURNCMD = 100
uint nCmd = TrackPopupMenu(hMenu, 0x100, cursorPos.X, cursorPos.Y, 0, _hwnd, IntPtr.Zero);
PostMessage(_hwnd, 0x0000, IntPtr.Zero, IntPtr.Zero);
if (nCmd != 0)
{
if (nCmd == ID_TRAY_OPEN)
{
NotifyIcon_Click(this, EventArgs.Empty);
}
else if (nCmd == ID_TRAY_EXIT)
{
Application.Current.Exit();
}
}
DestroyMenu(hMenu);
}
}