如何在pyqt中实现带动画的动态QMenu

弹出菜单的视觉效果

QLineEdit 原生的菜单弹出效果十分生硬,而且样式很丑。所以照着Groove中单行输入框弹出菜单的样式和动画效果写了一个可以实现动态变化Item的弹出菜单,根据剪贴板的内容是否为文本、编辑框是否有文本以及是否有选中文本分为6种情况,大体效果如下所示(ヾ(๑╹◡╹)ノ" 硝子依旧如此迷人:
如何在pyqt中实现带动画的动态QMenu

具体实现流程

Menu 继承自 QMenu,在这个类中通过调用自定义类 WindowEffect 的方法来调用win10的api从而实现Aero效果和阴影效果,定义WindowEffect的代码放在了文末的链接中,可以自取,而Aero效果的实现方法不妨先康康的我的第一篇博客《如何在pyqt中优雅地实现窗口磨砂效果》,需要指出的是要想实现这两种效果需要事先安装好微软的相关依赖库文件。抛开这个不谈,Menu类的具体代码如下:

import sys
from ctypes.wintypes import HWND

from PyQt5.QtCore import QAbstractAnimation, QEasingCurve, QEvent, Qt, QPropertyAnimation, QRect
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QAction, QApplication, QGraphicsDropShadowEffect, QMenu

from window_effect import WindowEffect


class Menu(QMenu):
    """ 自定义菜单 """
    windowEffect = WindowEffect()

    def __init__(self, string='', parent=None):
        super().__init__(string,parent)
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.Popup | Qt.NoDropShadowWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground | Qt.WA_StyledBackground)
        self.setQss()

    def event(self, e: QEvent):
        if e.type() == QEvent.WinIdChange:
            self.hWnd = HWND(int(self.winId()))
            self.setMenuEffect()
        return QMenu.event(self, e)

    def setMenuEffect(self):
        """ 添加特效 """
        self.windowEffect.setAeroEffect(self.hWnd)
        self.windowEffect.addShadowEffect(True,self.hWnd)

    def setQss(self):
        """ 设置层叠样式 """
        with open('menu.qss', encoding='utf-8') as f:
            self.setStyleSheet(f.read())

LineEditMenu 类

LineEditMenu 继承自 Menu,需要注意的是这个类对 parent 的是有要求的,具体要求见代码:


class LineEditMenu(Menu):
    """ 单行输入框右击菜单 """
    def __init__(self, parent):
        super().__init__('', parent)
        # 实例化动画,注意不能直接改width这个只读的属性
        self.animation = QPropertyAnimation(self, b'geometry')
        self.initWidget()

    def initWidget(self):
        """ 初始化小部件 """
        self.setObjectName('lineEditMenu')
        # 设置动画持续时间
        self.animation.setDuration(300)
        # 设置插值方式
        self.animation.setEasingCurve(QEasingCurve.OutQuad)

    def createActions(self):
        # 创建动作
        self.cutAct = QAction(
            QIcon('images\\黑色剪刀.png'), '剪切', self, shortcut='Ctrl+X', triggered=self.parent().cut)
        self.copyAct = QAction(
            QIcon('images\\黑色复制.png'), '复制', self, shortcut='Ctrl+C', triggered=self.parent().copy)
        self.pasteAct = QAction(
            QIcon('images\\黑色粘贴.png'), '粘贴', self, shortcut='Ctrl+V', triggered=self.parent().paste)
        self.cancelAct = QAction(
            QIcon('images\\黑色撤销.png'), '取消操作', self, shortcut='Ctrl+Z', triggered=self.parent().undo)
        self.selectAllAct = QAction('全选', self, shortcut='Ctrl+A', triggered=self.parent().selectAll)
        # 创建动作列表
        self.action_list = [self.cutAct, self.copyAct, self.pasteAct, self.cancelAct, self.selectAllAct]

    def exec_(self, pos):
    	""" 重写exec_() """
        # 删除所有动作
        self.clear()
        # clear会直接delete之前的动作故需重新创建
        self.createActions()
        # 初始化属性,本来是在没有添加动画时为qss设置的,设置动画之后这个属性没什么用
        self.setProperty('hasCancelAct', 'false')
        width = 176
        # 本来是在后面调用columnCount()来计算item个数的,结果算出来是1,所以手动创建一个变量来记录Item个数
        actionNum = len(self.action_list)
        # 访问系统剪贴板
        self.clipboard = QApplication.clipboard()
        # 根据剪贴板内容是否为text分两种情况讨论
        if self.clipboard.mimeData().hasText():
            # 再根据3种情况分类讨论
            if self.parent().text():
                self.setProperty('hasCancelAct', 'true')
                width = 213
                if self.parent().selectedText():
                    self.addActions(self.action_list)
                else:
                    self.addActions(self.action_list[2:])
                    actionNum -= 2
            else:
                self.addAction(self.pasteAct)
                actionNum = 1
        else:
            if self.parent().text():
                self.setProperty('hasCancelAct', 'true')
                width = 213
                if self.parent().selectedText():
                    self.addActions(self.action_list[:2] + self.action_list[3:])
                    actionNum -= 1
                else:
                    self.addActions(self.action_list[3:])
                    actionNum -= 3
            else:
                return
        # 每个item的高度为38px,10为上下的内边距和
        height = actionNum * 38 + 10
        # 不能把初始的宽度设置为0px,不然会报警
        self.animation.setStartValue(
            QRect(pos.x(), pos.y(), 1, height))
        self.animation.setEndValue(
            QRect(pos.x(), pos.y(), width, height))
        self.setStyle(QApplication.style())
        # 开始动画
        self.animation.start()
        super().exec_(pos)

源代码和dll

动图中用到的编辑框是可以实现对音频文件标签信息的写入的,具体实现方法放在了我的github仓库中,喜欢的话可以给我点个小星星,下面是这次用到的代码、dll以及资源文件的网盘链接(提取码:fl4m):链接

上一篇:如何在pyqt中实现窗口磨砂效果


下一篇:Nuitka打包PyQt程序