news 2026/10/21 22:17:42

Windows-classic-samples 逐窗口 DPI 感知示例:Win32 高 DPI 缩放的四种模式与 SetThreadDpiAwarenessContext 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Windows-classic-samples 逐窗口 DPI 感知示例:Win32 高 DPI 缩放的四种模式与 SetThreadDpiAwarenessContext 实战
  • 示例工程

【免费下载链接】Windows-classic-samples

This repo contains samples that demonstrate the API used in Windows classic desktop applications.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-classic-samples
点击查看免费下载

本文基于 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 部分组成:

  1. 启动器窗口(Launcher):一个对话框,提供若干按钮,每个按钮对应一种 DPI 感知模式,点击后可按该模式弹出一个新的示例窗口;
  2. 示例窗口(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 开发,但通常可在后续版本中无需修改直接编译运行。

构建示例

  1. 启动 Visual Studio,选择File>Open>Project/Solution;
  2. 进入示例所在目录,双击 Microsoft Visual Studio Solution 文件(DpiAwarenessContext.sln);
  3. 按F5,或使用Build>Build Solution。

该解决方案包含两个工程:客户端主程序与插件 DLL,见 DpiAwarenessContext.sln 与 PluginDLL.vcxproj。

运行并观察不同模式的差异

按 README 的"Run the sample"部分,运行后的预期行为为:

  1. 应用初始化时,显示一个Per-Monitor DPI aware的顶层窗口(即启动器),其中显示该窗口的 DPI 感知模式和窗口 DPI 值;
  2. 将该顶层窗口移动到 DPI 值不同的显示器上时,窗口会被重新缩放、非客户区随之缩放,窗口中显示的 DPI 数值也会更新;
  3. 点击任一按钮后,会以该按钮描述的感知模式弹出一个新的顶层窗口。窗口内可见:DPI 相关信息、一个"以相同上下文打开系统对话框"的按钮,以及一段来自另一个 DLL、按 System-DPI 感知渲染的子窗口内容(模拟插件)。

在单显示器设备上,可在应用运行期间修改显示设置中的"更改文本、应用等项目的大小"(缩放因子),同样能观察到不同模式的缩放差异。

源码剖析一:启动器如何为每个窗口选择 DPI 上下文

启动器是一个模态对话框,其窗口过程HostDialogProc根据按钮 ID 选择对应的DPI_AWARENESS_CONTEXT,再调用CreateSampleWindow。从 DpiAwarenessContext.cpp 中的按钮分支可以看到完整的映射关系:

启动器按钮资源 ID传入的 DPI 上下文附加行为
UnawareIDC_BUTTON_UNAWAREDPI_AWARENESS_CONTEXT_UNAWARE无
System awareIDC_BUTTON_SYSTEMDPI_AWARENESS_CONTEXT_SYSTEM_AWARE无
8.1 (Per-Monitor v1)IDC_BUTTON_81DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE无
1607 (Per-Monitor v1 + 非客户区缩放)IDC_BUTTON_1607DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE置位bEnableNonClientDpiScaling
1703 (Per-Monitor v2)IDC_BUTTON_1703DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2无
1803 (v2 + 子窗口 DPI 隔离)IDC_BUTTON_1803DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2置位bChildWindowDpiIsolation

bEnableNonClientDpiScaling与bChildWindowDpiIsolation两个标志通过CreateParams结构体随CreateWindowExW传入,在WM_NCCREATE阶段生效,这正是"同一进程内逐窗口选择不同感知模式"的核心实现。

源码剖析二:CreateSampleWindow 中的上下文切换

CreateSampleWindow(DpiAwarenessContext.cpp#L350-L386)演示了逐窗口切换 DPI 感知的标准写法:

  1. 创建前:调用SetThreadDpiAwarenessContext(context),把目标上下文设为当前线程的 DPI 感知上下文,并保存返回值(之前的上下文);
  2. 以 96 DPI 的未缩放尺寸调用CreateWindowExW创建窗口(窗口类名为SetThreadDpiAwarenessContextSample),随后按实际 DPI 重新缩放(见下文DoInitialWindowSetup);
  3. 创建后立即恢复:SetThreadDpiAwarenessContext(previousDpiContext)把线程上下文还原,避免影响同线程后续创建的窗口;
  4. 若启用子窗口 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):

  1. 从HIWORD(wParam)取得新 DPI,从lParam取系统建议的新窗口缩放矩形,先SetWindowPos缩放顶层窗口;
  2. 再调用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):

  1. 检测到PROP_DPIISOLATION标志后,先调用SetThreadDpiHostingBehavior(DPI_HOSTING_BEHAVIOR_MIXED);
  2. 再把线程 DPI 上下文临时切到DPI_AWARENESS_CONTEXT_SYSTEM_AWARE;
  3. 调用插件导出的PlugInDll::CreateContentHwnd创建内容 HWND;
  4. 恢复线程的 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 感知上下文渲染,对照观察即可验证:不同感知模式下,连系统对话框的清晰度和非客户区表现也会不同。

