Skip to content

Instantly share code, notes, and snippets.

@isaksavo
Last active August 18, 2023 08:07
Show Gist options
  • Save isaksavo/f080ea9598cedfe65f6eeb9ccecb6760 to your computer and use it in GitHub Desktop.
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.
<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>
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;
}
}
}
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;
}
}
}
@isaksavo
Copy link
Author

See this StackOverflow post for more info

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment