Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Window does not resize properly when moved to larger display

Tags:

c#

windows

wpf

My WPF application is exhibiting strange behavior on my two monitor laptop development system. The second monitor has a resolution of 1920 x 1080; the laptop's resolution is 1366 x 768. The laptop is running Windows 8.1 and both displays have their DPI settings set to 100%. When it is plugged in, the second monitor is the primary display. Obviously, when the second monitor is not plugged in, the laptop's display is the primary display.

The application window is always maximized but can be minimized. It cannot be dragged The problem has to do with how the window is displayed when it is moved from one monitor to the other when you plug the second monitor in or unplug it.

When the program is started with the second monitor plugged in, it moves to the laptop's display when it is unplugged. The WPF code handles this change correctly, too. That is, it detects that the original size can't fit on the new monitor so it redraws it to fit. When the second monitor is plugged back in, it moves back to the second monitor and redraws itself at the proper size for that monitor. This is exactly what I want in this scenario. The problem is when the program is started in the other configuration.

When the program is started without the second monitor plugged in, it's drawn at the proper size for the laptop's display. When the second monitor is plugged in with the program running, the window moves to the second monitor, but it is drawn wrong. Since the program is maximized, it has a huge black border surrounding it on three sides with the content displayed in an area the same size it was on the laptop's display.

Edit: I've just finished some testing and WPF does not seem to handle resolution changes from smaller resolution to higher resolution properly. The window's behavior is identical to what I'm getting when I start the program on the laptop's display & then plug in the second monitor. At least it's consistent.

I've found that I can get notification of when the second monitor is plugged in, or of screen resolution changes, by handling the SystemEvents.DisplaySettingsChanged event. In my testing, I've found that the when the window moves from the smaller display to the larger one, that the Width, Height, ActualWidth, and ActualHeight are unchanged when the window moves to the larger window. The best I've been able to do is to get the Height & Width properties to values that match the working area of the monitor, but the ActualWidth and ActualHeight properties won't change.

How do I force the window to treat my problem case as though it were just a resolution change? Or, how do I force the window to change its ActualWidth and ActualHeight properties to the correct values?

The window descends from a class I wrote called DpiAwareWindow:

public class DpiAwareWindow : Window {

    private const int LOGPIXELSX               = 88;
    private const int LOGPIXELSY               = 90;
    private const int MONITOR_DEFAULTTONEAREST = 0x00000002;
    protected enum MonitorDpiType {
        MDT_Effective_DPI = 0,
        MDT_Angular_DPI   = 1,
        MDT_Raw_DPI       = 2,
        MDT_Default       = MDT_Effective_DPI
    }

    public Point CurrentDpi { get; private set; }

    public bool IsPerMonitorEnabled;

    public Point ScaleFactor { get; private set; }

    protected HwndSource source;

    protected Point systemDpi;

    protected Point WpfDpi { get; set; }

    public DpiAwareWindow()
        : base() {
        // Watch for SystemEvent notifications
        SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged;

        // Set up the SourceInitialized event handler
        SourceInitialized += DpiAwareWindow_SourceInitialized;
    }

    ~DpiAwareWindow() {
        // Deregister our SystemEvents handler
        SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged;
    }

    private void DpiAwareWindow_SourceInitialized( object sender, EventArgs e ) {
        source = (HwndSource) HwndSource.FromVisual( this );
        source.AddHook( WindowProcedureHook );

        // Determine if this application is Per Monitor DPI Aware.
        IsPerMonitorEnabled = GetPerMonitorDPIAware() == ProcessDpiAwareness.Process_Per_Monitor_DPI_Aware;

        // Is the window in per-monitor DPI mode?
        if ( IsPerMonitorEnabled ) {
            // It is.  Calculate the DPI used by the System.
            systemDpi = GetSystemDPI();

            // Calculate the DPI used by WPF.
            WpfDpi = new Point {
                X = 96.0 * source.CompositionTarget.TransformToDevice.M11,
                Y = 96.0 * source.CompositionTarget.TransformToDevice.M22
            };

            // Get the Current DPI of the monitor of the window.
            CurrentDpi = GetDpiForHwnd( source.Handle );

            // Calculate the scale factor used to modify window size, graphics and text.
            ScaleFactor = new Point {
                X = CurrentDpi.X / WpfDpi.X,
                Y = CurrentDpi.Y / WpfDpi.Y
            };

            // Update Width and Height based on the on the current DPI of the monitor
            Width  = Width  * ScaleFactor.X;
            Height = Height * ScaleFactor.Y;

            // Update graphics and text based on the current DPI of the monitor.
            UpdateLayoutTransform( ScaleFactor );
        }
    }

