PyQt 实现飞机姿态仪源码分享
PyQt 实现飞机姿态仪源码分享
- 一、源码分享
-
- 1、源码分享
-
- 1.1、attitude_instrument.py
- 1.2、main.py
-
- 1.3、库安装
- 1.4、资源下载
- 2、效果展示
- 二、实现原理
-
- 1、 核心特性与优势
- 2、 构造函数与基本用法
-
- 2.1、 构造函数
- 2.2 、基本属性设置
- 3、在 PFD 仪表项目中的应用分析
-
- 3.1、 创建与添加项
- 3.2、 动态更新与变换
- 3.3、 性能优化建议
- 4、 常用方法详解
- 5、注意事项与常见问题
-
- 5.1 、文件路径与资源
- 5.2 、坐标系统
- 5.3 、性能考量
- 5.4 、与 QSvgWidget 的区别
- 6、完整示例:创建一个简单的 SVG 仪表指针
- 7、总结
一、源码分享
1、源码分享
1.1、attitude_instrument.py
import math
import sys
from PyQt6.QtWidgets import QGraphicsView, QGraphicsScene, QWidget
from PyQt6.QtSvgWidgets import QGraphicsSvgItem
from PyQt6.QtWidgets import QGraphicsTextItem
from PyQt6.QtGui import QFont, QColor, QTransform
from PyQt6.QtCore import Qt
M_PI = math.pi
class AttitudeInstrument(QGraphicsView):
class PressureUnit:
STD = 0
MB = 1
IN = 2
def __init__(self, parent: QWidget = None):
super().__init__(parent)
self.m_originalWidth = 300
self.m_originalHeight = 300
self.m_backZ = 0
self.m_maskZ = 100
self.m_scene = None
self.m_adi = None
self.m_alt = None
self.m_asi = None
self.m_hsi = None
self.m_vsi = None
self.m_itemBack = None
self.m_itemMask = None
self.m_scaleX = 1.0
self.m_scaleY = 1.0
self.reset()
self.m_scene = QGraphicsScene(self)
self.setScene(self.m_scene)
self.m_scene.clear()
self.m_adi = self.ADI(self.m_scene)
self.m_alt = self.ALT(self.m_scene)
self.m_asi = self.ASI(self.m_scene)
self.m_hsi = self.HSI(self.m_scene)
self.m_vsi = self.VSI(self.m_scene)
self.init()
def __del__(self):
if self.m_scene:
self.m_scene.clear()
self.reset()
del self.m_adi
del self.m_alt
del self.m_asi
del self.m_hsi
del self.m_vsi
def reset(self):
self.m_itemBack = None
self.m_itemMask = None
def reinit(self):
if self.m_scene:
self.m_scene.clear()
self.init()
def update(self):
self.updateView()
def resizeEvent(self, event):
super().resizeEvent(event)
self.reinit()
def init(self):
w = self.width()
h = self.height()
self.m_scaleX = w / self.m_originalWidth
self.m_scaleY = h / self.m_originalHeight
self.m_adi.init(self.m_scaleX, self.m_scaleY)
self.m_alt.init(self.m_scaleX, self.m_scaleY)
self.m_asi.init(self.m_scaleX, self.m_scaleY)
self.m_hsi.init(self.m_scaleX, self.m_scaleY)
self.m_vsi.init(self.m_scaleX, self.m_scaleY)
self.m_itemBack = QGraphicsSvgItem("image/pfd_back.svg")
self.m_itemBack.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemBack.setZValue(self.m_backZ)
self.m_itemBack.setTransform(QTransform.fromScale(self.m_scaleX, self.m_scaleY), True)
self.m_scene.addItem(self.m_itemBack)
self.m_itemMask = QGraphicsSvgItem("image/pfd_mask.svg")
self.m_itemMask.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemMask.setZValue(self.m_maskZ)
self.m_itemMask.setTransform(QTransform.fromScale(self.m_scaleX, self.m_scaleY), True)
self.m_scene.addItem(self.m_itemMask)
self.centerOn(w / 2, h / 2)
self.updateView()
def updateView(self):
w = self.width()
h = self.height()
self.m_scaleX = w / self.m_originalWidth
self.m_scaleY = h / self.m_originalHeight
self.m_adi.update(self.m_scaleX, self.m_scaleY)
self.m_alt.update(self.m_scaleX, self.m_scaleY)
self.m_asi.update(self.m_scaleX, self.m_scaleY)
self.m_hsi.update(self.m_scaleX, self.m_scaleY)
self.m_vsi.update(self.m_scaleX, self.m_scaleY)
self.m_scene.update()
# 对外API
def setRoll(self, roll: float):
self.m_adi.setRoll(roll)
def setPitch(self, pitch: float):
self.m_adi.setPitch(pitch)
def setFlightPathMarker(self, aoa: float, sideslip: float, visible=True):
self.m_adi.setFlightPathMarker(aoa, sideslip, visible)
def setSlipSkid(self, slipSkid: float):
self.m_adi.setSlipSkid(slipSkid)
def setTurnRate(self, turnRate: float):
self.m_adi.setTurnRate(turnRate)
def setBarH(self, barH: float, visible=True):
self.m_adi.setBarH(barH, visible)
def setBarV(self, barV: float, visible=True):
self.m_adi.setBarV(barV, visible)
def setDotH(self, dotH: float, visible=True):
self.m_adi.setDotH(dotH, visible)
def setDotV(self, dotV: float, visible=True):
self.m_adi.setDotV(dotV, visible)
def setAltitude(self, alt: float):
self.m_alt.setAltitude(alt)
def setPressure(self, press: float, unit: int):
self.m_alt.setPressure(press, unit)
def setAirspeed(self, spd: float):
self.m_asi.setAirspeed(spd)
def setMachNo(self, mach: float):
self.m_asi.setMachNo(mach)
def setHeading(self, hdg: float):
self.m_hsi.setHeading(hdg)
def setClimbRate(self, vs: float):
self.m_vsi.setClimbRate(vs)
# ===================== ADI 地平仪 =====================
class ADI:
def __init__(self, scene: QGraphicsScene):
self.m_scene = scene
self.m_itemBack = None
self.m_itemLadd = None
self.m_itemRoll = None
self.m_itemSlip = None
self.m_itemTurn = None
self.m_itemPath = None
self.m_itemMark = None
self.m_itemBarH = None
self.m_itemBarV = None
self.m_itemDotH = None
self.m_itemDotV = None
self.m_itemMask = None
self.m_itemScaleH = None
self.m_itemScaleV = None
self.m_roll = 0.0
self.m_pitch = 0.0
self.m_angleOfAttack = 0.0
self.m_sideslipAngle = 0.0
self.m_slipSkid = 0.0
self.m_turnRate = 0.0
self.m_barH = 0.0
self.m_barV = 0.0
self.m_dotH = 0.0
self.m_dotV = 0.0
self.m_pathValid = True
self.m_pathVisible = True
self.m_barHVisible = True
self.m_barVVisible = True
self.m_dotHVisible = True
self.m_dotVVisible = True
self.m_scaleX = 1.0
self.m_scaleY = 1.0
self.m_originalPixPerDeg = 3.0
self.m_deltaLaddBack_max = 52.5
self.m_deltaLaddBack_min = -52.5
self.m_maxSlipDeflection = 20.0
self.m_maxTurnDeflection = 55.0
self.m_maxBarsDeflection = 40.0
self.m_maxDotsDeflection = 50.0
self.m_originalAdiCtr = (150.0, 125.0)
self.m_originalBackPos = (45.0, -85.0)
self.m_originalLaddPos = (110.0, -175.0)
self.m_originalRollPos = (45.0, 20.0)
self.m_originalSlipPos = (145.5, 68.5)
self.m_originalTurnPos = (142.5, 206.0)
self.m_originalPathPos = (135.0, 113.0)
self.m_originalBarHPos = (149.0, 85.0)
self.m_originalBarVPos = (110.0, 124.0)
self.m_originalDotHPos = (145.0, 188.0)
self.m_originalDotVPos = (213.0, 120.0)
self.m_backZ = 10
self.m_laddZ = 20
self.m_rollZ = 30
self.m_slipZ = 40
self.m_pathZ = 40
self.m_barsZ = 50
self.m_dotsZ = 50
self.m_scalesZ = 51
self.m_maskZ = 60
self.m_turnZ = 70
self.reset()
def reset(self):
self.m_itemBack = self.m_itemLadd = self.m_itemRoll = self.m_itemSlip = None
self.m_itemTurn = self.m_itemPath = self.m_itemMark = self.m_itemBarH = None
self.m_itemBarV = self.m_itemDotH = self.m_itemDotV = self.m_itemMask = None
self.m_itemScaleH = self.m_itemScaleV = None
self.m_roll = self.m_pitch = self.m_angleOfAttack = self.m_sideslipAngle = 0.0
self.m_slipSkid = self.m_turnRate = self.m_barH = self.m_barV = self.m_dotH = self.m_dotV = 0.0
self.m_pathValid = self.m_pathVisible = self.m_barHVisible = self.m_barVVisible = self.m_dotHVisible = self.m_dotVVisible = True
def init(self, scaleX: float, scaleY: float):
self.m_scaleX = scaleX
self.m_scaleY = scaleY
self.reset()
self.m_itemBack = QGraphicsSvgItem("image/pfd_adi_back.svg")
self.m_itemBack.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemBack.setZValue(self.m_backZ)
self.m_itemBack.setTransform(QTransform.fromScale(scaleX, scaleY), True)
ox = self.m_originalAdiCtr[0] - self.m_originalBackPos[0]
oy = self.m_originalAdiCtr[1] - self.m_originalBackPos[1]
self.m_itemBack.setTransformOriginPoint(ox, oy)
self.m_itemBack.moveBy(scaleX * self.m_originalBackPos[0], scaleY * self.m_originalBackPos[1])
self.m_scene.addItem(self.m_itemBack)
self.m_itemLadd = QGraphicsSvgItem("image/pfd_adi_ladd.svg")
self.m_itemLadd.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemLadd.setZValue(self.m_laddZ)
self.m_itemLadd.setTransform(QTransform.fromScale(scaleX, scaleY), True)
ox = self.m_originalAdiCtr[0] - self.m_originalLaddPos[0]
oy = self.m_originalAdiCtr[1] - self.m_originalLaddPos[1]
self.m_itemLadd.setTransformOriginPoint(ox, oy)
self.m_itemLadd.moveBy(scaleX * self.m_originalLaddPos[0], scaleY * self.m_originalLaddPos[1])
self.m_scene.addItem(self.m_itemLadd)
self.m_itemRoll = QGraphicsSvgItem("image/pfd_adi_roll.svg")
self.m_itemRoll.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemRoll.setZValue(self.m_rollZ)
self.m_itemRoll.setTransform(QTransform.fromScale(scaleX, scaleY), True)
ox = self.m_originalAdiCtr[0] - self.m_originalRollPos[0]
oy = self.m_originalAdiCtr[1] - self.m_originalRollPos[1]
self.m_itemRoll.setTransformOriginPoint(ox, oy)
self.m_itemRoll.moveBy(scaleX * self.m_originalRollPos[0], scaleY * self.m_originalRollPos[1])
self.m_scene.addItem(self.m_itemRoll)
self.m_itemSlip = QGraphicsSvgItem("image/pfd_adi_slip.svg")
self.m_itemSlip.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemSlip.setZValue(self.m_slipZ)
self.m_itemSlip.setTransform(QTransform.fromScale(scaleX, scaleY), True)
ox = self.m_originalAdiCtr[0] - self.m_originalSlipPos[0]
oy = self.m_originalAdiCtr[1] - self.m_originalSlipPos[1]
self.m_itemSlip.setTransformOriginPoint(ox, oy)
self.m_itemSlip.moveBy(scaleX * self.m_originalSlipPos[0], scaleY * self.m_originalSlipPos[1])
self.m_scene.addItem(self.m_itemSlip)
self.m_itemTurn = QGraphicsSvgItem("image/pfd_adi_turn.svg")
self.m_itemTurn.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemTurn.setZValue(self.m_turnZ)
self.m_itemTurn.setTransform(QTransform.fromScale(scaleX, scaleY), True)
self.m_itemTurn.moveBy(scaleX * self.m_originalTurnPos[0], scaleY * self.m_originalTurnPos[1])
self.m_scene.addItem(self.m_itemTurn)
self.m_itemPath = QGraphicsSvgItem("image/pfd_adi_path.svg")
self.m_itemPath.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemPath.setZValue(self.m_pathZ)
self.m_itemPath.setTransform(QTransform.fromScale(scaleX, scaleY), True)
self.m_itemPath.moveBy(scaleX * self.m_originalPathPos[0], scaleY * self.m_originalPathPos[1])
self.m_scene.addItem(self.m_itemPath)
self.m_itemMark = QGraphicsSvgItem("image/pfd_adi_mark.svg")
self.m_itemMark.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemMark.setZValue(self.m_pathZ)
self.m_itemMark.setTransform(QTransform.fromScale(scaleX, scaleY), True)
self.m_itemMark.moveBy(scaleX * self.m_originalPathPos[0], scaleY * self.m_originalPathPos[1])
self.m_scene.addItem(self.m_itemMark)
self.m_itemBarH = QGraphicsSvgItem("image/pfd_adi_barh.svg")
self.m_itemBarH.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemBarH.setZValue(self.m_barsZ)
self.m_itemBarH.setTransform(QTransform.fromScale(scaleX, scaleY), True)
self.m_itemBarH.moveBy(scaleX * self.m_originalBarHPos[0], scaleY * self.m_originalBarHPos[1])
self.m_scene.addItem(self.m_itemBarH)
self.m_itemBarV = QGraphicsSvgItem("image/pfd_adi_barv.svg")
self.m_itemBarV.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemBarV.setZValue(self.m_barsZ)
self.m_itemBarV.setTransform(QTransform.fromScale(scaleX, scaleY), True)
self.m_itemBarV.moveBy(scaleX * self.m_originalBarVPos[0], scaleY * self.m_originalBarVPos[1])
self.m_scene.addItem(self.m_itemBarV)
self.m_itemDotH = QGraphicsSvgItem("image/pfd_adi_doth.svg")
self.m_itemDotH.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemDotH.setZValue(self.m_dotsZ)
self.m_itemDotH.setTransform(QTransform.fromScale(scaleX, scaleY), True)
self.m_itemDotH.moveBy(scaleX * self.m_originalDotHPos[0], scaleY * self.m_originalDotHPos[1])
self.m_scene.addItem(self.m_itemDotH)
self.m_itemDotV = QGraphicsSvgItem("image/pfd_adi_dotv.svg")
self.m_itemDotV.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemDotV.setZValue(self.m_dotsZ)
self.m_itemDotV.setTransform(QTransform.fromScale(scaleX, scaleY), True)
self.m_itemDotV.moveBy(scaleX * self.m_originalDotVPos[0], scaleY * self.m_originalDotVPos[1])
self.m_scene.addItem(self.m_itemDotV)
self.m_itemMask = QGraphicsSvgItem("image/pfd_adi_mask.svg")
self.m_itemMask.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemMask.setZValue(self.m_maskZ)
self.m_itemMask.setTransform(QTransform.fromScale(scaleX, scaleY), True)
self.m_scene.addItem(self.m_itemMask)
self.update(scaleX, scaleY)
def update(self, scaleX: float, scaleY: float):
self.m_scaleX = scaleX
self.m_scaleY = scaleY
delta = self.m_originalPixPerDeg * self.m_pitch
roll_rad = M_PI * self.m_roll / 180.0
sinRoll = math.sin(roll_rad)
cosRoll = math.cos(roll_rad)
self.m_itemBack.setRotation(-self.m_roll)
self.m_itemLadd.setRotation(-self.m_roll)
self.m_itemRoll.setRotation(-self.m_roll)
self.m_itemSlip.setRotation(-self.m_roll)
# 俯仰位移
deltaLB = max(min(delta, self.m_deltaLaddBack_max), self.m_deltaLaddBack_min)
backDx = self.m_scaleX * deltaLB * sinRoll
backDy = self.m_scaleY * deltaLB * cosRoll
self.m_itemBack.setPos(self.m_originalBackPos[0]*scaleX + backDx, self.m_originalBackPos[1]*scaleY + backDy)
laddDx = self.m_scaleX * delta * sinRoll
laddDy = self.m_scaleY * delta * cosRoll
self.m_itemLadd.setPos(self.m_originalLaddPos[0]*scaleX + laddDx, self.m_originalLaddPos[1]*scaleY + laddDy)
# 侧滑小球
slipOff = self.m_maxSlipDeflection * self.m_slipSkid
slipDx = self.m_scaleX * slipOff * cosRoll
slipDy = -self.m_scaleY * slipOff * sinRoll
self.m_itemSlip.setPos(self.m_originalSlipPos[0]*scaleX + slipDx, self.m_originalSlipPos[1]*scaleY + slipDy)
# 转弯速率指示器
turnDx = self.m_scaleX * self.m_maxTurnDeflection * self.m_turnRate
self.m_itemTurn.setX(self.m_originalTurnPos[0]*scaleX + turnDx)
# 飞行航迹矢量
pathDx = self.m_scaleX * self.m_originalPixPerDeg * self.m_sideslipAngle
pathDy = self.m_scaleY * self.m_originalPixPerDeg * self.m_angleOfAttack
self.m_itemPath.setPos(self.m_originalPathPos[0]*scaleX + pathDx, self.m_originalPathPos[1]*scaleY + pathDy)
self.m_itemPath.setVisible(self.m_pathVisible)
self.m_itemMark.setVisible(self.m_pathVisible and not self.m_pathValid)
# 偏差杆
hBarDx = self.m_scaleX * self.m_maxBarsDeflection * self.m_barH
self.m_itemBarH.setX(self.m_originalBarHPos[0]*scaleX + hBarDx)
self.m_itemBarH.setVisible(self.m_barHVisible)
vBarDy = self.m_scaleY * self.m_maxBarsDeflection * self.m_barV
self.m_itemBarV.setY(self.m_originalBarVPos[1]*scaleY + vBarDy)
self.m_itemBarV.setVisible(self.m_barVVisible)
# 偏差点
hDotDx = self.m_scaleX * self.m_maxDotsDeflection * self.m_dotH
self.m_itemDotH.setX(self.m_originalDotHPos[0]*scaleX + hDotDx)
self.m_itemDotH.setVisible(self.m_dotHVisible)
vDotDy = self.m_scaleY * self.m_maxDotsDeflection * self.m_dotV
self.m_itemDotV.setY(self.m_originalDotVPos[1]*scaleY + vDotDy)
self.m_itemDotV.setVisible(self.m_dotVVisible)
def setRoll(self, roll: float):
self.m_roll = max(min(roll, 180.0), -180.0)
def setPitch(self, pitch: float):
self.m_pitch = max(min(pitch, 90.0), -90.0)
def setFlightPathMarker(self, aoa: float, sideslip: float, visible=True):
self.m_angleOfAttack = aoa
self.m_sideslipAngle = sideslip
self.m_pathValid = True
if not (-15 <= aoa <= 15 and -10 <= sideslip <= 10):
self.m_pathValid = False
self.m_pathVisible = visible
def setSlipSkid(self, slipSkid: float):
self.m_slipSkid = max(min(slipSkid, 1.0), -1.0)
def setTurnRate(self, turnRate: float):
self.m_turnRate = max(min(turnRate, 1.0), -1.0)
def setBarH(self, barH: float, visible=True):
self.m_barH = max(min(barH, 1.0), -1.0)
self.m_barHVisible = visible
def setBarV(self, barV: float, visible=True):
self.m_barV = max(min(barV, 1.0), -1.0)
self.m_barVVisible = visible
def setDotH(self, dotH: float, visible=True):
self.m_dotH = max(min(dotH, 1.0), -1.0)
self.m_dotHVisible = visible
def setDotV(self, dotV: float, visible=True):
self.m_dotV = max(min(dotV, 1.0), -1.0)
self.m_dotVVisible = visible
# ===================== ALT 高度表 =====================
class ALT:
def __init__(self, scene):
self.m_scene = scene
# 图元对象
self.m_itemBack = None
self.m_itemScale1 = None
self.m_itemScale2 = None
self.m_itemLabel1 = None
self.m_itemLabel2 = None
self.m_itemLabel3 = None
self.m_itemGround = None
self.m_itemFrame = None
self.m_itemAltitude = None
self.m_itemPressure = None
# 文字颜色
self.m_frameTextColor = QColor(255, 255, 255)
self.m_pressTextColor = QColor(0, 255, 0)
self.m_labelsColor = QColor(255, 255, 255)
# 字体定义,区分Windows/其他系统
self.m_frameTextFont = QFont("Courier")
self.m_labelsFont = QFont("Courier")
if sys.platform == "win32":
self.m_frameTextFont.setPointSizeF(9.0)
self.m_labelsFont.setPointSizeF(7.0)
else:
self.m_frameTextFont.setPointSizeF(10.0)
self.m_labelsFont.setPointSizeF(8.0)
self.m_frameTextFont.setStretch(QFont.Stretch.Condensed)
self.m_frameTextFont.setWeight(QFont.Weight.Bold)
self.m_labelsFont.setStretch(QFont.Stretch.Condensed)
self.m_labelsFont.setWeight(QFont.Weight.Bold)
# 业务数值
self.m_altitude = 0.0
self.m_pressure = 0.0
self.m_pressureUnit = 0
# 刻度位移缓存
self.m_scale1DeltaY_new = 0.0
self.m_scale1DeltaY_old = 0.0
self.m_scale2DeltaY_new = 0.0
self.m_scale2DeltaY_old = 0.0
self.m_groundDeltaY_new = 0.0
self.m_groundDeltaY_old = 0.0
self.m_labelsDeltaY_new = 0.0
self.m_labelsDeltaY_old = 0.0
# 缩放
self.m_scaleX = 1.0
self.m_scaleY = 1.0
# 基准常量
self.m_originalPixPerAlt = 0.150
self.m_originalScaleHeight = 300.0
self.m_originalLabelsX = 250.0
self.m_originalLabel1Y = 50.0
self.m_originalLabel2Y = 125.0
self.m_originalLabel3Y = 200.0
# 图元原始坐标
self.m_originalBackPos = (231.0, 37.5)
self.m_originalScale1Pos = (231.0, -174.5)
self.m_originalScale2Pos = (231.0, -474.5)
self.m_originalGroundPos = (231.5, 124.5)
self.m_originalFramePos = (225.0, 110.0)
self.m_originalAltitudeCtr = (254.0, 126.0)
self.m_originalPressureCtr = (254.0, 225.0)
# Z层级
self.m_backZ = 70
self.m_scaleZ = 77
self.m_labelsZ = 78
self.m_groundZ = 79
self.m_frameZ = 110
self.m_frameTextZ = 120
self.reset()
def reset(self):
# 清空所有图元
self.m_itemBack = None
self.m_itemScale1 = None
self.m_itemScale2 = None
self.m_itemLabel1 = None
self.m_itemLabel2 = None
self.m_itemLabel3 = None
self.m_itemGround = None
self.m_itemFrame = None
self.m_itemAltitude = None
self.m_itemPressure = None
self.m_altitude = 0.0
self.m_pressure = 0.0
self.m_pressureUnit = 0
self.m_scale1DeltaY_new = 0.0
self.m_scale1DeltaY_old = 0.0
self.m_scale2DeltaY_new = 0.0
self.m_scale2DeltaY_old = 0.0
self.m_groundDeltaY_new = 0.0
self.m_groundDeltaY_old = 0.0
self.m_labelsDeltaY_new = 0.0
self.m_labelsDeltaY_old = 0.0
def init(self, scaleX: float, scaleY: float):
self.m_scaleX = scaleX
self.m_scaleY = scaleY
self.reset()
# 1. 背景
self.m_itemBack = QGraphicsSvgItem("image/pfd_alt_back.svg")
self.m_itemBack.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemBack.setZValue(self.m_backZ)
self.m_itemBack.setTransform(QTransform.fromScale(scaleX, scaleY), True)
bx = scaleX * self.m_originalBackPos[0]
by = scaleY * self.m_originalBackPos[1]
self.m_itemBack.moveBy(bx, by)
self.m_scene.addItem(self.m_itemBack)
# 2. 刻度1
self.m_itemScale1 = QGraphicsSvgItem("image/pfd_alt_scale.svg")
self.m_itemScale1.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemScale1.setZValue(self.m_scaleZ)
self.m_itemScale1.setTransform(QTransform.fromScale(scaleX, scaleY), True)
s1x = scaleX * self.m_originalScale1Pos[0]
s1y = scaleY * self.m_originalScale1Pos[1]
self.m_itemScale1.moveBy(s1x, s1y)
self.m_scene.addItem(self.m_itemScale1)
# 3. 刻度2
self.m_itemScale2 = QGraphicsSvgItem("image/pfd_alt_scale.svg")
self.m_itemScale2.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemScale2.setZValue(self.m_scaleZ)
self.m_itemScale2.setTransform(QTransform.fromScale(scaleX, scaleY), True)
s2x = scaleX * self.m_originalScale2Pos[0]
s2y = scaleY * self.m_originalScale2Pos[1]
self.m_itemScale2.moveBy(s2x, s2y)
self.m_scene.addItem(self.m_itemScale2)
# 4. 刻度标签Label1
self.m_itemLabel1 = QGraphicsTextItem("99999")
self.m_itemLabel1.setCacheMode(QGraphicsTextItem.CacheMode.NoCache)
self.m_itemLabel1.setZValue(self.m_labelsZ)
self.m_itemLabel1.setDefaultTextColor(self.m_labelsColor)
self.m_itemLabel1.setFont(self.m_labelsFont)
self.m_itemLabel1.setTransform(QTransform.fromScale(scaleX, scaleY), True)
lw1 = self.m_itemLabel1.boundingRect().width()
lh1 = self.m_itemLabel1.boundingRect().height()
l1x = scaleX * (self.m_originalLabelsX - lw1 / 2.0)
l1y = scaleY * (self.m_originalLabel1Y - lh1 / 2.0)
self.m_itemLabel1.moveBy(l1x, l1y)
self.m_scene.addItem(self.m_itemLabel1)
# Label2
self.m_itemLabel2 = QGraphicsTextItem("99999")
self.m_itemLabel2.setCacheMode(QGraphicsTextItem.CacheMode.NoCache)
self.m_itemLabel2.setZValue(self.m_labelsZ)
self.m_itemLabel2.setDefaultTextColor(self.m_labelsColor)
self.m_itemLabel2.setFont(self.m_labelsFont)
self.m_itemLabel2.setTransform(QTransform.fromScale(scaleX, scaleY), True)
lw2 = self.m_itemLabel2.boundingRect().width()
lh2 = self.m_itemLabel2.boundingRect().height()
l2x = scaleX * (self.m_originalLabelsX - lw2 / 2.0)
l2y = scaleY * (self.m_originalLabel2Y - lh2 / 2.0)
self.m_itemLabel2.moveBy(l2x, l2y)
self.m_scene.addItem(self.m_itemLabel2)
# Label3
self.m_itemLabel3 = QGraphicsTextItem("99999")
self.m_itemLabel3.setCacheMode(QGraphicsTextItem.CacheMode.NoCache)
self.m_itemLabel3.setZValue(self.m_labelsZ)
self.m_itemLabel3.setDefaultTextColor(self.m_labelsColor)
self.m_itemLabel3.setFont(self.m_labelsFont)
self.m_itemLabel3.setTransform(QTransform.fromScale(scaleX, scaleY), True)
lw3 = self.m_itemLabel3.boundingRect().width()
lh3 = self.m_itemLabel3.boundingRect().height()
l3x = scaleX * (self.m_originalLabelsX - lw3 / 2.0)
l3y = scaleY * (self.m_originalLabel3Y - lh3 / 2.0)
self.m_itemLabel3.moveBy(l3x, l3y)
self.m_scene.addItem(self.m_itemLabel3)
# 地面遮挡
self.m_itemGround = QGraphicsSvgItem("image/pfd_alt_ground.svg")
self.m_itemGround.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemGround.setZValue(self.m_groundZ)
self.m_itemGround.setTransform(QTransform.fromScale(scaleX, scaleY), True)
gx = scaleX * self.m_originalGroundPos[0]
gy = scaleY * self.m_originalGroundPos[1]
self.m_itemGround.moveBy(gx, gy)
self.m_scene.addItem(self.m_itemGround)
# 外层边框
self.m_itemFrame = QGraphicsSvgItem("image/pfd_alt_frame.svg")
self.m_itemFrame.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemFrame.setZValue(self.m_frameZ)
self.m_itemFrame.setTransform(QTransform.fromScale(scaleX, scaleY), True)
fx = scaleX * self.m_originalFramePos[0]
fy = scaleY * self.m_originalFramePos[1]
self.m_itemFrame.moveBy(fx, fy)
self.m_scene.addItem(self.m_itemFrame)
# 主高度数字文本
self.m_itemAltitude = QGraphicsTextItem(" 0")
self.m_itemAltitude.setCacheMode(QGraphicsTextItem.CacheMode.NoCache)
self.m_itemAltitude.setZValue(self.m_frameTextZ)
self.m_itemAltitude.setDefaultTextColor(self.m_frameTextColor)
self.m_itemAltitude.setFont(self.m_frameTextFont)
self.m_itemAltitude.setTransform(QTransform.fromScale(scaleX, scaleY), True)
aw = self.m_itemAltitude.boundingRect().width()
ah = self.m_itemAltitude.boundingRect().height()
atx = scaleX * (self.m_originalAltitudeCtr[0] - aw / 2.0)
aty = scaleY * (self.m_originalAltitudeCtr[1] - ah / 2.0)
self.m_itemAltitude.moveBy(atx, aty)
self.m_scene.addItem(self.m_itemAltitude)
# 气压QNH文本
self.m_itemPressure = QGraphicsTextItem(" STD ")
self.m_itemPressure.setCacheMode(QGraphicsTextItem.CacheMode.NoCache)
self.m_itemPressure.setZValue(self.m_frameTextZ)
self.m_itemPressure.setDefaultTextColor(self.m_pressTextColor)
self.m_itemPressure.setFont(self.m_frameTextFont)
self.m_itemPressure.setTransform(QTransform.fromScale(scaleX, scaleY), True)
pw = self.m_itemPressure.boundingRect().width()
ph = self.m_itemPressure.boundingRect().height()
ptx = scaleX * (self.m_originalPressureCtr[0] - pw / 2.0)
pty = scaleY * (self.m_originalPressureCtr[1] - ph / 2.0)
self.m_itemPressure.moveBy(ptx, pty)
self.m_scene.addItem(self.m_itemPressure)
self.update(scaleX, scaleY)
def update(self, scaleX: float, scaleY: float):
self.m_scaleX = scaleX
self.m_scaleY = scaleY
self.updateAltitude()
self.updatePressure()
# 缓存旧位移值
self.m_scale1DeltaY_old = self.m_scale1DeltaY_new
self.m_scale2DeltaY_old = self.m_scale2DeltaY_new
self.m_groundDeltaY_old = self.m_groundDeltaY_new
self.m_labelsDeltaY_old = self.m_labelsDeltaY_new
def setAltitude(self, altitude: float):
self.m_altitude = altitude
if self.m_altitude < 0.0:
self.m_altitude = 0.0
elif self.m_altitude > 99999.0:
self.m_altitude = 99999.0
def setPressure(self, pressure: float, pressureUnit: int):
self.m_pressure = pressure
if self.m_pressure < 0.0:
self.m_pressure = 0.0
elif self.m_pressure > 2000.0:
self.m_pressure = 2000.0
self.m_pressureUnit = 0
if pressureUnit == 1:
self.m_pressureUnit = 1
elif pressureUnit == 2:
self.m_pressureUnit = 2
def updateAltitude(self):
# 5位空格填充
self.m_itemAltitude.setPlainText(f"{self.m_altitude:5.0f}")
self.updateScale()
self.updateScaleLabels()
def updatePressure(self):
if self.m_pressureUnit == 0:
self.m_itemPressure.setPlainText(" STD ")
elif self.m_pressureUnit == 1:
self.m_itemPressure.setPlainText(f"{self.m_pressure:.0f} MB")
else:
self.m_itemPressure.setPlainText(f"{self.m_pressure:.2f} IN")
def updateScale(self):
self.m_scale1DeltaY_new = self.m_scaleY * self.m_originalPixPerAlt * self.m_altitude
self.m_scale2DeltaY_new = self.m_scale1DeltaY_new
self.m_groundDeltaY_new = self.m_scale1DeltaY_new
scaleSingleHeight = self.m_scaleY * self.m_originalScaleHeight
scaleDoubleHeight = scaleSingleHeight * 2.0
# 循环滚动偏移
while self.m_scale1DeltaY_new > scaleSingleHeight + self.m_scaleY * 74.5:
self.m_scale1DeltaY_new -= scaleDoubleHeight
while self.m_scale2DeltaY_new > scaleDoubleHeight + self.m_scaleY * 74.5:
self.m_scale2DeltaY_new -= scaleDoubleHeight
if self.m_groundDeltaY_new > self.m_scaleY * 100.0:
self.m_groundDeltaY_new = self.m_scaleY * 100.0
# 仅增量移动
self.m_itemScale1.moveBy(0.0, self.m_scale1DeltaY_new - self.m_scale1DeltaY_old)
self.m_itemScale2.moveBy(0.0, self.m_scale2DeltaY_new - self.m_scale2DeltaY_old)
self.m_itemGround.moveBy(0.0, self.m_groundDeltaY_new - self.m_groundDeltaY_old)
def updateScaleLabels(self):
tmp = math.floor(self.m_altitude + 0.5)
alt = int(tmp - (tmp % 500))
alt1 = alt + 500.0
alt2 = float(alt)
alt3 = alt - 500.0
self.m_labelsDeltaY_new = self.m_scaleY * self.m_originalPixPerAlt * self.m_altitude
while self.m_labelsDeltaY_new > self.m_scaleY * 37.5:
self.m_labelsDeltaY_new -= self.m_scaleY * 75.0
if self.m_labelsDeltaY_new < 0.0 and self.m_altitude > alt2:
alt1 += 500.0
alt2 += 500.0
alt3 += 500.0
dy = self.m_labelsDeltaY_new - self.m_labelsDeltaY_old
self.m_itemLabel1.moveBy(0.0, dy)
self.m_itemLabel2.moveBy(0.0, dy)
self.m_itemLabel3.moveBy(0.0, dy)
# 标签显隐与文字
if 0 < alt1 <= 100000:
self.m_itemLabel1.setVisible(True)
self.m_itemLabel1.setPlainText(f"{alt1:5.0f}")
else:
self.m_itemLabel1.setVisible(False)
if 0 < alt2 <= 100000:
self.m_itemLabel2.setVisible(True)
self.m_itemLabel2.setPlainText(f"{alt2:5.0f}")
else:
self.m_itemLabel2.setVisible(False)
if 0 < alt3 <= 100000:
self.m_itemLabel3.setVisible(True)
self.m_itemLabel3.setPlainText(f"{alt3:5.0f}")
else:
self.m_itemLabel3.setVisible(False)
# ===================== ASI 空速表 =====================
class ASI:
def __init__(self, scene):
self.m_scene = scene
# 图元对象
self.m_itemBack = None
self.m_itemScale1 = None
self.m_itemScale2 = None
self.m_itemLabel1 = None
self.m_itemLabel2 = None
self.m_itemLabel3 = None
self.m_itemLabel4 = None
self.m_itemLabel5 = None
self.m_itemLabel6 = None
self.m_itemLabel7 = None
self.m_itemFrame = None
self.m_itemAirspeed = None
self.m_itemMachNo = None
# 文字颜色
self.m_frameTextColor = QColor(255, 255, 255)
self.m_labelsColor = QColor(255, 255, 255)
# 字体区分 Windows / 其他系统
self.m_frameTextFont = QFont("Courier")
self.m_labelsFont = QFont("Courier")
if sys.platform == "win32":
self.m_frameTextFont.setPointSizeF(9.0)
self.m_labelsFont.setPointSizeF(7.0)
else:
self.m_frameTextFont.setPointSizeF(10.0)
self.m_labelsFont.setPointSizeF(8.0)
self.m_frameTextFont.setStretch(QFont.Stretch.Condensed)
self.m_frameTextFont.setWeight(QFont.Weight.Bold)
self.m_labelsFont.setStretch(QFont.Stretch.Condensed)
self.m_labelsFont.setWeight(QFont.Weight.Bold)
# 业务数值
self.m_airspeed = 0.0
self.m_machNo = 0.0
# 刻度位移缓存
self.m_scale1DeltaY_new = 0.0
self.m_scale1DeltaY_old = 0.0
self.m_scale2DeltaY_new = 0.0
self.m_scale2DeltaY_old = 0.0
self.m_labelsDeltaY_new = 0.0
self.m_labelsDeltaY_old = 0.0
# 缩放系数
self.m_scaleX = 1.0
self.m_scaleY = 1.0
# 基准常量
self.m_originalPixPerSpd = 1.5
self.m_originalScaleHeight = 300.0
self.m_originalLabelsX = 43.0
self.m_originalLabel1Y = 35.0
self.m_originalLabel2Y = 65.0
self.m_originalLabel3Y = 95.0
self.m_originalLabel4Y = 125.0
self.m_originalLabel5Y = 155.0
self.m_originalLabel6Y = 185.0
self.m_originalLabel7Y = 215.0
# 图元原始坐标
self.m_originalBackPos = (25.0, 37.5)
self.m_originalScale1Pos = (56.0, -174.5)
self.m_originalScale2Pos = (56.0, -474.5)
self.m_originalFramePos = (0.0, 110.0)
self.m_originalAirspeedCtr = (40.0, 126.0)
self.m_originalMachNoCtr = (43.0, 225.0)
# Z层级
self.m_backZ = 70
self.m_scaleZ = 80
self.m_labelsZ = 90
self.m_frameZ = 110
self.m_frameTextZ = 120
self.reset()
def reset(self):
# 清空所有图元引用
self.m_itemBack = None
self.m_itemScale1 = None
self.m_itemScale2 = None
self.m_itemLabel1 = None
self.m_itemLabel2 = None
self.m_itemLabel3 = None
self.m_itemLabel4 = None
self.m_itemLabel5 = None
self.m_itemLabel6 = None
self.m_itemLabel7 = None
self.m_itemFrame = None
self.m_itemAirspeed = None
self.m_itemMachNo = None
self.m_airspeed = 0.0
self.m_machNo = 0.0
self.m_scale1DeltaY_new = 0.0
self.m_scale1DeltaY_old = 0.0
self.m_scale2DeltaY_new = 0.0
self.m_scale2DeltaY_old = 0.0
self.m_labelsDeltaY_new = 0.0
self.m_labelsDeltaY_old = 0.0
def init(self, scaleX: float, scaleY: float):
self.m_scaleX = scaleX
self.m_scaleY = scaleY
self.reset()
# 1. 背景
self.m_itemBack = QGraphicsSvgItem("image/pfd_asi_back.svg")
self.m_itemBack.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemBack.setZValue(self.m_backZ)
self.m_itemBack.setTransform(QTransform.fromScale(scaleX, scaleY), True)
bx = scaleX * self.m_originalBackPos[0]
by = scaleY * self.m_originalBackPos[1]
self.m_itemBack.moveBy(bx, by)
self.m_scene.addItem(self.m_itemBack)
# 2. 刻度1
self.m_itemScale1 = QGraphicsSvgItem("image/pfd_asi_scale.svg")
self.m_itemScale1.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemScale1.setZValue(self.m_scaleZ)
self.m_itemScale1.setTransform(QTransform.fromScale(scaleX, scaleY), True)
s1x = scaleX * self.m_originalScale1Pos[0]
s1y = scaleY * self.m_originalScale1Pos[1]
self.m_itemScale1.moveBy(s1x, s1y)
self.m_scene.addItem(self.m_itemScale1)
# 3. 刻度2
self.m_itemScale2 = QGraphicsSvgItem("image/pfd_asi_scale.svg")
self.m_itemScale2.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemScale2.setZValue(self.m_scaleZ)
self.m_itemScale2.setTransform(QTransform.fromScale(scaleX, scaleY), True)
s2x = scaleX * self.m_originalScale2Pos[0]
s2y = scaleY * self.m_originalScale2Pos[1]
self.m_itemScale2.moveBy(s2x, s2y)
self.m_scene.addItem(self.m_itemScale2)
# 刻度标签 Label1~Label7
def create_label(y_pos):
label = QGraphicsTextItem("999")
label.setCacheMode(QGraphicsTextItem.CacheMode.NoCache)
label.setZValue(self.m_labelsZ)
label.setDefaultTextColor(self.m_labelsColor)
label.setFont(self.m_labelsFont)
label.setTransform(QTransform.fromScale(scaleX, scaleY), True)
lw = label.boundingRect().width()
lh = label.boundingRect().height()
lx = scaleX * (self.m_originalLabelsX - lw / 2.0)
ly = scaleY * (y_pos - lh / 2.0)
label.moveBy(lx, ly)
self.m_scene.addItem(label)
return label
self.m_itemLabel1 = create_label(self.m_originalLabel1Y)
self.m_itemLabel2 = create_label(self.m_originalLabel2Y)
self.m_itemLabel3 = create_label(self.m_originalLabel3Y)
self.m_itemLabel4 = create_label(self.m_originalLabel4Y)
self.m_itemLabel5 = create_label(self.m_originalLabel5Y)
self.m_itemLabel6 = create_label(self.m_originalLabel6Y)
self.m_itemLabel7 = create_label(self.m_originalLabel7Y)
# 外层边框
self.m_itemFrame = QGraphicsSvgItem("image/pfd_asi_frame.svg")
self.m_itemFrame.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemFrame.setZValue(self.m_frameZ)
self.m_itemFrame.setTransform(QTransform.fromScale(scaleX, scaleY), True)
fx = scaleX * self.m_originalFramePos[0]
fy = scaleY * self.m_originalFramePos[1]
self.m_itemFrame.moveBy(fx, fy)
self.m_scene.addItem(self.m_itemFrame)
# 空速数字文本
self.m_itemAirspeed = QGraphicsTextItem("000")
self.m_itemAirspeed.setCacheMode(QGraphicsTextItem.CacheMode.NoCache)
self.m_itemAirspeed.setZValue(self.m_frameTextZ)
self.m_itemAirspeed.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
self.m_itemAirspeed.setDefaultTextColor(self.m_frameTextColor)
self.m_itemAirspeed.setFont(self.m_frameTextFont)
self.m_itemAirspeed.setTransform(QTransform.fromScale(scaleX, scaleY), True)
aw = self.m_itemAirspeed.boundingRect().width()
ah = self.m_itemAirspeed.boundingRect().height()
atx = scaleX * (self.m_originalAirspeedCtr[0] - aw / 2.0)
aty = scaleY * (self.m_originalAirspeedCtr[1] - ah / 2.0)
self.m_itemAirspeed.moveBy(atx, aty)
self.m_scene.addItem(self.m_itemAirspeed)
# 马赫数文本
self.m_itemMachNo = QGraphicsTextItem(".000")
self.m_itemMachNo.setCacheMode(QGraphicsTextItem.CacheMode.NoCache)
self.m_itemMachNo.setZValue(self.m_frameTextZ)
self.m_itemMachNo.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
self.m_itemMachNo.setDefaultTextColor(self.m_frameTextColor)
self.m_itemMachNo.setFont(self.m_frameTextFont)
self.m_itemMachNo.setTransform(QTransform.fromScale(scaleX, scaleY), True)
mw = self.m_itemMachNo.boundingRect().width()
mh = self.m_itemMachNo.boundingRect().height()
mtx = scaleX * (self.m_originalMachNoCtr[0] - mw / 2.0)
mty = scaleY * (self.m_originalMachNoCtr[1] - mh / 2.0)
self.m_itemMachNo.moveBy(mtx, mty)
self.m_scene.addItem(self.m_itemMachNo)
self.update(scaleX, scaleY)
def update(self, scaleX: float, scaleY: float):
self.m_scaleX = scaleX
self.m_scaleY = scaleY
self.updateAirspeed()
# 保存上一帧位移
self.m_scale1DeltaY_old = self.m_scale1DeltaY_new
self.m_scale2DeltaY_old = self.m_scale2DeltaY_new
self.m_labelsDeltaY_old = self.m_labelsDeltaY_new
def setAirspeed(self, airspeed: float):
self.m_airspeed = airspeed
if self.m_airspeed < 0.0:
self.m_airspeed = 0.0
elif self.m_airspeed > 9999.0:
self.m_airspeed = 9999.0
def setMachNo(self, machNo: float):
self.m_machNo = machNo
if self.m_machNo < 0.0:
self.m_machNo = 0.0
elif self.m_machNo > 99.9:
self.m_machNo = 99.9
def updateAirspeed(self):
# 3位数字补零
self.m_itemAirspeed.setPlainText(f"{self.m_airspeed:03.0f}")
# Mach 文本格式化逻辑与C++完全一致
if self.m_machNo < 1.0:
mach_val = 1000.0 * self.m_machNo
self.m_itemMachNo.setPlainText(f".{mach_val:03.0f}")
else:
if self.m_machNo < 10.0:
self.m_itemMachNo.setPlainText(f"{self.m_machNo:.2f}")
else:
self.m_itemMachNo.setPlainText(f"{self.m_machNo:.1f}")
self.updateScale()
self.updateScaleLabels()
def updateScale(self):
self.m_scale1DeltaY_new = self.m_scaleY * self.m_originalPixPerSpd * self.m_airspeed
self.m_scale2DeltaY_new = self.m_scale1DeltaY_new
scaleSingleHeight = self.m_scaleY * self.m_originalScaleHeight
scaleDoubleHeight = scaleSingleHeight * 2.0
# 循环滚动偏移
while self.m_scale1DeltaY_new > scaleSingleHeight + self.m_scaleY * 74.5:
self.m_scale1DeltaY_new -= scaleDoubleHeight
while self.m_scale2DeltaY_new > scaleDoubleHeight + self.m_scaleY * 74.5:
self.m_scale2DeltaY_new -= scaleDoubleHeight
# 增量移动刻度
self.m_itemScale1.moveBy(0.0, self.m_scale1DeltaY_new - self.m_scale1DeltaY_old)
self.m_itemScale2.moveBy(0.0, self.m_scale2DeltaY_new - self.m_scale2DeltaY_old)
def updateScaleLabels(self):
self.m_labelsDeltaY_new = self.m_scaleY * self.m_originalPixPerSpd * self.m_airspeed
tmp = math.floor(self.m_airspeed + 0.5)
spd_base = int(tmp - (tmp % 20))
spd1 = spd_base + 60.0
spd2 = spd_base + 40.0
spd3 = spd_base + 20.0
spd4 = float(spd_base)
spd5 = spd_base - 20.0
spd6 = spd_base - 40.0
spd7 = spd_base - 60.0
while self.m_labelsDeltaY_new > self.m_scaleY * 15.0:
self.m_labelsDeltaY_new -= self.m_scaleY * 30.0
if self.m_labelsDeltaY_new < 0.0 and self.m_airspeed > spd4:
spd1 += 20.0
spd2 += 20.0
spd3 += 20.0
spd4 += 20.0
spd5 += 20.0
spd6 += 20.0
spd7 += 20.0
dy = self.m_labelsDeltaY_new - self.m_labelsDeltaY_old
self.m_itemLabel1.moveBy(0.0, dy)
self.m_itemLabel2.moveBy(0.0, dy)
self.m_itemLabel3.moveBy(0.0, dy)
self.m_itemLabel4.moveBy(0.0, dy)
self.m_itemLabel5.moveBy(0.0, dy)
self.m_itemLabel6.moveBy(0.0, dy)
self.m_itemLabel7.moveBy(0.0, dy)
# 标签显隐与填充文字
def set_label_text(label, val):
if 0 <= val <= 10000:
label.setVisible(True)
label.setPlainText(f"{val:3.0f}")
else:
label.setVisible(False)
set_label_text(self.m_itemLabel1, spd1)
set_label_text(self.m_itemLabel2, spd2)
set_label_text(self.m_itemLabel3, spd3)
set_label_text(self.m_itemLabel4, spd4)
set_label_text(self.m_itemLabel5, spd5)
set_label_text(self.m_itemLabel6, spd6)
set_label_text(self.m_itemLabel7, spd7)
# ===================== HSI 航向仪 =====================
class HSI:
def __init__(self, scene: QGraphicsScene):
self.m_scene = scene
self.m_itemBack = None
self.m_itemFace = None
self.m_itemMarks = None
self.m_itemFrameText = None
self.m_frameTextColor = QColor(255, 255, 255)
self.m_frameTextFont = QFont("Courier")
self.m_frameTextFont.setPointSizeF(10.0)
self.m_frameTextFont.setStretch(QFont.Stretch.Condensed)
self.m_frameTextFont.setWeight(QFont.Weight.Bold)
self.m_heading = 0.0
self.m_scaleX = 1.0
self.m_scaleY = 1.0
# 基准坐标点,和C++完全一致
self.m_originalHsiCtr = (150.0, 345.0)
self.m_originalBackPos = (60.0, 240.0)
self.m_originalFacePos = (45.0, 240.0)
self.m_originalMarksPos = (134.0, 219.0)
self.m_originalFrameTextCtr = (149.5, 227.5)
# Z轴层级
self.m_backZ = 80
self.m_faceZ = 90
self.m_marksZ = 110
self.m_frameTextZ = 120
self.reset()
def reset(self):
self.m_itemBack = None
self.m_itemFace = None
self.m_itemMarks = None
self.m_itemFrameText = None
self.m_heading = 0.0
def init(self, scaleX: float, scaleY: float):
self.m_scaleX = scaleX
self.m_scaleY = scaleY
self.reset()
# 背景
self.m_itemBack = QGraphicsSvgItem("image/pfd_hsi_back.svg")
self.m_itemBack.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemBack.setZValue(self.m_backZ)
self.m_itemBack.setTransform(QTransform.fromScale(scaleX, scaleY), True)
bx = self.m_scaleX * self.m_originalBackPos[0]
by = self.m_scaleY * self.m_originalBackPos[1]
self.m_itemBack.moveBy(bx, by)
self.m_scene.addItem(self.m_itemBack)
# 航向刻度盘面(可旋转核心)
self.m_itemFace = QGraphicsSvgItem("image/pfd_hsi_face.svg")
self.m_itemFace.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemFace.setZValue(self.m_faceZ)
self.m_itemFace.setTransform(QTransform.fromScale(scaleX, scaleY), True)
# 旋转原点 = 中心坐标 - face原始位置
originX = self.m_originalHsiCtr[0] - self.m_originalFacePos[0]
originY = self.m_originalHsiCtr[1] - self.m_originalFacePos[1]
self.m_itemFace.setTransformOriginPoint(originX, originY )
fx = self.m_scaleX * self.m_originalFacePos[0]
fy = self.m_scaleY * self.m_originalFacePos[1]
self.m_itemFace.moveBy(fx, fy)
self.m_scene.addItem(self.m_itemFace)
# 固定标记圈
self.m_itemMarks = QGraphicsSvgItem("image/pfd_hsi_marks.svg")
self.m_itemMarks.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemMarks.setZValue(self.m_marksZ)
self.m_itemMarks.setTransform(QTransform.fromScale(scaleX, scaleY), True)
mx = self.m_scaleX * self.m_originalMarksPos[0]
my = self.m_scaleY * self.m_originalMarksPos[1]
self.m_itemMarks.moveBy(mx, my)
self.m_scene.addItem(self.m_itemMarks)
# 航向数字文本
self.m_itemFrameText = QGraphicsTextItem("000")
self.m_itemFrameText.setCacheMode(QGraphicsTextItem.CacheMode.NoCache)
self.m_itemFrameText.setZValue(self.m_frameTextZ)
self.m_itemFrameText.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
self.m_itemFrameText.setDefaultTextColor(self.m_frameTextColor)
self.m_itemFrameText.setFont(self.m_frameTextFont)
self.m_itemFrameText.setTransform(QTransform.fromScale(scaleX, scaleY), True)
textW = self.m_itemFrameText.boundingRect().width()
textH = self.m_itemFrameText.boundingRect().height()
tx = self.m_scaleX * (self.m_originalFrameTextCtr[0] - textW / 2.0)
ty = self.m_scaleY * (self.m_originalFrameTextCtr[1] - textH / 2.0)
self.m_itemFrameText.moveBy(tx, ty)
self.m_scene.addItem(self.m_itemFrameText)
self.update(scaleX, scaleY)
def update(self, scaleX: float, scaleY: float):
self.m_scaleX = scaleX
self.m_scaleY = scaleY
self.updateHeading()
def setHeading(self, heading: float):
self.m_heading = heading
# 角度归一化 0~360
while self.m_heading < 0.0:
self.m_heading += 360.0
while self.m_heading > 360.0:
self.m_heading -= 360.0
def updateHeading(self):
# 盘面反向旋转 -heading
self.m_itemFace.setRotation(-self.m_heading)
# 四舍五入取整,3位补零
fHeading = math.floor(self.m_heading + 0.5)
self.m_itemFrameText.setPlainText(f"{fHeading:03.0f}")
# ===================== VSI 升降速度表 =====================
class VSI:
def __init__(self, scene):
self.m_scene = scene
self.m_itemScale = None
self.m_itemArrow = None
self.m_climbRate = 0.0
self.m_arrowDeltaY_new = 0.0
self.m_arrowDeltaY_old = 0.0
self.m_scaleX = 1.0
self.m_scaleY = 1.0
# 基准常量
self.m_originalMarkeHeight = 75.0
self.m_originalPixPerSpd1 = 30.0
self.m_originalPixPerSpd2 = 20.0
self.m_originalPixPerSpd4 = 5.0
# 原始坐标
self.m_originalScalePos = (275.0, 50.0)
self.m_originalArrowPos = (284.0, 124.0)
# Z层级
self.m_scaleZ = 70
self.m_arrowZ = 80
self.reset()
def reset(self):
self.m_itemScale = None
self.m_itemArrow = None
self.m_climbRate = 0.0
self.m_arrowDeltaY_new = 0.0
self.m_arrowDeltaY_old = 0.0
def init(self, scaleX: float, scaleY: float):
self.m_scaleX = scaleX
self.m_scaleY = scaleY
self.reset()
# 刻度底板
self.m_itemScale = QGraphicsSvgItem("image/pfd_vsi_scale.svg")
self.m_itemScale.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemScale.setZValue(self.m_scaleZ)
self.m_itemScale.setTransform(QTransform.fromScale(scaleX, scaleY), True)
sx = scaleX * self.m_originalScalePos[0]
sy = scaleY * self.m_originalScalePos[1]
self.m_itemScale.moveBy(sx, sy)
self.m_scene.addItem(self.m_itemScale)
# 指针箭头
self.m_itemArrow = QGraphicsSvgItem("image/pfd_vsi_arrow.svg")
self.m_itemArrow.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
self.m_itemArrow.setZValue(self.m_arrowZ)
self.m_itemArrow.setTransform(QTransform.fromScale(scaleX, scaleY), True)
ax = scaleX * self.m_originalArrowPos[0]
ay = scaleY * self.m_originalArrowPos[1]
self.m_itemArrow.moveBy(ax, ay)
self.m_scene.addItem(self.m_itemArrow)
self.update(scaleX, scaleY)
def update(self, scaleX: float, scaleY: float):
self.m_scaleX = scaleX
self.m_scaleY = scaleY
self.updateVSI()
self.m_arrowDeltaY_old = self.m_arrowDeltaY_new
def setClimbRate(self, climbRate: float):
self.m_climbRate = climbRate
if self.m_climbRate > 6.3:
self.m_climbRate = 6.3
elif self.m_climbRate < -6.3:
self.m_climbRate = -6.3
def updateVSI(self):
climbRateAbs = math.fabs(self.m_climbRate)
arrowDeltaY = 0.0
if climbRateAbs <= 1.0:
arrowDeltaY = self.m_originalPixPerSpd1 * climbRateAbs
elif climbRateAbs <= 2.0:
arrowDeltaY = self.m_originalPixPerSpd1 + self.m_originalPixPerSpd2 * (climbRateAbs - 1.0)
else:
arrowDeltaY = self.m_originalPixPerSpd1 + self.m_originalPixPerSpd2 + self.m_originalPixPerSpd4 * (
climbRateAbs - 2.0)
if self.m_climbRate < 0.0:
arrowDeltaY *= -1.0
self.m_arrowDeltaY_new = self.m_scaleY * arrowDeltaY
# 增量移动
self.m_itemArrow.moveBy(0.0, self.m_arrowDeltaY_old - self.m_arrowDeltaY_new)
1.2、main.py
import sys
import math
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QGraphicsView
from PyQt6.QtCore import QTimer
from attitude_instrument import AttitudeInstrument
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("PFD Instrument Demo")
self.resize(800, 790)
# 模拟UI布局,模拟ui->graphicsView
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
self.graphicsView = AttitudeInstrument()
layout.addWidget(self.graphicsView)
# 定时器
self.timer = QTimer()
self.timer.timeout.connect(self.on_timer_timeout)
self.timer.start(20) # 20ms刷新,和C++一致
# 仿真全局变量
self.m_realTime = 0.0
self.alpha = 0.0
self.beta = 0.0
self.roll = 0.0
self.pitch = 0.0
self.heading = 0.0
self.slipSkid = 0.0
self.turnRate = 0.0
self.devH = 0.0
self.devV = 0.0
self.airspeed = 0.0
self.machNo = 0.0
self.altitude = 0.0
self.pressure = 0.0
self.climbRate = 0.0
self.adf = 0.0
self.dme = 0.0
def on_timer_timeout(self):
# 时间步进 +0.01s
self.m_realTime += 10 / 1000.0
# 先更新仪表数值
self.graphicsView.setFlightPathMarker(self.alpha, self.beta)
self.graphicsView.setRoll(self.roll)
self.graphicsView.setPitch(self.pitch)
self.graphicsView.setSlipSkid(self.slipSkid)
self.graphicsView.setTurnRate(self.turnRate / 6.0)
self.graphicsView.setBarH(self.devH)
self.graphicsView.setDotH(self.devH)
self.graphicsView.setBarV(self.devV)
self.graphicsView.setDotV(self.devV)
self.graphicsView.setHeading(self.heading)
self.graphicsView.setAirspeed(self.airspeed)
self.graphicsView.setMachNo(self.machNo)
self.graphicsView.setAltitude(self.altitude)
# 气压单位 IN 枚举
self.graphicsView.setPressure(self.pressure, AttitudeInstrument.PressureUnit.IN)
self.graphicsView.setClimbRate(self.climbRate / 100.0)
# 正弦仿真公式 和C++完全一致
t = self.m_realTime
self.alpha = 20.0 * math.sin(t / 10.0)
self.beta = 15.0 * math.sin(t / 10.0)
self.roll = 180.0 * math.sin(t / 10.0)
self.pitch = 90.0 * math.sin(t / 20.0)
self.heading = 360.0 * math.sin(t / 40.0)
self.slipSkid = 1.0 * math.sin(t / 10.0)
self.turnRate = 7.0 * math.sin(t / 10.0)
self.devH = 1.0 * math.sin(t / 20.0)
self.devV = 1.0 * math.sin(t / 20.0)
self.airspeed = 125.0 * math.sin(t / 40.0) + 125.0
self.altitude = 9000.0 * math.sin(t / 40.0) + 9000.0
self.pressure = 2.0 * math.sin(t / 20.0) + 30.0
self.climbRate = 650.0 * math.sin(t / 20.0)
self.machNo = self.airspeed / 650.0
self.adf = -360.0 * math.sin(t / 50.0)
self.dme = 99.0 * math.sin(t / 100.0)
# 刷新视图
self.graphicsView.update()
if __name__ == "__main__":
app = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec())
1.3、库安装

