使用 SOUI 开发高 DPI 桌面应用程序[转载]
原文:使用 SOUI 开发高 DPI 桌面应用程序_吹泡泡的小猫的博客-CSDN博客
补充说明:soui3以后版本对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 50
- int 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,当时在这个问题上浪费了不少时间,希望你可以避免。
参考文献
使用 SOUI 开发高 DPI 桌面应用程序[转载]的更多相关文章
- (原创)高DPI适配经验系列:(四)高DPI适配示例
一.前言 光说不练假把式. 原理说再多,也不如一个例子直观明了.所以本篇文章就来通过一个例子演示一下高DPI适配的流程. 相信看完的你,一定会有所收获! 本文地址:https://www.cnblog ...
- 支持 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 应用开发
Windows 10 自 1703 开始引入第二代的多屏 DPI 机制(PerMonitor V2),而 WPF 框架可以支持此第二代的多屏 DPI 机制. 本文将介绍 WPF 框架利用第二代多屏 D ...
- Windows 下的高 DPI 应用开发(UWP / WPF / Windows Forms / Win32)
本文将介绍 Windows 系统中高 DPI 开发的基础知识.由于涉及到坐标转换,这种转换经常发生在计算的不知不觉中:所以无论你使用哪种 Windows 下的 UI 框架进行开发,你都需要了解这些内容 ...
- electron之Windows下使用 html js css 开发桌面应用程序
1.atom/electron github: https://github.com/atom/electron 中文文档: https://github.com/atom/electron/tree ...
- Visual Studio 2012 开发环境配置+控制台工具+桌面应用程序
一.界面布局视图设置 1.窗口的布局.控制台窗口运行恢复到开发环境的设置方法 也可以保存好设好的个性化设置,导入设置: 2.视图|服务器资源管理器(sever explorer) 可以访问数据源.服务 ...
- 解决VS在高DPI下设计出的Winform程序界面变形问题
在目前高分屏流行的情况下,windows缩放与布局仍然设置为100%就显得太小(特别是笔记本),通常会调整为125%或150%, VS在缩放与布局设置为非100%的时候,就会自动启动DPI感知模式,以 ...
- QtQuick桌面应用程序开发指导 3)达到UI而功能_B 4)动态管理Note物_A
3.2 把Page Item和Marker Item绑定 之前我们实现了PagePanel组件, 使用了三个state来切换Page组件的opacity属性; 这一步我们会使用Marker和Marke ...
- 修改minifest使桌面软件支持高dpi
在VisualStudio中可以很方便的设置manifest以支持高dpi的用户界面.当然也可以手工修改manifest文件来添加对高dpi的支持. QQ在高dpi方面做的尤其差,对高dpi的支持迟迟 ...
- Win10强制程序高DPI缩放设置
起因 工作原因,需要在win10上安装数个古老vc版本(vc6,vc2008,vc2010),但是显示器是2K的,DPI缩放有问题 尝试 VC6比较好解决:右键,属性,兼容性,更改高DPI设置,勾选替 ...
- 手把手教会 VS2022 设计 Winform 高DPI兼容程序 (net461 net6.0 双出)
本文主要解决两个问题 C# Winform高DPI字体模糊. 高DPI下(缩放>100%), UI设计器一直提示缩放到100%, 如果不重启到100%,设计的控件会乱飞. 建立测试程序 新建.N ...
随机推荐
- 关于 java.util.concurrent.RejectedExecutionException
遇到java.util.concurrent.RejectedExecutionException 目前看来,最主要有2种原因. 第一: 你的线程池ThreadPoolExecutor 显示的shut ...
- MySQL之数据排序
在MySQL中,我们经常需要从数据库中检索数据,并根据特定的要求对数据进行排序.通常情况下,我们会根据数据中某一列的值进行排序,例如按照价格从低到高或从高到低对商品进行排序.但有时候,我们需要在数据中 ...
- Litctf2024-郑州轻工业大学第二届ctf-校内赛道wp
战队:怎落笔都不对 最终成绩校内第4 MISC 1. 盯帧珍珠 打开文件发现是一个图片,放入 010 查看得文件头是 gif 格式 改为gif后缀得到一个GIF图,在下面这个网站分解,即可得到flag ...
- 使用Docker快速部署一个Net项目
前言 Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级.可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化. 优点 Web 应用的自动化打包和发布. 自动化测试和 ...
- MYSQL8以上修改初始root密码的方法
很遗憾的告诉你,你网上查到的各种改my.enf ,各种update,在新版MYSQL中,统统失效. 解决方法,如下: 1. [root@yisu-5f735cb14d716 ~]# service m ...
- Vue开启Gzip
Vue配置 1.安装 npm install --save-dev compression-webpack-plugin@5.0.0 const CompressionWebpackPlugin = ...
- 【信号与系统】求使系统稳定的常数K的范围
- Go语言实现国密证书加密与解析技术详解
Go语言实现国密证书加密与解析技术详解 前言 在当今数字化时代,信息安全成为企业和个人关注的焦点.国密算法作为中国自主研发的加密标准,广泛应用于各类安全场景.Go语言以其简洁.高效的特性,成为众多开发 ...
- Qt音视频开发42-人脸识别客户端
一.前言 人脸识别客户端程序,不需要和人脸识别相关的库在一起,而是通过协议通信来和人脸识别服务端通信交互,人脸识别客户端和服务端程序框架,主要是为了提供一套通用的框架,按照定好的协议,实现人脸识别的相 ...
- Qt音视频开发25-ffmpeg音量设置
一.前言 音视频的播放.关闭.暂停.继续这几个基本功能,绝大部分人都是信手拈来的搞定,关于音量调节还是稍微饶了下弯弯,最开始打算采用各个系统的api来处理,坐下来发现不大好,系统的支持不完美,比如有些 ...