Sergio
Sergio

Reputation: 2108

WindowChrome ResizeBorderThickness issue

I am styling a Window, but I noticed this strange behaviour of WindowChrome (in .NET FW 4.0, from external Microsoft.Windows.Shell dll).

I set the WindowChrome with AllowTransparency = true and WindowStyle = None.

If I set the WindowChrome's ResizeBorderThickness <= 7 everything works perfectly, but if I do

ResizeBorderThickness="8"

or more, when the Window is Maximized I can't drag it from the last top pixel near the top edge of the screen, and for each +1 exceeding 7 I must start dragging 1 pixel more down from the edge.

This is annoying 'cause it disable a common behaviour when closing a window, forcing me to set it to 7 or less.

Can someone explain me this behaviour?

Thank you!

Upvotes: 5

Views: 4826

Answers (3)

Gh61
Gh61

Reputation: 9756

I've used Noir answer and it looked like it's working.

PROBLEM 1: In some configuration of different sized screens the window will still get incorrect size and will hide part of itself under the taskbar (tested on Windows 10 and 11).

PROBLEM 2: I've find wrong behaviour for specific setting of Main screen scaled up to 150% and secondary scaled to lower value - eg. 100%. When maximizing on secondary screen, the size is not calculated correctly and the content of the window is 150% sized.

So I have added fixes for these 2 problems:

  1. new method RefreshMaxHeight that will set MaxHeight and MaxWidth of the window (called from OnStateChanged), so the window will not hide under taskbar.

  2. I've changed the algorithm for setting max size of the window in HookProc call, to be only correcting the wrong position original message. (Because of the first fix here, we don't need to set the size to the size of the WorkingArea).

You can see the whole up-to-date solution here: https://github.com/Gh61/wpf-custom-window-snap/blob/main/MainWindow.Fullscreen.cs

The code, doing all the magic:

/// <summary>
/// This needs to be called in OnStateChanged - for fullscreen size to correctly work.
/// </summary>
private void RefreshMaxHeight()
{
    if (WindowState == WindowState.Maximized)
    {
        // se the MaxHeight to the size of WorkingArea, where the window is
        var windowHandle = new WindowInteropHelper(this).Handle;
        var screen = System.Windows.Forms.Screen.FromHandle(windowHandle);

        // FullScreen is then limited by this and will not go behind the taskbar
        this.MaxHeight = screen.WorkingArea.Height;
        this.MaxWidth = screen.WorkingArea.Width;
    }
    else
    {
        // reset MaxHeight
        this.MaxHeight = double.PositiveInfinity;
        this.MaxWidth = double.PositiveInfinity;
    }
}

private IntPtr HookProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    if (msg == WM_GETMINMAXINFO)
    {
        // We need to tell the system what our size should be when maximized. Otherwise it will cover the whole screen,
        // including the task bar.
        MINMAXINFO mmi = (MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(MINMAXINFO));

        // Adjust the maximized size and position to fit the work area of the correct monitor
        IntPtr monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);

        if (monitor != IntPtr.Zero)
        {
            MONITORINFO monitorInfo = new MONITORINFO();
            monitorInfo.cbSize = Marshal.SizeOf(typeof(MONITORINFO));
            GetMonitorInfo(monitor, ref monitorInfo);

            RECT rcWorkArea = monitorInfo.rcWork;
            RECT rcMonitorArea = monitorInfo.rcMonitor;

            // this can be outside the working area
            var originalX = mmi.ptMaxPosition.X;// eg. -6
            var originalY = mmi.ptMaxPosition.Y;// eg. -6

            // do the position correction
            mmi.ptMaxPosition.X = Math.Abs(rcWorkArea.Left - rcMonitorArea.Left);
            mmi.ptMaxPosition.Y = Math.Abs(rcWorkArea.Top - rcMonitorArea.Top);

            var difX = originalX - mmi.ptMaxPosition.X;
            var difY = originalY - mmi.ptMaxPosition.Y;

            // Do the size correction based on original Position
            mmi.ptMaxSize.X += 2 * difX;
            mmi.ptMaxSize.Y += 2 * difY;
        }

        Marshal.StructureToPtr(mmi, lParam, true);
    }

    return IntPtr.Zero;
}

and Finally calling the code:

protected override void OnSourceInitialized(EventArgs e)
{
    base.OnSourceInitialized(e);
    ((HwndSource)PresentationSource.FromVisual(this)).AddHook(HookProc); // <-- hooking HERE
}

protected override void OnStateChanged(EventArgs e)
{
    base.OnStateChanged(e);

    RefreshMaxHeight(); // <-- fixing Height HERE

    RefreshMaximizeButton();
    RefreshWindowChrome();
}

Upvotes: 0

Isak Savo
Isak Savo

Reputation: 35934

If you have a full screen application (WindowStyle set to None and AllowTransparency set to true) you need to make some tweaks to the otherwise excellent answer from Noir:

Instead of using the work area to determine the max boundaries, use the rcMonitor:

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);

To make full screen work, the window need to remove the WindowChrome completely when in maximized mode:

// Run this whenever the window state changes (maximize, restore, ...)
WindowChrome chrome ;
if (WindowState == WindowState.Maximized)
    chrome = null;
else
    chrome = new WindowChrome() { ... }
