Maximized WPF window is approximately 8 pixels too large on each side

0

I'm trying to fix the following issues with maximizing my WindowChrome window:

  1. The maximized window extends off screen approximately 8 px on each side.
  2. Aero peek shows a transparent area to the top and left of about 8 px.
  3. Auto hide task bar does not work with the maximized window.

I have found a great article that explains how to fix the taskbar problem. Incidentally, Aero peek, auto hide taskbar, and maximizing the window all work when using this solution, but only when the task bar is set to auto hide. If it's not set to auto hide, the code just returns the standard MINMAXINFO.

The strange thing is, the standard MINMAXINFO seems like it should result in the correct placement and size of the window when maximized. I know this because if I simply subtract 1 pixel from the x value of the MINMAXINFO position or max size, the maximized window is in the correct place but it's width is exactly 1 pixel too small.

However, if I leave the standard MINMAXINFO alone, the window is too large by much more than a single pixel. It's almost as if adding or subtracting anything from ptMaxPosition or ptMaxSize.x causes the window to use the custom MINMAXINFO. I've tried adding and subtracting zero but that didn't work, unfortunately.

Here is a video that shows the issue and will hopefully help to clarify the issue.

Here is the code I'm using:

    public MainWindow()
    {
        SourceInitialized += new EventHandler(Window1_SourceInitialized); 
        InitializeComponent();
    }

    void Window1_SourceInitialized(object sender, EventArgs e)
    {
        WindowSizing.WindowInitialized(this);
    }

    public static class WindowSizing
    {
        const int MONITOR_DEFAULTTONEAREST = 0x00000002;

        #region DLLImports
        [DllImport("shell32", CallingConvention = CallingConvention.StdCall)]
        public static extern int SHAppBarMessage(int dwMessage, ref APPBARDATA pData);

        [DllImport("user32", SetLastError = true)]
        static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

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

        [DllImport("user32")]
        internal static extern IntPtr MonitorFromWindow(IntPtr handle, int flags);
        #endregion

        private static MINMAXINFO AdjustWorkingAreaForAutoHide(IntPtr monitorContainingApplication, MINMAXINFO mmi)
        {
            IntPtr hwnd = FindWindow("Shell_TrayWnd", null);

            if (hwnd == null)
            {
                return mmi;
            }

            IntPtr monitorWithTaskbarOnIt = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);

            if (!monitorContainingApplication.Equals(monitorWithTaskbarOnIt))
            {
                return mmi;
            }

            APPBARDATA abd = new APPBARDATA();

            abd.cbSize = Marshal.SizeOf(abd);

            abd.hWnd = hwnd;

            SHAppBarMessage((int)ABMsg.ABM_GETTASKBARPOS, ref abd);

            int uEdge = GetEdge(abd.rc);

            bool autoHide = Convert.ToBoolean(SHAppBarMessage((int)ABMsg.ABM_GETSTATE, ref abd));

            if (!autoHide)
            {
                return mmi;
            }

            switch (uEdge)
            {
                case (int)ABEdge.ABE_LEFT:
                    mmi.ptMaxPosition.x += 2;
                    mmi.ptMaxTrackSize.x -= 2;
                    mmi.ptMaxSize.x -= 2;
                    break;
                case (int)ABEdge.ABE_RIGHT:
                    mmi.ptMaxSize.x -= 2;
                    mmi.ptMaxTrackSize.x -= 2;
                    break;
                case (int)ABEdge.ABE_TOP:
                    mmi.ptMaxPosition.y += 2;
                    mmi.ptMaxTrackSize.y -= 2;
                    mmi.ptMaxSize.y -= 2;
                    break;
                case (int)ABEdge.ABE_BOTTOM:
                    mmi.ptMaxSize.y -= 2;
                    mmi.ptMaxTrackSize.y -= 2;
                    break;
                default:
                    return mmi;
            }
            return mmi;
        }

        private static int GetEdge(RECT rc)
        {
            int uEdge = -1;

            if (rc.top == rc.left && rc.bottom > rc.right)
            {
                uEdge = (int)ABEdge.ABE_LEFT;
            }
            else if (rc.top == rc.left && rc.bottom < rc.right)
            {
                uEdge = (int)ABEdge.ABE_TOP;
            }
            else if (rc.top > rc.left)
            {
                uEdge = (int)ABEdge.ABE_BOTTOM;
            }
            else
            {
                uEdge = (int)ABEdge.ABE_RIGHT;
            }

            return uEdge;
        }

        public static void WindowInitialized(Window window)
        {
            IntPtr handle = (new WindowInteropHelper(window)).Handle;
            HwndSource.FromHwnd(handle).AddHook(new HwndSourceHook(WindowProc));
        }

        private static IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            switch (msg)
            {
                case 0x0024:
                    WmGetMinMaxInfo(hwnd, lParam);
                    handled = true;
                    break;
            }

            return (IntPtr)0;
        }

        private static void WmGetMinMaxInfo(IntPtr hwnd, IntPtr lParam)
        {
            MINMAXINFO mmi = (MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(MINMAXINFO));
            IntPtr monitorContainingApplication = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);

            if (monitorContainingApplication != System.IntPtr.Zero)
            {
                MONITORINFO monitorInfo = new MONITORINFO();
                GetMonitorInfo(monitorContainingApplication, 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);
                mmi.ptMaxTrackSize.x = mmi.ptMaxSize.x;                                //maximum drag X size for the window
                mmi.ptMaxTrackSize.y = mmi.ptMaxSize.y;                                //maximum drag Y size for the window
                mmi.ptMinTrackSize.x = 200;                                            //minimum drag X size for the window
                mmi.ptMinTrackSize.y = 40;                                             //minimum drag Y size for the window
                mmi = AdjustWorkingAreaForAutoHide(monitorContainingApplication, mmi); //need to adjust sizing if taskbar is set to autohide
            }
            Marshal.StructureToPtr(mmi, lParam, true);
        }

        public enum ABEdge
        {
            ABE_LEFT = 0,
            ABE_TOP = 1,
            ABE_RIGHT = 2,
            ABE_BOTTOM = 3
        }

        public enum ABMsg
        {
            ABM_NEW = 0,
            ABM_REMOVE = 1,
            ABM_QUERYPOS = 2,
            ABM_SETPOS = 3,
            ABM_GETSTATE = 4,
            ABM_GETTASKBARPOS = 5,
            ABM_ACTIVATE = 6,
            ABM_GETAUTOHIDEBAR = 7,
            ABM_SETAUTOHIDEBAR = 8,
            ABM_WINDOWPOSCHANGED = 9,
            ABM_SETSTATE = 10
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct APPBARDATA
        {
            public int cbSize;
            public IntPtr hWnd;
            public int uCallbackMessage;
            public int uEdge;
            public RECT rc;
            public bool lParam;
        }

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

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        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 POINT
        {
            public int x;
            public int y;

            public POINT(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        }

        [StructLayout(LayoutKind.Sequential, Pack = 0)]
        public struct RECT
        {
            public int left;
            public int top;
            public int right;
            public int bottom;
        }

In this code:

    private static void WmGetMinMaxInfo(IntPtr hwnd, IntPtr lParam)
    {
        MINMAXINFO mmi = (MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(MINMAXINFO));
        IntPtr monitorContainingApplication = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);

        if (monitorContainingApplication != System.IntPtr.Zero)
        {
            MONITORINFO monitorInfo = new MONITORINFO();
            GetMonitorInfo(monitorContainingApplication, 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);
            mmi.ptMaxTrackSize.x = mmi.ptMaxSize.x;                                //maximum drag X size for the window
            mmi.ptMaxTrackSize.y = mmi.ptMaxSize.y;                                //maximum drag Y size for the window
            mmi.ptMinTrackSize.x = 200;                                            //minimum drag X size for the window
            mmi.ptMinTrackSize.y = 40;                                             //minimum drag Y size for the window
            mmi = AdjustWorkingAreaForAutoHide(monitorContainingApplication, mmi); //need to adjust sizing if taskbar is set to autohide
        }
        Marshal.StructureToPtr(mmi, lParam, true);
    }

If I change:

        mmi.ptMaxSize.x = Math.Abs(rcWorkArea.right - rcWorkArea.left);
        mmi.ptMaxSize.y = Math.Abs(rcWorkArea.bottom - rcWorkArea.top);

To (for testing purposes):

        mmi.ptMaxSize.x = 1920 //Math.Abs(rcWorkArea.right - rcWorkArea.left);
        mmi.ptMaxSize.y = 1080 //Math.Abs(rcWorkArea.bottom - rcWorkArea.top);

Then the maximized window is off screen by about 8 pixels. However, if I change it to:

        mmi.ptMaxSize.x = 1919 //Math.Abs(rcWorkArea.right - rcWorkArea.left);
        mmi.ptMaxSize.y = 1080 //Math.Abs(rcWorkArea.bottom - rcWorkArea.top);

Then the maximized window is perfect except it's one pixel too small in width. Does anyone know why this happens?

I have some new info to add. My wpf window is using WindowStyle = none, so in order to add back the animations I've created a Window_Loaded event and reset the style of the window as following:

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        hWnd = new WindowInteropHelper(this).Handle;
        //IntPtr myStyle = new IntPtr(WS.WS_CAPTION | WS.WS_MINIMIZEBOX | WS.WS_MAXIMIZEBOX | WS.WS_SYSMENU | WS.WS_SIZEBOX);
        IntPtr myStyle = new IntPtr(WS.WS_CAPTION | WS.WS_MINIMIZEBOX | WS.WS_SYSMENU | WS.WS_SIZEBOX);
        SetWindowLongPtr(new HandleRef(null, hWnd), GWL_STYLE, myStyle);
    }

