2023-11-15 21:57:29 +08:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
"""
|
|
|
|
Created on 2019年7月26日
|
|
|
|
@author: Irony
|
|
|
|
@site: https://pyqt5.com https://github.com/892768447
|
|
|
|
@email: 892768447@qq.com
|
|
|
|
@file: CustomWidgets.CAvatar
|
|
|
|
@description: 头像
|
|
|
|
"""
|
|
|
|
import os
|
|
|
|
|
|
|
|
from PyQt5.QtCore import QUrl, QRectF, Qt, QSize, QTimer, QPropertyAnimation, \
|
|
|
|
QPointF, pyqtProperty
|
|
|
|
from PyQt5.QtGui import QPixmap, QColor, QPainter, QPainterPath, QMovie
|
|
|
|
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkDiskCache, \
|
|
|
|
QNetworkRequest
|
|
|
|
from PyQt5.QtWidgets import QWidget, qApp
|
|
|
|
|
|
|
|
__Author__ = 'Irony'
|
|
|
|
__Copyright__ = 'Copyright (c) 2019 Irony'
|
|
|
|
__Version__ = 1.0
|
|
|
|
|
|
|
|
|
|
|
|
class CAvatar(QWidget):
|
|
|
|
Circle = 0 # 圆圈
|
|
|
|
Rectangle = 1 # 圆角矩形
|
|
|
|
SizeLarge = QSize(128, 128)
|
|
|
|
SizeMedium = QSize(64, 64)
|
|
|
|
SizeSmall = QSize(32, 32)
|
|
|
|
StartAngle = 0 # 起始旋转角度
|
|
|
|
EndAngle = 360 # 结束旋转角度
|
|
|
|
|
2023-11-15 22:32:11 +08:00
|
|
|
def __init__(self, *args, shape=0, url='', img_bytes=None, cacheDir=False, size=QSize(64, 64), animation=False,
|
|
|
|
**kwargs):
|
2023-11-15 21:57:29 +08:00
|
|
|
super(CAvatar, self).__init__(*args, **kwargs)
|
|
|
|
self.url = ''
|
|
|
|
self._angle = 0 # 角度
|
|
|
|
self.pradius = 0 # 加载进度条半径
|
|
|
|
self.animation = animation # 是否使用动画
|
|
|
|
self._movie = None # 动态图
|
|
|
|
self._pixmap = QPixmap() # 图片对象
|
|
|
|
self.pixmap = QPixmap() # 被绘制的对象
|
|
|
|
self.isGif = url.endswith('.gif')
|
|
|
|
# 进度动画定时器
|
|
|
|
self.loadingTimer = QTimer(self, timeout=self.onLoading)
|
|
|
|
# 旋转动画
|
|
|
|
self.rotateAnimation = QPropertyAnimation(
|
|
|
|
self, b'angle', self, loopCount=1)
|
|
|
|
self.setShape(shape)
|
|
|
|
self.setCacheDir(cacheDir)
|
|
|
|
self.setSize(size)
|
2023-11-15 22:32:11 +08:00
|
|
|
if img_bytes:
|
|
|
|
self.setBytes(img_bytes)
|
|
|
|
else:
|
|
|
|
self.setUrl(url)
|
2023-11-15 21:57:29 +08:00
|
|
|
|
|
|
|
def paintEvent(self, event):
|
|
|
|
super(CAvatar, self).paintEvent(event)
|
|
|
|
# 画笔
|
|
|
|
painter = QPainter(self)
|
|
|
|
painter.setRenderHint(QPainter.Antialiasing, True)
|
|
|
|
painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
|
|
|
|
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
|
|
|
|
# 绘制
|
|
|
|
path = QPainterPath()
|
|
|
|
diameter = min(self.width(), self.height())
|
|
|
|
if self.shape == self.Circle:
|
|
|
|
radius = int(diameter / 2)
|
|
|
|
elif self.shape == self.Rectangle:
|
|
|
|
radius = 4
|
|
|
|
halfW = self.width() / 2
|
|
|
|
halfH = self.height() / 2
|
|
|
|
painter.translate(halfW, halfH)
|
|
|
|
path.addRoundedRect(
|
|
|
|
QRectF(-halfW, -halfH, diameter, diameter), radius, radius)
|
|
|
|
painter.setClipPath(path)
|
|
|
|
# 如果是动画效果
|
|
|
|
if self.rotateAnimation.state() == QPropertyAnimation.Running:
|
|
|
|
painter.rotate(self._angle) # 旋转
|
|
|
|
painter.drawPixmap(
|
|
|
|
QPointF(-self.pixmap.width() / 2, -self.pixmap.height() // 2), self.pixmap)
|
|
|
|
else:
|
|
|
|
painter.drawPixmap(-int(halfW), -int(halfH), self.pixmap)
|
|
|
|
# 如果在加载
|
|
|
|
if self.loadingTimer.isActive():
|
|
|
|
diameter = 2 * self.pradius
|
|
|
|
painter.setBrush(
|
|
|
|
QColor(45, 140, 240, int((1 - self.pradius / 10) * 255)))
|
|
|
|
painter.setPen(Qt.NoPen)
|
|
|
|
painter.drawRoundedRect(
|
|
|
|
QRectF(-self.pradius, -self.pradius, diameter, diameter), self.pradius, self.pradius)
|
|
|
|
|
|
|
|
def enterEvent(self, event):
|
|
|
|
"""鼠标进入动画
|
|
|
|
:param event:
|
|
|
|
"""
|
|
|
|
if not (self.animation and not self.isGif):
|
|
|
|
return
|
|
|
|
self.rotateAnimation.stop()
|
|
|
|
cv = self.rotateAnimation.currentValue() or self.StartAngle
|
|
|
|
self.rotateAnimation.setDuration(
|
|
|
|
540 if cv == 0 else int(cv / self.EndAngle * 540))
|
|
|
|
self.rotateAnimation.setStartValue(cv)
|
|
|
|
self.rotateAnimation.setEndValue(self.EndAngle)
|
|
|
|
self.rotateAnimation.start()
|
|
|
|
|
|
|
|
def leaveEvent(self, event):
|
|
|
|
"""鼠标离开动画
|
|
|
|
:param event:
|
|
|
|
"""
|
|
|
|
if not (self.animation and not self.isGif):
|
|
|
|
return
|
|
|
|
self.rotateAnimation.stop()
|
|
|
|
cv = self.rotateAnimation.currentValue() or self.EndAngle
|
|
|
|
self.rotateAnimation.setDuration(int(cv / self.EndAngle * 540))
|
|
|
|
self.rotateAnimation.setStartValue(cv)
|
|
|
|
self.rotateAnimation.setEndValue(self.StartAngle)
|
|
|
|
self.rotateAnimation.start()
|
|
|
|
|
|
|
|
def onLoading(self):
|
|
|
|
"""更新进度动画
|
|
|
|
"""
|
|
|
|
if self.loadingTimer.isActive():
|
|
|
|
if self.pradius > 9:
|
|
|
|
self.pradius = 0
|
|
|
|
self.pradius += 1
|
|
|
|
else:
|
|
|
|
self.pradius = 0
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def onFinished(self):
|
|
|
|
"""图片下载完成
|
|
|
|
"""
|
|
|
|
self.loadingTimer.stop()
|
|
|
|
self.pradius = 0
|
|
|
|
reply = self.sender()
|
|
|
|
|
|
|
|
if self.isGif:
|
|
|
|
self._movie = QMovie(reply, b'gif', self)
|
|
|
|
if self._movie.isValid():
|
|
|
|
self._movie.frameChanged.connect(self._resizeGifPixmap)
|
|
|
|
self._movie.start()
|
|
|
|
else:
|
|
|
|
data = reply.readAll().data()
|
|
|
|
reply.deleteLater()
|
|
|
|
del reply
|
|
|
|
self._pixmap.loadFromData(data)
|
|
|
|
if self._pixmap.isNull():
|
|
|
|
self._pixmap = QPixmap(self.size())
|
|
|
|
self._pixmap.fill(QColor(204, 204, 204))
|
|
|
|
self._resizePixmap()
|
|
|
|
|
|
|
|
def onError(self, code):
|
|
|
|
"""下载出错了
|
|
|
|
:param code:
|
|
|
|
"""
|
|
|
|
self._pixmap = QPixmap(self.size())
|
|
|
|
self._pixmap.fill(QColor(204, 204, 204))
|
|
|
|
self._resizePixmap()
|
|
|
|
|
|
|
|
def refresh(self):
|
|
|
|
"""强制刷新
|
|
|
|
"""
|
|
|
|
self._get(self.url)
|
|
|
|
|
|
|
|
def isLoading(self):
|
|
|
|
"""判断是否正在加载
|
|
|
|
"""
|
|
|
|
return self.loadingTimer.isActive()
|
|
|
|
|
|
|
|
def setShape(self, shape):
|
|
|
|
"""设置形状
|
|
|
|
:param shape: 0=圆形, 1=圆角矩形
|
|
|
|
"""
|
|
|
|
self.shape = shape
|
|
|
|
|
2023-11-15 22:32:11 +08:00
|
|
|
def setBytes(self, img_bytes):
|
|
|
|
self._pixmap = QPixmap()
|
|
|
|
if img_bytes[:4] == b'\x89PNG':
|
|
|
|
self._pixmap.loadFromData(img_bytes, format='PNG')
|
|
|
|
else:
|
|
|
|
self._pixmap.loadFromData(img_bytes, format='jfif')
|
|
|
|
self._resizePixmap()
|
|
|
|
|
2023-11-15 21:57:29 +08:00
|
|
|
def setUrl(self, url):
|
|
|
|
"""设置url,可以是本地路径,也可以是网络地址
|
|
|
|
:param url:
|
|
|
|
"""
|
2023-12-04 19:17:24 +08:00
|
|
|
return
|
2023-11-15 21:57:29 +08:00
|
|
|
self.url = url
|
|
|
|
self._get(url)
|
|
|
|
|
|
|
|
def setCacheDir(self, cacheDir=''):
|
|
|
|
"""设置本地缓存路径
|
|
|
|
:param cacheDir:
|
|
|
|
"""
|
|
|
|
self.cacheDir = cacheDir
|
|
|
|
self._initNetWork()
|
|
|
|
|
|
|
|
def setSize(self, size):
|
|
|
|
"""设置固定尺寸
|
|
|
|
:param size:
|
|
|
|
"""
|
|
|
|
if not isinstance(size, QSize):
|
|
|
|
size = self.SizeMedium
|
|
|
|
self.setMinimumSize(size)
|
|
|
|
self.setMaximumSize(size)
|
|
|
|
self._resizePixmap()
|
|
|
|
|
|
|
|
@pyqtProperty(int)
|
|
|
|
def angle(self):
|
|
|
|
return self._angle
|
|
|
|
|
|
|
|
@angle.setter
|
|
|
|
def angle(self, value):
|
|
|
|
self._angle = value
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def _resizePixmap(self):
|
|
|
|
"""缩放图片
|
|
|
|
"""
|
|
|
|
if not self._pixmap.isNull():
|
|
|
|
self.pixmap = self._pixmap.scaled(
|
|
|
|
self.width(), self.height(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def _resizeGifPixmap(self, _):
|
|
|
|
"""缩放动画图片
|
|
|
|
"""
|
|
|
|
if self._movie:
|
|
|
|
self.pixmap = self._movie.currentPixmap().scaled(
|
|
|
|
self.width(), self.height(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def _initNetWork(self):
|
|
|
|
"""初始化异步网络库
|
|
|
|
"""
|
|
|
|
if not hasattr(qApp, '_network'):
|
|
|
|
network = QNetworkAccessManager(self.window())
|
|
|
|
setattr(qApp, '_network', network)
|
|
|
|
# 是否需要设置缓存
|
|
|
|
if self.cacheDir and not qApp._network.cache():
|
|
|
|
cache = QNetworkDiskCache(self.window())
|
|
|
|
cache.setCacheDirectory(self.cacheDir)
|
|
|
|
qApp._network.setCache(cache)
|
|
|
|
|
|
|
|
def _get(self, url):
|
|
|
|
"""设置图片或者请求网络图片
|
|
|
|
:param url:
|
|
|
|
"""
|
|
|
|
if not url:
|
|
|
|
self.onError('')
|
|
|
|
return
|
|
|
|
if url.startswith('http') and not self.loadingTimer.isActive():
|
|
|
|
url = QUrl(url)
|
|
|
|
request = QNetworkRequest(url)
|
|
|
|
# request.setHeader(QNetworkRequest.UserAgentHeader, b'CAvatar')
|
|
|
|
# request.setRawHeader(b'Author', b'Irony')
|
|
|
|
request.setAttribute(
|
|
|
|
QNetworkRequest.FollowRedirectsAttribute, True)
|
|
|
|
if qApp._network.cache():
|
|
|
|
request.setAttribute(
|
|
|
|
QNetworkRequest.CacheLoadControlAttribute, QNetworkRequest.PreferNetwork)
|
|
|
|
request.setAttribute(
|
|
|
|
QNetworkRequest.CacheSaveControlAttribute, True)
|
|
|
|
reply = qApp._network.get(request)
|
|
|
|
self.pradius = 0
|
|
|
|
self.loadingTimer.start(50) # 显示进度动画
|
|
|
|
reply.finished.connect(self.onFinished)
|
|
|
|
reply.error.connect(self.onError)
|
|
|
|
return
|
|
|
|
self.pradius = 0
|
|
|
|
if os.path.exists(url) and os.path.isfile(url):
|
|
|
|
if self.isGif:
|
|
|
|
self._movie = QMovie(url, parent=self)
|
|
|
|
if self._movie.isValid():
|
|
|
|
self._movie.frameChanged.connect(self._resizeGifPixmap)
|
|
|
|
self._movie.start()
|
|
|
|
else:
|
|
|
|
self._pixmap = QPixmap(url)
|
|
|
|
self._resizePixmap()
|
|
|
|
else:
|
|
|
|
self.onError('')
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
import sys
|
|
|
|
from PyQt5.QtWidgets import QApplication
|
|
|
|
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
w = CAvatar(
|
2023-11-15 22:32:11 +08:00
|
|
|
img_bytes=b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x84\x00\x00\x00\x84\x08\x00\x00\x00\x00t/\xdc{\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00\x00 cHRM\x00\x00z&\x00\x00\x80\x84\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17p\x9c\xbaQ<\x00\x00\x00\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x0e\x8fIDATx\xda\xed\x9b[\x8c]\xd7Y\xc7\xbf\xff\xb7\xd6\xde\xfb\x9c3w\xdf&\xbe\xdf\x9d\xa4\xbe\xc4\x89krmI\xdc\x06\xa5i\xa0$\x95\xa0\x95\x02U\x0bBH(\\\xfa\x80\x10<\xf0\xd6\x07\xfa\x80D\x1e\xa0P\xa2\x02-\xa1)%j\x13\xd2\xe6~O\x9a\xc4J\x9a\xc4\x8e\xeb\xcb8\xf8\x9eq\xec\x8c\xe7~\xf6\xdek}\x7f\x1e\xce\x8c\xc7g<c\x8f\x8dP\x04:K\x9a\x87\xd93k\xed\xdf\xf9\xd6w\xfff0&\x1f\xff\xd2\x8f\x1b\xa0\x05\xd1\x82hA\xb4 Z\x10-\x88\x16D\x0b\xa2\x05\xd1\x82hA\xb4 Z\x10-\x88\x16D\x0b\xe2\xe3\x06hA\\.\x04\xe6\xf0\x1c""\x049\xf5\x90\x14\x10 \x85\x8d\xef.\x05\x02"\x94\xcbZJU\x9b8\x81\x02\x08\x1d\xa9J\x15\x11\xcc\xf0Q\xfc\xecG]&\x81\x88P h\x9c\x00\x8a\x88\xd0DL@\n\x00\x9b;\x04\x84\x00\xe2eb\x14\xe2\x05b\x02\x884d!@C4v\xbe(f\x85\xa0\x88ApY\xf2\x88.\x01HN^(\xa1AT"\x01\x88\x9e\x7f\xe2\xec\xd7\x01\x11\x9a\x9f\x90\xe7\xa5-\x1fI:\x00\x93*a\xe6\xa4\xa4\xa6\x9c\xd0\xceikv\xc5\xa48\x970\x10\x86\te\xe2\xac\xbf9e"\x14\xc6(\x92\xb0b\x89D\x03\x80\x08\x08T\x99i\x11\x01^\x92b\x82%\x08/\xd4\x86\xbd\x11r\xa1\xd5 \x84\xc1\x07"\xee\xdc\xb3ce\xee\xa1\xa6$\xd5$\xab\x7f0\xd0\xb9\x04*\xa22\xa9\xb3s\x81\x88\xe2)4\x99\x80\x9f\xc3\xad\x80b\xa0\xa9\xc5\xf0\xcc\xf3\xd9b\xe6\x95\xe0\x84\xa2F\xb5\xe2\x91\xe7\x97\xfe\xe1\x9a\xc2<\x02\x08a\x93s\x99\xfd:\xdc\x81\xe7\xfb\xe1\xa2\xa9QA\x02\x17e\x10\x08\x84j\xeaC\x91?\xbd\xab\xe2\xe8i".\x98+G\xfaN\x1c\x19\x89\xe2\xactP@\x9bN\x9b\x15\x02\xac}\xeb\x89\x93P\x95\t\xfb\xba\x08D\xe3\xb6,\xd4sG\xab\xf5\xf2\xf03C\x88^D\x85J\x89\xffu\xc4wVM\xc49\x13r\x9a\xdb\x9c\xddD\xd1\xbb\xf5\xdfG\x7f\xcfATJ\xc5\x1cl\x15B\x08\x9f\x1e\xde\xb8\xba\x1b5W\x7f*[\xd2\xbdpu\x1b\x18\xb3\x82\xf9\xeb\x03\\?\xdf\xd2\xa2\xf4<\xdf\xf0/\xe01\xfd\x1d{~\xd8sw%w\x14\xa5\x98^\x84\x82""\x8a\r\xdf\xfc\xce\xf2\xcd\x0b\xdfN+\xe3\x0f\x87\xb8\xee\xbek\xcb\x04\xe3\xb5\xf8\xeeK\xf9\xaaO\xb7\xa7uT\x82*\x85l:\xcd\xfd\xc5,g*\x13\x9cy\x7f\xff\xc2\xe5>:\x05\xe7\xa0\x11\x8d\xaf\x85m\xa1o\xcf\xdb\xc7d(]\xb8\xe2\xc6\x1d\xd7\xd4\x9cYR\x0e~{/v\xdce\x89c\xa4\x93(\x8a\xb9YG\xe9\x87zox\xf9\xe4\xc3\xab\xd6\xa4!\xb1\x8b\xdb\x06H\x81\x18\xc2\x8e\xab\xde\xdd\xd7\x7f\xf4x\xfb\x95_Y0\xbf\xcd\x8fV\x10\x92\xfa\xab\xafiu\xad\xef\xff\xa8\xad\xc3k.N\x8c\xde\xe6\x02\x91X\xfb\xc8\x96\xbb\xef\xef{q%9W\xa7I8qq\xe9\xaa\xba\x7f\xe4\xaf\xd0}CI\xa9w\x94\x92\xe4|M\x92\xc1\x1f\xffp\xa0\\\xbaz\xed-\xcb\x02\xc5I\x9c\x93\x89F\xc9\xdb\xf4\xa6u#oEK"/\xae\x97\x13a\x8aP\xab{\x89\x9ai\x10I\xd2h\xa4\xd7M\xcep\xf0\xe0P\xb1\xf7\x89\x87\xfb\xcdT\xa35\xbd\xf7\x02Q\xb42Z\xe9\xbd\xb3\\e\x19\xa3\x83\\,\x88Lz\xd5<d*\xb44/I+-Q\xf1E\xe5\xd3#\xa3Y]\xdf\xda[\x1d\xd9\xb4\x04f\x829\xea\x84\x88y\xf8\xbb\xb6\xf5V\x0c)\x82\xb9\xa8S/k\xf8\x8e\xa6\x83\x1a\x8f\rYf\xa5\xb7\x93A\xc3\xdb\xfd\x8b6%y\x8a\xd1\x8e\xb1\xc5\xbf\x0b\xe6\xfb\x7f\xe1\xdd\xb5\xf7\\\x91Wcd\xb3f^\x00\x02\x1a\x83\xae\xb2\xd2\xab\x01:\xc10\xf9\xb2Y\x82\tPHQ\xba\x91\x93>\xec\xfa\xe6\xa9E\x7f\xb2\xcd\x19\xd3\xa0\xa5y\xdb\xf7\xd0\x01\xae\xfe\xf2\x1as\x05\xc5ISVq\x81\xeb(\x13\x83\x99\xa51\n`\x93\x12\xa4\xc1\x899\x03g\xf4\x1c\xc1\r?\xb87\xb5\xfdeU\x8f\xc9\xday\xae,3Ww\xce\x87\x03\x7f\xb3/n\xf9\xdaV\x9a+t:\xfd\xec\x1e\x93,\xd5\x99(c\xc6\xd2CCC\x14\xce\xd1D\x83\xe8\xcc1-\xa9\xa3~hl,\xf3y\\w\xe7\x1d\xedCY-\x94\xd5\x10\x8b\x9d\x0f\x1c\xf5\xb7\xfd\xc1<\x12\x92\x80\xd6\x1c\x04fuV\x02OD\x07\x19T\x11\x188q\x1dQ\xd5\x04\xe6\xc1\x99<\x18$\xb4]\xb5\xf2\xe4@\x84\xdf\xf6\xa5\xb5C\xbe\xeaL\xbc!>\xfb\xd0\x01\xd7yko\xae\xa9\x16\x89L\x0b\xa2\x82\xd9\xc6\xd4 \xb32d\x05\x1f\x18\xea\xd9\xbe\x99>LlCd\x1c\x1e\xec\xad\x00\xcef\xda\x86 \xbe\xef\x9f_\tze\xd7\xe9p\xd3\xe7{\x05\x9a\x87\xe7\xff\xb1\xdf\xa5\xd6\xd3\xd9\xbd\xf6\xc6++\x88\xe2%\x9ck\xa3~\xfa\x9b\x01S\x98\x90*c\x99\xaf\x8f<\xf2\x83\xb0\xf9\x97\x95\xa1\xf1\xc9A\xd3\xf2\xfb\x8f\xf7\xddq\xcf\xd5\xa9\x988kd\xb2\x84PI\x15C\x89dt\xc9\x8a\x97\x93\xb0/P\x0f\xad\xee\x8db\x95#\x8f\x7f8?J\xc6\xc1\x93\xaf\xfd\xe4\
|
|
|
|
,
|
2023-11-15 21:57:29 +08:00
|
|
|
url='https://wx.qlogo.cn/mmhead/ver_1/DpDqmvTDORNWfLrMj26YicorEUREffl1G8FapawdKgINVH9g1icudfWesGrH9LqeGAz16z4PmkW9U1KAIM3btWgozZ1GaLF66bdKdxlMdazmibn2hpFeiaa4613dN6HM4Vfk/132')
|
|
|
|
w.show()
|
|
|
|
sys.exit(app.exec_())
|