PyQt+QML 实现飞控仪表盘:姿态、高度、速度,飞行数据等 实时显示(三)
- 一、说明
- 二、效果展示
- 三、源码分享
- 1、工程结构
- 2、库安装
- 3、main.py
- 4、main.qml
- 5、Vsi.qml
- 6、图片资源下载
- 四、实现原理
- 1、整体数据流
- 2、QML 属性绑定机制
- 3、仪表绘制原理
- 4、定时刷新与实时性
- 5、扩展:接入真实飞行数据
- 6、小结
一、说明
由于篇幅限制此篇只显示3个仪表,剩余见下一篇
二、效果展示
三、源码分享
1、工程结构
2、库安装
3、main.py
importsysimportosfromPyQt6.QtCoreimportQUrlfromPyQt6.QtGuiimportQGuiApplicationfromPyQt6.QtQmlimportQQmlApplicationEnginedefmain():app=QGuiApplication(sys.argv)engine=QQmlApplicationEngine()qml_path=os.path.join(os.path.dirname(os.path.abspath(__file__)),"main.qml")engine.load(QUrl.fromLocalFile(qml_path))ifnotengine.rootObjects():print("错误:无法加载 QML 文件",file=sys.stderr)sys.exit(-1)sys.exit(app.exec())if__name__=="__main__":main()4、main.qml
importQtQuickimportQtQuick.WindowimportQtQuick.ControlsimportQtQuick.Layouts ApplicationWindow{id:root visible:true width:1200height:900title:"Flight Instruments"color:"#1a1a2e"//Flight parameters(driven by test timerormanual control)propertyreal alpha:0propertyreal beta:0propertyreal roll:0propertyreal pitch:0propertyreal heading:0propertyreal slipSkid:0propertyreal turnRate:0propertyreal devH:0propertyreal devV:0propertyreal airspeed:0propertyreal altitude:0propertyreal pressure:30.0propertyreal climbRate:0propertyreal machNo:0propertyreal adf:0propertyreal dme:0//Simulation statepropertyboolautoMode:truepropertyreal realTime:0//3x3 Grid layout GridLayout{anchors.fill:parent columns:3rows:3rowSpacing:4columnSpacing:4//Row1:ADI,ALT,ASI Item{Layout.fillWidth:true Layout.fillHeight:true Label{anchors.top:parent.top anchors.horizontalCenter:parent.horizontalCenter text:"ADI"color:"#aaaacc"font.pixelSize:12z:10}Adi{anchors.fill:parent roll:root.roll pitch:root.pitch}}Item{Layout.fillWidth:true Layout.fillHeight:true Label{anchors.top:parent.top anchors.horizontalCenter:parent.horizontalCenter text:"ALT"color:"#aaaacc"font.pixelSize:12z:10}Alt{anchors.fill:parent altitude:root.altitude pressure:root.pressure}}Item{Layout.fillWidth:true Layout.fillHeight:true Label{anchors.top:parent.top anchors.horizontalCenter:parent.horizontalCenter text:"ASI"color:"#aaaacc"font.pixelSize:12z:10}Asi{anchors.fill:parent airspeed:root.airspeed}}//Row2:HSI,TC,NAV Item{Layout.fillWidth:true Layout.fillHeight:true Label{anchors.top:parent.top anchors.horizontalCenter:parent.horizontalCenter text:"HSI"color:"#aaaacc"font.pixelSize:12z:10}Hsi{anchors.fill:parent heading:root.heading}}Item{Layout.fillWidth:true Layout.fillHeight:true Label{anchors.top:parent.top anchors.horizontalCenter:parent.horizontalCenter text:"TC"color:"#aaaacc"font.pixelSize:12z:10}Tc{anchors.fill:parent turnRate:root.turnRate slipSkid:root.slipSkid*15.0}}Item{Layout.fillWidth:true Layout.fillHeight:true Label{anchors.top:parent.top anchors.horizontalCenter:parent.horizontalCenter text:"NAV"color:"#aaaacc"font.pixelSize:12z:10}Nav{anchors.fill:parent heading:root.heading headingBug:0course:0bearing:root.adf bearingVisible:true deviation:root.devH deviationVisible:true distance:root.dme distanceVisible:true}}//Row3:VSI,Controls Item{Layout.fillWidth:true Layout.fillHeight:true Label{anchors.top:parent.top anchors.horizontalCenter:parent.horizontalCenter text:"VSI"color:"#aaaacc"font.pixelSize:12z:10}Vsi{anchors.fill:parent climbRate:root.climbRate}}//Control Panel-spans2columns Item{Layout.columnSpan:2Layout.fillWidth:true Layout.fillHeight:true Rectangle{anchors.fill:parent anchors.margins:8color:"#16213e"radius:8border.color:"#334466"border.width:1Column{anchors.centerIn:parent spacing:12width:parent.width*0.8Label{text:"Flight Instruments Test Panel"color:"#ffffff"font.pixelSize:16font.bold:true anchors.horizontalCenter:parent.horizontalCenter}Button{text:root.autoMode ?"⏸ Stop Auto":"▶ Start Auto"anchors.horizontalCenter:parent.horizontalCenter width:200palette.button:root.autoMode ?"#e94560":"#0f3460"palette.buttonText:"#ffffff"onClicked:root.autoMode=!root.autoMode}Grid{columns:3spacing:8anchors.horizontalCenter:parent.horizontalCenter Label{text:"Roll:";color:"#aaaacc";font.pixelSize:11}Label{text:roll.toFixed(1)+"°";color:"#ffffff";font.pixelSize:11}Label{text:"";color:"#aaaacc";font.pixelSize:11}Label{text:"Pitch:";color:"#aaaacc";font.pixelSize:11}Label{text:pitch.toFixed(1)+"°";color:"#ffffff";font.pixelSize:11}Label{text:"";color:"#aaaacc";font.pixelSize:11}Label{text:"Heading:";color:"#aaaacc";font.pixelSize:11}Label{text:heading.toFixed(1)+"°";color:"#ffffff";font.pixelSize:11}Label{text:"";color:"#aaaacc";font.pixelSize:11}Label{text:"Speed:";color:"#aaaacc";font.pixelSize:11}Label{text:airspeed.toFixed(0)+" kt";color:"#ffffff";font.pixelSize:11}Label{text:"Alt:";color:"#aaaacc";font.pixelSize:11}Label{text:altitude.toFixed(0)+" ft";color:"#ffffff";font.pixelSize:11}Label{text:"V/S:";color:"#aaaacc";font.pixelSize:11}Label{text:climbRate.toFixed(0)+" ft/m";color:"#ffffff";font.pixelSize:11}}}}}}//Test Timer-generates sinusoidal flight data(portedfromMainWindow.cpp)Timer{id:testTimer interval:50repeat:true running:true triggeredOnStart:false onTriggered:{if(!root.autoMode)returnconst dt=interval/1000.0root.realTime+=dt const t=root.realTime root.alpha=20.0*Math.sin(t/10.0)root.beta=15.0*Math.sin(t/10.0)root.roll=180.0*Math.sin(t/10.0)root.pitch=90.0*Math.sin(t/20.0)root.heading=360.0*Math.sin(t/40.0)root.slipSkid=1.0*Math.sin(t/10.0)root.turnRate=7.0*Math.sin(t/10.0)root.devH=1.0*Math.sin(t/20.0)root.devV=1.0*Math.sin(t/20.0)root.airspeed=125.0*Math.sin(t/40.0)+125.0root.altitude=9000.0*Math.sin(t/40.0)+9000.0root.pressure=2.0*Math.sin(t/20.0)+30.0root.climbRate=650.0*Math.sin(t/20.0)root.machNo=root.airspeed/650.0root.adf=-360.0*Math.sin(t/50.0)root.dme=99.0*Math.sin(t/100.0)}}}5、Vsi.qml
importQtQuick Item{id:root//Vertical Speed Indicator//Original size:240x240propertyreal climbRate:0//ft/min,range:-2000to2000readonlypropertyreal origW:240readonlypropertyreal origH:240propertyreal s:Math.min(width/origW,height/origH)Item{width:root.origW height:root.origH scale:root.s anchors.centerIn:parent//Face(z:-20)Image{width:240height:240source:"image/vsi/vsi_face.svg"fillMode:Image.PreserveAspectFit}//Hand(z:-10)-rotates by climbRate*0.086Image{id:vsiHand width:240height:240source:"image/vsi/vsi_hand.svg"fillMode:Image.PreserveAspectFit transformOrigin:Item.Center rotation:{var clamped=Math.max(-2000,Math.min(2000,root.climbRate))returnclamped*0.086}}//Case(z:10)Image{width:240height:240source:"image/vsi/vsi_case.svg"fillMode:Image.PreserveAspectFit}}}6、图片资源下载
https://download.csdn.net/download/qq_15181569/93089298
四、实现原理
本项目的核心思路是PyQt6 负责程序入口与 QML 引擎加载,QML 负责界面绘制与飞行数据驱动,两者通过 Qt 的属性绑定机制实现数据与界面的解耦。下面从数据流、仪表绘制、定时刷新三个层面展开说明。
1、整体数据流
整个系统采用单向数据流设计,数据从main.qml顶层的ApplicationWindow属性出发,向下传递给各个仪表组件:
- 数据源:
main.qml中的Timer组件每 50ms 触发一次onTriggered,用正弦函数模拟飞行数据(如roll = 180 * sin(t/10))。 - 数据分发:所有模拟数据写入
ApplicationWindow的property real属性中,例如roll、pitch、airspeed、altitude、climbRate等。 - 数据消费:各仪表组件(
Adi、Alt、Asi、Vsi等)通过属性绑定(如roll: root.roll)自动接收最新值,无需手动刷新界面。
2、QML 属性绑定机制
QML 最强大的特性之一是属性绑定(Property Binding)。当绑定表达式中引用的属性发生变化时,绑定会自动重新求值,并更新界面。
以Vsi.qml为例:
property real climbRate: 0 // 外部传入的升降速率 Image { id: vsiHand rotation: { var clamped = Math.max(-2000, Math.min(2000, root.climbRate)) return clamped * 0.086 } }climbRate是Vsi组件对外暴露的属性,由main.qml中Vsi { climbRate: root.climbRate }绑定传入。- 当
root.climbRate变化时,vsiHand.rotation的绑定表达式自动重新计算,指针随之旋转。 0.086是刻度系数,将-2000 ~ 2000 ft/min的输入映射到约-172° ~ 172°的指针转角,对应表盘刻度范围。
3、仪表绘制原理
每个仪表组件(如Vsi.qml)采用三层图片叠加 + 旋转变换的方式绘制:
| 图层 | 作用 | z 值 |
|---|---|---|
表盘底图(vsi_face.svg) | 显示刻度、数字、单位 | -20 |
指针(vsi_hand.svg) | 随数据旋转,指示当前值 | -10 |
表壳(vsi_case.svg) | 覆盖指针转轴,增强立体感 | 10 |
关键代码:
// 自适应缩放:以 240x240 为基准,按实际尺寸等比缩放 readonly property real origW: 240 readonly property real origH: 240 property real s: Math.min(width / origW, height / origH) Item { width: root.origW height: root.origH scale: root.s anchors.centerIn: parent // 三个 Image 图层叠加 }scale: root.s实现仪表随容器大小自适应缩放,保证在不同窗口尺寸下不变形。- 指针旋转使用
transformOrigin: Item.Center,以图片中心为旋转轴,符合仪表指针的物理行为。
4、定时刷新与实时性
main.qml中的Timer是数据驱动的核心:
Timer { id: testTimer interval: 50 // 50ms 刷新一次,即 20 FPS repeat: true running: true onTriggered: { if (!root.autoMode) return const dt = interval / 1000.0 root.realTime += dt const t = root.realTime // 用正弦函数模拟飞行数据变化 root.roll = 180.0 * Math.sin(t / 10.0) root.pitch = 90.0 * Math.sin(t / 20.0) root.airspeed = 125.0 * Math.sin(t / 40.0) + 125.0 root.altitude = 9000.0 * Math.sin(t / 40.0) + 9000.0 root.climbRate = 650.0 * Math.sin(t / 20.0) // ... 其余参数同理 } }- 刷新频率:50ms 间隔对应 20 FPS,兼顾流畅度与 CPU 占用。
- 数据平滑:正弦函数保证数据连续变化,指针不会出现跳变。
- 手动模式:点击控制面板的按钮可切换
autoMode,关闭自动刷新后,可通过外部接口(如串口、UDP)注入真实飞行数据,实现真实场景接入。
5、扩展:接入真实飞行数据
当前使用Timer模拟数据,实际项目中只需将数据源替换为真实设备即可:
// 示例:通过信号槽接收外部数据 Connections { target: backendObj // PyQt6 暴露的 QObject function onFlightDataChanged(roll, pitch, airspeed, altitude, climbRate) { root.roll = roll root.pitch = pitch root.airspeed = airspeed root.altitude = altitude root.climbRate = climbRate } }在main.py中,可通过engine.rootContext().setContextProperty("backendObj", backend)将 Python 对象注入 QML,实现 Python 与 QML 的双向通信,从而对接真实飞控数据。
6、小结
| 模块 | 职责 |
|---|---|
main.py | 创建 QGuiApplication,加载 QML 引擎 |
main.qml | 定义窗口布局、数据属性、定时器、控制面板 |
Vsi.qml等仪表组件 | 接收数据,通过图片叠加与旋转绘制仪表 |
| 属性绑定 | 数据变化自动驱动界面刷新,无需手动更新 |
通过PyQt6 + QML 属性绑定 + 定时器驱动的组合,实现了高性能、易扩展的飞控仪表盘。后续接入真实数据时,只需替换数据源,界面层无需任何改动。