WPF 穩定的全屏化視窗方法

来源:https://www.cnblogs.com/lindexi/p/18186221
-Advertisement-
Play Games

1. 案例 成某三甲醫預約系統, 該項目在2024年初進行上線測試,在正常運行了兩天後,業務系統報錯:The connection pool has been exhausted, either raise MaxPoolSize (currently 800) or Timeout (curren ...


本文來告訴大家在 WPF 中,設置視窗全屏化的一個穩定的設置方法。在設置視窗全屏的時候,經常遇到的問題就是應用程式雖然設置最大化加無邊框,但是此方式經常會有任務欄冒出來,或者說視窗沒有貼屏幕的邊。本文的方法是基於 Win32 的,由 lsj 提供的方法,當前已在 1000 多萬台設備上穩定運行超過三年時間,只有很少的電腦才偶爾出現任務欄不消失的情況

本文的方法核心方式是通過 Hook 的方式獲取當前視窗的 Win32 消息,在消息裡面獲取顯示器信息,根據獲取顯示器信息來設置視窗的尺寸和左上角的值。可以支持在全屏,多屏的設備上穩定設置全屏。支持在全屏之後,視窗可通過 API 方式(也可以用 Win + Shift + Left/Right)移動,調整大小,但會根據目標矩形尋找顯示器重新調整到全屏狀態

設置全屏在 Windows 的要求就是覆蓋屏幕的每個像素,也就是要求視窗蓋住整個屏幕、視窗沒有WS_THICKFRAME樣式、視窗不能有標題欄且最大化

使用本文提供的 FullScreenHelper 類的 StartFullScreen 方法即可進入全屏。進入全屏的視窗必須具備的要求如上文所述,不能有標題欄。如以下的演示例子,設置視窗樣式 WindowStyle="None" 如下麵代碼

<Window x:Class="KenafearcuweYemjecahee.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:KenafearcuweYemjecahee"
        mc:Ignorable="d" WindowStyle="None"
        Title="MainWindow" Height="450" Width="800"/>

視窗樣式不是強行要求,可以根據自己的業務決定。但如果有視窗樣式,那將根據視窗的樣式決定全屏的行為。我推薦預設設置為 WindowStyle="None" 用於解決預設的視窗沒有貼邊的問題

為了演示如何調用全屏方法,我在視窗添加一個按鈕,在點擊按鈕時,在後臺代碼進入或退出全屏

    <ToggleButton HorizontalAlignment="Center" VerticalAlignment="Center" Click="Button_OnClick">全屏</ToggleButton>

以下是點擊按鈕的邏輯

        private void Button_OnClick(object sender, RoutedEventArgs e)
        {
            var toggleButton = (ToggleButton)sender;

            if (toggleButton.IsChecked is true)
            {
                FullScreenHelper.StartFullScreen(this);
            }
            else
            {
                FullScreenHelper.EndFullScreen(this);
            }
        }

本文其實是將原本團隊內部的邏輯抄了一次,雖然我能保證團隊內的版本是穩定的,但是我不能保證在抄的過程中,我寫了一些逗比邏輯,讓這個全屏代碼不穩定

以下是具體的實現方法,如不想瞭解細節,那請到本文最後拷貝代碼即可

先來聊聊 StartFullScreen 方法的實現。此方法需要實現讓沒有全屏的視窗進入全屏,已進入全屏的視窗啥都不做。在視窗退出全屏時,還原進入全屏之前的視窗的狀態。為此,設置兩個附加屬性,用來分別記錄視窗全屏前位置和樣式的附加屬性,在進入全屏視窗的方法嘗試獲取視窗信息設置到附加屬性

        /// <summary>
        /// 用於記錄視窗全屏前位置的附加屬性
        /// </summary>
        private static readonly DependencyProperty BeforeFullScreenWindowPlacementProperty =
            DependencyProperty.RegisterAttached("BeforeFullScreenWindowPlacement", typeof(WINDOWPLACEMENT?),
                typeof(Window));

        /// <summary>
        /// 用於記錄視窗全屏前樣式的附加屬性
        /// </summary>
        private static readonly DependencyProperty BeforeFullScreenWindowStyleProperty =
            DependencyProperty.RegisterAttached("BeforeFullScreenWindowStyle", typeof(WindowStyles?), typeof(Window));

        public static void StartFullScreen(Window window)
        {
            //確保不在全屏模式
            if (window.GetValue(BeforeFullScreenWindowPlacementProperty) == null &&
                window.GetValue(BeforeFullScreenWindowStyleProperty) == null)
            {
                var hwnd = new WindowInteropHelper(window).EnsureHandle();
                var hwndSource = HwndSource.FromHwnd(hwnd);

                //獲取當前視窗的位置大小狀態並保存
                var placement = new WINDOWPLACEMENT();
                placement.Size = (uint) Marshal.SizeOf(placement);
                Win32.User32.GetWindowPlacement(hwnd, ref placement);
                window.SetValue(BeforeFullScreenWindowPlacementProperty, placement);

                //獲取視窗樣式
                var style = (WindowStyles) Win32.User32.GetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE);
                window.SetValue(BeforeFullScreenWindowStyleProperty, style);
            }
            else
            {
                 // 視窗在全屏,啥都不用做
            }
        }

以上代碼用到的 Win32 方法和類型定義,都可以在本文最後獲取到,在這裡就不詳細寫出

在進入全屏模式時,需要完成的步驟如下

  • 需要將視窗恢復到還原模式,在有標題欄的情況下最大化模式下無法全屏。去掉 WS_MAXIMIZE 樣式,使視窗變成還原狀。不能使用 ShowWindow(hwnd, ShowWindowCommands.SW_RESTORE) 方法,避免看到視窗變成還原狀態這一過程,也避免影響視窗的 Visible 狀態

  • 需要去掉 WS_THICKFRAME 樣式,在有該樣式的情況下不能全屏

  • 去掉 WS_MAXIMIZEBOX 樣式,禁用最大化,如果最大化會退出全屏

   style &= (~(WindowStyles.WS_THICKFRAME | WindowStyles.WS_MAXIMIZEBOX | WindowStyles.WS_MAXIMIZE));
   Win32.User32.SetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE, (IntPtr) style);

以上寫法是 Win32 函數調用的特有方式,習慣就好。在 Win32 的函數設計中,因為當初每個位元組都是十分寶貴的,所以恨不得一個位元組當成兩個來用,這也就是參數為什麼通過枚舉的二進位方式,看起來很複雜的邏輯設置的原因

全屏的過程,如果有 DWM 動畫,將會看到視窗閃爍。因此如果設備上有開啟 DWM 那麼進行關閉動畫。對應的,需要在退出全屏的時候,重新打開 DWM 過渡動畫

                //禁用 DWM 過渡動畫 忽略返回值,若DWM關閉不做處理
                Win32.Dwmapi.DwmSetWindowAttribute(hwnd, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 1,
                    sizeof(int));

接著就是本文的核心邏輯部分,通過 Hook 的方式修改視窗全屏,使用如下代碼添加 Hook 用來拿到視窗消息

                //添加Hook,在視窗尺寸位置等要發生變化時,確保全屏
                hwndSource.AddHook(KeepFullScreenHook);

private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
	// 代碼忽略,在下文將告訴大家
}       

為了觸發 KeepFullScreenHook 方法進行實際的設置視窗全屏,可以通過設置一下視窗的尺寸的方法,如下麵代碼

                if (Win32.User32.GetWindowRect(hwnd, out var rect))
                {
                    //不能用 placement 的坐標,placement是工作區坐標,不是屏幕坐標。

                    //使用視窗當前的矩形調用下設置視窗位置和尺寸的方法,讓Hook來進行調整視窗位置和尺寸到全屏模式
                    Win32.User32.SetWindowPos(hwnd, (IntPtr) HwndZOrder.HWND_TOP, rect.Left, rect.Top, rect.Width,
                        rect.Height, (int) WindowPositionFlags.SWP_NOZORDER);
                }

這就是 StartFullScreen 的所有代碼

        /// <summary>
        /// 開始進入全屏模式
        /// 進入全屏模式後,視窗可通過 API 方式(也可以用 Win + Shift + Left/Right)移動,調整大小,但會根據目標矩形尋找顯示器重新調整到全屏狀態。
        /// 進入全屏後,不要修改樣式等視窗屬性,在退出時,會恢復到進入前的狀態
        /// 進入全屏模式後會禁用 DWM 過渡動畫
        /// </summary>
        /// <param name="window"></param>
        public static void StartFullScreen(Window window)
        {
            if (window == null)
            {
                throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能為 null");
            }

            //確保不在全屏模式
            if (window.GetValue(BeforeFullScreenWindowPlacementProperty) == null &&
                window.GetValue(BeforeFullScreenWindowStyleProperty) == null)
            {
                var hwnd = new WindowInteropHelper(window).EnsureHandle();
                var hwndSource = HwndSource.FromHwnd(hwnd);

                //獲取當前視窗的位置大小狀態並保存
                var placement = new WINDOWPLACEMENT();
                placement.Size = (uint) Marshal.SizeOf(placement);
                Win32.User32.GetWindowPlacement(hwnd, ref placement);
                window.SetValue(BeforeFullScreenWindowPlacementProperty, placement);

                //修改視窗樣式
                var style = (WindowStyles) Win32.User32.GetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE);
                window.SetValue(BeforeFullScreenWindowStyleProperty, style);
                //將視窗恢復到還原模式,在有標題欄的情況下最大化模式下無法全屏,
                //這裡採用還原,不修改標題欄的方式
                //在退出全屏時,視窗原有的狀態會恢復
                //去掉WS_THICKFRAME,在有該樣式的情況下不能全屏
                //去掉WS_MAXIMIZEBOX,禁用最大化,如果最大化會退出全屏
                //去掉WS_MAXIMIZE,使視窗變成還原狀態,不使用ShowWindow(hwnd, ShowWindowCommands.SW_RESTORE),避免看到視窗變成還原狀態這一過程(也避免影響視窗的Visible狀態)
                style &= (~(WindowStyles.WS_THICKFRAME | WindowStyles.WS_MAXIMIZEBOX | WindowStyles.WS_MAXIMIZE));
                Win32.User32.SetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE, (IntPtr) style);

                //禁用 DWM 過渡動畫 忽略返回值,若DWM關閉不做處理
                Win32.Dwmapi.DwmSetWindowAttribute(hwnd, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 1,
                    sizeof(int));

                //添加Hook,在視窗尺寸位置等要發生變化時,確保全屏
                hwndSource.AddHook(KeepFullScreenHook);

                if (Win32.User32.GetWindowRect(hwnd, out var rect))
                {
                    //不能用 placement 的坐標,placement是工作區坐標,不是屏幕坐標。

                    //使用視窗當前的矩形調用下設置視窗位置和尺寸的方法,讓Hook來進行調整視窗位置和尺寸到全屏模式
                    Win32.User32.SetWindowPos(hwnd, (IntPtr) HwndZOrder.HWND_TOP, rect.Left, rect.Top, rect.Width,
                        rect.Height, (int) WindowPositionFlags.SWP_NOZORDER);
                }
            }
        }

在 KeepFullScreenHook 方法就是核心的邏輯,通過收到 Win 消息,判斷是 WM_WINDOWPOSCHANGING 消息,獲取當前屏幕範圍,設置給視窗

        /// <summary>
        /// 確保視窗全屏的Hook
        /// 使用HandleProcessCorruptedStateExceptions,防止訪問記憶體過程中因為一些致命異常導致程式崩潰
        /// </summary>
        [HandleProcessCorruptedStateExceptions]
        private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            //處理WM_WINDOWPOSCHANGING消息
            const int WINDOWPOSCHANGING = 0x0046;
            if (msg != WINDOWPOSCHANGING) return IntPtr.Zero;

            // 忽略代碼
        }

此方法會用到一些 Win32 的記憶體訪問,雖然以上代碼在實際測試中和在實際的用戶設備上運行沒有發現問題,但是當時在寫的時候,為了防止訪問記憶體過程中因為一些致命異常導致程式崩潰,就加上了 HandleProcessCorruptedStateExceptions 特性。在 dotnet core 下,此 HandleProcessCorruptedStateExceptionsAttribute 特性已失效。詳細請看 升級到 dotnet core 之後 HandleProcessCorruptedStateExceptions 無法接住異常

按照 Win32 消息的定義,可以先獲取WINDOWPOS結構體

                //得到WINDOWPOS結構體
                var pos = (WindowPosition) Marshal.PtrToStructure(lParam, typeof(WindowPosition));

                if ((pos.Flags & WindowPositionFlags.SWP_NOMOVE) != 0 &&
                    (pos.Flags & WindowPositionFlags.SWP_NOSIZE) != 0)
                {
                    //既然你既不改變位置,也不改變尺寸,我就不管了...
                    return IntPtr.Zero;
                }

通過 IsIconic 方法判斷當前視窗是否被最小化,如果最小化也不做全屏

                if (Win32.User32.IsIconic(hwnd))
                {
                    // 如果在全屏期間最小化了視窗,那麼忽略後續的位置調整。
                    // 否則按後續邏輯,會根據視窗在 -32000 的位置,計算出錯誤的目標位置,然後就跳到主屏了。
                    return IntPtr.Zero;
                }

如果在最小化也做全屏,將會因為最小化的視窗的 Y 坐標在 -32000 的位置,在全屏的設備上,如果是在副屏最小化的,將會計算出錯誤的目標位置,然後就跳到主屏了

獲取視窗的現在的矩形,用來計算視窗所在顯示器信息,然後將顯示器的範圍設置給視窗

                //獲取視窗現在的矩形,下麵用來參考計算目標矩形
                if (Win32.User32.GetWindowRect(hwnd, out var rect))
                {
                    var targetRect = rect; //視窗想要變化的目標矩形

                    if ((pos.Flags & WindowPositionFlags.SWP_NOMOVE) == 0)
                    {
                        //需要移動
                        targetRect.Left = pos.X;
                        targetRect.Top = pos.Y;
                    }

                    if ((pos.Flags & WindowPositionFlags.SWP_NOSIZE) == 0)
                    {
                        //要改變尺寸
                        targetRect.Right = targetRect.Left + pos.Width;
                        targetRect.Bottom = targetRect.Top + pos.Height;
                    }
                    else
                    {
                        //不改變尺寸
                        targetRect.Right = targetRect.Left + rect.Width;
                        targetRect.Bottom = targetRect.Top + rect.Height;
                    }

                    //使用目標矩形獲取顯示器信息
                    var monitor = Win32.User32.MonitorFromRect(targetRect, MonitorFlag.MONITOR_DEFAULTTOPRIMARY);
                    var info = new MonitorInfo();
                    info.Size = (uint) Marshal.SizeOf(info);
                    if (Win32.User32.GetMonitorInfo(monitor, ref info))
                    {
                        //基於顯示器信息設置視窗尺寸位置
                        pos.X = info.MonitorRect.Left;
                        pos.Y = info.MonitorRect.Top;
                        pos.Width = info.MonitorRect.Right - info.MonitorRect.Left;
                        pos.Height = info.MonitorRect.Bottom - info.MonitorRect.Top;
                        pos.Flags &= ~(WindowPositionFlags.SWP_NOSIZE | WindowPositionFlags.SWP_NOMOVE |
                                       WindowPositionFlags.SWP_NOREDRAW);
                        pos.Flags |= WindowPositionFlags.SWP_NOCOPYBITS;

                        if (rect == info.MonitorRect)
                        {
                            var hwndSource = HwndSource.FromHwnd(hwnd);
                            if (hwndSource?.RootVisual is Window window)
                            {
                                //確保視窗的 WPF 屬性與 Win32 位置一致,防止有逗比全屏後改 WPF 的屬性,發生一些詭異的行為
                                //下麵這樣做其實不太好,會再次觸發 WM_WINDOWPOSCHANGING 來著.....但是又沒有其他時機了
                                // WM_WINDOWPOSCHANGED 不能用 
                                //(例如:在進入全屏後,修改 Left 屬性,會進入 WM_WINDOWPOSCHANGING,然後在這裡將消息里的結構體中的 Left 改回,
                                // 使對 Left 的修改無效,那麼將不會進入 WM_WINDOWPOSCHANGED,視窗尺寸正常,但視窗的 Left 屬性值錯誤。)
                                var logicalPos =
                                    hwndSource.CompositionTarget.TransformFromDevice.Transform(
                                        new System.Windows.Point(pos.X, pos.Y));
                                var logicalSize =
                                    hwndSource.CompositionTarget.TransformFromDevice.Transform(
                                        new System.Windows.Point(pos.Width, pos.Height));
                                window.Left = logicalPos.X;
                                window.Top = logicalPos.Y;
                                window.Width = logicalSize.X;
                                window.Height = logicalSize.Y;
                            }
                            else
                            {
                                //這個hwnd是前面從Window來的,如果現在他不是Window...... 你信麽
                            }
                        }

                        //將修改後的結構體拷貝回去
                        Marshal.StructureToPtr(pos, lParam, false);
                    }
                }

