基于传统技术开发的 Windows 桌面应用,在高分辨率的显示设备上表现得“惨不忍睹”。随着高分辨率显示设备的普及,所有桌面应用程序的开发人员,都需要关注自己的软件在不同的 DPI 上的表现。

1 应用程序感知 DPI 变化

在 Windows 2000 之前,大部分大部分开发人员对显示器分辨率的关注点是如何让自己的程序在低分辨率的显示器上表现正常,因为过低的分辨率会导致窗口界面显示不完整。随着垂直分辨率低于 768 的显示设备逐步被淘汰,为 Windows XP 和 Windows 7 开发软件的程序员在很长一段时间都不需要考虑显示器的分辨率问题。但是近几年,高分辨率显示器开始迅速普及,Windows 8 或 Windows 10 的桌面程序需要再次应对高分辨率显示设备的挑战。

1.1 DPI 感知的类型

为了让应用程序在不同 DPI (Dots-Per-Inchs)显示设备上都表现正常,需要感知显示设备的分辨率变化。随着 Windows 的发展,对 DPI 感知也经历了一系列技术更新。从 DPI 无感知,到 Windows 8.1 开始支持 Per-Monitor,再到 Windows 10 开始支持的 Per-Monitor V2,应用程序感知 DPI 变化,并动态调整窗口显示的方法也越来越简单。

1.1.1 DPI 无感知

传统的 Windows 总是以 96 DPI 显示窗口系统,此时系统的显示缩放比例就是 100%。当用户调整显示缩放比例的时候,实际上调整得是显示器的分辨率(DPI),比如缩放比例 125% 对应的是 120 DPI,150% 缩放比例对应的是 144 DPI,200% 对应的 DPI 是 192。对 DPI 无感知的应用程序,在高 DPI 的情况下会显示一个非常小的窗口,有些情况下会小到看不清楚窗口内容。Windows 系统在高 DPI 的时候,会提示优化应用程序的显示效果,对 DPI 无感知的程序来说,这种“优化”就是用拉伸的方式放大窗口内容。但是这种放大是基于光栅位图的缩放,不是基于矢量技术的缩放,通常会导致窗口显示模糊,尤其是文字的边缘轮廓模糊,人眼看起来非常不舒服。

1.1.2 Per-Monitor

从 Windows 8.1 开始,操作系统为应用程序增加了一种感知系统 DPI 变化的能力,就是 Per-Monitor。当显示设备的 DPI 发生变化的时候,对于使用了 Per-Monitor 技术的应用程序,系统不再做窗口显示的拉伸放大,而是向程序的顶层窗口发送 WM _ DPICHANGED 消息,让应用程序根据变化调整自己。

Per-Monitor 技术的限制性主要是开发人员的应用不方便,顶层窗口在收到 WM _ DPICHANGED 消息的时候,不仅要负责计算所有的子窗口的位置,还需要在窗口创建时的 WM NCCREATE 消息处理中调用 `EnableNonClientDpiScaling` 这个 API,让系统帮忙处理非客户去的正确缩放。

1.1.3 Per-Monitor V2

从 Windows 10 1703 开始,操作系统开始支持 Per-Monitor V2 级别的感知,它比 Per-Monitor 具有更多的感知模式,比如在不同显示分辨率的两个显示器之间拖动窗口的时候,也能收到 WM _ DPICHANGED 消息。另外,Per-Monitor V2 不仅向顶层窗口发送 WM _ DPICHANGED 消息,还向所有的子窗口发送 WM _ DPICHANGED 消息,这就大大减少了主窗口控件调整的复杂度。除此之外,Per-Monitor V2 还自动处理非客户去的正确缩放,对公用对话框(比如文件选择对话框,颜色对话框)也做了正确的缩放处理。

1.2 让应用程序感知 DPI 变化

1.2.1 API 调用

让桌面应用程序支持 DPI 变化有两种方法,一种是采用编程方式在程序初始化的时候调用某个 API 告知操作系统本程序的 DPI 感知能力,另一种是使用程序清单文件(manifest),本节介绍第一种方法。有这种功能的 API 有两个,功能上是等效的。第一个是 `SetProcessDpiAwareness` ,通过 `value` 参数通知操作系统本程序的 DPI 感知级别。使用这个 API 需要包含 shellscalingapi.h 头文件,并导入 Shcore.lib 库,其原型如下:

HRESULT SetProcessDpiAwareness(PROCESS_DPI_AWARENESS value);

`PROCESS_DPI_AWARENESS` 有三个值可选:

