利用Python实现桌面小部件——基本功能实现

一、简介

        这里利用python实现的桌面小部件为桌面宠物,采用PyQt5库开发,最终实现随机移动,点击相应及拖拽相应,后面添加了单词查询及辅助记单词的功能。本文主要介绍前半部分的实现,也是桌面小部件的基本要求。

二、主界面实现

        这里创建了MainWindow类实现桌面小部件的显示以及移动,同时管理其它类及功能。

class MainWindow(QWidget):
    def __init__(self, parent=None):
        # 调用父类构造方法
        super(MainWindow, self).__init__(parent)
        # 设置窗口标志:无边框、保持在顶部、子窗口
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.SubWindow)
        # 禁用自动背景填充
        self.setAutoFillBackground(False)
        # 设置窗口背景为透明
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        # 重新绘制窗口
        self.repaint()

        self.par = Parameter()

        # 创建一个标签用于显示图像
        self.label = QLabel()
        # 获取屏幕宽度和高度
        self.screenWidth, self.screenHeight = QApplication.desktop().width(), QApplication.desktop().height()
        # 设置图像路径/默认人物
        # 初始化窗口宽度和高度
        self.width, self.height = 0, 0
        # 根据默认路径设置图像
        self.setImage(rf':images\{self.par.character}1.png')
        # 设置窗口初始位置
        self.par.x, self.par.y = self.screenWidth - self.width, self.screenHeight - self.height - 100
        self.par.ground = self.par.y
        # 创建菜单和操作项
        self.iconMenu = QMenu(self)
        actionQuit = QAction('退出', self, triggered=self.quit)
        actionQuit.setIcon(QIcon(r':images\sayori1.png'))
        self.iconMenu.addAction(actionQuit)
        actionSetting = QAction('设置', self, triggered=lambda: self.setting.show())
        actionSetting.setIcon(QIcon(r':images\natsuki1.png'))
        self.iconMenu.addAction(actionSetting)
        actionAudio = QAction('音频', self, triggered=self.playAudio)
        actionAudio.setIcon(QIcon(r':images\monika2.png'))
        self.iconMenu.addAction(actionAudio)

        # 创建系统托盘图标
        self.tray_icon = QSystemTrayIcon(self)
        self.tray_icon.setIcon(QIcon(':images\monika1.png'))
        self.tray_icon.setContextMenu(self.iconMenu)
        self.tray_icon.show()

        # 初始化定时器
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.action)
        self.timer.start(1000)

        # 设置菜单
        self.setting = SettingWindow()
        self.setting.signalCharacterChanged.connect(self.setCharacter)

        # 设置查询模式
        self.queryInterface = InputWindow(self)
        self.queryInterface.move(self.par.x+80, self.par.y-50)
        self.queryInterface.showWord.move(self.par.x - 290, self.par.y - 230)

        # 设置提问模式
        self.askInterface = AskWindow(self)
        self.askInterface.move(self.par.x+80, self.par.y-50)
        self.askInterface.optionWindow.move(self.par.x-290, self.par.y-230)

        self.lay = QVBoxLayout()
        self.lay.addWidget(self.label)
        self.setLayout(self.lay)
        self.setPosition()
        self.show()

        在__init__方法中,设置了窗口为无边框、子窗口并保持在顶部,这保证了该应用仅在窗口上绘制图片、不创建任务栏图标,并在大多数情况下(比如搜狗输入法就是例外)保持为最前窗口显示。

        Parameter类是专门用于管理参数的类,拥有在类之间共享变量,后面会讲到。

        在接下来的代码中,为桌面部件设置了初始角色(请忽略角色图片名)并创建了系统托盘图标以及相应的菜单栏和选项,属于桌面小部件常规管理方式。timer是计时器,用于决定角色何时产生随机移动。SettingWindow是用来创建设置界面并设置参数的类,InputWindow和AskWindow是后面添加的功能。最后是设置布局、位置及显示。

