Qt小组件 - 3 imageLabel
ImageLabel
之前有过一个文章,但是用的不方便,在这重新写一下,弥补一下qfluentwidgets
库的功能欠缺
特点
- 支持http图片
- 支持
bytes、base64
的转码 - 支持图片文件蒙版填充
- 支持图片圆角
- 重写
setScaledContents
设置图片是否居中显示
ZoomImageLabel 缩放动画标签,仿包子漫画的一个展示标签,包子漫画
# coding: utf-8
import base64
from pathlib import Path
from random import randint
from typing import Unionfrom 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 singledispatchmethodfrom common import USER_AGENTclass ImageLabel(QLabel):clicked = Signal()finished = Signal()@singledispatchmethoddef __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 = Falseself.__scaledContents = Falseself.__topLeftRadius = 0self.__topRightRadius = 0self.__bottomLeftRadius = 0self.__bottomRightRadius = 0self.__radius = 0self.manager = QNetworkAccessManager(self)self._postInit()@__init__.registerdef _(self, image: Union[str, QImage, QPixmap, Path], parent=None):self.__init__(parent)self.setImage(image)def _postInit(self):passdef _onFrameChanged(self, index: int):self.image = self.movie().currentImage()self.update()def setBorderRadius(self, topLeft: int, topRight: int, bottomLeft: int, bottomRight: int):""" 设置图像的边界半径 """self.__topLeftRadius = topLeftself.__topRightRadius = topRightself.__bottomLeftRadius = bottomLeftself.__bottomRightRadius = bottomRightself.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 = imageself.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():returnh = 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():returnw = 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():returnself.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 = radiusself.__topLeftRadius = self.__topRightRadius = self.__bottomLeftRadius = self.__bottomRightRadius = radiusself.update()def getRadius(self) -> int:return self.__radiusdef setTopLeftRadius(self, radius: int):self.__topLeftRadius = radiusself.update()def getTopLeftRadius(self) -> int:return self.__topLeftRadiusdef setTopRightRadius(self, radius: int):self.__topRightRadius = radiusself.update()def getTopRightRadius(self) -> int:return self.__topRightRadiusdef setBottomLeftRadius(self, radius: int):self.__bottomLeftRadius = radiusself.update()def getBottomLeftRadius(self) -> int:return self.__bottomLeftRadiusdef setBottomRightRadius(self, radius: int):self.__bottomRightRadius = radiusself.update()def getBottomRightRadius(self) -> int:return self.__bottomRightRadiusdef setScaledContents(self, contents: bool):self.__scaledContents = contentsself.update()def getScaledContents(self) -> bool:return self.__scaledContentsdef setMaskColor(self, color: QColor):self.__maskColor = colorself.update()def getMaskColor(self) -> QColor:return self.__maskColordef setEnabledMask(self, enabled: bool):self.__maskEnabled = enabledself.update()def getEnabledMask(self) -> bool:return self.__maskEnableddef _creatPainterPath(self):path = QPainterPath()w, h = self.width(), self.height()# top linepath.moveTo(self.topLeftRadius, 0)path.lineTo(w - self.topRightRadius, 0)# top right arcd = self.topRightRadius * 2path.arcTo(w - d, 0, d, d, 90, -90)# right linepath.lineTo(w, h - self.bottomRightRadius)# bottom right arcd = self.bottomRightRadius * 2path.arcTo(w - d, h - d, d, d, 0, -90)# bottom linepath.lineTo(self.bottomLeftRadius, h)# bottom left arcd = self.bottomLeftRadius * 2path.arcTo(0, h - d, d, d, -90, -90)# left linepath.lineTo(0, self.topLeftRadius)# top left arcd = self.topLeftRadius * 2path.arcTo(0, 0, d, d, -180, -90)return pathdef _creatPainterImage(self):if self.scaledContents:image = self.image.scaled(self.size() * self.devicePixelRatioF(),Qt.AspectRatioMode.KeepAspectRatioByExpanding,Qt.TransformationMode.SmoothTransformation) # type: QImageiw, ih = image.width(), image.height()w = self.width() * self.devicePixelRatioF()h = self.height() * self.devicePixelRatioF()x, y = (iw - w) / 2, (ih - h) / 2image = image.copy(int(x), int(y), int(w), int(h))return imageelse:image = self.image.scaled(self.size() * self.devicePixelRatioF(),Qt.AspectRatioMode.IgnoreAspectRatio,Qt.TransformationMode.SmoothTransformation)return imagedef _creatPainterRect(self):return self.rect()def paintEvent(self, event: QPaintEvent):if self.isNull():returnpainter = 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():returnself.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_strscaledContents = 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 = zoomRatioself.update()def getZoomRatio(self) -> float:return self.__zoomRatiodef 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 rectFzoomRatio = Property(float, getZoomRatio, setZoomRatio)
singledispatchmethod
# coding: utf-8
from functools import singledispatch, update_wrapperclass singledispatchmethod:"""Single-dispatch generic method descriptor.Supports wrapping existing descriptors and handles non-descriptorcallables 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 = funcdef register(self, cls, method=None):"""generic_method.register(cls, func) -> funcRegisters 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.funcfor v in kwargs.values():if v.__class__ in self.dispatcher.registry:method = self.dispatcher.dispatch(v.__class__)if method is not self.func:breakreturn method.__get__(obj, cls)(*args, **kwargs)_method.__isabstractmethod__ = self.__isabstractmethod___method.register = self.registerupdate_wrapper(_method, self.func)return _method@propertydef __isabstractmethod__(self):return getattr(self.func, '__isabstractmethod__', False)