    protected Point GetDpiForHwnd( IntPtr hwnd ) {
        IntPtr monitor = MonitorFromWindow( hwnd, MONITOR_DEFAULTTONEAREST );

        uint newDpiX = 96;
        uint newDpiY = 96;
        if ( GetDpiForMonitor( monitor, (int) MonitorDpiType.MDT_Effective_DPI, ref newDpiX, ref newDpiY ) != 0 ) {
            return new Point {
                X = 96.0,
                Y = 96.0
            };
        }

        return new Point {
            X = (double) newDpiX,
            Y = (double) newDpiY
        };
    }

    public static ProcessDpiAwareness GetPerMonitorDPIAware() {
        ProcessDpiAwareness awareness = ProcessDpiAwareness.Process_DPI_Unaware;

        try {
            Process curProcess = Process.GetCurrentProcess();
            int result = GetProcessDpiAwareness( curProcess.Handle, ref awareness );
            if ( result != 0 ) {
                throw new Exception( "Unable to read process DPI level" );
            }

        } catch ( DllNotFoundException ) {
            try {
                // We're running on either Vista, Windows 7 or Windows 8.  Return the correct ProcessDpiAwareness value.
                awareness = IsProcessDpiAware() ? ProcessDpiAwareness.Process_System_DPI_Aware : ProcessDpiAwareness.Process_DPI_Unaware;

            } catch ( EntryPointNotFoundException ) { }

        } catch ( EntryPointNotFoundException ) {
            try {
                // We're running on either Vista, Windows 7 or Windows 8.  Return the correct ProcessDpiAwareness value.
                awareness = IsProcessDpiAware() ? ProcessDpiAwareness.Process_System_DPI_Aware : ProcessDpiAwareness.Process_DPI_Unaware;

            } catch ( EntryPointNotFoundException ) { }
        }

        // Return the value in awareness.
        return awareness;
    }

    public static Point GetSystemDPI() {
        IntPtr hDC = GetDC( IntPtr.Zero );
        int newDpiX = GetDeviceCaps( hDC, LOGPIXELSX );
        int newDpiY = GetDeviceCaps( hDC, LOGPIXELSY );
        ReleaseDC( IntPtr.Zero, hDC );

        return new Point {
            X = (double) newDpiX,
            Y = (double) newDpiY
        };
    }

    public void OnDPIChanged() {
        ScaleFactor = new Point {
            X = CurrentDpi.X / WpfDpi.X,
            Y = CurrentDpi.Y / WpfDpi.Y
        };

        UpdateLayoutTransform( ScaleFactor );
    }

    public virtual void SystemEvents_DisplaySettingsChanged( object sender, EventArgs e ) {
        // Get the handle for this window.  Need to worry about a window that has been created by not yet displayed.
        IntPtr handle = source == null ? new HwndSource( new HwndSourceParameters() ).Handle : source.Handle;

        // Get the current DPI for the window we're on.
        CurrentDpi = GetDpiForHwnd( handle );

        // Adjust the scale factor.
        ScaleFactor = new Point {
            X = CurrentDpi.X / WpfDpi.X,
            Y = CurrentDpi.Y / WpfDpi.Y
        };

        // Update the layout transform
        UpdateLayoutTransform( ScaleFactor );
    }

    private void UpdateLayoutTransform( Point scaleFactor ) {
        if ( IsPerMonitorEnabled ) {
            if ( ScaleFactor.X != 1.0 || ScaleFactor.Y != 1.0 ) {
                LayoutTransform = new ScaleTransform( scaleFactor.X, scaleFactor.Y );
            } else {
                LayoutTransform = null;
            }
        }
    }

