class AngleSet(QWidget): acted = pyqtSignal(int, BtnSource) def __init__(self, source, parent=None): super().__init__(parent) self.source = source self.active = 0 self.is_limited = False self.limit = 4 if self.source is BtnSource.HUNDREDS else 10 self.setMinimumSize(100, 105 + self.limit * 50) self.anim_gp = QParallelAnimationGroup() self.anim_gp.finished.connect(self.post_animation) y_offset = 55 self.digits = [] self.digits_pos = [idx for idx in range(self.limit)] for digit in range(self.limit): tmp_btn = AngleButton(digit, self) tmp_btn.move(0, y_offset) y_offset = y_offset + 50 tmp_btn.pressed.connect(self.switch_active) self.digits.append(tmp_btn) @pyqtSlot(int) def switch_active(self, target=None): if target is None: target = -1 for index in range(len(self.digits)): self.active = target self.digits[index].activate(False) return for index in range(len(self.digits)): cur = self.digits[index] if cur.index == self.active: cur.activate(False) if cur.index == target: cur.activate(True) self.active = target self.acted.emit(self.active, self.source) self.anim_set(target) def anim_set(self, target): self.anims = [] self.anim_gp.clear() diff = self.digits_pos.index(target) if not diff: return positions = self.digits_pos[diff:] + self.digits_pos[:diff] duration = diff * 50 * 1.5 self.loopers = [] y_offset = 55 + self.limit * 50 for position in positions: digit = self.digits[position] new_pos = positions.index(digit.index) cur_pos = self.digits_pos.index(digit.index) if new_pos > cur_pos: dummy = DummyButton(digit.index, self.limit, self) dummy.move(0, y_offset) y_offset = y_offset + 50 dummy.show() self.loopers.append(dummy) anim = QPropertyAnimation(dummy, b"pos", dummy) anim.setStartValue(dummy.pos()) anim.setEndValue( QPoint(dummy.pos().x(), dummy.pos().y() - diff * 50)) self.anims.append(anim) anim = QPropertyAnimation(digit, b"pos", digit) anim.setStartValue(digit.pos()) anim.setEndValue( QPoint(digit.pos().x(), digit.pos().y() - diff * 50)) self.anims.append(anim) digit.disable(True) for anim in self.anims: anim.setDuration(duration) anim.setEasingCurve(QEasingCurve.OutQuad) self.anim_gp.addAnimation(anim) self.anim_gp.start() self.digits_pos = positions @pyqtSlot() def post_animation(self): for looper in self.loopers: self.digits[looper.index].move(looper.pos()) looper.setParent(None) for digit in self.digits: digit.disable(False) self.parent().rectify() def reset(self): for digit in self.digits: digit.activate(False) self.active = -1 self.anim_set(0)
class SlidingStackedWidget(QStackedWidget): animationEasingCurve = QtDynamicProperty('animationEasingCurve', int) animationDuration = QtDynamicProperty('animationDuration', int) verticalMode = QtDynamicProperty('verticalMode', bool) wrap = QtDynamicProperty('wrap', bool) animationFinished = pyqtSignal() LeftToRight, RightToLeft, TopToBottom, BottomToTop, Automatic = list(range(5)) def __init__(self, parent=None): super(SlidingStackedWidget, self).__init__(parent) self.animationEasingCurve = QEasingCurve.Linear self.animationDuration = 250 self.verticalMode = False self.wrap = False self._active = False self._animation_group = QParallelAnimationGroup() self._animation_group.finished.connect(self._SH_AnimationGroupFinished) def slideInNext(self): next_index = self.currentIndex() + 1 if self.wrap or next_index < self.count(): self.slideInIndex(next_index % self.count(), direction=self.BottomToTop if self.verticalMode else self.RightToLeft) def slideInPrev(self): previous_index = self.currentIndex() - 1 if self.wrap or previous_index >= 0: self.slideInIndex(previous_index % self.count(), direction=self.TopToBottom if self.verticalMode else self.LeftToRight) def slideInIndex(self, index, direction=Automatic): self.slideInWidget(self.widget(index), direction) def slideInWidget(self, widget, direction=Automatic): if self.indexOf(widget) == -1 or widget is self.currentWidget(): return if self._active: return self._active = True prev_widget = self.currentWidget() next_widget = widget if direction == self.Automatic: if self.indexOf(prev_widget) < self.indexOf(next_widget): direction = self.BottomToTop if self.verticalMode else self.RightToLeft else: direction = self.TopToBottom if self.verticalMode else self.LeftToRight width = self.frameRect().width() height = self.frameRect().height() # the following is important, to ensure that the new widget has correct geometry information when sliding in the first time next_widget.setGeometry(0, 0, width, height) if direction in (self.TopToBottom, self.BottomToTop): offset = QPoint(0, height if direction == self.TopToBottom else -height) elif direction in (self.LeftToRight, self.RightToLeft): offset = QPoint(width if direction == self.LeftToRight else -width, 0) # re-position the next widget outside of the display area prev_widget_position = prev_widget.pos() next_widget_position = next_widget.pos() next_widget.move(next_widget_position - offset) next_widget.show() next_widget.raise_() prev_widget_animation = QPropertyAnimation(prev_widget, b"pos") prev_widget_animation.setDuration(self.animationDuration) prev_widget_animation.setEasingCurve(QEasingCurve(self.animationEasingCurve)) prev_widget_animation.setStartValue(prev_widget_position) prev_widget_animation.setEndValue(prev_widget_position + offset) next_widget_animation = QPropertyAnimation(next_widget, b"pos") next_widget_animation.setDuration(self.animationDuration) next_widget_animation.setEasingCurve(QEasingCurve(self.animationEasingCurve)) next_widget_animation.setStartValue(next_widget_position - offset) next_widget_animation.setEndValue(next_widget_position) self._animation_group.clear() self._animation_group.addAnimation(prev_widget_animation) self._animation_group.addAnimation(next_widget_animation) self._animation_group.start() def _SH_AnimationGroupFinished(self): prev_widget_animation = self._animation_group.animationAt(0) next_widget_animation = self._animation_group.animationAt(1) prev_widget = prev_widget_animation.targetObject() next_widget = next_widget_animation.targetObject() self.setCurrentWidget(next_widget) prev_widget.hide() # this may have been done already by QStackedWidget when changing the current widget above -Dan prev_widget.move(prev_widget_animation.startValue()) # move the out-shifted widget back to its original position self._animation_group.clear() self._active = False self.animationFinished.emit()
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()
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 QTile(QGraphicsObject): colorMap = { Tetris.I: QColor("#53bbf4"), Tetris.J: QColor("#e25fb8"), Tetris.L: QColor("#ffac00"), Tetris.O: QColor("#ecff2e"), Tetris.S: QColor("#97eb00"), Tetris.T: QColor("#ff85cb"), Tetris.Z: QColor("#ff5a48") } def __init__(self, qTetris: 'QTetris', tetrimino: Tetris.Tetrimino, tile: Tetris.Tile): super(QTetris.QTile, self).__init__() tile.delegate = self self.color = self.colorMap[type(tetrimino)] self.qTetris = qTetris self.moveAnimation = QParallelAnimationGroup() self.dropAnimation = QPropertyAnimation(self, b'pos') self.collapseAnimation = QPropertyAnimation(self, b'pos') self.shiftAnimation = QPropertyAnimation(self, b'pos') self.collapseAnimation.finished.connect( lambda tl=tile: tile.delegate.disappeared(tl)) self.qTetris.scene.addItem(self) self.setPos(QPointF(0, 4)) self.moved(tile) def moved(self, tile: Tetris.Tile): translation = QPropertyAnimation(self, b'pos') start, end = self.pos(), QPointF(tile.row, tile.column) curve, speed, delay = QEasingCurve.OutBack, 1 / 50, -1 self.animate(translation, start, end, curve, speed, delay) rotation = QPropertyAnimation(self, b'rotation') start, end = self.rotation(), tile.rotation curve, speed, delay = QEasingCurve.OutBack, 1, -1 self.animate(rotation, start, end, curve, speed, delay) rotation.setDuration(translation.duration()) self.moveAnimation.clear() self.moveAnimation.addAnimation(translation) self.moveAnimation.addAnimation(rotation) self.moveAnimation.start() def dropped(self, tile: Tetris.Tile): start, end = self.pos(), QPointF(tile.row, tile.column) curve, speed, delay = QEasingCurve.OutBounce, 1 / 50, 0 self.animate(self.dropAnimation, start, end, curve, speed, delay) def collapsed(self, tile: Tetris.Tile): start, end = self.pos(), QPointF( tile.row, tile.column + 2 * tile.tetris.num_columns) curve, speed, delay = QEasingCurve.InOutExpo, 1 / 50, 800 if self.dropAnimation.state() == QAbstractAnimation.Running: start = self.dropAnimation.endValue() self.animate(self.collapseAnimation, start, end, curve, speed, delay) def shifted(self, tile: Tetris.Tile): start, end = self.pos(), QPointF(tile.row, tile.column) curve, speed, delay = QEasingCurve.OutBounce, 1 / 100, 1200 if self.dropAnimation.state() == QAbstractAnimation.Running: start = self.dropAnimation.endValue() self.animate(self.shiftAnimation, start, end, curve, speed, delay) def disappeared(self, tile: Tetris.Tile): self.qTetris.scene.removeItem(self) def paint(self, painter: QPainter, styleOption: QStyleOptionGraphicsItem, widget: QWidget = None): pen = QPen() pen.setWidthF(0.05) pen.setColor(Qt.darkGray) painter.setPen(pen) brush = QBrush() brush.setColor(self.color) brush.setStyle(Qt.SolidPattern) painter.setBrush(brush) topLeft = QPointF(0, 0) bottomRight = QPointF(1, 1) rectangle = QRectF(topLeft, bottomRight) rectangle.translate(-0.5, -0.5) painter.drawRect(rectangle) @staticmethod def animate(animation: QPropertyAnimation, start: Union[QPointF, int, float], end: Union[QPointF, int, float], curve: QEasingCurve = QEasingCurve.Linear, speed: float = 1 / 50, delay: int = -1): animation.setStartValue(start) animation.setEndValue(end) animation.setEasingCurve(curve) if type(start) == type(end) == QPointF: distance = (end - start).manhattanLength() else: distance = abs(end - start) animation.setDuration(round(distance / speed)) if delay == 0: animation.start() if delay > 0: QTimer.singleShot(delay, animation.start) def boundingRect(self): topLeft = QPointF(0, 0) bottomRight = QPointF(1, 1) rectangle = QRectF(topLeft, bottomRight) rectangle.translate(-0.5, -0.5) return rectangle