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

554 lines
15 KiB
Python

# coding:utf-8
from typing import List, Union
from PySide6.QtCore import Qt, Property, QPoint, Signal, QSize, QRectF, QUrl
from PySide6.QtGui import (QPainter, QPixmap, QPalette, QColor, QFont, QImage, QPainterPath,
QImageReader, QBrush, QMovie, QDesktopServices)
from PySide6.QtWidgets import QLabel, QWidget, QPushButton, QApplication
from ...common.exception_handler import exceptionHandler
from ...common.overload import singledispatchmethod
from ...common.font import setFont, getFont
from ...common.style_sheet import FluentStyleSheet, setCustomStyleSheet, setCustomStyleSheet
from ...common.config import qconfig, isDarkTheme
from .menu import LabelContextMenu
class PixmapLabel(QLabel):
""" Label for high dpi pixmap """
def __init__(self, parent=None):
super().__init__(parent)
self.__pixmap = QPixmap()
def setPixmap(self, pixmap: QPixmap):
self.__pixmap = pixmap
self.setFixedSize(pixmap.size())
self.update()
def pixmap(self):
return self.__pixmap
def paintEvent(self, e):
if self.__pixmap.isNull():
return super().paintEvent(e)
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing |
QPainter.SmoothPixmapTransform)
painter.setPen(Qt.NoPen)
painter.drawPixmap(self.rect(), self.__pixmap)
class FluentLabelBase(QLabel):
""" Fluent label base class
Constructors
------------
* FluentLabelBase(`parent`: QWidget = None)
* FluentLabelBase(`text`: str, `parent`: QWidget = None)
"""
@singledispatchmethod
def __init__(self, parent: QWidget = None):
super().__init__(parent)
self._init()
@__init__.register
def _(self, text: str, parent: QWidget = None):
self.__init__(parent)
self.setText(text)
def _init(self):
FluentStyleSheet.LABEL.apply(self)
self.setFont(self.getFont())
self.setTextColor()
qconfig.themeChanged.connect(lambda: self.setTextColor(self.lightColor, self.darkColor))
self.customContextMenuRequested.connect(self._onContextMenuRequested)
return self
def getFont(self):
raise NotImplementedError
@exceptionHandler()
def setTextColor(self, light=QColor(0, 0, 0), dark=QColor(255, 255, 255)):
""" set the text color of label
Parameters
----------
light, dark: QColor | Qt.GlobalColor | str
text color in light/dark mode
"""
self._lightColor = QColor(light)
self._darkColor = QColor(dark)
setCustomStyleSheet(
self,
f"FluentLabelBase{{color:{self.lightColor.name(QColor.NameFormat.HexArgb)}}}",
f"FluentLabelBase{{color:{self.darkColor.name(QColor.NameFormat.HexArgb)}}}"
)
@Property(QColor)
def lightColor(self):
return self._lightColor
@lightColor.setter
def lightColor(self, color: QColor):
self.setTextColor(color, self.darkColor)
@Property(QColor)
def darkColor(self):
return self._darkColor
@darkColor.setter
def darkColor(self, color: QColor):
self.setTextColor(self.lightColor, color)
@Property(int)
def pixelFontSize(self):
return self.font().pixelSize()
@pixelFontSize.setter
def pixelFontSize(self, size: int):
font = self.font()
font.setPixelSize(size)
self.setFont(font)
@Property(bool)
def strikeOut(self):
return self.font().strikeOut()
@strikeOut.setter
def strikeOut(self, isStrikeOut: bool):
font = self.font()
font.setStrikeOut(isStrikeOut)
self.setFont(font)
@Property(bool)
def underline(self):
return self.font().underline()
@underline.setter
def underline(self, isUnderline: bool):
font = self.font()
font.setStyle()
font.setUnderline(isUnderline)
self.setFont(font)
def _onContextMenuRequested(self, pos):
menu = LabelContextMenu(parent=self)
menu.exec(self.mapToGlobal(pos))
class CaptionLabel(FluentLabelBase):
""" Caption text label
Constructors
------------
* CaptionLabel(`parent`: QWidget = None)
* CaptionLabel(`text`: str, `parent`: QWidget = None)
"""
def getFont(self):
return getFont(12)
class BodyLabel(FluentLabelBase):
""" Body text label
Constructors
------------
* BodyLabel(`parent`: QWidget = None)
* BodyLabel(`text`: str, `parent`: QWidget = None)
"""
def getFont(self):
return getFont(14)
class StrongBodyLabel(FluentLabelBase):
""" Strong body text label
Constructors
------------
* StrongBodyLabel(`parent`: QWidget = None)
* StrongBodyLabel(`text`: str, `parent`: QWidget = None)
"""
def getFont(self):
return getFont(14, QFont.DemiBold)
class SubtitleLabel(FluentLabelBase):
""" Subtitle text label
Constructors
------------
* SubtitleLabel(`parent`: QWidget = None)
* SubtitleLabel(`text`: str, `parent`: QWidget = None)
"""
def getFont(self):
return getFont(20, QFont.DemiBold)
class TitleLabel(FluentLabelBase):
""" Sub title text label
Constructors
------------
* TitleLabel(`parent`: QWidget = None)
* TitleLabel(`text`: str, `parent`: QWidget = None)
"""
def getFont(self):
return getFont(28, QFont.DemiBold)
class LargeTitleLabel(FluentLabelBase):
""" Large title text label
Constructors
------------
* LargeTitleLabel(`parent`: QWidget = None)
* LargeTitleLabel(`text`: str, `parent`: QWidget = None)
"""
def getFont(self):
return getFont(40, QFont.DemiBold)
class DisplayLabel(FluentLabelBase):
""" Display text label
Constructors
------------
* DisplayLabel(`parent`: QWidget = None)
* DisplayLabel(`text`: str, `parent`: QWidget = None)
"""
def getFont(self):
return getFont(68, QFont.DemiBold)
class ImageLabel(QLabel):
""" Image label
Constructors
------------
* ImageLabel(`parent`: QWidget = None)
* ImageLabel(`image`: str | QImage | QPixmap, `parent`: QWidget = None)
"""
clicked = Signal()
@singledispatchmethod
def __init__(self, parent: QWidget = None):
super().__init__(parent)
self.image = QImage()
self.setBorderRadius(0, 0, 0, 0)
self._postInit()
@__init__.register
def _(self, image: str, parent=None):
self.__init__(parent)
self.setImage(image)
@__init__.register
def _(self, image: QImage, parent=None):
self.__init__(parent)
self.setImage(image)
@__init__.register
def _(self, image: QPixmap, parent=None):
self.__init__(parent)
self.setImage(image)
def _postInit(self):
pass
def _onFrameChanged(self, index: int):
self.image = self.movie().currentImage()
self.update()
def setBorderRadius(self, topLeft: int, topRight: int, bottomLeft: int, bottomRight: int):
""" set the border radius of image """
self._topLeftRadius = topLeft
self._topRightRadius = topRight
self._bottomLeftRadius = bottomLeft
self._bottomRightRadius = bottomRight
self.update()
def setImage(self, image: Union[str, QPixmap, QImage] = None):
""" set the image of label """
self.image = image or QImage()
if isinstance(image, str):
reader = QImageReader(image)
if reader.supportsAnimation():
self.setMovie(QMovie(image))
else:
self.image = reader.read()
elif isinstance(image, QPixmap):
self.image = image.toImage()
self.setFixedSize(self.image.size())
self.update()
def scaledToWidth(self, width: int):
if self.isNull():
return
h = int(width / self.image.width() * self.image.height())
self.setFixedSize(width, h)
if self.movie():
self.movie().setScaledSize(QSize(width, h))
def scaledToHeight(self, height: int):
if self.isNull():
return
w = int(height / self.image.height() * self.image.width())
self.setFixedSize(w, height)
if self.movie():
self.movie().setScaledSize(QSize(w, height))
def setScaledSize(self, size: QSize):
if self.isNull():
return
self.setFixedSize(size)
if self.movie():
self.movie().setScaledSize(size)
def isNull(self):
return self.image.isNull()
def mouseReleaseEvent(self, e):
super().mouseReleaseEvent(e)
self.clicked.emit()
def setPixmap(self, pixmap: QPixmap):
self.setImage(pixmap)
def pixmap(self) -> QPixmap:
return QPixmap.fromImage(self.image)
def setMovie(self, movie: QMovie):
super().setMovie(movie)
self.movie().start()
self.image = self.movie().currentImage()
self.movie().frameChanged.connect(self._onFrameChanged)
def paintEvent(self, e):
if self.isNull():
return
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
path = QPainterPath()
w, h = self.width(), self.height()
# top line
path.moveTo(self.topLeftRadius, 0)
path.lineTo(w - self.topRightRadius, 0)
# top right arc
d = self.topRightRadius * 2
path.arcTo(w - d, 0, d, d, 90, -90)
# right line
path.lineTo(w, h - self.bottomRightRadius)
# bottom right arc
d = self.bottomRightRadius * 2
path.arcTo(w - d, h - d, d, d, 0, -90)
# bottom line
path.lineTo(self.bottomLeftRadius, h)
# bottom left arc
d = self.bottomLeftRadius * 2
path.arcTo(0, h - d, d, d, -90, -90)
# left line
path.lineTo(0, self.topLeftRadius)
# top left arc
d = self.topLeftRadius * 2
path.arcTo(0, 0, d, d, -180, -90)
# draw image
image = self.image.scaled(
self.size()*self.devicePixelRatioF(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
painter.setPen(Qt.NoPen)
painter.setClipPath(path)
painter.drawImage(self.rect(), image)
@Property(int)
def topLeftRadius(self):
return self._topLeftRadius
@topLeftRadius.setter
def topLeftRadius(self, radius: int):
self.setBorderRadius(radius, self.topRightRadius, self.bottomLeftRadius, self.bottomRightRadius)
@Property(int)
def topRightRadius(self):
return self._topRightRadius
@topRightRadius.setter
def topRightRadius(self, radius: int):
self.setBorderRadius(self.topLeftRadius, radius, self.bottomLeftRadius, self.bottomRightRadius)
@Property(int)
def bottomLeftRadius(self):
return self._bottomLeftRadius
@bottomLeftRadius.setter
def bottomLeftRadius(self, radius: int):
self.setBorderRadius(self.topLeftRadius, self.topRightRadius, radius, self.bottomRightRadius)
@Property(int)
def bottomRightRadius(self):
return self._bottomRightRadius
@bottomRightRadius.setter
def bottomRightRadius(self, radius: int):
self.setBorderRadius(
self.topLeftRadius, self.topRightRadius, self.bottomLeftRadius, radius)
class AvatarWidget(ImageLabel):
""" Avatar widget
Constructors
------------
* AvatarWidget(`parent`: QWidget = None)
* AvatarWidget(`image`: str | QImage | QPixmap, `parent`: QWidget = None)
"""
def _postInit(self):
self.setRadius(48)
self.lightBackgroundColor = QColor(0, 0, 0, 50)
self.darkBackgroundColor = QColor(255, 255, 255, 50)
def getRadius(self):
return self._radius
def setRadius(self, radius: int):
self._radius = radius
setFont(self, radius)
self.setFixedSize(2*radius, 2*radius)
self.update()
def setImage(self, image: Union[str, QPixmap, QImage] = None):
super().setImage(image)
self.setRadius(self.radius)
def setBackgroundColor(self, light: QColor, dark: QColor):
self.lightBackgroundColor = QColor(light)
self.darkBackgroundColor = QColor(light)
self.update()
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
if not self.isNull():
self._drawImageAvatar(painter)
else:
self._drawTextAvatar(painter)
def _drawImageAvatar(self, painter: QPainter):
# center crop image
image = self.image.scaled(
self.size()*self.devicePixelRatioF(), Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) # type: QImage
iw, ih = image.width(), image.height()
d = self.getRadius() * 2 * self.devicePixelRatioF()
x, y = (iw - d) / 2, (ih - d) / 2
image = image.copy(int(x), int(y), int(d), int(d))
# draw image
path = QPainterPath()
path.addEllipse(QRectF(self.rect()))
painter.setPen(Qt.NoPen)
painter.setClipPath(path)
painter.drawImage(self.rect(), image)
def _drawTextAvatar(self, painter: QPainter):
if not self.text():
return
painter.setBrush(self.darkBackgroundColor if isDarkTheme() else self.lightBackgroundColor)
painter.setPen(Qt.NoPen)
painter.drawEllipse(QRectF(self.rect()))
painter.setFont(self.font())
painter.setPen(Qt.white if isDarkTheme() else Qt.black)
painter.drawText(self.rect(), Qt.AlignCenter, self.text()[0].upper())
radius = Property(int, getRadius, setRadius)
class HyperlinkLabel(QPushButton):
""" Hyperlink label
Constructors
------------
* HyperlinkLabel(`parent`: QWidget = None)
* HyperlinkLabel(`text`: str, `parent`: QWidget = None)
* HyperlinkLabel(`url`: QUrl, `parent`: QWidget = None)
"""
@singledispatchmethod
def __init__(self, parent=None):
super().__init__(parent=parent)
self._url = QUrl()
setFont(self, 14)
self.setUnderlineVisible(False)
FluentStyleSheet.LABEL.apply(self)
self.setCursor(Qt.PointingHandCursor)
self.clicked.connect(self._onClicked)
@__init__.register
def _(self, text: str, parent=None):
self.__init__(parent)
self.setText(text)
@__init__.register
def _(self, url: QUrl, text: str, parent=None):
self.__init__(parent)
self.setText(text)
self._url = url
def getUrl(self) -> QUrl:
return self._url
def setUrl(self, url: Union[QUrl, str]):
self._url = QUrl(url)
def isUnderlineVisible(self):
return self._isUnderlineVisible
def setUnderlineVisible(self, isVisible: bool):
self._isUnderlineVisible = isVisible
self.setProperty('underline', isVisible)
self.setStyle(QApplication.style())
def _onClicked(self):
if self.getUrl().isValid():
QDesktopServices.openUrl(self.getUrl())
url = Property(QUrl, getUrl, setUrl)
underlineVisible = Property(bool, isUnderlineVisible, setUnderlineVisible)