开发每个监视器识别 DPI 的 WPF 应用程序

重要的 API

注意

本页介绍Windows 8.1的旧版 WPF 开发。 如果要开发用于Windows 10的 WPF 应用程序,请参阅 GitHub 上的最新文档。

 

Windows 8.1为开发人员提供了新功能,用于创建可按监视器感知 DPI 的桌面应用程序。 为了利用此功能,每个监视器 DPI 感知的应用程序必须执行以下操作:

  • 更改窗口尺寸以保持在任何显示器上显示的物理大小一致
  • 为新窗口大小重新布局和重新呈现图形
  • 选择针对 DPI 级别适当缩放的字体
  • 选择并加载为 DPI 级别定制的位图资产

为了便于创建按监视器 DPI 感知的应用程序,Windows 8.1提供了以下 Microsoft Win32APIs:

  • SetProcessDpiAwareness (或 DPI 清单条目) 将进程设置为指定的 DPI 感知级别,然后确定 Windows 如何缩放 UI。 这将取代 SetProcessDPIAware
  • GetProcessDpiAwareness 返回 DPI 感知级别。 这将取代 IsProcessDPIAware
  • GetDpiForMonitor 返回监视器的 DPI。
  • 当窗口的位置发生更改,使其大部分区域在位置更改之前或用户移动显示滑块之前与 DPI 不同的 DPI 相交时,会将 WM_DPICHANGED 窗口通知发送到每个监视器的 DPI 感知应用程序。 若要创建在用户将应用程序移动到其他显示器时重设大小并重新呈现自身的应用程序,请使用此通知。

有关 Windows 8.1 中桌面应用程序支持的各种 DPI 感知级别的更多详细信息,请参阅编写DPI-Aware桌面和 Win32 应用程序主题。

DPI 缩放和 WPF

Windows Presentation Foundation (WPF) 应用程序默认为系统 DPI 感知。 有关不同 DPI 感知级别的定义,请参阅 编写DPI-Aware桌面和 Win32 应用程序的主题。 WPF 图形系统使用与设备无关的单元来使分辨率和设备独立。 WPF 根据当前系统 DPI 自动缩放每个独立于设备的像素。 这样,当窗口所在的监视器的 DPI 为相同的系统 DPI 时,WPF 应用程序就可以自动缩放。 但是,由于 WPF 应用程序是系统 dpi 感知应用程序,因此当应用程序移动到具有不同 DPI 的监视器或使用控制面板中的滑块更改 DPI 时,OS 将缩放应用程序。 OS 中的缩放可能会导致 WPF 应用程序显得模糊,尤其是在缩放是非整型时。 为了避免缩放 WPF 应用程序,需要将其更新为每个监视器的 DPI 感知。

按监视器感知 WPF 示例演练

按监视器感知 WPF 示例是更新为按监视器 DPI 感知的示例 WPF 应用程序。 示例由两个项目组成:

  • NativeHelpers.vcxproj:这是一个本机帮助程序项目,它实现核心功能,以利用上述 Win32APIs 使 WPF 应用程序按监视器 DPI 感知。 该项目包含两个类:
    • PerMonDPIHelpers:为 DPI 相关操作(例如检索活动监视器的当前 DPI、将进程设置为按监视器 DPI 感知等)提供帮助程序函数的类。
    • PerMonitorDPIWindow:派生自 System.Windows.Window 的基类,它实现使 WPF 应用程序窗口成为按监视器 dpi 感知的功能。 根据监视器的 DPI 而不是系统 DPI 调整窗口大小、图形呈现大小和字体大小。
  • WPFApplication.csproj:使用 PerMonitorDPIWindow (PerMonitorDPIWindow) 的示例 WPF 应用程序,并展示当窗口移动到具有不同 DPI 的监视器或使用“显示”控制面板中的滑块更改 DPI 时,应用程序窗口和呈现的大小如何调整。

若要运行示例,请执行以下步骤:

  1. 下载并解压缩 按监视器感知 WPF 示例
  2. 启动 Microsoft Visual Studio 并选择“文件>打开>项目/解决方案
  3. 浏览到包含解压缩示例的目录。 转到示例名为 的目录,然后双击 Visual Studio Solution (.sln) 文件
  4. 按 F7 或使用 生成 > 解决方案 生成示例
  5. 按 Ctrl+F5 或使用 “调试 > 启动但不调试” 来运行示例

若要查看更改 DPI 对已更新为使用示例中的基类按监视器 DPI 感知的 WPF 应用程序的影响,请将应用程序窗口移动到具有不同 DPI 的显示器和从具有不同 DPI 的显示器移出。 当窗口在监视器之间移动时,将使用 WPF 的可缩放图形系统根据显示器的 DPI 更新窗口大小和 UI 比例,而不是由 OS 缩放。 应用程序的 UI 以本机方式呈现,并且不显示模糊。 如果没有两台具有不同 DPI 的显示器,请通过更改“显示”控制面板中的滑块来更改 DPI。 更改滑块并单击“ 应用 ”将调整应用程序窗口的大小并自动更新 UI 缩放。

