【python】开发了一个电子桌面桌宠,会要饭,满屏跑,效果太棒了

【python】开发了一个电子桌面桌宠,会要饭,满屏跑,效果太棒了

文档版本:1.0
适用平台:Windows
开发语言:Python 3.10
GUI 框架:PySide6
当前宠物:橘白猫「小橘」、藏獒「小山」



目录

  1. 项目概览
  2. 技术栈
  3. 项目结构
  4. 系统架构
  5. 模块详解
    • 5.1 入口 & 应用生命周期 (main.py / app.py)
    • 5.2 动画资源管理 (assets.py)
    • 5.3 数据模型 (models.py)
    • 5.4 动画状态机 (controller.py)
    • 5.5 透明渲染窗口 (window.py)
    • 5.6 设置持久化 (settings.py)
    • 5.7 路径解析 (resources.py)
    • 5.8 宠物选择 & 目录 (selection.py / pet_catalog.py)
  6. 动画清单格式 (animations.json)
  7. 交互系统
    • 7.1 鼠标眼动追踪
    • 7.2 拖拽与点击
    • 7.3 自主行为
    • 7.4 系统托盘
  8. 精灵生成管线
  9. 构建与打包
  10. 测试
  11. 配置与环境变量

1. 项目概览

本项目是一款离线的 Windows 桌面宠物应用。宠物以透明、无边框、始终置顶的窗口呈现在桌面上,支持自主行走、奔跑、休息,观察鼠标并响应用户的点击、拖拽、喂食等操作。

当前版本包含两只宠物:

宠物ID昵称状态数总帧数步行动画
橘白猫orange_cat小橘178716 帧
藏獒tibetan_mastiff小山1713720 帧

2. 技术栈

层级技术版本用途
语言Python≥3.10主体逻辑
GUIPySide6≥6.6, <7透明窗口、渲染、系统托盘
图像处理Pillow≥9.1, <12精灵图加载与处理
打包PyInstaller≥5.1生成独立 .exe
精灵生成NumPy / OpenCV≥1.23 / ≥4.5光流插值、色键去底
配置格式JSON动画清单、用户设置
设置存储Windows Registry开机自启注册表项

3. 项目结构

桌面宠物游戏/ ├── main.py # 应用入口 ├── run.bat # 快速启动脚本 ├── build.bat # 完整构建脚本 ├── pyproject.toml # 项目元数据、Ruff 配置 ├── requirements.txt # 运行时依赖 ├── requirements-dev.txt # 开发构建依赖 ├── orange_cat_pet.spec # PyInstaller 打包配置 │ ├── desktop_pet/ # 核心程序包 │ ├── __init__.py # 版本号 (1.0.0) │ ├── app.py # 应用生命周期协调器 │ ├── window.py # 透明宠物窗口 │ ├── controller.py # 动画状态机 │ ├── assets.py # 动画资源加载器 │ ├── models.py # 数据模型 & 枚举 │ ├── settings.py # 设置持久化 & 自启管理 │ ├── resources.py # 路径解析 (源码/打包) │ ├── selection.py # 宠物选择对话框 │ └── pet_catalog.py # 宠物定义目录 │ ├── assets/ # 游戏资源 │ ├── animations.json # 猫咪动画清单 │ ├── tibetan_mastiff_animations.json # 藏獒动画清单 │ ├── IMAGEGEN_PROMPTS.md # 精灵图 AI 生成提示词 │ ├── icons/orange_cat.ico # 应用图标 │ └── sprites/ # 精灵图表 / 生成帧 │ ├── generated/ # 猫咪已处理帧 (87 张 PNG) │ └── tibetan_mastiff/generated/ # 藏獒已处理帧 (137 张 PNG) │ ├── tools/ # 构建工具 │ ├── build_sprites.py # 猫精灵抽取 & 步态插值 │ ├── build_mastiff_sprites.py # 藏獒精灵抽取 & 动画生成 │ └── make_icon.py # Windows .ico 生成 │ ├── tests/ # 单元测试 │ ├── test_assets.py # 资源完整性测试 │ ├── test_models_settings.py # 设置序列化测试 │ ├── test_qt_smoke.py # Qt 烟雾测试 │ └── test_selection.py # 选择对话框测试 │ ├── build/orange_cat_pet/ # PyInstaller 构建中间产物 └── dist/OrangeCatPet/ # 最终发布目录

