ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Notepad++源码解析与二次开发实践:从编译到插件编写

Notepad++源码解析与二次开发实践:从编译到插件编写 简介Notepad v8.6.6 完整源代码打包整理面向想深入 Windows 原生编辑器实现原理的中高级开发者可用于学习 Win32 消息机制、熟悉 Scintilla 文本控件、做编辑器定制或二次开发。压缩包共 2000 个文件、约 11.48MB。包含 cpp、hpp、h、cxx 等 C/C 源文件形成主程序与功能逻辑xml 配置菜单、工具栏及快捷键rc 管理对话框和版本信息styled、folded 定义各语言语法高亮与折叠规则ico、bmp、png 为界面图标素材另附 makefile、vcxproj、Python 脚本等构建工具链目录结构完整便于按模块检索。目前已有 877 人学习下载。整包可对照设计分析逐模块读代码从 Win32 程序入口与消息循环到 Scintilla 接口集成与事件扩展再到插件管理器的加载通信机制能直观理解语法高亮、多文档、自动完成等功能的落地方式配合工程文件可本地编译调试也可自行裁剪或新增语言支持是学习成熟开源桌面软件架构的实用资料。 Notepad 用了十几年几乎每天都会打开但真正把 v8.6.6 的源代码完整拉下来、编译过、甚至改过几行再跑起来是最近才做的事。以前它对我来说就是个比记事本强很多的编辑器这次源码摆在眼前反倒让我第一次认真思考一个看着很轻量的编辑器底层到底是怎么组织的。扒完代码之后最大的感受是Notepad 不是一个简单的小工具而是一个很完整的 C 桌面应用工程核心逻辑分了两大块——PowerEditor 主程序和 Scintilla 编辑内核加上一堆配套模块整体复杂度远超预期。这篇文章不打算搞“逐行读源码”那种教学而是从实际操作出发把源码从哪获取、目录结构怎么看、怎么编译成 exe、怎么改功能、怎么写一个 hex-editor 插件、以及源码管理和保护这些话题一次性捋清楚。适合想把 Notepad 当学习模板的 C 开发者或者想基于它做二次开发、写插件的朋友。看完之后你应该能自己动手把这份源码玩起来。1. 源代码获取与工程结构解读1.1 源码从哪拿、选哪个版本源码在 GitHub 上仓库名是 notepad-plus-plus/notepad-plus-plus找 v8.6.6 这个 tag 直接下载 zip 或者 git clone 都可以。我建议用 git clone 而不是下载压缩包因为后面要对比版本差异、回退改动有 git 历史会方便很多。有人会问为什么不直接拉 master 分支因为 master 是开发分支代码可能在某个中间状态今天能编译明天可能就有接口变动不适合拿来当稳定工程研究。v8.6.6 是已经发布的正式版本代码质量经过一轮回归测试用这个版本来学习和定制都更省心。版本号本身也有信息量8 是主版本86 是功能迭代6 是修订。v8.6.6 在功能上属于修复型版本改动不大但对于看源码来说恰恰因为改动少代码结构更“安静”适合做基线。1.2 源码目录结构一次看懂克隆下来之后仓库根目录里并不是一堆散落的 cpp 文件而是分了几大块。我第一次打开时有点懵后来理清楚了PowerEditor主程序主体Notepad 的窗口、菜单、快捷键、设置管理、文件标签全在这。Scintilla编辑器内核负责文本渲染、光标、选区、滚动、自动换行这些底层编辑行为。Lexilla词法分析库各种编程语言的高亮、折叠、自动补全都依赖它。boost官方仓库里带了一份 Boost 库节省了手动下载第三方依赖的麻烦。npp-event、nl-eol 等小模块处理插件事件、换行符等辅助逻辑。为什么要拆成这么多模块因为 Scintilla 和 Lexilla 是独立项目Notepad 只是它的一个宿主这种设计保证了编辑内核可以被其他项目复用。看源码时建议先逛 PowerEditor/src再钻进 Scintilla不要一上来就去啃编辑内核容易陷进细节出不来。1.3 v8.6.6 相比旧版的结构变化如果以前看过 v7.x 的源码会发现 v8.x 在工程结构上做了一些调整最明显的是插件接口和构建系统更统一了。v7 时代很多插件作者是拿老接口硬写到 v8 之后官方把插件接口抽象得更干净对外暴露的头文件也集中在 PowerEditor/src/plugininterface 下。这个变化对我们要写插件的人来说是好事后面第 5 节会具体说。另外v8.6.6 的代码注释比旧版少了一些但函数划分非常清晰命名也很直白比如Notepad_plus::doMenuCommand、ScintillaView::execute这种看函数名就能猜出大半逻辑。这也是我推荐大家直接读源码而不是只看文档的原因——这种命名风格让你很容易顺着调用链往下摸。2. 核心模块解析一个编辑器是怎么跑起来的2.1 入口点WinMain 之后发生了什么Windows 桌面程序的入口是WinMainNotepad 也不例外。在 PowerEditor/src/WinMain.cpp 里可以看到整个程序的生命周期先初始化公共控件、解析命令行参数然后构建一个NppParameters对象来加载配置接着创建主窗口最后进入消息循环。有意思的是主窗口的创建并不复杂复杂的是消息循环里对WM_COMMAND、WM_NOTIFY、WM_SIZE等消息的分散处理。Notepad 把不同消息分发给Notepad_plus类里的不同成员函数这也是 Win32 程序常见的写法。如果你想理解“Windows 程序是怎么响应用户操作的”顺着WndProc看一遍就能摸到门路。之前我写过几个 Win32 小工具看到这里的消息分发逻辑顿时觉得熟悉又亲切。2.2 编辑内核 Scintilla 到底做了什么Scintilla 是一个独立的编辑控件最初是为 SciTE 编辑器写的后来被很多项目采用。Notepad 只是它最出名的宿主之一。文本编辑的核心能力比如光标闪烁、键盘输入、高亮当前行、自动缩进、断行全都在 Scintilla 内部完成。你可能会问Notepad 自己干什么答案是它只负责“编辑之外”的事。比如用户点了菜单“查找”Notepad 用 Scintilla 提供的SCI_FINDTEXT接口去搜索用户切换语法语言Notepad 加载对应的 Lexilla 语言库然后调用SCI_SETLEXER告诉 Scintilla 用哪个词法分析器。可以简单理解为Scintilla 是发动机Notepad 是驾驶舱。看源码时如果陷入 Scintilla 的代码里出不来记住这个分工就足够了。2.3 菜单命令与插件接口的联动方式菜单是 Win32 资源的典型用法。在 PowerEditor/src/resource.h 里定义了所有菜单项的 ID而 resource.rc 里描述了菜单布局。菜单点击后Windows 向主窗口发送WM_COMMAND消息Notepad_plus类根据LOWORD(wParam)判断是哪个菜单 ID再交给对应的处理函数。插件接口也搭在这条链路上。Notepad 通过插件管理器加载 plugins 目录下的 DLL每个 DLL 需要导出getName、setInfo、beNotified等函数。主程序通过NppData结构把主窗口句柄、Scintilla 句柄传给插件插件就能直接操控编辑器。这套设计思路很简洁主程序不依赖具体插件插件只需要实现约定好的几个导出函数剩下的自由发挥。2.4 文件读写与编码检测文件读写这块很容易被忽略但对一个国际化文本编辑器来说极其重要。Notepad 处理文件时先判断 BOMByte Order Mark再尝试用 UTF-8、UTF-16、ANSI 等编码解码。v8.6.6 的代码里对无 BOM 的 UTF-8 会做启发式检测防止中文和西文文本乱码。这里有一个真实的坑如果文件被识别成 ANSI但实际是 UTF-8 且没有 BOM打开后全是乱码。Notepad 提供“编码”菜单里的“编码字符集”来手动修正这个逻辑在FileManager::loadFile里能跟踪到。看这段代码能学到很多文本编码理论比看 RFC 文档要直观得多。3. 本地编译把源码变成能跑的 exe3.1 编译环境准备与依赖说明编译 Notepad 不需要 Linux就在 Windows 环境里用 Visual Studio。v8.6.6 源码推荐用 VS2022如果使用 VS2019可能需要改平台工具集但也能编过。安装时务必勾选“使用 C 的桌面开发”工作负载否则没有 Windows SDK 和 MSVC 编译器。第三方依赖其实不用太操心官方仓库自带 boostScintilla 和 Lexilla 也在仓库内。但有些版本需要额外下载 GnuPG 相关的库主要用于签名的功能如果不需要可以跳过或关闭相关宏。编译前看下仓库根目录的 README里面有官方给出的环境要求和依赖清单按着做就行。我刚开始编译时贪快没看 README结果在缺少头文件上卡了半小时。3.2 编译步骤启动到生成 exe编译流程一共三步打开工程、选配置、生成。先进入PowerEditor/visualstudio目录打开NotepadPlusPlus.sln。解决方案里通常有 Notepad、Scintilla、Lexilla 等工程。建议选择Releasex64除非你要做 32 位插件测试否则没必要用 x86。然后右键解决方案“重新生成”。第一次编译会比较慢大概几分钟到十几分钟取决于机器性能。编译成功后在PowerEditor/bin目录下会生成notepad.exe。但此时直接双击运行可能会报缺少 DLL因为 Scintilla 和 Lexilla 的动态库不一定被复制到 bin 目录。最省事的办法是把SciLexer.dll和lexilla.dll手动拷到 exe 同目录或者检查项目设置的“后期生成事件”里有没有复制库文件。3.3 常见编译错误与对应解法我编译时遇到的第一个错误是找不到 boost 头文件原因很简单Boost 目录虽然在仓库里但 VS 的附加包含目录没配好。解决方法是打开项目属性在“C/C → 常规 → 附加包含目录”里添加 boost 的根目录。另一个高频错误是 LNK2019 无法解析的外部符号。这个多半是库目录没配置或者编译顺序不对导致链接时找不到 SciLexer 的导入库。把解决方案里所有需要引用的项目都编译一遍再回去链接通常能解决。还有一个容易忽略的问题Release 编译默认启用优化调试时单步会跳来跳去断点可能不命中。如果打算跟代码建议用Debug配置编译或者把优化关掉。3.4 做一个绿色便携版如果你只是想把自编译的 exe 拿给同事用可以在 exe 同目录下新建一个portable文件夹。Notepad 检测到这个目录后会把配置和插件数据写到里面而不是注册表或 AppData。这样就把一套“绿色版”做好了拷贝整个目录就能带走很适合做定制分发。4. 源码级定制自己动手改一个功能4.1 怎么定位要改的代码定制 Notepad 的第一步是知道“功能对应的代码在哪”。我的习惯是从菜单资源入手。比如想把“复制文件路径”加到右键菜单的某个位置先在resource.h里搜“COPY”相关的资源 ID然后在resource.rc里找到菜单定义确认菜单 ID 唯一最后在Notepad_plus.cpp或WinMenu.cpp里搜索这个 ID 对应的case分支。Visual Studio 的“查找所有引用”功能在这里非常好用直接把资源 ID 的所有引用列出来。定位代码的另一个技巧是在“运行”窗口输NPP_或者IDM_前缀记忆法。Notepad 的菜单 ID 都以IDM_开头紧跟功能名比如IDM_FILE_OPEN、IDM_EDIT_CUT。按规范把 IDM 前缀 功能名粘到搜索框基本一找一准。4.2 实际改一个给右键菜单加“复制文件路径”我拿这个功能举例子因为它改动小、效果直观。在resource.h里找一个空余的 ID 定义大概是这种写法#define IDM_FILE_COPYFULLPATH 51046然后在resource.rc的右键菜单一般是IDR_POPUP_EDIT里加一行菜单项比如POPUP Current File BEGIN MENUITEM Copy Full Path, IDM_FILE_COPYFULLPATH END最后在Notepad_plus.cpp的消息处理里加一个分支case IDM_FILE_COPYFULLPATH: { const TCHAR* path _pPublicInterface-getCurrentFilePath(); if (path) ScintillaView::putClipboard(path); break; }这里putClipboard是 Scintilla 的接口并不是所有版本都有同名函数实际调用时需要看接口定义但逻辑就是这么个逻辑。改完重新编译右键就能看到新菜单项。4.3 调试和验证修改用 VS 打开解决方案后把 Notepad 工程设为启动项目按 F5 就能进入调试。在刚才的case分支里断点鼠标右键菜单点一下断点就会命中。这个过程能很直观地看到菜单命令从 UI 到处理函数再到剪贴板操作的全链路。有一点要提醒Notepad 遵循 GPL 协议如果你基于源码定制后要分发给别人必须把修改后的源码开源。如果你只是内部自己用不对外发布那没问题。但不要搞“改了代码却闭源”的操作这是合规红线。5. 以 hex-editor 插件为例给 Notepad 扩展能力5.1 插件开发的接口基础写 Notepad 插件本质是做一个 DLL并导出几个固定函数。核心接口定义在PowerEditor/src/plugininterface.h里面能看到NppData、FuncItem这些结构体。一个最小插件至少要导出这些函数isUnicode()告诉主程序插件是否使用 UnicodegetName()返回插件名称getFuncsArray()返回菜单项数组setInfo()接收主程序传进来的 NppDatabeNotified()接收编辑器事件通知。插件被加载时主程序会调用setInfo把主窗口和 Scintilla 的句柄交给插件插件之后的菜单点击事件通过messageProc或beNotified处理。这块接口虽然多但都很简单。5.2 用 C 编写一个最小可用插件在 VS 里新建一个“动态链接库 (DLL)”项目然后把插件头文件路径加进来写一个简单的入口#include plugininterface.h NppData nppData; BOOL APIENTRY DllMain(HMODULE, DWORD, LPVOID) { return TRUE; } void setInfo(NppData data) { nppData data; } const TCHAR* getName() { return TEXT(MyFirstPlugin); } void myAction() { ::SendMessage(nppData._nppHandle, WM_COMMAND, IDM_FILE_OPEN, 0); } FuncItem funcItems[] { { TEXT(Open File), myAction, NULL }, }; FuncItem* getFuncsArray(int* count) { *count 1; return funcItems; } LRESULT messageProc(UINT, WPARAM, LPARAM) { return TRUE; } void beNotified(SCNotification*) { }编译生成 DLL 后放到 Notepad 的 plugins 目录重启 Notepad就能在“插件”菜单里看到MyFirstPlugin。这个最小插件虽然啥正经功能都没有但已经打通了主程序到插件的整条链路。5.3 接入十六进制查看能力思路和坑真正要做 hex-editor不能只靠上面这个空壳。二进制编辑和纯文本编辑的逻辑完全不同不可能指望 Scintilla 直接支持。常见做法是插件创建一个独立窗口或者子窗口读取文件字节流用ListView或自绘控件显示十六进制和 ASCII 列。网上有现成的 HexEditor 开源插件如果不想从零写可以直接参考它的源码。它里面的核心是读文件到缓冲区然后根据当前滚动位置计算应该显示哪些字节。如果要支持编辑还需要维护撤销栈、处理光标映射工作量瞬间翻好几倍。所以我建议如果只是偶尔看二进制文件先做只读模式等确实需要编辑再考虑完整实现。这里有个实际坑如果你的插件是 x64 编译但 Notepad 是 x86 版本DLL 会被拒绝加载。反过来也一样。所以插件架构必须和主程序一致共享相同位数。我在第一次测试插件时就是吃了这个亏换成了 x64 后一切正常。5.4 插件调试与分发体验插件调试最推荐的方式是“编译后在 Visual Studio 里附加到进程”。先启动 Notepad然后在 VS 里选择“调试 → 附加到进程”选 notepad.exe再在插件代码里打断点。加载时会不会进断点能直观反应 DLL 有没有被加载成功。分发插件只需要把 DLL 和一个说明文档打包不需要安装程序。用户把 DLL 放进 plugins 目录就能用。如果插件依赖第三方 DLL记得一起带上并注意位数一致。这个小生态特别友好这也是 Notepad 插件数量多的原因之一。6. 源代码管理、加密与反编译绕不开的现实问题6.1 用 Git 管理这份源码把 Notepad 源码 clone 到本地后不要直接在主分支上乱改。我的做法是先建立自己的工作分支比如my-v8.6.6-custom然后基于 v8.6.6 tag 创建分支。这样官方后续发布新版本我还可以通过git fetch upstream拉到本地用git diff看官方改了哪些地方再决定要不要把修复合入自己的定制版。.gitignore很重要。编译生成的 bin、obj、.vs 目录都不应该提交否则工作区会非常乱。官方仓库里已经有一套 .gitignore但我自己会在根目录加一个PowerEditor/bin/ PowerEditor/visualstudio/.vs/ *.user这样能避免把本地环境差异误提交到代码库。6.2 源代码加密的常见手段辨析很多人一提到源码保护就问“怎么给 C 源码加密”这是个误区。C 编译后直接生成机器码反编译还原的是汇编不是原始源码。所以所谓“加密”更多是防御性措施。对于 C 项目常见做法是把核心算法编译成静态库或动态库只开放头文件接口调用方看不到实现。对于脚本语言如 Python、JavaScript可以混淆代码、转成字节码或在服务器端运行核心逻辑客户端只调用接口。还有一种思路是许可证机制程序运行起来之后从加密狗或在线服务器校验授权而不是直接加密代码文件。Notepad 本身是开源项目不需要加密。但如果基于它做商业扩展需要注意 GPL 协议不能把扩展后的源码全部藏起来。更合理的做法是保留核心开源商业功能用独立插件、独立进程实现避免受传染性协议约束。6.3 反编译能还原源码吗我之前也好奇C 编译后的 exe 和 C# 编译后的 exe 反编译难度差多少实测下来差距非常大。C# 这类 .NET 程序编译出来的是 IL 指令用 dnSpy 或 ILSpy 几乎能还原 90% 的原始代码结构类名、方法名都在只差少量注释。所以很多 WinForm 程序“打包后丢源码”并不是绝对安全。Notepad 这种原生 Win32 程序反编译就难多了。Release 编译后符号表会被剥离函数名只剩下地址反编译出来的代码是汇编和伪代码阅读成本极高。如果你想给自己的 C 程序增加一点保护编译时要保证 Release 配置下“生成调试信息”设置为无或DEBUG_INFO_NONE同时启用/O2优化这能让反编译的代码更难看懂。7. 常见问题与排查实录7.1 问题速查表问题现象常见原因解决方向编译找不到 boost 头文件C1083 无法打开包括文件附加包含目录未配置在 VS 项目属性里添加 boost 根目录链接错误 LNK2019无法解析的外部符号缺少依赖库或编译顺序不对重新生成依赖项目检查库目录运行 exe 提示缺少 DLLSciLexer.dll 不存在Scintilla 库没有复制到 bin手动复制 DLL 或调整后期生成事件插件没有出现在菜单插件 DLL 不显示架构位数不匹配或导出函数缺失确认 x86/x64 一致检查 Dll 导出函数调试断点不命中F5 运行后断点变空心Release 优化导致代码被内联改用 Debug 配置或关闭优化打开文件乱码中文显示成乱码编码识别失败用“编码”菜单手动指定字符集7.2 我踩过的坑与独家经验第一个坑是编译时偷懒没有按 README 准备依赖。我以为仓库自带 boost 就够了结果发现缺少某个版本的 libcurl 头文件折腾半天才发现官方文档里写得很清楚。从那以后任何开源项目的源码我都先看构建文档不自己瞎猜。第二个坑是用 VS2022 编译旧分支时平台工具集默认是 v143但代码里有些地方是给 v140 写的导致编译警告非常多。这不是致命问题但警告太多会掩盖真正重要的错误。我的办法是先把警告级别调高看一眼有没有影响结果的警告再决定要不要处理。第三个坑是插件加载失败但它不弹任何提示。如果插件 DLL 的导出函数签名不对Notepad 只会在日志里记录一行很多新手根本不知道去哪看日志。后来我学会了在%APPDATA%\Notepad\下找plugins相关日志或者直接用 DebugView 抓取调试输出问题排查效率提升不少。最后分享一个经验在动源码之前先用 Git 打一个干净的 tag比如my-baseline-v8.6.6。因为源码研究过程里很容易改乱有了这个 tag任何折腾都能一键回退再也不会出现“改到一半发现回不去”的尴尬。Notepad 的源码并不神秘它就是一套规规矩矩的 Win32 Scintilla 工程。真正值钱的不是某个高深技术而是它把一个成熟桌面软件的“菜单、编辑、插件、配置、编码”等模块组织得清清楚楚。花一个周末把这份源码编译一遍再顺着菜单消息走几条主线你对桌面应用开发的理解会比看一年的“架构文章”都深。如果看完这篇你也打算动手我建议从最简单的地方切入先编译再加一个菜单项然后试着写个只有几行代码的最小插件。一步一步来你会看到一个编辑器是如何从无到有变成一个每天被全球开发者依赖的工具的。本文还有配套的精品资源点击获取
返回列表