1.4、资源下载
文字顶部下载
2、效果展示



二、实现原理
主要是通过QGraphicsSvgItem加载图片绘制。
QGraphicsSvgItem 是 PyQt6 中用于在 QGraphicsScene 中显示 SVG(可缩放矢量图形)文件的核心类。它继承自 QGraphicsItem,因此可以像其他图形项一样进行变换、旋转、缩放,并参与场景的渲染和交互。SVG 格式的图形具有无损缩放、文件体积小等优点,非常适合用于仪表盘、图标、示意图等需要高保真缩放的界面元素。
1、 核心特性与优势
- 矢量图形支持:渲染的是基于 XML 的矢量图形,放大缩小不会失真。
- 轻量级:相比位图,SVG 文件通常更小,且渲染质量更高。
-
场景集成:作为
QGraphicsItem的子类,可以无缝集成到QGraphicsScene/QGraphicsView架构中,享受场景管理、碰撞检测、变换、Z 值排序等所有功能。 - 样式与交互:可通过 CSS 样式部分控制 SVG 外观,并支持鼠标事件等交互。
2、 构造函数与基本用法
2.1、 构造函数
from PyQt6.QtSvgWidgets import QGraphicsSvgItem
# 1. 创建空项,稍后设置文件
item1 = QGraphicsSvgItem()
item1.setElementId("layer1") # 可选:指定 SVG 内特定元素
item1.setSharedRenderer(renderer) # 可选:共享渲染器
# 2. 直接从文件创建
item2 = QGraphicsSvgItem("path/to/image.svg")
# 3. 指定父项(QGraphicsItem)
item3 = QGraphicsSvgItem("path/to/image.svg", parent=other_item)
2.2 、基本属性设置
# 设置 Z 值(叠放顺序)
item.setZValue(100)
# 设置缓存模式(提高性能,但静态图推荐 NoCache)
item.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache) # 或 DeviceCoordinateCache
# 设置变换(缩放、旋转、平移)
transform = QTransform()
transform.scale(2.0, 2.0) # 放大2倍
transform.rotate(45) # 旋转45度
item.setTransform(transform)
# 设置变换原点(旋转/缩放的中心点)
item.setTransformOriginPoint(50, 50) # 相对于 item 自身坐标系
# 移动项
item.moveBy(100, 50) # 相对移动
item.setPos(200, 300) # 绝对位置
3、在 PFD 仪表项目中的应用分析
回顾上文 attitude_instrument.py,QGraphicsSvgItem 被广泛用于绘制仪表的各个部件:
3.1、 创建与添加项
# 创建 SVG 项并指定文件
self.m_itemBack = QGraphicsSvgItem("image/pfd_back.svg")
# 禁用缓存以保证动态更新时正确渲染
self.m_itemBack.setCacheMode(QGraphicsSvgItem.CacheMode.NoCache)
# 设置 Z 值决定绘制顺序
self.m_itemBack.setZValue(self.m_backZ)
# 应用缩放变换(适应窗口大小)
self.m_itemBack.setTransform(QTransform.fromScale(self.m_scaleX, self.m_scaleY), True)
# 添加到场景
self.m_scene.addItem(self.m_itemBack)
3.2、 动态更新与变换
在 ADI(姿态指示器)类的 update 方法中,通过计算位移和旋转来更新 SVG 项的位置,模拟飞机姿态:
# 计算俯仰导致的位移(在 roll 旋转后的坐标系中)
delta = self.m_originalPixPerDeg * self.m_pitch
roll_rad = M_PI * self.m_roll / 180.0
sinRoll = math.sin(roll_rad)
cosRoll = math.cos(roll_rad)
# 地平线背景的位移
backDx = self.m_scaleX * delta * sinRoll
backDy = self.m_scaleY * delta * cosRoll
self.m_itemBack.setPos(self.m_originalBackPos[0]*self.m_scaleX + backDx,
self.m_originalBackPos[1]*self.m_scaleY + backDy)
# 应用旋转(整个项绕其变换原点旋转)
self.m_itemBack.setRotation(-self.m_roll)
3.3、 性能优化建议
-
缓存策略:
-
NoCache:默认,每次重绘都重新渲染 SVG。适用于动态变化的项(如仪表指针、移动的地平线)。 -
DeviceCoordinateCache:将项渲染为位图缓存,适合静态或很少变化的 SVG。在 PFD 中,因为仪表盘元素频繁移动/旋转,因此选择了NoCache。
-
-
共享渲染器:
如果多个QGraphicsSvgItem显示同一个 SVG 文件的不同部分(或同一文件),可以共享一个QSvgRenderer以减少内存占用:from PyQt6.QtSvg import QSvgRenderer renderer = QSvgRenderer("image/pfd_adi_back.svg") item1 = QGraphicsSvgItem() item1.setSharedRenderer(renderer) item2 = QGraphicsSvgItem() item2.setSharedRenderer(renderer) -
Z 值管理:
通过setZValue()控制叠放顺序。在 PFD 中,背景 Z=0,刻度盘 Z=10,指针 Z=20,遮罩 Z=100,确保视觉层次正确。
4、 常用方法详解
| 方法 | 说明 | 示例 |
|---|---|---|
setElementId(id: str) |
仅渲染 SVG 文件中指定 ID 的元素(图层) | item.setElementId("needle") |
elementId() -> str |
返回当前设置的元素 ID | print(item.elementId()) |
setSharedRenderer(renderer: QSvgRenderer) |
共享渲染器,节省内存 | 见上文 |
renderer() -> QSvgRenderer |
获取关联的渲染器 | renderer = item.renderer() |
boundingRect() -> QRectF |
返回项的边界矩形(本地坐标) | rect = item.boundingRect() |
paint(painter, option, widget) |
重写以自定义绘制(通常不需要) | 继承后重写 |
type() -> int |
返回项的类型标识(QGraphicsItem.UserType + x) |
用于自定义项类型识别 |
5、注意事项与常见问题
5.1 、文件路径与资源
- SVG 文件需在运行时可访问。在打包应用时,需将 SVG 文件包含进资源系统(
.qrc)或确保其位于可执行文件相对路径下。 - 示例中使用了相对路径
"image/pfd_back.svg",因此项目目录下需有image/文件夹存放所有 SVG 资源。
5.2 、坐标系统
-
QGraphicsSvgItem的本地坐标系原点 (0,0) 通常对应 SVG 文件的左上角。 -
通过
setTransformOriginPoint()设置变换原点,后续的setRotation()、scale()等操作将围绕该点进行。 -
在 PFD 示例中,为了精确控制旋转中心(如仪表盘中心),计算了变换原点相对于项位置的偏移:
originX = self.m_originalHsiCtr[0] - self.m_originalFacePos[0] originY = self.m_originalHsiCtr[1] - self.m_originalFacePos[1] self.m_itemFace.setTransformOriginPoint(originX, originY)
5.3 、性能考量
- 复杂的 SVG(多层、渐变、滤镜)在频繁更新时可能导致性能下降。对于实时仪表,应简化 SVG 设计,减少节点数量。
- 如果仪表有大量静态背景元素,可考虑将它们合并为一个 SVG 文件,或使用
QGraphicsPixmapItem渲染为位图以提升性能。
5.4 、与 QSvgWidget 的区别
-
QSvgWidget是一个独立的窗口部件,可直接放入布局。 -
QGraphicsSvgItem是场景中的一项,更适合复杂、可交互的图形界面(如仪表盘、绘图工具)。 - 在
QGraphicsView体系内,优先使用QGraphicsSvgItem。
6、完整示例:创建一个简单的 SVG 仪表指针
import sys
from PyQt6.QtWidgets import QApplication, QGraphicsView, QGraphicsScene
from PyQt6.QtSvgWidgets import QGraphicsSvgItem
from PyQt6.QtCore import QTimer, QPointF
import math
class SimpleGauge(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
# 背景
self.background = QGraphicsSvgItem("gauge_background.svg")
self.scene.addItem(self.background)
# 指针
self.needle = QGraphicsSvgItem("gauge_needle.svg")
self.needle.setTransformOriginPoint(50, 50) # 假设指针旋转中心在 (50,50)
self.scene.addItem(self.needle)
# 定时旋转指针
self.angle = 0
self.timer = QTimer()
self.timer.timeout.connect(self.updateNeedle)
self.timer.start(100) # 每100ms更新一次
def updateNeedle(self):
self.angle = (self.angle + 5) % 360
self.needle.setRotation(self.angle)
if __name__ == "__main__":
app = QApplication(sys.argv)
gauge = SimpleGauge()
gauge.resize(400, 400)
gauge.show()
sys.exit(app.exec())
7、总结
QGraphicsSvgItem 是 PyQt6 中处理矢量图形的利器,尤其适合需要高保真缩放、复杂层次结构的可视化应用(如航空仪表 PFD)。通过合理设置 Z 值、变换原点和缓存模式,可以构建出既美观又高效的动态界面。本文结合了实际 PFD 项目代码,详细讲解了其核心用法、性能优化技巧及常见注意事项,希望能帮助你更好地在 PyQt6 项目中使用 SVG 图形。