小结:本示例可复用的工程经验

  1. 逐窗口切换感知模式的正确姿势是"创建前设上下文 → 创建窗口 → 立即恢复",且只影响顶层窗口,子窗口默认继承父窗口;
  2. 需要子 HWND 与宿主不同缩放模式时,用SetThreadDpiHostingBehavior并成对保存/恢复,这是 Windows 10 1803+ 的能力;
  3. Per-Monitor 感知的窗口必须自行处理布局:WM_DPICHANGED中先用系统建议矩形缩放顶层窗口,再按MulDiv(设计值96, uDpi, 96)重排子控件、用SystemParametersInfoForDpi换字体;
  4. 非客户区缩放在 v2 上下文(1703+)下自动生效,1607 上需显式EnableNonClientDpiScaling;
  5. 宿主进程内嵌入"不感知 DPI 的插件式外部 HWND"时,应预期其被位图拉伸,并以子窗口 DPI 隔离把这种不一致限制在可控范围内。

以上行为均可在 Samples/DPIAwarenessPerWindow/ 下的client/与PluginDLL/两个工程中逐行对照验证。

  • 示例工程

【免费下载链接】Windows-classic-samples

This repo contains samples that demonstrate the API used in Windows classic desktop applications.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-classic-samples
点击查看免费下载

相关推荐

上一篇:Electric Agents 的 composer_input 约定:从解析式输入、Slash 命令到 ProseMirror 编辑器的完整实现指南
下一篇:WPF UI FontIcon 控件完全指南:基于字体的图标渲染、核心属性与实战用法

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 9:17:38

千亿赛道洗牌!数据标注产业换挡,垂类标注迎来红利期

从“人工点点点”到千亿级产业&#xff0c;从数据标注到高质量数据集建设&#xff0c;数据产业正在经历一场深刻的换挡升级。“算力决定下限&#xff0c;数据决定上限”——这已成为AI行业的核心共识。中国信通院人工智能研究所樊威博士在近日的“极致人才”栏目中提到&#xf…

作者头像 李华
网站建设 2026/10/21 22:17:27

QuPath OpenSlide 扩展:本地库来源、依赖版本清单与打包实现解析

计算机视觉图像处理桌面应用机器学习医疗健康 【免费下载链接】qupath QuPath - Open-source bioimage analysis for research 项目地址&#xff1a; https://gitcode.com/gh_mirrors/qu/qupath 点击查看 免费下载 本文基于 QuPath 仓库中 OpenSlide 扩展的许可证来源说明文档…

作者头像 李华
网站建设 2026/10/21 22:17:28

两串铁锂电池主动均衡设计:MP2642GR-Z与PIC18F4525软硬协同

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 9:01:04

Apollo-11制导计算机:4KB内存下的实时调度与容错设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 19:51:28

2026年薪酬设计实用指南:好机构这样选,不踩坑

在2026年的企业经营环境中&#xff0c;薪酬设计早已不是“年度调薪表”那么简单。当人才流动加速、行业薪酬带宽持续拉宽&#xff0c;一份数据精准、时效性强、能落地执行的薪酬报告&#xff0c;已经成为企业决策的基础设施。但现实是&#xff0c;很多HR花了大价钱买回的报告&a…

作者头像 李华