refactor(view): 重构主窗口和自定义Fluent窗口实现

将CustomFluentWindow改为继承自MSFluentWindow,简化导航接口实现
更新导航项设置方式,使用objectName作为路由键
优化头像widget的添加方式,直接使用QIcon而非NavigationAvatarWidget
移除冗余代码,统一使用MSFluentWindow提供的方法
This commit is contained in:
2025-11-01 20:39:53 +08:00
parent f006729311
commit ebf784146e
2 changed files with 155 additions and 281 deletions

View File

@@ -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,17 +138,39 @@ 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("开始初始化窗口设置")
self.resize(960, 780)

View File

@@ -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,44 +14,30 @@ 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(
@@ -59,51 +45,77 @@ class CustomFluentWindowBase(BackgroundAnimationWidget, FramelessWindow):
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
# 从stackedWidget中移除
self.stackedWidget.removeWidget(interface)
interface.hide()
def switchTo(self, interface: QWidget):
self.stackedWidget.setCurrentWidget(interface, popOut=False)
# 从导航中移除
self.navigationInterface.removeWidget(interface.objectName())
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())
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)
@@ -119,31 +131,25 @@ 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()
@@ -153,15 +159,18 @@ class CustomFluentWindowBase(BackgroundAnimationWidget, FramelessWindow):
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)
@@ -171,193 +180,30 @@ class CustomFluentWindowBase(BackgroundAnimationWidget, FramelessWindow):
)
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_()