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:
- You measured it already
- You measured one of its ancestors, or
- 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:
- It may be that the element's parent would have passed in a smaller availableSize
- It is inefficient if the actual DesiredSize is zero (it is remeasured repeatedly)
- 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.
The exact statement you're referencing is:
On average, 48dp translate to a physical size of about 9mm
If you look at this image from the Supporting Multiple Screens doc:
You'll see that there are density buckets (ldpi, mdpi, hdpi, xhdpi). Each of these are not fixed densities, but rather a range of densities which end up mapping to fixed numbers used for various density calculations (120, 160, 240, 320 respectively). 160 is rarely the actual pixels per inch for an mdpi device, it's just the abstracted value used for simplicity's sake.
Your calculations are correct, but you assume that 160ppi is the average density (assuming mdpi devices) just because 160dpi is the abstracted value for mdpi. Apparently it's not, if the statement you reference is indeed true. I suspect that there are a lot of devices which average out to around 200ppi which end up getting categorized as hdpi. That would be: (48dp / 200ppi) * 1.5 * 25.4 mm/in = 9.14400 mm. Just a guess, but certainly I think the underlying reason is that the average isn't 160ppi.
Update:
Here's another quote from the doc from the design site:
If you design your elements to be at least 48dp high and wide you can guarantee that [...] your targets will never be smaller than the minimum recommended target size of 7mm regardless of what screen they are displayed on.
So the size varies from > 7mm to at least 9mm (given 9mm is supposed to be average I would think the top range should be < 11mm). Yes, 48dp should be "approximately" the same size on all screens, but what "approximately" really means is not really specified. Your 7.62mm is within range. Their 9mm value is just for the "average" which is an unspecified ppi.
Best Answer
Are you sure that the display is actually 96 ppi? Going by the specs, and my own calculations, your Lenovo S10e actually has a density of ~116.36 ppi, which is probably where your difference is coming in.
Assuming these specs are correct:
10.1" diagonal
1024 x 576 resolution
16:9 screen ratio (taken from above resolution)
Using some geometric formulas, you can get the actual width and height of the monitor as:
Width: 8.8"
Height: 4.95"
Dividing 1024/8.8 and 576/4.95 gives you 116.36 pixels per inch, rather than 96.
Using this instead, a 168 pixel image should display as 168/116.36, or ~1.44", which is consistent with your results. I wouldn't put too much faith in the xdpyinfo results. :)