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

6
我有一个窗口是使用WPF设计的,我将其放在WinForms主窗口的中心。现在,我想移动主窗口,同时WPF窗口也必须移动到主窗口的中心!
但是我有一个问题,只有当窗口位于主窗口中心时,主窗口才在屏幕中心。否则,它会以不同于Windows坐标的形式运作。我只需将主窗口的偏移值添加到窗口位置即可。
现在我得出结论,WPF窗口上的像素坐标与WinForms不同!
如何将 WPF 窗口位置转换为 WinForms 基本位置,反之亦然? Owner Form 代码如下:
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);
    }
}

你目前有什么代码可以将WPF窗口定位在WinForms表单的中心并保持在那里? - Paul
5
WPF中的测量单位是英寸,而不是像素。请搜索“wpf将英寸转换为像素”以获取明显的结果。 - Hans Passant
2个回答

14

获取WPF中DPI值的最佳方法

方法1

在WPF中,获取DPI值的方法与在Windows Forms中相同。 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.");
}

所以我们用精美的 Win API 调用取代了对托管 GDI+ 的依赖。这是一种改进吗?在我看来,只要我们在 Windows 上运行,Win API 就是最常见的分母,它很轻量级。在其他平台上,我们可能首先就不会遇到这个困境。

别被那个 ArgumentNullException 欺骗了。这个解决方案和第一个方案一样强大。如果 System.Drawing.Graphics 无法获得设备上下文,它也会抛出同样的异常。


方法3

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

读取这个值很简单,但我不建议这样做。你知道,官方 API 和各种设置存储之间是有区别的。API 是一个公共契约,即使内部逻辑完全重写了,它也不会改变(如果不这样,整个平台就糟糕了,不是吗?)。

但是没有人保证内部存储将保持不变。它可能已经持续了几十年,但描述其重定位的关键设计文档可能已经等待批准。你永远不知道。

始终坚持使用 API(无论是本机、Windows Forms、WPF 等等)。即使底层代码从你知道的位置读取该值。


方法4

这是一种相当优雅的 WPF 方法,我在这篇博客文章中找到了它的说明。它基于 System.Windows.Media.CompositionTarget 类提供的功能,最终代表 WPF 应用程序绘制的显示表面。该类提供了两个有用的方法:

  • 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(否则,为什么需要转换像素坐标呢?)。但是,如果你的视觉元素尚未附加到演示源(也就是说,它还没有显示出来),你将无法获取演示源(因此,我们要检查是否为空并构造一个新的HwndSource)。

参考资料


一个人在哪里获取Visual?我有Window。 - Alan Baljeu
如果您使用WPF,可以通过以下方法从Visual中获取DPI:VisualTreeHelper.GetDpi(visual ?? System.Windows.Application.Current.MainWindow) - Behzad

1

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

formVal = Me.LogicalToDeviceUnits(WPFval)
和可以是整数或尺寸。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接