问题
使用 wxPython 编写的程序在某些高 DPI 的电脑(通常是笔记本)上显示出来的字体会非常模糊:
事实上 wxPython 是支持高 DPI 的,但是由于我们的程序没有显式指明支持高 DPI,因此系统默认不支持高 DPI,就直接强行缩放画面,导致字体模糊。
系统方法
我们可以在系统的兼容性设置里强制更改某个程序的 DPI 缩放设置。
这里面有三个选项:
- 应用程序:不强制缩放,交由应用程序处理。
- 系统:默认使用的就是这种。前面说过,程序不支持高 DPI 的情况下就会强制缩放。
- 系统(增强):针对 GDI 程序特别优化的缩放,“系统”是位图缩放,相当于缩放照片;“系统(增强)”是矢量缩放,不会变模糊。
应用程序 | |
---|---|
系统 | |
系统(增强) |
可以看到第三种效果最好。
(图片压缩,图上可能看不太出来)
程序方法
根据微软官方的文档,声明程序支持高 DPI 是通过设置清单文件来实现的。因此对于 Python 程序来说,高 DPI 设置只有打包后才会生效。
这里我用的是 Pyinstaller,根据文档,在打包命令里增加一个选项:
--manifest <清单文件路径>
或者如果你用 .spec 文件来打包的话,在 EXE 段里增加一个 manifest
:
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
# ...
)
pyz = PYZ(a.pure)
exe = EXE(
# ...
manifest='manifest.xml', # 清单文件
)
然后新建 manifest.xml
文件,粘贴以下内容进去:
<!-- 兼容性:系统 -->
<?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">
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" />
</dependentAssembly>
</dependency>
<!-- <asmv2:trustInfo xmlns:asmv2="urn:schemas-microsoft-com:asm.v2">
<asmv2:security>
<asmv2:requestedPrivileges>
<asmv2:requestedExecutionLevel level="asInvoker" uiAccess="false" />
</asmv2:requestedPrivileges>
</asmv2:security>
</asmv2:trustInfo> -->
<asmv3:application>
<asmv3:windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, system</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>
这个清单文件是从 wxWidgets 项目里复制出来的(见参考 7)。被注释掉的那一段是由于 Pyinstaller 会自动添加相同的代码,导致配置重复,从而无法启动,因此注释掉避免重复。
加上这段代码后就可以达到上面的“系统”效果。
如果想要用“系统(增强)”效果,还需要加上 gdiScaling
选项。
<!-- 兼容性:系统(增强) -->
<?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">
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" />
</dependentAssembly>
</dependency>
<!-- <asmv2:trustInfo xmlns:asmv2="urn:schemas-microsoft-com:asm.v2">
<asmv2:security>
<asmv2:requestedPrivileges>
<asmv2:requestedExecutionLevel level="asInvoker" uiAccess="false" />
</asmv2:requestedPrivileges>
</asmv2:security>
</asmv2:trustInfo> -->
<asmv3:application>
<asmv3:windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, system</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<!-- 在这里 -->
<gdiScaling xmlns="http://schemas.microsoft.com/SMI/2017/WindowsSettings">true</gdiScaling>
</asmv3:windowsSettings>
</asmv3:application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>
代码修改
前面说过 wxPython 框架支持高 DPI,但是这并不意味着你用 wxPython 写出来的程序也支持高 DPI。
为了适配高 DPI,另外还需要做的一件事就是移除代码里所有硬编码的坐标信息,改成逻辑坐标。
具体来说,是用 FromDIP
方法在运行时将逻辑坐标转换为物理坐标。
比如初始化窗口时设置的窗口大小,
self.SetSize(600, 400)
改成
self.SetSize(self.FromDIP(600), self.FromDIP(400))
这个逻辑坐标实际上是 DPI=96(Windows 下缩放=100%)时的坐标,因此修改代码的时候需要注意一下。如果你不是在缩放=100%的情况下设计的窗体,那么转换后可能大小、位置会发生变化。
参考
- What’s the difference between Windows 10’s application scaling settings? - SuperUser
- Using Pyinstaller - pyinstaller 6.0.0 documentation
- Application manifests - Win32 apps | Microsoft Learn
- Improving the high-DPI experience in GDI based Desktop Apps - Windows Developer Blog
- wxWidgets: High DPI Support in wxWidgets
- wx.Window — wxPython Phoenix 4.2.2 documentation
- wxWidgets/include/wx/msw/wx_dpi_aware_pmv2.manifest at master · wxWidgets/wxWidgets