HorizontalOffset goes in the wrong direction for High DPI primary screen

Viewed 1171

I am currently working on some high DPI issues in our WPF app (.NET 4.6.1 - System DPI-awareness is active).

Generally the app does what we expect it to do - scale depending on the current displays DPI setting, also when moving it from screen A @ 100% to screen B @ 150% it changes it's overall scale correctly "at the half-point".

Most of the open issues where there because we had some pixel-/DIP-based calculations which did not took the DPI-setting into consideration. This I fixed by calculating in the correct DPI values:

var source = PresentationSource.FromVisual(this);
var dpiX = source?.CompositionTarget?.TransformToDevice.M11 ?? 1;
var dpiY = source?.CompositionTarget?.TransformToDevice.M22 ?? 1;

There I found out the first strange thing (at least for me):

  1. If the primary display is set to e.g. 125% I get 1.25 for dpiX for all screens, even the secondary screen @ 100%, but there all pixel-values are already multiplied by 1.25 (meaning a 1600x1200 pixel screen has a working size of 2000x1500).
  2. And it is exactly the other way around if the primary screen is at 100% and the secondary screen is at e.g. 150%: I always get 1 for dpiX, but all values are already correct and no correction is necessary (=> or multiply/dived by 1 does not break it).

But now to my actual problem:
I have some pop-ups I am placing at the center of their placement-targets with the following binding:

<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>

and converter:

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(); }
}

For case 2 everything already works correctly without the commented out code, but for case 1 I tried dividing and multiplying the DPI value, but in the end the correct thing was to multiply it by -1 to get it to work correctly.

Why ist that the case?
And how can I savely detect when this is needed? dpiX > 1?

I am also open for other solutions to the scaling issue or to the center-placement as a whole.

P.S.: I am running Windows 10 1703 with .NET 4.7 installed (App still targets 4.6.1 for some other reasons).

UPDATE:
I created a demo-solution: https://github.com/chrfin/HorizontalOffsetError
If the main screen is at 100% it is correct:
Correct
but if the main screen is e.g. 125% it is off:
Wrong
BUT if I than add *-1 to the offset it is correct again:
Corrected

...but why?

1 Answers
Related