一、简介
这里利用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 # 行走动作概率
放张效果图: