解决 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 有一种处理纵向扩展 UI 的方法,以利用增加的像素计数。 应用程序通过声明自己“系统 DPI 感知”来选择加入此系统。不执行此操作的应用程序由系统扩展。 这可能会导致整个应用程序均匀拉伸像素的“模糊”用户体验。 例如:

    DPI Issues Fuzzy

    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 Issues Scaling

布局问题

主要可以通过保留 UI 中缩放的点而不是使用绝对位置(具体而言,以像素单位为单位)来避免常见的布局问题。 例如:

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

  • 网格中的列需要调整为纵向扩展文本的宽度。

  • 还需要纵向扩展元素之间的硬编码大小或空间。 仅基于文本尺寸的大小通常很好,因为字体会自动纵向扩展。

    类中 DpiHelper 提供了帮助程序函数,允许在 X 轴和 Y 轴上缩放:

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

  • int space = DpiHelper.LogicalToDeviceUnitsX (10):

  • int height = VsUI::D piHelper::LogicalToDeviceUnitsY(5);

    有 LogicalToDeviceUnits 重载允许缩放对象,例如 Rect、Point 和 Size。

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

Visual Studio DPI 帮助程序库在本机和托管窗体中可用,可供其他应用程序在 Visual Studio shell 外部使用。

若要使用库,请转到 Visual Studio VSSDK 扩展性示例 并克隆高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);
    
    

在可缩放 UI 中处理 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% 等)的非常大的缩放级别,使用 bicubic 生成模糊、已冲出 UI 的图标图像。 这些图像与清脆文本的印象几乎就像是光学错觉。 图像似乎更接近眼睛,与文本无关。 通过首先将具有 NearestNeighbor 的图像缩放到最大倍数(例如 200% 或 300%),并使用 bicubic 缩放到其余部分(另外 50%),可以改进此放大大小的缩放结果。

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

DPI Issues Double Scaling Example

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

步骤 1:使用 NearestNeighbor 将图像预缩放到 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" />

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

<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 接口(请参阅有关 IDocHostUIHandlerMSDN 文章:

[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 接口(请参阅有关 ICustomDocMSDN 文章:

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