這就是在 Hook 裡面的邏輯,接下來看退出全屏的方法

在退出全屏需要設置為視窗進入全屏之前的樣式等信息

        /// <summary>
        /// 退出全屏模式
        /// 視窗會回到進入全屏模式時保存的狀態
        /// 退出全屏模式後會重新啟用 DWM 過渡動畫
        /// </summary>
        /// <param name="window"></param>
        public static void EndFullScreen(Window window)
        {
            if (window == null)
            {
                throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能為 null");
            }

            //確保在全屏模式並獲取之前保存的狀態
            if (window.GetValue(BeforeFullScreenWindowPlacementProperty) is WINDOWPLACEMENT placement
                && window.GetValue(BeforeFullScreenWindowStyleProperty) is WindowStyles style)
            {
                var hwnd = new WindowInteropHelper(window).Handle;

                if (hwnd == IntPtr.Zero)
                {
                    // 句柄為 0 只有兩種情況:
                    //  1. 雖然視窗已進入全屏,但視窗已被關閉;
                    //  2. 視窗初始化前,在還沒有調用 StartFullScreen 的前提下就調用了此方法。
                    // 所以,直接 return 就好。
                    return;
                }

                var hwndSource = HwndSource.FromHwnd(hwnd);

                //去除hook
                hwndSource.RemoveHook(KeepFullScreenHook);

                //恢復保存的狀態
                //不要改變Style里的WS_MAXIMIZE,否則會使視窗變成最大化狀態,但是尺寸不對
                //也不要設置回Style里的WS_MINIMIZE,否則會導致視窗最小化按鈕顯示成還原按鈕
                Win32.User32.SetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE,
                    (IntPtr) (style & (~(WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE))));

                if ((style & WindowStyles.WS_MINIMIZE) != 0)
                {
                    //如果視窗進入全屏前是最小化的,這裡不讓視窗恢復到之前的最小化狀態,而是到還原的狀態。
                    //大多數情況下,都不期望在退出全屏的時候,恢復到最小化。
                    placement.ShowCmd = Win32.ShowWindowCommands.SW_RESTORE;
                }

                if ((style & WindowStyles.WS_MAXIMIZE) != 0)
                {
                    //提前調用 ShowWindow 使視窗恢復最大化,若通過 SetWindowPlacement 最大化會導致閃爍,只靠其恢復 RestoreBounds.
                    Win32.User32.ShowWindow(hwnd, Win32.ShowWindowCommands.SW_MAXIMIZE);
                }

                Win32.User32.SetWindowPlacement(hwnd, ref placement);

                if ((style & WindowStyles.WS_MAXIMIZE) ==
                    0) //如果視窗是最大化就不要修改WPF屬性,否則會破壞RestoreBounds,且WPF視窗自身在最大化時,不會修改 Left Top Width Height 屬性
                {
                    if (Win32.User32.GetWindowRect(hwnd, out var rect))
                    {
                        //不能用 placement 的坐標,placement是工作區坐標,不是屏幕坐標。

                        //確保視窗的 WPF 屬性與 Win32 位置一致
                        var logicalPos =
                            hwndSource.CompositionTarget.TransformFromDevice.Transform(
                                new System.Windows.Point(rect.Left, rect.Top));
                        var logicalSize =
                            hwndSource.CompositionTarget.TransformFromDevice.Transform(
                                new System.Windows.Point(rect.Width, rect.Height));
                        window.Left = logicalPos.X;
                        window.Top = logicalPos.Y;
                        window.Width = logicalSize.X;
                        window.Height = logicalSize.Y;
                    }
                }

                //重新啟用 DWM 過渡動畫 忽略返回值,若DWM關閉不做處理
                Win32.Dwmapi.DwmSetWindowAttribute(hwnd, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 0,
                    sizeof(int));

                //刪除保存的狀態
                window.ClearValue(BeforeFullScreenWindowPlacementProperty);
                window.ClearValue(BeforeFullScreenWindowStyleProperty);
            }
        }

