解决 DPI 问题

越来越多的设备附带“高分辨率”屏幕。 这些屏幕通常每英寸超过 200 像素(ppi)。 在这些计算机上使用应用程序时,需要将内容放大,以便在设备的正常观看距离下清晰查看内容。 截至 2014 年,高密度显示器的主要目标是移动设备(平板电脑、拍壳笔记本电脑和手机)。

Windows 8.1 及更高版本包含多个功能,使这些计算机能够同时与显示器和环境配合使用,同时将计算机连接到高密度和标准密度显示器。

  • Windows 可以使用“使文本和其他项目更大或更小”设置(自 Windows XP 起可用)将内容缩放到设备。

  • Windows 8.1 及更高版本将自动缩放内容,使大多数应用程序在显示不同像素密度之间移动时保持一致。 当主显示器是高密度(200% 缩放),辅助显示器是标准密度(100%),Windows 将自动缩放应用程序窗口内容在辅助显示器上(为应用程序呈现的每 4 个像素显示 1 个像素)。

  • 对于显示器的像素密度和查看距离(Windows 7 及更高版本,OEM 可配置),Windows 默认为正确的缩放比例。

  • 从 Windows 8.1 S14 起,Windows 可以在超过 280 ppi 的新设备上自动将内容扩展到 250%。

    Windows 有一种处理缩放用户界面的方法,以充分利用像素数量的增加。 应用程序通过声明自己为“系统 DPI 感知”来加入此系统。未执行此操作的应用程序将由系统自动缩放。 这可能会导致整个应用程序像素均匀拉伸,带来“模糊不清”的用户体验。 例如:

    DPI 问题模糊

    Visual Studio 选择识别 DPI 缩放,因此不会“虚拟化”。

    Windows(和 Visual Studio)利用多种 UI 技术,这些技术具有处理系统设置的缩放系数的不同方法。 例如:

  • WPF 以独立于设备的方式度量控件(单位,而不是像素)。 WPF UI 会自动根据当前 DPI 进行缩放。

  • 无论 UI 框架如何,所有文本大小都以磅为单位表示,因此由系统视为与 DPI 无关。 当绘制到显示设备时,Win32、WinForms 和 WPF 中的文本已正确纵向扩展。

  • Win32/WinForms 对话框和窗口具有启用根据文本内容调整大小的布局的方法和机制(例如,通过网格布局、流式布局和表格布局面板)。 这些功能可以避免在字体大小增加时不缩放的硬编码像素位置。

  • 系统或资源根据系统指标(例如,SM_CXICON 和 SM_CXSMICON)提供的图标已按比例放大。

旧版 Win32 (GDI、GDI+) 和基于 WinForms 的 UI

虽然 WPF 已经具有很高的 DPI 感知,但我们的大部分基于 Win32/GDI 的代码最初不是用 DPI 感知编写的。 Windows 提供了 DPI 缩放 API。 Win32 问题的修复应在整个产品中一致地使用这些问题。 Visual Studio 提供了一个帮助程序类库,以避免重复功能并确保产品之间的一致性。

高分辨率图像

本部分主要面向扩展 Visual Studio 2013 的开发人员。 对于 Visual Studio 2015,请使用内置于 Visual Studio 中的映像服务。 你可能还发现需要支持/面向许多版本的 Visual Studio,因此在 2015 年使用映像服务不是一个选项,因为它在以前的版本中不存在。 此部分也适合你。

扩大过小的图像

使用一些常见方法,可以将过小的图像放大并在 GDI 和 WPF 上呈现。 托管 DPI 帮助程序类可用于内部和外部 Visual Studio 集成器,以解决缩放图标、位图、图像条纹和图像列表的问题。 基于 Win32 的本机 C/C++ 帮助程序可用于缩放 HICON、HBITMAP、HIMAGELIST 和 VsUI::GdiplusImage。 位图的缩放通常只需在引用辅助库后进行单行更改。 例如:

