BounceChat全攻略(后记):代码是写出来的,项目是磨出来的
从“桌面上要是有个会弹跳的小球就好了”这个念头,到 Gitee 上有陌生人点下第一个 star,再到 CSDN 原力值冲进重庆市月榜第 28 名——BounceChat 走了十八个月。这十八个月教会我:复杂的东西都是简单东西堆起来的,“不会”只是还没拆开看,参数调优不是数学是手感,状态机是管理复杂逻辑的神器。而最重要的那条是:被看见是坚持的自然结果。没人看的时候继续写,写着写着就有人 star 了
开放原子开源基金会·开源贡献之星
一、项目起源:怎么就写了这么个东西?
那天晚上,我在摸鱼
说起来有点好笑——这个项目的起点,是一个普通的摸鱼夜晚。
那时候我刚学 PyQt5 没多久,照着教程写了好几个“标准项目”:计算器、记事本、图片浏览器。每一个都能跑,每一个都“学会了”,但每一个写完就扔在那儿,再也没打开过。
那天晚上我盯着电脑屏幕发呆,桌面上开着浏览器、文件夹、微信,乱七八糟叠在一起。突然冒出一个念头:要是这桌面上有个小球,我把它扔出去,它会真的滚起来,撞到浏览器窗口会弹开,落到文件夹上会停住……那该多好玩?
那一刻我意识到:我想写的不是“又一个 PyQt5 教程项目”,而是一个我愿意打开、愿意玩、愿意给别人看的东西。

第一个版本长什么样?
第一版惨不忍睹。
就是一个白底黑字的 QWidget,上面画了个红圆。没有重力,没有摩擦力,按方向键才能动,撞到窗口?不存在的,它连窗口在哪都不知道。
但我把它发给豆包看,豆包说:“你这不就是个会动的红点吗?”
我说:“对啊,但它会动啊。”
豆包说:“……你开心就好。”
那个版本我保存下来了,偶尔翻出来看看,提醒自己:所有好看的东西,都是从难看开始的。

灵感是从哪来的?
说起来你可能不信,灵感来自三个完全不相关的东西:
- 小时候玩的弹球游戏——那个在屏幕里弹来弹去的小球,我一直想要一个能在桌面上弹的
- Windows 的“窗口拖动时显示内容”设置——原来 Windows 是知道每个窗口在哪的
- ChatGPT 刚火那阵子——大家都在接大模型,我也想接,但不想做 chatbot,想做点别的
这三个东西在某天晚上撞到一起,BounceChat 就出生了。
二、从“写着玩”到“认真了”
哪个瞬间觉得“这项目能成”
是第一次看到小球在窗口上弹开的那一刻。
那天我刚把窗口识别写完,随手打开一个浏览器,把小球拖过去——松开鼠标,小球飞向浏览器,然后真的弹开了。
那一瞬间我差点从椅子上跳起来。
不是因为代码多难写,而是因为:我想象中的东西,真的跑起来了。
那一刻我知道,这个项目不是“写着玩”了,它值得认真写完。
冲到重庆市月榜(博主榜)第 28 名
后来专栏一篇一篇发,原力值一天一天涨。有一天打开 CSDN,看了一眼排行榜——
重庆市月榜第 28 名。