下麵是 FullScreenHelper 的核心代碼,此類型依賴一些 Win32 方法的定義,這部分我就不在博客中寫出,大家可以從本文最後獲取所有源代碼

    /// <summary>
    /// 用來使視窗變得全屏的輔助類
    /// 採用設置視窗位置和尺寸,確保蓋住整個屏幕的方式來實現全屏
    /// 目前已知需要滿足的條件是:視窗蓋住整個屏幕、視窗沒有WS_THICKFRAME樣式、視窗不能有標題欄且最大化
    /// </summary>
    public static partial class FullScreenHelper
    {
        /// <summary>
        /// 用於記錄視窗全屏前位置的附加屬性
        /// </summary>
        private static readonly DependencyProperty BeforeFullScreenWindowPlacementProperty =
            DependencyProperty.RegisterAttached("BeforeFullScreenWindowPlacement", typeof(WINDOWPLACEMENT?),
                typeof(Window));

        /// <summary>
        /// 用於記錄視窗全屏前樣式的附加屬性
        /// </summary>
        private static readonly DependencyProperty BeforeFullScreenWindowStyleProperty =
            DependencyProperty.RegisterAttached("BeforeFullScreenWindowStyle", typeof(WindowStyles?), typeof(Window));

        /// <summary>
        /// 開始進入全屏模式
        /// 進入全屏模式後,視窗可通過 API 方式(也可以用 Win + Shift + Left/Right)移動,調整大小,但會根據目標矩形尋找顯示器重新調整到全屏狀態。
        /// 進入全屏後,不要修改樣式等視窗屬性,在退出時,會恢復到進入前的狀態
        /// 進入全屏模式後會禁用 DWM 過渡動畫
        /// </summary>
        /// <param name="window"></param>
        public static void StartFullScreen(Window window)
        {
            if (window == null)
            {
                throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能為 null");
            }

            //確保不在全屏模式
            if (window.GetValue(BeforeFullScreenWindowPlacementProperty) == null &&
                window.GetValue(BeforeFullScreenWindowStyleProperty) == null)
            {
                var hwnd = new WindowInteropHelper(window).EnsureHandle();
                var hwndSource = HwndSource.FromHwnd(hwnd);

                //獲取當前視窗的位置大小狀態並保存
                var placement = new WINDOWPLACEMENT();
                placement.Size = (uint) Marshal.SizeOf(placement);
                Win32.User32.GetWindowPlacement(hwnd, ref placement);
                window.SetValue(BeforeFullScreenWindowPlacementProperty, placement);

                //修改視窗樣式
                var style = (WindowStyles) Win32.User32.GetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE);
                window.SetValue(BeforeFullScreenWindowStyleProperty, style);
                //將視窗恢復到還原模式,在有標題欄的情況下最大化模式下無法全屏,
                //這裡採用還原,不修改標題欄的方式
                //在退出全屏時,視窗原有的狀態會恢復
                //去掉WS_THICKFRAME,在有該樣式的情況下不能全屏
                //去掉WS_MAXIMIZEBOX,禁用最大化,如果最大化會退出全屏
                //去掉WS_MAXIMIZE,使視窗變成還原狀態,不使用ShowWindow(hwnd, ShowWindowCommands.SW_RESTORE),避免看到視窗變成還原狀態這一過程(也避免影響視窗的Visible狀態)
                style &= (~(WindowStyles.WS_THICKFRAME | WindowStyles.WS_MAXIMIZEBOX | WindowStyles.WS_MAXIMIZE));
                Win32.User32.SetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE, (IntPtr) style);

                //禁用 DWM 過渡動畫 忽略返回值,若DWM關閉不做處理
                Win32.Dwmapi.DwmSetWindowAttribute(hwnd, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 1,
                    sizeof(int));

                //添加Hook,在視窗尺寸位置等要發生變化時,確保全屏
                hwndSource.AddHook(KeepFullScreenHook);

                if (Win32.User32.GetWindowRect(hwnd, out var rect))
                {
                    //不能用 placement 的坐標,placement是工作區坐標,不是屏幕坐標。

                    //使用視窗當前的矩形調用下設置視窗位置和尺寸的方法,讓Hook來進行調整視窗位置和尺寸到全屏模式
                    Win32.User32.SetWindowPos(hwnd, (IntPtr) HwndZOrder.HWND_TOP, rect.Left, rect.Top, rect.Width,
                        rect.Height, (int) WindowPositionFlags.SWP_NOZORDER);
                }
            }
        }

        /// <summary>
        /// 退出全屏模式
        /// 視窗會回到進入全屏模式時保存的狀態
        /// 退出全屏模式後會重新啟用 DWM 過渡動畫
        /// </summary>
        /// <param name="window"></param>
        public static void EndFullScreen(Window window)
        {
            if (window == null)
            {
                throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能為 null");
            }

            //確保在全屏模式並獲取之前保存的狀態
            if (window.GetValue(BeforeFullScreenWindowPlacementProperty) is WINDOWPLACEMENT placement
                && window.GetValue(BeforeFullScreenWindowStyleProperty) is WindowStyles style)
            {
                var hwnd = new WindowInteropHelper(window).Handle;

                if (hwnd == IntPtr.Zero)
                {
                    // 句柄為 0 只有兩種情況:
                    //  1. 雖然視窗已進入全屏,但視窗已被關閉;
                    //  2. 視窗初始化前,在還沒有調用 StartFullScreen 的前提下就調用了此方法。
                    // 所以,直接 return 就好。
                    return;
                }


                var hwndSource = HwndSource.FromHwnd(hwnd);

                //去除hook
                hwndSource.RemoveHook(KeepFullScreenHook);

                //恢復保存的狀態
                //不要改變Style里的WS_MAXIMIZE,否則會使視窗變成最大化狀態,但是尺寸不對
                //也不要設置回Style里的WS_MINIMIZE,否則會導致視窗最小化按鈕顯示成還原按鈕
                Win32.User32.SetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE,
                    (IntPtr) (style & (~(WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE))));

                if ((style & WindowStyles.WS_MINIMIZE) != 0)
                {
                    //如果視窗進入全屏前是最小化的,這裡不讓視窗恢復到之前的最小化狀態,而是到還原的狀態。
                    //大多數情況下,都不期望在退出全屏的時候,恢復到最小化。
                    placement.ShowCmd = Win32.ShowWindowCommands.SW_RESTORE;
                }

                if ((style & WindowStyles.WS_MAXIMIZE) != 0)
                {
                    //提前調用 ShowWindow 使視窗恢復最大化,若通過 SetWindowPlacement 最大化會導致閃爍,只靠其恢復 RestoreBounds.
                    Win32.User32.ShowWindow(hwnd, Win32.ShowWindowCommands.SW_MAXIMIZE);
                }

                Win32.User32.SetWindowPlacement(hwnd, ref placement);

                if ((style & WindowStyles.WS_MAXIMIZE) ==
                    0) //如果視窗是最大化就不要修改WPF屬性,否則會破壞RestoreBounds,且WPF視窗自身在最大化時,不會修改 Left Top Width Height 屬性
                {
                    if (Win32.User32.GetWindowRect(hwnd, out var rect))
                    {
                        //不能用 placement 的坐標,placement是工作區坐標,不是屏幕坐標。

                        //確保視窗的 WPF 屬性與 Win32 位置一致
                        var logicalPos =
                            hwndSource.CompositionTarget.TransformFromDevice.Transform(
                                new System.Windows.Point(rect.Left, rect.Top));
                        var logicalSize =
                            hwndSource.CompositionTarget.TransformFromDevice.Transform(
                                new System.Windows.Point(rect.Width, rect.Height));
                        window.Left = logicalPos.X;
                        window.Top = logicalPos.Y;
                        window.Width = logicalSize.X;
                        window.Height = logicalSize.Y;
                    }
                }

                //重新啟用 DWM 過渡動畫 忽略返回值,若DWM關閉不做處理
                Win32.Dwmapi.DwmSetWindowAttribute(hwnd, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 0,
                    sizeof(int));

                //刪除保存的狀態
                window.ClearValue(BeforeFullScreenWindowPlacementProperty);
                window.ClearValue(BeforeFullScreenWindowStyleProperty);
            }
        }

        /// <summary>
        /// 確保視窗全屏的Hook
        /// 使用HandleProcessCorruptedStateExceptions,防止訪問記憶體過程中因為一些致命異常導致程式崩潰
        /// </summary>
        [HandleProcessCorruptedStateExceptions]
        private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            //處理WM_WINDOWPOSCHANGING消息
            const int WINDOWPOSCHANGING = 0x0046;
            if (msg == WINDOWPOSCHANGING)
            {
                try
                {
                    //得到WINDOWPOS結構體
                    var pos = (WindowPosition) Marshal.PtrToStructure(lParam, typeof(WindowPosition));

                    if ((pos.Flags & WindowPositionFlags.SWP_NOMOVE) != 0 &&
                        (pos.Flags & WindowPositionFlags.SWP_NOSIZE) != 0)
                    {
                        //既然你既不改變位置,也不改變尺寸,我就不管了...
                        return IntPtr.Zero;
                    }

                    if (Win32.User32.IsIconic(hwnd))
                    {
                        // 如果在全屏期間最小化了視窗,那麼忽略後續的位置調整。
                        // 否則按後續邏輯,會根據視窗在 -32000 的位置,計算出錯誤的目標位置,然後就跳到主屏了。
                        return IntPtr.Zero;
                    }

                    //獲取視窗現在的矩形,下麵用來參考計算目標矩形
                    if (Win32.User32.GetWindowRect(hwnd, out var rect))
                    {
                        var targetRect = rect; //視窗想要變化的目標矩形

                        if ((pos.Flags & WindowPositionFlags.SWP_NOMOVE) == 0)
                        {
                            //需要移動
                            targetRect.Left = pos.X;
                            targetRect.Top = pos.Y;
                        }

                        if ((pos.Flags & WindowPositionFlags.SWP_NOSIZE) == 0)
                        {
                            //要改變尺寸
                            targetRect.Right = targetRect.Left + pos.Width;
                            targetRect.Bottom = targetRect.Top + pos.Height;
                        }
                        else
                        {
                            //不改變尺寸
                            targetRect.Right = targetRect.Left + rect.Width;
                            targetRect.Bottom = targetRect.Top + rect.Height;
                        }

                        //使用目標矩形獲取顯示器信息
                        var monitor = Win32.User32.MonitorFromRect(targetRect, MonitorFlag.MONITOR_DEFAULTTOPRIMARY);
                        var info = new MonitorInfo();
                        info.Size = (uint) Marshal.SizeOf(info);
                        if (Win32.User32.GetMonitorInfo(monitor, ref info))
                        {
                            //基於顯示器信息設置視窗尺寸位置
                            pos.X = info.MonitorRect.Left;
                            pos.Y = info.MonitorRect.Top;
                            pos.Width = info.MonitorRect.Right - info.MonitorRect.Left;
                            pos.Height = info.MonitorRect.Bottom - info.MonitorRect.Top;
                            pos.Flags &= ~(WindowPositionFlags.SWP_NOSIZE | WindowPositionFlags.SWP_NOMOVE |
                                           WindowPositionFlags.SWP_NOREDRAW);
                            pos.Flags |= WindowPositionFlags.SWP_NOCOPYBITS;

                            if (rect == info.MonitorRect)
                            {
                                var hwndSource = HwndSource.FromHwnd(hwnd);
                                if (hwndSource?.RootVisual is Window window)
                                {
                                    //確保視窗的 WPF 屬性與 Win32 位置一致,防止有逗比全屏後改 WPF 的屬性,發生一些詭異的行為
                                    //下麵這樣做其實不太好,會再次觸發 WM_WINDOWPOSCHANGING 來著.....但是又沒有其他時機了
                                    // WM_WINDOWPOSCHANGED 不能用 
                                    //(例如:在進入全屏後,修改 Left 屬性,會進入 WM_WINDOWPOSCHANGING,然後在這裡將消息里的結構體中的 Left 改回,
                                    // 使對 Left 的修改無效,那麼將不會進入 WM_WINDOWPOSCHANGED,視窗尺寸正常,但視窗的 Left 屬性值錯誤。)
                                    var logicalPos =
                                        hwndSource.CompositionTarget.TransformFromDevice.Transform(
                                            new System.Windows.Point(pos.X, pos.Y));
                                    var logicalSize =
                                        hwndSource.CompositionTarget.TransformFromDevice.Transform(
                                            new System.Windows.Point(pos.Width, pos.Height));
                                    window.Left = logicalPos.X;
                                    window.Top = logicalPos.Y;
                                    window.Width = logicalSize.X;
                                    window.Height = logicalSize.Y;
                                }
                                else
                                {
                                    //這個hwnd是前面從Window來的,如果現在他不是Window...... 你信麽
                                }
                            }

                            //將修改後的結構體拷貝回去
                            Marshal.StructureToPtr(pos, lParam, false);
                        }
                    }
                }
                catch
                {
                    // 這裡也不需要日誌啥的,只是為了防止上面有逗比邏輯,在消息迴圈裡面炸了
                }
            }

            return IntPtr.Zero;
        }
    }

