Qt DPI相关逻辑
今天给大家分享下Qt高分屏相关知识,依然从源码开始,本文源码版本qt5.14.16
static qreal qt_effective_device_pixel_ratio(QWindow *window = nullptr)
{if (!qApp->testAttribute(Qt::AA_UseHighDpiPixmaps))return qreal(1.0);if (window)return window->devicePixelRatio();return qApp->devicePixelRatio(); // Don't know which window to target.
}
上面代码是qicon类中,计算缩放比下尺寸的函数,可以清晰看到最开始判断条件是一个flag值,Qt::AA_UseHighDpiPixmaps,如果未开启,就直接返回1.0
原因可能有几个:
-
Qt 没开启 High-DPI 支持
Qt 默认是逻辑 DPI = 96,如果你没开启 DPI scaling,那么qt_effective_device_pixel_ratio(window)
就始终返回 1。需要在
main()
里启用:QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
我来解释下上面三行分别有什么用:
AA_EnableHighDpiScaling让 Qt 根据系统 DPI 自动对窗口和 UI 元素做缩放(逻辑坐标 → 物理坐标),相当于 Qt 内部帮你把 150% 转换成 1.5 的缩放因子。
AA_UseHighDpiPixmaps让 Qt 根据 devicePixelRatio 自动选择/生成合适的 pixmap(比如自动加载 @2x 图片,或者内部存两份 pixmap)。
(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough) 则是选择让缩放比支持小数而不是round取整,例如1.5的缩放比不会返回为2 -
传入的
QWindow* window
没有关联物理屏幕
如果window == nullptr
,或者它还没show()
,Qt 没法取到屏幕的devicePixelRatio
,也会返回默认的 1。qDebug() << window->devicePixelRatio(); qDebug() << window->screen()->devicePixelRatio(); qDebug() << QGuiApplication::primaryScreen()->devicePixelRatio();
如果这些都是
1
,说明 Qt 根本没感知到你的缩放。 -
Windows 下 DPI 感知等级
- 如果 exe 的 manifest 里没有启用 DPI awareness(Per Monitor V2 / Per Monitor),Windows 会自动给你的程序做 DPI 虚拟化,Qt 拿到的 DPI 就是 96 → 比例 = 1。
- 在 Qt 5.6 以后,需要确保 manifest 或者
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling)
打开,否则系统缩放不会反映到devicePixelRatio
。(Qt 的 windeployqt 默认加了)
-
Qt::AA_EnableHighDpiScaling
让 Qt 根据系统 DPI 自动对窗口和 UI 元素做缩放(逻辑坐标 → 物理坐标),相当于 Qt 内部帮你把150%
转换成1.5
的缩放因子。 -
Qt::AA_UseHighDpiPixmaps
让 Qt 根据devicePixelRatio
自动选择/生成合适的 pixmap(比如自动加载@2x
图片,或者内部存两份 pixmap)。
👉 如果没启用,它们都走“老 DPI 模式”:逻辑像素 = 物理像素,devicePixelRatio
就始终是 1
。
-
如果你的应用只关心“逻辑 DPI 转换”(比如自己算 px = dpi/96):
👉 不要启用AA_EnableHighDpiScaling
,否则会有重复计算风险。 -
如果你希望 Qt 自动帮你缩放 UI 和图标(多数桌面应用):
👉 启用AA_EnableHighDpiScaling
+AA_UseHighDpiPixmaps
,然后 不要再自己做 dpi/96 的换算,只用QWindow::devicePixelRatio()
。 -
Qt 5.14+ 的推荐写法:
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
这样 Qt 就不会把 1.5 round 到 2.0,而是保留 1.5。
✅ 一句话总结:
- 要么完全交给 Qt(开属性,不做 dpi/96 计算);
- 要么完全自己算(关属性,手动做 dpi/96);
- 混用就会出现“1.5 → 2.0 → 再除 96 → 翻倍”的情况。
Qt 里 devicePixelRatio
之所以从 1.5 → 2.0,是因为 Qt 默认用了 取整策略(Round to nearest integer)。
从 Qt 5.14 开始,Qt 提供了一个接口,可以控制缩放因子的取整策略:
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
可选策略
-
Qt::HighDpiScaleFactorRoundingPolicy::Round
(默认)
四舍五入,1.5 → 2.0 -
Qt::HighDpiScaleFactorRoundingPolicy::Ceil
向上取整,1.5 → 2.0 -
Qt::HighDpiScaleFactorRoundingPolicy::Floor
向下取整,1.5 → 1.0 -
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough
✅
保留原值,不取整,1.5 → 1.5
用法示例
在 main()
里,QApplication
/QGuiApplication
构造之前调用:
int main(int argc, char *argv[])
{QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);QApplication app(argc, argv);...return app.exec();
}
这样在 150% 缩放的屏幕上,QWindow::devicePixelRatio()
、QPixmap::devicePixelRatio()
就会返回 1.5
,而不是 2.0
。
qreal QScreen::logicalDotsPerInch() const
{Q_D(const QScreen);QDpi dpi = QHighDpiScaling::isActive() ? QHighDpiScaling::logicalDpi(this) : d->logicalDpi;return (dpi.first + dpi.second) * qreal(0.5);
}
这里开启了上述说的的attribute后,返回值变成了96,而不是缩放比的1.5后的144
这个点是 Qt HighDPI 的两个不同层次:
为什么启用 AA_EnableHighDpiScaling
后变成了 96
启用 HighDPI scaling 以后,Qt 会把 系统缩放(1.5, 2.0 …)折算进了“scale factor”,
而把 逻辑 DPI 重新归一化为 96
(也就是 “Qt 坐标空间” 总是 96 DPI)。
这样做的原因是:
- 保证应用在不同缩放下使用的 逻辑坐标系统是一致的。
- 让开发者不需要关心 DPI,统一用 96 作为“逻辑 DPI”,缩放全部交给
devicePixelRatio
。
举例:150% 缩放的显示器
-
没开 HighDPI 属性时
QScreen::logicalDotsPerInch()
= 144 (96 × 1.5)QWindow::devicePixelRatio()
= 1.0
👉 你必须自己做
dpi / 96.0
的换算。 -
开了 HighDPI 属性时
QScreen::logicalDotsPerInch()
= 96QWindow::devicePixelRatio()
= 1.5
👉 逻辑 DPI 固定 96,缩放信息放到
devicePixelRatio
里。
关键点
- 逻辑 DPI (
logicalDotsPerInch
):给 UI 计算字体/布局用,Qt 强制规范成 96,避免应用乱掉。 - devicePixelRatio:才是真正反映缩放比例的参数。
✅ 总结:
看到 96
并不是 Qt 算错,而是因为开启了 HighDPI scaling,Qt 把缩放从 DPI 拆出去放到了 devicePixelRatio()
。
所以在这个模式下,不要再依赖 logicalDotsPerInch()
来算缩放,应该用:
qreal scale = window->devicePixelRatio(); // 或 screen->devicePixelRatio()
High-DPI 在 “启用 / 未启用” 两种模式下的行为尽可能全面地整理成:核心结论 → 对照表 → 公式 / 代码检测片段 → 实务建议 / 检查清单
先给核心结论:
- 启用 Qt 的 High-DPI(
Qt::AA_EnableHighDpiScaling
/QT_ENABLE_HIGHDPI_SCALING
)后,Qt 会把 “缩放” 的信息放到devicePixelRatio()
,并把逻辑 DPI 归一到 96(方便统一逻辑坐标系); 不要同时再用dpi/96
做二次缩放。 (codebrowser.dev)- 非整数缩放(例如 1.5)可能被 Qt 按策略取整(默认 Qt5 是 Round)——可以通过
QGuiApplication::setHighDpiScaleFactorRoundingPolicy()
改为 PassThrough 保留 1.5。 (doc.qt.io)- High-DPI 的最终可见效果还受 环境变量(
QT_SCALE_FACTOR
/QT_AUTO_SCREEN_SCALE_FACTOR
/QT_SCREEN_SCALE_FACTORS
) 和 Windows 的进程 DPI awareness(manifest / Per-Monitor v2) 影响。 (doc.qt.io)
对照表(便于快速查找)
概念 / API | High-DPI 关闭(默认老行为) | High-DPI 启用(AA_EnableHighDpiScaling / Qt 自动缩放) | 说明 / 取值与使用建议 |
---|---|---|---|
QScreen::logicalDotsPerInch() | 返回屏幕实际 DPI(例如 144,96×1.5) | 返回 96(逻辑 DPI 统一归一),缩放转移到 devicePixelRatio() | 启用后不要用 logicalDotsPerInch()/96 来算缩放;改用 devicePixelRatio() 。 (codebrowser.dev) |
QScreen::physicalDotsPerInch() | 代表显示器物理 DPI(与系统设置相关) | 仍能从底层平台查询到原始物理 DPI(平台实现差异) | 若要底层实际 DPI,可查询 platform 接口或 QPlatformScreen 。 |
QWindow::devicePixelRatio() / qt_effective_device_pixel_ratio(window) | 通常 1.0 (Qt 未启用高 DPI) | 返回屏幕缩放因子(1.5 / 2.0 / …),受 rounding policy 影响 | 若启用 High-DPI,以它为准 来换算像素。 (doc.qt.io) |
QPixmap::devicePixelRatio() | 多数情况为 1 ,除非你手动设置或加载 @2x 资源 | pixmap 持有自己的 DPR,用于绘制到高 DPR 屏幕(Qt 会为你选或扩展) | 使用 AA_UseHighDpiPixmaps 可自动管理 pixmap。 |
QIcon::pixmap() | 需要你按 DPI 手动选择资源或缩放 | Qt 会基于 DPR 给合适尺寸/分辨率的 pixmap(如果 Qt 能识别和管理) | 别既启用 Qt scaling 又在外面再做 dpi/96 的换算,否则重复缩放。 |
QScreen::logicalDotsPerInchX/Y() | = 实际屏幕 DPI | = 96(统一逻辑 DPI) | 同上。 |
环境变量 | QT_SCALE_FACTOR / QT_AUTO_SCREEN_SCALE_FACTOR 可影响(在 Qt < 5.6/5.14 行为差异) | 同上;且 QT_SCALE_FACTOR 会与原生 DPR 相乘,得到最终 DPR | 推荐用环境变量做临时测试或覆盖,生产把 DPI awareness 放到 manifest/代码里。 (doc.qt.io) |
Rounding 策略 | 无(因为通常 DPR=1) | Qt 默认(Qt5)将非整数四舍五入(Round);可改为 PassThrough 保留 1.5 | 使用 QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); 保留精确值。 (doc.qt.io) |
关键公式(如何换算 pixel / logical)
(对照两种模式给出公式,方便直接套用)
-
High-DPI 未启用(你自己处理 DPI)
scale = screen->logicalDotsPerInch() / 96.0
(例如 144/96 = 1.5)physical_pixels = logical_pixels * scale
-
High-DPI 启用(交给 Qt)
scale = window->devicePixelRatio()
或screen->devicePixelRatio()
(例如 1.5)physical_pixels = logical_pixels * scale
- 注意:
QScreen::logicalDotsPerInch()
在这种模式通常返回 96,不要再用logicalDotsPerInch()/96
做重复换算。 (codebrowser.dev)
常见坑
- 同时开启
AA_EnableHighDpiScaling
又手动用dpi/96
:会造成“重复缩放”。 - 双屏(100% + 150%)且 Qt 默认 Round 策略:你会看到某屏返回
devicePixelRatio() = 2.0
(1.5 被 round 到 2),外观比系统其它窗口更“大”。可以改为PassThrough
。 (doc.qt.io) - Windows 下如果没有把进程设为 Per-Monitor DPI aware(manifest),Windows 可能会对程序做 DPI 虚拟化,导致 Qt 读到的仍是 96(即 DPR = 1)。务必检查 manifest / dpiawareness。 (微软学习)
推荐的 main()
启用方式(如果你想让 Qt 负责缩放并保留 1.5)
int main(int argc, char **argv)
{// 必须在创建 Q(Core)Application 之前QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);// 保留非整数缩放(不要 round 到 2.0)QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);QApplication app(argc, argv);...return app.exec();
}
说明:把 rounding 政策放在 application 构造前设置。 (doc.qt.io)
诊断小段:打印各项值(把它放到某个窗口 show()
后调用)
把下面拷到程序合适位置(窗口已 show()
)可以看清楚当前各屏和窗口的实际值:
#include <QDebug>void dumpDpiInfo(QWindow* w = nullptr)
{qDebug() << "QT envs: QT_SCALE_FACTOR=" << qEnvironmentVariable("QT_SCALE_FACTOR")<< " QT_AUTO_SCREEN_SCALE_FACTOR=" << qEnvironmentVariable("QT_AUTO_SCREEN_SCALE_FACTOR");QList<QScreen*> screens = QGuiApplication::screens();for (QScreen* s : screens) {qDebug() << "Screen" << s->name()<< "logicalDPI:" << s->logicalDotsPerInch()<< "physicalDPI:" << s->physicalDotsPerInch()<< "devicePixelRatio:" << s->devicePixelRatio()<< "geometry:" << s->geometry();}if (w) {qDebug() << "Window devicePixelRatio():" << w->devicePixelRatio()<< "qt_effective_device_pixel_ratio:" << qt_effective_device_pixel_ratio(w);}
}
这样可以直接看到:logicalDotsPerInch()
、devicePixelRatio()
、以及环境变量对最终 DPR 的影响。
实务建议(如果在做跨屏/跨平台的生产级 UI)
- 二选其一: 要么让 Qt 完全负责(启用
AA_EnableHighDpiScaling
+AA_UseHighDpiPixmaps
,不要再做 dpi/96),要么完全关掉(自己用logicalDotsPerInch()/96
做缩放)。混用会出错。 (doc.qt.io) - 在双屏或非整数缩放场景下,推荐把 rounding policy 设为
PassThrough
(保留 1.5)以避免四舍五入导致的视觉差异。 (doc.qt.io) - 在 Windows 上把 DPI awareness 放在 manifest(Per-Monitor V2)或使用
qt.conf
的dpiawareness
参数进行设置,避免被 Windows 自动虚拟化。 (微软学习) - 使用
QT_SCALE_FACTOR
/QT_AUTO_SCREEN_SCALE_FACTOR
做临时测试,但生产中以 manifest +AA_
为主。 (doc.qt.io)
快速检查清单(遇到 DPR=1 或逻辑 DPI=96 但期望看到 144 时按这个排查)
main()
是否在QApplication
前设置了AA_EnableHighDpiScaling
?(若没设置,Qt 可能不启用 high-dpi)QWindow*
是否有效且已经show()
?(若未 show,屏幕信息可能不可用)- 是否设置了 rounding policy 导致 1.5 被 round 到 2?(
PassThrough
可保留 1.5) - Windows 下 exe 的 manifest 是否设置了合适的 DPI awareness?(Per-Monitor v2 推荐)
- 有没有同时自己做
dpi/96
的手动换算?(若 Qt 已处理,就不要再做)
启用高 DPI 缩放后,图标变模糊,基本上就是 资源不足(低分辨率图标被放大) 或者 缩放策略不理想 导致的。Qt 和 Windows 都提供了几类解决方法,我整理一下常见做法和适用场景。
1. 提供高分屏资源(首选方案)
这是最根本、最推荐的方案。
-
QIcon 多分辨率加载
- Qt 支持
QIcon::addFile(":/icons/foo.png", QSize(16,16))
加入不同尺寸的资源。 - Qt 会在渲染时根据
devicePixelRatio
选取最合适的那张。 - 推荐提供:16×16, 24×24, 32×32, 48×48, 64×64, 128×128。
- Qt 支持
-
@2x 命名规则(类似 iOS Retina)
- Qt 会自动识别
icon.png
和icon@2x.png
,在devicePixelRatio=2.0
的屏幕上优先使用 @2x 图。 - Qt 5.6+ 支持,Qt 5.14+ 配合
AA_UseHighDpiPixmaps
更完善。
- Qt 会自动识别
2. 启用高 DPI pixmap 管理
-
在
main()
里加:QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
这样
QIcon::pixmap()
会考虑devicePixelRatio
,自动选用合适的分辨率。
3. 控制缩放算法(避免模糊)
Qt 默认的缩放算法是 平滑插值,对小图标放大会变糊。可以通过 Qt::TransformationMode
调整:
- 平滑缩放(默认,
Qt::SmoothTransformation
):抗锯齿,但容易糊。 - 快速缩放(
Qt::FastTransformation
):更锐利,可能有锯齿,但在小图标上反而更清晰。
示例:
QPixmap pix = icon.pixmap(size * devicePixelRatio, mode, state);
pix.setDevicePixelRatio(devicePixelRatio);
QPixmap sharp = pix.scaled(size, Qt::KeepAspectRatio, Qt::FastTransformation);
4. 使用矢量图标(SVG/Icon Fonts)
避免位图放大的根本办法:
- QSvgRenderer + QIcon:直接加载
.svg
资源,缩放时无损。 - Icon Font(FontAwesome, Material Icons):随 DPI 缩放,无模糊。
Qt 提供 QIcon::fromTheme()
也可以加载系统矢量图标。
5. Windows / 系统级别设置
-
在 Windows manifest 声明 Per-Monitor DPI Aware v2,否则系统可能对应用做位图缩放 → 模糊。
-
在 Qt 里确认启用了
AA_EnableHighDpiScaling
,并设置QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
避免 1.5 → 2.0 的取整放大导致图标更糊。
6. 程序内的补救算法
如果现有资源有限,可以在程序里做一些图标渲染优化:
- 在小图标上用
FastTransformation
,在大图上用SmoothTransformation
。 - 对缩放后的 QPixmap 做一次锐化滤镜(Sobel/Laplacian 卷积),但这只是补救。
- 部分项目会用 MipMap 思路:事先生成多级别分辨率的图标,运行时按 DPR 选择。
✅ 建议实践路线
- 优先准备多分辨率资源(含 @2x),或直接用 SVG。
- 在 Qt 层启用
AA_UseHighDpiPixmaps
,让 Qt 自动挑资源。 - 对必须缩放的小图标,手动选择
Qt::FastTransformation
,必要时锐化。 - 确保 manifest 正确声明 DPI Awareness,避免系统层“二次模糊”。
QIcon 管理类(比如 HiDpiIconManager
),自动封装:
- 从资源里加载多分辨率图标
- 根据 DPR 选择最优资源
- 必要时做一次锐化缩放
这样只要写 HiDpiIconManager::load(":/icons/foo")
,就能自动拿到高清图标,不用每次考虑 DPR
在 缩放比 = 1.5 的情况下,Qt 会怎么挑资源?
1. Qt 的资源命名规则
Qt 支持类似 iOS 的 @Nx
命名规则:
icon.png
→ 基础资源,认为是@1x
icon@2x.png
→ 为 2 倍缩放屏幕准备icon@3x.png
→ 为 3 倍缩放屏幕准备
2. Qt 的匹配逻辑
当 devicePixelRatio = X
时,Qt 会选择最接近的整数资源:
- 1.0 → 使用
icon.png
- 1.25, 1.5, 1.75 → Qt 仍然选择
icon.png
(1x 图),然后 按 1.5 缩放算法拉伸 - 2.0 → 使用
icon@2x.png
- 3.0 → 使用
icon@3x.png
也就是说:
👉 非整数缩放(1.25、1.5、1.75)时,Qt 不会去找 icon@1.5x.png
,而是从最近的整数资源(通常 @1x
或 @2x
)里缩放。
3. 为什么会模糊
- 如果只有
icon.png
(1x),在 150% 时 Qt 会把它放大 → 模糊。 - 如果有
icon@2x.png
,Qt 可能会在1.5
的情况下选择它,然后再缩小 → 效果会好很多(比放大更清晰)。
4. 常见解决方案
- 提供
@2x
资源:哪怕缩放比是 1.5,Qt 也能用高分图再缩小 → 更清晰。 - 矢量图 (SVG):Qt 会按缩放比重新渲染,避免模糊。
- 开启
AA_UseHighDpiPixmaps
:让 Qt 知道应该优先找@2x
图。 - 算法优化:Qt 默认用平滑缩放(bilinear),如果需要可以改为
Qt::FastTransformation
或更高质量的Qt::SmoothTransformation
。
📌 总结
- 在 1.5 倍缩放 下,Qt 默认仍然用
icon.png
并缩放。 - 如果额外提供了
icon@2x.png
,Qt 会更倾向于用高分图再缩小,显示更清晰。 - 最推荐的方案是 准备 @2x 图 或 直接用 SVG 矢量图。
1. SVG 本身的特点
理论上 SVG 是矢量图,按道理在缩放时不会模糊(它应该无限清晰)。
但是实际情况取决于 SVG 的内容:
- 如果路径(
<path>
、<polygon>
等)用的是矢量描述 → 缩放时清晰 ✅ - 如果内部嵌入了
<image>
或者某些近似栅格化的效果(如滤镜、半透明像素对齐不整)→ 放大时可能模糊 ❌
SVG 内容里:
- 主体都是
<path>
、<polygon>
,是矢量的。 - 但是关键在于:坐标体系是
32x32
,然后 Qt 在 1.5 倍时请求的是48x48
像素图像。
2. 为什么还是模糊
Qt 处理 SVG 时大概流程是:
-
用
QSvgRenderer
把矢量渲染到QImage
/QPixmap
上。 -
输出尺寸 = 目标
QPixmap
大小(比如 48x48)。 -
问题:
QSvgRenderer
默认用浮点坐标渲染,最后 Qt 会把它光栅化 → 变成像素。- 如果缩放比例不是整数(比如 1.5),那么很多直线/边框会落在 半个像素 上。
- 结果 Qt 会做抗锯齿(抗锯齿 = 插值 = 模糊感)。
所以根本原因是:
👉 SVG 是矢量的,但光栅化到 48×48 时,不可避免要插值和抗锯齿 → 看上去发虚。
3. 常见解决方法
方法 A:禁用抗锯齿,让边界硬对齐
QPainter p(&pixmap);
p.setRenderHint(QPainter::Antialiasing, false);
p.setRenderHint(QPainter::TextAntialiasing, false);
p.setRenderHint(QPainter::SmoothPixmapTransform, false);
renderer.render(&p);
这样线条会锐利,但可能有锯齿。
方法 B:渲染到更高分辨率,再缩小
比如目标是 48x48
,可以先渲染 96x96
,然后缩小 → 细节更多,更接近 @2x 的清晰度:
QPixmap pix(96, 96);
pix.fill(Qt::transparent);QPainter p(&pix);
renderer.render(&p);pix = pix.scaled(48, 48, Qt::KeepAspectRatio, Qt::SmoothTransformation);
相当于“超采样抗锯齿”,很多 UI 库(包括 macOS)用的就是这个 trick。
方法 C:使用整数坐标和尺寸
如果 SVG viewBox 是 32x32,那么最好让缩放比 = 整数倍(32→64,32→96),避免落在半像素。
但这就和 DPI 缩放的 1.25、1.5、1.75 冲突了 → 只能靠 oversampling 来弥补。
方法 D:优先考虑 SVG + IconEngine
Qt 里 QIcon::fromTheme
或 QIcon::addFile(svg)
,Qt 会根据 devicePixelRatio
自动挑更合适的分辨率绘制。
但如果比例是 1.5,它仍然会渲染到 48x48,然后触发上面说的半像素问题 → 本质依旧是 oversampling 方案更靠谱。
✅ 总结
-
SVG 本身没问题,模糊是因为 1.5 倍缩放导致路径落在半像素,Qt 抗锯齿插值后发虚。
-
解决办法:
- 关掉抗锯齿(锐利但有锯齿感)。
- 或者用 oversampling 渲染更大尺寸再缩小(推荐)。
- 如果能强制用整数倍缩放(2x、3x),那就最清晰。
QOpenGlWidget尺寸问题
上述所说的设置主要影响 QPainter / QWidget 绘制的像素比例,但 不会直接改变 QOpenGLWidget 的尺寸,原因如下:
关键点
-
QWidget 的尺寸 (QSize)
QWidget::size()
返回的单位始终是 逻辑像素,即跟随 Qt 的 DPI 缩放策略,而不是物理像素。 -
OpenGL 的 framebuffer 尺寸
对于QOpenGLWidget
,真正的渲染区域大小由QOpenGLWidget::devicePixelRatioF()
决定。
实际 OpenGL 渲染用的分辨率 =widget->size() * widget->devicePixelRatioF()
。 -
高 DPI 时的现象
即使你在 Qt 中启用了高 DPI scaling,QOpenGLWidget::size()
看起来没变(逻辑像素),但 framebuffer 实际会变大。
正确获取物理像素的方法是:QSize framebufferSize = openglWidget->size() * openglWidget->devicePixelRatioF();
建议解决办法
如果你需要在 OpenGL 里得到 正确的像素大小,不要直接用 width()/height()
,而要用 framebuffer 的真实尺寸:
void MyOpenGLWidget::resizeGL(int w, int h)
{qreal dpr = devicePixelRatioF();glViewport(0, 0, int(w * dpr), int(h * dpr));
}
或者直接:
QSize framebufferSize = size() * devicePixelRatioF();
glViewport(0, 0, framebufferSize.width(), framebufferSize.height());
✅ 总结:
QOpenGLWidget
的逻辑尺寸没变是 正常的。
在高 DPI 模式下,应该使用 devicePixelRatioF()
结合逻辑大小,才能得到真实的渲染分辨率。
鼠标事件捕捉的是逻辑坐标,对不上缩放后的内容
OpenGL 渲染用的是物理像素坐标,而 QWidget 的事件坐标用的是逻辑像素坐标。
也就是说:
QMouseEvent::pos()
/QMouseEvent::x(), y()
→ 逻辑坐标(受 DPI 缩放影响)glViewport()
、OpenGL 绘制 → 物理坐标(真实像素数)
所以当你在 resizeGL
里用了 w * devicePixelRatioF()
设置 viewport 后,鼠标点的位置就和渲染不匹配了。
🔧 解决办法
你需要在鼠标事件中,把逻辑坐标转为物理坐标(乘上 devicePixelRatioF()
):
void MyOpenGLWidget::mousePressEvent(QMouseEvent *event)
{qreal dpr = devicePixelRatioF();QPoint logicalPos = event->pos(); // 逻辑像素QPointF physicalPos = logicalPos * dpr; // 转为物理像素qDebug() << "Mouse logical:" << logicalPos<< " physical:" << physicalPos;// 用 physicalPos 去对应 OpenGL 里的坐标系
}
同样适用于 mouseMoveEvent
/ mouseReleaseEvent
等。
void QOpenGLWidget::resizeGL(int width, int height)
{makeCurrent(); int ratio = QApplication::desktop()->devicePixelRatio(); glViewport(0, 0, (GLint)width*ratio, (GLint)height*ratio);
}
这里放大了尺寸不会会存在修改QOpenGLWidget的width和height;
在 resizeGL(int width, int height)
里:
-
width
、height
参数是 Qt 传递给 OpenGL 的逻辑窗口大小(单位是 Qt 的 device independent pixel,受高 DPI 缩放影响)。 -
里面写的
int ratio = QApplication::desktop()->devicePixelRatio(); glViewport(0, 0, (GLint)width * ratio, (GLint)height * ratio);
只是告诉 OpenGL “实际要渲染到多少像素大小的 framebuffer”。这不会反过来去修改 Qt 的 widget 大小。
换句话说:
- QWidget 的
width()
/height()
由 Qt 控件系统管理,只会因为用户调整窗口、布局管理等改变,不会因为你调用glViewport
、glOrtho
而改变。 - 乘以
ratio
只是把 OpenGL 的渲染区域扩展到物理像素级别(比如 Retina 屏上,一个 widget 的width=100
,ratio=2
,那么实际渲染区域是 200 像素宽)。这是 渲染层面的放大,而不是 Qt widget 尺寸的修改。
所以 widget 在 Qt 看来大小不变(resizeEvent
、width()
、height()
都不会受影响),只是 OpenGL 绘制用的视口坐标范围变大了。
要验证很简单,可以在 resizeGL
里打印:
qDebug() << "widget size:" << this->width() << this->height()<< "viewport:" << width*ratio << height*ratio;
你会发现 widget size 一直是逻辑大小,而 viewport 是物理像素大小。
如果存在OpenGLwidget小于绘制尺寸,这样也可以绘制成功吗?
结论是:会存在 QOpenGLWidget 的逻辑大小 < 你设置的 glViewport 尺寸,但依然能绘制成功。
原因在于:
1. QOpenGLWidget 的实际 framebuffer 尺寸
-
Qt 内部会根据 逻辑大小 × devicePixelRatio 为
QOpenGLWidget
创建一个 FBO(framebuffer object)。 -
例如:
- widget 逻辑大小:
200 × 150
- ratio:
2
(Retina 屏) - Qt 实际分配的 FBO:
400 × 300
- widget 逻辑大小:
这保证了设置的
glViewport(0, 0, width*ratio, height*ratio);
和实际 FBO 一致,所以绘制不会出问题。
2. 如果“手动”设置 viewport 大于实际 FBO 尺寸
比如写:
glViewport(0, 0, width*ratio*2, height*ratio*2);
那超出 FBO 范围的区域不会被显示,OpenGL 仍然会绘制,但 超出的部分会被裁剪掉。
所以看起来“也能绘制成功”,只是你只能看到 FBO 有效区域的内容。
3. widget 的逻辑大小 vs 绘制大小
- 逻辑大小 (QWidget::width/height):由 Qt 管理,决定了控件在窗口中的布局占用。
- 绘制大小 (OpenGL FBO 尺寸):Qt 根据逻辑大小和 ratio 自动分配。
- glViewport 尺寸:你自己指定的绘制坐标范围,如果和 FBO 一致,就完美匹配。
✅ 所以,QOpenGLWidget
即使逻辑尺寸小(比如 200×150),只要正确用 devicePixelRatio
,OpenGL 渲染结果都会显示出来;
❌ 如果 viewport 超过 FBO 尺寸,OpenGL 仍然会“画”,但超出区域被丢弃,看不到。
如果实际widget大小是200 × 150,但是绘制内容是400*300,那么在绘制内容的右下角点击,会出现没点击到widget吗
1. Widget 的边界永远是 200×150(逻辑像素)
- Qt 的事件系统完全基于 逻辑坐标系。
- 也就是说,
QOpenGLWidget
的矩形区域就是200×150
,鼠标只有在这个范围内才会把事件交给它。 - 如果你用
glViewport(0,0,400,300)
,虽然 OpenGL 渲染了 400×300 的内容,但实际显示在屏幕上的可见矩形区域仍然只是 200×150 的逻辑大小对应的物理像素矩形。
2. 高 DPI 下 Qt 的处理
-
当
devicePixelRatio = 2
时:- Widget 的逻辑大小:200×150
- Qt 自动给它分配的 framebuffer 大小:400×300 物理像素
-
所以虽然你告诉 OpenGL 要画 400×300,实际上正好对应了 Qt 分配的 FBO 尺寸。
-
结果就是:你点击右下角(逻辑坐标 199,149),对应到物理坐标 (398,298),正好是 OpenGL 渲染区域的右下角。
3. 为什么“不会点到外面”
因为 Qt 的窗口系统只会把 200×150 的逻辑矩形 区域交给这个 widget,它之外根本没有你的 OpenGL 内容。
所以即使 OpenGL 里你写了 400×300 的 viewport,它也不会超出 widget 的矩形——Qt 已经帮你把物理像素和逻辑像素匹配好了。
✅ 结论
- 不会出现“点到 widget 外面”的情况。
- 在高 DPI 下,
200×150
的逻辑 widget 就对应400×300
的物理 FBO。 - 你点击右下角时,鼠标事件给你的是
(199,149)
(逻辑坐标),乘上ratio=2
后刚好是 OpenGL 的(398,298)
。
转换成物理坐标(OpenGL FBO 用的) 再转发。
关键点
QMouseEvent::pos()
→ 逻辑坐标,范围是 widget 的width() × height()
。- OpenGL 的
glViewport
/glOrtho
→ 物理坐标,范围是width*devicePixelRatio × height*devicePixelRatio
。 - 你需要在
mousePressEvent
里做一次映射。
推荐实现
你可以封装一个小工具函数,比如:
QPointF SubOpenGLWidget::mapToPhysical(const QPoint& logicalPos) const
{qreal ratio = devicePixelRatioF(); // 推荐用这个,支持浮点和多屏return QPointF(logicalPos.x() * ratio,logicalPos.y() * ratio);
}
然后在事件函数里这样用:
void SubOpenGLWidget::mousePressEvent(QMouseEvent* pEvent)
{ODA_ASSERT_ONCE(pEvent->isAccepted());bool bAccepted = false;if (!m_pView.isNull()){// 转换逻辑坐标 -> 物理坐标QPointF physPos = mapToPhysical(pEvent->pos());// 可以选择构造一个新的 QMouseEvent 转发出去QMouseEvent physEvent(pEvent->type(),physPos, // 物理坐标pEvent->button(),pEvent->buttons(),pEvent->modifiers());emit m_pView->mousePress(m_pView, &physEvent, bAccepted);}
}
注意事项
- 不要直接改原来的
pEvent
,因为 Qt 事件对象属于事件循环管理。最好新建一个临时的QMouseEvent
(如上)。 - 一定要用
devicePixelRatioF()
而不是老的QApplication::desktop()->devicePixelRatio()
,因为后者在多显示器 / 不同 DPI 下不准确。
👌 那我帮你写一个完整的 SubOpenGLWidget::event(QEvent* ev)
,覆盖所有 涉及 pos
坐标的输入事件:鼠标、滚轮、拖拽、触摸。这样以后所有位置相关的事件都能自动转成 物理坐标 再转发。
完整实现
bool SubOpenGLWidget::event(QEvent* ev)
{switch (ev->type()){// --- 鼠标相关 ---case QEvent::MouseButtonPress:case QEvent::MouseButtonRelease:case QEvent::MouseButtonDblClick:case QEvent::MouseMove:{QMouseEvent* me = static_cast<QMouseEvent*>(ev);QPointF physPos = mapLogicalToPhysical(me->pos(), this);QMouseEvent physEvent(me->type(),physPos, // 物理坐标me->button(),me->buttons(),me->modifiers());return QOpenGLWidget::event(&physEvent);}// --- 滚轮 ---case QEvent::Wheel:{QWheelEvent* we = static_cast<QWheelEvent*>(ev);QPointF physPos = mapLogicalToPhysical(we->position(), this);QWheelEvent physEvent(physPos,we->globalPosition(),we->pixelDelta(),we->angleDelta(),we->buttons(),we->modifiers(),we->phase(),we->inverted(),we->source());return QOpenGLWidget::event(&physEvent);}// --- 拖拽 ---case QEvent::DragEnter:case QEvent::DragMove:case QEvent::Drop:{QDropEvent* de = static_cast<QDropEvent*>(ev);QPointF physPos = mapLogicalToPhysical(de->posF(), this);QDropEvent physEvent(physPos,de->possibleActions(),de->mimeData(),de->mouseButtons(),de->keyboardModifiers(),de->type());return QOpenGLWidget::event(&physEvent);}// --- 触摸 ---case QEvent::TouchBegin:case QEvent::TouchUpdate:case QEvent::TouchEnd:{QTouchEvent* te = static_cast<QTouchEvent*>(ev);QList<QTouchEvent::TouchPoint> physPoints;physPoints.reserve(te->touchPoints().size());for (const QTouchEvent::TouchPoint& tp : te->touchPoints()){QTouchEvent::TouchPoint physTp(tp);physTp.setPos(mapLogicalToPhysical(tp.pos(), this));physTp.setStartPos(mapLogicalToPhysical(tp.startPos(), this));physTp.setLastPos(mapLogicalToPhysical(tp.lastPos(), this));physPoints << physTp;}QTouchEvent physEvent(te->type(),te->device(),te->modifiers(),te->touchPointStates(),physPoints);return QOpenGLWidget::event(&physEvent);}default:break;}// 没有处理的交给基类return QOpenGLWidget::event(ev);
}
辅助函数
inline QPointF SubOpenGLWidget::mapLogicalToPhysical(const QPointF& logicalPos, const QWidget* w) const
{qreal ratio = w->devicePixelRatioF();return QPointF(logicalPos.x() * ratio,logicalPos.y() * ratio);
}
说明
- 鼠标类:按键、移动、双击都转成物理坐标。
- 滚轮类:只转换
position()
,globalPosition()
保持不变。 - 拖拽类:用
QDropEvent::posF()
(Qt 5.14+ 才有),低版本可以用pos()
。 - 触摸类:所有
TouchPoint
的pos
、startPos
、lastPos
全部转换。