Search code examples
c#wpfnotificationssystem-tray

What is causing my NotifyIcon to hide after Alt+F4?


I have A WPF app using a winforms NotifyIcon for showing a context menu on the tray. When I perform the following steps the icon disappears.

  1. Right click on the notify icon in the tray
  2. Select a context menu item that displays a modal dialog
  3. Dismiss that dialog
  4. Press Alt+F4

Here is a minimal example where I see this bug.

XAML:

<Window x:Class="killtrayicon.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:killtrayicon"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <Button Content="button" Click="Button_Click"/>
</Grid>
</Window>

Code behind:

namespace killtrayicon
{
    using System.Windows;

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private System.Windows.Forms.NotifyIcon notifyIcon = new System.Windows.Forms.NotifyIcon();

        public MainWindow()
        {
            InitializeComponent();

            notifyIcon.Icon = Properties.Resources.icon;
            notifyIcon.Visible = true;
            notifyIcon.Text = "test";
            notifyIcon.ContextMenu = new System.Windows.Forms.ContextMenu();
            notifyIcon.ContextMenu.MenuItems.Add("click", (s, e) =>
            {
                MessageBox.Show("menu");
            });
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            notifyIcon.Icon = Properties.Resources.icon;
        }
    }
}

Clicking the button in my main window resets the icon and the notify icon appears again. So the notify icon itself has not been removed. Inspecting the instance of NotifyIcon shows it is still visible before resetting the icon and the Icon property is pointing to a valid ICO in my resources.

I suspect that the context menu is the issue because if I show a modal dialog via clicking on the tray icon, this issue doesn't occur.

How do I get the NotifyIcon to not respond to Alt+F4?

Edit: This question is a duplicate of this question but that question has no sample code to reproduce the issue (dead link), the link to the issue submitted to Microsoft is also a dead link, and there's no accepted answer with an actual solution.


Solution

  • I discovered the solution. The NotifyIcon creates a hidden NativeWindow as a recipient for window messages generated by the icon created by Shell_NotifyIcon. That window is using the default window proc which handles Alt+F4 like any other window. By turning it into WM_CLOSE. You need to use Win32 API's to subclass the HWND in that NativeWindow, intercept that WM_CLOSE, and ignore it.

    First add some Win32 methods from comctl32.dll:

    public static class Comctl32
    {
        public const string DLL = "comctl32.dll";
    
        public const uint WM_CLOSE = 0x0010;
        public const uint WM_NCDESTROY = 0x0082;
    
        public delegate IntPtr SubclassWndProc(IntPtr hWnd, uint uMsg, UIntPtr wParam, UIntPtr lParam, UIntPtr uIdSubclass, UIntPtr dwRefData);
    
        [DllImport(DLL, CharSet = CharSet.Auto, SetLastError = true, CallingConvention = CallingConvention.StdCall)]
        public static extern bool SetWindowSubclass(
            [param: In]
                IntPtr hWnd,
            [param: In]
                SubclassWndProc pfnSubclass,
            [param: In]
                UIntPtr uIdSubclass,
            [param: In]
                UIntPtr dwRefData);
    
        [DllImport(DLL, CharSet = CharSet.Auto, SetLastError = true, CallingConvention = CallingConvention.StdCall)]
        public static extern bool RemoveWindowSubclass(
            [param: In]
                IntPtr hWnd,
            [param: In]
                SubclassWndProc pfnSubclass,
            [param: In]
                UIntPtr uIdSubclass);
    
        [DllImport(DLL, CharSet = CharSet.Auto, SetLastError = true, CallingConvention = CallingConvention.StdCall)]
        public static extern IntPtr DefSubclassProc(
            [param: In]
                IntPtr hWnd,
            [param: In, MarshalAs(UnmanagedType.U4)]
                uint uMsg,
            [param: In]
                UIntPtr WPARAM,
            [param: In]
                UIntPtr LPARAM);
    }
    

    Then add code to your NotifyIcon to subclass the hidden tray icon window. You'll need to use reflection to get at the window because it's not public:

    private Native.Comctl32.SubclassWndProc subclassWndProc;
    
    ...
    
    // Get the HWND from the notify icon
    Type notifyIconType = typeof(System.Windows.Forms.NotifyIcon);
    BindingFlags hidden = BindingFlags.NonPublic | BindingFlags.Instance;
    var window = notifyIconType.GetField("window", hidden).GetValue(this.notifyIcon) as System.Windows.Forms.NativeWindow;
    
    // Inject our window proc to intercept window messages
    this.subclassWndProc = this.TrayWndProc;
    Native.Comctl32.SetWindowSubclass(window.Handle, this.subclassWndProc, UIntPtr.Zero, UIntPtr.Zero);
    

    Then intercept the WM_CLOSE to ignore Alt+F4. We also make sure to un-subclass on WM_NCDESTROY:

    private IntPtr TrayWndProc(IntPtr hWnd, uint uMsg, UIntPtr wParam, UIntPtr lParam, UIntPtr uIdSubclass, UIntPtr dwRefData)
    {
        switch (uMsg)
        {
            // Ignore the close message to avoid Alt+F4 killing the tray icon
            case Native.Comctl32.WM_CLOSE:
                return IntPtr.Zero;
    
            // Clean up subclassing
            case Native.Comctl32.WM_NCDESTROY:
                Native.Comctl32.RemoveWindowSubclass(hWnd, this.subclassWndProc, UIntPtr.Zero))
                break;
        }
    
        // Invoke the default window proc
        return Native.Comctl32.DefSubclassProc(hWnd, uMsg, wParam, lParam);
    }