简体   繁体   中英

How to close WPF system tray application from another app?

I have a windowless WPF application using great NotifyIcon by Philipp Sumi. I'm gladly replacing my old WinForms based version, however, I have one problem.

My app has uninstaller that closes the tray application before removing the executable files. The closing thing is done with sending WM_CLOSE message to the tray application process. It's relatively easy to listen to such messages with WinForms, however - how to do it with WPF, and then again, maybe there is a better way of remotely telling my WPF tray application to shutdown? I mean, what else is there? Pipes?

THIS IS NOT A DUPLICATE. There is no "main window" in my app. So nothing referring to main windows will work.

I believe the system has to have a way to tell an app to close itself, in case of let's say restart or shutdown. That's why we get "this and that app is not responding, do you want to shutdown anyway" or similar messages.

Here's my naive approach:

using (var process = Process.GetProcessesByName("MyApp").FirstOrDefault()) {
    const uint WM_SYSCOMMAND = 0x0112;
    const uint SC_CLOSE = 0xF060;
    const uint WM_CLOSE = 0x0010;
    var hwnd = process.MainWindowHandle;
    NativeMethods.SendMessage(hwnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
    foreach (ProcessThread thread in process.Threads) {
        NativeMethods.PostThreadMessage(
            (uint)thread.Id,
            WM_SYSCOMMAND, 
            (IntPtr)SC_CLOSE,
            IntPtr.Zero
        );
    }
} 

Doesn't work. Of course hwnd is always IntPtr.Zero, unless I create a window, obviously I don't want to create window. The application threads ignore SC_CLOSE, so no joy here either.

OK, I tried to create invisible window. That approach works if the window has the ShowInTaskBar set to true. Not good.

Then I created a sponge window, from System.Windows.Forms.NativeWindow.

Of course that invisible window is perfectly capable of receiving WM_CLOSE and any other messages, however, it is not set as the process main window, so I cannot target it with my uninstall app.

Currently I'm out of ideas.

As most of the time, I had to figure it out myself. Here's how.

First: we have the process. The process has no main window, no windows at all, so all communication with windows is off the table. But the process has threads. And the threads have their message queue, though it's accessible with Win32 API.

So here's the receiving end:

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application {

    // (...)

    /// <summary>
    /// Sent as a signal that a window or an application should terminate.
    /// </summary>
    const uint WM_CLOSE = 0x0010;

    /// <summary>
    /// Retrieves a message from the calling thread's message queue. The function dispatches incoming sent messages until a posted message is available for retrieval.
    /// </summary>
    /// <param name="lpMsg">MSG structure that receives message information from the thread's message queue.</param>
    /// <param name="hWnd">A handle to the window whose messages are to be retrieved. The window must belong to the current thread. Use <see cref="IntPtr.Zero"/> to retrieve thread message.</param>
    /// <param name="wMsgFilterMin">The integer value of the lowest message value to be retrieved.</param>
    /// <param name="wMsgFilterMax">The integer value of the highest message value to be retrieved.</param>
    /// <returns>Non-zero for any message but WM_QUIT, zero for WM_QUIT, -1 for error.</returns>
    [DllImport("user32.dll")]
    static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);

    /// <summary>
    /// Waits indefinitely for a specific thread message.
    /// </summary>
    /// <param name="signal">Signal, message value.</param>
    private void WaitForSignal(uint signal) => GetMessage(out var msg, IntPtr.Zero, signal, signal);

    /// <summary>
    /// WPF application startup.
    /// </summary>
    /// <param name="e">Event arguments.</param>
    protected override async void OnStartup(StartupEventArgs e) {
        base.OnStartup(e);
        // ... initialization code ...
        await Task.Run(() => WaitForSignal(WM_CLOSE));
        Shutdown();
    }

    // (...)

}

Sending end:

/// <summary>
/// Demo.
/// </summary>
class Program {

    /// <summary>
    /// Sent as a signal that a window or an application should terminate.
    /// </summary>
    const uint WM_CLOSE = 0x0010;

    /// <summary>
    /// Posts a message to the message queue of the specified thread. It returns without waiting for the thread to process the message.
    /// </summary>
    /// <param name="threadId">The identifier of the thread to which the message is to be posted.</param>
    /// <param name="msg">The type of message to be posted.</param>
    /// <param name="wParam">Additional message-specific information.</param>
    /// <param name="lParam">Additional message-specific information.</param>
    /// <returns></returns>
    [return: MarshalAs(UnmanagedType.Bool)]
    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool PostThreadMessage(uint threadId, uint msg, IntPtr wParam, IntPtr lParam);

    /// <summary>
    /// Closes a windowless application pointed with process name.
    /// </summary>
    /// <param name="processName">The name of the process to close.</param>
    static void CloseWindowless(string processName) {
        foreach (var process in Process.GetProcessesByName(processName)) {
            using (process) {
                foreach (ProcessThread thread in process.Threads) PostThreadMessage((uint)thread.Id, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
            }
        }
    }

    /// <summary>
    /// Main application entry point.
    /// </summary>
    /// <param name="args">Command line arguments.</param>
    static void Main(string[] args) => CloseWindowless("MyAppName");

}

And the voodoo magic behind: P/Invoke part is pretty obvious. What's more interesting is WPF Application . OnStartup override behavior.

If the method is synchronous and does not exit, the application hangs. If it's marked asynchronous however, it doesn't have to exit. It waits indefinitely for anything awaitable. This is exactly what we needed, because we can invoke Shutdown() only from the main UI thread. We cannot block that thread, so we have to wait for messages on another thread. We send the WM_CLOSE to all process threads, so it will get it. When it ends, the Shutdown method is called from the main thread and that's it.

What's best in this solution it doesn't need System.Windows.Forms reference.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM