initial fluent-widgets ui
This commit is contained in:
413
qfluentwidgets/window/fluent_window.py
Normal file
413
qfluentwidgets/window/fluent_window.py
Normal file
@ -0,0 +1,413 @@
|
||||
# coding:utf-8
|
||||
from typing import Union
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import Qt, QSize, QRect
|
||||
from PySide6.QtGui import QIcon, QPainter, QColor
|
||||
from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QApplication
|
||||
|
||||
from ..common.config import qconfig
|
||||
from ..common.icon import FluentIconBase
|
||||
from ..common.router import qrouter
|
||||
from ..common.style_sheet import FluentStyleSheet, isDarkTheme, setTheme, Theme
|
||||
from ..common.animation import BackgroundAnimationWidget
|
||||
from ..components.widgets.frameless_window import FramelessWindow
|
||||
from ..components.navigation import (NavigationInterface, NavigationBar, NavigationItemPosition,
|
||||
NavigationBarPushButton, NavigationTreeWidget)
|
||||
from .stacked_widget import StackedWidget
|
||||
|
||||
from qframelesswindow import TitleBar, TitleBarBase
|
||||
|
||||
|
||||
class FluentWindowBase(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)
|
||||
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 _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):
|
||||
super().paintEvent(e)
|
||||
painter = QPainter(self)
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.setBrush(self.backgroundColor)
|
||||
painter.drawRect(self.rect())
|
||||
|
||||
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())
|
||||
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.AlignLeft | Qt.AlignVCenter)
|
||||
self.window().windowIconChanged.connect(self.setIcon)
|
||||
|
||||
# add title label
|
||||
self.titleLabel = QLabel(self)
|
||||
self.hBoxLayout.insertWidget(1, self.titleLabel, 0, Qt.AlignLeft | Qt.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.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 FluentWindow(FluentWindowBase):
|
||||
""" 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())
|
||||
|
||||
|
||||
class MSFluentTitleBar(FluentTitleBar):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.hBoxLayout.insertSpacing(0, 20)
|
||||
self.hBoxLayout.insertSpacing(2, 2)
|
||||
|
||||
|
||||
class MSFluentWindow(FluentWindowBase):
|
||||
""" Fluent window in Microsoft Store style """
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitleBar(MSFluentTitleBar(self))
|
||||
|
||||
self.navigationInterface = NavigationBar(self)
|
||||
|
||||
# initialize layout
|
||||
self.hBoxLayout.setContentsMargins(0, 48, 0, 0)
|
||||
self.hBoxLayout.addWidget(self.navigationInterface)
|
||||
self.hBoxLayout.addWidget(self.stackedWidget, 1)
|
||||
|
||||
self.titleBar.raise_()
|
||||
self.titleBar.setAttribute(Qt.WA_StyledBackground)
|
||||
|
||||
def addSubInterface(self, interface: QWidget, icon: Union[FluentIconBase, QIcon, str], text: str,
|
||||
selectedIcon=None, position=NavigationItemPosition.TOP, isTransparent=False) -> NavigationBarPushButton:
|
||||
""" 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
|
||||
|
||||
selectedIcon: str | QIcon | FluentIconBase
|
||||
the icon of navigation item in selected state
|
||||
|
||||
position: NavigationItemPosition
|
||||
the position of navigation item
|
||||
"""
|
||||
if not interface.objectName():
|
||||
raise ValueError("The object name of `interface` 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),
|
||||
selectedIcon=selectedIcon,
|
||||
position=position
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class SplitTitleBar(TitleBar):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
# add window icon
|
||||
self.iconLabel = QLabel(self)
|
||||
self.iconLabel.setFixedSize(18, 18)
|
||||
self.hBoxLayout.insertSpacing(0, 12)
|
||||
self.hBoxLayout.insertWidget(1, self.iconLabel, 0, Qt.AlignLeft | Qt.AlignBottom)
|
||||
self.window().windowIconChanged.connect(self.setIcon)
|
||||
|
||||
# add title label
|
||||
self.titleLabel = QLabel(self)
|
||||
self.hBoxLayout.insertWidget(2, self.titleLabel, 0, Qt.AlignLeft | Qt.AlignBottom)
|
||||
self.titleLabel.setObjectName('titleLabel')
|
||||
self.window().windowTitleChanged.connect(self.setTitle)
|
||||
|
||||
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 SplitFluentWindow(FluentWindow):
|
||||
""" Fluent window with split style """
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitleBar(SplitTitleBar(self))
|
||||
|
||||
if sys.platform == "darwin":
|
||||
self.titleBar.setFixedHeight(48)
|
||||
|
||||
self.widgetLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.titleBar.raise_()
|
||||
self.navigationInterface.displayModeChanged.connect(self.titleBar.raise_)
|
||||
|
||||
|
||||
class FluentBackgroundTheme:
|
||||
""" Fluent background theme """
|
||||
DEFAULT = (QColor(243, 243, 243), QColor(32, 32, 32)) # light, dark
|
||||
DEFAULT_BLUE = (QColor(240, 244, 249), QColor(25, 33, 42))
|
||||
Reference in New Issue
Block a user