本文所有代碼在 githubgitee 上完全開源

不嫌棄麻煩的話,還請自行下載代碼,自己構建。可以通過如下方式獲取本文的源代碼,先創建一個空文件夾,接著使用命令行 cd 命令進入此空文件夾,在命令行裡面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 5b0440c6617b87cdd9953dc68e706b22c5939ccb

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git

獲取代碼之後,進入 KenafearcuweYemjecahee 文件夾

特別感謝 lsj 提供的邏輯


通過 lsj 閱讀 Avalonia 的邏輯,找到了 ITaskbarList2::MarkFullscreenWindow 方法,通過此方式可以通知任務欄不要顯示到最頂,以下是我測試的行為

當調用 ITaskbarList2::MarkFullscreenWindow 方法設置給到某個視窗時,如此視窗處於激活狀態,此視窗所在的屏幕的任務欄將不會置頂,任務欄將會在其他視窗下方。這裡的其他視窗指的是任意的視窗,即任務欄不再具備最頂層的特性。換句話說就是這個方法不會輔助視窗本身進入全屏,僅僅只是用於處理任務欄在全屏視窗的行為,這也符合 ITaskbarList 介面的含義。而至於設置給到的某個視窗,此視窗是否真的全屏,那 MarkFullscreenWindow 方法也管不了了,也就是說即使設置給一個普通的非全屏的視窗,甚至非最大化的視窗,也是可以的

先編寫簡單的代碼,用於測試 ITaskbarList2::MarkFullscreenWindow 的行為

先定義 ITaskbarList2 這個 COM 介面,代碼如下

        [ComImport]
        [Guid("602D4995-B13A-429b-A66E-1935E44F4317")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface ITaskbarList2
        {
            [PreserveSig]
            int HrInit();

            [PreserveSig]
            int AddTab(IntPtr hwnd);

            [PreserveSig]
            int DeleteTab(IntPtr hwnd);

            [PreserveSig]
            int ActivateTab(IntPtr hwnd);

            [PreserveSig]
            int SetActiveAlt(IntPtr hwnd);

            [PreserveSig]
            int MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
        }

以上代碼裡面的 InterfaceType 特性是必須的,需要加上 InterfaceIsIUnknown 參數。因為根據官方文檔的如下描述可知道 ITaskbarList2 是繼承 ITaskbarList 的,而 ITaskbarList 是繼承 IUnknown 的

The ITaskbarList2 interface inherits from ITaskbarList. ITaskbarList2 also has these types of members
The ITaskbarList interface inherits from the IUnknown interface.

在 dotnet 裡面,需要標記 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 特性,否則將會缺失 IUnknown 的預設幾個方法,導致實際 C# 代碼調用的代碼非預期,可能導致進程炸掉

以上代碼裡面,咱需要關註使用的只有 MarkFullscreenWindow 方法。為了更好的進行測試,接下來編輯 MainWindow.xaml 添加一個按鈕,用於點擊時進入或退出全屏模式,即調用 MarkFullscreenWindow 方法時,傳入的 fFullscreen 參數的值

        <ToggleButton HorizontalAlignment="Center" VerticalAlignment="Center" Click="Button_OnClick">全屏</ToggleButton>

編輯後臺代碼,實現 Button_OnClick 功能

        private void Button_OnClick(object sender, RoutedEventArgs e)
        {
            var toggleButton = (ToggleButton) sender;

            FullScreenHelper.MarkFullscreenWindowTaskbarList(new WindowInteropHelper(this).Handle, toggleButton.IsChecked is true);
        }

以上的 FullScreenHelper.MarkFullscreenWindowTaskbarList 封裝方法的實現如下

    public static partial class FullScreenHelper
    {
        public static void MarkFullscreenWindowTaskbarList(IntPtr hwnd, bool isFullscreen)
        {
            try
            {
                var CLSID_TaskbarList = new Guid("56FDF344-FD6D-11D0-958A-006097C9A090");
                var obj = Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_TaskbarList));
                (obj as ITaskbarList2)?.MarkFullscreenWindow(hwnd, isFullscreen);
            }
            catch
            {
                //應該不會掛
            }
        }
    }

完成以上代碼運行的界面如下,可以看到這是一個非全屏也非最大化的視窗

以上代碼放在 githubgitee 上,可以使用如下命令行拉取代碼

