移动到较大的显示器时,窗口无法正确调整大小

时间:2014-02-28 19:17:05

标签: c# wpf windows

我的WPF应用程序在我的两台显示器笔记本电脑开发系统上表现出奇怪的行为。第二台显示器的分辨率为1920 x 1080;笔记本电脑的分辨率为1366 x 768.笔记本电脑运行的是Windows 8.1,两台显示器的DPI设置都设置为100%。插入后,第二个显示器是主显示器。显然,当未插入第二台显示器时,笔记本电脑的显示屏是主显示屏。

应用程序窗口始终最大化,但可以最小化。它无法拖动问题与当您将第二个显示器插入或拔出时将窗口从一个显示器移动到另一个显示器时的显示方式有关。

当插入第二台显示器启动程序时,它会在拔下插头时移动到笔记本电脑的显示屏上。 WPF代码也正确处理此更改。也就是说,它检测到原始大小不适合新显示器,因此重绘它以适应。当第二个显示器重新插入时,它会移回第二个显示器并以适合该显示器的大小重新绘制。这正是我在这种情况下想要的。问题是当程序在其他配置中启动时。

在没有插入第二台显示器的情况下启动程序时,它会以笔记本电脑显示屏的正确尺寸绘制。在程序运行时插入第二台显示器时,窗口移动到第二台显示器,但绘制错误。由于程序最大化,它的三面都有一个巨大的黑色边框,内容显示在与笔记本电脑显示屏上相同的区域。

修改 的 我刚刚完成了一些测试,WPF似乎无法正确处理从较小分辨率到较高分辨率的分辨率变化。窗口的行为与我在笔记本电脑的显示屏上启动程序时的行为相同。然后插入第二台显示器。至少它是一致的。

我发现通过处理SystemEvents.DisplaySettingsChanged事件,我可以获得插入第二台显示器的时间或屏幕分辨率更改的通知。在我的测试中,我发现当窗口从较小的显示移动到较大的显示时,WidthHeightActualWidthActualHeight是当窗口移动到更大的窗口时不变。我能做的最好的事情就是获得Height& Width属性值与显示器的工作区域匹配,但ActualWidthActualHeight属性不会更改。

如何强制窗口处理我的问题案例,好像它只是一个分辨率更改?或者,如何强制窗口将其ActualWidthActualHeight属性更改为正确的值?

窗口来自我写的一个名为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
}

我认为问题不在于此代码中;我认为它在WPF Window课程中。我需要找到解决这个问题的方法。但是,我可能是错的。

修改

我有一个测试程序,其中包含一个从我的DpiAwareWindow类下降的普通窗口。当屏幕分辨率改变时,它表现出类似的行为。但是,作为测试,我更改了代码,因此窗口来自Window类,我没有看到行为。因此DpiAwareWindow代码中的某些内容无效。

如果要问的话不是太多,那么VS 2013下载this WPF Per Monitor DPI Aware sample program的人可以构建它吗?看看它是否以较低的屏幕分辨率启动时表现正常,然后屏幕分辨率会增加?

修改2

我刚刚做了一些测试,我发现如果我在WinMessages.WM_GETMINMAXINFO方法的WindowProcedureHook语句中注释掉整个switch案例,就不会发生问题。此代码的目的是限制最大化窗口的大小,以便它不会模糊任务栏。

添加此代码是为了使最大化窗口不会遮挡任务栏。当屏幕分辨率发生变化时,它返回的内容与WPF中运行的逻辑之间似乎存在某种交互。

2 个答案:

答案 0 :(得分:5)

我终于解决了这个问题。事实证明,我需要做的是在switch方法的WindowProcedureHook语句中更改一行:

        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;

通过此更改,收到WM_GETMINMAXINFO消息时通常在WPF中执行的代码仍会运行,但它会使用对代码生成的MINMAXINFO对象的更改来执行其工作。通过此更改,WPF窗口可以正确处理分辨率更改。

修改

事实证明,代码不再需要专门针对屏幕分辨率或安装的显示器更改。也就是说,不再需要SystemEvent.DisplaySettingsChanged事件处理程序。

答案 1 :(得分:0)

原来这不是一个复杂的修复。 需要将MinTrackSize点(边界)设置为辅助监视器的工作区域尺寸。

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