(WinForms) DpiHelper.LogicalToDeviceUnits(ref image);

缩放映像列表取决于映像列表是在加载时完成还是是在运行时追加的。 如果在加载时完成,请使用图像列表进行调用 LogicalToDeviceUnits() ,就像是位图一样。 当代码在撰写图像列表之前需要加载单个位图时,请确保缩放图像列表的图像大小:

imagelist.ImageSize = DpiHelper.LogicalToDeviceUnits(imagelist.ImageSize);

在本机代码中,可以在创建图像列表时缩放尺寸,如下所示:

ImageList_Create(VsUI::DpiHelper::LogicalToDeviceUnitsX(16),VsUI::DpiHelper::LogicalToDeviceUnitsY(16), ILC_COLOR32|ILC_MASK, nCount, 1);

库中的函数允许指定调整大小算法。 缩放要放置在图像列表中的图像时,请确保指定用于透明度的背景色,或使用 NearestNeighbor 缩放(这将导致失真在 125% 和 150%)。

DpiHelper请参阅有关 MSDN 的文档。

下表显示了如何按相应的 DPI 缩放因子缩放图像的示例。 橙色中概述的图像表示自 Visual Studio 2013(100%-200% DPI 缩放)的最佳做法:

DPI 问题缩放

布局问题

可以通过在 UI 中保持点的相对缩放,而不是使用绝对位置(特别是像素单位),来避免常见的布局问题。 例如:

  • 布局/文本位置需要调整以适应纵向扩展的图像。

  • 网格中的列宽需要调整以适应放大文本。

  • 还需要将硬编码的大小或元素之间的空间进行放大。 仅基于文本维度的大小通常没有问题,因为字体会自动放大。

    DpiHelper 类中提供了辅助函数,允许在 X 和 Y 轴上进行缩放。

  • LogicalToDeviceUnitsX/LogicalToDeviceUnitsY(函数允许在 X 轴和 Y 轴上进行缩放)

  • int space = DpiHelper.LogicalToDeviceUnitsX (10):

  • int height = VsUI::DpiHelper::LogicalToDeviceUnitsY(5);

    有几个 LogicalToDeviceUnits 重载,可以用于缩放 Rect、Point 和 Size 等对象。

使用 DPIHelper 库/类缩放图像和布局

Visual Studio DPI 帮助程序库可用于本机代码和托管代码形式,可以在 Visual Studio shell 之外被其他应用程序使用。

若要使用库,请转到 Visual Studio VSSDK 扩展性示例 并克隆 High-DPI_Images_Icons 示例。

在源文件中,包括 VsUIDpiHelper.h 并调用类的 VsUI::DpiHelper 静态函数:

#include "VsUIDpiHelper.h"

int cxScaled = VsUI::DpiHelper::LogicalToDeviceUnitsX(cx);
VsUI::DpiHelper::LogicalToDeviceUnits(&hBitmap);

注释

不要在模块级或类级静态变量中使用帮助程序函数。 该库还对线程同步使用静态,并且可能会遇到顺序初始化问题。 将这些静态变量转换为非静态成员变量,或将它们包装到函数中(以便在首次访问时构造)。

若要从将在 Visual Studio 环境中运行的托管代码访问 DPI 帮助程序函数,请执行以下作:

  • 使用的项目必须引用最新版本的 Shell MPF。 例如:

    <Reference Include="Microsoft.VisualStudio.Shell.14.0.dll" />
    
  • 确保项目具有对 System.Windows.FormsPresentationCorePresentationUI 的引用。

  • 在代码中,使用 Microsoft.VisualStudio.PlatformUI 命名空间并调用 DpiHelper 类的静态函数。 对于受支持的类型(点、大小、矩形等),提供了返回新缩放对象的扩展函数。 例如:

    using Microsoft.VisualStudio.PlatformUI;
    double x = DpiHelper.LogicalToDeviceUnitsX(posX);
    Point ptScaled = ptOriginal.LogicalToDeviceUnits();
    DpiHelper.LogicalToDeviceUnits(ref bitmap);
    
    

