如何将 WPF 英寸单位转换为 Winforms 像素,反之亦然?

How to Convert a WPF inch unit to Winforms pixels and vice versa?

我有一个 window 是 WPF and I used that on center of a WinForms 所有者设计的。 现在,我想移动所有者表单,目前我的 WPF window 也必须移动到表单的中心!

但是我有一个问题,只有当 window 位于屏幕中心的表单中心时。并且以与 Windows 坐标不同的形式行事。 我只是将表格的位移值添加到 window 位置。

现在我得出结论,WPF Windows 上的像素坐标与 WinForms 不同!

如何将 WPF window 位置转换为 WinForms 基本位置,反之亦然?

所有者表格代码是:

public partial class Form1 : Form
{
    private WPF_Window.WPF win;

    public Form1()
    {
        InitializeComponent();

        win = new WPF();
        win.Show();
        CenterToParent(win);
    }

    private void CenterToParent(System.Windows.Window win)
    {
        win.Left = this.Left + (this.Width - win.Width) / 2;
        win.Top = this.Top + (this.Height - win.Height) / 2;
    }

    protected override void OnMove(EventArgs e)
    {
        base.OnMove(e);
        CenterToParent(win);
    }
}

Best way to get DPI value in WPF

方法一

这与您在 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 作为 HWNDHDC 参数传递会导致 Graphics 对象包装整个屏幕的设备上下文。

不过这种方法有一个问题。它依赖于 Windows Forms/GDI+ 基础设施。您将必须添加对 System.Drawing 程序集的引用。大不了?不确定你,但对我来说这是一个要避免的问题。


方法二

让我们更进一步,以 Win API 的方式进行。 GetDeviceCaps 函数检索指定设备的各种信息,当我们分别向它传递 LOGPIXELSXLOGPIXELSY 参数时,能够检索水平和垂直 DPI。

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 赢 API 就是最小公分母。它很轻。在其他平台上,我们可能一开始就不会遇到这种困境。

不要被那个 ArgumentNullException 愚弄了。该解决方案与第一个解决方案一样稳健。 System.Drawing.Graphics 如果它也无法获取设备上下文,也会抛出同样的异常。


方法三

正如官方记录的那样 here 注册表中有一个特殊的键:HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDPI. 它存储一个 DWORD 值,这正是用户选择的值显示设置对话框中的 DPI(那里称为字体大小)。

阅读它很简单,但我不推荐它。你看官方的API和各种设置的存储是有区别的。 API 是一个 public 合约,即使内部逻辑被完全重写也保持不变(如果不是整个平台很糟糕,不是吗?)。

但是没有人保证内部存储会保持不变。它可能已经持续了几十年,但描述其搬迁的重要设计文件可能已经在等待批准。你永远不知道。

始终坚持 API(无论是什么,原生的,Windows 表单,WPF 等)。即使底层代码从你知道的位置读取值。


方法四

这是一种非常优雅的 WPF 方法,我在 this blog post 中找到了它。它基于 System.Windows.Media.CompositionTarget class 提供的功能,最终表示绘制 WPF 应用程序的显示表面。 class 提供了 2 个有用的方法:

  • TransformFromDevice
  • TransformToDevice

名称是不言自明的,在这两种情况下我们都得到一个 System.Windows.Media.Matrix 对象,其中包含设备单位(像素)和独立单位之间的映射系数。 M11 将包含 X 轴的系数,M22 将包含 Y 轴的系数。

到目前为止我们一直在考虑单位->像素方向让我们用CompositionTarget.TransformToDevice.重写我们的助手当调用这个方法时,M11和M22将包含我们计算的值:

  • dpiX / 96
  • dpiY / 96

所以在 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。我们需要它作为计算的基础(之前的示例为此使用了整个屏幕的设备上下文)。我认为这不是什么大问题,因为在 运行 构建 WPF 应用程序时,您很可能手头有一个 Visual(否则,为什么需要转换像素坐标?)。但是,如果您的视觉对象尚未附加到演示源(即尚未显示),则无法获取演示源(因此,我们检查 NULL 并构建一个新的 HwndSource).

Reference

我刚刚发现并测试了这个(在 VB 中):

formVal = Me.LogicalToDeviceUnits(WPFval)

formVal 和 WPFval 可以是整数或大小。