- 示例工程
【免费下载链接】Windows-classic-samples
This repo contains samples that demonstrate the API used in Windows classic desktop applications.
本文基于 Windows-classic-samples 仓库中的DPIAwarenessPerWindow示例,讲解桌面 Win32 应用在高 DPI(高缩放)场景下的四种 DPI 感知模式(DPI-unaware、System-DPI aware、Per-Monitor DPI aware、Per-Monitor DPI aware v2),并深入剖析该示例如何借助SetThreadDpiAwarenessContext实现"同一进程内不同顶层窗口使用不同 DPI 缩放模式",以及如何用SetThreadDpiHostingBehavior隔离子窗口的 DPI 行为。读完本文,你能掌握各模式的适用场景、WM_DPICHANGED下的手动缩放流程、非客户区缩放开关,以及宿主管线中"插件式"外部 HWND 的 DPI 隔离方案。
示例概览:一个启动器窗口 + 可切换感知模式的示例窗口
该示例的核心目的是直观展示不同版本的 Windows 如何处理 Win32 UI 的高 DPI 缩放。从 Samples/DPIAwarenessPerWindow/README.md 的描述看,应用由两个 UI 部分组成:
- 启动器窗口(Launcher):一个对话框,提供若干按钮,每个按钮对应一种 DPI 感知模式,点击后可按该模式弹出一个新的示例窗口;
- 示例窗口(Sample window):展示窗口自身的 DPI 感知模式、当前 DPI 值,内部还有一个按钮用于打开系统对话框,以及一块来自另一个 DLL 的"外部内容"(模拟插件)。
创建桌面应用时,需要指定应用的 DPI 感知级别,共有以下四种模式:
| 模式 | 行为特征 |
|---|---|
| DPI-unaware | 应用始终按 96 DPI 绘制,Windows 将其整体按位图拉伸(放大会发虚) |
| System-DPI aware | 按主显示器(系统)的 DPI 绘制;移动到不同 DPI 的显示器时由 Windows 再缩放 |
| Per-Monitor DPI aware | 窗口跟随所在显示器的 DPI,在WM_DPICHANGED时收到通知并自行重布局 |
| Per-Monitor DPI aware v2 | 在 v1 基础上提供DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2独立上下文,非客户区可自动按监视器 DPI 缩放,并支持子窗口 DPI 隔离 |
每种模式在"应用所在显示器的 DPI/缩放因子发生变化"时行为不同。触发这类变化的场景包括:应用窗口移动到不同 DPI 的显示器、通过远程桌面从不同 DPI 的设备接入、或在显示设置页中更改缩放因子。
关键机制:从"进程唯一模式"到"逐窗口模式"
在SetThreadDpiAwarenessContextAPI 出现之前,一个进程在整个生命周期内只能有一种 DPI 感知模式,开发者必须为整个应用的所有 UI 权衡同一种缩放策略。
SetThreadDpiAwarenessContext的引入带来了更大的灵活性,可以"分而治之"地逐步改进应用的 DPI 支持。例如:把最重要的窗口优先改造为逐监视器(per-monitor)处理 DPI 变化,而让次要的顶层窗口继续由 Windows 按 System-DPI 或 DPI-unaware 方式缩放。
README 中特别强调了两点限制,这也是示例着重验证的:
SetThreadDpiAwarenessContext只作用于顶层窗口。子窗口默认继承父窗口的 DPI 感知模式;- 若要子窗口以不同于父/宿主窗口的缩放模式运行,需配合
SetThreadDpiHostingBehavior覆盖默认继承行为。
运行环境要求
按 Samples/DPIAwarenessPerWindow/README.md 的说明:
- 操作系统(客户端):Windows 10,version 1803 或更新;
- Windows SDK:10.0.17134.0 或更新。
示例最初基于 Windows 10 version 1607、Visual Studio 2015 Update 3 开发,但通常可在后续版本中无需修改直接编译运行。
构建示例
- 启动 Visual Studio,选择File>Open>Project/Solution;
- 进入示例所在目录,双击 Microsoft Visual Studio Solution 文件(
DpiAwarenessContext.sln); - 按F5,或使用Build>Build Solution。
该解决方案包含两个工程:客户端主程序与插件 DLL,见 DpiAwarenessContext.sln 与 PluginDLL.vcxproj。
运行并观察不同模式的差异
按 README 的"Run the sample"部分,运行后的预期行为为:
- 应用初始化时,显示一个Per-Monitor DPI aware的顶层窗口(即启动器),其中显示该窗口的 DPI 感知模式和窗口 DPI 值;
- 将该顶层窗口移动到 DPI 值不同的显示器上时,窗口会被重新缩放、非客户区随之缩放,窗口中显示的 DPI 数值也会更新;
- 点击任一按钮后,会以该按钮描述的感知模式弹出一个新的顶层窗口。窗口内可见:DPI 相关信息、一个"以相同上下文打开系统对话框"的按钮,以及一段来自另一个 DLL、按 System-DPI 感知渲染的子窗口内容(模拟插件)。
在单显示器设备上,可在应用运行期间修改显示设置中的"更改文本、应用等项目的大小"(缩放因子),同样能观察到不同模式的缩放差异。
源码剖析一:启动器如何为每个窗口选择 DPI 上下文
启动器是一个模态对话框,其窗口过程HostDialogProc根据按钮 ID 选择对应的DPI_AWARENESS_CONTEXT,再调用CreateSampleWindow。从 DpiAwarenessContext.cpp 中的按钮分支可以看到完整的映射关系:
| 启动器按钮 | 资源 ID | 传入的 DPI 上下文 | 附加行为 |
|---|---|---|---|
| Unaware | IDC_BUTTON_UNAWARE | DPI_AWARENESS_CONTEXT_UNAWARE | 无 |
| System aware | IDC_BUTTON_SYSTEM | DPI_AWARENESS_CONTEXT_SYSTEM_AWARE | 无 |
| 8.1 (Per-Monitor v1) | IDC_BUTTON_81 | DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE | 无 |
| 1607 (Per-Monitor v1 + 非客户区缩放) | IDC_BUTTON_1607 | DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE | 置位bEnableNonClientDpiScaling |
| 1703 (Per-Monitor v2) | IDC_BUTTON_1703 | DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 | 无 |
| 1803 (v2 + 子窗口 DPI 隔离) | IDC_BUTTON_1803 | DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 | 置位bChildWindowDpiIsolation |
bEnableNonClientDpiScaling与bChildWindowDpiIsolation两个标志通过CreateParams结构体随CreateWindowExW传入,在WM_NCCREATE阶段生效,这正是"同一进程内逐窗口选择不同感知模式"的核心实现。
源码剖析二:CreateSampleWindow 中的上下文切换
CreateSampleWindow(DpiAwarenessContext.cpp#L350-L386)演示了逐窗口切换 DPI 感知的标准写法:
- 创建前:调用
SetThreadDpiAwarenessContext(context),把目标上下文设为当前线程的 DPI 感知上下文,并保存返回值(之前的上下文); - 以 96 DPI 的未缩放尺寸调用
CreateWindowExW创建窗口(窗口类名为SetThreadDpiAwarenessContextSample),随后按实际 DPI 重新缩放(见下文DoInitialWindowSetup); - 创建后立即恢复:
SetThreadDpiAwarenessContext(previousDpiContext)把线程上下文还原,避免影响同线程后续创建的窗口; - 若启用子窗口 DPI 隔离,则在创建前后同样成对地调用
SetThreadDpiHostingBehavior(DPI_HOSTING_BEHAVIOR_MIXED)并恢复原值。
其中注释明确说明:Windows 10 (1803) 支持"子 HWND 的 DPI 模式隔离",允许子 HWND 运行在与父(宿主)HWND 不同的 DPI 缩放模式中;没有这种隔离时,同一个 HWND 树中的所有 HWND 必须处于相同的 DPI 缩放模式。
源码剖析三:初始布局与手动 DPI 缩放
窗口创建后,WM_CREATE触发DoInitialWindowSetup(DpiAwarenessContext.cpp#L241-L323),其中体现了按感知模式选择"基准 DPI"的规则:
- 先通过
GetThreadDpiAwarenessContext()+GetAwarenessFromDpiAwarenessContext()判断当前窗口属于哪种模式; DPI_AWARENESS_SYSTEM_AWARE时取GetDpiForSystem()(约 #L255);DPI_AWARENESS_PER_MONITOR_AWARE时取GetDpiForWindow(hWnd)(约 #L260);- 然后按
MulDiv(设计尺寸96, uDpi, 96)计算实际像素尺寸并SetWindowPos缩放整个窗口。注释还指出一个关键细节:Windows 不会在窗口创建时发送 DPI 变化通知,即使窗口创建在大于 96 DPI 的显示器上,因此必须自行做初始缩放。
窗口中创建了一块 STATIC 控件、复选/单选/对话框按钮,以及通过PlugInDll::CreateContentHwnd从插件 DLL 加载的"外部内容"HWND,再SetParent到本窗口下,统一交给UpdateAndDpiScaleChildWindows做布局。
当 DPI 真正变化时,WM_DPICHANGED由HandleDpiChange处理(DpiAwarenessContext.cpp#L326-L348):
- 从
HIWORD(wParam)取得新 DPI,从lParam取系统建议的新窗口缩放矩形,先SetWindowPos缩放顶层窗口; - 再调用
UpdateAndDpiScaleChildWindows重做所有子控件布局。
UpdateAndDpiScaleChildWindows(DpiAwarenessContext.cpp#L141-L239)是典型的手动缩放范例:所有尺寸常量都按 96 DPI 设计值定义(如DEFAULT_PADDING96、WINDOW_WIDTH96、WINDOW_HEIGHT96),运行时用MulDiv(value96, uDpi, 96)换算;字体则通过SystemParametersInfoForDpi(SPI_GETICONTITLELOGFONT, ..., uDpi)取得指定 DPI 下的图标标题字体,创建新HFONT后向所有子控件广播WM_SETFONT。窗口中的静态文本由UpdateDpiString更新,直接展示GetThreadDpiAwarenessContext()、GetAwarenessFromDpiAwarenessContext()与GetDpiForWindow(...)三个值,方便对照观察(DpiAwarenessContext.cpp#L100-L137)。
非客户区缩放:EnableNonClientDpiScaling
非客户区(标题栏、菜单栏、滚动条等)在 Windows 8.1 上不会自动做 DPI 缩放。从WndProc的WM_NCCREATE分支注释(DpiAwarenessContext.cpp#L389-L437)可以看到版本演进:
- Windows 10 1607起,可通过调用
EnableNonClientDpiScaling(hWnd)开启非客户区缩放(即 "1607" 按钮的场景); - Windows 10 1703起,当线程 DPI 上下文为
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2时,非客户区自动按监视器 DPI 缩放,无需显式调用。
示例通过CREATESTRUCT->lpCreateParams传回的bEnableNonClientDpiScaling标志,在WM_NCCREATE中按需调用EnableNonClientDpiScaling,同时把bChildWindowDpiIsolation以窗口属性PROP_DPIISOLATION记录下来,供后续插件内容加载时判断。
源码剖析四:子窗口 DPI 隔离与"插件"DLL 的缩放陷阱
"1803" 按钮演示了本示例最核心的能力:让一个来自外部 DLL 的子窗口,以与宿主不同的缩放模式运行。流程如下(参见 DpiAwarenessContext.cpp#L292-L323 与 DpiAwarenessContext.cpp#L350-L386):
- 检测到
PROP_DPIISOLATION标志后,先调用SetThreadDpiHostingBehavior(DPI_HOSTING_BEHAVIOR_MIXED); - 再把线程 DPI 上下文临时切到
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE; - 调用插件导出的
PlugInDll::CreateContentHwnd创建内容 HWND; - 恢复线程的 DPI 上下文与 hosting behavior,最后
SetParent把外部内容挂到宿主窗口下。
插件侧的实现见 PluginDLL.cpp:
- 导出接口声明在 PluginHeader.h 中,只有一个对外入口
CreateContentHwnd(HINSTANCE, int, int),返回一个挂在HWND_MESSAGE下、等待宿主重挂的 HWND 树; CreateContentHwnd(PluginDLL.cpp#L67-L156)用GetDpiForSystem()获取"系统 DPI"(即基于主显示器缩放因子的全局 DPI),并按ScaleToSystemDPI把 96 DPI 设计值换算成系统 DPI 下的像素值。注释明确警告:处于 Per-Monitor 感知的进程中不要依赖这种取值,它要么返回 96、要么返回系统 DPI,而不会返回窗口所在监视器的 DPI;- 插件还会读取自己的线程上下文(
GetThreadDpiAwarenessContext,PluginDLL.cpp#L104)并把它转成字符串显示在 STATIC 控件里,证明"这段内容的线程当时处于哪种上下文"; - 一个细节很值得注意:插件对其 STATIC 控件做了子类化(
SubclassProc,PluginDLL.cpp#L174-L199),只允许第一次WM_SETFONT,之后一律拦截。注释解释了原因:宿主在 DPI 变化时会给整棵树广播新字体,若插件文字随之变清晰,反而掩盖了要演示的问题——不感知 DPI 的子 HWND 会被 Windows 以位图拉伸方式放大,这正是外部内容常见的高 DPI 缺陷。
清单与可选的 GDI 缩放
客户端的清单文件 DpiAwarenessContext.exe.manifest 中声明了应用级 DPI 感知:
<application xmlns="urn:schemas-microsoft-com/asm.v3"> <windowsSettings> <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness> <!-- Un-comment the line below to enable GDI-scaling in this project. This will enable text --> <!-- to render crisply in DPI-unaware contexts --> <!--<gdiScaling xmlns="http://schemas.microsoft.com/SMI/2017/WindowsSettings">true</gdiScaling>--> </windowsSettings> </application>清单把进程默认上下文声明为PerMonitorV2(启动器窗口即以此模式运行,与 README 描述一致),同时保留了一个被注释掉的gdiScaling开关:启用后,应用在 DPI-unaware 上下文中创建的内容,其文字也能通过 GDI 缩放保持清晰。由于SetThreadDpiAwarenessContext允许运行时覆盖,清单中的默认值只影响"未显式设置上下文"的窗口——这正是逐窗口方案的基线。
系统对话框同样继承线程上下文
示例窗口内的 "Open a System Dialog" 按钮触发ShowFileOpenDialog(DpiAwarenessContext.cpp#L475-L485),调用GetOpenFileName弹出标准"打开"对话框。由于该线程在创建宿主窗口后已恢复上下文,系统对话框会按其自身线程的 DPI 感知上下文渲染,对照观察即可验证:不同感知模式下,连系统对话框的清晰度和非客户区表现也会不同。
小结:本示例可复用的工程经验
- 逐窗口切换感知模式的正确姿势是"创建前设上下文 → 创建窗口 → 立即恢复",且只影响顶层窗口,子窗口默认继承父窗口;
- 需要子 HWND 与宿主不同缩放模式时,用
SetThreadDpiHostingBehavior并成对保存/恢复,这是 Windows 10 1803+ 的能力; - Per-Monitor 感知的窗口必须自行处理布局:
WM_DPICHANGED中先用系统建议矩形缩放顶层窗口,再按MulDiv(设计值96, uDpi, 96)重排子控件、用SystemParametersInfoForDpi换字体; - 非客户区缩放在 v2 上下文(1703+)下自动生效,1607 上需显式
EnableNonClientDpiScaling; - 宿主进程内嵌入"不感知 DPI 的插件式外部 HWND"时,应预期其被位图拉伸,并以子窗口 DPI 隔离把这种不一致限制在可控范围内。
以上行为均可在 Samples/DPIAwarenessPerWindow/ 下的client/与PluginDLL/两个工程中逐行对照验证。
- 示例工程
【免费下载链接】Windows-classic-samples
This repo contains samples that demonstrate the API used in Windows classic desktop applications.
相关推荐
Windows UWP DPI 缩放实战:解读 Windows-universal-samples 的 DpiScaling 示例
Windows UWP DPI 缩放实战:解读 Windows universal samples 的 DpiScaling 示例 导读 本文基于 Window
示例工程易宏基因组:高效自动化宏基因组分析流程的10倍性能优化解决方案
易宏基因组:高效自动化宏基因组分析流程的10倍性能优化解决方案 在微生物组学研究中,宏基因组分析面临着数据处理复杂、分析流程碎片化、可视化结果不统一等核心挑战。
科研生物信息学流程编排Dify工作流实战:为什么你的Web开发效率低下?3步诊断与解决方案
Dify工作流实战:为什么你的Web开发效率低下?3步诊断与解决方案 还在为传统Web开发中繁琐的前后端联调而头疼吗?面对产品经理频繁的界面修改需求,你是否感到
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考