How do I convert a WPF size to physical pixels?
Asked Answered
W

4

43

What's the best way to convert a WPF (resolution-independent) width and height to physical screen pixels?

I'm showing WPF content in a WinForms Form (via ElementHost) and trying to work out some sizing logic. I've got it working fine when the OS is running at the default 96 dpi. But it won't work when the OS is set to 120 dpi or some other resolution, because then a WPF element that reports its Width as 96 will actually be 120 pixels wide as far as WinForms is concerned.

I couldn't find any "pixels per inch" settings on System.Windows.SystemParameters. I'm sure I could use the WinForms equivalent (System.Windows.Forms.SystemInformation), but is there a better way to do this (read: a way using WPF APIs, rather than using WinForms APIs and manually doing the math)? What's the "best way" to convert WPF "pixels" to real screen pixels?

EDIT: I'm also looking to do this before the WPF control is shown on the screen. It looks like Visual.PointToScreen could be made to give me the right answer, but I can't use it, because the control isn't parented yet and I get InvalidOperationException "This Visual is not connected to a PresentationSource".

Worlock answered 20/7, 2010 at 0:39 Comment(2)
Is this what you're looking for? books.google.com/…Jud
@Ragepotato, that's just a conceptual overview - it doesn't give code to convert from WPF to pixels, and it doesn't explain how to handle the boundary conditions.Worlock
N
75

Transforming a known size to device pixels

If your visual element is already attached to a PresentationSource (for example, it is part of a window that is visible on screen), the transform is found this way:

var source = PresentationSource.FromVisual(element);
Matrix transformToDevice = source.CompositionTarget.TransformToDevice;

If not, use HwndSource to create a temporary hWnd:

Matrix transformToDevice;
using(var source = new HwndSource(new HwndSourceParameters()))
  transformToDevice = source.CompositionTarget.TransformToDevice;

Note that this is less efficient than constructing using a hWnd of IntPtr.Zero but I consider it more reliable because the hWnd created by HwndSource will be attached to the same display device as an actual newly-created Window would. That way, if different display devices have different DPIs you are sure to get the right DPI value.

Once you have the transform, you can convert any size from a WPF size to a pixel size:

var pixelSize = (Size)transformToDevice.Transform((Vector)wpfSize);

Converting the pixel size to integers

If you want to convert the pixel size to integers, you can simply do:

int pixelWidth = (int)pixelSize.Width;
int pixelHeight = (int)pixelSize.Height;

but a more robust solution would be the one used by ElementHost:

int pixelWidth = (int)Math.Max(int.MinValue, Math.Min(int.MaxValue, pixelSize.Width));
int pixelHeight = (int)Math.Max(int.MinValue, Math.Min(int.MaxValue, pixelSize.Height));

Getting the desired size of a UIElement

To get the desired size of a UIElement you need to make sure it is measured. In some circumstances it will already be measured, either because:

  1. You measured it already
  2. You measured one of its ancestors, or
  3. It is part of a PresentationSource (eg it is in a visible Window) and you are executing below DispatcherPriority.Render so you know measurement has already happened automatically.

If your visual element has not been measured yet, you should call Measure on the control or one of its ancestors as appropriate, passing in the available size (or new Size(double.PositivieInfinity, double.PositiveInfinity) if you want to size to content:

element.Measure(availableSize);

Once the measuring is done, all that is necessary is to use the matrix to transform the DesiredSize:

var pixelSize = (Size)transformToDevice.Transform((Vector)element.DesiredSize);

Putting it all together

Here is a simple method that shows how to get the pixel size of an element:

public Size GetElementPixelSize(UIElement element)
{
  Matrix transformToDevice;
  var source = PresentationSource.FromVisual(element);
  if(source!=null)
    transformToDevice = source.CompositionTarget.TransformToDevice;
  else
    using(var source = new HwndSource(new HwndSourceParameters()))
      transformToDevice = source.CompositionTarget.TransformToDevice;

  if(element.DesiredSize == new Size())
    element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));

  return (Size)transformToDevice.Transform((Vector)element.DesiredSize);
}

Note that in this code I call Measure only if no DesiredSize is present. This provides a convenient method to do everything but has several deficiencies:

  1. It may be that the element's parent would have passed in a smaller availableSize
  2. It is inefficient if the actual DesiredSize is zero (it is remeasured repeatedly)
  3. It may mask bugs in a way that causes the application to fail due to unexpected timing (eg. the code being called at or above DispatchPriority.Render)

Because of these reasons, I would be inclined to omit the Measure call in GetElementPixelSize and just let the client do it.