先創建一個空文件夾,接著使用命令行 cd 命令進入此空文件夾,在命令行裡面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin c8b8e6550b38d7fe109da5ed8fc63ab90c4dd7c5

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin c8b8e6550b38d7fe109da5ed8fc63ab90c4dd7c5

獲取代碼之後,進入 KenafearcuweYemjecahee 文件夾,即可獲取到源代碼

接下來可以做一個測試實現,測試其行為

  1. 啟動進程視窗,即此視窗為主視窗,拖動主視窗在任務欄位置。 此時可見任務欄在主視窗上方
  2. 點擊 全屏 按鈕,此時可見主視窗在任務欄上方,即任務欄在主視窗下方不會擋住主視窗
  3. 啟動記事本,拿到記事本視窗。此時可見主視窗失去焦點,顯示在任務欄下方,即任務欄擋住主視窗。此時拖動記事本視窗在任務欄位置,再點擊激活主視窗,讓主視窗獲取焦點,可見任務欄顯示在最下方,即任務欄在主視窗和記事本視窗下方

通過以上行為測試,大概可以知道,此 MarkFullscreenWindow 方法的作用只是處理任務欄是否在最頂層而已。只要設置給到 MarkFullscreenWindow 的句柄的視窗處於激活獲取焦點狀態,那麼任務欄就不會處於最頂層,將可能處於其他視窗的下方,即使其他視窗沒有調用 MarkFullscreenWindow 方法。因為此時完全就是靠視窗層級處理

另外 MarkFullscreenWindow 方法也沒有真的判斷傳入的視窗句柄對應的視窗是否真的處於全屏狀態,僅僅只是判斷傳入的視窗句柄對應處於激活獲取焦點時就將任務欄設置為非最頂層模式而已

估計在微軟底層實現是為了規避一些坑而作出如此詭異的行為。在此行為之下反而可以用在某些有趣的情況下,讓任務欄不要處於最頂層,和是否全屏需求可能沒有強關係。但此方法也可以更好的處理全屏視窗時,任務欄冒出來的問題

歡迎大家獲取我的代碼進行更多的測試

在雙屏設備下的 MarkFullscreenWindow 方法就更有趣了,簡單說就是雙屏模式下 MarkFullscreenWindow 隻影響主視窗所在的屏幕的任務欄的狀態,另一個屏幕不受影響

在有雙屏的設備上可以繼續上述測試行為,即上述測試行為在屏幕1上進行,現在還有屏幕2另一個屏幕

  1. 記原本啟動的記事本視窗為記事本1視窗,在屏幕1 啟動新的記事本,獲取記事本2視窗。此時主視窗自然丟失焦點,前臺視窗為剛啟動的記事本2視窗。任務欄在最上層,即任務欄蓋住主視窗
  2. 拖動記事本2視窗,從屏幕1 拖動到屏幕2 上,且沿著任務欄拖動。可見當記事本2視窗拖動到屏幕2 時,屏幕1 的任務欄回到主視窗下方,即屏幕1 的任務欄沒有擋住主視窗和記事本1視窗。再將記事本2視窗從屏幕2 拖回屏幕1 上,可見當記事本2視窗拖回屏幕1 時,屏幕1 的任務欄回到了最頂層狀態,即使任務欄蓋住主視窗和兩個記事本的視窗
  3. 將記事本2視窗拖到屏幕2 上,點擊屏幕1 的主視窗,讓屏幕1 的主視窗獲取焦點。此時符合預期的是主視窗在任務欄之上,任務欄沒有處於最頂層狀態。接著再點擊屏幕2 的記事本2視窗,讓記事本2視窗獲取焦點激活作為前臺視窗。此時可見屏幕1 的任務欄依舊處於非最上層狀態,即主視窗在任務欄之上,任務欄沒有擋住主視窗。在以上過程中,屏幕2 的任務欄都是保持最上層,即會擋住記事本2視窗。再將主視窗從屏幕1 拖動到屏幕2 上,可以看到當主視窗從屏幕1 拖動到屏幕2 時,屏幕1 的任務欄處於最頂層狀態,可以擋住記事本1視窗,屏幕2 的任務欄沒有處於最頂層狀態,在記事本2視窗下方

通過以上的測試可以看到,在 MarkFullscreenWindow 方法的判斷,其實只是判斷當前屏幕的激活順序最高的視窗是否設置了 MarkFullscreenWindow 方法。如果是則讓此屏幕的任務欄處於非最頂層的模式,相對來說多個屏幕下的邏輯會更加的複雜,從這個方面也能想象微軟在這個方法實現上有多少坑

