PyQt 实现飞机姿态仪源码分享

AI3天前发布 beixibaobao
6 0 0

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.pyQGraphicsSvgItem 被广泛用于绘制仪表的各个部件:

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、 性能优化建议

  1. 缓存策略

    • NoCache:默认,每次重绘都重新渲染 SVG。适用于动态变化的项(如仪表指针、移动的地平线)。
    • DeviceCoordinateCache:将项渲染为位图缓存,适合静态或很少变化的 SVG。在 PFD 中,因为仪表盘元素频繁移动/旋转,因此选择了 NoCache
  2. 共享渲染器
    如果多个 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)
    
  3. 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 图形。

在这里插入图片描述

© 版权声明

相关文章