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;

Там я обнаружил первую странную вещь (по крайней мере, для меня):

  1. Если основной дисплей установлен, например, 125%, я получаю 1,25 для dpiX для всех экранов, даже для вторичного экрана @ 100%, но там все значения пикселей уже умножены на 1,25 (что означает, что экран 1600x1200 пикселей имеет рабочий размер 2000x1500).
  2. И наоборот, если основной экран на 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;
    }
Другие вопросы по тегам