
简介Notepad v8.6.6 源代码压缩包以原生视窗接口编写未依赖跨平台框架是学习 Windows 桌面程序设计的理想样本。包内共两千个文件压缩后仅十一余兆核心由 C 与 C 头文件、实现文件构成同时包含近两百份 XML 配置、语法样式与折叠规则定义以及图标、脚本、工程文件和插件接口模块目录划分清晰便于按模块阅读。目前已有八百余人学习下载。通过这份源码可以弄清 Win32 消息循环如何驱动编辑器界面理解 Scintilla 文本组件的接入与扩展方式掌握多语言语法高亮、自动完成、代码折叠和插件管理机制的实现脉络。对于打算二次开发、定制编辑特性或参与开源维护的开发者这是一套完整且难得的参考工程。整体组织严谨注释与工程结构便于跟踪调试。1. Notepad v8.6.6源代码别只当“开源记事本”的源码看拿到 Notepad v8.6.6源代码大多数人第一反应是“这就是个记事本替代品的源码”但做过 Windows 桌面端开发的人会把它当成一份难得的完整工程它同时涉及 Scintilla 组件封装、插件 ABI 约定、Unicode 资源文件处理、绿色化交付这几条线。8.6.6 这版源码能编译、能改、能二次打包适合三类人想做私有插件的、想搞明白编辑器内核的、想给公司内部做定制版的。反直觉的结论是这套代码难的不是 C 语法而是工具链配合和资源文件编码这两块坑得多了编译成败全是细节。2. 从源码到 exe在 Windows 上把 Notepad v8.6.6 源码编译成可用版本Notepad 的源码管理用的是 Mercurialhg而不是 git官方仓库在 SourceForge 上。如果你只拿到代码快照压缩包目录里没有 .git 也正常。无论哪种方式最终你看到的顶层目录基本是 PowerEditor 和 scintilla 两大部分PowerEditor 是编辑器本体scintilla 是那个底层的编辑内核组件。这一步先别急着点开 sln 编译先把目录结构和依赖关系搞清楚后面能少踩一半坑。2.1 源码里到底有什么目录结构与三个必看的文件PowerEditor 目录下src 是主工程源码里面能看到 WinControls、MISC、NppIO、Scitilla 等几个子目录。这三个文件我建议最先打开PowerEditor/src/resource.h定义了所有资源 ID、插件通知码、版本宏。插件兼容性、菜单 ID、快捷键 ID 全在这里。PowerEditor/src/winmain.cpp入口文件从 WinMain 到消息循环的主链路。scintilla/include/Scintilla.hScintilla 对外暴露的消息常量SCI_*。编辑器所有编辑能力都通过这里的消息协议驱动。这三个文件能帮你把“Notepad 本体”和“Scintilla 内核”的边界划清楚。Notepad 本身不是一个编辑器内核它依赖 Scintilla 做文本渲染和编辑操作自己负责窗口框架、菜单、标签页、插件调度和文件管理。8.6.6 对应的 Scintilla 版本号在 scintilla/include/Scintilla.h 里有定义搜索 SCINTILLA_VERSION 就能看到。2.2 编译环境与依赖清单为什么不能只装 VS编译这套源码至少需要三样东西依赖用途常见版本选择Visual Studio编译主工程与资源带“使用 C 的桌面开发”工作负载即可社区版够用Windows SDK提供系统头文件与库与 VS 捆绑安装注意勾选较新的 SDK 版本Boost 库lexical_cast、filesystem 等依赖编译期依赖头文件个别功能需要编译好的库Boost 不是源码的一部分但要先准备好并让 VS 的包含目录指向它。更关键的是 SciLexer 库Notepad 运行目录里有一个 SciLexer.dll这就是由 scintilla 源码生成的独立产物。如果只打开 PowerEditor 的工程而忽略 scintilla你会在编译时遇到找不到 SciLexer.h 或 sciLexer.lib 的错误。常见做法是先编译 scintilla/win32 目录下的工程生成 SciLexer.dll 与 SciLexer.lib再回去编译 PowerEditor 主工程。2.3 完整构建步骤与命令行参数我习惯用命令行编译打包脚本也好写。这里以 VS2022 的开发者 PowerShell 为例源码解压到 D:\dev\npp-src# 1. 编译 Scintilla生成 SciLexer.dll 与 SciLexer.lib cd D:\dev\npp-src\scintilla\win32 nmake -f scintilla.mak # 2. 编译 PowerEditor 主工程 cd D:\dev\npp-src\PowerEditor nmake -f Makefile第一段命令会根据 scintilla.mak 里的默认目标在 win32/bin 下生成对应输出。如果你需要 Release/x64 版本可以在命令行追加 BUILD_DIR... 或目标名来切换具体目标名以你的源码包里 makefile 注释为准。第二段命令会去依赖目录里找 SciLexer 的产出找不到就报错并告诉你缺哪个文件这个报错信息是“瞎猜路径”最直接的提示。有两点要注意一是编译前必须确保当前命令行窗口是 VS 的开发者环境直接用系统 PowerShell 敲 nmake 会提示命令不存在二是如果是增量编译Scintilla 改动了头文件PowerEditor 基本会全量重编这是依赖关系决定的别怀疑自己改错了。2.4 首次启动前的初始化检查编译完成后PowerEditor/bin 下会出现 notepad.exe、SciLexer.dll、plugins 目录。先别急着双击用 dumpbin 查一下 exe 的依赖dumpbin /dependents D:\dev\npp-src\PowerEditor\bin\notepad.exe重点看输出里的 SciLexer.dll 是否在同一个目录、系统 DLL 是否齐全。接下来检查两件事一是 bin 目录下有没有 translations 文件夹缺了它菜单语言会回退成英文二是 SciLexer.dll 与你编译时使用的 lib 是否来自同一个源码提交。运行时如果出现“打开文件就闪退”先检查这两项比查代码快得多。3. 读源码的正确姿势启动流程、Scintilla 渲染与插件接口三条主线编译通了之后真正的价值在于读懂它。源码阅读最忌从头一个文件一个文件顺序看8.6.6 这种量级的工程建议沿着三条主线走进程启动链路、编辑渲染链路、插件通信链路。这三条线看懂你已经能应对绝大多数二次开发需求。3.1 winmain.cpp 到底做了什么从 winmain.cpp 的 WinMain 入口开始常见的执行顺序是初始化全局实例、加载配置文件、创建主窗口框架、创建 Scintilla 子窗口、注册消息循环。阅读时可以给关键函数做标记比如初始化函数里会调用 CreateWindow 创建主框架之后向框架发送消息创建各个子窗口。一个实用的读法在 WinMain 里打断点用调试器单步走一遍启动流程。你会发现启动顺序里有大量“消息驱动”的耦合比如菜单不是一次性建好的而是窗口创建后再通过消息动态填充。8.6.6 的多标签和文件夹工作区功能也是在主窗口创建后通过初始化 TabBar 和 FileManager 完成的。理解了这条启动链路以后自己加一个启动时初始化的功能才知道该把代码放在哪个阶段。3.2 Scintilla 封装与 EditView 的渲染路径语法着色、自动补全、行号栏这些能力不是 Notepad 自己现写的而是通过向 Scintilla 发送 SCI_* 消息实现的。Notepad 把对 Scintilla 的封装主要放在 src/Scitilla 子目录核心类包括 EditView 等。阅读时可以找一处设置字体或颜色的代码作为切入点// 给 Scintilla 设置默认样式字体Windows 下 Consolas 是常见等宽字 sciView-SendMsg(SCI_STYLESETFONT, STYLE_DEFAULT, (LPARAM)_T(Consolas)); // 设置字号注意 Scintilla 以 1/72 磅为单位不是像素 sciView-SendMsg(SCI_STYLESETSIZE, STYLE_DEFAULT, 12 * 72);第一行把 STYLE_DEFAULT 样式的字体设为 Consolas第二行设置字号Scintilla 内部字号单位是磅的 1/72所以 12pt 要写成 12 * 72。如果直接传 12显示出来的字会非常小这是新手常踩的数字陷阱。另外STYLE_DEFAULT 只是基础样式注释、字符串、关键字都有各自的 style 编号只设默认样式会让界面配色看着很违和所以读代码时最好连着看一组 StyleSetFont、StyleSetFore、StyleSetBack 的调用理解“样式族”的概念。3.3 插件接口结构从 NppData 到 DLL 的 ABI 约定想写 Notepad 插件核心是理解 NppData 与通知消息的约定。插件 DLL 要暴露一个固定的导出函数 setInfo(NppData)NppData 结构体里包含三个关键句柄nppHandle主窗口句柄、scintillaMainHandle 和 scintillaSecondHandle主次两个编辑视图的句柄。插件运行时就是靠这三个句柄与编辑器通信。通信消息分两类发给主窗口的 NPPM_* 消息和直接发给 Scintilla 的 SCI_* 消息。NPPM_* 常量值全部在 resource.h 中定义SCI_* 常量在 Scintilla.h 中定义。用惯了插件系统的人会发现Python Script 这类插件本质上也是按这套 ABI 绑定进 Notepad 进程的并不是什么外置脚本工具所以你在 Python 回调里同样会拿到 NppData 和消息 ID。了解这套结构之后插件升级不兼容的问题也能自己排查了通知码编号变了和 C ABI 无关基本都是常量值对不上。4. 改源码的第一步调整版本号、快捷键与便携化并重新打包读完源码就该动手改了。第一次改代码别上来就动核心渲染逻辑从版本号、标题栏、快捷键这类外围功能切入既能熟悉工程结构又能快速得到一个看得见的成果。这一章按最常见的定制需求走一遍改版本号、改快捷键、重新编译、打成绿色版。4.1 改版本号与标题栏字符串资源的埋点版本号通常集中在 resource.h 或版本资源文件里搜索 NPP_VERSION 或 VS_VERSION_INFO 就能找到。很多人改完发现标题栏没变因为版本信息散落在多处窗口标题、关于对话框、版本宏、安装包信息各自都有副本。我一般只改宏定义让编译时统一带过去避免手动改字符串漏项。# 在 PowerEditor/src/resource.h 中统一修改版本宏 # 例如把 8.6.6 改成 8.6.7同时更新 FILEVERSION 与 PRODUCTVERSION 两个字段改完后重新编译注意版本宏是全局共享的改一处通常会触发大量文件重编这是正常的别以为工程坏了。验证时用 dumpbin /version 看 exe 的文件版本再打开“关于”对话框看产品版本两处一致就说明改到位了。4.2 修改快捷键accelerator 资源的陷阱快捷键映射在 .rc 文件里的 ACCELERATOR 段落比如把“替换”从 CtrlH 改成 CtrlR直接改资源表就行。但要注意两件事一是快捷键冲突CtrlR 如果已被“运行”占用改动后两个菜单项会打架二是插件命令的 ID 冲突。插件注册的命令 ID 是从某个高位起始动态分配的你在静态资源表里写那个 ID 没有意义因为静态资源在插件加载之前就解析完了。# 在 .rc 文件的 ACCELERATOR 段中修改键位 # 例如 R, IDM_REPLACE, CONTROL, VIRTKEY改完资源文件编译时注意 .rc 改动经常触发整个资源重编耗时比想象中长这是正常现象。验证时打开“快捷键设置”对话框按新的组合键看是否生效同时试一下原快捷键是否已被释放。4.3 重新编译与打包生成自己的绿色版改动完成后用第 2 章的编译命令重新生成。打包时把 PowerEditor/bin 下的关键文件整理到一个新目录notepad.exe、SciLexer.dll、plugins 文件夹、translations 文件夹、langs.xml、stylers.xml。这个目录结构就是网上常见的 “notepad zip 绿色版” 形态。绿色版的本质是不写注册表、不碰 HKCU 配置。第一次启动时Notepad 会询问配置存放方式选择“跟随程序目录”后程序会在可执行文件同级目录创建 config.xml把会话、主题、快捷键设置全部存到本地而不是写入 %APPDATA%。如果用户直接拷贝整个目录到另一台机器配置也跟着走这就是绿色版能便携的原因。4.4 便携化的四个原则DLL 邻放、纯英文路径、坑位固定做绿色定制版时有四条经验SciLexer.dll 必须和 notepad.exe 同级目录放错位置程序会闪退整个目录路径不能有中文或空格否则部分插件在读取配置文件时会找不到路径本地化文件必须放在 translations插件 DLL 放 plugins插件配置尽量放 plugins/config目录结构一乱程序会静默回退到默认配置打包用 zip 而不用自解压 exe因为有些安全软件会拦截自解压释放 DLL 的过程。打包前跑一遍“复制到新目录 - 双击启动 - 打开一个 UTF-8 文件 - 关闭再启动”的完整流程确认配置写入的是程序目录而不是 AppData再交付出去。5. 避坑指南编译、调试与插件兼容的 5 个常见坑源码编译与定制我前后折腾过不少次这里把最容易让人翻车的 5 个问题按“现象 - 原因 - 解决”写清楚每一条都来自实际操作中的血泪经验。5.1 链接阶段报 LNK2019平台工具集和字符集不匹配现象编译到链接阶段报 LNK2019 unresolved external symbol错误信息里能看到 boost 或 SciLexer 相关符号无法解析。原因工程配置的字符集或平台工具集和你实际使用的库不一致。最常见的是把 VS2019 的工具集 v142 升到 VS2022 的 v143但 SciLexer.lib 还是用旧工具集编译的另一种情况是工程用的 Unicode 字符集却链接了以 MBCS 方式编译的第三方库。解决统一用官方默认配置字符集选 Unicode工具集选择与 SDK 匹配的版本编译 SciLexer 与编译主工程时平台工具集必须完全一致混用必炸。5.2 编译后菜单或界面文字空白现象程序能启动但菜单、对话框里大面积空白或显示乱码中文区域尤其严重。原因.rc 资源文件如果被存成了无 BOM 的 UTF-8Windows 资源编译器会按系统代码页比如 GBK去解析解析失败后资源直接丢失菜单就空了。解决把 .rc 和语言文件统一转成 UTF-16 LE 或带 BOM 的 UTF-8再重新编译。批量转换时注意别把代码里的字符串常量也一起转了只处理资源文件。5.3 剪贴板复制出来的是乱码现象从自己编译的版本里复制中文粘贴到其他应用变成乱码英文正常。原因Scintilla 内部拿到的文本是 UTF-8Windows 剪贴板需要的是 UnicodeCF_UNICODETEXT。Notepad 内部有转换层但如果你在自定义渲染或复制路径里直接调用了 SCI_GETTEXT跳过了转换层就会把 UTF-8 原样塞进剪贴板。解决所有取文本操作统一走内部封装函数先转成宽字符再走 SetClipboardData(CF_UNICODETEXT)。不要在自绘或右键菜单里直接调 SCI_GETTEXT 原始接口。5.4 升级版本后插件全部失效现象从旧版本升级到 8.6.6 定制版第三方插件菜单项全部消失或者插件弹窗报消息 ID 错误。原因插件 DLL 里写死的 NPPM_* 通知码和 resource.h 里定义的常量值对不上。旧版本和新版本如果重排了资源 ID老插件按旧值发消息新主程序根本识别不了。解决二次开发时不要动 resource.h 中已有的 NPPM_* 宏定义保持和官方一致。如果确实要新增自己的通知码从最后一个合法 ID 之后的位置往后加不要从中间插入重排。5.5 源码目录带中文或空格导致 nmake 报错现象hg 或 git 拉取正常但 nmake 阶段报错找不到路径或文件错误信息里的路径被截断。原因VC 工具链对路径里的非 ASCII 字符和空格支持不稳定某些老牌 makefile 在拼接路径时没有处理空格。解决源码根目录统一用纯英文、无空格的路径比如 D:\dev\npp-src。这一条能躲掉大量莫名其妙的路径问题不是玄学是工具链的已知短板。6. 进阶用法用自动化回归与性能验证给源码改动兜底改动源码最怕的是“改完能跑但不知道哪里悄悄坏了”。我习惯在每次定制后跑一遍自动化回归至少覆盖三个用例文件编码识别、命令行参数解析、插件加载。PowerEditor 源码里带有测试工程编译后可以直接跑测试目标哪怕你没写新测试已有的用例也能快速暴露资源或配置方面的问题。性能验证我一般用一段 PowerShell 脚本批量打开样本文件记录启动耗时和内存占用# 用 200 个文本文件验证打开速度与内存增长 $files Get-ChildItem -Path D:\samples\*.txt $sw [System.Diagnostics.Stopwatch]::StartNew() foreach ($file in $files) { Start-Process -FilePath D:\dev\npp-src\PowerEditor\bin\notepad.exe -ArgumentList $($file.FullName) } Start-Sleep -Seconds 3 $npp Get-Process -Name notepad | Select-Object -First 1 $sw.Stop() Write-Host 耗时: $($sw.ElapsedMilliseconds) ms, 内存: $($npp.WorkingSet64) bytes这段脚本的逻辑很简单批量启动多个编辑器进程打开样本文件记录整体耗时和首个进程的内存占用。参数说明样本文件的数量要固定每次改动前后跑同一批文件对比数字才有意义内存值不要对比不同机器只看同一台机器上的变化趋势。如果某次改动让内存增长超过 15%就要回头检查是不是消息循环里新增了重复创建对象的代码。最后一个习惯任何改动都不要只在本地改完就交付我会用源代码管理工具对比改动前后的差异。Notepad 用的是 Mercurial所以对比用 hg diff 和 hg annotate 定位具体改动行如果你自己的工程迁移到了 git对应的就是 git diff 和 git blame。这个习惯帮我抓到过不少“删了一行看似无关代码却连带破坏了插件初始化顺序”的隐蔽问题。改源码是个细活编译通过只是起点跑一遍回归、看一眼性能数据、审一遍差异才敢说这个版本能交付希望帮到你。本文还有配套的精品资源点击获取