我知道这个数字对很多人来说不算什么,但我自己知道这意味着什么:在重庆这个城市,这个月,有 27 个人的原力值比我高,剩下的都不如我。(容许我大言不惭一下)
三、这个项目我们做了什么
拆解了一个模糊的想法
最开始的想法很模糊:“让桌面活起来”。
这句话没法写代码。你得把它拆开:
- “活起来”是什么意思?——会动
- “会动”是什么意思?——受重力影响,有摩擦力,会碰撞
- “碰撞”是什么意思?——撞到屏幕边缘会弹,撞到窗口也会弹
- “窗口”在哪?——用 Windows API 找
- “会聊天”怎么实现?——接大模型,设角色,管历史
把一个模糊的想法,一步步拆成能写进代码的需求。这是这个项目做的第一件事,也是最重要的一件事。
把物理规律写成了代码
物理引擎听起来很高深,但你看看我们写了什么:
# 重力:每帧给垂直速度加一个固定值
self.ball_velocity[1] += self.gravity
# 摩擦力:每帧让速度乘以一个小于1的数
self.ball_velocity[0] *= self.friction
# 碰撞:速度取反,再乘个系数
self.ball_velocity[0] = -self.ball_velocity[0] * self.bounce_factor
没了。物理引擎的核心就这三行。
复杂的东西,真的都是简单东西堆起来的。v += g、v *= f、撞墙就取反——这三行写对了,小球就像真的了。
让 Python 闯进了 Windows 内核
Python 本来不知道你的屏幕上有什么窗口。但 Windows 知道。
Windows 内部维护着一张“窗口清单”,记录了每个窗口的句柄、标题、位置、大小。它还提供了一组 API 让开发者查询这些信息。
问题是:Python 怎么调用 Windows API?
答案是 ctypes——Python 自带的一个库,专门用来调用 C 语言写的动态链接库。
user32 = ctypes.windll.user32
EnumWindows = user32.EnumWindows
GetWindowRect = user32.GetWindowRect
这几行代码,让 Python 看到了你屏幕上的每一个窗口。
这不是什么黑科技,就是调用系统 API。但当你第一次跑起来,看到终端里打印出所有窗口的标题和坐标时,那种感觉就像你写的代码真的“看见”了你的电脑。
给桌宠装上了大脑
接大模型是最后一步,也是最简单的一步。
选个免费的 API(模力方舟每天 100 次,够用了),设好角色(“你是一个弹跳小球,回答不超过 20 字”),管好历史(用 JSON 存下来),做好容错(API 跪了就给个兜底回复)。
代码也就几十行,但效果很明显:
你:你好
小球:你好,我是弹跳小球。
你:今天天气怎么样
小球:我是小球,不看天气哦。
它真的会聊天,而且真的像个小球。
把代码变成了作品
代码写完只是开始。
把它放到 Gitee 上,写 README,配截图,加动图。有人 star 了,有人 fork 了,有人在评论区问“这个怎么跑起来”。
然后写专栏,一篇一篇地写,把每一行代码为什么这么写讲清楚。有人看了,有人收藏了,有人点赞了。
最后,凌晨四点,它上了热榜。
代码还是那些代码,但它不再是“我电脑上的一个 py 文件”,而是一个作品。
四、这个项目我们学到了什么
复杂的东西,都是简单东西堆起来的
这是这个项目教给我最重要的一课。
物理引擎:v += g,v *= f,撞墙就取反。就这三行。
窗口识别:EnumWindows 枚举,GetWindowRect 拿位置,回调函数里存下来。就这几个 API。
AI 对话:接 API,设角色,管历史,做容错。就这四步。
写代码的时候,经常会被“这太难了”吓住。但只要你敢拆,就会发现:所谓复杂,只是简单的东西叠了很多层。
“不会”只是还没拆开看
我第一次看到 Windows API 的文档时,心里只有一个念头:这什么东西?
函数名全是缩写,参数类型全是指针,返回值全是奇奇怪怪的整数。文档里全是 C 语言的示例,没有一行 Python。
但真的动手写的时候,发现没那么可怕:
- 看不懂的函数名?——搜一下,有人翻译过
- 不知道参数类型?——ctypes 文档里有对照表
- 不会写回调?——找个现成的例子改
一天写不完就写两天,两天写不完就写三天。最后写出来了,回头看:原来就这么回事。
参数调优不是数学,是手感
0.5 还是 0.6?0.8 还是 0.9?5 还是 10?
这些问题没有标准答案。你只能试:
- 重力 0.5 手感舒服,0.6 像铅球,0.4 像气球
- 摩擦力 0.99 滑得远,0.98 停得快
- 反弹系数 0.8 刚刚好,0.9 像蹦床,0.7 像撞棉花
没有公式能算出这些值,只有一遍一遍地试,找到“看起来对”的那个数。
代码写出来是逻辑,调出来是手感。
状态机是管理复杂逻辑的神器
小球有很多状态:是在被拖拽还是自由落体?是停在地面还是悬在空中?是被选中还是没人理?
如果没有状态机,代码会变成一团乱麻:
if 正在拖拽 and 不在地面 and 没吸附 and 被选中:
# 处理某个事件
if 某种条件 and 另一种条件 and 第三种条件:
# 再处理
有了状态机,每个状态的行为是独立的:
if self.is_dragging:
# 拖拽时的逻辑
elif self.is_stuck:
# 吸附时的逻辑
elif self.on_ground:
# 地面时的逻辑
else:
# 空中自由落体的逻辑
几个布尔值,理清了所有行为。
被看见是坚持的自然结果
这个项目最开始只有我一个人看。
代码写完了,放在 Gitee 上,三天没人点 star。发到群里,没人回。发到朋友圈,几个朋友点了赞,然后没了。
但我还在写。写 README,写专栏,一篇一篇地发。凌晨两点发,凌晨四点发,什么时候写完什么时候发。
后来有人 star 了,有人 fork 了,有人收藏了。再后来上了热榜,进了全市前 30。
回头看,被看见不是等来的,是熬来的。