4. 系统架构

┌──────────────────────────────────────────────────────────┐ │ main.py │ │ (入口, HIGHDPI 设置) │ └─────────────────┬────────────────────────────────────────┘ │ ┌─────────────────▼────────────────────────────────────────┐ │ DesktopPetApplication │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 应用协调: QApplication / SettingsStore │ │ │ │ 宠物切换: _activate_pet() / _choose_pet() │ │ │ │ 托盘管理: _create_or_refresh_tray() │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────┬────────────────────────────────────────┘ │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ ┌───────┐ ┌──────────┐ ┌─────────────┐ │Assets │ │Controller│ │Selection │ │Library│ │(状态机) │ │Dialog │ └───┬───┘ └────┬─────┘ └─────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────┐ │ PetWindow │ │ ┌───────────────────────────────┐ │ │ │ QWidget (透明, 置顶, 无边框) │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ QPainter 渲染当前帧 │ │ │ │ │ │ 眼球追踪计算 & 绘制 │ │ │ │ │ ├─────────────────────────┤ │ │ │ │ │ motion_timer (30ms) │ │ │ │ │ │ behaviour_timer (1s) │ │ │ │ │ │ single_click_timer │ │ │ │ │ ├─────────────────────────┤ │ │ │ │ │ 鼠标事件处理 │ │ │ │ │ │ 右键上下文菜单 │ │ │ │ │ └─────────────────────────┘ │ │ │ └───────────────────────────────┘ │ └─────────────────────────────────────┘

5. 模块详解

5.1 入口 & 应用生命周期 (main.py / app.py)

main.py— 应用入口,设置QT_ENABLE_HIGHDPI_SCALING=1环境变量后创建并启动DesktopPetApplication

DesktopPetApplication— 应用生命周期协调器 (desktop_pet/app.py:16),职责如下:

  • 初始化QApplication(应用名 “桌面宠物伙伴”,组织名 “OrangeCatDesktopPet”)
  • setQuitOnLastWindowClosed(False)确保关闭窗口后隐藏到托盘而非退出
  • run()方法:启动时先弹出宠物选择对话框,选择后进入 Qt 事件循环
  • _activate_pet()方法:切换宠物时销毁旧窗口、重新加载动画库、创建新窗口、刷新托盘
  • quit()方法:保存设置、隐藏托盘、退出应用

应用级信号流:

window.request_quit ──────────> app.quit() window.request_pet_selection ─> app.choose_pet()

5.2 动画资源管理 (assets.py)

AnimationLibrary(desktop_pet/assets.py:12) — 从 JSON 动画清单文件加载并管理所有动画资源。

核心功能:

方法说明
_load()解析 JSON 清单,为 17 个PetState构建AnimationClip
clip(state)返回指定状态的AnimationClip
pixmap(frame)惰性加载并缓存QPixmap(按路径缓存,避免重复文件 I/O)

加载过程:

  1. 读取 JSON → 解析canvas尺寸
  2. 遍历PetState枚举 → 从animations对象中取出帧数组
  3. 每帧解析pathduration_mseyes(眼球锚点)、eye_radiushitbox
  4. 校验文件存在性(缺失直接抛FileNotFoundError
  5. 构建不可变AnimationClipfrozendataclass)

5.3 数据模型 (models.py)

PetState(desktop_pet/models.py:9) — 17 种宠物状态的字符串枚举:

枚举值中文枚举值中文
IDLE待机BLINK眨眼
WATCH观察WALK行走
RUN奔跑SIT坐下
LIE趴下SLEEP睡觉
WAKE醒来STRETCH伸懒腰
GROOM舔毛YAWN打哈欠
HAPPY开心SURPRISED惊讶
ANGRY生气EAT进食
DRAGGED被拖拽

FrameMetadata(desktop_pet/models.py:29) — 不可变帧数据:

字段类型说明
pathPath图片文件路径
duration_msint帧持续时间 (最小值 16ms)
eyestuple[tuple[float, float], ...]眼球锚点坐标序列
eye_radiustuple[float, float]瞳孔基准半径 (x, y)
hitboxtuple[int, int, int, int]碰撞检测区域 (x, y, w, h)

AnimationClip(desktop_pet/models.py:38) — 不可变动画片段:

