简介:这份C++源代码资源实现了一个类似系统记事本的文本编辑程序,面向具备C++基础、希望从零理解GUI应用开发的学习者与开发者。项目围绕文本文件的读取、编辑与保存展开,并涉及查找替换、字体设置等常见功能,是练习面向对象编程与Windows界面开发的合适案例。压缩包共21个文件,约64KB,包含6个h头文件、5个cpp源文件,以及ico图标、bmp位图、rc资源脚本、dsp与dsw工程文件、dia对话框布局和可执行文件等,覆盖从工程配置、资源定义到主对话框逻辑与关于窗口实现的完整结构。已有482人学习下载,说明其作为入门参考具有一定认可度。通过阅读这些代码,读者可以了解MFC或WinAPI下窗口创建、消息处理、文件I/O与用户交互的基本流程,并借鉴一个C++项目的组织方式,适合作为课程设计或自学练手的实践素材。
1. 记事本C++源代码:从编译到二次开发的完整拆解
很多人第一次拿到记事本的C++源代码时,第一反应是“这不就是个能打字的小窗口吗”,结果打开工程一看,消息循环、窗口过程、编辑控件子类化、文件编码转换全挤在一起,改一行崩三处。这份资源解决的正是这个断层:它把Windows原生记事本的核心逻辑拆成可编译、可调试、可替换模块的完整工程,而不是只给一个黑匣子exe。适合两类人——刚学完C++语法想找一个能跑起来的GUI项目练手的新手,以及需要给内部工具加文本编辑能力、又不想引入Qt或Electron的熟手。代码基于Win32 API和标准C++,不依赖第三方框架,编译出来就是单文件可执行程序,拿来就能改菜单、加功能、换图标。
2. 工程结构与消息循环:先看懂再动手改
2.1 文件清单与模块职责
拿到源码包后别急着点编译,先花十分钟把目录结构过一遍。典型的记事本C++工程会包含这几个文件:main.cpp负责WinMain入口和窗口注册,NotepadApp.cpp封装应用级初始化,EditorWindow.cpp处理编辑控件的创建与消息分发,FileIO.cpp管打开保存和编码识别,resource.h和.rc文件定义菜单、快捷键和图标。有些版本会把查找替换单独拆成FindReplace.cpp,这个设计很关键——查找对话框是非模态的,需要独立的消息循环,混在主窗口里容易出玄学bug。
| 文件 | 职责 | 改动频率 |
|---|---|---|
| main.cpp | 入口、窗口类注册、消息泵 | 低 |
| EditorWindow.cpp | 编辑控件子类化、快捷键处理 | 中 |
| FileIO.cpp | 文件读写、编码检测、换行符转换 | 高 |
| FindReplace.cpp | 非模态查找替换对话框 | 中 |
| resource.rc | 菜单、加速键、图标资源 | 低 |
我一般会先编译原版跑一遍,确认基础功能正常,再动代码。编译命令用MSVC开发者命令行:
cl /EHsc /Fe:notepad.exe main.cpp NotepadApp.cpp EditorWindow.cpp FileIO.cpp FindReplace.cpp /link user32.lib gdi32.lib comdlg32.lib如果用的是MinGW,换成g++ -mwindows -o notepad.exe *.cpp -lcomdlg32。注意-mwindows不能省,否则会弹出一个多余的控制台窗口。编译通过后双击exe,测试打开一个UTF-8带BOM的文件和一个GBK文件,看中文是否乱码——这一步能提前暴露编码处理的坑。
2.2 消息循环与窗口过程的关键参数
Win32记事本的核心是GetMessage/TranslateMessage/DispatchMessage三件套。很多新手改代码时直接在WndProc里写业务逻辑,结果发现编辑控件的右键菜单不弹、Tab键不缩进。原因是编辑控件有自己的窗口过程,主窗口的WndProc只处理非编辑区域的消息。正确做法是对编辑控件做子类化,用SetWindowLongPtr替换它的GWLP_WNDPROC,在自定义过程里拦截WM_CHAR、WM_KEYDOWN,处理完再调CallWindowProc交还给系统默认过程。
// 子类化编辑控件,拦截Tab键实现缩进 WNDPROC g_oldEditProc = nullptr; LRESULT CALLBACK EditProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (msg == WM_KEYDOWN && wParam == VK_TAB) { // 在光标处插入4个空格,而不是切换焦点 SendMessage(hwnd, EM_REPLACESEL, TRUE, (LPARAM)" "); return 0; // 消息已处理,不再传递 } return CallWindowProc(g_oldEditProc, hwnd, msg, wParam, lParam); } // 在WM_CREATE中绑定 g_oldEditProc = (WNDPROC)SetWindowLongPtr(hEdit, GWLP_WNDPROC, (LONG_PTR)EditProc);这里EM_REPLACESEL的TRUE参数表示操作可撤销,用户按Ctrl+Z能回退。如果写成FALSE,缩进就变成不可撤销操作,体验直接翻车。CallWindowProc必须传旧的窗口过程指针,否则编辑控件的基本输入功能会丢失。这个模式在查找替换、自动换行、行号显示等扩展里反复用到,理解一次就能套用。
3. 文件读写与编码处理:把乱码问题按死在源头
3.1 编码检测的三种策略与选择依据
记事本最容易被吐槽的就是打开文件乱码。这份源码里FileIO.cpp通常实现了三种检测:BOM头检测、UTF-8合法性校验、系统默认代码页回退。BOM检测最简单,读前三个字节判断EF BB BF、FF FE、FE FF,分别对应UTF-8、UTF-16LE、UTF-16BE。但很多文件不带BOM,这时候就要靠UTF-8合法性校验——遍历字节序列,看是否符合UTF-8的编码规则,如果全部合法就按UTF-8处理,否则回退到CP_ACP。
// 简化的UTF-8合法性检测 bool IsValidUTF8(const char* data, size_t len) { size_t i = 0; while (i < len) { unsigned char c = data[i]; int follow = 0; if (c < 0x80) follow = 0; // ASCII else if ((c & 0xE0) == 0xC0) follow = 1; else if ((c & 0xF0) == 0xE0) follow = 2; else if ((c & 0xF8) == 0xF0) follow = 3; else return false; // 非法首字节 if (i + follow >= len) return false; for (int j = 1; j <= follow; j++) { if ((data[i+j] & 0xC0) != 0x80) return false; } i += follow + 1; } return true; }这个函数只做结构校验,不检查码点范围,所以像C0 80这种过长编码也会被判合法。实际工程里我会再加一层码点范围检查,把U+D800到U+DFFF的代理对排除掉。不过对于记事本场景,结构校验已经能覆盖95%的情况,剩下的交给用户手动切换编码。
3.2 换行符转换与保存时的编码选择
Windows记事本默认用\r\n,但打开Unix文件时只显示\n,如果不做转换直接保存,整个文件的换行就混了。源码里通常有一个NormalizeLineEndings函数,在加载时把\r\n和\r统一成\n存到内存,保存时再根据用户选择转回\r\n或保持\n。这个逻辑不复杂,但坑在于编辑控件的EM_GETLINE返回的文本里,换行符已经被系统转成了\r\n,所以内存里其实不用再转一遍,直接在保存时判断目标格式即可。
保存时的编码选择建议做成三档:ANSI(当前代码页)、UTF-8、UTF-8 with BOM。很多内部工具只需要UTF-8,那就默认UTF-8无BOM,兼容性最好。如果目标系统有老旧软件必须读BOM,再加一个复选框。注意WideCharToMultiByte转换时,lpUsedDefaultChar参数要传一个BOOL变量,如果返回TRUE说明有字符无法映射,这时候要提示用户而不是静默替换成问号。
提示:测试编码时至少准备四个文件——纯ASCII、GBK中文、UTF-8无BOM中文、UTF-8带BOM中文。每次改完FileIO.cpp都跑一遍这四个,能省下大量排查时间。
4. 避坑与排查:五个让编译和运行翻车的典型问题
4.1 现象:编译报错“无法解析的外部符号 _WinMain@16”
原因:链接器找不到入口点,通常是用了控制台子系统而不是Windows子系统。 解决:在链接选项里加/SUBSYSTEM:WINDOWS,MinGW加-mwindows。如果用的是CMake,检查add_executable后面有没有WIN32关键字。
4.2 现象:程序运行后窗口一闪而过
原因:WinMain里消息循环写成了while(GetMessage(...))但忘了TranslateMessage和DispatchMessage,或者WndProc里对WM_DESTROY没调PostQuitMessage。 解决:对照标准模板检查消息泵三件套是否齐全,WM_DESTROY里必须调PostQuitMessage(0),否则GetMessage永远不返回0。
4.3 现象:编辑控件里中文输入法候选框位置不对
原因:没处理WM_IME_STARTCOMPOSITION等IME消息,或者子类化时把IME消息也拦截了。 解决:在自定义EditProc里,对WM_IME_*系列消息直接CallWindowProc透传,不要做任何处理。如果还是不对,检查是否在WM_CREATE里设置了ImmAssociateContext。
4.4 现象:打开大文件(超过10MB)时界面卡死
原因:一次性把整个文件读进内存再SetWindowText,编辑控件重绘开销巨大。 解决:改用EM_SETSEL+EM_REPLACESEL分块追加,每块1MB左右,追加前先SendMessage(hEdit, WM_SETREDRAW, FALSE, 0),全部完成后再WM_SETREDRAW TRUE并InvalidateRect。这个优化能把10MB文件的加载时间从十几秒降到一秒内。
4.5 现象:保存后文件末尾多出空行或丢失最后一个换行
原因:EM_GETLINE获取的文本长度和实际字符数不一致,或者保存时手动补了\r\n但原文本已有。 解决:用GetWindowTextLength拿准确长度,GetWindowText一次性读取,不要逐行EM_GETLINE。保存时先检查末尾是否已有换行,没有才补。这个坑我踩过两次,后来每次改保存逻辑都先跑一个“空文件保存再打开”的用例。
5. 二次开发进阶:加行号栏和最近文件列表
5.1 行号栏的实现与性能取舍
给记事本加行号栏是常见的二次开发需求。思路是在编辑控件左侧放一个只读的静态控件或自绘窗口,根据编辑控件的行数和滚动位置同步绘制行号。关键是要处理EN_VSCROLL和EN_CHANGE通知,前者更新滚动偏移,后者重新计算总行数。计算行数不要用EM_GETLINECOUNT,那个在大文件下是O(n)操作,每次按键都调会卡。我一般用EM_GETLINECOUNT只在文件加载和粘贴大段文本时调一次,平时按键只更新当前行号。
// 行号栏绘制核心逻辑 void DrawLineNumbers(HDC hdc, HWND hEdit, int lineHeight) { int firstVisible = (int)SendMessage(hEdit, EM_GETFIRSTVISIBLELINE, 0, 0); int lineCount = (int)SendMessage(hEdit, EM_GETLINECOUNT, 0, 0); int visibleLines = 30; // 根据窗口高度算,这里简化 for (int i = 0; i < visibleLines && firstVisible + i < lineCount; i++) { char buf[16]; sprintf(buf, "%d", firstVisible + i + 1); TextOut(hdc, 5, i * lineHeight, buf, strlen(buf)); } }lineHeight要从编辑控件用EM_GETRECT和EM_GETLINECOUNT反推,或者直接取字体高度的tmHeight + tmExternalLeading。行号栏宽度根据最大行号位数动态调整,超过9999行就加宽,不然会截断。
5.2 最近文件列表的持久化与菜单动态更新
最近文件列表通常存在注册表HKEY_CURRENT_USER\Software\某内部工具\Recent下,或者同目录的ini文件里。我倾向用ini,方便绿色部署和排查。每次打开或保存文件后,把路径插到列表头部,去重,截断到10条。菜单项用ID_FILE_RECENT_BASE + index动态创建,在WM_INITMENUPOPUP里重建。
// 更新最近文件菜单 void UpdateRecentMenu(HMENU hMenu) { HMENU hRecent = GetSubMenu(hMenu, 1); // 假设“文件”菜单下第2项是最近文件 while (GetMenuItemCount(hRecent) > 0) DeleteMenu(hRecent, 0, MF_BYPOSITION); for (size_t i = 0; i < g_recentFiles.size(); i++) { char label[512]; sprintf(label, "&%d %s", i + 1, g_recentFiles[i].c_str()); AppendMenu(hRecent, MF_STRING, ID_FILE_RECENT_BASE + i, label); } }注意路径太长要截断中间部分,保留盘符和文件名,否则菜单会撑爆屏幕。点击后的处理在WM_COMMAND里判断ID范围,用ShellExecute打开文件,如果文件不存在就提示并从列表移除。这个功能代码量不大,但能显著提升工具感。
5.3 验证方法与我的固定习惯
每次改完代码,我会跑一个五步验证:编译无警告、打开四种编码文件不乱码、编辑后保存再打开内容一致、查找替换能正确定位、最近文件列表点击能打开。这五步走完基本能覆盖90%的回归问题。从那以后我每次提交代码前都强制走一遍这个清单,省下了无数次“改A崩B”的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取