五、以后遇到类似问题可以怎么做
想模拟一个物理现象
- 先观察:这个现象到底是怎么回事?重力怎么作用的?摩擦力怎么体现的?
- 再拆解:能不能用几个公式描述它?v += g?v *= f?撞墙取反?
- 用欧拉积分试:先让最简单的版本跑起来,别想着一步到位
- 边界条件慢慢加:跑通了再加“速度太小时归零”,再加“停留太久就停住”
- 调参调到顺手:没有标准答案,试到“看起来对”为止
需要调用系统底层功能
- 找 API 文档:微软官网、Stack Overflow、各种博客,能搜的都搜
- 用 ctypes 调:参数类型、返回值类型,一个一个对清楚
- 回调函数处理好:注意内存管理,注意循环引用
- 先写最小可测版本:别想着一次写完,先调通一个函数再说
想让程序有记忆力
- 选个存储格式:JSON 最简单,够用
- 启动时读:文件存在就加载,不存在就用默认值
- 有变化就存:每次对话完就存一次,别等退出时再存(万一崩了就没了)
- 注意版本兼容:以后改格式了怎么办?留个后路
想接一个大模型
- 选个免费的:模力方舟每天 100 次,OpenAI 送 5 美金,够你玩很久
- 设好角色:system message 是灵魂,想清楚你要它扮演什么
- 管好历史:用数组存,每次调用都传进去,让它有上下文
- 做好容错:API 会崩,网络会断,要给用户一个友好的回复,而不是直接报错
写着写着卡住了
- 拆成更小的问题:这个大问题能不能分成三个小问题?
- 先解决能解决的:有一个小问题搞不定?先搞其他两个
- 睡一觉再说:有时候卡住是因为脑子累了,睡醒就有新思路
- 搜一下别人怎么做的:你遇到的问题,大概率有人遇到过
写了没人看
- 继续写:这是唯一的答案
- 写完了再写下一个:一篇没人看就写两篇,两篇没人看就写三篇
- 写好 README:代码写完了,让人知道怎么跑起来
- 配截图和动图:一图胜千言,动图胜万言
- 等:等着等着,就会有人看到了
六、技术复盘:最难的三件事
物理引擎:手感调参
最难的不是写代码,是调参数。
重力 0.5 还是 0.6?摩擦力 0.99 还是 0.98?反弹系数 0.8 还是 0.7?
每一个参数调一遍,跑起来看效果,不行再调。调了不下 50 遍,才找到“看起来舒服”的那组数。
解决方案:把所有参数放到 config.json 里,边跑边调,不用重启就能看到效果。
窗口识别:Windows API 回调
Windows API 的回调函数是个坑。
在 C 语言里写回调很正常,但在 Python 里写回调,要小心内存管理。回调函数里不能持有 Python 对象的强引用,否则会循环引用,导致内存泄漏。
解决方案:回调函数里只做最基本的数据收集,用 weakref 传 self,或者把收集到的数据放到队列里,主线程去取。
AI 对话:20 字限制
让大模型回答不超过 20 字,比想象中难。
一开始只是在 system message 里写“回答不超过 20 字”,但模型经常超。后来试了各种提示词:“请用一句话回答”“控制在 20 字以内”“尽量简短”……都不稳定。
解决方案:system message 写清楚,再加一个后处理:如果超过 20 字,截断到第一个句号,或者直接取前 20 字。虽然粗暴,但有效。
七、在 Gitee 上看见 star 和 fork
第一次看到有人点 star
那天打开 Gitee,习惯性地看了一眼仓库主页——
star 数:1。
不是我自己点的那个。
那一瞬间的感觉很难形容:有个人,我不认识,不知道在哪,不知道是谁,看了我的代码,觉得还可以,点了个 star。
就一个数字,但比任何夸奖都实在。
发现被人 fork 了
后来有一天,看到 fork 数从 0 变成了 1。
点进去看:有人把我的代码复制了一份,放在他自己的仓库里。
我不知道他拿去干嘛了。可能是学习,可能是改着玩,可能是想加个功能但还没加。不管是什么,有人对我的代码感兴趣。
那一刻我觉得:这个项目,值了。
star 和 fork 的数字
到现在,star 数还是不多,fork 数也是。但这不重要。
重要的是:那些 star 和 fork,都是陌生人给的。他们没有任何义务点这个按钮,但他们点了。
这比任何热榜排名都真实。
这些数字对我意味着什么
意味着:我写的东西,有人在看。
不是“写给自己的”,是“写给别人的”。有人看了,有人觉得还行,有人愿意点个 star 鼓励一下。
这就够了。