typedef enum PROCESS_DPI_AWARENESS {PROCESS_DPI_UNAWARE,PROCESS_SYSTEM_DPI_AWARE,PROCESS_PER_MONITOR_DPI_AWARE
} ;

`PROCESS_DPI_UNAWARE` 表示程序不感知 DPI 变化,效果就是在高 DPI 的显示设备上显示一个非常小的窗口。`PROCESS_SYSTEM_DPI_AWARE` 表示让系统调整在高 DPI 时候的显示,通常就是窗口拉伸放大,会导致窗口内容或文字边缘模糊。`PROCESS_PER_MONITOR_DPI_AWARE` 表示应用程序自己感知 DPI 变化,不需要系统拉伸放大窗口,但是需要接收并处理 WM _ DPICHANGED 消息。

另一个 API 是 `SetProcessDpiAwarenessContext`,用于设置应用程序感知 DPI 变化的上下文,其原型是:

BOOL SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT value);

`DPI_AWARENESS_CONTEXT` 有 5 个值可选,分别是:

DPI_AWARENESS_CONTEXT_UNAWARE
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED

`DPI_AWARENESS_CONTEXT_UNAWARE` 表示程序不感知 DPI 变化,`DPI_AWARENESS_CONTEXT_SYSTEM_AWARE`表示让系统拉伸放大窗口(在高 DPI 的情况下),`DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE` 表示支持 Per-Monitor 感知,`DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2`表示支持 Per-Monitor V2 感知,`DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED` 在 Windows 10 1809 版本引入,效果和 `DPI_AWARENESS_CONTEXT_UNAWARE` 类似,但是系统会利用 GDI 技术的提升改善一下文字在拉伸后的显示效果,请阅读本文“参考文献”部分的链接了解更多 GDI Scaling 技术。

1.2.2 使用程序清单文件

相对于调用 API 的方式,微软更推荐使用程序清单文件(Manifest File)的方式让应用程序支持 DPI 感知。在程序清单文件中有两种标签( tag )可以设置程序的 DPI 感知,一种是在 Windows 10 之前引入的 <dpiAware>,另一种是在 Windows 10 之后引入的 <dpiAwareness>。<dpiAware> 标签只支持系统级别 DPI 感知,就是通过拉伸放大窗口的方式适应高 DPI,比如:

<dpiAware>false</dpiAware>

表示程序不支持 DPI 感知,而

<dpiAware>true</dpiAware>

表示支持系统级别 DPI 感知。

随着 Windows 10 引入的 <dpiAwareness>  标签具有更多的感知模式:

<dpiAwareness>unaware</dpiAwareness>
<dpiAwareness>system</dpiAwareness>
<dpiAwareness>PerMonitor</dpiAwareness>
<dpiAwareness>PerMonitorV2</dpiAwareness>

使用清单文件的好处就是可以在清单文件中同时包含这两种标签,因为在旧版本的 Windows 系统上,会忽略不支持的 <dpiAwareness> 标签,而在支持 <dpiAwareness> 标签的新系统上,又会忽略旧的 <dpiAware> 标签,简直完美。如果使用 API 编程方式,就需要判断一下操作系统的版本号,然后设置相应的 DPI 感知能力,显然比使用清单文件要麻烦的多。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"><asmv3:application><asmv3:windowsSettings><dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware><dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness></asmv3:windowsSettings></asmv3:application>
</assembly>

1.2.3 响应 WM _ DPICHANGED 消息

支持 Per Monitor 和 Per Monitor V2 感知级别的程序,需要响应 系统发送的 WM _ DPICHANGED 消息,并在消息中处理相关的显示调整。WM _ DPICHANGED 消息的定义如下:

#define WM_DPICHANGED       0x02E0
  • *wParam*

wParam 分两部分,高 16 位是窗口新的 X 轴方向 DPI,低 16 位是窗口新的 Y 轴方向 DPI

  • *lParam*

lParam 是一个 `RECT *` 类型的指针,内容是系统建议在 DPI 调整后的窗口大小和位置,应用程序可根据这个参数调整窗口的大小和位置,当然也可以不接受这个建议,自己计算窗口的大小和位置。

1.2.4 注意事项

需要注意的一点,就是 Per Monitor 级别 DPI 感知的应用程序,需要在窗口创建时调用 `EnableNonClientDpiScaling` 通知系统调整非客户去的显示,这个 API 的原型如下:

BOOL EnableNonClientDpiScaling(HWND hwnd);

一般建议放在 WM NCCREATE 消息中调用这个 API。

对于 Per Monitor V2 级别 DPI 感知的应用程序,不需要做这个事情,不要画蛇添足。

