如何将WPF大小转换为物理像素?

43

如何将WPF(分辨率无关)的宽度和高度转换为物理屏幕像素是最佳方法?

我在WinForms表单中显示WPF内容(通过ElementHost),并尝试制定一些大小逻辑。当操作系统运行在默认的96 dpi时,它可以正常工作。但是,当操作系统设置为120 dpi或其他分辨率时,由于对于WinForms来说,报告其宽度为96的WPF元素实际上是120个像素宽,因此它将无法正常工作。

我在System.Windows.SystemParameters上找不到任何“每英寸像素”设置。我确信我可以使用WinForms等效项(System.Windows.Forms.SystemInformation),但是否有更好的方法来完成这项任务(即使用WPF API而不是使用WinForms API并手动计算)?什么是将WPF“像素”转换为真实屏幕像素的“最佳方法”?

编辑:我也要在WPF控件显示在屏幕上之前进行此操作。看起来Visual.PointToScreen可以给我正确的答案,但我不能使用它,因为该控件尚未成为父控件,我会收到InvalidOperationException“此Visual未连接到PresentationSource”。


2
@Ragepotato,这只是一个概念性的概述——它并没有提供从WPF转换为像素的代码,并且也没有解释如何处理边界条件。 - Joe White
4个回答

75

将已知大小转换为设备像素

如果你的可视元素已经连接到 PresentationSource 上(例如,它是窗口中可见的一部分),则可以通过以下方式找到变换:

var source = PresentationSource.FromVisual(element);
Matrix transformToDevice = source.CompositionTarget.TransformToDevice;

如果没有,使用HwndSource创建一个临时的hWnd:

Matrix transformToDevice;
using(var source = new HwndSource(new HwndSourceParameters()))
  transformToDevice = source.CompositionTarget.TransformToDevice;

请注意,使用HwndSource构造比使用IntPtr.Zero的hWnd效率要低,但我认为它更可靠,因为HwndSource创建的hWnd将附加到与实际新创建的Window相同的显示设备上。这样,如果不同的显示设备有不同的DPI值,您可以确保获得正确的DPI值。

一旦您获得了转换,您就可以将任何大小从WPF大小转换为像素大小:

var pixelSize = (Size)transformToDevice.Transform((Vector)wpfSize);

将像素大小转换为整数

如果你想将像素大小转换为整数,你可以简单地执行以下操作:

int pixelWidth = (int)pixelSize.Width;
int pixelHeight = (int)pixelSize.Height;

但更健壮的解决方案是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));

获取 UIElement 的期望大小

要获取 UIElement 的期望大小,你需要确保它已经被测量过。在某些情况下,它可能已经被测量过了,这是因为:

  1. 你已经测量过它;
  2. 你测量过它的祖先元素;或者
  3. 它是 PresentationSource 的一部分(例如,在可见窗口中),并且你正在执行低于 DispatcherPriority.Render 的操作,因此你知道自动进行了测量。

如果你的可视元素还没有被测量过,你应该调用相应控件或其祖先元素的 Measure 方法,并传递可用的尺寸(或 new Size(double.PositiveInfinity, double.PositiveInfinity) 如果你想将其大小设为内容大小):

element.Measure(availableSize);

测量完成后,所需的只是使用矩阵来转换DesiredSize大小:

var pixelSize = (Size)transformToDevice.Transform((Vector)element.DesiredSize);

将所有内容放在一起

下面是一个简单的方法,展示了如何获取一个元素的像素大小:

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);
}
请注意,这段代码中只有在没有DesiredSize的情况下我才会调用Measure方法。这提供了一种便捷的方法来完成所有操作,但是它有几个缺点:
  1. 可能会出现元素的父级传递了一个更小的可用尺寸
  2. 如果实际的DesiredSize为零,则效率低下(需要反复重新测量)
  3. 它可能掩盖错误,导致应用程序由于意外的时间问题而失败(例如,在DispatchPriority.Render或以上级别的代码中调用)
因为这些原因,我倾向于在GetElementPixelSize中省略Measure调用,让客户端自己处理。