字段类型说明
statePetState所属状态
framestuple[FrameMetadata, ...]帧序列
loopbool是否循环播放
next_statePetState | None非循环动画结束后的过渡状态

PetSettings(desktop_pet/models.py:46) — 用户设置数据类(可变),支持from_dict/to_dictJSON 序列化,含输入校验。


5.4 动画状态机 (controller.py)

PetController(desktop_pet/controller.py:9) — 管理动画状态切换与帧推进。

优先级系统:每个状态有优先级数值,高优先级可抢占低优先级(非循环动画播放中会锁住):

优先级状态
100DRAGGED
90EAT
80HAPPY
75SURPRISED,ANGRY
65WAKE
55STRETCH,GROOM,YAWN
40SLEEP
25RUN
20WALK
15WATCH
10SIT,LIE
8BLINK
5IDLE

状态切换逻辑 (set_state,controller.py:51):

if 新状态 == 当前状态 and not force → 忽略 if 当前非循环动画未播完 and not force and 新优先级 < 当前优先级 → 忽略 否则 → 切换状态, 重置帧索引, 发射信号, 重新调度定时器

帧推进 (_advance,controller.py:76):

if 还有下一帧 → frame_index++ elif 循环动画 → 回到第 0 帧 else (非循环动画播完) → 过渡到 next_state (默认 IDLE) 发射 frame_changed → 重新调度定时器

5.5 透明渲染窗口 (window.py)

PetWindow(desktop_pet/window.py:18) — 继承QWidget,所有渲染与交互的核心。

窗口属性
属性
固定尺寸library.canvas_size(256×256)
WA_TranslucentBackgroundTrue
WA_NoSystemBackgroundTrue
autoFillBackgroundFalse
窗口标志FramelessWindowHint | Tool | WindowStaysOnTopHint
定时器
定时器间隔用途
motion_timer30ms行走/奔跑位移 ±2px (走) / ±5px (跑)
behaviour_timer1s饥饿/心情更新、随机行为决策
single_click_timer单次区分单击与双击
渲染管线 (paintEvent)
1. QPainter(painter) 描画到 Widget 2. 判断朝向: facing_right 决定是否水平翻转 3. 绘制精灵: drawPixmap(target_rect, pixmap) 4. 眼球追踪计算: a. 获取全局鼠标位置 QCursor.pos() b. 映射到 Widget 局部坐标 c. 遍历 frame.eyes 中每只眼睛的锚点 d. 计算方向向量 → 归一化 → 瞳孔偏移量 e. 绘制白色虹膜 + 黑色瞳孔 + 白色高光点
多显示器支持
  • 使用QApplication.screenAt(center)获取当前所在屏幕
  • 使用availableGeometry()获取不含任务栏的工作区
  • 移动时通过_clamped_position()约束宠物不出工作区边界
右键菜单

动态构建QMenu,包含以下选项:

选项功能
喂食切换到EAT状态
召回将宠物移到当前屏幕中心底部
选择宠物弹出选择对话框
暂停冻结/恢复动画和行为
置顶切换WindowStaysOnTopHint
开机启动写入/删除注册表自启项
退出保存设置 → 完全退出

5.6 设置持久化 (settings.py)

SettingsStore(desktop_pet/settings.py:22) — JSON 设置文件的读写封装。

存储路径说明
%LOCALAPPDATA%\OrangeCatDesktopPet\settings.json默认路径
ORANGE_CAT_DATA_DIR环境变量覆盖测试用途

原子保存机制 (save,settings.py:35):

写入 .tmp 文件 → 调用 Path.replace() 原子替换原文件

容错机制 (load,settings.py:26):

任何异常 (OSError, ValueError, TypeError, JSONDecodeError) → 返回默认 PetSettings

AutoStartManager(desktop_pet/settings.py:45) — 通过操作HKCU\Software\Microsoft\Windows\CurrentVersion\Run注册表键实现开机自启。

  • is_enabled()— 读取注册表判断是否已有启动项
  • set_enabled(bool)— 写入或删除注册表值
  • command()— 生成正确的启动命令行(区分源码运行 vs PyInstaller 打包)

5.7 路径解析 (resources.py)

resource_path(relative_path)— 统一路径解析:

if sys.frozen (PyInstaller 打包): 返回 Path(sys._MEIPASS) / relative_path else: 返回 Path(__file__).resolve().parents[1] / relative_path

