如何从另一个应用程序关闭 WPF 系统托盘应用程序?

Posted

技术标签:

【中文标题】如何从另一个应用程序关闭 WPF 系统托盘应用程序?【英文标题】:How to close WPF system tray application from another app? 【发布时间】:2019-10-14 06:10:38 【问题描述】:

我有一个 无窗口 WPF 应用程序,它使用了 Philipp Sumi 的出色 NotifyIcon。我很乐意更换基于 WinForms 的旧版本,但是,我遇到了一个问题。

我的应用程序有卸载程序,它会在删除可执行文件之前关闭托盘应用程序。关闭的事情是通过向托盘应用程序进程发送 WM_CLOSE 消息来完成的。但是,使用 WinForms 收听此类消息相对容易 - 如何使用 WPF 来做到这一点,再说一遍,也许有更好的方法可以远程告诉我的 WPF 托盘应用程序关闭?我的意思是,还有什么?管道?

这不是重复的。 我的应用程序中没有“主窗口”。所以没有任何引用主窗口的方法。

我相信系统必须有办法告诉应用程序自行关闭,以防重启或关闭。这就是为什么我们会收到“这个和那个应用程序没有响应,您是否仍要关闭”或类似消息的原因。

这是我幼稚的做法:

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

不起作用。当然hwnd总是IntPtr.Zero,除非我创建一个窗口,显然我不想创建窗口。应用程序线程忽略 SC_CLOSE,所以这里也没有乐趣。

好的,我尝试创建隐形窗口。如果窗口的 ShowInTaskBar 设置为 true,则该方法有效。不好。

然后我从 System.Windows.Forms.NativeWindow 创建了一个海绵窗口。

当然,那个不可见的窗口完全能够接收 WM_CLOSE 和任何其他消息,但是,它没有设置为进程主窗口,所以我不能用我的卸载应用程序来定位它。

目前我没有想法。

【问题讨论】:

相关***.com/questions/15236912/…? WPF app does not respond to WM_CLOSE的可能重复 管道是相当多的代码来实现。消息队列非常简单,我会首先考虑这一点。只是卸载需要与 wpf 应用程序对话吗?和。是否总是在用户计算机上启动任何进程? @Andy:只卸载应用程序。该过程通过自动启动功能运行,因此它应该总是启动。顺便说一句,它不能被杀死,因为托盘中的图标将一直保留,直到用户尝试将鼠标光标放在它上面。 【参考方案1】:

在大多数情况下,我必须自己弄清楚。 方法如下。

首先:我们有流程。该进程没有主窗口,根本没有窗口,因此与窗口的所有通信都是不可行的。但是进程有线程。并且线程有它们的消息队列,虽然它可以通过 Win32 API 访问。

所以这里是接收端:

/// <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();
    

    // (...)


发送端:

/// <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");


背后的巫术魔法:P/Invoke 部分非常明显。更有趣的是 WPF Application.OnStartup 覆盖行为。

如果方法是同步的并且不退出,则应用程序挂起。 但是,如果它被标记为异步,则不必退出。它无限期地等待任何可以等待的东西。这正是我们所需要的,因为我们只能从主 UI 线程调用 Shutdown()。我们不能阻塞那个线程,所以我们必须等待另一个线程上的消息。我们将 WM_CLOSE 发送到所有进程线程,所以它会得到它。当它结束时,会从主线程调用Shutdown 方法,就是这样。

此解决方案的最佳之处在于它不需要 System.Windows.Forms 参考。

【讨论】:

干得好。感谢您的帖子。

以上是关于如何从另一个应用程序关闭 WPF 系统托盘应用程序?的主要内容,如果未能解决你的问题,请参考以下文章

WPF - 将应用程序从系统托盘置于最前面

WPF 在关闭程序了之后,托盘图标依然存在,请问怎么解决?

如何使 WPF 折叠到 Windows 服务框中

C# winform 缩小到托盘 无法关机?

只有托盘图标的 WPF 应用程序

vc 如何实现关闭时最小化到托盘