ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Qt高分屏适配实战:解决界面错乱与图标模糊

Qt高分屏适配实战:解决界面错乱与图标模糊 写这篇东西之前先交代一下背景。上周一个老客户把程序搬到新买的 4K 笔记本上跑打开主界面直接傻眼字小得跟蚂蚁一样按钮叠成一片图标的边缘全是锯齿。我说这不是个案凡是有点历史的 Qt Widgets 程序在没有专门做过高分辨率屏幕适配的情况下放到高分屏上基本都会出现界面错乱异常。这篇文章把我这几年在 Qt 高分辨率适配上的实践经验整理出来从原理到配置、从资源到绘制、再到问题排查一步一步说清楚。适合正在维护 Qt 桌面项目、又刚好被高分屏折腾过的朋友。1. 高分屏为什么会把Qt界面弄乱1.1 两个最常见的“翻车现场”我接触过的项目里高分屏适配出问题基本分两类。第一类是窗口整体不缩放。程序在 1080P 显示器上一切正常插上 4K 外接屏之后窗口缩在屏幕角落里面的文字、控件小到没法看。这个时候窗口标题栏看起来是没问题的但内容区完全不是预期大小整体比例严重失调。本质上是程序没有启用 Qt 的 HighDPI 缩放机制系统告诉 Qt“这个屏幕分辨率是 3840x2160”Qt 就老老实实按物理像素摆控件于是界面全部缩小。第二类是窗口放大了但里面乱了。比如窗口本来是 320x240 的登录框在 200% 缩放下显示成 640x480但里面的输入框、按钮、背景图还是原来那个比例甚至互相重叠、露白底。这种情况比第一种更让人头疼因为根因不止一处可能图片资源没有高分辨率版本可能布局写的是绝对坐标可能自定义绘制没处理像素比。拿现实生活打个比方。你把一张 A4 纸上的设计稿拿去找复印店只把整张纸放大一倍但纸上画的小图标、文字、表格线全都保持原尺寸印出来当然是一张错乱的废纸。Qt 的高分辨率适配本质上是让“纸面”和“纸上元素”一起等比放大缺了哪一环都会出问题。1.2 一句话理解DPI缩放与逻辑坐标想处理问题得先分清两个概念物理像素和逻辑像素。物理像素是屏幕真实发光点4K 屏幕就是 3840x2160 个点。逻辑像素是程序在坐标体系里使用的单位。Windows 系统设置里如果选了 150% 缩放那么 1 个逻辑像素就等于 1.5 个物理像素。程序如果不做任何适配默认以为 1 就对应 1在 150% 缩放的屏幕上就会把这些元素画在物理像素上看起来就是“小了一圈”。Qt 的AA_EnableHighDpiScaling这个开关就是把“程序认为的屏幕分辨率”从物理值切换到系统逻辑值。开启之后Qt 会自动读取屏幕缩放因子然后把所有坐标和尺寸乘以这个因子。Qt 6 里这个行为已经默认开启所以新项目通常不会踩这个坑但老项目尤其是 Qt 5.6 之前为基础的项目仍然需要手动处理。还有个容易被忽略的点这个属性设置时机非常关键。必须在创建QApplication之前设置进程一旦启动很多属性就被锁定之后再调是无效的。后面会专门说这个。2. 开工前的基线配置三处改动让Qt认识高分屏2.1 main函数里的关键设置不管接手多老的 Qt 项目我会先在main函数最前面把下面这段代码加上。#include QApplication #include QtCore/QtGlobal int main(int argc, char *argv[]) { #if QT_VERSION QT_VERSION_CHECK(6, 0, 0) QGuiApplication::setHighDpiScaleFactorRoundingPolicy( Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); #else QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QApplication::setHighDpiScaleFactorRoundingPolicy( Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); #endif QApplication app(argc, argv); // 后续业务代码... return app.exec(); }AA_EnableHighDpiScaling让 Qt 开启整体缩放AA_UseHighDpiPixmaps让 QIcon 和 QPixmap 在加载时考虑高分屏因素。这两个属性通常一起出现。Qt 6 里这两个行为已经默认内置所以代码里用宏做了区分。注意这里写的是QApplication::setAttribute必须放在QApplication app(argc, argv);之前。我曾经见过有人把它放在构造之后结果界面上半部分正常、下半部分错位排查了半天才发现是顺序问题。如果项目暂时不方便升级 Qt 版本也可以通过环境变量应急QT_ENABLE_HIGHDPI_SCALING1和QT_SCALE_FACTOR_ROUNDING_POLICYPassThrough。但这只能缓一口气长期维护还是建议走代码配置。2.2 缩放策略不是随便选的Qt 5.14 之后提供了一个容易被忽视的配置缩放因子舍入策略QGuiApplication::setHighDpiScaleFactorRoundingPolicy。这个策略决定了当系统缩放比例是 125%、150%、175% 这种非整数时Qt 到底怎么处理缩放因子。常用策略有这么几个PassThrough不取整150% 就是 1.5画面清晰度最好。Round四舍五入取整150% 变成 2控件更大但可能有轻微“过度放大”的感觉。RoundPreferFloor四舍五入但更偏向向下取整比如 1.25 会取 11.75 会取 2。Floor和Ceil无条件向下或向上取整。我给不同项目推荐过不同策略经验是这样的自研控件比较少、界面以标准 QWidget 组件为主的程序直接用PassThrough显示效果最细腻。如果项目接了第三方控件、或者内部大量使用基于整数的坐标计算PassThrough偶尔会在控件边缘出现 1 像素的缝隙或轻微错位这时候换成RoundPreferFloor反而更稳。不想改代码的话启动程序时加一个环境变量也能实现同样效果QT_SCALE_FACTOR_ROUNDING_POLICYPassThrough ./your_app调试时先用环境变量验证哪种策略最合适再把选好的策略写进代码这样来回试错成本最低。2.3 Windows下还要配置DPI感知声明在 Windows 上只设置 Qt 属性是不够的还得让系统知道“这个程序能自己处理 DPI”。Windows 默认会对不声明 DPI 感知的程序做位图拉伸如果你同时开了 Qt 的 HighDPI 缩放系统拉伸一次、Qt 再缩放一次界面直接糊出天际。正确的做法是给程序加上 PerMonitorV2 的 manifest 声明。下面这份是我常用的模板?xml version1.0 encodingUTF-8 standaloneyes? assembly xmlnsurn:schemas-microsoft-com:asm.v1 manifestVersion1.0 xmlns:asmv3urn:schemas-microsoft-com:asm.v3 asmv3:application asmv3:windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/pm/dpiAware dpiAwareness xmlnshttp://schemas.microsoft.com/SMI/2016/WindowsSettingsPerMonitorV2, PerMonitor/dpiAwareness /asmv3:windowsSettings /asmv3:application /assembly在 qmake 工程里把这个 manifest 通过资源文件打进去。先在.pro文件里加上RC_FILE app.rc然后新建app.rc内容如下IDI_ICON1 ICON DISCARDABLE app.ico 1 RT_MANIFEST app.manifest这里app.ico是程序图标app.manifest就是上面的 XML 文件。如果项目原本已经有 rc 文件或者 manifest不要直接覆盖把节点合并进去再编。3. 资源与布局的适配清晰度和不错乱一起抓3.1 图片资源位图、SVG与多倍数图的取舍窗口能正常缩放了紧接着冒出来的问题就是图标模糊。很多老项目用的是一套 16x16、24x24 的 PNG 图标在高分屏上被强行放大边缘全是马赛克。原因很简单位图的分辨率是固定的放大只能插值插值就是糊。解决这个问题有三条路我按投入产出比排序给你。第一条路图标换 SVG。Qt 对 SVG 的支持很成熟qmake 工程里加一行QT svg然后直接把 SVG 文件加载进 QIcon 就能用。SVG 是矢量图缩放到任意尺寸都清晰代价是消耗一点点 CPU。对于图标、Logo、简单图形这是最干净的方案。第二条路提供多倍分辨率位图。如果资源是从设计稿里切出来的位图可以让设计给你出 2x、3x 版本然后按约定命名QIcon icon; icon.addFile(:/icons/icon.png); icon.addFile(:/icons/icon2x.png); icon.addFile(:/icons/icon3x.png);QIcon 会按当前屏幕的 devicePixelRatio 自动选择最合适的一张。如果只有两张图至少要保证 2x 存在150% 和 200% 缩放下表现都能接受。第三条路用字体图标。把图标做成字体程序里用 QLabel 或者自定义控件显示对应字符天然矢量、颜色可调。适合那些图标数量多、风格统一的程序。缺点是需要引入第三方字体库初期改造工作量会大一些。实际操作中我建议优先处理高频使用的图标比如工具栏按钮、菜单图标、状态栏小图标。低频使用的装饰性图片可以往后放先用 SVG 补上关键位置项目稳定性提升会非常明显。3.2 字体与文本pt与px的选择题文字问题也是高分屏适配的高频雷区而且比图片更隐蔽。Qt 里设置字体有两种方式点大小point size和像素大小pixel size。点是一个物理单位1 点等于 1/72 英寸在不同 DPI 的屏幕上会自动换算成不同像素数。像素就简单直接就是屏幕上的物理像素个数。代码里用QFont::setPixelSize(12)设置的字体在 100% 缩放下显示 12 像素在 200% 缩放下还是 12 像素所以视觉上会变小。正确方式是改用QFont::setPointSize(9)或者setPointSizeF(9.5)这样 Qt 会自动根据当前屏幕 DPI 换算成合适的像素值。样式表里同样要注意很多人写font-size: 14px;这里px也是物理像素建议改成font-size: 14pt;这样写在高分屏上会自动放大。做整体改造的时候直接全局搜一下样式表里的px能改的尽量改成pt。改完之后别忘了检查一下行高和字间距有些样式表里line-height写死了字号放大后行间距反而不够文字会挤在一起。3.3 布局与固定尺寸的改造思路高分屏适配里最气人的问题是界面看着没放大多少但内部控件全乱了。这时候十有八九是代码里写了大量绝对坐标比如setGeometry(100, 100, 320, 240); // 输入框 usernameEdit-setGeometry(50, 60, 220, 30); // 密码框 passwordEdit-setGeometry(50, 100, 220, 30); // 按钮 loginBtn-setGeometry(200, 150, 80, 30);这种代码在 100% 缩放下是好的但窗口一旦从 320x240 变成 480x360控件的位置和大小还是按原来那套物理像素算的自然就叠在一起。解决思路就一句话让 Qt 的布局系统接管坐标。QVBoxLayout、QHBoxLayout、QGridLayout、QFormLayout这些布局器会按照控件的大小策略自动计算位置和尺寸窗口缩放时它们会跟着重新布局基本不需要你操心。改造方法是把窗口容器里每个控件塞进对应的布局器再把布局器 setLayout 到容器上外层再包一层 QWidget。原本写的 setGeometry 全部删掉。如果有些区域必须保持固定尺寸比如视频画面、游戏画面正确的做法是把这个区域放进一个单独的 QWidget给它设置setFixedSize或者setMinimumSize再用布局器把这个 QWidget 和周围控件关联起来。这样既保证了特殊区域不变形又不会影响整窗布局。4. 自定义绘制场景连像素比都要自己管4.1 devicePixelRatio的使用边界大多数人在高分屏适配时栽的最后一个跟头是自定义绘制。这个坑比图片、布局都深因为报错不明显表现就是“画面糊了但不知道糊在哪”。先解释一个概念devicePixelRatio缩写 DPR表示一个逻辑像素对应几个物理像素。在 200% 缩放的屏幕上 DPR 是 2。Qt 的普通控件绘制QPainter会自动处理 DPR 转换所以你重写paintEvent时画直线、画矩形在 2 倍屏幕上依然清晰这个不需要你手动处理。问题出在你自己创建QPixmap或QImage当画布的时候。很多人会像下面这样写void MyWidget::paintEvent(QPaintEvent *) { QPainter painter(this); QPixmap canvas(size()); // 这里用的是逻辑尺寸 canvas.fill(Qt::transparent); QPainter p(canvas); // 画各种内容... p.end(); painter.drawPixmap(0, 0, canvas); }这段代码在高分屏上会出现明显问题QPixmap canvas(size())创建的是一个逻辑尺寸的画布内部物理像素其实只有width个。画上去的内容再通过painter.drawPixmap贴到屏幕上时Qt 默认会把它放大到逻辑尺寸对应的物理尺寸于是内容就被拉糊了。正确写法是这样const qreal dpr devicePixelRatioF(); QPixmap canvas(size() * dpr); canvas.setDevicePixelRatio(dpr);setDevicePixelRatio是在告诉 Qt这张画布内部每个逻辑单位背后有 dpr 个物理像素。之后你用逻辑坐标往画布上画Qt 会原样把内容映射到足够多的物理像素上显示出来就是清晰的。4.2 一个自绘控件的完整适配写法拿最常见的自定义进度条举个例子。这个控件我们项目里几乎每个模块都有适配逻辑很典型。class ProgressBar : public QWidget { Q_OBJECT public: explicit ProgressBar(QWidget *parent nullptr) : QWidget(parent) {} void setProgress(int value) { m_value qBound(0, value, 100); update(); } protected: void paintEvent(QPaintEvent *) override { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); // 绘制背景 QRectF bgRect QRectF(rect()).adjusted(1, 1, -1, -1); painter.setPen(Qt::NoPen); painter.setBrush(QColor(#E8EAEC)); painter.drawRoundedRect(bgRect, height() / 2.0, height() / 2.0); // 绘制进度填充 qreal ratio m_value / 100.0; QRectF progressRect bgRect; progressRect.setWidth(bgRect.width() * ratio); painter.setBrush(QColor(#2D8CF0)); painter.drawRoundedRect(progressRect, height() / 2.0, height() / 2.0); // 绘制文本 painter.setPen(QColor(#333333)); QFont font painter.font(); font.setPointSize(9); painter.setFont(font); painter.drawText(rect(), Qt::AlignCenter, QString(%1%).arg(m_value)); } private: int m_value 0; };这段代码里的QPainter painter(this)拿到的坐标系已经是逻辑坐标系Qt 内部会按 DPR 把绘制结果写到屏幕上。所以画圆角矩形、画文字都不需要手动乘 DPR。关键点在于两个字体用setPointSize而不是setPixelSize所有坐标都基于rect()动态计算不要写死任何像素值。如果哪天需要把这个自绘控件的结果缓存成一张图再贴到别处那就按上一节说的创建画布时乘 DPR 并调用setDevicePixelRatio。4.3 截图与离屏渲染的高分屏细节还有一种很常见的需求把某个控件截图保存或者做离屏渲染。这里的高分屏细节也要注意。widget-grab()返回的 QPixmap 在高层分屏下会自动带上 DPR保存到文件时物理分辨率是正常的QPixmap pm widget-grab(); pm.save(snapshot.png);但如果你把这张图交给 OpenCV、图像算法库或者其他不认识 DPR 的库去处理就得注意图的width()可能不等于控件逻辑宽度而是逻辑宽度乘以 DPR。处理前先看一眼实际尺寸或者主动转成指定物理分辨率QImage image pm.toImage();toImage()转换后也是保留物理像素的不要下意识认为它就是逻辑尺寸。用QScreen::grabWindow截取整个屏幕或指定区域时坐标系统也容易混。Qt 文档里说这个接口使用设备无关坐标实际测试下来不同平台行为有差异。稳妥的办法是用窗口句柄拿到窗口的devicePixelRatio再手动换算坐标。我遇到最多的“黑边”和“截出来只有左上角一小块”问题基本都是因为传入的宽高在物理坐标和逻辑坐标之间混用。截图前先确认好你到底是要逻辑区域还是物理区域不要混着传。5. 问题排查与老项目改造从现象到方案的经验笔记5.1 高频问题速查表把这几年处理过的高分屏问题汇总成一张表方便你排查时对照现象常见原因处理方式整个窗口不放大内容小到看不清未启用高DPI缩放或启用顺序不对main中先配置属性再创建QApplication窗口放大了但图标模糊位图分辨率不足或没开AA_UseHighDpiPixmaps图标换SVG/多倍图检查属性配置字体不随界面放大样式表或代码里用了px像素字号改成pt点大小控件重叠、布局错乱用了绝对坐标setGeometry或写死尺寸改用布局器设置minimumSize替代fixedSize自绘画面糊、只画出部分内容创建QPixmap时没处理devicePixelRatio画布尺寸乘DPR并setDevicePixelRatio双屏拖动窗口时一边正常一边错乱DPI感知声明缺失或缩放策略不当补充PerMonitorV2 manifest调整舍入策略截图黑边或只有左上角截图坐标在物理/逻辑坐标间混用明确使用逻辑坐标用devicePixelRatio换算绝大多数情况下问题出在前三行。如果项目用了比较老的 Qt 版本比如 5.6 之前我建议至少升级到 5.15 LTS 或者 6.5 LTS。老版本的高DPI接口不成体系靠环境变量只能救急长期维护成本太高。5.2 老项目改造的性价比路线老项目改造最怕“想一口吃成胖子”。我的建议是按下面这个优先级推进每一步改完都值得发布一个版本第一步改 main 函数把高DPI属性、舍入策略配上外加补全 manifest。这一步通常能解决 60% 的界面错乱问题而且风险极小。第二步扫一遍样式表和代码里的字体设置把所有px字号改成pt。这一步解决的是“字体不放大”的问题改动面不大收益却很直观。第三步把高频图标换成 SVG 或 2x 位图。这一步解决“图标模糊”视觉观感立刻上一个档次。第四步逐个处理自定义绘制控件。哪个控件被投诉得最多先改哪个不要试图一次全部重写。第五步慢慢消灭 setGeometry。这个工作量最大可以和业务需求合并着做每碰一个窗口就顺手把布局器替换上去。整个改造过程中建议单独拉一个分支每完成一个模块就在高分辨率屏和普通屏上分别截图对比确认没有回归再合并。截图对比是最好的验证方式比肉眼盯着屏幕靠谱得多。5.3 调试心得与环境变量用法调试高分屏适配不能每次全靠插显示屏。有些环境变量可以帮你快速模拟各种缩放比例强烈建议记下来。通用强制缩放比例QT_SCALE_FACTOR1.25 ./your_app QT_SCALE_FACTOR1.5 ./your_app QT_SCALE_FACTOR2 ./your_app设置缩放策略QT_SCALE_FACTOR_ROUNDING_POLICYPassThrough ./your_app QT_SCALE_FACTOR_ROUNDING_POLICYRound ./your_app多屏场景可以按屏幕单独指定QT_SCREEN_SCALE_FACTORSDP-11.25;DP-22 ./your_app程序里也可以打印关键信息方便在异常环境中快速定位QScreen *screen qApp-primaryScreen(); qDebug() devicePixelRatio: screen-devicePixelRatio() logicalDotsPerInch: screen-logicalDotsPerInch() size: screen-size();实际调试的时候我习惯先把QT_SCALE_FACTOR设成 1.5在同一个窗口里来回切换同时观察三个地方字体是否锐利、图标是否清晰、布局是否错位。第一轮问题处理完再把环境变量去掉放到真实屏幕上回归一轮。还有一点关于双屏场景的提醒如果程序在副屏上表现正常、主屏上错乱或者反过来多半是 DPI 感知声明没生效去查 manifest 和系统缩放设置如果两个屏幕来回拖都不对那基本可以确定是代码里的坐标计算没有遵循“逻辑坐标优先”的原则。这几年处理了不下十个高分屏适配项目我的体会有三点。第一高分屏适配不是一步到位的功能而是开发习惯的问题新代码里写任何坐标和尺寸时都先想想在 1.5 倍缩放下是否还成立。第二遇到模糊先别急着换高清图先把 main 函数和高DPI策略检查一遍顺序错了做什么都白搭。第三多用布局器和矢量资源少写绝对坐标和位图这不是审美问题是维护成本问题。最后分享一个小技巧。发布之前把程序放在一台 125% 缩放的 Windows 和一台 2 倍缩放的大屏 Linux 上各跑一遍90% 的用户现场问题都能提前暴露。别问我是怎么知道的都是踩坑踩出来的经验。
返回列表