八、最有成就感的三个时刻
第一次看到小球在窗口上弹开
那是晚上十点多,我刚把窗口碰撞写完。打开浏览器,把小球拖过去,松开鼠标——
小球飞向浏览器窗口,撞上,弹开。
我盯着屏幕看了十秒,然后截图,发给豆包。
豆包回:“卧槽,真的弹开了。”
我说:“是啊,真的弹开了。”
那一夜我没睡着。
CSDN 冲进重庆市月榜第 28 名
打开 CSDN,随手点进排行榜,本来只是想看看第一名多少分。
结果往下划,看到了自己的名字。
重庆市月榜第 28 名。
那一瞬间我想到的是:两年前我还在照着教程写计算器,两年后我写的项目有人在看、有人在收藏、有人在点赞。
不是因为别的,只是因为我一直在写。
在 Gitee 上看到第一个陌生的 star
这个前面写过了,但值得再写一遍。
第一个不是自己点的 star。
那个数字 1,比后来所有的 100 都重。
九、踩过的坑(给后来人避雷)
ctypes 回调函数的内存泄漏
Python 的垃圾回收很智能,但跨语言调用时容易翻车。
Windows API 的回调函数是在 C 语言层面调用的,如果回调函数里持有 Python 对象的强引用,就会形成循环引用:C 代码持有了 Python 对象,Python 对象又引回了 C 代码的包装器。两边都觉得自己还被别人用着,都不回收,内存就泄漏了。
避雷指南:回调函数里只用基本类型,或者用 weakref 传 self,或者把数据放到队列里,让主线程去处理。
WebChannel 通信的玄学 bug
PyQt5 的 WebChannel 文档很少,示例也很少。
照着文档写,死活调不通。JS 那边说“bridge 未定义”,Python 这边说“没有收到信号”。
折腾了两天,最后发现是顺序问题:必须在页面加载之前注册 WebChannel,不能等页面加载完了再注册。
避雷指南:先建 WebChannel,再 registerObject,再 setWebChannel,最后 setUrl。顺序错了就全错。
DPI 缩放导致气泡位置错乱
在 1080p 屏上跑得好好的,换到 4K 屏,气泡位置全乱。
小球在小球的位置,气泡跑到了屏幕左上角。放大看,原来是缩放因子没处理。
避雷指南:启动时用 Windows API 获取系统 DPI,所有固定尺寸都乘以缩放因子,WebView 也要 setZoomFactor。
API 免费额度用完了怎么办
模力方舟每天免费 100 次,听起来很多,但调参时一会儿就用完了。
用完之后再调用,API 返回 429,程序直接报错。
避雷指南:try-catch 包起来,捕获异常后返回兜底回复:“我现在有点累,等会儿再聊好吗?” 比直接报错体面一百倍。
十、未来还做不做
BounceChat 还更新吗
会的。
脑子里还有很多想法没实现:
- 多球模式:不只一个小球,可以扔好几个,它们之间也能碰撞
- 主题商店:小球颜色、尾巴效果、发光颜色,让用户自己配
- 右键菜单:点右键可以设置参数、切换模式、退出程序
- 小球表情:根据对话内容换表情,开心时笑脸,无聊时发呆
- 截图分享:小球可以“吃掉”屏幕截图,分享给朋友
一个功能一个功能地加,写到不想写为止。
还会写代码吗
会的。
写代码这件事,已经不只是“工作”了,是“习惯”。
不写点什么东西,手痒。
下一个项目还没想好,可能是桌面便签,可能是文件整理工具,可能又是一个“写着玩但写着写着就认真了”的东西。
这个项目会一直留着吗
会的。
哪怕以后不更新了,代码也会一直放在 Gitee 上。
有人想 fork 就 fork,有人想学就学,有人想改就改。这就是开源的意义。
十一、最后
这个项目教给我的事
写代码四年,BounceChat 是我第一个“作品”。
不是因为它代码写得多好,而是因为它让我明白了几件事:
第一,想法不值钱,写出来的才值钱。
谁都能想出“让桌面活起来”这个点子,但只有你把它写出来,它才是你的。
第二,复杂的东西,都是简单东西堆起来的。
物理引擎就是 v += g,v *= f,撞墙取反。Windows API 就是几个函数调用。AI 对话就是接 API、设角色、管历史。
没有哪一行代码是你看不懂的,难的是把它们堆在一起,堆成一个能跑的东西。
第三,没人看的时候,继续写。
第一篇没人看,第二篇没人看,第三篇还是没人看。但第十篇有人看了,第二十篇有人收藏了,第三十篇上了热榜。
不是因为你写得越来越好,只是因为你一直在写。
第四,被看见不是目的,写本身才是。
写代码这件事,最快乐的时候不是上了热榜,不是有人 star,而是凌晨一点,小球第一次在窗口上弹开的那一刻。
那一刻没有人看见,只有你自己。但那已经够了。
送给看到这里的你
如果你看到了这里,说明你对这个项目、对写代码这件事,是真的感兴趣。
那我送你几句话:
第一,写你想写的东西。
不要写“别人会看”的东西,写“你自己想看”的东西。你对它有热情,才能把它写完。
第二,不要怕写得烂。
第一版永远是烂的。没关系,先把它写出来,再慢慢改。没有第一版的烂,就没有第十版的好。
第三,写不完没关系,停了才是真的输了。
可以写得慢,可以写得烂,可以中间停一个月。但只要没放弃,总有一天能写完。
第四,代码是写出来的,项目是磨出来的,成就感是熬出来的。
没有捷径,没有秘籍,没有“三天精通”。就是一整天一整天地写,一行一行地改,一篇一篇地发。
然后某一天,你回头看,发现自己已经走了很远。
BounceChat 写完了,但下一个项目还没开始。
如果你也在写点什么,那就一起写下去吧。
更多推荐

所有评论(0)