1.3 应用程序需要做的修改

当应用程序启用 DPI 感知后,一些 Windows API 的行为会发生一些变化,有些 API 会根据当前进程的感知上下文返回对应调整后的结果,但是也有一些 API 不会这么做。比如 `GetSystemMetrics()` API ,总是按照 DPI=96 的情况返回相关的数值,比如图标大小,窗口边框宽度。如果需要根据 DPI 变化调整了显示缩放比例之后的数值,需要使用对应的 `GetSystemMetricsForDpi()`。属于此类情况的常见 API 有:

单个 DPI 版本 Per-Monitor 版本
GetSystemMetrics GetSystemMetricsForDpi
AdjustWindowRectEx AdjustWindowRectExForDpi
SystemParametersInfo SystemParametersInfoForDpi
GetDpiForMonitor GetDpiForWindow

除了这些  API 的行为差异之外,如果你的代码中使用了数字作为硬编码的情况,也需要调整。比如创建窗口时指定窗口的大小:

// Add a button
HWND hWndChild = CreateWindow(L"BUTTON", L"Click Me",  WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,  50, 50, 100, 50, hWnd, (HMENU)NULL, NULL, NULL);

这里的位置和大小都是硬编码,如果要支持 DPI 变化感知,就需要做响应的调整计算,比如:

// dpi = 96 时的设计大小
#define INITIALX_96DPI 50
#define INITIALY_96DPI 50
#define INITIALWIDTH_96DPI 100
#define INITIALHEIGHT_96DPI 50int iDpi = GetDpiForWindow(hWnd);
int dpiScaledX = MulDiv(INITIALX_96DPI, iDpi, 96);
int dpiScaledY = MulDiv(INITIALY_96DPI, iDpi, 96);
int dpiScaledWidth = MulDiv(INITIALWIDTH_96DPI, iDpi, 96);
int dpiScaledHeight = MulDiv(INITIALHEIGHT_96DPI, iDpi, 96);HWND hWndChild = CreateWindow(L"BUTTON", L"Click Me",  WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,  dpiScaledX, dpiScaledY, dpiScaledWidth, dpiScaledHeight, hWnd, (HMENU)NULL, NULL, NULL);

还有一种情况就是对于一些不支持 DPI 感知,也没有对应的 DPI 感知版本的陈旧 API,比如 `LoadIcon`、`LoadImage`,此时就需要根据实际形况做处理,使用其他手段将得到的内容做适当的放大或缩小处理。

有些 API 在进程启用了 DPI 感知上下文后,会返回根据缩放级别调整后的虚拟化结果。关于 API 的这些差异,目前只有少数总结出来的资料,缺少系统化的文档。对于一些重要的操作,如果不确定 DPI 感知是否会产生不良的影响,可以考虑使用 `SetThreadDpiAwarenessContext` ,暂时停止当前线程的 DPI 感知,在执行完这些重要的操作后再恢复当前线程的 DPI 感知。

2 使用 SOUI 库

让系统感知 DPI 变化并不难,难得是在 DPI 变化的时候调整窗口的显示。但是如果你使用的界面库支持自动调整,那就非常容易了。SOUI2 的最新版本就支持根据 DPI 动态调整窗口的显示,只需要做很少的操作就可以实现在不同的 DPI 设备上自适应窗口显示。SOUI2 的 Demo 程序中,有个名为 MultiLangs 的例子,就演示了 SOUI2 的自适应窗口显示能力。根据这个例子,再补充上 Per Monitor 级别设置和 WM _ DPICHANGED 消息响应,就可以轻松地让使用 SOUI 做界面的应用程序具有高 DPI 自适应的能力。

2.1 界面资源布置

SOUI2 库中资源布置默认的位置和大小单位都是像素,但是最新的 SOUI2 支持新的单位:dp。对于需要根据 DPI 调整位置和大小的控件,需要使用 dp 单位,比如:size="80dp, 26dp"。

对于图片资源来说,可以根据每种分辨率设置一种图片,SOUI 会根据系统 DPI 选择适当的图片,MultiLangs 例子也演示了这种用法。不过,对于大部分使用 imgframe 九宫格类型贴图的图片资源来说,基本上不用考虑为不同的分辨率准备不同的图片资源文件,因为对于大多数系统来说,最大 200% 的调整,也就是需要图片大小放大两倍, SOUI2 使用的渲染系统应对这种级别的拉伸放大绰绰有余。

2.2 WM _ DPICHANGED 消息处理

SOUI2 对 `WM _ DPICHANGED` 消息已经做了对应的分派宏,可以在 `BEGIN_MSG_MAP_EX` 中直接使用:

void OnDpiChanged(WORD dpi, const RECT* desRect);BEGIN_MSG_MAP_EX(CMainDlg)MSG_WM_DPICHANGED(OnDpiChanged)END_MSG_MAP()

在这个消息响应中,要给全部 SOUI 的子窗口发送一个 `UM_SETSCALE` 消息,通知所有子窗口缩放级别发生变化,同时调整窗口的大小。

void CMainDlg::OnDpiChanged(WORD dpi, const RECT* desRect)
{int nScale = ScaleFromSystemDpi(dpi); //根据 DPI 返回对应的缩放级别SDispatchMessage(UM_SETSCALE, nScale, 0);//接受系统的建议位置和大小SetWindowPos(NULL, desRect->left, desRect->top, desRect->right - desRect->left,desRect->bottom - desRect->top, SWP_NOZORDER | SWP_NOACTIVATE);
}

`ScaleFromSystemDpi` 的作用是根据 DPI 返回对应的缩放比例,一般 96 对应的是 100%,120 对应的是 125%,144 对应的是 150%,192 对应的是 200%。

2.3 使用 SDpiHandler 嵌入类

如果不想手工处理 `WM _ DPICHANGED` 消息,还可以考虑使用 SOUI 提供的 SDpiHandler 嵌入类,直接将 `WM _ DPICHANGED` 消息的默认处理加入窗口的消息分派表中。 SDpiHandler 类的使用非常简单,首先在目标窗口的继承关系中添加 SDpiHandler,例如:

class CMainDlg : public SHostWnd....  , // 其他派生关系public SDpiHandler<CMainDlg>

然后在消息分派表中插入这个嵌入类的消息分派:

 BEGIN_MSG_MAP_EX(CMainDlg)...CHAIN_MSG_MAP(SDpiHandler<CMainDlg>)...END_MSG_MAP()

2.4 获取当前系统的 DPI

尽管`WM _ DPICHANGED` 消息会告知应用程序当前的 DPI 变化,但是当程序启动的时候,是不会收到这个消息的,所以需要在程序启动的时候获取系统 DPI,初始化 SOUI 窗口系统的缩放级别。Windows 10 1607 以后的 SDK 提供了 `GetDpiForSystem` API,可以直接获取系统的 DPI,这个 API 的原型是:

UINT GetDpiForSystem()

对于较早的 Windows 版本,可以用这个传统的方法获取屏幕显示设备的 DPI:

UINT GetSystemDeviceDpi()
{HDC hDCScreen = ::GetDC(NULL);UINT dpiY = ::GetDeviceCaps(hDCScreen, LOGPIXELSY);::ReleaseDC(NULL, hDCScreen);return dpiY;
}

2.5 注意事项

尽管 SOUI 使用很方便,但是还是有一些“坑”需要填上,比如字体,对于全局的字体,指定字号时可以不适用 dp 单位,比如在资源定义中定义的全局字体:

```
<font face="微软雅黑" size="14"/>
```

但是如果是在控件中使用 font 属性指定的字体,则需要使用 dp 单位,否则控件的字体将始终使用指定的字号,不会随着 DPI 的缩放级别变化,比如这样使用:

```
font="face:微软雅黑,size:14dp"
```

还有就是对于菜单的使用,无论是 `SMenu` 还是 `SMenuEx`类,`TrackPopupMenu()` 函数的最后一个参数是缩放级别,需要使用 `GetScale()` 获取当前窗口的缩放级别,然后传递给菜单。如果不传递缩放级别参数的话,系统总是使用默认值 100,当时在这个问题上浪费了不少时间,希望你可以避免。

参考文献

Improving the high-DPI experience in GDI-based Desktop apps

High DPI Desktop Application Development on Windows

