Win32Api -- 使應用Always on top的幾種方法

louzi發表於2021-06-25

本文介紹幾種使應用一直置於頂層的方法。

問題描述

一般情況下,想要將應用置於頂層,設定其TopMost屬性為true即可。對於多個設定了TopMost屬性的應用,後啟用的在上面。

但有的應用,比如全域性的快捷操作工具條,它需要在所有應用之上,即使是設定了TopMost的應用。

解決思路

注意:使某個應用永遠不會被其它應用覆蓋,這本身是個偽命題。因為假如有兩個程式(A和B)這樣做,拖動兩個視窗使它們重疊,這兩個視窗中的一個必須在另一個之上,這在邏輯上是互相矛盾的。

所以應該儘量避免這種情況,如果非要這樣做,本文提供如下幾種辦法實現(不要將兩個這樣的應用重疊,否則會不停將置頂)。

首先,該應用程式需要設定其TopMost屬性為true,這樣普通視窗本身就會在它下面。本文主要討論該視窗如何置於設定了TopMost屬性的視窗之上。

方案一:捕獲WM_WINDOWPOSCHANGING訊息

我們知道,使用Win32的SetWindowPos介面可以改變視窗的Z Order,可以猜測,當另外一個應用置頂時,我們的應用會改變其Z Order,因此,我們可以嘗試捕獲WM_WINDOWPOSCHANGING訊息。

當視窗的大小、位置、Z序改變時,視窗會接收到WM_WINDOWPOSCHANGING訊息,我們可以使用WndProc處理視窗訊息。當捕獲到該訊息時,我們可以嘗試將應用再次置頂。關鍵程式碼如下,測試可行,但不確定是否有副作用:

/// <summary>
/// 方案一:捕獲WM_WINDOWPOSCHANGING訊息,若無SWP_NOZORDER標誌,則置頂
/// </summary>
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    switch (msg)
    {
        case Win32Api.WM_WINDOWPOSCHANGING:
            Win32Api.WINDOWPOS wp = (Win32Api.WINDOWPOS)Marshal.PtrToStructure(
                lParam, typeof(Win32Api.WINDOWPOS));
            if ((wp.flags & Win32Api.SWP_NOZORDER) == 0)
                _ = SetTopMostLater(); // 不使用棄元編譯器會發出警告
            break;
    }

    return IntPtr.Zero;
}

private async Task SetTopMostLater()
{
    await Task.Delay(300);
    var interopHelper = new WindowInteropHelper(this);
    Win32Api.SetWindowPos(interopHelper.Handle, Win32Api.HWND_TOPMOST, 0, 0, 0, 0, Win32Api.TOPMOST_FLAGS);
}

方案二:迴圈置頂

這個是比較容易想到的一個方案,每隔一定的時間給應用設定下TopMost,該方案也是可行的:

/// <summary>
/// 方案二:迴圈置頂
/// </summary>
/// <returns></returns>
private async Task SetTopMostLoop()
{
    while (true)
    {
        await Task.Delay(2000);
        var interopHelper = new WindowInteropHelper(this);
        Win32Api.SetWindowPos(interopHelper.Handle, Win32Api.HWND_TOPMOST, 0, 0, 0, 0, Win32Api.TOPMOST_FLAGS);
    }
}

方案三:使用鉤子

思考一下,其實大部分情況下,使用滑鼠或鍵盤等其它輸入裝置才會導致視窗的置頂被搶,因此可以使用全域性鉤子捕獲輸入事件,然後進行處理。

該方案是存在瑕疵的,因為存在不使用輸入裝置開啟某個應用的情況,這種情況下置頂效果就會被新開啟的置頂應用搶佔。

// 方案三:當滑鼠按下時置頂(僅考慮了滑鼠)
private void MouseHook_OnMouseActivity(object sender, System.Windows.Forms.MouseEventArgs e)
{
    Console.WriteLine("mouse down......");
    _ = SetTopMostLater();
}

private MouseHook _mouseHook;

最後,本文是我對該問題想到的一些解決方案,Windows系統的工作管理員可以執行在所有應用的最上層,也許微軟正是考慮到上文提到的偽命題,因此沒有開放該介面吧,瞭解原理的小夥伴歡迎討論。

本文三種方案的完整demo見GitHub,可以參考的連結(關於該話題的討論較老了):連結一連結二

相關文章