使用 WPF 示例中的帮助程序项目将现有 WPF 应用程序更新为按监视器 dpi 感知

如果你有一个现有的 WPF 应用程序,并希望利用示例中的 DPI 帮助程序项目使其感知 DPI,请执行以下步骤。

  1. 下载并解压缩按监视器感知 WPF 示例

  2. 启动 Visual Studio 并选择“文件>打开>项目/解决方案

  3. 浏览到包含现有 WPF 应用程序的目录,然后双击 Visual Studio Solution (.sln) 文件

  4. 右键单击“解决方案>添加>现有项目,显示“添加:现有项目”菜单选择的屏幕截图

  5. 在文件选择对话框中,浏览到包含解压缩示例的目录。 打开示例名为 的目录,浏览到文件夹“NativeHelpers”,选择 Visual C++ 项目文件“NativeHelpers.vcxproj”,然后单击“ 确定”

  6. 右键单击项目“NativeHelpers”,然后选择“ 生成”。 这将生成NativeHelpers.dll,该NativeHelpers.dll将作为对 WPF 应用程序的引用添加到下一步演示生成菜单选择的屏幕截图

  7. 从 WPF 应用程序添加对 NativeHelpers.dll 的引用。 展开 WPF 应用程序项目,右键单击“ 引用 ”,然后单击“ 添加引用...”

  8. 在生成的对话中,展开 “解决方案 ”部分。 在“项目”下,选择“NativeHelpers”,并单击“确定,显示资源管理器对话框的屏幕截图

  9. 展开 WPF 应用程序项目,展开 “属性”,然后打开 AssemblyInfo.cs。 对 AssemblyInfo.cs 进行以下添加

    • 在使用 System.Windows.Media; (引用节中添加对 System.Windows.Media 的引用)
    • 添加 DisableDpiAwareness 属性 ([assembly: DisableDpiAwareness])

    说明其他属性的屏幕截图

  10. 从 PerMonitorDPIWindow 基类继承 main WPF 窗口类

    • 更新main WPF 窗口的 .cs 文件,以从 PerMonitorDPIWindow 基类继承

      • 通过添加 行,在 reference 节中添加对 NativeHelpers 的引用 using NativeHelpers;
      • 从 PerMonitorDPIWindow 类继承main窗口类

      说明 c++ 参考的屏幕截图

    • 更新 main WPF 窗口的 .xaml 文件以继承自 PerMonitorDPIWindow 基类

      • 通过添加 行,在 reference 节中添加对 NativeHelpers 的引用 xmlns:src="clr-namespace:NativeHelpers;assembly=NativeHelpers"
      • 从 PerMonitorDPIWindow 类继承main窗口类

      说明添加 .xaml 引用的屏幕截图

  11. 按 F7 或使用 生成 > 解决方案 生成示例

  12. 按 Ctrl+F5 或使用 “调试 > 启动但不调试” 来运行示例

按监视器感知 WPF 示例应用程序演示了如何通过响应WM_DPICHANGED窗口通知,将 WPF 应用程序更新为按监视器 DPI 感知。 为了响应窗口通知,此示例根据窗口所打开的监视器的当前 DPI 更新 WPF 所使用的缩放转换。 窗口通知的 wParam 包含 wParam 中的新 DPI。 lParam 包含一个矩形,该矩形具有新建议窗口的大小和位置,并针对新的 DPI 进行了缩放。

注意:

注意

由于此示例将覆盖 WPF 窗口的根节点的窗口大小和缩放转换,因此在以下情况下,应用程序开发人员可能需要进一步的工作:

  • 窗口的大小会影响应用程序的其他部分,例如此 WPF 窗口托管在另一个应用程序中。
  • 扩展此类的 WPF 应用程序正在对根视觉对象设置一些其他转换;示例可能会覆盖 WPF 应用程序本身正在应用的一些其他转换。

 

WPF 示例中的帮助程序项目概述

