# coding:utf-8 import sys from typing import Union from PyQt6.QtCore import QRect, QSize, Qt from PyQt6.QtGui import QColor, QIcon, QPainter, QPixmap from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QVBoxLayout, QWidget from qfluentwidgets.common.animation import BackgroundAnimationWidget from qfluentwidgets.common.config import qconfig from qfluentwidgets.common.icon import FluentIconBase from qfluentwidgets.common.router import qrouter from qfluentwidgets.common.style_sheet import ( FluentStyleSheet, isDarkTheme, ) from qfluentwidgets.components.navigation import ( NavigationInterface, NavigationItemPosition, NavigationTreeWidget, ) from qfluentwidgets.components.widgets.frameless_window import FramelessWindow from qframelesswindow import TitleBar, TitleBarBase from app.view.widgets.stacked_widget import StackedWidget class CustomFluentWindowBase(BackgroundAnimationWidget, FramelessWindow): """Fluent window base class""" def __init__(self, parent=None): self._isMicaEnabled = False self._lightBackgroundColor = QColor(240, 244, 249) self._darkBackgroundColor = QColor(32, 32, 32) self._backgroundPixmap = None # 存储背景图片 self._backgroundOpacity = 1.0 # 背景图片不透明度 super().__init__(parent=parent) self.hBoxLayout = QHBoxLayout(self) self.stackedWidget = StackedWidget(self) self.navigationInterface = None # initialize layout self.hBoxLayout.setSpacing(0) self.hBoxLayout.setContentsMargins(0, 0, 0, 0) FluentStyleSheet.FLUENT_WINDOW.apply(self.stackedWidget) # enable mica effect on win11 self.setMicaEffectEnabled(True) # show system title bar buttons on macOS if sys.platform == "darwin": self.setSystemTitleBarButtonVisible(True) qconfig.themeChangedFinished.connect(self._onThemeChangedFinished) def addSubInterface( self, interface: QWidget, icon: Union[FluentIconBase, QIcon, str], text: str, position=NavigationItemPosition.TOP, ): """add sub interface""" raise NotImplementedError def removeInterface(self, interface: QWidget, isDelete=False): """remove sub interface Parameters ---------- interface: QWidget sub interface to be removed isDelete: bool whether to delete the sub interface """ raise NotImplementedError def switchTo(self, interface: QWidget): self.stackedWidget.setCurrentWidget(interface, popOut=False) def _onCurrentInterfaceChanged(self, index: int): widget = self.stackedWidget.widget(index) self.navigationInterface.setCurrentItem(widget.objectName()) qrouter.push(self.stackedWidget, widget.objectName()) self._updateStackedBackground() def _updateStackedBackground(self): isTransparent = self.stackedWidget.currentWidget().property( "isStackedTransparent" ) if bool(self.stackedWidget.property("isTransparent")) == isTransparent: return self.stackedWidget.setProperty("isTransparent", isTransparent) self.stackedWidget.setStyle(QApplication.style()) def setCustomBackgroundColor(self, light, dark): """set custom background color Parameters ---------- light, dark: QColor | Qt.GlobalColor | str background color in light/dark theme mode """ self._lightBackgroundColor = QColor(light) self._darkBackgroundColor = QColor(dark) self._updateBackgroundColor() def setBackgroundImage(self, imagePath: str, opacity: float = 1.0): """设置背景图片 Parameters ---------- imagePath: str 背景图片路径 opacity: float 背景图片不透明度,范围0.0-1.0 """ self._backgroundPixmap = QPixmap(imagePath) if self._backgroundPixmap.isNull(): print(f"无法加载背景图片: {imagePath}") return self._backgroundOpacity = max(0.0, min(1.0, opacity)) # 确保在0-1范围内 # 设置StackedWidget为透明,以便显示背景图片 self.stackedWidget.setProperty("isTransparent", True) self.stackedWidget.setStyle(QApplication.style()) self.update() # 触发重绘 def removeBackgroundImage(self): """移除背景图片""" self._backgroundPixmap = None # 恢复StackedWidget的默认背景 self.stackedWidget.setProperty("isTransparent", False) self.stackedWidget.setStyle(QApplication.style()) self.update() def _normalBackgroundColor(self): if not self.isMicaEffectEnabled(): return ( self._darkBackgroundColor if isDarkTheme() else self._lightBackgroundColor ) return QColor(0, 0, 0, 0) def _onThemeChangedFinished(self): if self.isMicaEffectEnabled(): self.windowEffect.setMicaEffect(self.winId(), isDarkTheme()) def paintEvent(self, e): # 创建绘制器 painter = QPainter(self) # 如果有背景图片,先绘制背景图片 if self._backgroundPixmap and not self._backgroundPixmap.isNull(): # 设置不透明度 painter.setOpacity(self._backgroundOpacity) # 缩放图片以适应窗口大小 scaled_pixmap = self._backgroundPixmap.scaled( self.size(), Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation ) painter.drawPixmap(0, 0, scaled_pixmap) # 然后调用父类的绘制方法 super().paintEvent(e) def setMicaEffectEnabled(self, isEnabled: bool): """set whether the mica effect is enabled, only available on Win11""" if sys.platform != "win32" or sys.getwindowsversion().build < 22000: return self._isMicaEnabled = isEnabled if isEnabled: self.windowEffect.setMicaEffect(self.winId(), isDarkTheme()) # 启用Mica效果时移除背景图片 self.removeBackgroundImage() else: self.windowEffect.removeBackgroundEffect(self.winId()) self.setBackgroundColor(self._normalBackgroundColor()) def isMicaEffectEnabled(self): return self._isMicaEnabled def systemTitleBarRect(self, size: QSize) -> QRect: """Returns the system title bar rect, only works for macOS Parameters ---------- size: QSize original system title bar rect """ return QRect( size.width() - 75, 0 if self.isFullScreen() else 9, 75, size.height() ) def setTitleBar(self, titleBar): super().setTitleBar(titleBar) # hide title bar buttons on macOS if ( sys.platform == "darwin" and self.isSystemButtonVisible() and isinstance(titleBar, TitleBarBase) ): titleBar.minBtn.hide() titleBar.maxBtn.hide() titleBar.closeBtn.hide() class FluentTitleBar(TitleBar): """Fluent title bar""" def __init__(self, parent): super().__init__(parent) self.setFixedHeight(48) self.hBoxLayout.removeWidget(self.minBtn) self.hBoxLayout.removeWidget(self.maxBtn) self.hBoxLayout.removeWidget(self.closeBtn) # add window icon self.iconLabel = QLabel(self) self.iconLabel.setFixedSize(18, 18) self.hBoxLayout.insertWidget( 0, self.iconLabel, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter ) self.window().windowIconChanged.connect(self.setIcon) # add title label self.titleLabel = QLabel(self) self.hBoxLayout.insertWidget( 1, self.titleLabel, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter ) self.titleLabel.setObjectName("titleLabel") self.window().windowTitleChanged.connect(self.setTitle) self.vBoxLayout = QVBoxLayout() self.buttonLayout = QHBoxLayout() self.buttonLayout.setSpacing(0) self.buttonLayout.setContentsMargins(0, 0, 0, 0) self.buttonLayout.setAlignment(Qt.AlignmentFlag.AlignTop) self.buttonLayout.addWidget(self.minBtn) self.buttonLayout.addWidget(self.maxBtn) self.buttonLayout.addWidget(self.closeBtn) self.vBoxLayout.addLayout(self.buttonLayout) self.vBoxLayout.addStretch(1) self.hBoxLayout.addLayout(self.vBoxLayout, 0) FluentStyleSheet.FLUENT_WINDOW.apply(self) def setTitle(self, title): self.titleLabel.setText(title) self.titleLabel.adjustSize() def setIcon(self, icon): self.iconLabel.setPixmap(QIcon(icon).pixmap(18, 18)) class CustomFluentWindow(CustomFluentWindowBase): """Fluent window""" def __init__(self, parent=None): super().__init__(parent) self.setTitleBar(FluentTitleBar(self)) self.navigationInterface = NavigationInterface(self, showReturnButton=True) self.widgetLayout = QHBoxLayout() # initialize layout self.hBoxLayout.addWidget(self.navigationInterface) self.hBoxLayout.addLayout(self.widgetLayout) self.hBoxLayout.setStretchFactor(self.widgetLayout, 1) self.widgetLayout.addWidget(self.stackedWidget) self.widgetLayout.setContentsMargins(0, 48, 0, 0) self.navigationInterface.displayModeChanged.connect(self.titleBar.raise_) self.titleBar.raise_() def addSubInterface( self, interface: QWidget, icon: Union[FluentIconBase, QIcon, str], text: str, position=NavigationItemPosition.TOP, parent=None, isTransparent=False, ) -> NavigationTreeWidget: """add sub interface, the object name of `interface` should be set already before calling this method Parameters ---------- interface: QWidget the subinterface to be added icon: FluentIconBase | QIcon | str the icon of navigation item text: str the text of navigation item position: NavigationItemPosition the position of navigation item parent: QWidget the parent of navigation item isTransparent: bool whether to use transparent background """ if not interface.objectName(): raise ValueError("The object name of `interface` can't be empty string.") if parent and not parent.objectName(): raise ValueError("The object name of `parent` can't be empty string.") interface.setProperty("isStackedTransparent", isTransparent) self.stackedWidget.addWidget(interface) # add navigation item routeKey = interface.objectName() item = self.navigationInterface.addItem( routeKey=routeKey, icon=icon, text=text, onClick=lambda: self.switchTo(interface), position=position, tooltip=text, parentRouteKey=parent.objectName() if parent else None, ) # initialize selected item if self.stackedWidget.count() == 1: self.stackedWidget.currentChanged.connect(self._onCurrentInterfaceChanged) self.navigationInterface.setCurrentItem(routeKey) qrouter.setDefaultRouteKey(self.stackedWidget, routeKey) self._updateStackedBackground() return item def removeInterface(self, interface, isDelete=False): self.navigationInterface.removeWidget(interface.objectName()) self.stackedWidget.removeWidget(interface) interface.hide() if isDelete: interface.deleteLater() def resizeEvent(self, e): self.titleBar.move(46, 0) self.titleBar.resize(self.width() - 46, self.titleBar.height())