本文來告訴大家在 WPF 中,設定視窗全屏化的一個穩定的設定方法。在設定視窗全屏的時候,經常遇到的問題就是應用程式雖然設定最大化加無邊框,但是此方式經常會有工作列冒出來,或者說視窗沒有貼螢幕的邊。本文的方法是基於 Win32 的,由 lsj 提供的方法,當前已在 1000 多萬臺裝置上穩定執行超過三年時間,只有很少的電腦才偶爾出現工作列不消失的情況
簡單的 WPF 全屏視窗只需設定 WindowStyle 和 WindowState 屬性即可,如以下 XAML 程式碼
<Window
...
Title="MainWindow" Height="450" Width="800"
WindowStyle="None" WindowState="Maximized">
...
</Window>
或如下的後臺 cs 程式碼
Window window = xxx;
window.WindowStyle = WindowStyle.None;
window.WindowState = WindowState.Maximized;
儘管以上的方法足夠簡單且大部分情況下行之有效,然而在很多使用者的裝置上都會常遇到工作列冒出來,或者說視窗沒有貼螢幕的邊等問題
本文提供了基於 win32 的穩定方法,經過了大量裝置的執行測試,基本可以確認本文的方法是非常穩定的全屏視窗的方法,只有很少的電腦才偶爾出現工作列不消失的情況。本文的所使用的方法由 lsj 提供,我只是一個記錄此技術的工具人
本文的方法核心方式是透過 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);
}
}
本文其實是將原本團隊內部的邏輯抄了一次,雖然我能保證團隊內的版本是穩定的,但是我不能保證在抄的過程中,我寫了一些逗比邏輯,讓這個全屏程式碼不穩定
以下是具體的實現方法,如不想了解細節,那請到本文最後複製程式碼即可。本文的方法已經合入到 https://github.com/HandyOrg/HandyControl 倉庫,不想抄程式碼的夥伴可以直接使用 https://www.nuget.org/packages/HandyControl 庫
先來聊聊 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,