想让自己家的猫住进屏幕里,在任务栏上走来走去、伸懒腰、打盹。这篇文章记录我从一张猫照片开始,做桌面宠物的完整过程,包括素材、程序、踩过的坑。
一、先把问题拆开:桌面宠物 = 素材 + 程序 + 行为
一开始我以为桌面宠物就是"一张会动的图",动手后才发现它有三层,每层独立可做、可以换:
- 素材层:每个动作都是一段带透明通道的动画(GIF、PNG 序列帧都行)。
- 程序层:一个无边框、透明、永远置顶的小窗口,负责播放动画、响应拖拽和点击。
- 行为层:一个小状态机,决定猫什么时候待机、什么时候走动、被点了有什么反应。
想清楚这三层之后,就可以分头开工,互不干扰。
二、素材篇:从一张猫照片到动作动画
2.1 我的路线:AI 从照片生成动作短片
我家猫是只圆滚滚的白灰色折耳猫,拍照很好看,但照片是静态的。我的思路是:用 AI 图生视频,从照片生成"走路、跳跃、打招呼、玩耍、伸懒腰、坐下、睡觉"等动作短片,再抠成透明素材。
听起来很顺,但实操里全是细节。下面这套规范,是我用三轮试错换来的:
2.2 素材的硬性规范(血泪版)
- 每一帧,猫的身体必须完整在画面内——头顶、尾巴尖都不许出画。AI 生成视频时很容易把尾巴甩出画面,废片率最高的一项。
- 绿幕背景每一帧都要铺满,不能有穿帮,方便后面抠像。
- 所有动作里猫的体型必须一致,不然切动作时会突然变大变小,非常出戏。
- 构图按动作选:横向动作(走路、玩耍、伸懒腰)用横版;纵向动作(跳上、跳下)用竖版。横版硬套竖版内容,身体边缘会被挡住。
- 统一尺寸和帧率,8 个动作一个标准,程序里才好处理。
2.3 三轮试错实录
- 第一轮:生成了 8 个动作并统一尺寸。结果自己先毙掉了:有的片段里尾巴出了画,跳跃时头顶出了画,更糟的是绿幕抠像把猫身体抠掉了一块。
- 第二轮:意识到问题不只出在"统一尺寸"这一步,有些源片本身就把猫裁掉了。于是重新生成了 8 段源片,这次保证猫完整入镜。
- 第三轮:做了绿幕预览版(每个动作 10 秒)。还是不行:所有片段边缘都有色边,而且横版版本实际上只是竖版内核,身体边缘依然被遮挡,效果和上一版差不多。
三轮下来,我决定先暂停这条路线。不是 AI 不行,是"照片→视频→抠像"这条链路对质检的要求极高,每一帧都要检查,工作量远超预期。
2.4 如果重来,我会这样做
- 提示词里写死:"全身完整入镜,四周留出边距",别指望模型自己懂。
- 先拿 1 个动作跑通全流程(生成→规范→抠像→进程序),确认 OK 再批量做另外 7 个。
- 备选路线:逐帧手绘(工作量大但最可控)、Live2D(适合二次元形象)、3D 模型(Blender + 程序渲染,动作最自由)。
三、程序篇:让猫在桌面上动起来
素材是皮,程序是骨。下面是一个最小可跑的思路,我用 Python + PySide6 实现,几十行代码就能让猫住进桌面。
3.1 窗口三件套
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.SubWindow)
self.setAttribute(Qt.WA_TranslucentBackground)
无边框、背景透明、永远置顶——桌面宠物的窗口就这三行。Qt.SubWindow 是为了让它不抢任务栏图标。
3.2 动画播放:一个 QLabel + QMovie 就够了
每个动作对应一个透明 GIF,切换动作就是换一个文件。GIF 尺寸变化时调一下 adjustSize(),窗口跟着变。
3.3 行为状态机
核心就是一个定时器:每 60ms 走一拍,走动时平移窗口,到屏幕边缘回头;每隔几秒随机切一次状态(待机/走动/睡觉);双击切换到"打招呼";长时间不动就睡觉,被拖拽或点击就醒来。
3.4 完整示例代码(可直接运行)
import random
import sys
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QMovie, QAction
from PySide6.QtWidgets import QApplication, QLabel, QMenu
# 动作名 -> 对应 GIF 文件(第二章做好的透明素材放 assets/ 里)
GIF = {
"idle": "assets/idle.gif",
"walk": "assets/walk.gif",
"greet": "assets/greet.gif",
"sleep": "assets/sleep.gif",
}
class DesktopPet(QLabel):
def __init__(self):
super().__init__()
self.movie = QMovie()
self.setMovie(self.movie)
# 窗口三件套:无边框 + 透明 + 置顶
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.SubWindow)
self.setAttribute(Qt.WA_TranslucentBackground)
self._drag = None
self._vx = 0 # 走动时的水平速度
self._cooldown = 0
self.set_state("idle")
self.move(900, 600)
self.timer = QTimer(self)
self.timer.timeout.connect(self._tick)
self.timer.start(60)
def set_state(self, name):
self.movie.stop()
self.movie.setFileName(GIF[name])
self.movie.start()
self.adjustSize()
self._vx = random.choice([-4, 4]) if name == "walk" else 0
def _tick(self):
if self._vx: # 走动:平移窗口,到边缘回头
x = self.x() + self._vx
if x < 0 or x + self.width() > self.screen().size().width():
self._vx = -self._vx
self.move(x, self.y())
self._cooldown -= 1
if self._cooldown <= 0:
self.set_state(random.choice(["idle", "idle", "walk", "sleep"]))
self._cooldown = random.randint(60, 140)
# 拖拽:按下记偏移,移动跟鼠标
def mousePressEvent(self, e):
if e.button() == Qt.LeftButton:
self._drag = e.globalPosition().toPoint() - self.pos()
def mouseMoveEvent(self, e):
if self._drag:
self.move(e.globalPosition().toPoint() - self._drag)
def mouseReleaseEvent(self, e):
self._drag = None
def mouseDoubleClickEvent(self, e):
self.set_state("greet")
self._cooldown = 80
# 右键菜单:手动切动作 / 退出
def contextMenuEvent(self, e):
menu = QMenu(self)
for name in GIF:
act = QAction(f"切换到:{name}", self)
act.triggered.connect(lambda _=False, n=name: self.set_state(n))
menu.addAction(act)
menu.addSeparator()
quit_act = QAction("退出", self)
quit_act.triggered.connect(QApplication.quit)
menu.addAction(quit_act)
menu.exec(e.globalPos())
if __name__ == "__main__":
app = QApplication(sys.argv)
pet = DesktopPet()
pet.show()
sys.exit(app.exec())
运行前:pip install PySide6,把做好的透明 GIF 按名字放进 assets/ 文件夹,python main.py 就能看到猫了。
3.5 可以继续加的功能
- 开机自启动(各系统不同,macOS 可用
launchd,Windows 放启动文件夹); - 点击 / 投喂互动:点一下猫,"咕噜"一声加好感度;
- 多显示器支持:走动时跨屏;
- 打包成单个可执行文件,见下一章。
四、在 macOS 上跑起来:被系统拦截怎么办
自己写的程序没有开发者签名,macOS 第一次运行大概率拦截。按顺序试:
- 右键 → 打开:在访达里对着程序右键选择"打开",会多一个"仍要打开"的选项,这是最正规的单次放行。
- 仅处理自己构建且确认可信的程序:如果程序确由自己构建或来源完全可信,但复制后仍帶圉隔离标记,可谨慎执行:
不要对来源不明的程序使用这条命令。xattr -cr /Applications/你的程序.app - 本地临时签名:仅用于自己测试的打包程序,可以添加临时签名;它不能替代正式的开发者签名、公证和安全审核:
codesign --force --deep --sign - /Applications/你的程序.app
打包建议用 PyInstaller:
pip install pyinstaller
pyinstaller --windowed --name DesktopPet main.py
--windowed 表示不要弹终端窗口。注意把 assets/ 资源一起打进包(--add-data),否则换台机器 GIF 就找不到了。
五、开工 checklist
- ☐ 选定形象:照片、手绘、Live2D、3D,四选一
- ☐ 定动作清单(建议先做 4 个:待机、走路、被点击、睡觉)
- ☐ 先拿 1 个动作跑通"生成→抠像→进程序"全流程
- ☐ 质检每一帧:身体完整、绿幕铺满、体型一致
- ☐ 程序实现窗口三件套 + 状态机 + 拖拽
- ☐ macOS / Windows 真机测试运行与打包
- ☐ 发到个人网站,写篇记录(比如这篇 🙂)
记录于 2026 年 10 月。从一张猫照片开始,到屏幕上多了一只猫——过程比结果有意思。