三、事件响应

        为了让桌面小部件响应用户操作(主要是鼠标事件),需要重写关于鼠标事件的方法:

    def mousePressEvent(self, event):
        # 处理鼠标按键事件
        if event.button() == Qt.MiddleButton and not self.par.isAction:
            # 当中键被按下且没有动作时,开始拖拽
            self.par.isDrag = True
            self.mouseDragPos = event.globalPos() - self.pos()
            event.accept()
            self.setCursor(QCursor(Qt.OpenHandCursor))
        elif event.button() == Qt.LeftButton:
            # 当左键被按下时,执行动作:跳跃(欢快)
            self.jumpHappy()
        elif event.button() == Qt.RightButton:
            # 当右键被按下时,进入查询模式
            self.query()

    def mouseMoveEvent(self, event):
        # 处理鼠标移动事件,当中键被按下且允许拖拽时,人物随鼠标移动
        if Qt.MiddleButton and self.par.isDrag:
            self.par.x, self.par.y = (event.globalPos() - self.mouseDragPos).x(), (
                        event.globalPos() - self.mouseDragPos).y()
            self.setPosition()
        event.accept()

    def mouseReleaseEvent(self, event):
        # 处理鼠标释放事件,中键释放时取消拖拽,记录新位置
        if event.button() == Qt.MiddleButton and self.par.isDrag:
            self.par.isDrag = False
            self.setCursor(QCursor(Qt.ArrowCursor))
            self.par.ground = self.pos().y()
            self.par.x, self.par.y = self.pos().x(), self.par.ground
            self.queryInterface.move(self.par.x+80, self.par.y-50)
            self.queryInterface.showWord.move(self.par.x - 290, self.par.y - 230)

    def wheelEvent(self, event):
        if self.par.isLoading or self.par.isQuery:
            return
        angle = event.angleDelta()
        angleY = angle.y()
        if angleY > 0:
            if not self.par.isAsking:
                if not self.par.isChoosing:
                    self.askInterface.setWord()
                self.askInterface.show()
                self.askInterface.optionWindow.show()
                self.par.isAsking = True
            else:
                if not self.par.isChoosing or self.par.allowSkip:
                    self.askInterface.setWord()
        elif angleY < 0:
            if self.par.isAsking:
                self.askInterface.hide()
                self.askInterface.optionWindow.hide()
                self.par.isAsking = False

        里面相当多的内容是后面添加的,暂时忽略掉。为了实现基本功能,仅需要关注前三个函数的部分内容。具体而言,在处理鼠标按键操作时,若按键为中键且角色当前没有在进行随机动作,设置拖拽标签为真,若按键为左键且角色当前没有在进行随机动作,进行一次随机动作;当处理鼠标释放操作时,若按键为中键且正在拖拽,设置拖拽标签为假;当处理鼠标移动操作时,若按键为中键且正在拖拽,更新角色到当前鼠标位置。