支持参数形式:

  • resource_path("assets/animations.json")— 字符串
  • resource_path(Path("assets/sprites/generated/idle/00.png"))Path对象

5.8 宠物选择对话框 (selection.py / pet_catalog.py)

PetSelectionDialog(desktop_pet/selection.py) — 模态对话框 (960×630),首次启动时弹出,其后可通过右键菜单打开。

  • 展示所有宠物的预览卡片(图片、名称、描述、选中按钮)
  • 当前已选宠物高亮显示
  • 点击确定后触发app._activate_pet()

PetDefinition(desktop_pet/pet_catalog.py) — 宠物定义的不可变 dataclass:

字段类型说明
pet_idstr宠物唯一标识
display_namestr中文显示名
descriptionstr简短描述
manifestPath动画清单路径
preview_imagePath选择页预览图路径
frame_countint总帧数

当前宠物目录:

  • 小橘:orange_catassets/animations.json(87 帧)
  • 小山:tibetan_mastiffassets/tibetan_mastiff_animations.json(137 帧)

6. 动画清单格式 (animations.json)

{ "version": 2, "canvas": [256, 256], "animations": { "idle": { "frames": [ { "path": "assets/sprites/generated/idle/00.png", "duration_ms": 100, "eyes": [[120.3, 80.5], [140.2, 80.5]], "eye_radius": [5.0, 4.0], "hitbox": [10, 20, 236, 236] } ], "loop": true }, "eat": { "frames": [ /* ... */ ], "loop": false, "next_state": "idle" } } }

字段说明:

字段类型必填说明
versionint清单格式版本
canvas[int, int]画布尺寸
animations.<state>.framesarray帧数组 (至少 1 帧)
frames[].pathstring相对路径
frames[].duration_msint帧显示时长 (ms)
frames[].eyes[[float, float]]眼球锚点坐标
frames[].eye_radius[float, float]瞳孔半径,默认 [5, 4]
frames[].hitbox[int, int, int, int]点击碰撞区
animations.<state>.loopbool是否循环,默认true
animations.<state>.next_statestring播完后过渡到的状态

7. 交互系统

7.1 鼠标眼动追踪

每帧渲染时实时计算瞳孔位置,产生「宠物注视鼠标」的效果。

算法步骤:

1. 获取全局鼠标坐标: QCursor.pos() 2. 映射到 Widget 坐标系: widget->mapFromGlobal(global_pos) 3. 计算宠物中心: (width/2, height/2) 4. 对每只眼睛的锚点 (eye_x, eye_y): 5. dx = mouse_x - eye_x 6. dy = mouse_y - eye_y 7. dist = sqrt(dx² + dy²) 8. scale = 1 - clamp(dist / max_distance, 0, 1) 9. pupil_x = eye_x + normalize(dx) * max_offset * scale 10. pupil_y = eye_y + normalize(dy) * max_offset * scale 11. 绘制: - QColor(255, 255, 255, 220) 画白色虹膜 - QColor(20, 20, 20, 235) 画黑色瞳孔在偏移位置 - QColor(255, 255, 255, 180) 画小白色高光点

7.2 拖拽与点击

事件处理方式
mousePressEvent记录拖拽起点;启动单击计时器 (300ms)
mouseMoveEvent超过拖拽阈值 (4px) 后进入DRAGGED状态;实时更新窗口位置
mouseReleaseEvent结束拖拽,回到IDLE;保存位置
mouseDoubleClickEvent取消单击计时器;切换到HAPPY状态
单击超时 (300ms)切换到SURPRISED状态
contextMenuEvent弹出右键菜单

7.3 自主行为

behaviour_timer(1 秒间隔) 执行以下逻辑:

  1. 饥饿值更新:每秒 -0.02,高活跃度状态额外 -0.03
  2. 心情值更新:非暂停状态下微调
  3. 随机行为决策:根据饥饿值、心情值、当前状态,概率性切换到行走、奔跑、坐下、趴下、睡觉、舔毛、伸懒腰、打哈欠、眨眼等状态
  4. 边缘弹跳:碰到屏幕边缘时调转方向 (facing_right = not facing_right)

7.4 系统托盘

操作效果
单击 / 双击托盘图标召回宠物 (call_home())
托盘图标使用宠物HAPPY状态第一帧作为图标
托盘 Tooltip显示{宠物名}桌宠
托盘右键菜单与窗口右键菜单相同

