我用一个 .ps1 文件写了一只会走路的桌面猫:不装环境、不构建、双击就跑
先看成品:它是一只像素猫,站在 Windows 桌面上走来走去,会发呆、伸懒腰、跳舞、撒欢,你点它的头它会记住,久了会有好感度等级。
它叫 Desktop Cat(桌面小猫)。比较特别的地方不是它长什么样,而是它的实现方式:
整个程序就是一个 .ps1 文件,里面内联了一整段 C#(WinForms),运行时用 Add-Type 现场编译。没有构建步骤、没有依赖、源码只有一个文件。
这篇文章讲它为什么会长成这样,以及中间踩的坑 —— 尤其是那个把单核 CPU 吃到 81% 的性能 Bug。
一、它到底是什么
| 项目 | 实际情况 |
|---|---|
| 动作 | 12 组动画(发呆 / 走路 / 伸懒腰 / 跳舞 / 撒欢 / 摔倒等) |
| 大小 | 4 档(75% / 100% / 125% / 150%) |
| 好感度 | 6 级,从「陌生」到「心之友」,上限 400 |
| 右键菜单 | 13 项,全中文 |
| 主程序 | 54,272 字节(约 53 KB)的 DesktopCat.exe |
| 源码 | 一个 2,717 行的 pet.ps1 |
| 空闲占用 | 单核 0.4% ~ 0.8%,内存 60 MB 上下 |
| 网络请求 | 0(一个字都不发) |
| 注册表写入 | 0(数据只写在自己目录下的 .petdata) |
| 许可 | MIT |
安装方式就一句话:解压,双击 exe。卸载就是删文件夹。
二、为什么会走到「PowerShell 内联 C#」这条路上
最早我是想做 Electron 的 —— 生态熟、写界面快。结果在目标机器上是这样的:
FATAL:platform_channel.cc(83) Check failed: .: 拒绝访问。 (0x5)
进程直接起不来。换了几种方式都一样,基本可以判定是被机器上的安全策略拦住了。这条路就废了。
那就得重新回答一个更基本的问题:我要给用户的到底是一个什么样的东西?
- 零依赖:不能让用户去装 .NET SDK、装 Node、装 Python。双击就该能用。
- 不联网:我不希望一个桌面摆件偷偷往外发数据,这也是它后来能上应用推荐站的原因之一。
- 不要构建步骤:这是给我自己定的。改一行代码要跑一遍编译链,我这种一天改几十次的人受不了。
- 单文件:源码就一个文件,扔到哪里都能跑。
Windows 上能满足这四条的组合,其实只有一个:
- 系统自带 PowerShell 5.1,也自带 .NET Framework;
- PowerShell 的
Add-Type可以在运行时把一段 C# 编译成内存里的程序集,然后直接调用。
于是就有了这个结构:一个 .ps1 文件 = 一段内联的 C# 程序 + 一点 PowerShell 胶水。
它的好处和代价都很明显:
好处:用户零依赖。Win10 / Win11 开箱就能跑,不需要管理员权限,不需要装任何东西。
代价:源码是 .ps1,IDE 的断点调试基本用不上;每次启动都要把内联的那段 C# 编译一遍(同一次运行里只编译一次,但下一次启动会重新来);另外,杀毒软件对「脚本里编译代码」这种行为天然敏感,需要跟用户解释清楚。
三、技术点 1:Add-Type 内联编译 WinForms
核心就两行参数的事情,但有几个坑:
Add-Type -TypeDefinition $csharpCode -ReferencedAssemblies System.Windows.Forms, System.Drawing, System.Core
-ReferencedAssemblies必须显式写上System.Windows.Forms和System.Drawing,不写就编译不过。- C# 代码建议用 here-string 包起来。注意:内联的 C# 里不要出现和 here-string 结束符冲突的字符序列,否则 PowerShell 会提前截断,报的错会非常莫名。
- 编译只做一次,编译结果在内存里,后面直接
New-Object那个类型就行。 - 编译失败时
Add-Type抛的异常信息比较绕,建议先拿一段 10 行的最小 C# 跑通,再往里搬代码。
整个程序的骨架就是这样:PowerShell 负责资源和配置,C# 负责窗口、绘制、动画和消息处理。
四、技术点 2:透明窗口 + 按像素决定「能不能点」
要做一个不被识破的桌宠,窗口必须是「看得见猫、看不见框」的。WinForms 里最省事的路子是透明色:
FormBorderStyle = FormBorderStyle.None;
TransparencyKey = Color.FromArgb(255, 1, 2, 3); // 用一个几乎不会出现在素材里的颜色
SetStyle(ControlStyles.OptimizedDoubleBuffer
| ControlStyles.AllPaintingInWmPaint
| ControlStyles.UserPaint, true);
用 (255,1,2,3) 这种颜色当透明键,是为了避免和素材里的颜色撞车 —— 一旦撞上,猫身上就会出现一个真实的洞。
真正麻烦的是点击穿透。桌面宠物有两个互相矛盾的需求:
- 猫身上的点击要能被它收到(好点头涨好感度);
- 猫外面的区域必须完全穿透,你在桌面上点图标、拖文件不能被它挡住。
做法是处理 WM_NCHITTEST(0x0084):
- 命中测试时,去看该像素的 alpha 值;
- alpha 低于阈值 → 返回
HTTRANSPARENT(-1),系统把点击交给下面的窗口; - alpha 够高 → 返回正常值,这次点击就算在猫身上。
阈值我取的是 40。这个数字直接决定手感:取太低,猫周围的半透明边缘会「偷」掉你的点击;取太高,猫的轮廓会点不中。
五、技术点 3:全局鼠标钩子,只挂一个消息
窗口是穿透的,所以「点猫」这件事不能靠窗体自己收 MouseDown —— 它收不到。这里用了全局低级鼠标钩子:
SetWindowsHookEx(WH_MOUSE_LL /* 14 */, proc, IntPtr.Zero, 0);
关键是克制:只处理 WM_LBUTTONDOWN,其它消息立刻放行返回。低级钩子是全局的,回调写得越重,整台机器的鼠标就越钝。
这一条也是它 CPU 占用能压下来的前提之一 —— 实测整个动画循环摊到每帧大约 0.12 毫秒。
六、技术点 4(重点):性能坑,从单核 81% 到 0.5%
这是整个项目里最值得写的一节。
第一版的写法很自然:桌宠要在「有变化的时候」重绘,那就在 Application.Idle 里画 —— 空闲就画,听起来合情合理。
实测结果是灾难:
- 跑 20 秒;
- 用进程的
TotalProcessorTime前后差值去比对墙钟时间; - 结果:20 秒的墙钟时间里烧掉了 16.27 秒 CPU 时间,等效于单核 81% 。
也就是说,桌面上摆一只猫,等于有一个核心在满负荷跑。这显然不能发布。
问题出在哪:Application.Idle 不是「系统空闲」的意思,它是**「消息队列空了」**的意思。桌面程序的消息队列大部分时间都是空的,所以这个事件被触发得极其频繁,动画就被画成了「尽可能快地重绘」。
修法:把「重绘」从「空闲就画」改成帧驱动。
- 用一个 15 毫秒间隔的
Timer去推进行为状态机(散步、发呆、切换动作); - 只有当状态真的发生变化时才
Invalidate()请求重绘; - 没有变化就什么都不做 —— 不重绘、不分配、不计算。
改完之后,空闲时的 CPU 占用是 单核 0.4% ~ 0.8% ,内存 60 MB 上下。
前后差了大概两个数量级,而代码量并没有增加多少。
给同样写 WinForms / WPF 动画的人一句提醒:别把 Application.Idle 当定时器用。 它不是「空闲时执行一次」,它是「只要没事干就执行」,用它驱动动画就是个定时炸弹。
七、技术点 5:不联网的持久化
它的数据只有一份 config.json,内容就是位置、好感度、互动次数、静音、锁定、缩放、是否置顶这几项。
有几个可以说的决定:
为什么手写 JSON 解析。PowerShell 5.1 的 ConvertFrom-Json 在碰到某些字符时行为不理想,而我又不想为了一个小配置去引第三方模块。所以最后是手写的:读文件,找 "key",取它后面第一个数字。写的时候同样手拼字符串。
一个要提醒用户的坑:程序在退出时会保存配置。所以运行期间直接去改 config.json,改动会被覆盖掉 —— 想改数字,先把程序退干净再改。
数据全部限制在自己目录内。不写注册表、不写 %APPDATA%、不联网。卸载就是删文件夹,不留任何东西。
八、几个设计上的小决定
▸ 插图片:
5-右键菜单.png(就是这 13 项菜单)
- 双击:把猫叫回屏幕中央,不给好感度 —— 否则用双击刷等级比认真互动还快。
- 好感度只有 4 个入口:点头顶 +2、点身体 +1、拽尾巴 -1、滚轮 ±10。入口少,等级才有意义。
- 等级门槛:0 / 20 / 50 / 100 / 200 / 400,六档。前两档很快,后面越来越慢。
- 透明色不要用纯黑或纯白:素材里一定会撞。
九、最后
它现在是 MIT 开源的,源码、Release 免安装包都在同一个仓库:
- GitHub:
github.com/wt0812/desktop-cat - Gitee(国内更快):
gitee.com/wutong2005/desktop-cat
它也被国内做软件推荐很多年的小众软件(Appinn)收录了一帖:meta.appinn.net/t/topic/93169
如果你也想用「PowerShell 内联 C#」这种方式做 Windows 小工具,我的建议是:先用它做一个 50 行以内的窗口,跑通 Add-Type 的引用和 here-string 这两个坑,再往里堆功能。 这条路最大的成本不在于写代码,而在于调试体验差 —— 所以别等它长到几千行才发现这点。
(本文的代码细节都来自实际项目,数字都是真机实测;如果有说得不对的地方,欢迎在评论区指出来。)