我用一个 .ps1 文件写了一只会走路的桌面猫:不装环境、不构建、双击就跑

0 阅读8分钟

我用一个 .ps1 文件写了一只会走路的桌面猫:不装环境、不构建、双击就跑

先看成品:它是一只像素猫,站在 Windows 桌面上走来走去,会发呆、伸懒腰、跳舞、撒欢,你点它的头它会记住,久了会有好感度等级。

3-桌面截图.png 它叫 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

6-动作状态图.png

安装方式就一句话:解压,双击 exe。卸载就是删文件夹。

858ed8608f2d36ba57c3a6e835e673ef.gif

二、为什么会走到「PowerShell 内联 C#」这条路上

最早我是想做 Electron 的 —— 生态熟、写界面快。结果在目标机器上是这样的:

FATAL:platform_channel.cc(83) Check failed: .: 拒绝访问。 (0x5)

进程直接起不来。换了几种方式都一样,基本可以判定是被机器上的安全策略拦住了。这条路就废了。

那就得重新回答一个更基本的问题:我要给用户的到底是一个什么样的东西?

  1. 零依赖:不能让用户去装 .NET SDK、装 Node、装 Python。双击就该能用。
  2. 不联网:我不希望一个桌面摆件偷偷往外发数据,这也是它后来能上应用推荐站的原因之一。
  3. 不要构建步骤:这是给我自己定的。改一行代码要跑一遍编译链,我这种一天改几十次的人受不了。
  4. 单文件:源码就一个文件,扔到哪里都能跑。

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 这两个坑,再往里堆功能。 这条路最大的成本不在于写代码,而在于调试体验差 —— 所以别等它长到几千行才发现这点。

(本文的代码细节都来自实际项目,数字都是真机实测;如果有说得不对的地方,欢迎在评论区指出来。)