Files
fluent_widgets_pyside6/qfluentwidgets/components/widgets/card_widget.py
2025-08-14 18:45:16 +08:00

369 lines
11 KiB
Python

# coding:utf-8
from typing import List, Union
from PySide6.QtCore import Qt, Signal, QRectF, Property, QPropertyAnimation, QPoint, QSize
from PySide6.QtGui import QPixmap, QPainter, QColor, QPainterPath, QFont, QIcon
from PySide6.QtWidgets import QWidget, QFrame, QVBoxLayout, QHBoxLayout, QLabel
from ...common.overload import singledispatchmethod
from ...common.style_sheet import isDarkTheme, FluentStyleSheet
from ...common.animation import BackgroundAnimationWidget, DropShadowAnimation
from ...common.font import setFont
from ...common.icon import FluentIconBase
from .label import BodyLabel, CaptionLabel
from .icon_widget import IconWidget
class CardWidget(BackgroundAnimationWidget, QFrame):
""" Card widget """
clicked = Signal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self._isClickEnabled = False
self._borderRadius = 5
def mouseReleaseEvent(self, e):
super().mouseReleaseEvent(e)
self.clicked.emit()
def setClickEnabled(self, isEnabled: bool):
self._isClickEnabled = isEnabled
self.update()
def isClickEnabled(self):
return self._isClickEnabled
def _normalBackgroundColor(self):
return QColor(255, 255, 255, 13 if isDarkTheme() else 170)
def _hoverBackgroundColor(self):
return QColor(255, 255, 255, 21 if isDarkTheme() else 64)
def _pressedBackgroundColor(self):
return QColor(255, 255, 255, 8 if isDarkTheme() else 64)
def getBorderRadius(self):
return self._borderRadius
def setBorderRadius(self, radius: int):
self._borderRadius = radius
self.update()
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
w, h = self.width(), self.height()
r = self.borderRadius
d = 2 * r
isDark = isDarkTheme()
# draw top border
path = QPainterPath()
# path.moveTo(1, h - r)
path.arcMoveTo(1, h - d - 1, d, d, 240)
path.arcTo(1, h - d - 1, d, d, 225, -60)
path.lineTo(1, r)
path.arcTo(1, 1, d, d, -180, -90)
path.lineTo(w - r, 1)
path.arcTo(w - d - 1, 1, d, d, 90, -90)
path.lineTo(w - 1, h - r)
path.arcTo(w - d - 1, h - d - 1, d, d, 0, -60)
topBorderColor = QColor(0, 0, 0, 20)
if isDark:
if self.isPressed:
topBorderColor = QColor(255, 255, 255, 18)
elif self.isHover:
topBorderColor = QColor(255, 255, 255, 13)
else:
topBorderColor = QColor(0, 0, 0, 15)
painter.strokePath(path, topBorderColor)
# draw bottom border
path = QPainterPath()
path.arcMoveTo(1, h - d - 1, d, d, 240)
path.arcTo(1, h - d - 1, d, d, 240, 30)
path.lineTo(w - r - 1, h - 1)
path.arcTo(w - d - 1, h - d - 1, d, d, 270, 30)
bottomBorderColor = topBorderColor
if not isDark and self.isHover and not self.isPressed:
bottomBorderColor = QColor(0, 0, 0, 27)
painter.strokePath(path, bottomBorderColor)
# draw background
painter.setPen(Qt.NoPen)
rect = self.rect().adjusted(1, 1, -1, -1)
painter.setBrush(self.backgroundColor)
painter.drawRoundedRect(rect, r, r)
borderRadius = Property(int, getBorderRadius, setBorderRadius)
class SimpleCardWidget(CardWidget):
""" Simple card widget """
def __init__(self, parent=None):
super().__init__(parent)
def _normalBackgroundColor(self):
return QColor(255, 255, 255, 13 if isDarkTheme() else 170)
def _hoverBackgroundColor(self):
return self._normalBackgroundColor()
def _pressedBackgroundColor(self):
return self._normalBackgroundColor()
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
painter.setBrush(self.backgroundColor)
if isDarkTheme():
painter.setPen(QColor(0, 0, 0, 48))
else:
painter.setPen(QColor(0, 0, 0, 12))
r = self.borderRadius
painter.drawRoundedRect(self.rect().adjusted(1, 1, -1, -1), r, r)
class ElevatedCardWidget(SimpleCardWidget):
""" Card widget with shadow effect """
def __init__(self, parent=None):
super().__init__(parent)
self.shadowAni = DropShadowAnimation(self, hoverColor=QColor(0, 0, 0, 20))
self.shadowAni.setOffset(0, 5)
self.shadowAni.setBlurRadius(38)
self.elevatedAni = QPropertyAnimation(self, b'pos', self)
self.elevatedAni.setDuration(100)
self._originalPos = self.pos()
self.setBorderRadius(8)
def enterEvent(self, e):
super().enterEvent(e)
if self.elevatedAni.state() != QPropertyAnimation.Running:
self._originalPos = self.pos()
self._startElevateAni(self.pos(), self.pos() - QPoint(0, 3))
def leaveEvent(self, e):
super().leaveEvent(e)
self._startElevateAni(self.pos(), self._originalPos)
def mousePressEvent(self, e):
super().mousePressEvent(e)
self._startElevateAni(self.pos(), self._originalPos)
def _startElevateAni(self, start, end):
self.elevatedAni.setStartValue(start)
self.elevatedAni.setEndValue(end)
self.elevatedAni.start()
def _hoverBackgroundColor(self):
return QColor(255, 255, 255, 16) if isDarkTheme() else QColor(255, 255, 255)
def _pressedBackgroundColor(self):
return QColor(255, 255, 255, 6 if isDarkTheme() else 118)
class CardSeparator(QWidget):
""" Card separator """
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setFixedHeight(3)
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
if isDarkTheme():
painter.setPen(QColor(255, 255, 255, 46))
else:
painter.setPen(QColor(0, 0, 0, 12))
painter.drawLine(2, 1, self.width() - 2, 1)
class HeaderCardWidget(SimpleCardWidget):
""" Header card widget """
@singledispatchmethod
def __init__(self, parent=None):
super().__init__(parent)
self.headerView = QWidget(self)
self.headerLabel = QLabel(self)
self.separator = CardSeparator(self)
self.view = QWidget(self)
self.vBoxLayout = QVBoxLayout(self)
self.headerLayout = QHBoxLayout(self.headerView)
self.viewLayout = QHBoxLayout(self.view)
self.headerLayout.addWidget(self.headerLabel)
self.headerLayout.setContentsMargins(24, 0, 16, 0)
self.headerView.setFixedHeight(48)
self.vBoxLayout.setSpacing(0)
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.addWidget(self.headerView)
self.vBoxLayout.addWidget(self.separator)
self.vBoxLayout.addWidget(self.view)
self.viewLayout.setContentsMargins(24, 24, 24, 24)
setFont(self.headerLabel, 15, QFont.DemiBold)
self.view.setObjectName('view')
self.headerView.setObjectName('headerView')
self.headerLabel.setObjectName('headerLabel')
FluentStyleSheet.CARD_WIDGET.apply(self)
self._postInit()
@__init__.register
def _(self, title: str, parent=None):
self.__init__(parent)
self.setTitle(title)
def getTitle(self):
return self.headerLabel.text()
def setTitle(self, title: str):
self.headerLabel.setText(title)
def _postInit(self):
pass
title = Property(str, getTitle, setTitle)
class CardGroupWidget(QWidget):
def __init__(self, icon: Union[str, FluentIconBase, QIcon], title: str, content: str, parent=None):
super().__init__(parent=parent)
self.vBoxLayout = QVBoxLayout(self)
self.hBoxLayout = QHBoxLayout()
self.iconWidget = IconWidget(icon)
self.titleLabel = BodyLabel(title)
self.contentLabel = CaptionLabel(content)
self.textLayout = QVBoxLayout()
self.separator = CardSeparator()
self.__initWidget()
def __initWidget(self):
self.separator.hide()
self.iconWidget.setFixedSize(20, 20)
self.contentLabel.setTextColor(QColor(96, 96, 96), QColor(206, 206, 206))
self.vBoxLayout.setSpacing(0)
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.addLayout(self.hBoxLayout)
self.vBoxLayout.addWidget(self.separator)
self.textLayout.addWidget(self.titleLabel)
self.textLayout.addWidget(self.contentLabel)
self.hBoxLayout.addWidget(self.iconWidget)
self.hBoxLayout.addLayout(self.textLayout)
self.hBoxLayout.addStretch(1)
self.hBoxLayout.setSpacing(15)
self.hBoxLayout.setContentsMargins(24, 10, 24, 10)
self.textLayout.setContentsMargins(0, 0, 0, 0)
self.textLayout.setSpacing(0)
self.hBoxLayout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.textLayout.setAlignment(Qt.AlignmentFlag.AlignCenter)
def title(self):
return self.titleLabel.text()
def setTitle(self, text: str):
self.titleLabel.setText(text)
def content(self):
return self.contentLabel.text()
def setContent(self, text: str):
self.contentLabel.setText(text)
def icon(self):
return self.iconWidget.icon
def setIcon(self, icon: Union[str, FluentIconBase, QIcon]):
self.iconWidget.setIcon(icon)
def setIconSize(self, size: QSize):
self.iconWidget.setFixedSize(size)
def setSeparatorVisible(self, isVisible: bool):
self.separator.setVisible(isVisible)
def isSeparatorVisible(self):
return self.separator.isVisible()
def addWidget(self, widget: QWidget, stretch=0):
self.hBoxLayout.addWidget(widget, stretch=stretch)
class GroupHeaderCardWidget(HeaderCardWidget):
""" Group header card widget """
def _postInit(self):
super()._postInit()
self.groupWidgets = [] # type: List[CardGroupWidget]
self.groupLayout = QVBoxLayout()
self.groupLayout.setSpacing(0)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.groupLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.addLayout(self.groupLayout)
def addGroup(self, icon: Union[str, FluentIconBase, QIcon], title: str, content: str, widget: QWidget, stretch=0) -> CardGroupWidget:
""" add widget to a new group
Parameters
----------
icon: str | QIcon | FluentIconBase
the icon to be drawn
title: str
the title of card
content: str
the content of card
widget: QWidget
the widget to be added
stretch: int
the layout stretch of widget
"""
group = CardGroupWidget(icon, title, content, self)
group.addWidget(widget, stretch=stretch)
if self.groupWidgets:
self.groupWidgets[-1].setSeparatorVisible(True)
self.groupLayout.addWidget(group)
self.groupWidgets.append(group)
return group
def groupCount(self):
return len(self.groupWidgets)