在具有缩放功能的用户界面中解决 WPF 图像模糊问题

在 WPF 中,位图由 WPF 自动调整为当前 DPI 缩放级别,使用高质量的 bicubic 算法(默认值),该算法适用于图片或大型屏幕截图,但不适合菜单项图标,因为它引入了感知模糊。

建议:

  • 对于徽标图像和横幅插图,可以使用默认 BitmapScalingMode 大小调整模式。

  • 对于菜单项和图标图像,BitmapScalingMode 应在不会导致其他失真伪影的情况下使用,以消除模糊(在 200% 和 300% 时)。

  • 对于非 100% 的倍数的大型缩放级别(例如,250% 或 350%),使用双三次插值进行图标图像缩放会导致 UI 模糊且失去清晰度。 首先使用 NearestNeighbor 将图像缩放为 100 个% 的最大倍数(例如 200% 或 300%),然后从那里使用 bicubic 缩放,从而获得更好的结果。 有关更多信息,请参阅有关在大型 DPI 级别下预缩放 WPF 图像的特殊情况。

    Microsoft.VisualStudio.PlatformUI 命名空间中的 DpiHelper 类提供可用于绑定的成员 BitmapScalingMode 。 它将允许 Visual Studio shell 根据 DPI 缩放因子统一控制产品中的位图缩放模式。

    若要在 XAML 中使用它,请添加:

xmlns:vsui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.14.0"

<Setter Property="RenderOptions.BitmapScalingMode" Value="{x:Static vs:DpiHelper.BitmapScalingMode}" />

Visual Studio shell 已在顶级窗口和对话框中设置此属性。 在 Visual Studio 中运行的基于 WPF 的 UI 将继承它。 如果设置未传播到特定 UI 片段,则可以在 XAML/WPF UI 的根元素上设置它。 发生此情况的位置包括弹出窗口、具有 Win32 父元素的元素以及在独立进程中运行的设计器窗口,例如 Blend。

某些 UI 可以独立于系统设置的 DPI 缩放级别进行缩放,例如 Visual Studio 文本编辑器和基于 WPF 的设计器(WPF 桌面和 Windows 应用商店)。 在这些情况下,不应使用 DpiHelper.BitmapScalingMode。 若要在编辑器中解决此问题,IDE 团队创建了一个标题为 RenderOptions.BitmapScalingMode 的自定义属性。 根据系统和 UI 的组合缩放级别,将该属性值设置为 HighQuality 或 NearestNeighbor。

特殊案例:为较高 DPI 等级预缩放 WPF 图像

对于不是 100% 的整数倍的缩放比例(例如,250%、350%等),使用双三次插值缩放图标图像会导致图像模糊和界面褪色。 这些图像与清晰的文本搭配起来,给人的印象几乎像是光学错觉。 图像似乎更接近眼睛,并且相比文本来看显得模糊不清。 通过先使用 NearestNeighbor 将图像缩放至100%的最大整数倍(例如200%或300%),然后使用bicubic缩放剩余部分(额外的50%),可改进此放大倍率下的缩放效果。

下面是结果差异的示例,其中第一个图像使用改进的两步缩放算法 100%->200%->250%进行缩放,第二个图像仅使用双立方 100%->250%。

DPI 问题双重缩放示例

为了使 UI 能够使用此双重缩放,需要修改用于显示每个 Image 元素的 XAML 标记。 以下示例演示如何使用 DpiHelper 库和 Shell.12/14 在 Visual Studio 中的 WPF 中使用双缩放。

步骤 1:使用最近邻算法将图像分别放大到 200%、300% 等。

使用应用于绑定的转换器或 XAML 标记扩展来对图像进行预缩放。 例如:

<vsui:DpiPrescaleImageSourceConverter x:Key="DpiPrescaleImageSourceConverter" />

<Image Source="{Binding Path=SelectedImage, Converter={StaticResource DpiPrescaleImageSourceConverter}}" Width="16" Height="16" />