    public virtual IntPtr WindowProcedureHook( IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled ) {
        // Determine which Monitor is displaying the Window
        IntPtr monitor = MonitorFromWindow( hwnd, MONITOR_DEFAULTTONEAREST );

        // Switch on the message.
        switch ( (WinMessages) msg ) {
            case WinMessages.WM_DPICHANGED:
                // Marshal the value in the lParam into a Rect.
                RECT newDisplayRect = (RECT) Marshal.PtrToStructure( lParam, typeof( RECT ) );

                // Set the Window's position & size.
                Vector ul = source.CompositionTarget.TransformFromDevice.Transform( new Vector( newDisplayRect.left, newDisplayRect.top ) );
                Vector hw = source.CompositionTarget.TransformFromDevice.Transform( new Vector( newDisplayRect.right = newDisplayRect.left, newDisplayRect.bottom - newDisplayRect.top ) );
                Left      = ul.X;
                Top       = ul.Y;
                Width     = hw.X;
                Height    = hw.Y;

                // Remember the current DPI settings.
                Point oldDpi = CurrentDpi;

                // Get the new DPI settings from wParam
                CurrentDpi = new Point {
                    X = (double) ( wParam.ToInt32() >> 16 ),
                    Y = (double) ( wParam.ToInt32() & 0x0000FFFF )
                };

                if ( oldDpi.X != CurrentDpi.X || oldDpi.Y != CurrentDpi.Y ) {
                    OnDPIChanged();
                }

                handled = true;
                return IntPtr.Zero;

            case WinMessages.WM_GETMINMAXINFO:
                // lParam has a pointer to the MINMAXINFO structure.  Marshal it into managed memory.
                MINMAXINFO mmi = (MINMAXINFO) Marshal.PtrToStructure( lParam, typeof( MINMAXINFO ) );
                if ( monitor != IntPtr.Zero ) {
                    MONITORINFO monitorInfo = new MONITORINFO();
                    GetMonitorInfo( monitor, monitorInfo );

                    // Get the Monitor's working area
                    RECT rcWorkArea    = monitorInfo.rcWork;
                    RECT rcMonitorArea = monitorInfo.rcMonitor;

                    // Adjust the maximized size and position to fit the work area of the current monitor
                    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 );
                }

                // Copy our changes to the mmi object back to the original
                Marshal.StructureToPtr( mmi, lParam, true );
                handled = true;
                return IntPtr.Zero;

            default:
                // Let the WPF code handle all other messages. Return 0.
                return IntPtr.Zero;
        }
    }

    [DllImport( "user32.dll", CallingConvention = CallingConvention.StdCall )]
    protected static extern IntPtr GetDC( IntPtr hWnd );

    [DllImport( "gdi32.dll", CallingConvention = CallingConvention.StdCall )]
    protected static extern int GetDeviceCaps( IntPtr hDC, int nIndex );

    [DllImport( "shcore.dll", CallingConvention = CallingConvention.StdCall )]
    protected static extern int GetDpiForMonitor( IntPtr hMonitor, int dpiType, ref uint xDpi, ref uint yDpi );

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

    [DllImport( "shcore.dll", CallingConvention = CallingConvention.StdCall )]
    protected static extern int GetProcessDpiAwareness( IntPtr handle, ref ProcessDpiAwareness awareness );

    [DllImport( "user32.dll", CallingConvention = CallingConvention.StdCall )]
    protected static extern bool IsProcessDpiAware();

    [DllImport( "user32.dll", CallingConvention = CallingConvention.StdCall )]
    protected static extern IntPtr MonitorFromWindow( IntPtr hwnd, int flag );

    [DllImport( "user32.dll", CallingConvention = CallingConvention.StdCall )]
    protected static extern void ReleaseDC( IntPtr hWnd, IntPtr hDC );
}

public enum SizeMessages {
    SIZE_RESTORED  = 0,
    SIZE_MINIMIZED = 1,
    SIZE_MAXIMIZED = 2,
    SIZE_MAXSHOW   = 3,
    SIZE_MAXHIDE   = 4
}

public enum WinMessages : int {
    WM_DPICHANGED        = 0x02E0,
    WM_GETMINMAXINFO     = 0x0024,
    WM_SIZE              = 0x0005,
    WM_WINDOWPOSCHANGING = 0x0046,
    WM_WINDOWPOSCHANGED  = 0x0047,
}

public enum ProcessDpiAwareness {
    Process_DPI_Unaware           = 0,
    Process_System_DPI_Aware      = 1,
    Process_Per_Monitor_DPI_Aware = 2
}

I don't think that the problem is in this code; I think it's in the WPF Window class. I need to find a way to work around this problem. However, I could be wrong.

EDIT:

I have a test program which contains a normal window that descends from my DpiAwareWindow class. It is exhibiting similar behavior when the screen resolution changes. But, as a test, I changed the code so the window descended from the Window class and I did not see the behavior. So there is something in the DpiAwareWindow code that doesn't work.

