Method 1
It’s the same way you did that in Windows Forms. System.Drawing.Graphics
object provides convenient properties to get horizontal and vertical DPI. Let’s sketch up a helper method:
/// <summary>
/// Transforms device independent units (1/96 of an inch)
/// to pixels
/// </summary>
/// <param name="unitX">a device independent unit value X</param>
/// <param name="unitY">a device independent unit value Y</param>
/// <param name="pixelX">returns the X value in pixels</param>
/// <param name="pixelY">returns the Y value in pixels</param>
public void TransformToPixels(double unitX,
double unitY,
out int pixelX,
out int pixelY)
{
using (Graphics g = Graphics.FromHwnd(IntPtr.Zero))
{
pixelX = (int)((g.DpiX / 96) * unitX);
pixelY = (int)((g.DpiY / 96) * unitY);
}
// alternative:
// using (Graphics g = Graphics.FromHdc(IntPtr.Zero)) { }
}
You can use it transforms both coordinates as well as size values. It’s pretty simple and robust and completely in managed code (at least as far as you, the consumer, is concerned). Passing IntPtr.Zero
as HWND
or HDC
parameter results in a Graphics
object that wraps a device context of the entire screen.
There is one problem with this approach though. It has a dependency on Windows Forms/GDI+ infrastructure. You are going to have to add a reference to System.Drawing assembly. Big deal? Not sure about you, but for me this is an issue to avoid.
Method 2
Let’s take it one step deeper and do it the Win API way. GetDeviceCaps
function retrieves various information for the specified device and is able to retrieve horizontal and vertical DPI’s when we pass it LOGPIXELSX
and LOGPIXELSY
parameters respectively.
GetDeviceCaps
function is defined in gdi32.dll
and is probably what System.Drawing.Graphics
uses under the hood.
Let’s have a look at what our helper has become:
[DllImport("gdi32.dll")]
public static extern int GetDeviceCaps(IntPtr hDc, int nIndex);
[DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDc);
public const int LOGPIXELSX = 88;
public const int LOGPIXELSY = 90;
/// <summary>
/// Transforms device independent units (1/96 of an inch)
/// to pixels
/// </summary>
/// <param name="unitX">a device independent unit value X</param>
/// <param name="unitY">a device independent unit value Y</param>
/// <param name="pixelX">returns the X value in pixels</param>
/// <param name="pixelY">returns the Y value in pixels</param>
public void TransformToPixels(double unitX,
double unitY,
out int pixelX,
out int pixelY)
{
IntPtr hDc = GetDC(IntPtr.Zero);
if (hDc != IntPtr.Zero)
{
int dpiX = GetDeviceCaps(hDc, LOGPIXELSX);
int dpiY = GetDeviceCaps(hDc, LOGPIXELSY);
ReleaseDC(IntPtr.Zero, hDc);
pixelX = (int)(((double)dpiX / 96) * unitX);
pixelY = (int)(((double)dpiY / 96) * unitY);
}
else
throw new ArgumentNullException("Failed to get DC.");
}
So we have exchanged a dependency on managed GDI+ for the dependency on fancy Win API calls. Is that an improvement? In my opinion yes, as long as we run on Windows Win API is a least common denominator. It is lightweight. On other platforms we wouldn’t probably have this dilemma in the first place.
And don’t get fooled by that ArgumentNullException
. This solution is as robust as the first one. System.Drawing.Graphics
will throw this same exception if it can’t obtain a device context too.
Method 3
As officially documented here there is a special key in the registry: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDPI.
It stores a DWORD value which is exactly what the user chooses for DPI in the display settings dialog (it’s called a font size there).
Reading it is a no-brainer but I wouldn’t recommend it. You see there is a difference between an official API and a storage for various settings. The API is a public contract that stays the same even if the internal logic is totally rewritten (If it doesn’t the whole platform sucks, doesn’t it?).
But nobody guarantees that the internal storage will remain the same. It may have lasted for a couple of decades but a crucial design document that describes its relocation may already be pending an approval. You never know.
Always stick to API (whatever it is, native, Windows Forms, WPF, etc). Even if the underlying code reads the value from the location you know.
Method 4
This is a pretty elegant WPF approach that I’ve found documented in this blog post. It is based on the functionality provided by System.Windows.Media.CompositionTarget
class that ultimately represents the display surface on which the WPF application is drawn. The class provides 2 useful methods:
TransformFromDevice
TransformToDevice
The names are self-explanatory and in both cases we get a System.Windows.Media.Matrix
object that contains the mapping coefficients between device units (pixels) and independent units. M11 will contain a coefficient for the X axis and M22 – for the Y axis.
As we have been considering a units->pixels direction so far let’s re-write our helper with CompositionTarget.TransformToDevice.
When calling this method M11 and M22 will contain values that we calculated as:
So on a machine with DPI set to 120 the coefficients will be 1.25.
Here’s the new helper:
/// <summary>
/// Transforms device independent units (1/96 of an inch)
/// to pixels
/// </summary>
/// <param name="visual">a visual object</param>
/// <param name="unitX">a device independent unit value X</param>
/// <param name="unitY">a device independent unit value Y</param>
/// <param name="pixelX">returns the X value in pixels</param>
/// <param name="pixelY">returns the Y value in pixels</param>
public void TransformToPixels(Visual visual,
double unitX,
double unitY,
out int pixelX,
out int pixelY)
{
Matrix matrix;
var source = PresentationSource.FromVisual(visual);
if (source != null)
{
matrix = source.CompositionTarget.TransformToDevice;
}
else
{
using (var src = new HwndSource(new HwndSourceParameters()))
{
matrix = src.CompositionTarget.TransformToDevice;
}
}
pixelX = (int)(matrix.M11 * unitX);
pixelY = (int)(matrix.M22 * unitY);
}
I had to add one more parameter to the method, the Visual
. We need it as a base for calculations (previous samples used the device context of the entire screen for that). I don’t think it’s a big issue as you are more than likely to have a Visual
at hand when running your WPF application (otherwise, why would you need to translate pixel coordinates?). However, if your visual hasn't been attached to a presentation source (that is, it hasn't been shown yet) you can't get the presentation source (thus, we have a check for NULL and construct a new HwndSource
).
Reference