代码
card.py
# coding: utf-8
import sys
from pathlib import Path
from typing import Union
from PySide6.QtCore import QPropertyAnimation, QEasingCurve, Property, Qt, Signal, QEvent
from PySide6.QtGui import QPainter, QPixmap, QImage, QPaintEvent, QPainterPath, QEnterEvent, \
QMouseEvent
from PySide6.QtWidgets import QWidget, QApplication, QMainWindow
class AccordionCard(QWidget):
hovered = Signal(object, bool)
stretchChanged = Signal(object, float)
clicked = Signal()
def __init__(self, image: Union[str, Path, QPixmap, QImage], parent=None):
super().__init__(parent)
self.image = QImage()
self.__stretch = 1.0
self.__radius = 5.0
self.animation = QPropertyAnimation(self, b'stretch', self)
self.animation.setDuration(200)
self.animation.setEasingCurve(QEasingCurve.Type.OutQuart)
self.animation.valueChanged.connect(lambda value: self.stretchChanged.emit(self, value))
self.setImage(image)
def setImage(self, image: Union[str, Path, QPixmap, QImage]):
if isinstance(image, str):
image = QImage(image)
elif isinstance(image, Path):
image = QImage(image.as_posix())
elif isinstance(image, QPixmap):
image = image.toImage()
self.image = image
def getStretch(self) -> float:
return self.__stretch
def setStretch(self, stretch: float):
if self.__stretch == stretch:
return
self.__stretch = stretch
self.updateGeometry()
def setRadius(self, radius: float):
self.__radius = radius
self.update()
def getRadius(self) -> float:
return self.__radius
def setTargetStretch(self, stretch: float):
if self.animation.state() == QPropertyAnimation.State.Running:
self.animation.stop()
self.animation.setStartValue(self.stretch)
self.animation.setEndValue(stretch)
self.animation.start()
def enterEvent(self, event: QEnterEvent):
self.hovered.emit(self, True)
super().enterEvent(event)
def leaveEvent(self, event: QEvent):
self.hovered.emit(self, False)
super().leaveEvent(event)
def mouseReleaseEvent(self, event: QMouseEvent):
if event.button() == Qt.MouseButton.LeftButton:
self.clicked.emit()
return super().mouseReleaseEvent(event)
def paintEvent(self, event: QPaintEvent):
painter = QPainter(self)
painter.setRenderHints(
QPainter.RenderHint.Antialiasing |
QPainter.RenderHint.SmoothPixmapTransform |
QPainter.RenderHint.LosslessImageRendering
)
path = QPainterPath()
path.addRoundedRect(self.rect(), self.radius, self.radius)
painter.setClipPath(path)
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))
painter.drawImage(self.rect(), image)
stretch = Property(float, getStretch, setStretch)
radius = Property(float, getRadius, setRadius)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = QMainWindow()
window.resize(400, 400)
window.setContentsMargins(20, 20, 20, 20)
card = AccordionCard('img/FvCpS5naYAA06ej.jpg')
card.hovered.connect(print)
window.setCentralWidget(card)
window.show()
sys.exit(app.exec())
widget.py
# coding: utf-8
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout
from card import AccordionCard
class AccordionWidget(QWidget):
def __init__(self, orientation=Qt.Orientation.Horizontal, parent=None):
super().__init__(parent)
self._cards = []
self._cardStretch = {}
if orientation == Qt.Orientation.Horizontal:
self.setLayout(QHBoxLayout())
else:
self.setLayout(QVBoxLayout())
self.layout().setSpacing(5)
def addCard(self, card: AccordionCard):
card.setStretch(1.0)
card.hovered.connect(self.onCardHovered)
card.stretchChanged.connect(self.onCardStretchChanged)
card.clicked.connect(lambda c=card: print(c, "Card clicked"))
self._cards.append(card)
self._cardStretch[card] = 1.0
self.layout().addWidget(card)
def onCardHovered(self, card: AccordionCard, hovered: bool):
expanded_stretch = 5.0
default_stretch = 1.0
if hovered:
for c in self._cards:
if c == card:
card.setTargetStretch(expanded_stretch)
else:
c.setTargetStretch(default_stretch)
else:
for c in self._cards:
c.setTargetStretch(default_stretch)
def onCardStretchChanged(self, card: AccordionCard, stretch: float):
self._cardStretch[card] = stretch
self.updateLayoutStretches()
def updateLayoutStretches(self):
if not self._cards:
return
for index, card in enumerate(self._cards):
layout_stretch = int(self._cardStretch[card] * 1000)
# print(f"Card {index} current stretch: {self._cardStretch[card]} layout stretch factor: {layout_stretch}")
self.layout().setStretch(index, layout_stretch)
main.py
# coding: utf-8
from pathlib import Path
from PySide6.QtCore import Qt
from card import AccordionCard
from widget import AccordionWidget
from PySide6.QtWidgets import QApplication, QMainWindow
import sys
app = QApplication(sys.argv)
window = QMainWindow()
window.resize(800, 600)
accordion = AccordionWidget(Qt.Orientation.Vertical, parent=window)
for file in Path('./img').glob('*.jpg'):
card = AccordionCard(file)
accordion.addCard(card)
window.setCentralWidget(accordion)
window.setContentsMargins(20, 20, 20, 20)
window.show()
sys.exit(app.exec())
运行
复制后,运行main.py
预览效果
项目参考
参考c++项目
gitee: https://gitee.com/chiyaun/Anime_carousel
github: https://github.com/daishuboluo/Anime_carousel.git