If it's not too much to ask, could someone with VS 2013 download this WPF Per Monitor DPI Aware sample program, build it & see if it behaves properly when started with a lower screen resolution and then the screen resolution is increased?

Edit 2

I've just did some testing and I've found that the problem does not happen if I comment out the entire WinMessages.WM_GETMINMAXINFO case in the WindowProcedureHook method's switch statement. The purpose of this code is to limit the size of a maximized window so it does not obscure the Task Bar.

This code was added to keep a maximized window from obscuring the task bar. There seems to be some kind of interaction between what it returns and whatever logic is running in WPF when the screen resolution changes.

like image 239
Tony Vitabile Avatar asked Feb 28 '14 19:02

Tony Vitabile


People also ask

How do I fix scaling between monitors?

Select Display > Change the size of text, apps, and other items, and then adjust the slider for each monitor. Right-click the application, select Properties, select the Compatibility tab, and then select the Disable display scaling on high DPI settings check box.

Why do my Windows shrink when I move them?

When you try to resize a window or move a window from one monitor to a higher-resolution monitor and it disappears, what is happening is the window is getting “caught between screens.” This can be the result of using a high screen resolution on a small screen.


2 Answers

I've finally resolved this problem. It turns out that what I needed to do is change one line in the switch statement in the WindowProcedureHook method:

        case WinMessages.WM_GETMINMAXINFO:
            // lParam has a pointer to the MINMAXINFO structure.  Marshal it into managed memory.
            MINMAXINFO mmi = (MINMAXINFO) Marshal.PtrToStructure( lParam, typeof( MINMAXINFO ) );
            if ( monitor != IntPtr.Zero ) {
                MONITORINFO monitorInfo = new MONITORINFO();
                GetMonitorInfo( monitor, monitorInfo );

                // Get the Monitor's working area
                RECT rcWorkArea    = monitorInfo.rcWork;
                RECT rcMonitorArea = monitorInfo.rcMonitor;

                // Adjust the maximized size and position to fit the work area of the current monitor
                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 );
            }

            // Copy our changes to the mmi object back to the original
            Marshal.StructureToPtr( mmi, lParam, true );
            handled = false; // This line used to set handled to true
            return IntPtr.Zero;

With this change, the code that's normally executed in WPF when the WM_GETMINMAXINFO message is received still runs, but it uses the change to the MINMAXINFO object made by the code in order to do its work. With this change, the WPF window handles the resolution changes properly.

EDIT

And it turns out that the code no longer needs to look specifically for a screen resolution or installed monitor change. That is, the SystemEvent.DisplaySettingsChanged event handler is no longer needed.

like image 171
Tony Vitabile Avatar answered Sep 25 '22 05:09

Tony Vitabile


Turns out its not a complicated fix. The MinTrackSize point (bounds) needs to be set to the working area dimensions of the secondary monitor.

private static void WmGetMinMaxInfo(System.IntPtr hwnd, System.IntPtr lParam)
{
    MINMAXINFO mmi = (MINMAXINFO)System.Runtime.InteropServices.Marshal.PtrToStructure(lParam, typeof(MINMAXINFO));

    /*  0x0001        // center rect to monitor 
        0x0000        // clip rect to monitor 
        0x0002        // use monitor work area 
        0x0000        // use monitor entire area */

    int MONITOR_DEFAULTTONEAREST = 0x00000002;
    System.IntPtr monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);

    if (monitor != System.IntPtr.Zero)
    {
        MONITORINFO monitorInfo = new MONITORINFO();
        GetMonitorInfo(monitor, monitorInfo);
        RECT rcWorkArea = monitorInfo.rcWork;
        RECT rcMonitorArea = monitorInfo.rcMonitor;

        // set the maximize size of the application
        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);

        // reset the bounds of the application to the monitor working dimensions
        mmi.ptMaxTrackSize.x = mmi.ptMaxSize.x;
        mmi.ptMaxTrackSize.y = mmi.ptMaxSize.y;
    }

    System.Runtime.InteropServices.Marshal.StructureToPtr(mmi, lParam, true);
}

[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct MINMAXINFO
{
    public POINT ptReserved;
    public POINT ptMaxSize;
    public POINT ptMaxPosition;
    public POINT ptMinTrackSize;
    public POINT ptMaxTrackSize;
};
like image 39
Lenny Avatar answered Sep 23 '22 05:09

Lenny