WeChatMsg/app/components/CAvatar.py

298 lines
21 KiB
Python
Raw Normal View History

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:
"""
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_())