<Image Source="{vsui:DpiPrescaledImage Images/Help.png}" Width="16" Height="16" />

如果图像还需要进行主题化设置(大多数情况下,如果不是全部),标记可以使用不同的转换器,先对图像进行主题化,然后预先缩放。 标记可以根据所需的转换输出使用DpiPrescaleThemedImageConverterDpiPrescaleThemedImageSourceConverter

<vsui:DpiPrescaleThemedImageSourceConverter x:Key="DpiPrescaleThemedImageSourceConverter" />

<Image Width="16" Height="16">
  <Image.Source>
    <MultiBinding Converter="{StaticResource DpiPrescaleThemedImageSourceConverter}">
      <Binding Path="Icon" />
      <Binding Path="(vsui:ImageThemingUtilities.ImageBackgroundColor)"
               RelativeSource="{RelativeSource Self}" />
      <Binding Source="{x:Static vsui:Boxes.BooleanTrue}" />
    </MultiBinding>
  </Image.Source>
</Image>

步骤 2:确保当前 DPI 的最终大小正确。

由于 WPF 使用设置在 UIElement 上的 BitmapScalingMode 属性按当前 DPI 缩放 UI,因此如果图像控件使用预缩放的图像作为其源,显示的图像将看起来比预期大两到三倍。 以下是几种反驳此效果的方法:

  • 如果知道原始图像的维度为 100%,则可以指定图像控件的确切大小。 在应用缩放之前,这些大小将反映 UI 的大小。

    <Image Source="{Binding Path=SelectedImage, Converter={StaticResource DpiPrescaleImageSourceConverter}}" Width="16" Height="16" />
    
  • 如果原始图像的大小未知,则 LayoutTransform 可用于缩减最终图像对象。 例如:

    <Image Source="{Binding Path=SelectedImage, Converter={StaticResource DpiPrescaleImageSourceConverter}}" >
        <Image.LayoutTransform>
            <ScaleTransform
                ScaleX="{x:Static vsui:DpiHelper.PreScaledImageLayoutTransformScale}"
                ScaleY="{x:Static vsui:DpiHelper.PreScaledImageLayoutTransformScale}" />
        </Image.LayoutTransform>
    </Image>
    

为 WebOC 启用 HDPI 支持

默认情况下,WebOC 控件(如 WPF 中的 WebBrowser 控件或 IWebBrowser2 接口)不会启用 HDPI 检测和支持。 结果将是一个嵌入控件,其显示内容在高分辨率显示器上太小。 下面介绍如何在特定 WebOC 实例中启用高 DPI 支持。

