刚刚发行的 Windows App SDK 1.1 较前面几个版本,在可用性上有很大的提升。但是在一些细节处的 API 上,还是有很多问题。例如,在通过 DecodePixelWidth 属性给 BitmapImage 对象设置解码尺寸时,无法指定逻辑像素数,只能通过一些 “奇技淫巧” 来绕过这一限制。

为什么指定解码尺寸

当然,不指定解码尺寸也不是不能用。BitmapImage 类会按照图片原始尺寸进行展示。但是这样有两个问题:

  1. 如果原始图片很大,则位图数据将一直驻留内存,带来不必要的内存压力。我司产品曾经因为这一问题,险些酿成事故
  2. 如果图片的实际展示区域远小于图片的原始尺寸,则缩放过程中会造成严重的失真,视觉上很不美观

DecodePixelType 属性

在 UWP 中,BitmapImage 类提供了 DecodePixelType 属性,可以指定设置的解码尺寸是物理像素还是逻辑像素。在 Windows App SDK 中,这一 API 仍然存在,但可惜实际上不生效。参见这一 GitHub Issue.

这意味着我们只能根据当前屏幕的缩放比例,手动从希望得到的逻辑分辨率计算出对应的物理分辨率。

DisplayInformation

在 UWP 中有 DisplayInformation 类,可以用于获取当前视图的缩放比例,仅需一行代码:

1
double scaleFactor = DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel;

之后将其与逻辑分辨率相乘即可得到相应的物理分辨率。

可惜这一 API 在 Windows App SDK 上也无法使用。原因是 GetForCurrentView() 方法需要一个 CoreWindow. 而 Windows App SDK 是基于 Win32 的,没有 CoreWindow 这一说。参见这一 GitHub Issue.

原生互操作

最后本人从这一 GitHub Issue 的评论中找到提示,可以使用 .NET 的原生互操作能力来直接调用 Win32 的 API 来获取缩放比例。通过查阅文档可以得到对应的互操作方法签名。

1
2
3
4
5
6
7
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
[DllImport("User32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);

// https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/nf-shellscalingapi-getscalefactorformonitor
[DllImport("Shcore.dll")]
private static extern int GetScaleFactorForMonitor(IntPtr hMon, out int pScale);

当前窗口的 HWND 可以通过 Windows App SDK 提供的互操作帮助方法获得:

1
2
3
IntPtr hwnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
IntPtr hMon = MonitorFromWindow(hwnd, 1);
int hResult = GetScaleFactorForMonitor(hMon, out int scale);

得到的缩放比例是一个 C++ 枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// https://docs.microsoft.com/en-us/windows/win32/api/shtypes/ne-shtypes-device_scale_factor
typedef enum DEVICE_SCALE_FACTOR {
DEVICE_SCALE_FACTOR_INVALID = 0,
SCALE_100_PERCENT = 100,
SCALE_120_PERCENT = 120,
SCALE_125_PERCENT = 125,
SCALE_140_PERCENT = 140,
SCALE_150_PERCENT = 150,
SCALE_160_PERCENT = 160,
SCALE_175_PERCENT = 175,
SCALE_180_PERCENT = 180,
SCALE_200_PERCENT = 200,
SCALE_225_PERCENT = 225,
SCALE_250_PERCENT = 250,
SCALE_300_PERCENT = 300,
SCALE_350_PERCENT = 350,
SCALE_400_PERCENT = 400,
SCALE_450_PERCENT = 450,
SCALE_500_PERCENT = 500
};

注意到要与 DisplayInformation.RawPixelsPerViewPixel 属性等效的话,需要除以 100. 于是完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Runtime.InteropServices;
using Microsoft.UI.Xaml;
using WinRT.Interop;

namespace DL444.WinAppSdkScalingDemo
{
internal static class WindowExtensions
{
public static double GetScaleFactor(this Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
IntPtr hMon = MonitorFromWindow(hwnd, 1);
int hResult = GetScaleFactorForMonitor(hMon, out int scale);
return hResult == 0 ? scale / 100.0 : 0;
}

[DllImport("User32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);

[DllImport("Shcore.dll")]
private static extern int GetScaleFactorForMonitor(IntPtr hMon, out int pScale);
}
}