2
这个回答有点过于杀鸡焉用牛刀了,因为在问题中我已经提到了宽度和高度。(微笑) - Joe White
1
它返回 double 类型的宽度和高度。我想要的是 ElementHost 实际的大小,这意味着 int 类型,并按照 ElementHost 相同的方式四舍五入取整。 - Joe White
1
另外,需要注意的是,理论上单个正在运行的 WPF 应用程序可以在两个不同的显示器上具有可见窗口,每个窗口都有自己独立的 DPI 设置,只要操作系统支持。这就是为什么我的第一个建议是使用元素的实际 PresentationSource 获取 TransformToDevice,而不是使用虚拟或 IntPtr.Zero 窗口。 - Ray Burns
1
原来每次调用此代码时,会在屏幕上出现一个小窗口,然后很快就消失了。罪魁祸首是 new HwndSourceParameters("")。如果你移除这个参数,并使用 new HwndSourceParameters(),则窗口不会闪烁,但所有内容仍然可以正确缩放。 - Joe White
4
您使用 HwndSource 的示例缺少 CompositionTarget 元素。您需要将这个代码段: transformToDevice = source.TransformToDevice; 更改为: transformToDevice = source.CompositionTarget.TransformToDevice; - Roger Sanders
显示剩余5条评论

12

屏幕工作区(Screen.WorkingArea)和系统参数工作区(SystemParameters.WorkArea)之间的简单比例:

private double PointsToPixels (double wpfPoints, LengthDirection direction)
{
    if (direction == LengthDirection.Horizontal)
    {
        return wpfPoints * Screen.PrimaryScreen.WorkingArea.Width / SystemParameters.WorkArea.Width;
    }
    else
    {
        return wpfPoints * Screen.PrimaryScreen.WorkingArea.Height / SystemParameters.WorkArea.Height;
    }
}

private double PixelsToPoints(int pixels, LengthDirection direction)
{
    if (direction == LengthDirection.Horizontal)
    {
        return pixels * SystemParameters.WorkArea.Width / Screen.PrimaryScreen.WorkingArea.Width;
    }
    else
    {
        return pixels * SystemParameters.WorkArea.Height / Screen.PrimaryScreen.WorkingArea.Height;
    }
}

public enum LengthDirection
{
    Vertical, // |
    Horizontal // ——
}

这在多显示器情况下也能正常工作。


对我来说完美地工作了。不错的简单解决方案! - JoeMjr2
谢谢,但请将枚举重命名为类似“枚举轴{宽度,高度}”的形式,这样更容易理解。 - Alex

9
我找到了一种方法,但我不是很喜欢它:
using (var graphics = Graphics.FromHwnd(IntPtr.Zero))
{
    var pixelWidth = (int) (element.DesiredSize.Width * graphics.DpiX / 96.0);
    var pixelHeight = (int) (element.DesiredSize.Height * graphics.DpiY / 96.0);
    // ...
}

我不喜欢它,因为(一)它需要引用 System.Drawing,而不是使用 WPF API;(二)我必须自己进行数学计算,这意味着我会重复 WPF 的实现细节。在 .NET 3.5 中,我必须截断计算结果以匹配 ElementHost 在 AutoSize=true 时的操作,但我不知道这在将来的 .NET 版本中是否仍然准确。
这似乎是有效的,所以我发布它以帮助他人。但如果有更好的答案,请发表。

WPF没有本地方法可以获取屏幕DPI,这比我见过的一些Win32方法更简洁:(然而,一旦你计算出正确的转换比率,你可以设置一个顶级缩放变换(从WPF“像素”返回到主机/屏幕像素)。如果WPF控件在您控制下的WinForms环境中托管,您可能可以从主机传递信息--仍然很棘手。 - user166390
在Win 7和8.1上对我有用。使用NotifyIcon的私有“窗口”值作为传递给FromHwnd的hwnd。私有值是未来MS更改的脆弱性风险,但它很快并且似乎工作正常。有关获取该值的说明,请参见CodeProject示例,但如果不可用,则此SO答案包括反射代码以获取NativeWindow.Handle - Daryn
这个问题的主要问题是,如果多个显示器使用不同的缩放模式,则无法正常工作。唉 - 如果.NET(或Windows)有一种简单的方法来获取每个显示器的缩放模式就好了。 - Rick Strahl

1

刚才在 ObjectBrowser 中快速查找了一下,发现有些很有趣的东西,你可能会想看看。

System.Windows.Form.AutoScaleMode,它有一个叫做 DPI 的属性。以下是文档,这可能是你正在寻找的:

public const System.Windows.Forms.AutoScaleMode Dpi = 2 Member of System.Windows.Forms.AutoScaleMode

Summary: Controls scale relative to the display resolution. Common resolutions are 96 and 120 DPI.

将其应用于您的窗体,应该就可以解决问题了。

{享受}


1
this.AutoScaleMode = AutoScaleMode.None; 或者一般地说,AutoScaleMode 选项只能在 Winforms 中使用。 - ssamko

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