HorizontalOffset идет в неправильном направлении для основного экрана с высоким разрешением
В настоящее время я работаю над некоторыми проблемами с высоким DPI в нашем приложении WPF (.NET 4.6.1 - Активна поддержка DPI системы).
Как правило, приложение выполняет то, что мы ожидаем, - масштабировать в зависимости от текущих настроек DPI для дисплеев; также при перемещении его с экрана A @ 100% на экран B @ 150% оно корректно изменяет общий масштаб "на половину".
Большинство открытых вопросов было там, потому что у нас были некоторые расчеты на основе пикселей /DIP, которые не учитывали настройку DPI. Это я исправил, рассчитав правильные значения DPI:
var source = PresentationSource.FromVisual(this);
var dpiX = source?.CompositionTarget?.TransformToDevice.M11 ?? 1;
var dpiY = source?.CompositionTarget?.TransformToDevice.M22 ?? 1;
Там я обнаружил первую странную вещь (по крайней мере, для меня):
- Если основной дисплей установлен, например, 125%, я получаю 1,25 для
dpiX
для всех экранов, даже для вторичного экрана @ 100%, но там все значения пикселей уже умножены на 1,25 (что означает, что экран 1600x1200 пикселей имеет рабочий размер 2000x1500). - И наоборот, если основной экран на 100%, а дополнительный экран, например, на 150%: я всегда получаю 1 за
dpiX
, но все значения уже верны и коррекция не требуется (=> или умножение / деление на 1 не нарушает его).
Но теперь к моей актуальной проблеме:
У меня есть несколько всплывающих окон, которые я помещаю в центр их целей размещения со следующей привязкой:
<Popup.HorizontalOffset>
<MultiBinding Converter="{lth:CenterConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="PlacementTarget.ActualWidth" />
<Binding RelativeSource="{RelativeSource Self}" Path="Child.ActualWidth" />
<Binding RelativeSource="{RelativeSource Self}" Path="." />
</MultiBinding>
</Popup.HorizontalOffset>
и конвертер:
public class CenterConverter : MarkupExtension, IMultiValueConverter
{
public override object ProvideValue(IServiceProvider serviceProvider) => this;
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Any(v => v == DependencyProperty.UnsetValue))
return Double.NaN;
double placementTargetWidth = (double)values[0];
double elementWidth = (double)values[1];
var offset = (placementTargetWidth - elementWidth) / 2;
////if (values.Length >= 3 && values[2] is Visual)
////{
//// var source = PresentationSource.FromVisual((Visual)values[2]);
//// var dpiX = source?.CompositionTarget?.TransformToDevice.M11 ?? 1;
//// offset *= -1; //dpiX;
////}
return offset;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotSupportedException(); }
}
Для случая 2 все уже работает правильно без закомментированного кода, но для случая 1 я попытался разделить и умножить значение DPI, но в итоге правильным было умножить его на -1
чтобы заставить его работать правильно.
Почему это так?
И как я могу безопасно определить, когда это необходимо? dpiX > 1
?
Я также открыт для других решений проблемы масштабирования или размещения центра в целом.
PS: Я использую Windows 10 1703 с установленным.NET 4.7 (приложение по-прежнему ориентировано на 4.6.1 по некоторым другим причинам).
ОБНОВИТЬ:
Я создал демо-решение: https://github.com/chrfin/HorizontalOffsetError
Если основной экран на 100%, это правильно:
но если основной экран, например, 125%, он выключен:
НО, если я добавлю *-1 к смещению, это снова правильно:
...но почему?
1 ответ
Я сделал что-то подобное. Я должен был пойти в winforms, чтобы выполнить это:
/// <summary>
/// Calculates and sets the correct start location for a dialog to appear.
/// </summary>
/// <param name="form">The dialog to be displayed</param>
/// <param name="screen">Desired screen</param>
public static void SetStartLocation(Form form, Screen screen)
{
form.StartPosition = FormStartPosition.Manual;
// Calculate the new top left corner of the form, so it will be centered.
int newX = (screen.WorkingArea.Width - form.Width) / 2 + screen.WorkingArea.X;
int newY = (screen.WorkingArea.Height - form.Height) / 2 + screen.WorkingArea.Y;
form.Location = new Point(newX, newY);
}
Запустите этот код в отладчике, затем посмотрите на экран, осмотрите все переменные экрана и убедитесь, что они имеют смысл, но не будут. Вы делаете предположение.
посмотрите на GetScreen()
/// <summary>
/// Handles drags that go "outside" the screen and returns the mouse delta from the last mouse position.
/// When the user wants to edit a value greater, but the mouse is at the edge of the screen we want to wrap the mouse position.
///
/// Wrapping the mouse is non trival do to multiple monitor setups but Forms has a screen class that encapsolates the
/// low level calls required to determine the screen the user is working on and it's bounds.
/// Wrapping is confusing because there are edge cases which mess with the coordinate system. For example, if your primary monitor
/// is your second monitor which is on the right and the app is on your left screen the mouse
/// coordinates will be in the negative and second monitors mouse coords would max at 1920 ( depending on resolution ).
/// Alternatively if screen 1 is your primary and screen 2 is your secondary then X=3xxx will be your far right max mouse position.
///
/// When we wrap, we need to take that into account and not have the delta go out of whack.
/// Note: This mouse wrapping works exactly the same as unity does when the user does a value drag.
/// Note: When the mouse does a wrap, we musn't set get the position until the next move event, or the set will fail.
/// </summary>
/// <param name="delta"> the amount the mouse movement has changed since this was last called</param>
/// <returns>true if delta was gotten succesfully</returns>
private bool GetScreenWrappedDragVector(out Vector delta)
{
delta = new Vector(); // Always set the out parameter
// We need to determine what our window is, otherwise the coordinate system will be off if your in a child window on other monitor!
var element = Mouse.DirectlyOver as UIElement;
if (element != null)
{
Window parentWindow = Window.GetWindow(element);
if (parentWindow != null)
{
System.Windows.Forms.Screen screen = GetScreen(parentWindow);
var mousePos = Win32.GetCursorPos();
if ((int)mousePos.X >= screen.WorkingArea.Right - 1)
{
Win32.SetCursorPos(screen.WorkingArea.Left, (int)mousePos.Y);
m_lastOnMouseMoveValue.X = screen.WorkingArea.Left;
return false;
}
if ((int)mousePos.X <= screen.WorkingArea.Left)
{
Win32.SetCursorPos(screen.WorkingArea.Right, (int)mousePos.Y);
m_lastOnMouseMoveValue.X = screen.WorkingArea.Right;
return false;
}
delta = mousePos - m_lastOnMouseMoveValue;
m_lastOnMouseMoveValue = mousePos;
}
}
if (delta.Length <= 0)
return false;
return true;
}