364 lines
12 KiB
Python
364 lines
12 KiB
Python
# 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())
|