实现 IDocHostUIHandler 接口(请参阅 有关 IDocHostUIHandler 的 MSDN 文章:

[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("BD3F23C0-D43E-11CF-893B-00AA00BDCE1A")]
public interface IDocHostUIHandler
{
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int ShowContextMenu(
        [In, MarshalAs(UnmanagedType.U4)] int dwID,
        [In] POINT pt,
        [In, MarshalAs(UnmanagedType.Interface)] object pcmdtReserved,
        [In, MarshalAs(UnmanagedType.IDispatch)] object pdispReserved);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetHostInfo([In, Out] DOCHOSTUIINFO info);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int ShowUI(
        [In, MarshalAs(UnmanagedType.I4)] int dwID,
        [In, MarshalAs(UnmanagedType.Interface)] object activeObject,
        [In, MarshalAs(UnmanagedType.Interface)] object commandTarget,
        [In, MarshalAs(UnmanagedType.Interface)] object frame,
        [In, MarshalAs(UnmanagedType.Interface)] object doc);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int HideUI();
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int UpdateUI();
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int EnableModeless([In, MarshalAs(UnmanagedType.Bool)] bool fEnable);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int OnDocWindowActivate([In, MarshalAs(UnmanagedType.Bool)] bool fActivate);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int OnFrameWindowActivate([In, MarshalAs(UnmanagedType.Bool)] bool fActivate);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int ResizeBorder(
        [In] COMRECT rect,
        [In, MarshalAs(UnmanagedType.Interface)] object doc,
        bool fFrameWindow);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int TranslateAccelerator(
        [In] ref MSG msg,
        [In] ref Guid group,
        [In, MarshalAs(UnmanagedType.I4)] int nCmdID);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetOptionKeyPath(
        [Out, MarshalAs(UnmanagedType.LPArray)] string[] pbstrKey,
        [In, MarshalAs(UnmanagedType.U4)] int dw);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetDropTarget(
        [In, MarshalAs(UnmanagedType.Interface)] IOleDropTarget pDropTarget,
        [MarshalAs(UnmanagedType.Interface)] out IOleDropTarget ppDropTarget);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetExternal([MarshalAs(UnmanagedType.IDispatch)] out object ppDispatch);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int TranslateUrl(
        [In, MarshalAs(UnmanagedType.U4)] int dwTranslate,
        [In, MarshalAs(UnmanagedType.LPWStr)] string strURLIn,
        [MarshalAs(UnmanagedType.LPWStr)] out string pstrURLOut);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int FilterDataObject(
        IDataObject pDO,
        out IDataObject ppDORet);
    }

(可选)实现 ICustomDoc 接口(请参阅 有关 ICustomDoc 的 MSDN 文章:

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("3050F3F0-98B5-11CF-BB82-00AA00BDCE0B")]
public interface ICustomDoc
{
    void SetUIHandler(IDocHostUIHandler pUIHandler);
}

将实现 IDocHostUIHandler 的类与 WebOC 的文档相关联。 如果实现了上述的 ICustomDoc 接口,那么一旦 WebOC 的文档属性有效,请将其转换为 ICustomDoc,调用 SetUIHandler 方法,并传递实现 IDocHostUIHandler 的类。

// "this" references that class that owns the WebOC control and in this case also implements the IDocHostUIHandler interface
ICustomDoc customDoc = (ICustomDoc)webBrowser.Document;
customDoc.SetUIHandler(this);

如果未实现 ICustomDoc 接口,那么一旦 WebOC 的文档属性有效,则需要将其强制转换为 IOleObject,并调用 SetClientSite 方法,传入实现 IDocHostUIHandler 的类。 在传递给 GetHostInfo 方法调用的 DOCHOSTUIINFO 上设置 DOCHOSTUIFLAG_DPI_AWARE 标志:

public int GetHostInfo(DOCHOSTUIINFO info)
{
    // This is what the default site provides.
    info.dwFlags = (DOCHOSTUIFLAG)0x5a74012;
    // Add the DPI flag to the defaults
    info.dwFlags |=.DOCHOSTUIFLAG.DOCHOSTUIFLAG_DPI_AWARE;
    return S_OK;
}

这应该是您使 WebOC 控件支持 HPDI 所需的一切。

提示

  1. 如果 WebOC 控件上的文档属性发生更改,则可能需要将文档与 IDocHostUIHandler 类重新关联。

  2. 如果上述不起作用,则存在 WebOC 未识别 DPI 标志更改的已知问题。 解决此问题的最可靠方法是切换 WebOC 的光学缩放,这意味着两次调用具有两个不同的缩放百分比值。 此外,如果需要此解决方法,则可能需要在每个导航调用上执行该解决方法。

    // browser2 is a SHDocVw.IWebBrowser2 in this case
    // EX: Call the Exec twice with DPI%-1 and then DPI% as the zoomPercent values
    IOleCommandTarget cmdTarget = browser2.Document as IOleCommandTarget;
    if (cmdTarget != null)
    {
        object commandInput = zoomPercent;
        cmdTarget.Exec(IntPtr.Zero,
                       OLECMDID_OPTICAL_ZOOM,
                       OLECMDEXECOPT_DONTPROMPTUSER,
                       ref commandInput,
                       ref commandOutput);
    }