class AnimatedButton(QToolButton): def __init__(self, on_tick_func, anim_start=0, anim_end=255, anim_duration=300, parent=None): super().__init__(parent=parent) self._opacity = 0 self.anim_start = anim_start self.anim_end = anim_end self.anim_duration = anim_duration self.anim = QPropertyAnimation(self, b'opacity') self.anim.setDuration(anim_duration) self.anim.setStartValue(anim_start) self.anim.setEndValue(anim_end) self.on_tick = on_tick_func def set_anim_start(self, val): self.anim_start = val self.anim.setStartValue(val) def set_anim_end(self, val): self.anim_end = val self.anim.setEndValue(val) def set_anim_duration(self, val): self.anim_duration = val self.anim.setDuration(val) def get_opacity(self): return self._opacity def set_opacity(self, value): self._opacity = value if value != 1: self.on_tick(self, value) opacity = pyqtProperty('int', get_opacity, set_opacity) def enterEvent(self, event): self.anim.stop() self.anim.setStartValue(self.anim.currentValue()) self.anim.setEndValue(self.anim_end) self.anim.start() super().enterEvent(event) def leaveEvent(self, event): self.anim.stop() self.anim.setStartValue(self.anim.currentValue()) self.anim.setEndValue(self.anim_start) self.anim.start() super().leaveEvent(event)
class PushButtonFont(QPushButton): LoadingText = "\uf110" def __init__(self, *args, **kwargs): super(PushButtonFont, self).__init__(*args, **kwargs) self.fontSize = self.font().pointSize() * 2 self._rotateAnimationStarted = False self._rotateAnimation = QPropertyAnimation(self) self._rotateAnimation.setTargetObject(self) self._rotateAnimation.setStartValue(1) self._rotateAnimation.setEndValue(12) self._rotateAnimation.setDuration(1000) self._rotateAnimation.setLoopCount(-1) # 无限循环 self._rotateAnimation.valueChanged.connect(self.update) self.clicked.connect(self._onClick) def paintEvent(self, _): option = QStyleOptionButton() self.initStyleOption(option) painter = QStylePainter(self) if self._rotateAnimationStarted: option.text = "" painter.drawControl(QStyle.CE_PushButton, option) if not self._rotateAnimationStarted: return painter.save() font = self.font() font.setPointSize(self.fontSize) font.setFamily("FontAwesome") painter.setFont(font) # 变换坐标为正中间 painter.translate(self.rect().center()) # 旋转90度 painter.rotate(self._rotateAnimation.currentValue() * 30) fm = self.fontMetrics() # 在变换坐标后的正中间画文字 w = fm.width(self.LoadingText) h = fm.height() painter.drawText( QRectF(0 - w * 2, 0 - h, w * 2 * 2, h * 2), Qt.AlignCenter, self.LoadingText) painter.restore() def _onClick(self): if self._rotateAnimationStarted: self._rotateAnimationStarted = False self._rotateAnimation.stop() return self._rotateAnimationStarted = True self._rotateAnimation.start() def update(self, _=None): super(PushButtonFont, self).update()
class Window(QWidget): def __init__(self, *args, **kwargs): super(Window, self).__init__(*args, **kwargs) layout = QVBoxLayout(self) layout.addWidget(QPushButton('start', self, clicked=self.onStart)) layout.addWidget(QPushButton('pause', self, clicked=self.onPause)) layout.addWidget(QPushButton('resume', self, clicked=self.onResume)) self._offset = 0 self.pani = QPropertyAnimation(self, b'offset', self) self.pani.setDuration(540) self.pani.setLoopCount(1) self.pani.setStartValue(0) self.pani.setEndValue(360) def onPause(self): self.pani.stop() v = self.pani.currentValue() print('current value:', v, 'duration:', int(v / 360 * 540)) self.pani.setDuration(int(v / 360 * 540)) self.pani.setStartValue(v) self.pani.setEndValue(0) def onResume(self): self.pani.start() def onStart(self): self.pani.start() @pyqtProperty(int) def offset(self): return self._offset @offset.setter def offset(self, o): print(o) self._offset = o
class RotateButton(QPushButton): STARTVALUE = 0 # 起始旋转角度 ENDVALUE = 360 # 结束旋转角度 DURATION = 540 # 动画完成总时间 def __init__(self, *args, image='', **kwargs): super(RotateButton, self).__init__(*args, **kwargs) self.setCursor(Qt.PointingHandCursor) self._angle = 0 # 角度 self._padding = 10 # 阴影边距 self._image = '' # 图片路径 self._shadowColor = QColor(33, 33, 33) # 阴影颜色 self._pixmap = None # 图片对象 # 属性动画 self._animation = QPropertyAnimation(self, b'angle', self) self._animation.setLoopCount(1) # 只循环一次 self.setPixmap(image) # 绑定提示框 #ToolTip.bind(self) def paintEvent(self, event): """绘制事件""" text = self.text() option = QStyleOptionButton() self.initStyleOption(option) option.text = '' # 不绘制文字 painter = QStylePainter(self) painter.setRenderHint(QStylePainter.Antialiasing) painter.setRenderHint(QStylePainter.HighQualityAntialiasing) painter.setRenderHint(QStylePainter.SmoothPixmapTransform) painter.drawControl(QStyle.CE_PushButton, option) # 变换坐标为正中间 painter.translate(self.rect().center()) painter.rotate(self._angle) # 旋转 # 绘制图片 if self._pixmap and not self._pixmap.isNull(): w = self.width() h = self.height() pos = QPointF(-self._pixmap.width() / 2, -self._pixmap.height() / 2) painter.drawPixmap(pos, self._pixmap) elif text: # 在变换坐标后的正中间画文字 fm = self.fontMetrics() w = fm.width(text) h = fm.height() rect = QRectF(0 - w * 2, 0 - h, w * 2 * 2, h * 2) painter.drawText(rect, Qt.AlignCenter, text) else: super(RotateButton, self).paintEvent(event) def enterEvent(self, _): """鼠标进入事件""" # 设置阴影 # 边框阴影效果 ''' effect = QGraphicsDropShadowEffect(self) effect.setBlurRadius(self._padding * 2) effect.setOffset(0, 0) effect.setColor(self._shadowColor) self.setGraphicsEffect(effect) ''' # 开启旋转动画 self._animation.stop() cv = self._animation.currentValue() or self.STARTVALUE self._animation.setDuration(self.DURATION if cv == 0 else int(cv / self.ENDVALUE * self.DURATION)) self._animation.setStartValue(cv) self._animation.setEndValue(self.ENDVALUE) self._animation.start() def leaveEvent(self, _): """鼠标离开事件""" # 取消阴影 self.setGraphicsEffect(None) # 旋转动画 self._animation.stop() cv = self._animation.currentValue() or self.ENDVALUE self._animation.setDuration(int(cv / self.ENDVALUE * self.DURATION)) self._animation.setStartValue(cv) self._animation.setEndValue(self.STARTVALUE) self._animation.start() def setPixmap(self, path): if not os.path.exists(path): self._image = '' self._pixmap = None return self._image = path size = min(self.width(), self.height()) - self.padding # 需要边距的边框 radius = int(size / 2) image = QImage(size, size, QImage.Format_ARGB32_Premultiplied) image.fill(Qt.transparent) # 填充背景为透明 pixmap = QPixmap(path).scaled(size, size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) # QPainter painter = QPainter() painter.begin(image) painter.setRenderHint(QPainter.Antialiasing, True) painter.setRenderHint(QPainter.SmoothPixmapTransform, True) # QPainterPath path = QPainterPath() path.addRoundedRect(0, 0, size, size, radius, radius) # 切割圆 painter.setClipPath(path) painter.drawPixmap(0, 0, pixmap) painter.end() self._pixmap = QPixmap.fromImage(image) self.update() def pixmap(self): return self._pixmap @pyqtProperty(str) def image(self): return self._image @image.setter def image(self, path): self.setPixmap(path) @pyqtProperty(int) def angle(self): return self._angle @angle.setter def angle(self, value): self._angle = value self.update() @pyqtProperty(int) def padding(self): return self._padding @padding.setter def padding(self, value): self._padding = value @pyqtProperty(QColor) def shadowColor(self): return self._shadowColor @shadowColor.setter def shadowColor(self, color): self._shadowColor = QColor(color)
class CAvatar(QWidget): Circle = 0 # 圆圈 Rectangle = 1 # 圆角矩形 SizeLarge = QSize(128, 128) SizeMedium = QSize(64, 64) SizeSmall = QSize(32, 32) StartAngle = 0 # 起始旋转角度 EndAngle = 360 # 结束旋转角度 def __init__(self, *args, shape=0, url='', cacheDir=False, size=QSize(64, 64), animation=False, **kwargs): 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) self.setUrl(url) 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, (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 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('')