四、执行动作

        在本桌面部件中,笔者设置了两种随机动作及一种按键动作。当然可以根据想法随时添加:

    def action(self):
        # 根据当前状态决定是否开始一次新动作
        if self.par.isAction or self.par.isDrag or self.par.isAsking or (not self.par.allowMoveWhileQuery and self.par.isQuery):
            # 如果当前正在执行动作、拖拽、或查询且不支持移动,则重新计时
            self.timer.start(randint(self.par.timeMin, self.par.timeMax))
            return None
        self.par.isAction = True
        # 随机决定本次行动
        randomNum = randint(0, 100)
        if randomNum <= self.par.probabilityJump:
            self.jump()
        elif randomNum <= self.par.probabilityWalk:
            self.walk()

        动作的实现原理大同小异,这里仅以跳跃动作为例:

    def jump(self):
        """
        角色执行轻跳动作的函数。
        调整垂直和水平速度以及面向,确保角色在边界内并随机改变方向。
        初始化跳跃过程中的定时器,控制角色的跳跃动作和落地处理。
        """
        # 设置垂直速度为跳跃速度,根据角色当前位置调整面向
        self.par.velY = self.par.jumpVelY
        if self.par.x <= 0:
            if self.par.face != 1:
                self.par.face = 1
                self.transformImage()
        elif self.par.x >= self.screenWidth - self.width:
            if self.par.face != -1:
                self.par.face = -1
                self.transformImage()
        elif choice([-1, 1]) != self.par.face:
            self.par.face *= -1
            self.transformImage()
        # 设置水平速度,根据面向调整
        self.par.velX = self.par.face * self.par.jumpVelX

        def jump_step():
            """
            跳跃过程中的单步操作函数。
            控制角色在空中位置的更新,包括垂直和水平移动。
            判断角色是否落地,若未落地则继续跳跃,否则停止跳跃并重置状态。
            """
            # 更新角色的垂直位置和速度
            self.par.y -= self.par.velY
            self.par.velY -= self.par.G
            # 更新角色的水平位置
            self.par.x += self.par.velX
            self.setPosition()
            # 如果角色尚未落地,准备继续执行下一帧跳跃步骤
            if self.par.y <= self.par.ground:
                self.timer_jump.singleShot(int(self.par.jumpFrameTime * 1000), jump_step)
            else:
                # 角色落地后,重置位置和速度,并停止跳跃状态
                self.par.y = self.par.ground
                self.par.velX = 0
                self.par.velY = 0
                self.setPosition()
                self.par.isAction = False
                # 重新启动常规定时器,随机时间间隔
                self.timer.start(randint(self.par.timeMin, self.par.timeMax))

        # 初始化跳跃定时器,设置为单次执行,启动跳跃步骤函数
        self.timer_jump = QTimer(self)
        self.timer_jump.singleShot(int(self.par.jumpFrameTime * 1000), jump_step)

         这里需要注意几个细节:检查边界条件,确保角色不会跳出屏幕;在某些情况下应当禁用跳跃操作,或在跳跃情况下禁用其它操作;在角色被拖拽移动后记录更新“地面”位置;调整图片朝向以适应角色移动方向。

五、设置界面

        创建一个窗口添加单选按钮并设置回调函数调整参数,这里需要一个角色改变的信号连接到MainWindow类里的槽。

六、参数类

        我们只希望参数类只有一个实例存在,以便于共享变量。为此需要用到单例模式:

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

使用时(删减了部分代码):

class Parameter(metaclass=SingletonMeta):

    character = r'natsuki'


    allowBlurMatch = False  # 是否模糊匹配
    allowMoveWhileQuery = True  # 是否查询时允许移动
    allowSkip = False  # 是否允许提问时跳过单词
    allowSelectInNewWord = False  # 是否从生词本中选择
    allowAutoAddNewWord = True  # 是否自动加入生词本
    allowAutoRemoveNewWord = False  # 是否自动移除生词

    isAction = False  # 正在进行动作
    isDrag = False  # 正在被拖拽
    isQuery = False  # 正在查询
    isLoading = False  # 正在加载数据
    isMatchingWord = False  # 正在匹配单词
    isCorrect = False  # 选择了正确的答案
    isAsking = False  # 正在提问模式
    isChoosing = False  # 正在选择答案
    isPlayingAudio = False  # 正在播放音频

    # natsuki, sayori & monika
    x = 0  # 窗口位置
    y = 0  # 窗口位置
    face = -1  # -1-左 0-隐藏 1-右
    velX = 0  # 当前横向速度
    velY = 0  # 当前纵向速度
    jumpVelX = 3  # 跳跃时横向速度
    jumpVelY = 10  # 跳跃时纵向速度
    jumpFrameTime = 0.015  # 跳跃每帧耗时
    walkDistance = 20  # 行走距离
    walkVel = 1  # 行走速度
    walkFrameTime = 0.01  # 行走每帧耗时
    jumpHappyVel = 25  # 单击跳跃速度
    jumpHappyFrameTime = 0.02  # 单击跳跃每帧耗时
    G = 2  # 重力加速度
    ground = 0  # 地平面位置
    timeMin = 1000  # 随机动作时间下限
    timeMax = 3000  # 随机动作时间上限
    probabilityJump = 30  # 跳跃动作概率
    probabilityWalk = 100  # 行走动作概率

放张效果图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值