Last active
August 18, 2023 08:07
-
-
Save isaksavo/f080ea9598cedfe65f6eeb9ccecb6760 to your computer and use it in GitHub Desktop.
Work around bug in windows maximize behavior on multi monitor/multi DPI full screen apps.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Grid Background="Ivory" x:Name="WindowRoot"> | |
<Border HorizontalAlignment="Stretch" VerticalAlignment="Top" Background="MediumAquamarine" x:Name="ButtonContainer" | |
MouseLeftButtonDown="Header_MouseLeftButtonDown"> | |
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> | |
<Label /> | |
<Button Margin="10" Content="Full Screen" x:Name="FullscreenButton" Click="FullScreenClick" Padding="20,5" /> | |
<Button Margin="10" Content="Maximize" Click="MaximizeClick" Padding="20,5" /> | |
<Button Margin="10" Content="Restore" Click="RestoreClick" Padding="20,5" /> | |
<Button Margin="10" Content="Minimize" Click="MinimizeClick" Padding="20,5" /> | |
<Button Margin="10" Content="Quit" Click="QuitClick" Padding="20,5" /> | |
<Label /> | |
</StackPanel> | |
</Border> | |
<Border VerticalAlignment="Bottom" Background="Coral"> | |
<TextBlock Text="Bottom row" Foreground="Black" FontSize="16" HorizontalAlignment="Center" VerticalAlignment="Bottom"/> | |
</Border> | |
</Grid> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public partial class MainWindow : Window | |
{ | |
/// <summary> | |
/// When true, the app will maximize to cover the taskbar | |
/// </summary> | |
public bool IsFullscreen | |
{ | |
get => _maximizeFixer.IsFullScreen; | |
private set => _maximizeFixer.IsFullScreen = value; | |
} | |
private readonly WindowSizeMultiMonitorFixer _maximizeFixer; | |
public MainWindow() | |
{ | |
InitializeComponent(); | |
// This is what gives us the full screen ability. Must be set before the window is initialized and cannot be changed afterwards | |
WindowStyle = WindowStyle.None; | |
AllowsTransparency = true; | |
// Create it here, but enable it when the window is loaded to ensure the HWND is properly initialized | |
_maximizeFixer = new WindowSizeMultiMonitorFixer(this); | |
Loaded += OnLoaded; | |
} | |
private void OnLoaded(object sender, RoutedEventArgs e) | |
{ | |
_maximizeFixer.Enable(); | |
SetFullScreenMode(false); | |
Loaded -= OnLoaded; | |
} | |
private void SetCustomBorder() | |
{ | |
var chrome = GetWindowChrome(); | |
WindowChrome.SetWindowChrome(this, chrome); | |
} | |
private WindowChrome? GetWindowChrome() | |
{ | |
if (WindowState == WindowState.Maximized) | |
// The chrome interferes with the full screen behavior (makes the taskbar appear on top). | |
// Just disable it when maximized, since it isn't needed anyway then | |
return null; | |
return new WindowChrome() | |
{ | |
GlassFrameThickness = new Thickness(0), | |
ResizeBorderThickness = new Thickness(4), | |
CornerRadius = new CornerRadius(4), | |
CaptionHeight = 0, | |
}; | |
} | |
private void FullScreenClick(object sender, RoutedEventArgs e) | |
{ | |
SetFullScreenMode(!IsFullscreen); | |
} | |
private void SetFullScreenMode(bool fullScreen) | |
{ | |
IsFullscreen = fullScreen; | |
if (fullScreen) | |
{ | |
if (WindowState == WindowState.Maximized) | |
WindowState = WindowState.Normal; | |
WindowState = WindowState.Maximized; | |
FullscreenButton.Content = "Exit Full Screen"; | |
} | |
else | |
{ | |
if (WindowState == WindowState.Maximized) | |
{ | |
// Need to cycle to make the change take effect | |
WindowState = WindowState.Normal; | |
WindowState = WindowState.Maximized; | |
} | |
FullscreenButton.Content = "Enter Full Screen"; | |
} | |
} | |
private void MaximizeClick(object sender, RoutedEventArgs e) | |
{ | |
WindowState = WindowState.Maximized; | |
} | |
private void RestoreClick(object sender, RoutedEventArgs e) | |
{ | |
WindowState = WindowState.Normal; | |
} | |
private void MinimizeClick(object sender, RoutedEventArgs e) | |
{ | |
WindowState = WindowState.Minimized; | |
} | |
private void QuitClick(object sender, RoutedEventArgs e) | |
{ | |
Application.Current.Shutdown(0); | |
} | |
/// <inheritdoc /> | |
protected override void OnStateChanged(EventArgs e) | |
{ | |
base.OnStateChanged(e); | |
SetCustomBorder(); | |
} | |
private void Header_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) | |
{ | |
if (e.ClickCount == 2) | |
{ | |
ToggleWindowState(); | |
return; | |
} | |
if (WindowState != WindowState.Maximized) | |
{ | |
DragMove(); | |
} | |
} | |
private void ToggleWindowState() | |
{ | |
if (WindowState != WindowState.Maximized) | |
{ | |
WindowState = WindowState.Maximized; | |
} | |
else | |
{ | |
WindowState = WindowState.Normal; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class WindowSizeMultiMonitorFixer | |
{ | |
private readonly Window _window; | |
private HwndSource? _src; | |
public WindowSizeMultiMonitorFixer(Window window) | |
{ | |
_window = window; | |
} | |
public void Enable() | |
{ | |
if (_src is null) | |
{ | |
WindowInteropHelper wiHelper = new WindowInteropHelper(_window); | |
IntPtr handle = wiHelper.Handle; | |
if (handle == IntPtr.Zero) | |
throw new InvalidOperationException("No HWND could be found for window. Maybe it's not initialized yet?"); | |
_src = HwndSource.FromHwnd(handle) ?? throw new InvalidOperationException("Could not create HwndSource from handle"); | |
} | |
_src.AddHook(CompatibilityMaximizedNoneWindowProc); | |
} | |
public void Disable() | |
{ | |
if (_src is null) | |
return; | |
_src.RemoveHook(CompatibilityMaximizedNoneWindowProc); | |
// _src.Dispose(); | |
// _src = null; | |
} | |
/// <summary> | |
/// When true, the maximized window should occupy the entire monitor (cover the task bar) | |
/// </summary> | |
public bool IsFullScreen { get; set; } | |
private IntPtr CompatibilityMaximizedNoneWindowProc( | |
IntPtr hwnd, | |
int msg, | |
IntPtr wParam, | |
IntPtr lParam, | |
ref bool handled) | |
{ | |
switch (msg) | |
{ | |
case 0x0024: // WM_GETMINMAXINFO | |
NativeMethods.MINMAXINFO mmi = | |
(NativeMethods.MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(NativeMethods.MINMAXINFO))!; | |
// Adjust the maximized size and position | |
// to fit the work area of the correct monitor | |
// int MONITOR_DEFAULTTONEAREST = 0x00000002; | |
IntPtr monitor = NativeMethods.MonitorFromWindow(hwnd, 0x00000002); | |
if (monitor != IntPtr.Zero) | |
{ | |
var monitorInfo = new NativeMethods.MONITORINFO(); | |
NativeMethods.GetMonitorInfo(monitor, monitorInfo); | |
NativeMethods.RECT rcWorkArea = monitorInfo.rcWork; | |
NativeMethods.RECT rcMonitorArea = monitorInfo.rcMonitor; | |
if (IsFullScreen) | |
{ | |
mmi.ptMaxPosition.x = 0; | |
mmi.ptMaxPosition.y = 0; | |
mmi.ptMaxSize.x = Math.Abs(rcMonitorArea.left - rcMonitorArea.right); | |
mmi.ptMaxSize.y = Math.Abs(rcMonitorArea.bottom - rcMonitorArea.top); | |
} | |
else | |
{ | |
mmi.ptMaxPosition.x = Math.Abs(rcWorkArea.left - rcMonitorArea.left); | |
mmi.ptMaxPosition.y = Math.Abs(rcWorkArea.top - rcMonitorArea.top); | |
mmi.ptMaxSize.x = Math.Abs(rcWorkArea.right - rcWorkArea.left); | |
mmi.ptMaxSize.y = Math.Abs(rcWorkArea.bottom - rcWorkArea.top); | |
} | |
} | |
Marshal.StructureToPtr(mmi, lParam, true); | |
handled = true; | |
break; | |
} | |
return IntPtr.Zero; | |
} | |
private static class NativeMethods | |
{ | |
[DllImport("user32")] | |
internal static extern bool GetMonitorInfo(IntPtr hMonitor, MONITORINFO lpmi); | |
[DllImport("user32")] | |
internal static extern IntPtr MonitorFromWindow(IntPtr handle, int flags); | |
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 4)] | |
public class MONITORINFO | |
{ | |
public int cbSize = Marshal.SizeOf(typeof(MONITORINFO)); | |
public RECT rcMonitor = new RECT(); | |
public RECT rcWork = new RECT(); | |
public int dwFlags = 0; | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
public struct RECT | |
{ | |
public int left; | |
public int top; | |
public int right; | |
public int bottom; | |
/// <inheritdoc /> | |
public override string ToString() => $"[{left} {top} {right} {bottom}]"; | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
public struct POINT | |
{ | |
public int x; | |
public int y; | |
public POINT(int x, int y) | |
{ | |
this.x = x; | |
this.y = y; | |
} | |
public override string ToString() => $"[{x},{y}]"; | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
public struct MINMAXINFO | |
{ | |
public POINT ptReserved; | |
public POINT ptMaxSize; | |
public POINT ptMaxPosition; | |
public POINT ptMinTrackSize; | |
public POINT ptMaxTrackSize; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See this StackOverflow post for more info