DisplaySettingsChanged

DisplaySettingsChanged

本文介绍了移动到更大的显示器时,窗口无法正确调整大小的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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.

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

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.

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

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.

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

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.

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

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?

该窗口来自我编写的一个名为 DpiAwareWindow 的类:

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
}

我不认为问题出在这段代码中;我认为它在 WPF Window 类中.我需要找到一种方法来解决这个问题.但是,我可能是错的.

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.

我有一个测试程序,它包含一个从我的 DpiAwareWindow 类派生的普通窗口.当屏幕分辨率改变时,它表现出类似的行为.但是,作为测试,我更改了代码,因此窗口是从 Window 类派生的,我没有看到行为.所以 DpiAwareWindow 代码中有些东西不起作用.

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.

如果不是问太多,有 VS 2013 的人可以下载 这个 WPF Per Monitor DPI Aware 示例程序,构建它&看看它在以较低的屏幕分辨率启动时是否表现正常,然后再增加屏幕分辨率?

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?

编辑 2

我刚刚做了一些测试,我发现如果我在 WindowProcedureHook 方法的 WinMessages.WM_GETMINMAXINFO 中注释掉整个 WinMessages.WM_GETMINMAXINFO 案例,问题就不会发生代码>开关语句.此代码的目的是限制最大化窗口的大小,使其不会遮挡任务栏.

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.

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

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.

推荐答案

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

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;

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

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.

编辑

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

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.

这篇关于移动到更大的显示器时,窗口无法正确调整大小的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-23 04:08