Exemple #1
0
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
Exemple #2
0
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()