Nettienetting answered 10/8, 2010 at 15:8 Comment(10)
This answer is serious overkill, since in the question I said I already have the width and height. (grin)Worlock
And it returns doubles for Width and Height. I want the actual size that the ElementHost would size to, which means ints, rounded the same way ElementHost does.Worlock
I added a section explaining how to convert to integers the same way as ElementHost.Nettienetting
Also, note that there is no theoretical reason why a single running WPF application cannot have windows visible on two different displays each with its own separate DPI setting, as long as the OS supports it. This is why my first suggestion was to use the element's actual PresentationSource to get the TransformToDevice rather than using a dummy or IntPtr.Zero window.Nettienetting
I still don't like having to duplicate the knowledge of how ElementHost chooses to round, but it looks like this is the best answer.Worlock
It turns out that this causes a small window to appear onscreen and then promptly disappear, every time this code is called. The culprit is the new HwndSourceParameters(""). If you remove the parameter, and do new HwndSourceParameters(), the window doesn't flash, but everything still scales correctly.Worlock
Thanks. I edited the answer so others won't have to look down here in the comments to figure it out.Nettienetting
In my case alternative HwndSource (when source==null) preforms wrong conversion. So be warned! An also please add comment to the case that the element hasn't been initialized or have already been disposed.Kaka
The method GetElementPixelSize does not compile as is (obviously you can't reuse source like that.)Penelopa
Your example using HwndSource is missing the CompositionTarget element. You need to change this: transformToDevice = source.TransformToDevice; to this: transformToDevice = source.CompositionTarget.TransformToDevice;Ciao
K
12

Simple proportion between Screen.WorkingArea and SystemParameters.WorkArea:

private double PointsToPixels (double wpfPoints, LengthDirection direction)
{
    if (direction == LengthDirection.Horizontal)
    {
        return wpfPoints * Screen.PrimaryScreen.WorkingArea.Width / SystemParameters.WorkArea.Width;
    }
    else
    {
        return wpfPoints * Screen.PrimaryScreen.WorkingArea.Height / SystemParameters.WorkArea.Height;
    }
}

private double PixelsToPoints(int pixels, LengthDirection direction)
{
    if (direction == LengthDirection.Horizontal)
    {
        return pixels * SystemParameters.WorkArea.Width / Screen.PrimaryScreen.WorkingArea.Width;
    }
    else
    {
        return pixels * SystemParameters.WorkArea.Height / Screen.PrimaryScreen.WorkingArea.Height;
    }
}

public enum LengthDirection
{
    Vertical, // |
    Horizontal // ——
}

This works fine with multiple monitors as well.

Kaka answered 5/9, 2013 at 12:50 Comment(2)
Worked perfectly for me. Nice simple solution!Thermotherapy
Thanks, but please rename enum to something like "enum Axis{Width,Height}", this will be much clearer to understand.Horrible
W
9

I found a way to do it, but I don't like it much:

using (var graphics = Graphics.FromHwnd(IntPtr.Zero))
{
    var pixelWidth = (int) (element.DesiredSize.Width * graphics.DpiX / 96.0);
    var pixelHeight = (int) (element.DesiredSize.Height * graphics.DpiY / 96.0);
    // ...
}

I don't like it because (a) it requires a reference to System.Drawing, rather than using WPF APIs; and (b) I have to do the math myself, which means I'm duplicating WPF's implementation details. In .NET 3.5, I have to truncate the result of the calculation to match what ElementHost does with AutoSize=true, but I don't know whether this will still be accurate in future versions of .NET.

This does seem to work, so I'm posting it in case it helps others. But if anyone has a better answer, please, post away.

Worlock answered 20/7, 2010 at 1:45 Comment(3)
There is no native WPF method to get the Screen DPI, and this is cleaner than some Win32 methods I have seen :( However, you can setup a top-level scale transformation (to go from WPF "pixels" back to host/screen pixels) once you work out the correct conversion ratio. If the WPF control is hosted within an WinForms environment under your control, you could possibly pass in the information from the host -- still icky.Sermonize
Worked for me in Win 7 & 8.1. Used the NotifyIcon's private 'window' value as the hwnd passed to FromHwnd. Private value is fragility risk for future MS changes, but it was quick and seems to work fine. For description of getting that value, see CodeProject example, but if not available this SO answer includes the reflection code to get the NativeWindow.Handle.Jaguarundi
The big issue with this is, it doesn't work with multiple monitors if the monitors use separate scaling modes. Sigh - if only .NET (or Windows for that matter) had an easy way to get the Scale mode for each monitor.Cahan
V
1

Just did a quick lookup in the ObjectBrowser and found something quite interesting, you might want to check it out.

System.Windows.Form.AutoScaleMode, it has a property called DPI. Here's the docs, it might be what you are looking for :

public const System.Windows.Forms.AutoScaleMode Dpi = 2 Member of System.Windows.Forms.AutoScaleMode

Summary: Controls scale relative to the display resolution. Common resolutions are 96 and 120 DPI.

Apply that to your form, it should do the trick.

{enjoy}

Vondavonni answered 13/8, 2010 at 15:21 Comment(1)
this.AutoScaleMode = AutoScaleMode.None; Or generally AutoScaleMode option can be used only in Winforms.Maurizia

© 2022 - 2024 — McMap. All rights reserved.