It seems that something in the WS_MAXIMIZE style is overriding the maximize size, because if I remove this style as shown in the above code, the maximized window fits perfectly! However, I also lose the aero snap functionality and double click caption to maximize which are provided by the WS_MAXIMIZE window style. If I could find a way to add back aero snap without passing the WS_MAXIMIZE style or a way to prevent WS_MAXIMIZE style from overriding my MINMAXINFO, my problems would be solved.

I found this post with more info on this problem. It seems that my suspicions were correct. The issue is that the window manager will only recalculate the maximized window size if the size provided in WM_GETMINMAXINFO is less than the dimensions of the monitor. So the dimensions are correct, they just aren't being used because they're exactly equal to the dimensions of the monitor. Instead, the window manager is using whatever the default maximize dimensions are which also adds 4 pixels on each side to account for borders (which I don't even have).

Update: I have tried almost everything I can think of with WndProc overrides and I am still seeking a solution to this problem, but I have some new info to add that might be of help.

I have insepected the window of Twitch Launcher, which has proper maximize behavior, with Spy++. I noticed that it is an intermediate D3D window where the main application window is a child window of a disabled parent window.

I am very new to the windows api so I'm in way over my head, but I wonder if the child window is set to only fill the dimensions of the disabled parent window that are within the bounds of the screen, and the parent window, which can't be seen, probably still extends 8 pixels off the screen on each side. I don't really know how to create a window within a window, but I'm going to experiment with it and see how it goes. If I get it working I'll post back with the answer. If you have anything to add that might help me out, please let me know.

c#
wpf
winapi
asked on Stack Overflow Jun 10, 2019 by CJF • edited Jun 20, 2019 by CJF

1 Answer

0

If you are configuring the window chrome to hide the OS provided window controls in favor of creating your own then you will need to adjust the border size in your windows content template maximized state to get the window to appear correctly within the screen boundaries. I use the following border style within my Window content template

<ControlTemplate TargetType="{x:Type Window}">
   <Border Background="{TemplateBinding Background}">
      <Border.Style>
         <!-- This style solves the over scanning like problem that happens when you 
              maximize a window that has had its OS provided window decorations removed.  
              This essentially makes the border of the window thicker to make up for the
              over scan when in the maximized state.  When removed from that state, the 
              border goes back to 0.-->
              <Style TargetType="{x:Type Border}">
                 <Setter Property="BorderThickness" Value="0.8"/>
                 <Setter Property="BorderBrush" Value="{StaticResource SecondaryColorBrush}"/>
                 <Setter Property="CornerRadius" Value="1"/>
                 <Style.Triggers>
                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, Path=WindowState}" Value="Maximized">
                       <Setter Property="BorderThickness" Value="7"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, Path=IsActive}" Value="True">
                       <Setter Property="BorderBrush" Value="{StaticResource AccentColorBrush}"/>
                    </DataTrigger>
                 </Style.Triggers>
              </Style>
           </Border.Style>
           <Grid>
           ...

Hope this addresses the problem you are encountering...

answered on Stack Overflow Jun 10, 2019 by Rob Goodwin

User contributions licensed under CC BY-SA 3.0