简介这是一份面向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 // 头文件里加 // QSetint 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是空格复位CtrlL是回水平你可以按项目习惯改。注意 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用 doubleDesigner 里能填小数。再说平滑动画。直接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确认绘制和事件都对再往主工程里搬——这样出问题时能快速定位是控件本身还是集成环境的问题。这套姿态球加按键控制的思路换成其他仪表转速表、罗盘也是同一套路把数学映射换掉就行。希望帮到你。本文还有配套的精品资源点击获取
