class BasicSongCard(QWidget): """ 歌曲卡基类 """ clicked = pyqtSignal(int) doubleClicked = pyqtSignal(int) playButtonClicked = pyqtSignal(int) addSongToPlayingSig = pyqtSignal(dict) checkedStateChanged = pyqtSignal(int, bool) addSongsToCustomPlaylistSig = pyqtSignal(str, list) def __init__(self, songInfo: dict, songCardType, parent=None): """ 实例化歌曲卡 Parameters ---------- songInfo: dict 歌曲信息字典 songCardType: `~SongCardType` 歌曲卡类型 parent: 父级 """ super().__init__(parent) self._getInfo(songInfo) self.__resizeTime = 0 self.__songCardType = songCardType # 歌曲卡类型 self.__SongNameCard = [SongTabSongNameCard, TrackNumSongNameCard][songCardType.value] # 初始化各标志位 self.isSongExist = True self.isPlaying = False self.isSelected = False self.isChecked = False self.isInSelectionMode = False self.isDoubleClicked = False # 记录songCard对应的item的下标 self.itemIndex = None # 创建小部件 if self.__songCardType == SongCardType.SONG_TAB_SONG_CARD: self.songNameCard = self.__SongNameCard(self.songName, self) elif self.__songCardType == SongCardType.ALBUM_INTERFACE_SONG_CARD: self.songNameCard = self.__SongNameCard(self.songName, self.tracknumber, self) self.__referenceWidgets() # 初始化小部件列表 self.__scaleableLabelTextWidth_list = [] # 可拉伸的标签的文本的宽度列表 self.__scaleableWidgetMaxWidth_list = [] # 可拉伸部件的最大宽度列表 self.__dynamicStyleLabel_list = [] self.__scaleableWidget_list = [] self.__clickableLabel_list = [] self.__labelSpacing_list = [] self.__label_list = [] self.__widget_list = [] # 存放所有的小部件 # 创建动画组和动画列表 self.aniGroup = QParallelAnimationGroup(self) self.__aniWidget_list = [] self.__deltaX_list = [] self.ani_list = [] # 安装事件过滤器 self.installEventFilter(self) # 信号连接到槽 self.playButton.clicked.connect(self.playButtonSlot) self.addToButton.clicked.connect(self.__showAddToMenu) self.checkBox.stateChanged.connect(self.checkedStateChangedSlot) def _getInfo(self, songInfo: dict): """ 从歌曲信息字典中获取信息 """ self.songInfo = songInfo self.songPath = songInfo.get("songPath", "") # type:str self.songName = songInfo.get("songName", "未知歌曲") # type:str self.songer = songInfo.get("songer", "未知歌手") # type:str self.album = songInfo.get("album", "未知专辑") # type:str self.year = songInfo.get("year", "未知年份") # type:str self.tcon = songInfo.get("tcon", "未知流派") # type:str self.duration = songInfo.get("duration", "0:00") # type:str self.tracknumber = songInfo.get("tracknumber", "0") # type:str def setScalableWidgets(self, scaleableWidget_list: list, scalebaleWidgetWidth_list: list, fixedWidth=0): """ 设置可随着歌曲卡的伸缩而伸缩的标签 Parameters ---------- scaleableWidget_list : 随着歌曲卡的伸缩而伸缩的小部件列表,要求第一个元素为歌名卡,后面的元素都是label scaleableWidgetWidth_list : 与可伸缩小部件相对应的小部件初始长度列表 fixedWidth : 为其他不可拉伸的小部件保留的宽度 """ if self.__scaleableWidgetMaxWidth_list: return self.__checkIsLengthEqual(scaleableWidget_list, scalebaleWidgetWidth_list) # 必须先将所有标签添加到列表中后才能调用这个函数 if not self.__label_list: raise Exception("必须先调用addLabels函数将标签添加到窗口中") # 歌名卡默认可拉伸 self.__scaleableWidget_list = scaleableWidget_list self.__scaleableWidgetMaxWidth_list = scalebaleWidgetWidth_list # 计算初始宽度 initWidth = (sum(self.__scaleableWidgetMaxWidth_list) + sum(self.__labelSpacing_list) + fixedWidth) self.resize(initWidth, 60) self.setFixedHeight(60) def addLabels(self, label_list: list, labelSpacing_list: list): """ 往歌曲卡中添加除了歌曲名卡之外的标签,只能初始化一次标签列表 Paramerter ---------- label_list : 歌名卡后的标签列表 labelSpacing_list : 每个标签的前置空白 """ if self.__label_list: return self.__checkIsLengthEqual(label_list, labelSpacing_list) self.__label_list = label_list self.__labelSpacing_list = labelSpacing_list self.__widget_list = [self.songNameCard] + self.__label_list # 移动小部件 for i in range(len(label_list)): label_list[i] def setDynamicStyleLabels(self, label_list: list): """ 设置需要动态更新样式的标签列表 """ self.__dynamicStyleLabel_list = label_list def setClickableLabels(self, clickableLabel_list: list): """ 设置可点击的标签列表 """ self.__clickableLabel_list = clickableLabel_list # 分配ID for label in self.__clickableLabel_list: label.setObjectName("clickableLabel") def setSelected(self, isSelected: bool): """ 设置选中状态 """ self.isSelected = isSelected if isSelected: self.setWidgetState("selected-leave") self.setCheckBoxBtLabelState("selected") else: self.songNameCard.setWidgetHidden(True) self.setWidgetState("notSelected-leave") state = "notSelected-play" if self.isPlaying else "notSelected-notPlay" self.setCheckBoxBtLabelState(state) self.setStyle(QApplication.style()) def setPlay(self, isPlay: bool): """ 设置播放状态并更新样式 """ self.isPlaying = isPlay self.isSelected = isPlay # 判断歌曲文件是否存在 self.isSongExist = os.path.exists(self.songPath) if isPlay: self.isSelected = True self.setCheckBoxBtLabelState("selected") self.setWidgetState("selected-leave") else: self.setCheckBoxBtLabelState("notSelected-notPlay") self.setWidgetState("notSelected-leave") self.songNameCard.setPlay(isPlay, self.isSongExist) self.setStyle(QApplication.style()) def setCheckBoxBtLabelState(self, state: str): """ 设置复选框、按钮和标签动态属性 Parameters ---------- state: str 复选框、按钮和标签的状态,可以是: * `notSelected-notPlay` * `notSelected-play` * `selected` """ self.songNameCard.setCheckBoxBtLabelState(state, self.isSongExist) for label in self.__dynamicStyleLabel_list: label.setProperty("state", state) def setWidgetState(self, state: str): """ 设置按钮组窗口和自己的状态 Parameters ---------- state: str 窗口状态,可以是: * `notSelected-leave` * `notSelected-enter` * `notSelected-pressed` * `selected-leave` * `selected-enter` * `selected-pressed` """ self.songNameCard.setButtonGroupState(state) self.setProperty("state", state) def setAnimation(self, aniWidget_list: list, deltaX_list: list): """ 设置小部件的动画 Parameters ---------- aniWidget_list : 需要设置动画的小部件列表 deltaX_list : 和aniWidget_list相对应的动画位置偏移量列表 """ self.__checkIsLengthEqual(aniWidget_list, deltaX_list) self.__aniWidget_list = aniWidget_list self.__deltaX_list = deltaX_list # 清空动画组的内容 self.ani_list.clear() self.aniGroup.clear() self.__ani_list = [ QPropertyAnimation(widget, b"geometry") for widget in self.__aniWidget_list ] # 初始化动画 for ani in self.__ani_list: ani.setDuration(400) ani.setEasingCurve(QEasingCurve.OutQuad) self.aniGroup.addAnimation(ani) self.getAniTargetX_list() def getAniTargetX_list(self): """ 计算动画的初始值 """ self.__aniTargetX_list = [] for deltaX, widget in zip(self.__deltaX_list, self.__aniWidget_list): self.__aniTargetX_list.append(deltaX + widget.x()) def eventFilter(self, obj, e: QEvent): """ 安装监听 """ if obj == self: if e.type() == QEvent.Enter: self.songNameCard.checkBox.show() self.songNameCard.buttonGroup.setHidden(self.isInSelectionMode) state = "selected-enter" if self.isSelected else "notSelected-enter" self.setWidgetState(state) self.setStyle(QApplication.style()) elif e.type() == QEvent.Leave: # 不处于选择模式下时,如果歌曲卡没被选中而鼠标离开窗口就隐藏复选框和按钮组窗口 if not self.isSelected: self.songNameCard.buttonGroup.hide() self.songNameCard.checkBox.setHidden( not self.isInSelectionMode) state = "selected-leave" if self.isSelected else "notSelected-leave" self.setWidgetState(state) self.setStyle(QApplication.style()) elif e.type() == QEvent.MouseButtonPress: state = "selected-pressed" if self.isSelected else "notSelected-pressed" if e.button() == Qt.LeftButton: self.isSelected = True self.setWidgetState(state) self.setStyle(QApplication.style()) elif e.type() == QEvent.MouseButtonRelease and e.button( ) == Qt.LeftButton: self.setWidgetState("selected-leave") self.setCheckBoxBtLabelState("selected") # 鼠标松开时将设置标签为白色 self.setStyle(QApplication.style()) elif e.type() == QEvent.MouseButtonDblClick: self.isDoubleClicked = True return super().eventFilter(obj, e) def mousePressEvent(self, e): """ 鼠标按下时移动小部件 """ super().mousePressEvent(e) # 移动小部件 if self.aniGroup.state() == QAbstractAnimation.Stopped: for deltaX, widget in zip(self.__deltaX_list, self.__aniWidget_list): widget.move(widget.x() + deltaX, widget.y()) else: self.aniGroup.stop() # 强制停止还未结束的动画 for targetX, widget in zip(self.__aniTargetX_list, self.__aniWidget_list): widget.move(targetX, widget.y()) def mouseReleaseEvent(self, e: QMouseEvent): """ 鼠标松开时开始动画 """ for ani, widget, deltaX in zip(self.__ani_list, self.__aniWidget_list, self.__deltaX_list): ani.setStartValue( QRect(widget.x(), widget.y(), widget.width(), widget.height())) ani.setEndValue( QRect(widget.x() - deltaX, widget.y(), widget.width(), widget.height())) self.aniGroup.start() if e.button() == Qt.LeftButton: self.clicked.emit(self.itemIndex) # 左键点击时才发送信号 if self.isDoubleClicked and e.button() == Qt.LeftButton: self.isDoubleClicked = False if not self.isPlaying: # 发送点击信号 self.aniGroup.finished.connect(self.__aniFinishedSlot) def __aniFinishedSlot(self): """ 动画完成时发出双击信号 """ self.doubleClicked.emit(self.itemIndex) self.aniGroup.disconnect() def setClickableLabelCursor(self, cursor): """ 设置可点击标签的光标样式 """ for label in self.__clickableLabel_list: label.setCursor(cursor) def __referenceWidgets(self): """ 引用小部件 """ self.buttonGroup = self.songNameCard.buttonGroup self.playButton = self.songNameCard.playButton self.addToButton = self.songNameCard.addToButton self.checkBox = self.songNameCard.checkBox def playButtonSlot(self): """ 播放按钮按下时更新样式 """ self.playButtonClicked.emit(self.itemIndex) def checkedStateChangedSlot(self): """ 复选框选中状态改变对应的槽函数 """ self.isChecked = self.checkBox.isChecked() self.setSelected(self.isChecked) # 只要点击了复选框就进入选择模式,由父级控制退出选择模式 self.checkBox.show() self.setSelectionModeOpen(True) # 发出选中状态改变信号 self.checkedStateChanged.emit(self.itemIndex, self.isChecked) def setSelectionModeOpen(self, isOpenSelectionMode: bool): """ 设置是否进入选择模式, 处于选择模式下复选框一直可见,按钮不管是否处于选择模式都不可见 """ if self.isInSelectionMode == isOpenSelectionMode: return # 更新标志位 self.isInSelectionMode = isOpenSelectionMode # 设置按钮和复选框的可见性 self.checkBox.setHidden(not isOpenSelectionMode) self.buttonGroup.setHidden(True) def setChecked(self, isChecked: bool): """ 设置歌曲卡选中状态 """ self.checkBox.setChecked(isChecked) def updateSongCard(self): """ 更新歌曲卡 """ # 必须被子类重写 raise NotImplementedError def __checkIsLengthEqual(self, list_1: list, list_2: list): """ 检查输入的两个列表的长度是否相等,不相等则引发错误 """ if len(list_1) != len(list_2): raise Exception("两个列表的长度必须一样") def resizeEvent(self, e): """ 改变窗口大小时移动标签 """ self.__resizeTime += 1 if self.__resizeTime > 1: # 分配多出来的宽度 deltaWidth = self.width() - self.__originalWidth self.__originalWidth = self.width() equalWidth = int(deltaWidth / len(self.__scaleableWidget_list)) self.__scaleableWidgetMaxWidth_list = [ i + equalWidth for i in self.__scaleableWidgetMaxWidth_list ] else: self.__originalWidth = self.width() # 调整小部件宽度 self.adjustWidgetWidth() # 移动标签 x = self.songNameCard.width() for label, spacing in zip(self.__label_list, self.__labelSpacing_list): # 如果标签是可变长度的,就将width设置为其最大可变宽度 if label in self.__scaleableWidget_list: index = self.__scaleableWidget_list.index(label) width = self.__scaleableWidgetMaxWidth_list[index] else: width = label.width() label.move(x + spacing, 20) x = width + label.x() # 更新动画目标移动位置 self.getAniTargetX_list() def adjustWidgetWidth(self): """ 调整小部件宽度 """ # 计算标签的宽度 self.__getScaleableLabelTextWidth() self.songNameCard.resize(self.__scaleableWidgetMaxWidth_list[0], 60) for i in range(1, len(self.__scaleableWidget_list)): label = self.__scaleableWidget_list[i] textWidth = self.__scaleableLabelTextWidth_list[i - 1] maxLabelWidth = self.__scaleableWidgetMaxWidth_list[i] width = maxLabelWidth if textWidth > maxLabelWidth else textWidth label.setFixedWidth(width) def __getScaleableLabelTextWidth(self): """ 计算可拉伸的标签的文本宽度 """ fontMetrics = QFontMetrics(QFont("Microsoft YaHei", 9)) self.__scaleableLabelTextWidth_list = [ fontMetrics.width(label.text()) for label in self.__scaleableWidget_list[1:] ] def __showAddToMenu(self): """ 显示添加到菜单 """ addToMenu = AddToMenu(parent=self) addToGlobalPos = self.mapToGlobal(QPoint(0, 0)) + QPoint( self.addToButton.x() + self.buttonGroup.x(), 0) x = addToGlobalPos.x() + self.addToButton.width() + 5 y = addToGlobalPos.y() + int(self.addToButton.height() / 2 - (13 + 38 * addToMenu.actionCount()) / 2) addToMenu.playingAct.triggered.connect( lambda: self.addSongToPlayingSig.emit(self.songInfo)) addToMenu.addSongsToPlaylistSig.connect( lambda name: self.addSongsToCustomPlaylistSig.emit( name, [self.songInfo])) addToMenu.exec(QPoint(x, y)) @property def widget_list(self) -> list: """ 返回窗口内的所有小部件组成的列表 """ return self.__widget_list @property def label_list(self) -> list: """ 返回窗口内所有标签组成的列表 """ return self.__label_list
class RubberBandButton(QPushButton): def __init__(self, *args, **kwargs): super(RubberBandButton, self).__init__(*args, **kwargs) self.setFlat(True) self.setCursor(Qt.PointingHandCursor) self._width = 0 self._height = 0 self._bgcolor = QColor(Qt.green) def paintEvent(self, event): self._initAnimate() painter = QStylePainter(self) painter.setRenderHint(QPainter.Antialiasing, True) painter.setRenderHint(QPainter.HighQualityAntialiasing, True) painter.setRenderHint(QPainter.SmoothPixmapTransform, True) painter.setBrush(QColor(self._bgcolor)) painter.setPen(QColor(self._bgcolor)) painter.drawEllipse(QRectF( (self.minimumWidth() - self._width) / 2, (self.minimumHeight() - self._height) / 2, self._width, self._height )) # 绘制本身的文字和图标 options = QStyleOptionButton() options.initFrom(self) size = options.rect.size() size.transpose() options.rect.setSize(size) options.features = QStyleOptionButton.Flat options.text = self.text() options.icon = self.icon() options.iconSize = self.iconSize() painter.drawControl(QStyle.CE_PushButton, options) event.accept() def _initAnimate(self): if hasattr(self, '_animate'): return self._width = self.minimumWidth() * 7 / 8 self._height = self.minimumHeight() * 7 / 8 # self._width=175 # self._height=175 wanimate = QPropertyAnimation(self, b'rWidth') wanimate.setEasingCurve(QEasingCurve.OutElastic) wanimate.setDuration(700) wanimate.valueChanged.connect(self.update) wanimate.setKeyValueAt(0, self._width) # wanimate.setKeyValueAt(0.1, 180) # wanimate.setKeyValueAt(0.2, 185) # wanimate.setKeyValueAt(0.3, 190) # wanimate.setKeyValueAt(0.4, 195) wanimate.setKeyValueAt(0.5, self._width + 6) # wanimate.setKeyValueAt(0.6, 195) # wanimate.setKeyValueAt(0.7, 190) # wanimate.setKeyValueAt(0.8, 185) # wanimate.setKeyValueAt(0.9, 180) wanimate.setKeyValueAt(1, self._width) hanimate = QPropertyAnimation(self, b'rHeight') hanimate.setEasingCurve(QEasingCurve.OutElastic) hanimate.setDuration(700) hanimate.setKeyValueAt(0, self._height) # hanimate.setKeyValueAt(0.1, 170) # hanimate.setKeyValueAt(0.3, 165) hanimate.setKeyValueAt(0.5, self._height - 6) # hanimate.setKeyValueAt(0.7, 165) # hanimate.setKeyValueAt(0.9, 170) hanimate.setKeyValueAt(1, self._height) self._animate = QParallelAnimationGroup(self) self._animate.addAnimation(wanimate) self._animate.addAnimation(hanimate) def enterEvent(self, event): super(RubberBandButton, self).enterEvent(event) self._animate.stop() self._animate.start() @pyqtProperty(int) def rWidth(self): return self._width @rWidth.setter def rWidth(self, value): self._width = value @pyqtProperty(int) def rHeight(self): return self._height @rHeight.setter def rHeight(self, value): self._height = value @pyqtProperty(QColor) def bgColor(self): return self._bgcolor @bgColor.setter def bgColor(self, color): self._bgcolor = QColor(color)
class QtBubbleLabel(QWidget): BackgroundColor = QColor(195, 195, 195) BorderColor = QColor(150, 150, 150) def __init__(self, *args, **kwargs): super(QtBubbleLabel, self).__init__(*args, **kwargs) self.setWindowFlags(Qt.Window | Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.X11BypassWindowManagerHint) self.setMinimumWidth(200) self.setMinimumHeight(48) self.setAttribute(Qt.WA_TranslucentBackground, True) layout = QVBoxLayout(self) layout.setContentsMargins(8, 8, 8, 16) self.label = QLabel(self) layout.addWidget(self.label) self._desktop = QApplication.instance().desktop() self.animationGroup = QParallelAnimationGroup(self) def setText(self, text): self.label.setText(text) def text(self): return self.label.text() def stop(self): self.hide() self.animationGroup.stop() self.animationGroup.clear() self.close() def show(self): super(QtBubbleLabel, self).show() x = self.parent().geometry().x() y = self.parent().geometry().y() x2 = self.parent().size().width() y2 = self.parent().size().height() startPos = QPoint(x + int(x2 / 2) - int(self.width() / 2), y + int(y2 / 2)) endPos = QPoint(x + int(x2 / 2) - int(self.width() / 2), y + int(y2 / 2) - self.height() * 3 - 5) self.move(startPos) # 初始化动画 self.initAnimation(startPos, endPos) def initAnimation(self, startPos, endPos): # 透明度动画 opacityAnimation = QPropertyAnimation(self, b"opacity") opacityAnimation.setStartValue(1.0) opacityAnimation.setEndValue(0.0) # 设置动画曲线 opacityAnimation.setEasingCurve(QEasingCurve.InQuad) opacityAnimation.setDuration(3000) # 在4秒的时间内完成 # 往上移动动画 moveAnimation = QPropertyAnimation(self, b"pos") moveAnimation.setStartValue(startPos) moveAnimation.setEndValue(endPos) moveAnimation.setEasingCurve(QEasingCurve.InQuad) moveAnimation.setDuration(4000) # 在5秒的时间内完成 # 并行动画组(目的是让上面的两个动画同时进行) self.animationGroup.addAnimation(opacityAnimation) self.animationGroup.addAnimation(moveAnimation) self.animationGroup.finished.connect(self.close) # 动画结束时关闭窗口 self.animationGroup.start() def paintEvent(self, event): super(QtBubbleLabel, self).paintEvent(event) painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) # 抗锯齿 rectPath = QPainterPath() # 圆角矩形 height = self.height() - 8 # 往上偏移8 rectPath.addRoundedRect(QRectF(0, 0, self.width(), height), 5, 5) x = self.width() / 5 * 4 # 边框画笔 painter.setPen( QPen(self.BorderColor, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) # 背景画刷 painter.setBrush(self.BackgroundColor) # 绘制形状 painter.drawPath(rectPath) def windowOpacity(self): return super(QtBubbleLabel, self).windowOpacity() def setWindowOpacity(self, opacity): super(QtBubbleLabel, self).setWindowOpacity(opacity) opacity = pyqtProperty(float, windowOpacity, setWindowOpacity) def ShowMsg(self, text): self.stop() self.setText(text) self.setStyleSheet("color:black") self.show() @staticmethod def ShowMsgEx(owner, text): data = QtBubbleLabel(owner) data.setText(text) data.setStyleSheet("color:black") data.show() def ShowError(self, text): self.stop() self.setText(text) self.setStyleSheet("color:red") self.show() @staticmethod def ShowErrorEx(owner, text): data = QtBubbleLabel(owner) data.setText(text) data.setStyleSheet("color:red") data.show()