From ebf784146ef6be19e092e011f19727e42fe95224 Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Sat, 1 Nov 2025 20:39:53 +0800 Subject: [PATCH] =?UTF-8?q?refactor(view):=20=E9=87=8D=E6=9E=84=E4=B8=BB?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=92=8C=E8=87=AA=E5=AE=9A=E4=B9=89Fluent?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将CustomFluentWindow改为继承自MSFluentWindow,简化导航接口实现 更新导航项设置方式,使用objectName作为路由键 优化头像widget的添加方式,直接使用QIcon而非NavigationAvatarWidget 移除冗余代码,统一使用MSFluentWindow提供的方法 --- app/view/main_window.py | 68 +++-- app/view/widgets/custom_fluent_window.py | 368 +++++++---------------- 2 files changed, 155 insertions(+), 281 deletions(-) diff --git a/app/view/main_window.py b/app/view/main_window.py index 8ece8b7..874314a 100644 --- a/app/view/main_window.py +++ b/app/view/main_window.py @@ -53,55 +53,61 @@ class MainWindow(CustomFluentWindow): def updateNavigation(self): # 更新导航项文本 - self.navigationInterface.setItemText(self.ownFiledInterface, lang("我的文件")) + # 使用MSFluentWindow的方法更新导航项文本 + self.navigationInterface.setItemText(self.ownFiledInterface.objectName(), lang("我的文件")) self.navigationInterface.setItemText( - self.storagespaceInterface, lang("存储配额") + self.storagespaceInterface.objectName(), lang("存储配额") ) - self.navigationInterface.setItemText(self.taskInterface, lang("任务管理")) - self.navigationInterface.setItemText(self.appInfoInterface, lang("应用信息")) - ... + self.navigationInterface.setItemText(self.taskInterface.objectName(), lang("任务管理")) + self.navigationInterface.setItemText(self.appInfoInterface.objectName(), lang("应用信息")) def initNavigation(self): - self.navigationInterface.setAcrylicEnabled(True) - self.navigationInterface.setExpandWidth(200) logger.info("开始初始化导航界面") - + + # MSFluentWindow自带导航设置,不需要直接设置navigationInterface + # 注意:MSFluentWindow的addSubInterface方法支持selectedIcon参数 self.addSubInterface( self.ownFiledInterface, QIcon(":app/icons/Myfile.svg"), lang("我的文件"), - NavigationItemPosition.TOP, + position=NavigationItemPosition.TOP, ) self.addSubInterface( self.storagespaceInterface, QIcon(":app/icons/Storage.svg"), lang("存储配额"), - NavigationItemPosition.TOP, + position=NavigationItemPosition.TOP, ) self.addSubInterface( self.taskInterface, QIcon(":app/icons/Task.svg"), lang("任务管理"), - NavigationItemPosition.TOP, + position=NavigationItemPosition.TOP, ) self.addSubInterface( self.appInfoInterface, QIcon(":app/icons/Application.svg"), lang("应用信息"), - NavigationItemPosition.BOTTOM, + position=NavigationItemPosition.BOTTOM, ) # 创建默认头像widget,先使用本地默认头像 self.avatarWidget = NavigationAvatarWidget( userConfig.userName, ":app/images/logo.png" ) - self.navigationInterface.addWidget( + + # 使用MSFluentWindow的navigationInterface添加头像widget + # 直接使用原始图标而不是avatarWidget.avatar + self.navigationInterface.addItem( routeKey="settingInterface", - widget=self.avatarWidget, - position=NavigationItemPosition.BOTTOM, + icon=QIcon(":app/images/logo.png"), + text="", # 不显示文本,只显示头像 onClick=self.setPersonalInfoWidget, + selectable=False, + position=NavigationItemPosition.BOTTOM, ) + self.settingInterface = SettingInterface(self) self.stackedWidget.addWidget(self.settingInterface) @@ -132,16 +138,38 @@ class MainWindow(CustomFluentWindow): self.taskInterface._changePivot("Download") def setPersonalInfoWidget(self): - self.stackedWidget.setCurrentWidget( - self.stackedWidget.findChild(QWidget, "settingInterface") - ) - self.navigationInterface.setCurrentItem("settingInterface") + # 使用MSFluentWindow的方式切换到设置界面 + self.stackedWidget.setCurrentWidget(self.settingInterface) + # 由于设置项是通过addItem添加的非selectable项,不需要设置currentItem def onAvatarDownloaded(self, pixmap): - userConfig.setUserAvatarPixmap(pixmap) self.avatarWidget.setAvatar(pixmap) self.settingInterface.updateAvatar(pixmap) + + # 更新导航项中的头像图标 + if hasattr(self, 'navigationInterface'): + # 创建临时QIcon + icon = QIcon() + icon.addPixmap(pixmap) + + # 使用QFluentWidgets的正确API更新导航项图标 + # 由于NavigationBar没有setItemIcon方法,我们重新创建导航项 + # 先获取现有的设置项并移除 + try: + # 移除现有的设置项 + self.navigationInterface.removeItem("settingInterface") + # 重新添加设置项,使用新的头像图标 + self.navigationInterface.addItem( + routeKey="settingInterface", + icon=icon, + text="", # 不显示文本,只显示头像 + onClick=self.setPersonalInfoWidget, + selectable=False, + position=NavigationItemPosition.BOTTOM, + ) + except Exception as e: + logger.error(f"更新导航头像失败: {str(e)}") def initWindow(self): logger.info("开始初始化窗口设置") diff --git a/app/view/widgets/custom_fluent_window.py b/app/view/widgets/custom_fluent_window.py index 53050b4..c5f0a35 100644 --- a/app/view/widgets/custom_fluent_window.py +++ b/app/view/widgets/custom_fluent_window.py @@ -5,7 +5,7 @@ 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 import MSFluentWindow, NavigationItemPosition, setTheme, Theme from qfluentwidgets.common.config import qconfig from qfluentwidgets.common.icon import FluentIconBase from qfluentwidgets.common.router import qrouter @@ -14,101 +14,113 @@ from qfluentwidgets.common.style_sheet import ( isDarkTheme, ) from qfluentwidgets.components.navigation import ( - NavigationInterface, - NavigationItemPosition, - NavigationTreeWidget, + 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""" +class CustomFluentWindow(MSFluentWindow): + """自定义的Fluent窗口,基于MSFluentWindow实现""" def __init__(self, parent=None): + super().__init__(parent) + + # 背景相关设置 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 + + # 启用mica效果 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, + selectedIcon: Union[FluentIconBase, QIcon, str] = None, position=NavigationItemPosition.TOP, - ): - """add sub interface""" - raise NotImplementedError - - def removeInterface(self, interface: QWidget, isDelete=False): - """remove sub interface + parent=None, + isTransparent=False, + ) -> NavigationTreeWidget: + """添加子界面 Parameters ---------- interface: QWidget - sub interface to be removed + 要添加的子界面 + + icon: FluentIconBase | QIcon | str + 导航项的图标 + + selectedIcon: FluentIconBase | QIcon | str + 选中状态下的图标 + + text: str + 导航项的文本 + + position: NavigationItemPosition + 导航项的位置 + + parent: QWidget + 导航项的父项 + + isTransparent: bool + 是否使用透明背景 + """ + if not interface.objectName(): + raise ValueError("The object name of `interface` can't be empty string.") + + interface.setProperty("isStackedTransparent", isTransparent) + + # 使用MSFluentWindow的addSubInterface方法 + if selectedIcon: + item = super().addSubInterface(interface, icon, text, selectedIcon, position) + else: + item = super().addSubInterface(interface, icon, text, position=position) + + return item + + def removeInterface(self, interface, isDelete=False): + """移除子界面 + + Parameters + ---------- + interface: QWidget + 要移除的子界面 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()) - + # 从stackedWidget中移除 + self.stackedWidget.removeWidget(interface) + interface.hide() + + # 从导航中移除 + self.navigationInterface.removeWidget(interface.objectName()) + + if isDelete: + interface.deleteLater() + 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): """设置背景图片 @@ -119,245 +131,79 @@ class CustomFluentWindowBase(BackgroundAnimationWidget, FramelessWindow): opacity: float 背景图片不透明度,范围0.0-1.0 """ + # 如果启用了Mica效果,先禁用 + if self._isMicaEnabled: + self.setMicaEffectEnabled(False) + 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(): + if not self._isMicaEnabled: 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()) - + if self._isMicaEnabled: + # MSFluentWindow已经处理了Mica效果,这里只需要确保背景颜色正确 + self.setBackgroundColor(self._normalBackgroundColor()) + def paintEvent(self, e): - # 创建绘制器 - painter = QPainter(self) - - # 如果有背景图片,先绘制背景图片 + # 先调用父类的绘制方法 + super().paintEvent(e) + + # 如果有背景图片,绘制背景图片 if self._backgroundPixmap and not self._backgroundPixmap.isNull(): + painter = QPainter(self) + # 设置不透明度 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""" + """设置是否启用mica效果,仅在Win11上可用""" if sys.platform != "win32" or sys.getwindowsversion().build < 22000: + self._isMicaEnabled = False return - + self._isMicaEnabled = isEnabled - + if isEnabled: - self.windowEffect.setMicaEffect(self.winId(), isDarkTheme()) # 启用Mica效果时移除背景图片 self.removeBackgroundImage() + # MSFluentWindow自带Mica效果支持 + self.windowEffect.setMicaEffect(self.winId(), isDarkTheme()) else: self.windowEffect.removeBackgroundEffect(self.winId()) - + self.setBackgroundColor(self._normalBackgroundColor()) - + def isMicaEffectEnabled(self): + """获取是否启用了mica效果""" 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()) + super().resizeEvent(e) + # 确保标题栏正确显示 + if hasattr(self, 'titleBar'): + self.titleBar.raise_()