简介:这是一份面向Qt初学者与界面开发者的飞机水平仪示例工程,基于Qt库实现图形化仪表盘,并支持键盘快捷键实时控制仪表状态,适合用来练习GUI绘制与事件响应。压缩包共10个文件,约79KB,包含3个cpp与2个h源码文件、1个pro工程配置、1个vcxproj及filters等VS工程文件,另有1张png效果截图,整体结构紧凑,可直接在Visual Studio结合Qt环境下编译运行。资源核心价值在于演示了QPainter自定义绘图、QGraphicsView场景交互以及keyPressEvent键盘事件处理等关键知识点,读者可据此理解如何把图标素材通过资源系统嵌入程序,并掌握VS与Qt联合调试的配置方式。目前已有172人学习,适合作为Qt界面编程与跨平台开发的入门参考模板。
1. 飞机水平仪图标在 Qt 里到底怎么画:从一块画布到能动的姿态球
很多人第一次接到「做一个飞机水平仪图标」的需求时,脑子里想的是找一张 PNG 贴上去,结果真上手才发现静态图根本不够用——飞机一转弯、一俯仰,图标得跟着动,而且要在按键快捷控制下实时响应。这个标题讲的就是这件事:用 Qt 自绘一个飞机水平仪(Attitude Indicator,也叫姿态仪),并且用键盘快捷键控制它的俯仰、滚转和复位。它解决的是仪表类 UI 里「没有现成控件、第三方库又太重」的问题,适合做地面站、飞行模拟、车载姿态显示、工业 HMI 的 Qt 开发者。核心难点不在画圆,而在坐标系变换、双缓冲刷新和快捷键事件分发这三件事上,下面按能复现的顺序拆开讲。
2. 姿态球背后的数学与 Qt 绘制选型:为什么不用 QLabel 贴图
2.1 姿态仪的三个自由度与坐标映射
飞机水平仪要表达三个量:俯仰角(pitch)、滚转角(roll)、偏航角(yaw,多数小图标里省略)。视觉上,俯仰靠天地线的上下平移体现,滚转靠整个姿态球绕中心旋转体现。也就是说,屏幕坐标里做两件事:先按 roll 旋转坐标系,再按 pitch 平移天地线。
数学上不复杂,但坑在于 Qt 的旋转是绕原点(0,0)的,而我们要绕控件中心旋转。常见做法是painter.translate(center)把原点搬到中心,再painter.rotate(-roll),注意符号——飞机右滚时,视觉上地平线是往左倒的,符号搞反是新手第一个翻车点。
俯仰的像素映射需要一个比例系数,比如每度对应多少像素。我一般用pixelsPerDegree = height / 60.0,意思是控件高度覆盖 ±30 度俯仰范围,这个值直接决定灵敏度,后面参数章节细说。
2.2 用 QWidget 自绘还是 QGraphicsView
选型上三条路:QWidget 重写 paintEvent、QGraphicsView 场景、QML Canvas。做单个图标,QWidget 自绘最轻,依赖最少,也最好嵌进现有 Qt Widgets 工程。QGraphicsView 适合场景里有很多可交互图元,但为了一个姿态球引入场景坐标系是杀鸡用牛刀。QML 在 Qt Quick 工程里更顺,但如果你项目是纯 Widgets,混入 QML 会带来部署复杂度。
我一般选 QWidget 自绘,配合QPainter的抗锯齿。要注意的是,姿态球每帧都在变,必须开双缓冲,否则快速按键时会闪。Qt 5 之后 QWidget 默认就是双缓冲,但如果你在 paintEvent 里又手动QPixmap拼贴,反而可能引入撕裂,这点后面避坑章会讲。
2.3 最小可跑的绘制骨架
先给一个能画出静态姿态球的最小类,后面所有功能都在这上面加。
// AttitudeIndicator.h #pragma once #include <QWidget> class AttitudeIndicator : public QWidget { Q_OBJECT public: explicit AttitudeIndicator(QWidget *parent = nullptr); void setPitch(double deg); // 俯仰角,单位度 void setRoll(double deg); // 滚转角,单位度 protected: void paintEvent(QPaintEvent *event) override; private: double m_pitch = 0.0; double m_roll = 0.0; double m_pixelsPerDegree = 4.0; // 每度俯仰对应像素 };// AttitudeIndicator.cpp #include "AttitudeIndicator.h" #include <QPainter> #include <QPainterPath> AttitudeIndicator::AttitudeIndicator(QWidget *parent) : QWidget(parent) { setMinimumSize(200, 200); setFocusPolicy(Qt::StrongFocus); // 关键:能接收键盘事件 } void AttitudeIndicator::setPitch(double deg) { m_pitch = deg; update(); // 触发重绘,不要手动调 paintEvent } void AttitudeIndicator::setRoll(double deg) { m_roll = deg; update(); } void AttitudeIndicator::paintEvent(QPaintEvent *) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); const QPointF center(width() / 2.0, height() / 2.0); const double radius = qMin(width(), height()) / 2.0 - 4; // 裁剪成圆形,超出部分不画 QPainterPath clip; clip.addEllipse(center, radius, radius); p.setClipPath(clip); p.save(); p.translate(center); p.rotate(-m_roll); // 滚转:整个天地线旋转 p.translate(0, m_pitch * m_pixelsPerDegree); // 俯仰:天地线上下平移 // 天空 p.fillRect(QRectF(-radius * 2, -radius * 2, radius * 4, radius * 2), QColor(60, 130, 200)); // 地面 p.fillRect(QRectF(-radius * 2, 0, radius * 4, radius * 2), QColor(120, 80, 40)); // 地平线 p.setPen(QPen(Qt::white, 2)); p.drawLine(QPointF(-radius * 2, 0), QPointF(radius * 2, 0)); p.restore(); // 外圈 p.setPen(QPen(Qt::white, 3)); p.drawEllipse(center, radius, radius); }逻辑说明:paintEvent里先裁剪圆形,再translate到中心、rotate滚转、translate俯仰,这样天地线就跟着姿态动。setFocusPolicy(Qt::StrongFocus)是后面快捷键能生效的前提,漏了它键盘事件根本进不来。参数说明:m_pixelsPerDegree控制俯仰灵敏度,radius留 4 像素边距防止外圈被裁掉。这段代码直接编译就能看到一个会随setPitch/setRoll变化的姿态球。
3. 按键快捷控制怎么接:QShortcut、keyPressEvent 与事件分发的取舍
3.1 三种快捷键接入方式的区别
Qt 里做快捷键有三条路:重写keyPressEvent、用QShortcut、用QAction挂setShortcut。区别很实际:
keyPressEvent:只有控件有焦点时才收到,适合「这个图标被选中时才响应方向键」的场景。QShortcut:绑定到某个 widget 或窗口,即使焦点在别处也能触发,适合全局快捷键。QAction:能同时挂菜单、工具栏和快捷键,适合需要统一入口的正式功能。
飞机水平仪这种图标,如果它本身是可聚焦的仪表盘,用keyPressEvent最自然;如果希望不管焦点在哪都能用方向键调姿态,用QShortcut更稳。我一般两个都上:keyPressEvent处理方向键微调,QShortcut处理复位键。
3.2 用 keyPressEvent 做连续微调
连续按键要处理「按住不放」的重复触发。Qt 默认会发 auto-repeat 事件,但间隔由系统决定,做平滑动画时不够可控。我的做法是记录按下的键,在定时器里按固定步长累加,松开时停止。
// 在 AttitudeIndicator 类里补充 #include <QKeyEvent> #include <QTimer> // 头文件里加: // QSet<int> m_pressedKeys; // QTimer *m_repeatTimer; void AttitudeIndicator::keyPressEvent(QKeyEvent *e) { if (e->isAutoRepeat()) return; // 自己控制重复,忽略系统重复 m_pressedKeys.insert(e->key()); if (!m_repeatTimer->isActive()) m_repeatTimer->start(16); // 约 60 FPS QWidget::keyPressEvent(e); } void AttitudeIndicator::keyReleaseEvent(QKeyEvent *e) { if (e->isAutoRepeat()) return; m_pressedKeys.remove(e->key()); if (m_pressedKeys.isEmpty()) m_repeatTimer->stop(); QWidget::keyReleaseEvent(e); } // 定时器槽函数 void AttitudeIndicator::onRepeatTick() { const double step = 1.0; // 每帧变化 1 度 if (m_pressedKeys.contains(Qt::Key_Up)) setPitch(m_pitch + step); if (m_pressedKeys.contains(Qt::Key_Down)) setPitch(m_pitch - step); if (m_pressedKeys.contains(Qt::Key_Left)) setRoll(m_roll - step); if (m_pressedKeys.contains(Qt::Key_Right)) setRoll(m_roll + step); }逻辑说明:忽略系统 auto-repeat,自己用 16ms 定时器驱动,好处是步长和帧率完全可控,做动画时不会忽快忽慢。参数说明:step是每帧角度增量,16ms 对应约 60 度/秒的变化速度,想更快就调大 step 或缩短定时器间隔。注意m_repeatTimer要在构造函数里new QTimer(this)并连到onRepeatTick。
3.3 用 QShortcut 做复位和预设姿态
复位键、切到水平、切到某个预设角度,这类一次性动作适合 QShortcut。
// 构造函数里 auto *resetSc = new QShortcut(QKeySequence(Qt::Key_Space), this); connect(resetSc, &QShortcut::activated, this, [this]{ setPitch(0); setRoll(0); }); auto *levelSc = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_L), this); connect(levelSc, &QShortcut::activated, this, [this]{ setPitch(0); setRoll(0); });逻辑说明:QShortcut的父对象设为this,作用域就限制在这个控件内,不会和主窗口其他快捷键打架。参数说明:Qt::Key_Space是空格复位,Ctrl+L是回水平,你可以按项目习惯改。注意 QShortcut 在 Qt 6 里移到了QtGui模块,Qt 5 在QtWidgets,跨版本时头文件要留意。
3.4 快捷键和焦点策略的配合
如果发现按键没反应,九成是焦点问题。检查三件事:控件有没有setFocusPolicy(Qt::StrongFocus);控件是不是真的拿到了焦点(hasFocus());父窗口有没有把事件吞掉。调试时可以在keyPressEvent第一行打日志,确认事件到底有没有进来。这一步能省掉大量玄学排查时间。
4. 参数怎么调、性能怎么保:刷新率、抗锯齿与高分屏适配
4.1 决定手感的四个参数
| 参数 | 作用 | 常用值 | 调大后果 |
|---|---|---|---|
| pixelsPerDegree | 俯仰灵敏度 | 3~6 | 天地线移动过快,小角度看不清 |
| step(每帧角度) | 按键变化速度 | 0.5~2.0 | 微调变难,一按就过头 |
| 定时器间隔 | 刷新帧率 | 16ms | 低于 8ms 意义不大,CPU 白耗 |
| 抗锯齿 | 边缘平滑 | 开 | 关掉省 CPU,但圆边有锯齿 |
这四个参数里,pixelsPerDegree和step是手感的核心。我的经验是:控件高度 200px 时,pixelsPerDegree取 4 左右,俯仰 ±30 度刚好铺满;step取 1.0,按住方向键大约 2 秒能从水平转到 30 度,这个速度做演示和实操都舒服。
4.2 高分屏下的坐标与线宽
Qt 5.6 之后支持高分屏,但自绘控件如果不处理 devicePixelRatio,在 4K 屏上会糊或者线宽不对。稳妥做法是用浮点坐标绘制,让 QPainter 自己处理缩放,同时线宽用QPen的浮点宽度而不是整数。
// 高分屏下建议的初始化 setAttribute(Qt::WA_OpaquePaintEvent, false); // 绘制时全部用 QPointF / QRectF,避免整数截断 p.setPen(QPen(Qt::white, 2.0)); // 2.0 而不是 2逻辑说明:整数坐标在高 DPI 下会被取整到物理像素,导致圆不圆、线不匀。全用浮点,配合QPainter::Antialiasing,缩放时质量稳定。参数说明:线宽 2.0 是逻辑像素,实际物理宽度由 DPR 决定,不用手动乘。
4.3 只在必要时重绘
姿态球不是每秒都必须刷 60 次。如果姿态没变,update()不该被调用。setPitch/setRoll里可以先判断值有没有变,没变直接 return,省掉无谓重绘。对于电池供电的嵌入式设备(比如树莓派上跑 Qt),这一条能明显降功耗。
void AttitudeIndicator::setPitch(double deg) { if (qFuzzyCompare(m_pitch, deg)) return; // 值没变不重绘 m_pitch = deg; update(); }逻辑说明:qFuzzyCompare处理浮点比较,避免因为 0.0000001 的差异触发重绘。参数说明:如果姿态来自传感器,噪声大时可以加一个死区,比如变化小于 0.1 度就不更新。
5. 避坑与排查:姿态球和快捷键最容易翻车的五个地方
5.1 按键完全没反应
现象:程序跑起来,方向键按下去姿态球纹丝不动。原因:控件没有焦点策略,或者焦点被别的控件抢走。解决:构造函数里加setFocusPolicy(Qt::StrongFocus),并在需要时setFocus();如果嵌在复杂布局里,检查有没有别的控件默认抢焦点。
5.2 快速按键时画面撕裂或闪烁
现象:连续按方向键,姿态球边缘出现撕裂或整块闪。原因:在 paintEvent 里手动拼 QPixmap 又没处理好,或者关掉了 Qt 默认双缓冲。解决:不要在 paintEvent 里做额外缓冲,交给 Qt 默认机制;确认没有设置Qt::WA_PaintOnScreen这类破坏双缓冲的属性。
5.3 滚转方向反了
现象:按右键飞机应该右滚,结果地平线往反方向倒。原因:painter.rotate的符号和视觉预期相反。解决:记住「飞机右滚 = 地平线左倒」,所以rotate(-roll);如果还是不对,把符号反过来试一次,这是纯几何问题,试两次就对了。
5.4 高分屏下圆变成椭圆
现象:在 4K 屏或缩放 150% 的屏幕上,姿态球外圈变成椭圆。原因:用了整数坐标或固定像素尺寸,没跟随 DPR。解决:全部改用QPointF/QRectF,半径用qMin(width(), height())/2.0动态算,不要写死像素。
5.5 快捷键和主窗口冲突
现象:姿态球里的空格复位,结果触发了主窗口的播放/暂停。原因:QShortcut 作用域没限制,或者主窗口也注册了同一个键。解决:QShortcut 的父对象设为姿态球控件本身,作用域就限制在控件内;如果必须全局,用QShortcut::setContext(Qt::WidgetWithChildrenShortcut)明确范围。
6. 进阶:把姿态球做成可复用组件并加平滑动画
做到这里,一个能画、能按键控制的姿态球已经能跑了。但真放进项目,还有两件事值得做:一是把它抽成可复用组件,二是加平滑动画让姿态变化不那么生硬。
先说复用。把AttitudeIndicator做成一个独立类,对外只暴露setPitch/setRoll和几个样式属性(天空色、地面色、线宽),内部实现随便改不影响调用方。样式属性用Q_PROPERTY暴露,这样在 Qt Designer 里也能直接调,配合qt designer界面设计的工作流很顺。
class AttitudeIndicator : public QWidget { Q_OBJECT Q_PROPERTY(QColor skyColor READ skyColor WRITE setSkyColor) Q_PROPERTY(QColor groundColor READ groundColor WRITE setGroundColor) Q_PROPERTY(double pixelsPerDegree READ pixelsPerDegree WRITE setPixelsPerDegree) public: // ... 省略前面的接口 QColor skyColor() const { return m_skyColor; } void setSkyColor(const QColor &c) { m_skyColor = c; update(); } // groundColor、pixelsPerDegree 同理 private: QColor m_skyColor{60, 130, 200}; QColor m_groundColor{120, 80, 40}; };逻辑说明:Q_PROPERTY让这些属性出现在 Qt Designer 的属性编辑器里,设计界面时就能改颜色和灵敏度,不用改代码。参数说明:pixelsPerDegree用 double,Designer 里能填小数。
再说平滑动画。直接setPitch是瞬变的,按键微调时还好,但如果姿态来自传感器或预设切换,瞬变会很突兀。用QPropertyAnimation对 pitch/roll 做插值,200ms 内过渡到目标值,视觉上顺很多。注意动画期间要禁用按键输入,否则用户按键和动画会打架。
auto *anim = new QPropertyAnimation(this, "pitch"); anim->setDuration(200); anim->setStartValue(m_pitch); anim->setEndValue(targetPitch); anim->setEasingCurve(QEasingCurve::OutCubic); anim->start(QAbstractAnimation::DeleteWhenStopped);逻辑说明:QPropertyAnimation直接驱动pitch属性,前提是 pitch 也用Q_PROPERTY暴露。参数说明:200ms 是过渡时长,OutCubic是先快后慢的缓动,仪表类动画用这个曲线比较自然。
验证方法很简单:写个测试,用QTest::keyClick模拟按键,断言pitch/roll按预期变化;再截几帧图对比,确认滚转方向和高分屏下不变形。我自己的习惯是,任何自绘控件都先写一个能单独跑起来的最小 demo,确认绘制和事件都对,再往主工程里搬——这样出问题时能快速定位是控件本身还是集成环境的问题。这套姿态球加按键控制的思路,换成其他仪表(转速表、罗盘)也是同一套路,把数学映射换掉就行。希望帮到你。
本文还有配套的精品资源,点击获取