基於 MarkFullscreenWindow 的機制,優化 FullScreenHelper 的代碼,優化之後的代碼如下

    /// <summary>
    /// 用來使視窗變得全屏的輔助類
    /// 採用設置視窗位置和尺寸,確保蓋住整個屏幕的方式來實現全屏
    /// 目前已知需要滿足的條件是:視窗蓋住整個屏幕、視窗沒有WS_THICKFRAME樣式、視窗不能有標題欄且最大化
    /// </summary>
    public static partial class FullScreenHelper
    {
        public static void MarkFullscreenWindowTaskbarList(IntPtr hwnd, bool isFullscreen)
        {
            try
            {
                var CLSID_TaskbarList = new Guid("56FDF344-FD6D-11D0-958A-006097C9A090");
                var obj = Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_TaskbarList));
                (obj as ITaskbarList2)?.MarkFullscreenWindow(hwnd, isFullscreen);
            }
            catch
            {
                //應該不會掛
            }
        }

        /// <summary>
        /// 用於記錄視窗全屏前位置的附加屬性
        /// </summary>
        private static readonly DependencyProperty BeforeFullScreenWindowPlacementProperty =
            DependencyProperty.RegisterAttached("BeforeFullScreenWindowPlacement", typeof(WINDOWPLACEMENT?),
                typeof(Window));

        /// <summary>
        /// 用於記錄視窗全屏前樣式的附加屬性
        /// </summary>
        private static readonly DependencyProperty BeforeFullScreenWindowStyleProperty =
            DependencyProperty.RegisterAttached("BeforeFullScreenWindowStyle", typeof(WindowStyles?), typeof(Window));

        /// <summary>
        /// 開始進入全屏模式
        /// 進入全屏模式後,視窗可通過 API 方式(也可以用 Win + Shift + Left/Right)移動,調整大小,但會根據目標矩形尋找顯示器重新調整到全屏狀態。
        /// 進入全屏後,不要修改樣式等視窗屬性,在退出時,會恢復到進入前的狀態
        /// 進入全屏模式後會禁用 DWM 過渡動畫
        /// </summary>
        /// <param name="window"></param>
        public static void StartFullScreen(Window window)
        {
            if (window == null)
            {
                throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能為 null");
            }

            //確保不在全屏模式
            if (window.GetValue(BeforeFullScreenWindowPlacementProperty) == null &&
                window.GetValue(BeforeFullScreenWindowStyleProperty) == null)
            {
                var hwnd = new WindowInteropHelper(window).EnsureHandle();
                var hwndSource = HwndSource.FromHwnd(hwnd);

                //獲取當前視窗的位置大小狀態並保存
                var placement = new WINDOWPLACEMENT();
                placement.Size = (uint) Marshal.SizeOf(placement);
                Win32.User32.GetWindowPlacement(hwnd, ref placement);
                window.SetValue(BeforeFullScreenWindowPlacementProperty, placement);

                //修改視窗樣式
                var style = (WindowStyles) Win32.User32.GetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE);
                window.SetValue(BeforeFullScreenWindowStyleProperty, style);
                //將視窗恢復到還原模式,在有標題欄的情況下最大化模式下無法全屏,
                //這裡採用還原,不修改標題欄的方式
                //在退出全屏時,視窗原有的狀態會恢復
                //去掉WS_THICKFRAME,在有該樣式的情況下不能全屏
                //去掉WS_MAXIMIZEBOX,禁用最大化,如果最大化會退出全屏
                //去掉WS_MAXIMIZE,使視窗變成還原狀態,不使用ShowWindow(hwnd, ShowWindowCommands.SW_RESTORE),避免看到視窗變成還原狀態這一過程(也避免影響視窗的Visible狀態)
                style &= (~(WindowStyles.WS_THICKFRAME | WindowStyles.WS_MAXIMIZEBOX | WindowStyles.WS_MAXIMIZE));
                Win32.User32.SetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE, (IntPtr) style);

                //禁用 DWM 過渡動畫 忽略返回值,若DWM關閉不做處理
                Win32.Dwmapi.DwmSetWindowAttribute(hwnd, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 1,
                    sizeof(int));

                //添加Hook,在視窗尺寸位置等要發生變化時,確保全屏
                hwndSource.AddHook(KeepFullScreenHook);

                if (Win32.User32.GetWindowRect(hwnd, out var rect))
                {
                    //不能用 placement 的坐標,placement是工作區坐標,不是屏幕坐標。

                    //使用視窗當前的矩形調用下設置視窗位置和尺寸的方法,讓Hook來進行調整視窗位置和尺寸到全屏模式
                    Win32.User32.SetWindowPos(hwnd, (IntPtr) HwndZOrder.HWND_TOP, rect.Left, rect.Top, rect.Width,
                        rect.Height, (int) WindowPositionFlags.SWP_NOZORDER);
                }

                MarkFullscreenWindowTaskbarList(hwnd, true);
            }
        }

        /// <summary>
        /// 退出全屏模式
        /// 視窗會回到進入全屏模式時保存的狀態
        /// 退出全屏模式後會重新啟用 DWM 過渡動畫
        /// </summary>
        /// <param name="window"></param>
        public static void EndFullScreen(Window window)
        {
            if (window == null)
            {
                throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能為 null");
            }

            //確保在全屏模式並獲取之前保存的狀態
            if (window.GetValue(BeforeFullScreenWindowPlacementProperty) is WINDOWPLACEMENT placement
                && window.GetValue(BeforeFullScreenWindowStyleProperty) is WindowStyles style)
            {
                var hwnd = new WindowInteropHelper(window).Handle;

                if (hwnd == IntPtr.Zero)
                {
                    // 句柄為 0 只有兩種情況:
                    //  1. 雖然視窗已進入全屏,但視窗已被關閉;
                    //  2. 視窗初始化前,在還沒有調用 StartFullScreen 的前提下就調用了此方法。
                    // 所以,直接 return 就好。
                    return;
                }

                var hwndSource = HwndSource.FromHwnd(hwnd);

                //去除hook
                hwndSource.RemoveHook(KeepFullScreenHook);

                //恢復保存的狀態
                //不要改變Style里的WS_MAXIMIZE,否則會使視窗變成最大化狀態,但是尺寸不對
                //也不要設置回Style里的WS_MINIMIZE,否則會導致視窗最小化按鈕顯示成還原按鈕
                Win32.User32.SetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE,
                    (IntPtr) (style & (~(WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE))));

                if ((style & WindowStyles.WS_MINIMIZE) != 0)
                {
                    //如果視窗進入全屏前是最小化的,這裡不讓視窗恢復到之前的最小化狀態,而是到還原的狀態。
                    //大多數情況下,都不期望在退出全屏的時候,恢復到最小化。
                    placement.ShowCmd = Win32.ShowWindowCommands.SW_RESTORE;
                }

                if ((style & WindowStyles.WS_MAXIMIZE) != 0)
                {
                    //提前調用 ShowWindow 使視窗恢復最大化,若通過 SetWindowPlacement 最大化會導致閃爍,只靠其恢復 RestoreBounds.
                    Win32.User32.ShowWindow(hwnd, Win32.ShowWindowCommands.SW_MAXIMIZE);
                }

                Win32.User32.SetWindowPlacement(hwnd, ref placement);

                if ((style & WindowStyles.WS_MAXIMIZE) ==
                    0) //如果視窗是最大化就不要修改WPF屬性,否則會破壞RestoreBounds,且WPF視窗自身在最大化時,不會修改 Left Top Width Height 屬性
                {
                    if (Win32.User32.GetWindowRect(hwnd, out var rect))
                    {
                        //不能用 placement 的坐標,placement是工作區坐標,不是屏幕坐標。

                        //確保視窗的 WPF 屬性與 Win32 位置一致
                        var logicalPos =
                            hwndSource.CompositionTarget.TransformFromDevice.Transform(
                                new System.Windows.Point(rect.Left, rect.Top));
                        var logicalSize =
                            hwndSource.CompositionTarget.TransformFromDevice.Transform(
                                new System.Windows.Point(rect.Width, rect.Height));
                        window.Left = logicalPos.X;
                        window.Top = logicalPos.Y;
                        window.Width = logicalSize.X;
                        window.Height = logicalSize.Y;
                    }
                }

                //重新啟用 DWM 過渡動畫 忽略返回值,若DWM關閉不做處理
                Win32.Dwmapi.DwmSetWindowAttribute(hwnd, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 0,
                    sizeof(int));

                //刪除保存的狀態
                window.ClearValue(BeforeFullScreenWindowPlacementProperty);
                window.ClearValue(BeforeFullScreenWindowStyleProperty);
                MarkFullscreenWindowTaskbarList(hwnd, false);
            }
        }

        /// <summary>
        /// 確保視窗全屏的Hook
        /// 使用HandleProcessCorruptedStateExceptions,防止訪問記憶體過程中因為一些致命異常導致程式崩潰
        /// </summary>
        [HandleProcessCorruptedStateExceptions]
        private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            //處理WM_WINDOWPOSCHANGING消息
            const int WINDOWPOSCHANGING = 0x0046;
            if (msg == WINDOWPOSCHANGING)
            {
                try
                {
                    //得到WINDOWPOS結構體
                    var pos = (WindowPosition) Marshal.PtrToStructure(lParam, typeof(WindowPosition));

                    if ((pos.Flags & WindowPositionFlags.SWP_NOMOVE) != 0 &&
                        (pos.Flags & WindowPositionFlags.SWP_NOSIZE) != 0)
                    {
                        //既然你既不改變位置,也不改變尺寸,我就不管了...
                        return IntPtr.Zero;
                    }

                    if (Win32.User32.IsIconic(hwnd))
                    {
                        // 如果在全屏期間最小化了視窗,那麼忽略後續的位置調整。
                        // 否則按後續邏輯,會根據視窗在 -32000 的位置,計算出錯誤的目標位置,然後就跳到主屏了。
                        return IntPtr.Zero;
                    }

                    //獲取視窗現在的矩形,下麵用來參考計算目標矩形
                    if (Win32.User32.GetWindowRect(hwnd, out var rect))
                    {
                        var targetRect = rect; //視窗想要變化的目標矩形

                        if ((pos.Flags & WindowPositionFlags.SWP_NOMOVE) == 0)
                        {
                            //需要移動
                            targetRect.Left = pos.X;
                            targetRect.Top = pos.Y;
                        }

                        if ((pos.Flags & WindowPositionFlags.SWP_NOSIZE) == 0)
                        {
                            //要改變尺寸
                            targetRect.Right = targetRect.Left + pos.Width;
                            targetRect.Bottom = targetRect.Top + pos.Height;
                        }
                        else
                        {
                            //不改變尺寸
                            targetRect.Right = targetRect.Left + rect.Width;
                            targetRect.Bottom = targetRect.Top + rect.Height;
                        }

                        //使用目標矩形獲取顯示器信息
                        var monitor = Win32.User32.MonitorFromRect(targetRect, MonitorFlag.MONITOR_DEFAULTTOPRIMARY);
                        var info = new MonitorInfo();
                        info.Size = (uint) Marshal.SizeOf(info);
                        if (Win32.User32.GetMonitorInfo(monitor, ref info))
                        {
                            //基於顯示器信息設置視窗尺寸位置
                            pos.X = info.MonitorRect.Left;
                            pos.Y = info.MonitorRect.Top;
                            pos.Width = info.MonitorRect.Right - info.MonitorRect.Left;
                            pos.Height = info.MonitorRect.Bottom - info.MonitorRect.Top;
                            pos.Flags &= ~(WindowPositionFlags.SWP_NOSIZE | WindowPositionFlags.SWP_NOMOVE |
                                           WindowPositionFlags.SWP_NOREDRAW);
                            pos.Flags |= WindowPositionFlags.SWP_NOCOPYBITS;

                            if (rect == info.MonitorRect)
                            {
                                var hwndSource = HwndSource.FromHwnd(hwnd);
                                if (hwndSource?.RootVisual is Window window)
                                {
                                    //確保視窗的 WPF 屬性與 Win32 位置一致,防止有逗比全屏後改 WPF 的屬性,發生一些詭異的行為
                                    //下麵這樣做其實不太好,會再次觸發 WM_WINDOWPOSCHANGING 來著.....但是又沒有其他時機了
                                    // WM_WINDOWPOSCHANGED 不能用 
                                    //(例如:在進入全屏後,修改 Left 屬性,會進入 WM_WINDOWPOSCHANGING,然後在這裡將消息里的結構體中的 Left 改回,
                                    // 使對 Left 的修改無效,那麼將不會進入 WM_WINDOWPOSCHANGED,視窗尺寸正常,但視窗的 Left 屬性值錯誤。)
     

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 大家好,我是 Java陳序員。 問君能有幾多愁,開源項目解千愁! 今天,給大家介紹一個快速開發平臺,完全開源可商用! 關註微信公眾號:【Java陳序員】,獲取開源項目分享、AI副業分享、超200本經典電腦電子書籍等。 項目介紹 SmartAdmin —— 一個簡潔、高效、安全的快速開發平臺,以高質 ...
  • 大家好,我是R哥。 金三銀四結束了,上個月分享了一個 35K 入職的面試輔導案例: 35K*14 薪入職了,這公司只要不裁員,我能一直呆下去。。 今天再分享一個上個月讓人很有成就感的面試輔導 case: 外包、空窗四個月、薪資 10k、996 ——> 甲方公司、薪資15k、早九晚六(WLB),從報名 ...
  • 當一個線程被啟動後,如果再次調start()方法,將會拋出IllegalThreadStateException異常。 這是因為Java線程的生命周期只有一次。調用start()方法會導致系統在新線程中運行執行體,但是如果線程已經結束,則不能再次使用,需要重新創建一個新的線程對象並調用start()... ...
  • 正文 昨天玩到了凌晨 3 點,今天睡了一天…… 斷斷續續睡到 12 點起床,下午又從 5 點睡到了 7 點。我願稱之為睡神……. 其它時間就是做工作日一直沒時間做的雜事,比如洗衣服,刷鞋,換洗被套什麼的,還挺花時間。用了得有兩三個小時。 所以昨天說的今天開擺,那是真的開擺了 (笑。 現在晃一下頭,能 ...
  • 1. Spring6 的JdbcTemplate的JDBC模板類的詳細使用說明 @目錄1. Spring6 的JdbcTemplate的JDBC模板類的詳細使用說明每博一文案2. 環境準備3. 數據準備4. 開始4.1 從數據表中插入(添加)數據4.2 從數據表中修改數據4.3 從數據表中刪除數據4 ...
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • 背景 我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。 思路解釋 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron ...
一周排行
    -Advertisement-
    Play Games
  • 前言 微服務架構已經成為搭建高效、可擴展系統的關鍵技術之一,然而,現有許多微服務框架往往過於複雜,使得我們普通開發者難以快速上手並體驗到微服務帶了的便利。為瞭解決這一問題,於是作者精心打造了一款最接地氣的 .NET 微服務框架,幫助我們輕鬆構建和管理微服務應用。 本框架不僅支持 Consul 服務註 ...
  • 先看一下效果吧: 如果不會寫動畫或者懶得寫動畫,就直接交給Blend來做吧; 其實Blend操作起來很簡單,有點類似於在操作PS,我們只需要設置關鍵幀,滑鼠點來點去就可以了,Blend會自動幫我們生成我們想要的動畫效果. 第一步:要創建一個空的WPF項目 第二步:右鍵我們的項目,在最下方有一個,在B ...
  • Prism:框架介紹與安裝 什麼是Prism? Prism是一個用於在 WPF、Xamarin Form、Uno 平臺和 WinUI 中構建鬆散耦合、可維護和可測試的 XAML 應用程式框架 Github https://github.com/PrismLibrary/Prism NuGet htt ...
  • 在WPF中,屏幕上的所有內容,都是通過畫筆(Brush)畫上去的。如按鈕的背景色,邊框,文本框的前景和形狀填充。藉助畫筆,可以繪製頁面上的所有UI對象。不同畫筆具有不同類型的輸出( 如:某些畫筆使用純色繪製區域,其他畫筆使用漸變、圖案、圖像或繪圖)。 ...
  • 前言 嗨,大家好!推薦一個基於 .NET 8 的高併發微服務電商系統,涵蓋了商品、訂單、會員、服務、財務等50多種實用功能。 項目不僅使用了 .NET 8 的最新特性,還集成了AutoFac、DotLiquid、HangFire、Nlog、Jwt、LayUIAdmin、SqlSugar、MySQL、 ...
  • 本文主要介紹攝像頭(相機)如何採集數據,用於類似攝像頭本地顯示軟體,以及流媒體數據傳輸場景如傳屏、視訊會議等。 攝像頭採集有多種方案,如AForge.NET、WPFMediaKit、OpenCvSharp、EmguCv、DirectShow.NET、MediaCaptre(UWP),網上一些文章以及 ...
  • 前言 Seal-Report 是一款.NET 開源報表工具,擁有 1.4K Star。它提供了一個完整的框架,使用 C# 編寫,最新的版本採用的是 .NET 8.0 。 它能夠高效地從各種資料庫或 NoSQL 數據源生成日常報表,並支持執行複雜的報表任務。 其簡單易用的安裝過程和直觀的設計界面,我們 ...
  • 背景需求: 系統需要對接到XXX官方的API,但因此官方對接以及管理都十分嚴格。而本人部門的系統中包含諸多子系統,系統間為了穩定,程式間多數固定Token+特殊驗證進行調用,且後期還要提供給其他兄弟部門系統共同調用。 原則上:每套系統都必須單獨接入到官方,但官方的接入複雜,還要官方指定機構認證的證書 ...
  • 本文介紹下電腦設備關機的情況下如何通過網路喚醒設備,之前電源S狀態 電腦Power電源狀態- 唐宋元明清2188 - 博客園 (cnblogs.com) 有介紹過遠程喚醒設備,後面這倆天瞭解多了點所以單獨加個隨筆 設備關機的情況下,使用網路喚醒的前提條件: 1. 被喚醒設備需要支持這WakeOnL ...
  • 前言 大家好,推薦一個.NET 8.0 為核心,結合前端 Vue 框架,實現了前後端完全分離的設計理念。它不僅提供了強大的基礎功能支持,如許可權管理、代碼生成器等,還通過採用主流技術和最佳實踐,顯著降低了開發難度,加快了項目交付速度。 如果你需要一個高效的開發解決方案,本框架能幫助大家輕鬆應對挑戰,實 ...