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)