3. Qt输入事件模型
做嵌入式触摸屏开发,最头疼的问题之一就是屏幕适配。尤其是现在硬件迭代快,同一套软件可能要跑在3.5寸、7寸、10.1寸甚至更大尺寸的屏幕上。分辨率也从480x272一路飙到1920x1080。如果不做高DPI适配,那画面效果……嗯,我见过不少项目,在7寸屏上跑得好好的,换到10寸高分屏上,按钮小得像米粒,字都挤在一起。
这一章,我就把Qt里高DPI适配的核心思路拆开来讲。说白了就三件事:缩放机制怎么开、坐标怎么换算、字体图标怎么跟着走。
3.1 高DPI缩放:Qt到底在缩放什么?
先问一个问题:Qt的高DPI缩放,到底缩放的是什么?
很多人以为它只是把窗口变大。其实不是。Qt缩放的是逻辑像素到物理像素的映射关系。你写代码时用的单位是逻辑像素,屏幕实际发光的是物理像素。高DPI缩放,就是让一个逻辑像素对应更多物理像素,从而让UI元素在物理尺寸上保持一致。
核心概念:Qt 5.6之后引入了高DPI缩放支持,通过设置环境变量 QT_AUTO_SCREEN_SCALE_FACTOR 或 QT_SCALE_FACTOR 来控制。Qt 6中,默认就开启了缩放,但行为更精细。
我个人习惯在 main() 函数最前面就设置好缩放策略:
#include <QApplication>
#include <QHighDpiScaling>
int main(int argc, char *argv[])
{
// Qt 5.6+ 推荐方式
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// Qt 6 中默认启用,但可以设置策略
QApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
QApplication app(argc, argv);
// ... 其余代码
return app.exec();
}
这里有个坑。我在项目中遇到过,有些国产屏幕的EDID信息不准确,导致Qt自动检测的缩放因子是错的。比如明明是2.0的缩放,它给你算成1.75。结果就是界面元素忽大忽小,文字模糊。后来我改用固定缩放因子,配合屏幕尺寸手动判断,才稳定下来。
注意:PassThrough 策略会保留非整数缩放因子(如1.5、1.75),但某些老旧的QWidget控件在非整数缩放下会出现像素对齐问题。如果你的界面大量使用自定义绘制,建议用 RoundPreferFloor 或 RoundPreferCeiling 取整。
3.2 逻辑坐标与物理坐标:别搞混了
很多新手搞不清逻辑坐标和物理坐标的区别。我打个比方:
- 逻辑坐标:你写代码时用的坐标。比如
setGeometry(100, 100, 200, 50),这里的100、200、50都是逻辑值。 - 物理坐标:屏幕实际发光的像素点。在2倍缩放下,逻辑坐标(100, 100)对应物理坐标(200, 200)。
Qt内部会自动做这个换算。但如果你直接操作底层像素(比如QImage、QPixmap的像素级操作),就必须自己处理这个映射关系。
我整理了一个对照表,方便你理解:
| 概念 | 说明 | 常用API |
|---|---|---|
| 逻辑坐标 | 代码中使用的坐标,与DPI无关 | QWidget::geometry(), QRect |
| 物理坐标 | 屏幕实际像素坐标 | QScreen::geometry() |
| 设备像素比 | 物理像素 / 逻辑像素 | QWindow::devicePixelRatio() |
| 缩放因子 | Qt自动计算的缩放倍数 | QScreen::logicalDotsPerInch() / 96.0 |
举个例子。你需要在屏幕上画一条1像素宽的线。在2倍缩放屏上,如果直接用 QPainter::drawLine() 并设置笔宽为1,实际画出来的是2个物理像素宽。但如果你想要真正的1个物理像素宽,就得把笔宽设为 1.0 / devicePixelRatio()。
// 画一条真正的1物理像素宽的线
qreal dpr = widget->windowHandle()->devicePixelRatio();
QPen pen(Qt::black, 1.0 / dpr);
painter->setPen(pen);
painter->drawLine(0, 0, 100, 0);
经验之谈:在触摸屏应用中,触摸事件的坐标也是逻辑坐标。你不需要手动转换。但如果你用 QCursor::pos() 获取鼠标位置,返回的也是逻辑坐标。这一点在跨屏幕拖拽时尤其要注意。
3.3 字体适配:别让文字忽大忽小
字体适配是高DPI适配中最容易出问题的环节。为什么?因为字体有点阵和矢量之分,不同缩放因子下渲染效果差异很大。
Qt中字体大小的单位是点(point),1点 = 1/72英寸。理论上,设置 font.setPointSize(12),在任何DPI下物理尺寸都应该一样。但实际并非如此——因为Qt的字体引擎在不同缩放因子下,对点阵字体的渲染策略不同。
我建议的做法是:
- 优先使用矢量字体(如Noto Sans CJK、思源黑体)。矢量字体在缩放时不会失真。
- 字体大小用点值,不要用像素值。点值会自动适配DPI。
- 设置最小字体限制,防止在低DPI屏上字太小。
// 字体适配示例
QFont font;
font.setFamily("Noto Sans CJK SC");
font.setPointSize(12); // 使用点值,自动适配DPI
// 设置最小像素限制
int minPixelSize = 14;
int actualPixelSize = font.pixelSize();
if (actualPixelSize < minPixelSize) {
font.setPixelSize(minPixelSize);
}
app.setFont(font);
我曾经踩过的坑:在某个项目中,我用了 setPixelSize() 设置字体大小。结果在2倍缩放屏上,字体直接缩小了一半。因为 setPixelSize() 设置的是逻辑像素,而Qt在缩放时不会自动放大它。后来全部改成 setPointSize() 才解决。
3.4 图标适配:一套图还是多套图?
图标适配的策略,说白了就是:用SVG还是用PNG?
我的建议很明确:
- 优先用SVG。矢量图,无限缩放不失真。Qt的QSvgRenderer可以直接渲染到QPainter上。
- 如果必须用PNG,准备多套尺寸(如1x、2x、3x),根据设备像素比动态加载。
- 避免用字体图标(如Font Awesome的Unicode方式),因为不同缩放因子下对齐问题很难调。
SVG图标加载示例:
#include <QSvgRenderer>
#include <QPainter>
void MyWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
// 加载SVG图标
QSvgRenderer renderer(QString(":/icons/icon_settings.svg"));
QRectF targetRect(0, 0, 48, 48); // 逻辑尺寸
renderer.render(&painter, targetRect);
}
如果你用QIcon加载SVG,Qt会自动处理缩放:
QIcon icon(":/icons/icon_settings.svg");
// 设置图标大小,Qt会根据设备像素比自动缩放
pushButton->setIcon(icon);
pushButton->setIconSize(QSize(32, 32));
小技巧:SVG文件里如果包含 viewBox 属性,Qt会按比例缩放。如果没有,你需要手动设置 setViewBox()。我习惯在SVG文件头部加上 viewBox="0 0 24 24",这样Qt就能正确计算缩放比例。
3.5 整体适配流程:一张图说清楚
说了这么多,我把整个高DPI适配的决策流程画成了一张图。你照着这个流程走,基本不会出大问题。
这张图的核心逻辑是:先决定缩放策略(自动还是手动),然后依次处理坐标、字体、图标。每一步都有对应的Qt API和最佳实践。你把这个流程跑通,高DPI适配就稳了八成。
3.6 实战检查清单
最后,我列一个检查清单。每次做完适配,对着这个清单过一遍,能省去不少调试时间:
| 检查项 | 验证方法 | 常见问题 |
|---|---|---|
| 缩放因子是否正确 | qDebug() << screen->devicePixelRatio(); |
非整数缩放导致控件错位 |
| 字体大小是否一致 | 在不同DPI屏上对比文字物理尺寸 | 点阵字体在缩放后模糊 |
| 图标是否清晰 | 放大200%后观察边缘锯齿 | PNG图标在非整数倍缩放时模糊 |
| 触摸区域是否准确 | 用触摸笔点击控件边缘 | 逻辑坐标与物理坐标未对齐 |
| 自定义绘制是否适配 | 检查 QPainter 的缩放设置 |
笔宽、渐变未按DPI缩放 |
嗯,这一章的内容就到这里。高DPI适配说难不难,说简单也不简单。关键是把逻辑坐标和物理坐标的关系理清楚,然后字体和图标跟着策略走。你按我上面说的流程来,基本不会翻车。