在WPF中获取DPI值的最佳方法
Method 1
这与在 Windows 窗体中执行此操作的方式相同。System.Drawing.Graphics
对象提供了方便的属性来获取水平和垂直 DPI。让我们画出一个辅助方法:
/// <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)) { }
}
您可以使用它来转换坐标和大小值。它非常简单、健壮,并且完全采用托管代码(至少对于您(消费者)而言)。通过IntPtr.Zero
as HWND
or HDC
参数结果为Graphics
包装整个屏幕的设备上下文的对象。
但这种方法有一个问题。它依赖于 Windows Forms/GDI+ 基础结构。您必须添加对 System.Drawing 程序集的引用。有什么大不了的?不确定你的情况,但对我来说这是一个需要避免的问题。
Method 2
让我们更深入地了解一下,以 Win API 的方式来实现。GetDeviceCaps
函数检索指定设备的各种信息,并且当我们传递它时能够检索水平和垂直 DPILOGPIXELSX
and LOGPIXELSY
分别参数。
GetDeviceCaps
函数定义在gdi32.dll
可能是什么System.Drawing.Graphics
在引擎盖下使用。
让我们看看我们的助手变成了什么样子:
[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.");
}
因此,我们将对托管 GDI+ 的依赖关系替换为对精美 Win API 调用的依赖关系。这是一种改进吗?在我看来是的,只要我们在 Windows 上运行 Win API 就是最小公分母。它很轻。在其他平台上,我们可能一开始就不会遇到这种困境。
不要被这个愚弄了ArgumentNullException
。该解决方案与第一个解决方案一样强大。System.Drawing.Graphics
如果它也无法获取设备上下文,则会抛出相同的异常。
Method 3
正如官方记录的那样here注册表中有一个特殊的键:HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDPI.
它存储一个 DWORD 值,该值正是用户在显示设置对话框中为 DPI 选择的值(在那里称为字体大小)。
阅读它是理所当然的,但我不推荐它。您会看到官方 API 和各种设置的存储之间存在差异。 API 是一个公共合约,即使内部逻辑被完全重写,它也保持不变(如果不是整个平台很糟糕,不是吗?)。
但没有人保证内部存储会保持不变。它可能已经持续了几十年,但描述其搬迁的重要设计文件可能已经等待批准。你永远不会知道。
始终坚持使用 API(无论是什么,本机、Windows 窗体、WPF 等)。即使底层代码从您知道的位置读取值。
Method 4
这是一种非常优雅的 WPF 方法,我发现它记录在这篇博文。它基于提供的功能System.Windows.Media.CompositionTarget
最终表示绘制 WPF 应用程序的显示表面的类。该类提供了 2 个有用的方法:
TransformFromDevice
TransformToDevice
这些名称是不言自明的,在这两种情况下我们都会得到一个System.Windows.Media.Matrix
包含设备单元(像素)和独立单元之间的映射系数的对象。 M11 将包含 X 轴系数,M22 - Y 轴系数。
到目前为止,我们一直在考虑单位->像素方向,让我们用以下代码重写我们的助手CompositionTarget.TransformToDevice.
调用此方法时,M11 和 M22 将包含我们计算得出的值:
因此,在 DPI 设置为 120 的机器上,系数将为 1.25。
这是新的助手:
/// <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);
}
我必须向该方法添加一个参数,即Visual
。我们需要它作为计算的基础(之前的示例使用整个屏幕的设备上下文)。我认为这不是一个大问题,因为您很可能会遇到Visual
运行 WPF 应用程序时即可使用(否则,为什么需要转换像素坐标?)。但是,如果您的视觉对象尚未附加到演示文稿源(即尚未显示),您将无法获取演示文稿源(因此,我们检查 NULL 并构造一个新的HwndSource
).
参考