布丁店长 · 桌面宠物扩展技能(v8 单窗口合成架构)
本技能用于持续扩展「布丁店长」这个具体桌面宠物工程(代码位于 E:\WB个人版\desktop-pet\)。
当你要给它加新动作(如新增一种随机特效)、加新功能(如右键菜单项、输入框、气泡、托盘命令、自动点击)时,
按本技能的步骤操作即可,无需从零理解整个代码库。
> 如果你要的是「换成另一只完全不同的卡通形象」,请改用 桌面宠物 技能(换形象流程)。
〇、适用前提(给协作者)
- 本机需已有桌面宠物工程
E:\WB个人版\desktop-pet\,含: desktop_pet.py(主程序,约 2360 行,含自动点击完整版内嵌)assets\(素材目录:cat.png / cat_blink.png / cat_side.png / effect_*.png).venv\(Python 3.12 虚拟环境,已装 Pillow / pystray / pywin32 / opencv-python / numpy / PyInstaller)- 如需在另一台机器协作:直接把整个
E:\WB个人版\desktop-pet目录复制过去即可(.venv 随工程走)。 - 运行时素材优先级:
exe同目录的assets\会覆盖打包内嵌资源——只换图不用重新打包。
一、项目结构速览
| 路径 | 作用 |
|---|---|
E:\WB个人版\desktop-pet\desktop_pet.py |
主程序。透明窗口、呼吸、拖拽、右键菜单、系统托盘、特效系统、闹钟全部在这里 |
E:\WB个人版\desktop-pet\assets\ |
运行时素材(见下方清单) |
E:\WB个人版\desktop-pet\references\gen_effect.py |
本技能附带:把若干白底动作 GIF 抠底生成 effect_*.png(不影响 cat.png) |
E:\WB个人版\desktop-pet\auto_click_core.py |
自动点击核心引擎(ClickEngine 守护线程 + HotkeyThread F6/F7/F8),零第三方依赖、纯 ctypes;内嵌在 desktop_pet.py 的「自动点击」右键菜单里复用 |
E:\WB个人版\desktop-pet\snapshots\ |
可回溯节点(每次大改前存一份 desktop_pet.py + 布丁店长.exe) |
E:\WB个人版\desktop-pet\dist\布丁店长.exe |
正式默认包 |
E:\WB个人版\desktop-pet\.venv\ |
Python 3.12 虚拟环境 |
运行时素材清单(必须存在,否则启动报错):
- cat.png —— 空闲正面睁眼图(主图,待机展示)
- cat_blink.png —— 闭眼图(待机不显示,但加载时会读取,必须存在,可与 cat.png 相同)
- cat_side.png —— 侧面图(行走已禁用,但加载时会读取,必须存在,可与 cat.png 相同)
- effect_blink.png / effect_stretch.png —— 当前两个随机动作动画(APNG,即带帧的 PNG)
二、核心机制(改代码前必读)
- 透明窗口 + 单窗口合成:用品红色
#ff00fe作透明色键(KEY_COLOR),
配合win.attributes("-transparentcolor", ...)+overrideredirect(True)实现异形透明。
当前(v8)是单窗口:六边形外框与猫在PIL合成到同一张图后作为唯一窗口显示,
不再有第二个窗口,从根源消除「点空白六边形盖住猫」的 z 序竞争。
⚠️ 色键二值:像素要么精确等于KEY_COLOR(透明),要么完全不透明——
窗口内任何半透明像素都会与品红混成亮紫块且抠不掉(坑 8)。 - 内容锚点对齐:所有主图/特效都按「不透明内容底边中心」对齐(
_content_anchor),
切换图片时保持该锚点屏幕坐标不变,消除静态↔特效切换的大小/位置跳变。新增素材务必保证同源。 - 异形点击区:
_scan_rects+_apply_rects用SetWindowRgn让只有猫身能接收鼠标。
-_scan_rects是纯计算(可后台跑);_apply_rects必须主线程。
- 切勿逐帧调用SetWindowRgn——只在特效开始 / 切回主图时各设置一次固定区域
(用「所有帧 alpha 并集」ImageChops.lighter累积)。 - 特效播放:绝对时间轴查表(
_update_effect),主循环 10ms 轮询与帧长不整除也不会忽快忽慢。
间隔节奏:特效结束 → 冷却SPECIAL_COOLDOWN_MS=5000(5 秒)→ 再随机播下一个。 - 单实例锁:
CreateMutexW("Local\\DesktopPet_BlackCat_UniqueInstance"),重复启动会提示已运行。 - 启动日志:
pet_boot.log(exe 同目录),双击没反应时先看它。
三、如何新增一个随机动作(特效)
目标:给布丁店长加一种每 ~5 秒随机播放的新动作(如「招手」「打滚」)。形象(cat.png)不变。
步骤 1 · 准备白底 GIF
- 素材要求:白底(或纯色底)、角色居中、动作完整一段循环。
- 浅色角色(米/奶油色)不能用 floodFill 单阈值抠图(会把浅色猫身当背景填掉),用脚本里的中性白连通分量法。
步骤 2 · 抠白底生成素材
用本技能附带的 references/gen_effect.py(需工程的 .venv,已含 Pillow/opencv/numpy):
cd /e/WB个人版/desktop-pet
/e/WB个人版/desktop-pet/.venv/Scripts/python.exe \
<本技能目录>/references/gen_effect.py \
--action 招手=swap_in/招手.gif \
--action 打滚=swap_in/打滚.gif \
--out E:/desktop-pet/assets \
--method light # 浅色/白底用 light;深色/纯色背景改用 flood
脚本只生成 effect_招手.png / effect_打滚.png(APNG),不动 cat.png,布丁形象保持不变。
- 脚本只抠透明,不缩放——缩放由 desktop_pet.py 加载时统一做。
- --method flood 适用于深色 / 纯色背景(从四角 floodFill)。
步骤 3 · 注册特效
在 desktop_pet.py 的 effect_files 字典加一项(key 会进随机池):
self.effect_files = {
"blink": _find_asset("effect_blink.png", ["effect_blink.gif"]),
"stretch": _find_asset("effect_stretch.png", ["effect_stretch.gif"]),
"wave": _find_asset("effect_wave.png", ["effect_wave.gif"]), # 新增
}
_start_random_effect 会从 self.effect_cache 的 key 里随机选,加 key 即自动进入随机池,无需改其它代码。
步骤 4 · 缩放对齐(关键,避免「变大/变小」)
特效在 _load_effect_worker 里缩放:
- 默认所有动作按内容高度对齐到 PET_H=200,使空闲态与所有随机动作首帧高度一致。
- 特例 blink(旧眨眼):按宽度 PET_W=135 对齐(与空闲猫同源,避免眨眼忽大忽小)。
- 想统一调大小:改 PET_H(所有动作高度)或 PET_W(空闲猫/旧眨眼宽度)即可。
- EFFECT_REF_W 已废弃,不再参与缩放。
步骤 5 · 备份 + 打包
见第五节。
四、如何新增一个功能(如闹钟 / 菜单项)
参考已实现的闹钟功能作为模板:
- 右键菜单项(
_on_right_click):用Menu.add_cascade做二级菜单,或add_command直接加。 - 输入框 / 对话框:
_prompt_timed_alarm是标准模板——tk.Toplevel+overrideredirect+-topmost+
Entry+ 选项按钮组 + 确定/取消;用result字典在wait_window后取值。 - 头顶气泡:
_show_speech_bubble(text, duration_ms)现成可用(圆角 + CJK 折行)。 - 置顶提醒窗:
_show_major_event(text)现成可用(带OK, 知道了按钮)。 - 定时触发:把任务存
self.timed_alarms列表,在_tick开头调_check_alarms(now)到点触发。 - 托盘命令:
_run_tray的PystrayMenu加MenuItem,回调里self.q.put("xxx"),由_process_queue在主线程消费。
新增常量全部放 desktop_pet.py 顶部。
四·乙、自动点击完整版(已对齐 AutoClicker.exe v1.0,作为标准用法)
> 历史上自动点击曾被做成「简化小弹窗」,与独立的 AutoClicker.exe 体验不一致;
> 经用户确认,当前标准形态是对齐 AutoClicker.exe 的同款三大分区窗口,已存为基线节点
> snapshots/2026-09-24T15-44_autoclick_accepted_baseline。下次迭代不要再回退成简化版。
入口与形态
- 右键宠物 → 自动点击 → 弹出一个 560 宽、标题栏独立窗口(三大
ttk.LabelFrame分区),居中、不可拉伸。 - 窗口高度不写死:在控件布局完成后用
dlg.update_idletasks()+dlg.winfo_reqheight()按内容实际高度收口,
避免底部空白(详见坑 11)。 - 底层引擎在
auto_click_core.py(ClickEngine守护线程 +HotkeyThreadF6/F7/F8),零第三方依赖、纯 ctypes。
三大分区(与 AutoClicker 同款)
| 分区 | 控件 | 说明 |
|---|---|---|
| ① 点击参数 | 单击/双击、点击次数、开始延迟(秒)、执行间隔(毫秒)、双击内间隔(毫秒)、随机抖动(%) | 选「双击」时「双击内间隔」才启用(单选变「单击」时置灰 + 灰字提示) |
| ② 坐标位置 | 固定坐标(X/Y) / 跟随鼠标 / 多坐标序列 | 多坐标序列:列表框 + 增/删/上移/下移/清空;F8 抓坐标填入 X/Y;实时光标每 200ms 刷新 |
| ③ 控制与状态 | 开始(F6) / 停止(F7) 按钮、状态栏、进度条、急停勾选、最小化勾选、保存/载入配置、预计耗时 | 运行中窗口保持打开,进度条 + 状态栏实时刷新 |
关键交互(实现要点,照抄此行为)
- 热键:
F6开始 /F7停止 /F8抓坐标。F6/F7 走HotkeyThread,窗口打开时把_ac_start_fn = _start、_ac_stop_fn = _stop挂上,F6 直接复用窗口的"开始"函数(否则热键只读旧click_cfg会弹"请先打开自动点击设置并开始")。 - 进度转发:引擎把
progress/status消息放进self.click_q,主线程_drain_click_queue消费;转发前必须
dlg._refresh_click = _refresh_click把刷新函数挂到窗口对象上,否则进度条/状态栏永远不更新(坑 12)。 - 保存/载入配置:用
tkinter.filedialog.asksaveasfilename/askopenfilename(不要静默写死到固定路径),
让用户自选位置、能看到生成的 JSON 文件;JSON 字段:click_type/count/delay/interval_ms/double_gap_ms/jitter_pct/mode/x/y/points/minimize/failsafe。 - 预计耗时:
次数×间隔 + 延迟(+双击间隔)估算,显示「约 X 分 Y 秒(±抖动)」。 - 隐藏宠物窗口(防误点):勾选后点击开始时
withdraw()宠物本体;引擎结束回调_on_click_finish→_click_finish_gui
里deiconify()恢复,避免宠一直消失。 - 开始后最小化本窗口:勾选后
iconify()自动点击窗口自身,结束恢复。
改动落点速查(下次改自动点击直接看这几处)
_prompt_auto_click(约 1310 行起):窗口全部 UI。_drain_click_queue(约 1188 行):把click_q消息转发给click_win._refresh_click。_on_click_finish/_click_finish_gui:引擎结束回调 + 恢复宠物窗口。_click_hotkey_start/_click_hotkey_stop:F6/F7 热键,优先调self._ac_start_fn/self._ac_stop_fn。__init__:self.click_q / click_engine / click_hotkey / click_cfg / click_running / click_hid_pet / _ac_start_fn / _ac_stop_fn。
五、打包流程(含沙箱 safe-delete 绕过)
> 沙箱会拦截覆盖 dist\布丁店长.exe,导致打包后复制失败。用 Python 复制(不要 PowerShell/rm)。
# 1) 先杀掉正在跑的旧进程,否则 exe 被锁无法覆盖
tasklist | grep -i DesktopPet && taskkill /F /IM DesktopPet.exe
# 2) 大改前先存节点(可选但强烈建议)
mkdir -p E:/desktop-pet/snapshots/<时间戳>_说明
cp E:/desktop-pet/desktop_pet.py E:/desktop-pet/snapshots/<时间戳>_说明/
cp E:/desktop-pet/dist/布丁店长.exe E:/desktop-pet/snapshots/<时间戳>_说明/
# 3) 编译自检
cd /e/WB个人版/desktop-pet && /e/WB个人版/desktop-pet/.venv/Scripts/python.exe -m py_compile desktop_pet.py
# 4) 打包(用工程自带 build_pet.py,已处理 safe-delete 绕过)→ dist/DesktopPet.exe
cd /e/WB个人版/desktop-pet && /e/WB个人版/desktop-pet/.venv/Scripts/python.exe build_pet.py
# 5) 复制为正式名(绕开 safe-delete,用 Python)
cd /e/WB个人版/desktop-pet && /e/WB个人版/desktop-pet/.venv/Scripts/python.exe - <<'PY'
import os, shutil
src="dist/DesktopPet.exe"; dst="dist/布丁店长.exe"
if os.path.exists(dst): os.remove(dst)
shutil.copy2(src, dst)
print("size=%.1fMB" % (os.path.getsize(dst)/1024/1024))
PY
- 体验变体包用不同
--name(如desktoppet-125x200)。 - 若 GUI 启动异常,查
dist/pet_boot.log。
六、关键常量速查
| 常量 | 值 | 含义 |
|---|---|---|
KEY_COLOR |
#ff00fe |
透明色键(二值) |
PET_H |
200 | 待机猫内容高度(缩放基准);默认动作按此高度对齐 |
PET_W |
135 | 空闲猫 / 旧眨眼(blink) 显示宽度基准(按宽135缩放,高约216) |
SPECIAL_COOLDOWN_MS |
5000 | 特效结束到下一次可触发的冷却 = 5 秒 |
EFFECT_MIN/MAX_FRAME_MS |
40 / 300 | 单帧时长兜底,防 0/10ms 畸形帧抽搐 |
EFFECT_TICK_MS |
10 | 特效轮询周期(与帧长不整除不会抖) |
HEX_PAD_X/Y |
52 / 44 | 正六边形外框到猫身的间距 |
EFFECT_PAD |
12 | 六边形四周额外透明余量(容纳特效轻微溢出/呼吸) |
HEX_FILL |
(252,250,243) | 外框极淡暖白填充,绘制时务必不透明 +(255,) |
七、常见坑(血泪经验)
- alpha 二值化必须放在 resize 之后:LANCZOS 会插值出半透明像素→紫边。
- 逐帧
SetWindowRgn会卡顿/闪烁:只设一次固定「帧并集」区域。 - 呼吸用
geometry()移动即可,别重设 region。 - 内容锚点对齐:静态图与特效必须同源同锚点,否则切换时左偏/上抬。
- 浅色角色抠图用中性白连通分量,别用 floodFill 单阈值(会填掉浅色猫身)。
- 换素材优先丢 exe 同目录 assets,不用重打包;改代码逻辑才需打包。
- 打包复制用 Python,避开沙箱 safe-delete 拦截。
- 【最贵的一课】色键透明窗口里绝不能画半透明像素(排查多轮才定位的根因)。
- 原理:-transparentcolor走SetLayeredWindowAttributes的LWA_COLORKEY,是二值的:
像素要么精确等于KEY_COLOR(#ff00fe)被完全抠掉,要么完全不透明。没有中间态。
- 症状:抠图后若留半透明边缘(alpha 1~254),会与品红底混成亮紫色且抠不掉 → 角色周围一圈紫边;
成片半透明 → 一整块亮紫块。
- 定论:抠完背景后必须做 alpha 二值化,且必须放在 resize 之后。本工程
gen_effect.py/gen_swap.py已内置二值化(np.where(bg,0,255)),但用户在 GIMP/PS
里手修图时若留羽化边缘,仍会出紫边,需重导为硬边或回脚本重抠。 - 多个测试 exe 会害死你:曾同时存在 11 个
DesktopPet*.exe,用户反复运行旧版,
导致「修了却说没修好」的空耗。每次交付只保留唯一正式 exedist/布丁店长.exe,
旧构建移入dist/_old_builds/。若打包时覆盖 exe 报
PermissionError: [WinError 32] 另一个程序正在使用此文件,即证明旧实例仍在运行。 - GIF 帧时长畸形会抽搐:GIF 常见 0ms/10ms 帧,
_load_effect_worker已用
EFFECT_MIN/MAX_FRAME_MS兜底,无需手动处理。 - 弹窗高度别写死:自动点击窗口曾写死
560x600,内容不足时底部一大块空白。
正确做法:布局完成后dlg.update_idletasks()再dlg.geometry("560x%d+...+%d" % (dlg.winfo_reqheight(), ...))
按内容实际高度收口(宽度固定、居中)。任何「固定高度 + 内容可变」的弹窗都适用。 - 主线程→窗口的进度回调必须挂到窗口对象:
_drain_click_queue转发引擎进度时检查
hasattr(click_win, "_refresh_click"),若_refresh_click只是窗口内的闭包、dlg上没属性,
转发会永远静默失败(进度条/状态栏不动)。务必在初始化里dlg._refresh_click = _refresh_click。 - F6 热键要复用窗口的"开始":旧实现热键只读
self.click_cfg,而弹窗没把配置交进去,
按 F6 会弹"请先打开自动点击设置并开始"。窗口打开时self._ac_start_fn = _start、
self._ac_stop_fn = _stop,热键优先调它们,与点击按钮等价。 - 保存/载入配置用文件选择框:不要静默写死到
dist/auto_click_config.json,用户看不到也选不了;
用filedialog.asksaveasfilename/askopenfilename,成功提示带上实际文件名。