Окно не изменяет размер при перемещении на больший экран

Мое приложение WPF демонстрирует странное поведение на моей системе разработки ноутбуков с двумя мониторами. Второй монитор имеет разрешение 1920 x 1080; разрешение для ноутбука составляет 1366 x 768. На ноутбуке работает Windows 8.1, и на обоих дисплеях установлены настройки DPI на 100%. Когда он подключен, вторым монитором является основной монитор. Очевидно, что когда второй монитор не подключен, дисплей ноутбука является основным дисплеем.

Окно приложения всегда максимизировано, но может быть сведено к минимуму. Его нельзя перетаскивать Проблема связана с тем, как окно отображается, когда оно перемещается с одного монитора на другой, когда вы подключаете второй монитор или отсоединяете его.

Когда программа запускается при включенном втором мониторе, она переходит на дисплей ноутбука, когда он отключен от сети. Код WPF также корректно обрабатывает это изменение. То есть, он обнаруживает, что исходный размер не может поместиться на новом мониторе, чтобы он перерисовывал его для соответствия. Когда второй монитор снова подключен, он возвращается к второму монитору и перерисовывает себя под нужным размером для этого монитора. Это именно то, что я хочу в этом сценарии. Проблема заключается в том, что программа запускается в другой конфигурации.

Когда программа запускается без второго подключенного монитора, она нарисована на нужный размер для дисплея ноутбука. Когда второй монитор подключен к запущенной программе, окно перемещается на второй монитор, но отображается неправильно. Поскольку программа максимизирована, она имеет огромную черную границу, окружающую ее с трех сторон, с содержимым, отображаемым в области того же размера, что и на дисплее ноутбука.

Edit: Я только что закончил тестирование, и WPF, похоже, не справляется с разрешением разрешения с меньшего разрешения на более высокое разрешение. Поведение окна идентично тому, что я получаю, когда запускаю программу на дисплее ноутбука, а затем подключаю второй монитор. По крайней мере, это непротиворечиво.

Я обнаружил, что могу получить уведомление о том, когда второй монитор подключен или изменения разрешения экрана, обработав событие SystemEvents.DisplaySettingsChanged. В моем тестировании я обнаружил, что когда окно перемещается с меньшего дисплея на большее, то значения Width, Height, ActualWidth и ActualHeight не изменяются, когда окно перемещается к большему окно. Лучшее, что я смог сделать, - получить свойства Height и Width до значений, соответствующих рабочей области монитора, но свойства ActualWidth и ActualHeight не будут изменены.

Как заставить окно обрабатывать мой вопрос, как будто это просто изменение разрешения? Или, как заставить окно изменить его свойства ActualWidth и ActualHeight на правильные значения?

Окно спускается из класса, который я написал под названием 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 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 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. Мне нужно найти способ обойти эту проблему. Однако я мог ошибаться.

EDIT:

У меня есть тестовая программа, которая содержит нормальное окно, которое сходит с моего класса DpiAwareWindow. Он проявляет подобное поведение при изменении разрешения экрана. Но, как тест, я изменил код, чтобы окно опустилось из класса Window, и я не видел поведения. Итак, что-то в коде DpiAwareWindow не работает.

Если это не так уж много, чтобы спросить, мог ли кто-то с VS 2013 скачать этот WPF Per Monitor DPI Aware образец программы, построить его и посмотреть, он корректно работает при запуске с более низким разрешением экрана, а затем разрешение экрана увеличивается?

Изменить 2

Я только что провел некоторое тестирование, и я обнаружил, что проблема не возникает, если я прокомментирую весь случай WinMessages.WM_GETMINMAXINFO в WindowProcedureHook методе switch. Цель этого кода - ограничить размер максимизированного окна, чтобы он не скрывал панель задач.

Этот код был добавлен, чтобы сохранить максимально развернутое окно, закрывая панель задач. Кажется, что есть какое-то взаимодействие между тем, что он возвращает, и какой бы логикой не выполнялось в WPF при изменении разрешения экрана.

Ответ 1

Я наконец решил эту проблему. Оказывается, что мне нужно было сделать одну строку в выражении 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 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;

При этом изменении код, который обычно выполняется в WPF при получении сообщения WM_GETMINMAXINFO, все еще выполняется, но он использует изменение для объекта MINMAXINFO, созданного кодом, чтобы выполнить свою работу. С этим изменением окно WPF правильно обрабатывает разрешение.

ИЗМЕНИТЬ

И получается, что код больше не нужно искать конкретно для разрешения экрана или установки монитора. То есть обработчик событий SystemEvent.DisplaySettingsChanged больше не нужен.

Ответ 2

Вызывает не сложное исправление. Для параметра 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;
};