“在某个加班到深夜的晚上,我望着一成不变的桌面,心想:如果这儿有只可爱的小猫在走来走去、还能冲我眨眼,是不是整个编程体验都能变得柔和一些?”
小时候的电脑里,总有些奇奇怪怪又让人爱不释手的玩意儿,比如能陪你聊天的微软小助手 Clippy,或者是蹦来蹦去的小浣熊宠物。如今虽然操作系统越来越精简高效,但也未免显得太过冷清了。
这篇文章,记录了我如何用 PyQt 打造出一款“现代桌面宠物”的完整旅程。它不是玩具,也不是纯粹的代码练习,而是一次结合 GUI、动画、透明窗口、多线程、文件管理、逻辑控制等诸多知识点的 桌面宠物开发实战。
而最重要的是,它真的能在你电脑上“活起来”。
我打算做一只可以:
为了更直观地理清楚流程,我画了一张最初的功能模块图:
这个架构并不复杂,但覆盖的技术点很多,需要我们精细地设计每一个细节。
首先,确定技术栈与开发环境:
PyQt5
pygame
(用于音效播放)QMovie
(用于动态 gif 播放)pip install PyQt5 pygame
准备好环境后,我们就可以动手构建窗口。
一个桌面宠物的核心,就是它必须“漂浮”在桌面上,并且没有任何系统边框。于是我们需要创建一个 无边框+透明背景 的窗口。
from PyQt5.QtWidgets import QApplication, QLabel, QWidget
from PyQt5.QtGui import QMovie
from PyQt5.QtCore import Qt, QTimer, QPoint
import sys
class PetWidget(QWidget):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.FramelessWindowHint |
Qt.WindowStaysOnTopHint |
Qt.Tool) # 无边框、置顶、不出现在任务栏
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.label = QLabel(self)
self.movie = QMovie("assets/cat_idle.gif")
self.label.setMovie(self.movie)
self.movie.start()
self.resize(self.movie.currentPixmap().size())
self.setMouseTracking(True)
self.offset = None # 拖动支持
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.offset = event.pos()
def mouseMoveEvent(self, event):
if self.offset:
self.move(self.pos() + event.pos() - self.offset)
def mouseReleaseEvent(self, event):
self.offset = None
if __name__ == "__main__":
app = QApplication(sys.argv)
pet = PetWidget()
pet.show()
sys.exit(app.exec_())
这段代码完成了以下功能:
至此,一只“傻乎乎地站着”的猫咪已经出现在桌面上了。
接下来,我要带着这只“逛桌面”的小猫,教你如何让它“活”起来——会自己闲逛、会打盹、还会偶尔伸个懒腰。要实现这些,我们需要一个状态机来管理宠物的各种行为,然后再用定时器驱动动画切换与位置更新。
我希望猫猫能以“吟游诗人”的姿态漫步桌面:有时候它在窗边打个盹,有时候它闲庭信步,还会不经意地伸个懒腰。于是我给它规划了三个主要状态:
描绘一下这个状态机的样子:
这个状态机很简单,但能让猫猫显得生动。接下来,我们要用 Python + PyQt 的定时器来驱动它。
首先,在 PetWidget
类中增加一个内部状态管理器和定时器:
import random
from PyQt5.QtCore import QTimer, QRect
class PetWidget(QWidget):
def __init__(self):
# … 前面透明窗口与动画设置不变 …
# --- 行为状态机 ---
self.state = "Idle" # 当前状态
self.energy = 100 # “精力”值,下降到0时进入 Sleep
self.state_timer = QTimer() # 驱动状态逻辑
self.state_timer.timeout.connect(self.update_state)
self.state_timer.start(500) # 每 0.5s 调度一次
# 位置与移动
self.move_timer = QTimer() # 用于平滑移动
self.move_timer.timeout.connect(self.random_walk)
self.move_timer.start(50) # 每 0.05s 更新坐标
def update_state(self):
"""根据当前状态和条件切换行为状态,并更新对应动画。"""
if self.state == "Idle":
self.energy -= 1
# 当精力耗尽,进入打盹
if self.energy <= 0:
self.transition_to("Sleep")
# 5% 概率触发伸懒腰
elif random.random() < 0.05:
self.transition_to("Stretch")
elif self.state == "Sleep":
# 睡 10 次调度后醒来
if self.energy >= 50:
self.transition_to("Idle")
else:
self.energy += 5
elif self.state == "Stretch":
# 伸懒腰一次后恢复闲逛
self.transition_to("Idle")
def transition_to(self, new_state):
"""切换状态并加载相应动画。"""
self.state = new_state
if new_state == "Idle":
self.movie.stop()
self.movie = QMovie("assets/cat_idle.gif")
self.movie.start()
elif new_state == "Sleep":
self.movie.stop()
self.movie = QMovie("assets/cat_sleep.gif")
self.movie.start()
elif new_state == "Stretch":
self.movie.stop()
self.movie = QMovie("assets/cat_stretch.gif")
self.movie.start()
# 每次切换都要调整窗口大小,以适应不同动作帧
self.resize(self.movie.currentPixmap().size())
def random_walk(self):
"""在 Idle 状态下,猫进行随机漫步;其他状态不移动。"""
if self.state != "Idle":
return
# 当前窗口位置
x, y = self.x(), self.y()
# 随机生成一个微小位移
dx = random.randint(-5, 5)
dy = random.randint(-5, 5)
# 限制不超出屏幕范围(假设 1920x1080)
screen_w, screen_h = 1920, 1080
new_x = max(0, min(x + dx, screen_w - self.width()))
new_y = max(0, min(y + dy, screen_h - self.height()))
self.move(new_x, new_y)
我在 __init__
中新增了两个定时器:
update_state
方法根据当前 self.state
执行不同逻辑。为了让猫猫不至于一直打盹不醒,我设计了一个“精力”数值,越睡越充电;当“精力”充满了一半,就重新开始闲逛。
transition_to
方法负责加载并播放对应状态的 GIF 动画,同时根据新动画的画布大小重新调整窗口尺寸,保证不会出现画面裁剪。
random_walk
则是在闲逛状态下,随机生成 -5~5 像素的偏移,并且做屏幕边界检查,避免猫猫跑出桌面可见区域。
到此为止,我们的猫猫已经能自己在桌面上“走动”—走着走着一累就打个盹,偶尔还会伸个懒腰。为了让整个体验更丰富,下面将加入与用户交互的托盘菜单和右键菜单,以及音效反馈。
一个只会自己晃来晃去的桌面宠物还是略显孤零零,于是我决定:
PyQt5 里使用 QSystemTrayIcon
很方便。先在主程序里初始化托盘:
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction
from PyQt5.QtGui import QIcon
class PetApp(QApplication):
def __init__(self, argv):
super().__init__(argv)
# 创建主窗口
self.pet = PetWidget()
self.pet.show()
# --- 系统托盘 ---
self.tray = QSystemTrayIcon(QIcon("assets/tray_icon.png"), self)
self.tray.setToolTip("我的小猫咪")
tray_menu = QMenu()
action_show = QAction("显示/隐藏 猫猫")
action_quit = QAction("退出")
tray_menu.addAction(action_show)
tray_menu.addAction(action_quit)
self.tray.setContextMenu(tray_menu)
self.tray.show()
action_show.triggered.connect(self.toggle_pet)
action_quit.triggered.connect(self.exit_app)
def toggle_pet(self):
if self.pet.isVisible():
self.pet.hide()
else:
self.pet.show()
def exit_app(self):
self.pet.close()
self.quit()
这样,右下角就会出现一个猫爪图标。右键它,就能快速隐藏或退出应用,省去了找窗口的麻烦。
为了更直接的互动,我在 PetWidget
里重写了 contextMenuEvent
:
def contextMenuEvent(self, event):
menu = QMenu(self)
feed = menu.addAction("喂食 🍣")
mood = menu.addAction("查看心情 💭")
skin = menu.addAction("换个皮肤 🎨")
action = menu.exec_(event.globalPos())
if action == feed:
self.on_feed()
elif action == mood:
self.on_mood()
elif action == skin:
self.on_skin_change()
def on_feed(self):
"""喂食后精力 +20,上限 100,并播放吃东西动画 & 音效。"""
self.energy = min(100, self.energy + 20)
self.movie.stop()
self.movie = QMovie("assets/cat_eat.gif")
self.movie.start()
QTimer.singleShot(2000, lambda: self.transition_to("Idle"))
# 播放音效
pygame.mixer.Sound("assets/eat.wav").play()
def on_mood(self):
"""显示一个气泡,随机输出一句“心情语句”。"""
phrases = ["今天心情不错!", "有点想睡觉…", "好想要鱼干…"]
text = random.choice(phrases)
# 简易气泡提示
QMessageBox.information(self, "小猫心情", text)
def on_skin_change(self):
"""循环切换皮肤目录下的不同子文件夹。"""
# … 读取 skin/ 下所有子目录,切换到下一个 …
经过这一步,猫猫不再是孤独的打工人:你可以喂它🍣,它会“咕噜咕噜”吃掉;你可以问它心情,它会给你一个萌萌的回复;你还能随时给它换新衣服。
我一直觉得,动画如果没有声音,就像动画片没配音,总缺点什么。于是,给猫猫加上音效和简易的语音反馈,立刻让它从“纯视觉动画”跃升成了“多感官”交互小伙伴。
PyQt 自带的 QSound
虽然能播放 WAV,但用起来灵活性和兼容性都不够。朋友推荐我用 pygame.mixer
,它能同时加载多个音效,还能控制音量、循环次数,非常适合这种场景。
在程序最开始,我加上一段初始化音频的代码:
import pygame
# 在 QApplication 之前初始化 pygame
pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
这样,后续在任何交互函数里,只要一句
pygame.mixer.Sound("assets/meow.wav").play()
就能放出一声可爱的“喵~”。我还给猫猫的伸懒腰、打盹、吃东西等动作都配了专属音效,每次状态切换、交互反馈,桌面就像开了“猫猫声音秀”。
想让猫猫更“聪明一点”,我又尝试接入了一个简易的 TTS(Text-to-Speech)模块,让它在某些互动时“开口说话”。出于轻量考量,我用的是系统自带的命令行 TTS——macOS 下的 say
,Windows 下的 SAPI.SpVoice
。
下面是调用 macOS say
的示例(封装在一个工具函数里):
import subprocess
import platform
def speak(text):
system = platform.system()
if system == "Darwin": # macOS
subprocess.Popen(["say", text])
elif system == "Windows":
# Windows 平台请自行安装 pywin32 并使用 Dispatch
from win32com.client import Dispatch
speaker = Dispatch("SAPI.SpVoice")
speaker.Speak(text)
else:
# 其他平台暂不支持
pass
在 on_mood
方法里调用:
def on_mood(self):
phrases = ["今天心情不错!", "想晒晒太阳…", "有点想鱼干…"]
text = random.choice(phrases)
speak(text) # 猫猫突然“开口”!
QMessageBox.information(self, "小猫心情", text)
刚加入这段后,我打开程序,轻轻右键点“查看心情”,居然听到一声“今天心情不错!”,那一刻,我仿佛回到了怦然心动的童年游戏时光——原来,给 GUI 配上声音,就是这么有魔力。
在我看来,良好的 UI 不仅要“能用”,更要“好看且一致”。PyQt 的控件默认样式虽然靠谱,但如果你想让它更时尚,就得动手写 QSS(Qt Style Sheets)。
先在主程序入口加载一个全局 QSS 文件,让所有控件都套上主题:
def load_stylesheet(path):
with open(path, "r", encoding="utf-8") as f:
style = f.read()
app.setStyleSheet(style)
if __name__ == "__main__":
app = PetApp(sys.argv)
load_stylesheet("styles/theme.qss")
sys.exit(app.exec_())
styles/theme.qss
的大体结构,我用了深色半透明+圆角+柔和过渡动画,看上去既现代又不刺眼:
QMenu {
background: rgba(40, 40, 40, 0.8);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 5px;
}
QMenu::item {
padding: 6px 30px 6px 20px;
color: #eee;
}
QMenu::item:selected {
background: rgba(255, 255, 255, 0.1);
}
QMessageBox {
background: rgba(50, 50, 50, 0.9);
border-radius: 10px;
}
QMessageBox QLabel {
color: #fff;
font-size: 14px;
padding: 10px;
}
QSystemTrayIcon {
/* 托盘图标无需 QSS,但可以定制提示气泡样式 */
}
心得:QSS 和 CSS 十分相似,但并不完全等价。控件类名、伪状态(如
:selected
)都要照 Qt 文档写,否则不会生效。写深色主题时,注意文字和图标的对比度,别让用户看不清菜单项。
通过全局 QSS,QMenu
和 QMessageBox
都能立即“皮肤化”。之前我写的那几个交互函数,在重启程序后,立刻显得高大上——菜单圆角柔和、文字带点发光感,就如同专业软件的交互提示。
键菜单。在这里插入图片描述
随着项目功能越来越多,我的 assets/
目录也逐渐“杂乱”起来:有十几套 GIF、各式各样的 WAV、还有皮肤文件夹...早晚要被自己搞丢。于是我给它设计了一个 ResourceManager,来统一管理路径、加载和热切换。
import os
import json
class ResourceManager:
def __init__(self, base_path="assets"):
self.base = base_path
# 加载配置表,里边写明了每种状态对应的动画、音效
with open(os.path.join(self.base, "resources.json"), "r", encoding="utf-8") as f:
self.config = json.load(f)
def get_animation(self, state, skin="default"):
# e.g., self.config["default"]["Idle"] -> "default/idle.gif"
rel = self.config.get(skin, {}).get(state, None)
if rel:
return os.path.join(self.base, skin, rel)
else:
# 回退到 default 皮肤
rel = self.config["default"][state]
return os.path.join(self.base, "default", rel)
def get_sound(self, action):
rel = self.config["sounds"].get(action, None)
return os.path.join(self.base, "sounds", rel) if rel else None
assets/resources.json
示例如下:
{
"default": {
"Idle": "cat_idle.gif",
"Sleep": "cat_sleep.gif",
"Stretch": "cat_stretch.gif",
"Eat": "cat_eat.gif"
},
"skin2": {
"Idle": "cat2_idle.gif",
"Sleep": "cat2_sleep.gif",
"Stretch": "cat2_stretch.gif",
"Eat": "cat2_eat.gif"
},
"sounds": {
"meow": "meow.wav",
"eat": "eat.wav",
"stretch": "stretch.wav"
}
}
在代码里,只要调用:
anim_path = res_mgr.get_animation(self.state, self.current_skin)
self.movie = QMovie(anim_path)
就能省去手动拼路径的烦恼,新增皮肤也只需在 JSON 中登记即可。
每次重启程序时,猫猫都重新从桌面中央“降落”,还得你手动再把它拖到喜欢的位置,多不自然?为此,我设计了一个 配置管理模块,它负责保存并加载用户偏好:包括猫猫的位置、当前皮肤、音量等级等,让猫猫像真正的小伙伴一样“记得”你的喜好。
我们要做到的其实很简单:
我把配置管理封装在一个 SettingsManager
类里,底层使用 JSON 存储,跨平台都可以无忧。
import os, json
class SettingsManager:
def __init__(self, path="config.json"):
self.path = path
self.defaults = {
"position": [100, 100],
"skin": "default",
"volume": 0.8
}
self.data = {}
self.load()
def load(self):
"""加载配置;若文件不存在,则使用默认并创建文件。"""
if os.path.exists(self.path):
with open(self.path, "r", encoding="utf-8") as f:
try:
self.data = json.load(f)
except json.JSONDecodeError:
self.data = self.defaults.copy()
else:
self.data = self.defaults.copy()
self.save()
def save(self):
"""将内存中的配置写入磁盘。"""
with open(self.path, "w", encoding="utf-8") as f:
json.dump(self.data, f, indent=4, ensure_ascii=False)
def get(self, key):
return self.data.get(key, self.defaults.get(key))
def set(self, key, value):
self.data[key] = value
PetWidget
中接入配置class PetWidget(QWidget):
def __init__(self, settings: SettingsManager):
super().__init__()
self.settings = settings
# 还原位置
x, y = self.settings.get("position")
self.move(x, y)
# 还原皮肤
self.current_skin = self.settings.get("skin")
# 还原音量
volume = self.settings.get("volume")
pygame.mixer.music.set_volume(volume)
# …其他初始化…
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
# 在拖动结束后,保存位置
if self.offset:
pos = [self.x(), self.y()]
self.settings.set("position", pos)
self.settings.save()
self.offset = None
def on_skin_change(self):
# 切换皮肤后,保存到配置
skins = list(self.res_mgr.config.keys())
idx = skins.index(self.current_skin)
new_skin = skins[(idx + 1) % len(skins)]
self.current_skin = new_skin
self.settings.set("skin", new_skin)
self.settings.save()
# 重新加载当前状态对应的动画
self.transition_to(self.state)
def set_volume(self, vol: float):
"""外部可调用,例如在设置窗口里调节滑条后调用。"""
pygame.mixer.music.set_volume(vol)
self.settings.set("volume", vol)
self.settings.save()
这样一来,无论你拖猫猫到哪,它都再也不会“迷路”;无论你换多少次皮,它都记得最新的造型;无论你调过多少次音量,下次启动也能立即生效。
到这里,桌面宠物的功能已趋完整,接下来要做的就是打包,让没有 Python 环境、不会安装依赖的朋友也能一键运行。
我比较推荐两款工具:
我以 PyInstaller 为例,讲讲我的打包流程。
下面是我项目根目录下的 pet.spec
样例:
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['main.py'],
pathex=['.'],
binaries=[],
datas=[
('assets/*', 'assets'),
('styles/*', 'styles'),
('config.json', '.')
],
hiddenimports=['pygame', 'win32com'] # 如果用到 SAPI
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='DesktopPet',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='DesktopPet'
)
然后运行:
pyinstaller pet.spec
打包完成后,会在 dist/DesktopPet
下生成一个可执行文件。你只需把整个文件夹打包成 ZIP,分享给朋友即可。
sys._MEIPASS
来定位临时提取目录。 libasound2
)。 say
命令默认可用,Windows 需打包 pywin32
并注意权限。只要小心处理这些细节,就能顺利产出一个“免安装”的桌面宠物应用,让更多人来领养你的“猫猫”!
当我把猫猫“跑”得差不多后,突然萌生一个念头:若能支持 插件式扩展,让开发者轻松地给它增添新功能(比如天气预报、番茄钟、消息提醒等),那就更酷了。
我设计了一个简单的插件接口:
# plugin_interface.py
class BasePlugin:
def __init__(self, pet_widget):
self.pet = pet_widget
def on_load(self):
"""插件加载时调用,可用于注册定时任务、添加菜单项等。"""
def on_unload(self):
"""插件卸载时调用,用于清理资源。"""
def on_event(self, event_type, **kwargs):
"""统一事件回调,可接收 state_change、menu_action 等事件。"""
项目目录下创建一个 plugins/
文件夹,所有插件仅需放入 .py
文件,入口函数约定为 setup(plugin_manager)
。核心的 PluginManager 会在启动时扫描该目录,动态加载插件:
import os, importlib.util
class PluginManager:
def __init__(self, pet_widget):
self.pet = pet_widget
self.plugins = []
def load_plugins(self):
for file in os.listdir("plugins"):
if file.endswith(".py"):
path = os.path.join("plugins", file)
spec = importlib.util.spec_from_file_location(file[:-3], path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
plugin = mod.setup(self.pet)
self.plugins.append(plugin)
plugin.on_load()
def unload_plugins(self):
for plugin in self.plugins:
plugin.on_unload()
self.plugins.clear()
def dispatch(self, event_type, **kwargs):
for plugin in self.plugins:
plugin.on_event(event_type, **kwargs)
在主程序中初始化并调用:
self.plugin_mgr = PluginManager(self)
self.plugin_mgr.load_plugins()
在合适的时机(状态切换、菜单点击等),调用 self.plugin_mgr.dispatch(...)
,插件就能收到事件并做出反应。
为了展示插件能力,我写了一个“天气插件”,每小时自动获取一次天气并让猫猫在桌面上“打喷嚏”提示天气变化。
# plugins/weather_plugin.py
import threading, time
import requests # 需在打包时加入 hiddenimports
from plugin_interface import BasePlugin
class WeatherPlugin(BasePlugin):
def on_load(self):
self.thread = threading.Thread(target=self.fetch_loop, daemon=True)
self.thread.start()
def fetch_loop(self):
while True:
try:
data = requests.get("https://api.example.com/weather").json()
temp = data["temp"]
if temp < 5:
self.pet.speak("好冷,注意保暖!")
self.pet.transition_to("Stretch") # 假装打喷嚏伸懒腰
time.sleep(3600)
except Exception:
time.sleep(600)
def on_unload(self):
# 线程为 daemon,无需额外清理
pass
def setup(pet_widget):
return WeatherPlugin(pet_widget)
插件加载后,猫猫每到整点就会发布天气预报,进一步丰富了桌面宠物的“社交”属性。
一个常驻内存的小程序,最担心的就是内存泄漏或CPU 占用过高。为此,我花了点时间监测性能,并做了几项优化:
QMovie.setCacheMode(QMovie.CacheAll)
缓存所有帧,避免每帧都从磁盘加载。 self.movie.stop()
,再创建新 QMovie
,防止旧对象挂留。 借助任务管理器,我把猫猫的内存及CPU占用很小,几乎感觉不到它的存在。
到此为止,我们用 PyQt 构建了一只:
整个项目融合了 GUI 设计、多线程、文件 I/O、资源管理、打包部署 等多项技术点,既是趣味作品,也可作为 PyQt 进阶指南。
这些实现当然都要靠你们辣!
如果你也想动手打造属于自己的“桌面萌宠”,不妨从这篇实战开始。把这份代码克隆到本地,跟着思路一步步改造、扩展,或许下一只更萌的“小恐龙”或“小机器人”就凭你了!
“夜深人静时,那只小猫会在屏幕角落打盹,偶尔发出‘喵~’的一声,提醒我:再忙,也要适当休息。感谢它,陪我度过无数加班夜晚。”
(完)
不啦不啦不啦,喵喵喵喵喵喵喵喵,懂得都懂。
生不出来找个抠图工具抠一下
如果还有背景的画,可以找一个在线视频抠背景工具,找一个抠一下就行了。
然后找个视频文件转换工具转换一下就OK了。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。