Qt小组件 - 3 imageLabel

ImageLabel之前有过一个文章,但是用的不方便,在这重新写一下,弥补一下qfluentwidgets库的功能欠缺

特点

  • 支持http图片
  • 支持bytes、base64的转码
  • 支持图片文件蒙版填充
  • 支持图片圆角
  • 重写setScaledContents设置图片是否居中显示

ZoomImageLabel 缩放动画标签,仿包子漫画的一个展示标签,包子漫画

python 复制代码
# coding: utf-8
import base64
from pathlib import Path
from random import randint
from typing import Union

from PySide6.QtCore import Signal, QEvent, QSize, Qt, Property, QByteArray, QBuffer, QIODevice, QPropertyAnimation, \
    QRectF, QSizeF, QPointF, QUrl
from PySide6.QtGui import QImage, QPixmap, QImageReader, QMovie, QMouseEvent, QPaintEvent, QPainter, QPainterPath, \
    QColor
from PySide6.QtWidgets import QLabel
from PySide6.QtNetwork import QNetworkReply, QNetworkRequest, QNetworkAccessManager
from qfluentwidgets.common.overload import singledispatchmethod

from common import USER_AGENT


class ImageLabel(QLabel):
    clicked = Signal()
    finished = Signal()

    @singledispatchmethod
    def __init__(self, parent=None):
        super().__init__(parent)
        self.image = QImage()
        self.__maskColor = QColor(randint(0, 255), randint(0, 255), randint(0, 255), 50)
        self.__maskEnabled = False
        self.__scaledContents = False
        self.__topLeftRadius = 0
        self.__topRightRadius = 0
        self.__bottomLeftRadius = 0
        self.__bottomRightRadius = 0
        self.__radius = 0

        self.manager = QNetworkAccessManager(self)
        self._postInit()

    @__init__.register
    def _(self, image: Union[str, QImage, QPixmap, Path], 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):
        """ 设置图像的边界半径 """
        self.__topLeftRadius = topLeft
        self.__topRightRadius = topRight
        self.__bottomLeftRadius = bottomLeft
        self.__bottomRightRadius = bottomRight
        self.update()

    def _onFinished(self, reply: QNetworkReply):
        image = QImage()
        if reply.error() == QNetworkReply.NetworkError.NoError:
            data = reply.readAll()
            image.loadFromData(data)
        else:
            print(f"Network error: {reply.errorString()}")
        self.setImage(image)
        reply.deleteLater()

    def setUrl(self, url: Union[str, QUrl]):
        request = QNetworkRequest(QUrl(url))
        request.setHeader(QNetworkRequest.KnownHeaders.UserAgentHeader, USER_AGENT.encode())
        reply = self.manager.get(request)
        reply.finished.connect(lambda: self._onFinished(reply))

    def setImage(self, image: Union[str, QImage, QPixmap, Path]):
        if isinstance(image, (str, Path)):
            reader = QImageReader(str(image))
            if reader.supportsAnimation():
                self.setMovie(QMovie(str(image)))
            else:
                image = reader.read()
        elif isinstance(image, QPixmap):
            image = image.toImage()
        self.image = image
        self.update()
        self.finished.emit()

    def setPixmap(self, pixmap: Union[str, QImage, QPixmap, Path]):
        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 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 sizeHint(self) -> QSize:
        if self.image.isNull():
            return super().sizeHint()
        else:
            return self.image.size()

    def isNull(self) -> bool:
        return self.image.isNull()

    def mouseReleaseEvent(self, event: QMouseEvent):
        pos = event.position().toPoint()
        if event.button() == Qt.MouseButton.LeftButton and self.rect().contains(pos):
            self.clicked.emit()
        super().mouseReleaseEvent(event)

    def setRadius(self, radius: int):
        self.__radius = radius
        self.__topLeftRadius = self.__topRightRadius = self.__bottomLeftRadius = self.__bottomRightRadius = radius
        self.update()

    def getRadius(self) -> int:
        return self.__radius

    def setTopLeftRadius(self, radius: int):
        self.__topLeftRadius = radius
        self.update()

    def getTopLeftRadius(self) -> int:
        return self.__topLeftRadius

    def setTopRightRadius(self, radius: int):
        self.__topRightRadius = radius
        self.update()

    def getTopRightRadius(self) -> int:
        return self.__topRightRadius

    def setBottomLeftRadius(self, radius: int):
        self.__bottomLeftRadius = radius
        self.update()

    def getBottomLeftRadius(self) -> int:
        return self.__bottomLeftRadius

    def setBottomRightRadius(self, radius: int):
        self.__bottomRightRadius = radius
        self.update()

    def getBottomRightRadius(self) -> int:
        return self.__bottomRightRadius

    def setScaledContents(self, contents: bool):
        self.__scaledContents = contents
        self.update()

    def getScaledContents(self) -> bool:
        return self.__scaledContents

    def setMaskColor(self, color: QColor):
        self.__maskColor = color
        self.update()

    def getMaskColor(self) -> QColor:
        return self.__maskColor

    def setEnabledMask(self, enabled: bool):
        self.__maskEnabled = enabled
        self.update()

    def getEnabledMask(self) -> bool:
        return self.__maskEnabled

    def _creatPainterPath(self):
        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)
        return path

    def _creatPainterImage(self):
        if self.scaledContents:
            image = self.image.scaled(
                self.size() * self.devicePixelRatioF(),
                Qt.AspectRatioMode.KeepAspectRatioByExpanding,
                Qt.TransformationMode.SmoothTransformation
            )  # type: QImage

            iw, ih = image.width(), image.height()
            w = self.width() * self.devicePixelRatioF()
            h = self.height() * self.devicePixelRatioF()
            x, y = (iw - w) / 2, (ih - h) / 2
            image = image.copy(int(x), int(y), int(w), int(h))
            return image
        else:
            image = self.image.scaled(
                self.size() * self.devicePixelRatioF(),
                Qt.AspectRatioMode.IgnoreAspectRatio,
                Qt.TransformationMode.SmoothTransformation
            )
            return image

    def _creatPainterRect(self):
        return self.rect()

    def paintEvent(self, event: QPaintEvent):
        if self.isNull():
            return
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
        painter.setRenderHint(QPainter.RenderHint.LosslessImageRendering)

        painter.setPen(Qt.PenStyle.NoPen)
        painter.setClipPath(self._creatPainterPath())
        rect = self._creatPainterRect()
        painter.drawImage(rect, self._creatPainterImage())
        if self.enabledMask:
            painter.fillRect(rect, self.maskColor)
        painter.end()

    def save(self, fileName: Union[str, QIODevice], *args, **kwargs):
        if self.isNull():
            return
        self.image.save(fileName, *args, **kwargs)

    def bytes(self, format: str = 'PNG') -> bytes:
        """
        将图片转换为字节数组
        :param format: 图片格式
        :return:
        """
        if self.isNull():
            return b''
        byte_array = QByteArray()
        buffer = QBuffer(byte_array)
        buffer.open(QIODevice.OpenModeFlag.WriteOnly)
        self.save(buffer, format.upper())
        return byte_array.data()

    def base64(self, format: str = 'PNG', prefix: str = 'data:image/{};base64,') -> str:
        """
        :param format: 图片格式
        :param prefix: 前缀
        :return: base64字符串
        """
        if self.image.isNull():
            return ''
        suffix = format.upper()
        if suffix == 'JPG':
            suffix = 'JPEG'
        prefix = prefix.format(suffix)
        base64_str = prefix + base64.b64encode(self.bytes(suffix)).decode()
        return base64_str

    scaledContents = Property(bool, getScaledContents, setScaledContents)
    topLeftRadius = Property(int, getTopLeftRadius, setTopLeftRadius)
    topRightRadius = Property(int, getTopRightRadius, setTopRightRadius)
    bottomLeftRadius = Property(int, getBottomLeftRadius, setBottomLeftRadius)
    bottomRightRadius = Property(int, getBottomRightRadius, setBottomRightRadius)
    maskColor = Property(QColor, getMaskColor, setMaskColor)
    enabledMask = Property(bool, getEnabledMask, setEnabledMask)
    radius = Property(int, getRadius, setRadius)


class ZoomImageLabel(ImageLabel):
    """ 缩放图像标签 """

    def _postInit(self):
        self.__zoomRatio = 1.0  # 缩放比例
        self.animation = QPropertyAnimation(self, b"zoomRatio", self)
        self.animation.setDuration(300)
        self.animation.setStartValue(1.0)
        self.animation.setEndValue(1.4)

        self.setRadius(5)
        self.setScaledContents(True)

    def setMinZoomRatio(self, minZoomRatio: float):
        self.animation.setStartValue(minZoomRatio)

    def setMaxZoomRatio(self, maxZoomRatio: float):
        self.animation.setEndValue(maxZoomRatio)

    def setZoomRatio(self, zoomRatio: float):
        self.__zoomRatio = zoomRatio
        self.update()

    def getZoomRatio(self) -> float:
        return self.__zoomRatio

    def event(self, event: QEvent):
        if event.type() == QEvent.Type.Enter:
            self.animation.setDirection(QPropertyAnimation.Direction.Forward)
            self.animation.start()
        elif event.type() == QEvent.Type.Leave:
            self.animation.setDirection(QPropertyAnimation.Direction.Backward)
            self.animation.start()
        return super().event(event)

    def _creatPainterRect(self):
        image = self._creatPainterImage()
        imageRatio = image.width() / image.height()
        rectF = QRectF()
        rectF.setSize(QSizeF(imageRatio * self.height() * self.zoomRatio, self.height() * self.zoomRatio))
        rectF.moveCenter(QPointF(self.rect().center().x(), self.rect().center().y()))
        return rectF

    zoomRatio = Property(float, getZoomRatio, setZoomRatio)

singledispatchmethod

python 复制代码
# coding: utf-8
from functools import singledispatch, update_wrapper


class singledispatchmethod:
    """Single-dispatch generic method descriptor.

    Supports wrapping existing descriptors and handles non-descriptor
    callables as instance methods.
    """

    def __init__(self, func):
        if not callable(func) and not hasattr(func, "__get__"):
            raise TypeError(f"{func!r} is not callable or a descriptor")

        self.dispatcher = singledispatch(func)
        self.func = func

    def register(self, cls, method=None):
        """generic_method.register(cls, func) -> func

        Registers a new implementation for the given *cls* on a *generic_method*.
        """
        return self.dispatcher.register(cls, func=method)

    def __get__(self, obj, cls=None):
        def _method(*args, **kwargs):
            if args:
                method = self.dispatcher.dispatch(args[0].__class__)
            else:
                method = self.func
                for v in kwargs.values():
                    if v.__class__ in self.dispatcher.registry:
                        method = self.dispatcher.dispatch(v.__class__)
                        if method is not self.func:
                            break

            return method.__get__(obj, cls)(*args, **kwargs)

        _method.__isabstractmethod__ = self.__isabstractmethod__
        _method.register = self.register
        update_wrapper(_method, self.func)
        return _method

    @property
    def __isabstractmethod__(self):
        return getattr(self.func, '__isabstractmethod__', False)