一、为什么要自己写一个
我之前一直用 Windows 自带的便笺,记点东西够用,但它没有截屏。而我日常的活儿是「截一块屏幕 → 圈两下 → 顺手记到便签上」,这几步得在三四个工具之间来回跳,很烦。
前几年因为业务需要学了点 C#,勉强算初步掌握了开发。正好赶上这一波 AI,它成了我很大的一块助力------功能怎么收口、界面细节怎么排、连环 bug 怎么定位,方向是我拿的,但落地速度快了不止一倍,尤其是那些我自己写不利索的窗体绘制和 DPI 适配。
前后折腾下来,便签、截图、标注、文字识别、待办提醒这几件事被我合进了同一个免安装的小程序,叫 小羊便签 KimNotes(仓库名 KimNotes,图标是一只小羊)。现在是我自己每天开着的工具,版本走到 1.1.11,索性完整开源出来。
二、它现在能干什么

- 便签:无边框卡片窗体,边缘可拖拽拉伸,📌 能固定在桌面最上层。底部工具栏一键做项目符号、加粗、大小写、翻译、新建、截屏、转待办。
- 截图 :全局热键(默认
Ctrl + F1,设置页可换键)全屏框选,松手就是一张置顶悬浮图,支持滚轮缩放、拖动、右键菜单(复制 / 标注 / OCR / 另存 / 拾色 / 重置 / 销毁)。

- 标注 :矩形 / 箭头 / 马赛克 / 文字四种画笔,工具条就在截图正下方同一个窗体外框里 ,
Ctrl+Z撤销,Esc结束并把标注烘进图片。

- OCR 与翻译 :截图右键 OCR,识别结果新建一张便签写进去并复制到剪贴板;便签正文划选可中英互译。
- 待办与提醒 :待办独立存
todos.json,不污染便签正文。块状卡片按提醒时间排序、过期标红;单击整块 = 完成或恢复,双击 = 改内容和提醒时间。设了时间的到点弹置顶提醒小窗,响不响在设置里选(默认仅弹窗)。