为了使现有 WPF 应用程序按监视器 DPI 感知,NativeHelpers 库提供以下功能:

  • 将 WPF 应用程序标记为按 ponitor DPI 感知: 通过对当前进程调用 SetProcessDpiAwareness ,WPF 应用程序被标记为按监视器 DPI 感知。 将应用程序标记为按监视器 DPI 感知将确保

    • 当系统 DPI 与应用程序窗口所打开的监视器的当前 DPI 不匹配时,OS 不会缩放应用程序
    • 每当窗口的 DPI 更改时,将发送 WM_DPICHANGED 消息
  • 根据窗口所在监视器的初始 DPI 调整窗口尺寸、重新布局和重新呈现图形内容并选择字体: 将应用程序标记为按监视器 DPI 感知后,WPF 仍会根据系统 DPI 缩放窗口大小、图形和字号。 由于在应用启动时,系统 DPI 不保证与启动窗口的监视器的 DPI 相同,因此加载窗口后,库会调整这些值。 基类 PerMonitorDPIWindowOnLoaded () 处理程序中更新这些内容。

    通过更改窗口的 WidthHeight 属性来更新窗口大小。 通过将适当的缩放转换应用于 WPF 窗口的根节点来更新布局和大小。

    void PerMonitorDPIWindow::OnLoaded(Object^ , RoutedEventArgs^ ) 
    {   
    if (m_perMonitorEnabled)
        {
        m_source = (HwndSource^) PresentationSource::FromVisual((Visual^) this);
        HwndSourceHook^ hook = gcnew HwndSourceHook(this, &PerMonitorDPIWindow::HandleMessages);
        m_source->AddHook(hook); 
    
        //Calculate the DPI used by WPF.                    
        m_wpfDPI = 96.0 *  m_source->CompositionTarget->TransformToDevice.M11; 
    
        //Get the Current DPI of the monitor of the window. 
        m_currentDPI = NativeHelpers::PerMonitorDPIHelper::GetDpiForWindow(m_source->Handle);
    
        //Calculate the scale factor used to modify window size, graphics and text
        m_scaleFactor = m_currentDPI / m_wpfDPI; 
    
        //Update Width and Height based on the on the current DPI of the monitor
        Width = Width * m_scaleFactor;
        Height = Height * m_scaleFactor;
    
        //Update graphics and text based on the current DPI of the monitor
    UpdateLayoutTransform(m_scaleFactor);
        }
    }
    
    void PerMonitorDPIWindow::UpdateLayoutTransform(double scaleFactor)
    {
    // Adjust the rendering graphics and text size by applying the scale transform to the top         
    level visual node of the Window     
    
    if (m_perMonitorEnabled) 
        {       
            auto child = GetVisualChild(0);
            if (m_scaleFactor != 1.0) 
           {
            ScaleTransform^ dpiScale = gcnew ScaleTransform(scaleFactor, scaleFactor);
            child->SetValue(Window::LayoutTransformProperty, dpiScale);
            }
            else 
            {
            child->SetValue(Window::LayoutTransformProperty, nullptr);
            }           
        }
    }
    
  • 响应WM_DPICHANGED窗口通知: 根据窗口通知中传递的 DPI 更新窗口大小、图形和字号。 基类 PerMonitorDPIWindow 处理 HandleMessages () 方法中的窗口通知。

    通过使用窗口消息的 lparam 中传递的信息调用 SetWindowPos 来更新窗口大小。 通过将适当的缩放转换应用于 WPF 窗口的根节点来更新布局和图形大小。 使用窗口消息 的 wparam 中传递的 DPI 计算比例系数。

    IntPtr PerMonitorDPIWindow::HandleMessages(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, bool% )
    {
    double oldDpi;
    switch (msg)
        {
        case WM_DPICHANGED:
        LPRECT lprNewRect = (LPRECT)lParam.ToPointer();
        SetWindowPos(static_cast<HWND>(hwnd.ToPointer()), 0, lprNewRect->left, lprNewRect-
            >top, lprNewRect->right - lprNewRect->left, lprNewRect->bottom - lprNewRect->top, 
           SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOACTIVATE);
        oldDpi = m_currentDPI;
        m_currentDPI = static_cast<int>(LOWORD(wParam.ToPointer()));
        if (oldDpi != m_currentDPI) 
            {
            OnDPIChanged();
            }
        break;
        }
    return IntPtr::Zero;
    }
    
    void PerMonitorDPIWindow::OnDPIChanged() 
    {
    m_scaleFactor = m_currentDPI / m_wpfDPI;
    UpdateLayoutTransform(m_scaleFactor);
    DPIChanged(this, EventArgs::Empty);
    }
    
    void PerMonitorDPIWindow::UpdateLayoutTransform(double scaleFactor)
    {
    // Adjust the rendering graphics and text size by applying the scale transform to the top         
    level visual node of the Window     
    
    if (m_perMonitorEnabled) 
        {       
            auto child = GetVisualChild(0);
            if (m_scaleFactor != 1.0) 
           {
            ScaleTransform^ dpiScale = gcnew ScaleTransform(scaleFactor, scaleFactor);
            child->SetValue(Window::LayoutTransformProperty, dpiScale);
            }
            else 
            {
            child->SetValue(Window::LayoutTransformProperty, nullptr);
            }           
        }
    }
    

处理图像等资产的 DPI 更改

为了更新图形内容,示例 WPF 应用程序将缩放转换应用于 WPF 应用程序的根节点。 虽然这适用于 WPF 本机呈现的内容 (矩形、文本等) ,但这意味着图像等位图资产由 WPF 缩放。

为了避免缩放导致位图模糊,WPF 应用程序开发人员可以编写自定义 DPI 图像控件,该控件根据窗口所打开的监视器的当前 DPI 选择不同的资产。 当 DPI 更改时,图像控件可以依赖于为从 PerMonitorDPIWindow 使用的 WPF 窗口触发的 DPIChanged () 事件。

注意

图像控件还应在应用启动期间在 Loaded () WPF 窗口事件处理程序中选择正确的控件。