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

发布于 2024-09-10 21:15:09 字数 590 浏览 2 评论 0原文

将 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”。

What's the best way to convert a WPF (resolution-independent) width and height to physical screen pixels?

I'm showing WPF content in a WinForms Form (via ElementHost) and trying to work out some sizing logic. I've got it working fine when the OS is running at the default 96 dpi. But it won't work when the OS is set to 120 dpi or some other resolution, because then a WPF element that reports its Width as 96 will actually be 120 pixels wide as far as WinForms is concerned.

I couldn't find any "pixels per inch" settings on System.Windows.SystemParameters. I'm sure I could use the WinForms equivalent (System.Windows.Forms.SystemInformation), but is there a better way to do this (read: a way using WPF APIs, rather than using WinForms APIs and manually doing the math)? What's the "best way" to convert WPF "pixels" to real screen pixels?

EDIT: I'm also looking to do this before the WPF control is shown on the screen. It looks like Visual.PointToScreen could be made to give me the right answer, but I can't use it, because the control isn't parented yet and I get InvalidOperationException "This Visual is not connected to a PresentationSource".

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

穿越时光隧道 2024-09-17 21:15:09

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

如果您的视觉元素已附加到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;

请注意,这比使用 IntPtr.Zero 的 hWnd 构造效率低,但我认为它更可靠,因为 HwndSource 创建的 hWnd 将与实际新创建的 hWnd 附加到同一显示设备窗户会。这样,如果不同的显示设备具有不同的 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.PositivieInfinity, 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. 元素的父元素可能会传入较小的 availableSize
  2. 。如果实际 DesiredSize 为零(反复重新测量),则效率低下。
  3. 它可能会以某种方式掩盖错误,导致应用程序因意外时间而失败(例如,在 DispatchPriority.Render 或以上调用的代码)

由于这些原因,我倾向于省略 GetElementPixelSize 中的 Measure 调用,而让客户端执行此操作。

Transforming a known size to device pixels

If your visual element is already attached to a PresentationSource (for example, it is part of a window that is visible on screen), the transform is found this way:

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

If not, use HwndSource to create a temporary hWnd:

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

Note that this is less efficient than constructing using a hWnd of IntPtr.Zero but I consider it more reliable because the hWnd created by HwndSource will be attached to the same display device as an actual newly-created Window would. That way, if different display devices have different DPIs you are sure to get the right DPI value.

Once you have the transform, you can convert any size from a WPF size to a pixel size:

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

Converting the pixel size to integers

If you want to convert the pixel size to integers, you can simply do:

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

but a more robust solution would be the one used by 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));

Getting the desired size of a UIElement

To get the desired size of a UIElement you need to make sure it is measured. In some circumstances it will already be measured, either because:

  1. You measured it already
  2. You measured one of its ancestors, or
  3. It is part of a PresentationSource (eg it is in a visible Window) and you are executing below DispatcherPriority.Render so you know measurement has already happened automatically.

If your visual element has not been measured yet, you should call Measure on the control or one of its ancestors as appropriate, passing in the available size (or new Size(double.PositivieInfinity, double.PositiveInfinity) if you want to size to content:

element.Measure(availableSize);

Once the measuring is done, all that is necessary is to use the matrix to transform the DesiredSize:

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

Putting it all together

Here is a simple method that shows how to get the pixel size of an element:

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);
}

Note that in this code I call Measure only if no DesiredSize is present. This provides a convenient method to do everything but has several deficiencies:

  1. It may be that the element's parent would have passed in a smaller availableSize
  2. It is inefficient if the actual DesiredSize is zero (it is remeasured repeatedly)
  3. It may mask bugs in a way that causes the application to fail due to unexpected timing (eg. the code being called at or above DispatchPriority.Render)

Because of these reasons, I would be inclined to omit the Measure call in GetElementPixelSize and just let the client do it.

你好,陌生人 2024-09-17 21:15:09

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 // ——
}

这适用于多个监视器也是如此。

Simple proportion between Screen.WorkingArea and 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 // ——
}

This works fine with multiple monitors as well.

执着的年纪 2024-09-17 21:15:09

我找到了一种方法来做到这一点,但我不太喜欢它:

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);
    // ...
}

我不喜欢它,因为 (a) 它需要引用 System.Drawing,而不是使用 WPF API; (b) 我必须自己进行数学计算,这意味着我要复制 WPF 的实现细节。在 .NET 3.5 中,我必须截断计算结果以匹配 ElementHost 在 AutoSize=true 时所做的操作,但我不知道这在 .NET 的未来版本中是否仍然准确。

这似乎确实有效,所以我将其发布,以防对其他人有帮助。但如果有人有更好的答案,请留言。

I found a way to do it, but I don't like it much:

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);
    // ...
}

I don't like it because (a) it requires a reference to System.Drawing, rather than using WPF APIs; and (b) I have to do the math myself, which means I'm duplicating WPF's implementation details. In .NET 3.5, I have to truncate the result of the calculation to match what ElementHost does with AutoSize=true, but I don't know whether this will still be accurate in future versions of .NET.

This does seem to work, so I'm posting it in case it helps others. But if anyone has a better answer, please, post away.

素手挽清风 2024-09-17 21:15:09

刚刚在ObjectBrowser中进行了快速查找,发现了一些非常有趣的内容,您可能想查看一下。

System.Windows.Form.AutoScaleMode,它有一个名为DPI的属性。这是文档,它可能就是您正在寻找的内容:

公共常量
System.Windows.Forms.AutoScaleMode Dpi
= 2
System.Windows.Forms.AutoScaleMode 的成员

摘要:控制相对于的比例
显示分辨率。常见的
分辨率为 96 和 120 DPI。

将其应用到您的表单中,它应该可以解决问题。

{享受}

Just did a quick lookup in the ObjectBrowser and found something quite interesting, you might want to check it out.

System.Windows.Form.AutoScaleMode, it has a property called DPI. Here's the docs, it might be what you are looking for :

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.

Apply that to your form, it should do the trick.

{enjoy}

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文