Мое приложение 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 при изменении разрешения экрана.