WindowChrome.SetWindowChrome(this, chrome);

By wrapping the logic in a class that can keep state, we can even make our window enter and exit full screen mode at will:

if (IsFullScreen) 
{
    // Tell Windows that we want to occupy the entire monitor
    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
{
    // Tell Windows that we want to occupy the entire work area of the
    // current monitor (leaves the task bar visible)
    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);
}

A complete example with a WPF window using it is available as a github gist.

Upvotes: 0

Noir
Noir

Reputation: 417

The window doesn't have a strange behavior. Instead of it, the window has two strange behaviors.

  • (A) First strange behavior:

"[...] when the Window is Maximized I can't drag it from the last top pixel near the top edge of the screen [...]"

This behavior is due to the edge to resize is still active when the window changes to its maximized state. Indeed, this edge is always active. Setting the ResizeBorderThickness property, WindowChrome reserve that amount of pixels to control the behavior of resizing the window. Given that in maximized mode the resize events aren't allowed, then you will notice that these pixels don't allow any kind of behavior. This is precisely because WindowChrome reserve exclusively those pixels that control the behavior of resizing.

What is the solution? You need to notify WindowChrome must change to establish the ResizeBorderThickness property to 0 when the window is maximized. This can be done simply by setting WindowChrome again by a Trigger in xaml:

<Trigger Property="WindowState" Value="Maximized">
     <Setter Property="WindowChrome.WindowChrome">
          <Setter.Value>
               <WindowChrome ResizeBorderThickness="0" [...] />
          </Setter.Value>
     </Setter>
</Trigger>

Note: This can also do so in run-time code

  • (B) Second strange behavior:

"[...] If I set the WindowChrome's ResizeBorderThickness <= 7 everything works perfectly [...] and for each +1 exceeding 7 I must start dragging 1 pixel more down from the edge. [...]"

Take care. Actually this behavior isn't due to the value set in ResizeBorderThickness but this is due to set the property WindowStyle=None. When this property is set, the window takes on a strange behavior when maximized:

  1. The upper left edge of the window is not positioned at the point (0,0) of the current screen, but rather erratically becomes negative (in your case, on the Y axis the value seems to be -7).

  2. The size of the window takes the size of the current screen, when the normal behavior should be that the size of the window takes the size of current work area (current screen except task bar, etc...) of the current screen.

This strange behavior that takes the window makes that 7 pixels reserved for 'WindowChrome' aren't visible in the current screen (with ResizeBorderThickness="7", obviously), and therefore gives you the feeling that the property ResizeBorderThickness="7" works properly, when it isn't. In fact, this justifies the behavior when ResizeBorderThickness takes the value 8 or more.

What is the solution? It is necessary to force the window when maximizing a size and position on the work area of the current screen. Warning: if you only do it for the primary screen, the maximize event doesn't work properly for multiple screens.

The code that solves this problem I solved by calling an external API:

[DllImport("user32")]
internal static extern bool GetMonitorInfo(IntPtr hMonitor, MONITORINFO lpmi);
[DllImport("user32")]
internal static extern IntPtr MonitorFromWindow(IntPtr handle, int flags);

Defining the classes and structs:

[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;
}

[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
      public int x;
      public int y;
      public POINT(int x, int y) { this.x = x; this.y = y; }
}

[StructLayout(LayoutKind.Sequential)]
public struct MINMAXINFO
{
      public POINT ptReserved;
      public POINT ptMaxSize;
      public POINT ptMaxPosition;
      public POINT ptMinTrackSize;
      public POINT ptMaxTrackSize;
}

And finally defining the functions that add the hook WndProc to the window:

public static void CompatibilityMaximizedNoneWindow(Window window)
{
      WindowInteropHelper wiHelper = new WindowInteropHelper(window);
      System.IntPtr handle = wiHelper.Handle;
      HwndSource.FromHwnd(handle).AddHook(
                new HwndSourceHook(CompatibilityMaximizedNoneWindowProc));
}

private static System.IntPtr CompatibilityMaximizedNoneWindowProc(
    System.IntPtr hwnd,
    int msg,
    System.IntPtr wParam,
    System.IntPtr lParam,
    ref bool handled)
{
      switch (msg)
      {
      case 0x0024:    // WM_GETMINMAXINFO
            MINMAXINFO mmi =
                (MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(MINMAXINFO));

                // Adjust the maximized size and position
                // to fit the work area of the correct monitor
                // int MONITOR_DEFAULTTONEAREST = 0x00000002;
                System.IntPtr monitor = MonitorFromWindow(hwnd, 0x00000002);

                if (monitor != System.IntPtr.Zero)
                {

                      MONITORINFO monitorInfo = new MONITORINFO();
                      GetMonitorInfo(monitor, monitorInfo);
                      RECT rcWorkArea = monitorInfo.rcWork;
                      RECT rcMonitorArea = monitorInfo.rcMonitor;
                      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 (System.IntPtr)0;
}

With CompatibilityMaximizedNoneWindow API, you simply call the API in the constructor of the window, something like this:

public MyWindow
{
      [...]
      MyNamespace.CompatibilityMaximizedNoneWindow(this);
}

And the second strange behavior must be resolved. You will notice that the code to work, you must add reference PresentationFramework and the namespace System.Windows.Interop.

Upvotes: 15

Related Questions