Как закрыть приложение в системном трее WPF из другого приложения? - PullRequest
1 голос
/ 14 октября 2019

У меня есть приложение без окон WPF, использующее 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 и любые другие сообщения, однако ононе установлено в качестве основного окна процесса, поэтому я не могу настроить его для своего приложения удаления.

В настоящее время у меня нет идей.

1 Ответ

0 голосов
/ 15 октября 2019

Как и в большинстве случаев, мне пришлось самому в этом разобраться. Вот как.

Во-первых: у нас есть процесс. У этого процесса нет главного окна, вообще нет окон, поэтому вся связь с окнами не обсуждается. Но у процесса есть потоки. И потоки имеют свою очередь сообщений, хотя она доступна с 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 поведение переопределения.

Если метод является синхронным и не завершается, приложение зависает. Однако, если он помечен как асинхронный, выход не требуется. Он ждет бесконечно чего-нибудь ожидаемого. Это именно то, что нам нужно, потому что мы можем вызывать Shutdown() только из основного потока пользовательского интерфейса. Мы не можем заблокировать этот поток, поэтому нам приходится ждать сообщений в другом потоке. Мы отправляем WM_CLOSE всем потокам процесса, чтобы он его получил. Когда он заканчивается, метод Shutdown вызывается из основного потока и все.

Что лучше в этом решении, ему не нужна ссылка System.Windows.Forms.

...