8. 精灵生成管线

精灵制作与处理全流程由tools/build_sprites.pytools/build_mastiff_sprites.py实现。

整体流程

原始 4×4 姿态图集 (cat_pose_atlas.png / mastiff_pose_atlas_v2.png) │ ▼ ┌─────────────────────────────────────────────┐ │ 1. 提取: 4×4 网格分割,重叠区域扩展 │ │ 最大连通分量提取 → 透明背景角色 │ │ 2. 归一化: 各姿态统一到 256×256 画布 │ │ 保持底部基线对齐 │ │ 3. 去底: 洋红色 (#ff00ff) 色键 → 透明通道 │ │ 溢出色彩去除 (spill removal) │ └─────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ 步态关键帧 (walk_cycle_v5.png 的 4 张 │ │ 极值姿势: 左前/左后/右前/右后) │ │ │ │ │ ▼ │ │ Farneback 光流插值 (OpenCV) │ │ - 猫: 4 关键帧 → 3 中间帧/段 → 16 帧 │ │ - 藏獒: 4 关键帧 → 4 中间帧/段 → 20 帧 │ │ - RGBA 预乘处理避免透明边缘伪影 │ │ │ │ │ ▼ │ │ 状态动画: 对各姿态施加微妙运动变化 │ │ (位移 / 缩放 / 旋转) 实现呼吸、弹跳等效果 │ └─────────────────────────────────────────────┘ │ ▼ 输出: generated/ + animations.json │ ▼ make_icon.py → orange_cat.ico

光流插值关键细节

  • 使用 OpenCVcalcOpticalFlowFarneback对预乘 alpha 的 RGBA 数据做稠密光流估计
  • 每个像素通道独立插值,alpha 通道参与计算但插值后 clamp 到 [0,255]
  • 透明背景区域(alpha == 0)的 RGB 在插值前清零,避免「透明像素 RGB 污染」

9. 构建与打包

环境准备

python-m pip install-r requirements.txt# 运行时依赖python-m pip install-r requirements-dev.txt# 构建依赖

完整构建流程

build.bat按顺序执行:

tools/build_sprites.py → 生成猫咪精灵 tools/make_icon.py → 生成应用图标 pyinstaller --noconfirm --clean orange_cat_pet.spec → 打包

PyInstaller 配置 (orange_cat_pet.spec)

关键配置项:

  • 入口脚本:main.py
  • 窗口模式:console=False(不显示控制台窗口)
  • 包含资源目录:assets/整体打包进_MEIPASS
  • 额外二进制 / 数据文件:通过TOC清单指定

输出

dist/OrangeCatPet/OrangeCatPet.exe ← 最终可执行文件

10. 测试

运行测试

$env:QT_QPA_PLATFORM="offscreen"$env:ORANGE_CAT_DATA_DIR="$PWD\.runtime\test-data"python-m unittest discover-s tests-v

测试覆盖

测试文件覆盖范围
test_assets.py动画清单完整性、RGBA 图片验证、尺寸一致性、步态帧数、色键去底效果
test_models_settings.pyPetSettings序列化/反序列化、边界值校验、默认值恢复
test_qt_smoke.pyQt 环境可用性、窗口创建、动画剪辑加载
test_selection.py选择对话框 UI 元素、宠物卡片渲染

11. 配置与环境变量

运行环境

环境变量说明默认值
QT_ENABLE_HIGHDPI_SCALING启用高 DPI 缩放1
ORANGE_CAT_DATA_DIR设置文件存储目录 (用于测试)
QT_QPA_PLATFORMQt 平台插件 (测试用)

设置文件

  • 路径:%LOCALAPPDATA%\OrangeCatDesktopPet\settings.json
  • 格式: JSON
  • 字段:selected_pet,x,y,volume,always_on_top,autostart,hunger,mood,paused

开机自启

Windows 注册表项:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run └── OrangeCatDesktopPet = "<pythonw.exe路径>" "<main.py路径>"

源码运行时使用pythonw.exe(无控制台启动),打包后直接指向OrangeCatPet.exe


本文档描述的项目版本为 1.0.0,对应pyproject.toml中定义的版本。

若想要获取代码和游戏 ,绿泡泡搜索 “码来的小朋友” 然后发送回复“14桌面宠物” 即可获取。