使用 SOUI 开发高 DPI 桌面应用程序相关推荐

  1. 过高DPI桌面生活可能会很痛苦

    I've been using this Lenovo Yoga 2 Pro for the last few weeks, and lemme tell you, it's lovely. It's ...

  2. 2022年,开发独立 EXE 桌面应用程序,用什么语言、技术合适

    先说明一下对「独立 EXE」的要求: 1.程序只有一个 EXE 文件,不需要额外的安装程序. 2.程序的体积要足够小,不需要额外安装其他运行库.写「独立 EXE」本就是为了方便,如果没多少功能体积就达 ...

  3. C\C++ Qt开发的动态桌面壁纸程序

    这个程序可以实现把图片.动图.视频当成壁纸显示在图标图层之下,软件会保存上一次关闭时的选择 重启时恢复,有什么问题评论区看到会回复 有问题欢迎指出,需要编译好的程序 可以私信或留下联系方式发送 废话不 ...

  4. flex 开发的电子画板(桌面应用程序)

    可以画线,圆,方块,以及截取当前画板图像的功能. 直接上代码吧,我的环境是AIR 2.6, FB4. ElectronicBoard.mxml  主程序. <?xml version=" ...

  5. 教你一键开发桌面应用程序

    前言 因为公司项目关系,要开发一款桌面应用程序.说到桌面应用程序,有很多种解决方案,最终我们选了Electron,备受前端开发喜爱的VS Code正是Electron开发的,今天我们就来聊聊这个Ele ...

  6. Electron入门教程1 —— 编写第一个桌面应用程序

    前言: 最近临时起意,想开发桌面应用程序,但是我们肯定都先会想到微软的C#.而我又不想花时间去学习C#,而且就算学了C#,还是很难快速开发出好看的Windows桌面应用.所以此时我就想,既然移动app ...

  7. .net core WinFrom桌面应用程序 初体验

    1..net Core 创建WinFrom桌面应用程序 前提是安装好.netCore开发环境,以及最新的.netCore3.0以上 使用命令创建,打开CMD,输入:dotnet new winform ...

  8. VC获取屏幕dpi,win32绘图适配高dpi模式,windows屏幕缩放图像拉伸失真问题

    VC获取屏幕dpi,win32绘图适配高dpi模式 默认MFC支持高dpi模式 通过winapi提供接口获取屏幕dpi 使用==StretchBlt==代替==BitBlt==进行图像绘制 注意==M ...

  9. 高DPI下部分软件显示不全的解决方法

    首先这种情况大部分出现在的笔记本上,因为现在大部分笔记本的显示屏像素点更小,使得字也变得十分小,出于对眼睛的保护,大家会调整DpI让字体看起来不那么吃力 但是这样也带来了一些麻烦,如软件界面显示不全, ...

  10. python开发桌面软件-python适合windows的桌面应用程序开发吗?

    谢... 谢特!... (自己跑过来的). 曾经从事过几个桌面应用程序的开发, 来提供些建议 Qt 的 signal-slot 的机制做得很不错, 充分理解以后开发起来很顺手. 早期项目里, 举个栗子 ...

最新文章

  1. 清华学长手把手带你做UI自动化测试
  2. 亚马逊是如何进行软件开发的
  3. 七段式svpwm和5段式的区别_五段和七段SVPWM的比较分析.pdf
  4. Unity4.x 2D游戏开发基础教程第1章Unity及其组成的介绍
  5. c语言整形数组相加,[c语言]将两个整形升序数组合并为一个升序数组
  6. js 函数定义的方式
  7. 协方差矩阵的概念,算法以及自己的一些理解
  8. 一篇文章了解蛋白质组学研究
  9. OpenGL Primitive Restart原始重启的实例
  10. 浅谈Java锁,与JUC的常用类,集合安全类,常用辅助类,读写锁,阻塞队列,线程池,ForkJoin,volatile,单例模式不安全,CAS,各种锁
  11. 发现dba_segments和dba_extents中统计段空间大小居然不一样
  12. kafka通过零拷贝实现高效的数据传输
  13. pstate0 vid数值意义_天体运动的简单数值计算
  14. LeetCode 1750. 删除字符串两端相同字符后的最短长度(双指针)
  15. 【C++ primer】第七章 函数-C++的编程模块
  16. 对话英特尔高级副总裁 Raja:软件将为硬件释放无限潜力
  17. ISO/IEC 20000 信息技术(IT)服务管理体系及全套最新标准资料
  18. matlab复杂网络上的博弈演化,科学网—复杂网络上的演化博弈研究 - 汪秉宏的博文...
  19. Visual Studio2019安装vsix扩展文件
  20. 英尺C语言,C语言中关于英尺、英寸、厘米的换算

热门文章

  1. 《控制论导论》读书:机构-黑箱
  2. 2021年PTCMS4.3最新采集规则13条
  3. 学刘红杰老师博客营销,知如何提高博客访问流量
  4. roseha linux,RoseHA 9.0 for Linux快速安装说明_v2.0-2015-04.pdf
  5. 使用insightface进行人脸识别批量下载图片
  6. emouse思·睿—评论与观点整理之一
  7. logback教程logback快速入门超实用详细教程收藏这一篇就够了(万字长文)
  8. 联合分布及其随机变量
  9. PHP采集利器:phpQuery,像jQuery一样轻松采集内容
  10. 关于企业电子工单系统的解决方案