- 主题:8 套配色,便签 / 列表 / 待办 / 设置 / 提醒窗共用一套视觉,切换实时应用到所有已开窗口。
- 数据全在本地 :都在
%AppData%\KimNotes\,没有账号、没有云同步、没有后台上报,另有无痕模式,开启后完全不落盘。 - 托盘常驻(1.1.11):关掉所有窗口只是缩回托盘,进程不退出;托盘右键是 新建便签 / 便签列表 / 待办清单 / 设置 / 退出。
三、下载与运行
| 项目 | 值 |
|---|---|
| 版本 | 1.1.11 |
| 包 | KimNotes-v1.1.11.zip,376KB,3 个文件 |
| 文件 | KimNotes.exe(250KB) + KimNotes.exe.config + Newtonsoft.Json.dll |
| 系统 | Windows 10 / 11,需 .NET Framework 4.8(一般系统自带) |
| 许可 | Apache-2.0 |
解压到任意目录,双击 KimNotes.exe。同目录那个 .dll 是运行时依赖,不能只把 exe 单独拿走。数据都在 %AppData%\KimNotes\,删掉该目录就回全新状态。
- 仓库:github.com/kimlonger/KimNotes / 国内镜像 gitee.com/xiaoyangkim/KimNotes
- 官网:note.kimlulu.com
- 下载:Releases latest
四、实现细节(这一节是重点)
技术栈:C# / .NET Framework 4.8 / WinForms,旧式 csproj + packages.config,第三方依赖只有一个 Newtonsoft.Json 13.0.3。
4.1 2026 年为什么还用 WinForms
因为我要的「小」和「快」只有它能给:解压即用、单 exe、双击就出窗口、不需要 Runtime / 不用打包一个几百 MB 的浏览器内核。原生 TextBox 的多行编辑、剪贴板、键盘输入这些细节,自己拿别的方案重写一遍代价不小。代价是得自己处理无边框、DPI、阴影这些系统级的事------这也是下面几个坑的来源。
4.2 无边框卡片要有投影和 Win11 圆角
FormBorderStyle = None 之后窗体又平又硬,完全没有「便笺卡片」的感觉。两行扩展样式解决:
csharp
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED:双缓冲合成,降低拖动/刷新闪烁
cp.ClassStyle |= 0x00020000; // CS_DROPSHADOW:无边框下保留系统柔和投影
return cp;
}
}
Win11 的圆角不用自己画,走 DWM 属性(33 = DWMWA_WINDOW_CORNER_PREFERENCE,2 = DWMWCP_ROUND):
csharp
try { int pref = 2; DwmSetWindowAttribute(this.Handle, 33, ref pref, 4); } catch { }
catch 是故意的------Win10 上这个属性不存在,失败就退化成直角,不影响功能。
4.3 无边框窗体怎么拉伸
没有边框就没有拉伸热区,得自己接管 WM_NCHITTEST,把靠边的点报成系统的拉伸码(10/11/12/15 是左右上下,13/14/16/17 是四个角),系统就自动给你对应的拉伸光标和行为:
csharp
if (m.Msg == 0x84) // WM_NCHITTEST
{
int lp = m.LParam.ToInt32();
var pt = PointToClient(new Point(unchecked((short)(lp & 0xFFFF)), unchecked((short)((lp >> 16) & 0xFFFF))));
int b = Math.Max(Padding.Left, 4); // 命中环宽度,跟 DPI 一起缩放
bool l = pt.X <= b, r = pt.X >= ClientSize.Width - b;
bool t = pt.Y <= b, bo = pt.Y >= ClientSize.Height - b;
if (l && t) m.Result = (IntPtr)13;
else if (r) m.Result = (IntPtr)11;
else if (bo) m.Result = (IntPtr)15;
// ...其余组合同理
}
配套的画法是:标题栏高度内的左/右/上命中环涂成标题栏同色,让它白到底边到顶;标题栏以下的命中环是便签底色,跟内容同色,肉眼看不出那 4 像素。
4.4 多屏混缩放的 DPI
DeviceDpi / 系统 DPI 在「主屏 150% + 副屏 100%」这种组合下会把窗口在小屏上撑爆、在大屏上裁字。必须按窗口自己的句柄量:
csharp
public static float FactorFor(IntPtr hwnd)
{
uint dpi = 0;
try { dpi = Win32ApiHelper.GetDpiForWindow(hwnd); } catch { } // Win10 1607 以前没这个导出
return dpi == 0 ? Factor : dpi / 96f;
}
项目里所有尺寸------工具栏按钮、标题栏、标注工具条、下拉面板圆角------都从这个函数拿到的 S 乘出来,所以截图标注那条工具条在 150% 屏上是 34px,在 100% 屏上是 23px,都能刚好放下。
4.5 图标不引第三方图标库
工具栏那些图标全是代码画的:SVG 定义几何 → 预渲染成 PNG → 作为嵌入资源打进 exe,运行时 GetManifestResourceStream 读出来,再由自绘的 ToolIconButton 画上去(悬停换色,不需要第二套图)。这样图标跟着主题色走,也不用背一个几十 MB 的图标包。
4.6 托盘菜单:一个无边框置顶窗怎么知道"你点到外面了"
1.1.11 把托盘菜单从系统右键菜单换成了跟便签同族的无边框圆角卡片。第一版用 Deactivate 事件关窗,结果菜单经常关不掉------无边框 + TopMost 的卡片压根拿不到可靠的失焦事件(它常常根本没被真正激活过)。最后挂低级鼠标钩子,跨进程看到全局鼠标按下,落在卡片矩形外就关:
csharp
private const int WH_MOUSE_LL = 14;
hookId = SetWindowsHookEx(WH_MOUSE_LL, hookProc, GetModuleHandle(null), 0);
// OnFormClosing 里 UnhookWindowsHookEx,钩子不能漏
顺带一个坑:圆角本来想用 TransparencyKey 抠掉背景色,结果 GDI+ 抗锯齿的过渡色不会被抠掉,圆角边上留一圈彩色杂边。改成用 Region = new Region(GraphicsPath) 直接裁窗体,边就干净了。
4.7 「提醒到点不响」找了很久,根因在注册表
现象很迷惑:提醒逻辑明明触发了(去重文件里有记录、置顶小窗也弹了),就是没声音。
原因在 SystemSounds.Exclamation.Play()------它要在注册表里找 Windows 7 时代的声音事件名 Notification Exclamation,而 Win10/11 的事件名早换成了 SystemExclamation、Notification.Default 这套写法,很多机器上老事件根本没配图录 。于是 Play() 调用成功、什么都不响,一点异常都没有。
解法是不碰系统声音方案:自己合成一段 1.65 秒双音 wav(16kHz/16bit 单声道,约 52KB)作为嵌入资源打进 exe,用 SoundPlayer 直接播,只有资源读不到才回落 SystemSounds。响不响还做成了设置项(config.txt 的 remindMode,仅弹窗 / 弹窗+铃声),老配置没这个键按「仅弹窗」处理,升级后不会突然响起来吓用户一跳。
4.8 一个很蠢的 bug:保存配置会把主题弄丢
设置页的保存逻辑原先自己 new StreamWriter 整文件重写,但它只写自己认识的那 6 个键。于是程序其他地方写进去的 theme 就没了------点一次「保存」,主题回到默认浅蓝。
修法是让 config.txt 只有一个写出口:读的时候解析成字典,写的时候 InitConfig.SetConfigValues(字典) 整表写回。以后新增配置键不会再被谁悄悄吃掉。
4.9 依赖收敛:13 个文件 → 3 个文件
原先为了在 net48 上用 System.Text.Json,连带塞了 9 个 BCL 垫片 dll,解压包 13 个文件、840KB。查下来 System.Text.Json 只在一处解析翻译接口的返回,改成 JObject 之后整个删掉,包变成 3 个文件、376KB,bindingRedirect 那堆失效配置也一起清了。
五、已知问题
- OCR 和翻译走百度开放平台,
utils/Translator.cs、utils/RemoteCallUtils.cs里是我自己的明文 appid / secret,二次开发请换成自己的凭证。 - 云同步和在线更新还在开发中,界面上没做入口;「检查更新」指向自建后端,接口没就绪前不会自动弹更新提示。
- 托盘常驻是 1.1.11 刚加的,日常反馈还不全。Windows 11 上新出现的托盘图标默认缩在任务栏
^溢出区,要拖出来才常驻眼前。 - 曾试着给托盘单独做一帧 16px 加粗版小羊图标(32px 细线稿硬缩到托盘后线不到 1px),实机看效果反而不如原画,已经回退------托盘仍是原始小羊图标。
六、最后
第一次把这个项目完整发到技术站上。哪儿觉得别扭、或者你想要但我没做的功能,直接回帖或者开 issue,我天天看。代码写得并不漂亮,WinForms 的老式写法也还在,但每个坑都留在 commit message 里了,愿意翻。
说明:本文由我自己起草功能与踩坑内容,文字经 AI 润色;代码片段摘自项目仓库,全部实测过。
要是这篇对你有帮助,点赞、收藏、评论 一键三连支持一下------一个人加 AI 磨出来的小东西,这点反馈就是我继续往下更的动力。