☰
C++手搓Win32记事本:从窗口骨架到编码转换全解析
2026/10/21 3:52:35 网站建设 项目流程

简介:这份C++源代码实现了一个类似系统自带的记事本程序,面向已掌握C++基础、想进一步学习桌面GUI开发的初学者,也适合高校C++课程设计参考。项目基于MFC/Windows API,涵盖窗口创建、消息循环、文本编辑、文件打开保存等核心流程,并涉及对话框资源、菜单图标的定义与使用,能够帮助读者理解一个完整Windows程序从工程搭建到事件响应的组织方式。压缩包共21个文件,以头文件(h)和C++源文件(cpp)为主,另有项目配置(dsp/dsw)、资源脚本(rc)和编译好的运行exe,整体仅64KB,结构精简,适合对照源码逐步阅读。目前已有483人学习浏览,对于想实践C++GUI编程的人,可从中看到类的设计、STL容器使用以及文件I/O的具体写法;资源内已含可执行文件,便于验证效果后进一步扩展查找替换、字体设置等功能,是理论联系实际的实用样例。

1. 项目概览:用 C++ 手搓记事本,真正的价值是跑通整套 Win32 骨架

很多人收到这份「记事本 C++ 源代码」时,第一反应是“一个文本框加一个保存按钮,有什么好看的”。可真去编译运行后你会发现,一个看似平平无奇的记事本,背后已经装下了 Windows 桌面程序的整套骨架:窗口类注册、消息循环、菜单资源、文件对话框、编码转换、文本查找,这些概念任何一个单独抽出来都能写一篇长文,而这份源码把它们串成了一个能跑、能改、能塞进自己工具箱的完整程序。它不依赖任何第三方界面库,核心就是 Win32 API 加 C++ 标准库。如果你刚学完 C++,正在找一个能真正编译运行、能逐行读懂窗口机制的练手项目,或者做内部工具时需要一份无依赖、可裁剪的编辑器底子,这份源码值得你花一晚上把它跑通再读透。

2. 技术栈与工程结构:Win32 原生窗口撑起整套编辑功能

2.1 工程文件清单:每个文件在干什么

先把源码包解压,正常你会看到下面这种目录结构。如果拿到的是合并好的单文件版本也没关系,本文按拆分结构讲,因为不管项目大小,生产环境里通常都会把“窗口逻辑”和“业务逻辑”放到不同文件。

NotepadCpp/ ├── main.cpp 程序入口 + 窗口过程 + 菜单命令分发 ├── editor.cpp 文件读取、保存、查找替换等核心操作 ├── editor.h 编辑器模块的函数声明与公共结构 ├── stdafx.h Win32 头文件的统一引入,避免重复 include ├── resource.h 菜单、控件 ID 的宏定义 ├── notepad.rc 菜单布局、图标、版本信息的资源脚本 ├── icon.ico 程序图标 └── README.md 编译与使用说明

先看 main.cpp。这个文件是程序的启动开关,干了三件事:注册窗口类、创建主窗口、启动消息循环,同时把窗口过程 WndProc 里收到的 WM_COMMAND 按菜单 ID 分发下去。也就是说,窗口“活着”的过程都在这个文件里,而具体“干活”的动作,比如打开文件、保存、查找,全部交给 editor.cpp 里的函数。

editor.cpp 不关心窗口是怎么创建的,也不处理消息循环,它暴露的接口很纯粹:OpenFileContent(HWND, LPCTSTR)、SaveFileContent(HWND, LPCTSTR)、FindText(HWND, LPCTSTR) 这一层。好处是,以后想把记事本从 Win32 迁移到 Qt,主窗口怎么重建都行,editor.cpp 里的文件读取和换行处理几乎不用大改。

resource.h 和 notepad.rc 是一对。resource.h 用宏定义每个菜单项 ID,比如#define IDM_OPEN 1001、#define IDM_SAVE 1002,而 notepad.rc 用脚本描述菜单长什么样,把菜单文字、ID 和图标绑定起来。编译时资源编译器把 rc 编译成 .res 二进制,最后链接进 exe。把界面元素从 C++ 代码里拆出去,是 Win32 工程的常规玩法,以后改菜单文字不用碰代码,改 rc 再编译一遍就行。

2.2 Win32 API 与 C++ 标准库的分工边界

这套源码最值得借鉴的一点,是把“系统能力”和“算法能力”分得很清。窗口创建、消息循环、通用文件对话框、文本控件这些都是系统层面的能力,必须调 Win32 API,标准库替代不了。而文件读到内存之后的处理——字符串查找、大小写转换、按行拆分——这些是纯逻辑运算,用 std::wstring、std::vector 和 STL 算法来写,既简洁又不容易出错。

有人会问,为什么不直接用现成的界面框架,一个月就能做出功能更花哨的编辑器?原因很实在,这套源码的目标是让你彻底搞懂系统窗口机制本身。框架把窗口注册、消息循环全封装掉了,写个记事本是顺手,但出了问题你不知道去哪查。拉开这套源码的消息循环,你能亲眼看到 GetMessage 阻塞在哪一行,看到 WM_PAINT 是怎么被触发的。这种亲手调系统调用的经验,对后面调试任何 Windows 窗口程序都有帮助。

再说依赖。源码只用了 user32、gdi32、comdlg32 三组系统库,没有第三方静态库,编译产物是单个 exe,拷到其他 Windows 环境里直接跑。做内部小工具时,这种零依赖特性往往是最大的卖点。

2.3 程序入口 WinMain 与消息循环的作用

不管界面多复杂,Win32 GUI 程序都从 WinMain 开始,最终落进一个消息循环。典型骨架如下:

#include <windows.h> #include "resource.h" #include "editor.h" // 窗口类名,注册窗口和创建窗口时要用同一个名字 const wchar_t* g_ClassName = L"NotepadCppWndClass"; // 前向声明窗口过程函数,注册窗口类时需要它的函数指针 LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) { // 1. 描述窗口类:过程函数、图标、背景色 WNDCLASSEX wc = {}; wc.cbSize = sizeof(WNDCLASSEX); wc.lpfnWndProc = WndProc; // 所有消息都进这里 wc.hInstance = hInstance; wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON)); wc.hCursor = LoadCursor(nullptr, IDC_ARROW); wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1); // 系统窗口底色 wc.lpszClassName = g_ClassName; if (!RegisterClassEx(&wc)) return -1; // 2. 创建主窗口,返回句柄 HWND hwnd = CreateWindowExW( 0, g_ClassName, L"C++ 记事本", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, nullptr, nullptr, hInstance, nullptr); if (!hwnd) return -1; ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); // 3. 消息循环:程序停在这里等系统派发消息 MSG msg = {}; while (GetMessageW(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); // 键盘消息转 WM_CHAR 字符消息 DispatchMessageW(&msg); // 把消息交给 WndProc 处理 } return static_cast<int>(msg.wParam); }

这里有三处容易看走眼的地方。第一,WNDCLASSEX 的 lpfnWndProc 必须赋值,否则窗口收不到任何消息,程序会变成一个点不亮的白板。第二,hbrBackground 用 COLOR_WINDOW + 1,取的是系统窗口颜色,避免自己创建画刷带来资源泄露。第三,GetMessageW 返回 -1 表示错误,0 表示收到 WM_QUIT,正数表示取到消息,严谨的条件就用 while(GetMessageW(...))。

消息循环退出的唯一途径是收到 WM_QUIT。WM_QUIT 由 PostQuitMessage 产生,一般在 WndProc 的 WM_DESTROY 分支调用。这就是为什么每个窗口程序都要在 WM_DESTROY 里 post 退出消息,否则关掉窗口后进程还在后台挂着。

3. 核心模块拆解:一条 WM_COMMAND 消息走完从打开到保存的完整链路

3.1 窗口过程与 EDIT 子控件:所有动作的总出口

用户的所有操作,最终都以消息形式进入 WndProc。菜单点击变成 WM_COMMAND,键盘输入先 WM_KEYDOWN 再 WM_CHAR,鼠标弹起触发 WM_LBUTTONUP。这套源码把处理集中在一个大 switch 里,结构清楚:

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_CREATE: { // 创建 EDIT 子控件,作为主编辑区 g_hEdit = CreateWindowExW( 0, L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL, 0, 0, 0, 0, hwnd, nullptr, GetModuleHandleW(nullptr), nullptr); break; } case WM_SIZE: { // 窗口尺寸变化时,编辑框铺满整个客户区 MoveWindow(g_hEdit, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE); break; } case WM_COMMAND: { // 菜单 ID 在 wParam 低 16 位 switch (LOWORD(wParam)) { case IDM_OPEN: DoOpenFile(hwnd); break; case IDM_SAVE: DoSaveFile(hwnd); break; case IDM_SAVEAS: DoSaveAsFile(hwnd); break; case IDM_FIND: DoFindText(hwnd); break; case IDM_EXIT: DestroyWindow(hwnd); break; } break; } case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProcW(hwnd, msg, wParam, lParam); } return 0; }

WM_COMMAND 是所有 Win32 程序的核心枢纽。菜单项 ID 来自 notepad.rc 里的定义,DispatchMessage 把消息送到 WndProc 后,IDM_OPEN 对应“打开文件”命令。注意菜单 ID 要大于 0x8000,因为低位 ID 会跟子控件通知混在一起,数值冲突以后排查非常头疼。

WM_SIZE 分支是文本编辑器和普通静态窗口最大的区别。EDIT 控件本身不会自动跟随主窗口大小变化,必须在每次尺寸变更时手动 MoveWindow 到新的客户区。漏掉这个分支,就会出现“窗口拉大了编辑框还冻在左上角”的经典画面。

3.2 打开文件:从 GetOpenFileName 到编码转换上屏

打开文件是记事本里技术含量最高的部分,因为编码问题直接决定用户看到的是不是乱码。整个流程分成两段:先调通用对话框拿路径,再读文件做编码转换。

void DoOpenFile(HWND hwnd) { OPENFILENAMEW ofn = {}; wchar_t szFilePath[MAX_PATH] = {}; ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = hwnd; ofn.lpstrFilter = L"文本文件 (*.txt)\0*.txt\0所有文件 (*.*)\0*.*\0"; ofn.lpstrFile = szFilePath; ofn.nMaxFile = MAX_PATH; ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST; if (GetOpenFileNameW(&ofn) != 0) { LoadTextFromFile(hwnd, szFilePath); } }

GetOpenFileNameW 是系统通用对话框,文件类型过滤串用双 \0 结尾,这个格式非常反直觉,很容易漏写导致过滤失效。Flags 加 OFN_FILEMUSTEXIST,用户必须选一个真实存在的文件,避免写不存在路径时的额外错误分支。

文件加载的重头戏是编码判断。常见处理逻辑是:先读文件头,如果开头是 EF BB BF 三个字节,说明是有 BOM 的 UTF-8,此时跳过 3 个字节,用 MultiByteToWideChar 从 UTF-8 转成 UTF-16;否则按 ANSI 处理,用当前系统代码页转换。

bool LoadTextFromFile(HWND hwnd, const wchar_t* path) { std::ifstream f(path, std::ios::binary | std::ios::ate); if (!f) return false; std::streamsize size = f.tellg(); f.seekg(0, std::ios::beg); std::vector<char> buf(static_cast<size_t>(size)); f.read(buf.data(), size); f.close(); std::wstring wtext; int offset = 0; // 检查 BOM:EF BB BF 是 UTF-8 的标识 const unsigned char* p = reinterpret_cast<const unsigned char*>(buf.data()); if (size >= 3 && p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF) { offset = 3; } if (offset == 3) { // UTF-8 转 UTF-16,先取需要的缓冲区长度,再执行转换 int len = MultiByteToWideChar(CP_UTF8, 0, buf.data() + offset, static_cast<int>(size - offset), nullptr, 0); wtext.resize(len); MultiByteToWideChar(CP_UTF8, 0, buf.data() + offset, static_cast<int>(size - offset), wtext.data(), len); } else { // 按系统 ANSI 代码页处理,中文环境即 GBK int len = MultiByteToWideChar(CP_ACP, 0, buf.data(), static_cast<int>(size), nullptr, 0); wtext.resize(len); MultiByteToWideChar(CP_ACP, 0, buf.data(), static_cast<int>(size), wtext.data(), len); } // 把宽字符串一次性塞给编辑框控件 SetWindowTextW(g_hEdit, wtext.c_str()); return true; }

MultiByteToWideChar 第四个参数传入准确的字节数而不是 -1,是因为文件内容可能包含空字符或末尾没有 \0,用 size 是安全做法。先调一次拿长度,再 resize,再调一次完成转换,这是 Win32 编码转换的标准两段式写法,很多复制粘贴的代码因为“缓冲区开得不够大导致转换截断”翻过车。

SetWindowTextW 会把编辑框里原有内容整体替换,这是“打开文件”的基本行为。以后做多标签页或追加打开,要改成发送 EM_REPLACESEL,这个在二次开发章节细说。

3.3 保存文件:WideCharToMultiByte 与两种编码策略

保存的思路和打开相反:先 GetWindowTextW 取控件内容,再 WideCharToMultiByte 转目标编码,最后写文件。如果直接把 wchar_t 缓冲区交给 WriteFile,写出来的文件开头会带 FF FE,大多数文本编辑器能认,但命令行工具里就乱套了。

bool SaveTextToFile(HWND hwnd, const wchar_t* path, bool asUtf8) { int len = GetWindowTextLengthW(g_hEdit); std::wstring content(len + 1, L'\0'); GetWindowTextW(g_hEdit, content.data(), len + 1); content.resize(len); // 去掉末尾 \0 std::ofstream f(path, std::ios::binary); if (!f) return false; if (asUtf8) { int need = WideCharToMultiByte(CP_UTF8, 0, content.c_str(), static_cast<int>(content.size()), nullptr, 0, nullptr, nullptr); std::vector<char> utf8(need); WideCharToMultiByte(CP_UTF8, 0, content.c_str(), static_cast<int>(content.size()), utf8.data(), need, nullptr, nullptr); // 写带 BOM 的 UTF-8,和系统记事本默认行为对齐 const unsigned char bom[3] = { 0xEF, 0xBB, 0xBF }; f.write(reinterpret_cast<const char*>(bom), 3); f.write(utf8.data(), utf8.size()); } else { // 按系统 ANSI 代码页保存,纯英文内容与 ASCII 一致 int need = WideCharToMultiByte(CP_ACP, 0, content.c_str(), static_cast<int>(content.size()), nullptr, 0, nullptr, nullptr); std::vector<char> ansi(need); WideCharToMultiByte(CP_ACP, 0, content.c_str(), static_cast<int>(content.size()), ansi.data(), need, nullptr, nullptr); f.write(ansi.data(), ansi.size()); } f.close(); return true; }

WideCharToMultiByte 第五个参数传准确的字符数,而不是传 0 让它按 \0 结束判断,能正确处理内容中嵌了空字符的情况。写 UTF-8 时额外写了 BOM,这是和系统记事本对齐的取舍。如果你做的是程序员用的编辑器,反而应该去掉 BOM,因为编译器普遍不喜欢带 BOM 的源文件。选择哪种默认策略完全取决于用途,这也是二次开发时第一个要想清楚的设计决策。

3.4 查找文本:字符索引与光标定位的配合

查找功能是记事本里最能体现“字符索引”概念的部分。思路不复杂:取控件全部文本,用 std::wstring::find 定位关键词,命中后发 EM_SETSEL 让控件自动选中并滚动到目标位置。

void DoFindText(HWND hwnd) { int len = GetWindowTextLengthW(g_hEdit); std::wstring content(len + 1, L'\0'); GetWindowTextW(g_hEdit, content.data(), len + 1); content.resize(len); std::wstring keyword = L"hello"; // 实际应从查找输入框获取 size_t pos = content.find(keyword); if (pos != std::wstring::npos) { // EM_SETSEL 参数是字符位置,不是字节位置 SendMessageW(g_hEdit, EM_SETSEL, pos, pos + keyword.length()); SetFocus(g_hEdit); } else { MessageBoxW(hwnd, L"未找到匹配文本", L"查找", MB_OK | MB_ICONINFORMATION); } }

EM_SETSEL 的两个参数分别表示选中区起始和结束位置,单位是 UTF-16 字符索引。一个汉字在 UTF-16 下占一个单元,所以直接用 wstring::find 返回的 pos 传入就能精确定位,这正好绕开了“按字节传导致中文选区偏移”的经典坑。

注意 GetWindowTextW 只返回控件当前文本,不包含自动换行产生的软回车。如果以后要做按行统计,需要配合 EM_GETLINECOUNT 和 EM_GETLINE 逐行取数据,不能单纯依赖控件文本内容数行数,否则跟实际所见行数会对不上。

4. 编译与运行:从源码到可执行文件的完整过程

4.1 准备编译器与环境配置

在 Windows 上编译 Win32 程序,最省心的是直接用系统自带的 C++ 工具链,也就是安装开发工具时附带的那套命令行编译环境。装好后从开始菜单打开“x64 Native Tools Command Prompt”,里面已经配好 cl.exe、rc.exe 和所有库路径,直接编译即可。如果你更喜欢 GCC 系,也可以装 MinGW-w64 环境,后面给到对应命令。

注意,别在普通终端里直接敲 cl 命令,会报“找不到 cl.exe”,这不是代码问题,是环境没配置。系统会提示需要调用 VsDevCmd.bat 之类脚本,或用 IDE 自带的命令行窗口,路径配置都替你做好了。

4.2 命令行编译:资源脚本与链接细节

MSVC 环境下的完整编译命令包含编译资源和编译源码两条链路:

rc /r notepad.rc cl /nologo /EHsc /W4 /utf-8 main.cpp editor.cpp notepad.res ^ /link user32.lib gdi32.lib comdlg32.lib /SUBSYSTEM:WINDOWS

第一行 rc 工具把 notepad.rc 编译成 notepad.res,第二行 cl 编译两个 .cpp 文件并和资源文件一起链接。user32.lib 提供窗口和消息相关 API,gdi32.lib 提供绘图相关函数,comdlg32.lib 是通用文件对话框依赖的库。链接报 undefined symbol 时,多数就是对应库没加进这条命令。

用 MinGW-w64 的环境编译命令:

x86_64-w64-mingw32-g++ -std=c++17 -Wall -municode \ main.cpp editor.cpp -o notepad.exe \ -mwindows -luser32 -lgdi32 -lcomdlg32

-municode 告诉链接器入口函数是 wWinMain,-mwindows 表示 GUI 程序,不弹出控制台窗口。在本地用原生 MinGW,把 x86_64-w64-mingw32- 前缀去掉直接 g++ 编译即可。

4.3 功能验证清单与测试思路

编译成功后双击 notepad.exe,按下面清单逐项测试,能快速判断产物是否正确:

功能操作方式预期结果
打开文件菜单 → 打开文件内容上屏,中文不乱码
保存文件菜单 → 保存磁盘文件与编辑区一致
另存为菜单 → 另存为弹出路径选择对话框
查找菜单 → 查找跳转并选中第一个匹配词
窗口缩放拖拽窗口边缘编辑区随窗口同步平铺

测试时准备一个 GBK 编码的中文文件和一个带 BOM 的 UTF-8 文件分别打开,这是编码逻辑最容易翻车的地方。再准备一个 5MB 以上的日志文件,感受一下当前实现对大文件的处理开销,心里留个底,后面遇到卡死问题至少知道方向。

4.4 调试时的常见排查手段

程序跑着跑着不响应,先把调试器挂上,中断后看主线程栈停在哪个系统调用。多数情况看到卡在 GetMessageW 里,这其实是正常状态,说明消息派发完了在等新消息;真正的问题是卡在文件 IO 或者 SetWindowTextW 等控件刷新,这时断点要打在文件读取相关函数里逐步观察。

另一个常见的翻车现场是窗口能创建但菜单点不动。先查 notepad.rc 里菜单资源是否编译进了 exe,再看创建窗口时有没有把菜单句柄关联进去。资源加载失败的典型报错是找不到 ID,此时对比 resource.h 和 rc 文件里的 ID 值是否一致,少个下划线或多打个字母都会导致链接失败。

5. 避坑记录:Win32 记事本最容易翻车的五个位置

5.1 打开中文文件全是乱码

现象:用系统记事本写的 GBK 中文文件,在这个 C++ 记事本里打开后全是大段乱码。

原因:程序直接调 SetWindowTextW 把原始字节塞给宽字符控件,中间没有经过代码页转换。ANSI 编码的字节流被当作 UTF-16 解析,中文自然全乱。

解决:读取文件先看 BOM,有 EF BB BF 就按 CP_UTF8 转换,没有则按 CP_ACP 转换。CP_ACP 是当前系统默认代码页,简中系统是 936(GBK),繁中系统是 950(BIG5),这套逻辑在繁体环境也能正确打开当地文件。这是中文 Windows 编程最玄学的一关。

5.2 编辑框不跟随窗口缩放

现象:窗口拖大之后,编辑区仍然冻结在左上角一小块,其余区域是灰底。

原因:WM_SIZE 分支里没有对 g_hEdit 调 MoveWindow。编辑框创建时传了 0,0,0,0,此后如果没有收到尺寸更新,它就一直是零尺寸,正常情况下只能看到很小一块区域。

解决:在 WM_SIZE 分支里用 LOWORD(lParam)、HIWORD(lParam) 取到新客户区宽高,执行 MoveWindow(g_hEdit, 0, 0, cx, cy, TRUE)。这个分支是 Win32 控制类子窗口的标配,很容易被遗漏,尤其是从单窗口示例抄代码起步的人。

5.3 大文件一打开就界面卡死

现象:打开 10MB 的日志文件,窗口变白板,十几秒后才恢复响应。

原因:整个读取和设置操作都跑在 UI 线程里,一次性读入全部字节又 SetWindowTextW 整体塞回,编辑控件处理大文本本身就有较大开销,期间无法处理 WM_PAINT,表现就是卡死。

解决:文件超过设定阈值(比如 2MB)时,分段向编辑框发送 EM_REPLACESEL,或者只加载前 N 行并提示用户文件过大。要保留完整编辑能力,就需要换虚拟化渲染方案,这对初始项目来说超纲,但值得知道方向。

5.4 保存后文件末尾缺字符

现象:保存文本后用其他编辑器打开,最后几个字丢了。

原因:SaveTextToFile 里 GetWindowTextLengthW 取到长度后,又主动多分配一位,但取完没有立即 resize 去掉末尾 \0,导致转出来的编码里多了一个伪造的空字符位置,或因为 ofstream 没有显式 flush 导致缓冲区尾部没落盘。

解决:GetWindowTextLengthW 之后多分配一位存 \0,取完立刻 resize 去掉末尾,再执行转码。文件写入完成后显式调用 flush,不要指望析构函数兜底。这两步能根治大多数“文件尾部丢失”的问题。

5.5 查找中文关键词时位置偏移

现象:查找中文关键词,高亮位置明显比真实文字错位,英文关键词却正常。

原因:索引体系错乱。wstring::find 返回的是 wchar_t 字符个数索引,EM_SETSEL 同样按 wchar_t 计。跑偏的代码大多是在取文本和查找之间夹了一层 MultiByteToWideChar 或字符串拼接,导致 keyword 内部混入干扰字符,索引自然错位。

解决:从控件取文本统一用 GetWindowTextW,得到的就是 UTF-16 字符串,和 EM_SETSEL 的索引体系完全对齐。不要在中间插入任何编码转换又忘记重算偏移。这是中文窗口程序里反复出现的坑,排查时用调试器把两个索引值打印出来对比,一眼就能看出问题。

6. 二次开发路线:给记事本加字符统计与自动换行开关

6.1 先读结构再动手:两条改造线路怎么选

拿到源码先不要急着改代码,第一步是确认窗口逻辑和业务逻辑的边界。按项目原本的结构,窗口过程在 main.cpp,具体业务动作都在 editor.cpp。你要加“字符统计”功能,摆面前有两条路:直接在 WndProc 的 WM_COMMAND 里加一个新的 case,或者拆一个独立函数放 editor.cpp,由 WndProc 调用。

我建议选第二条路。把可测试逻辑从窗口回调里剥出去,是 Win32 程序演进过程中价值极高的习惯。统计逻辑如果不碰任何窗口句柄,就可以单独写测试数据验证,不必每次启动界面手工点。窗口过程里只留一行函数调用,后续维护时改动范围也小得多。

6.2 在菜单资源里加入新功能入口

在 notepad.rc 的菜单脚本里找到“编辑”弹出菜单,仿照现有项加一行:

POPUP "编辑(&E)" BEGIN MENUITEM "查找(&F)...", IDM_FIND MENUITEM SEPARATOR MENUITEM "字符统计(&S)...", IDM_STATS END

然后在 resource.h 里补上 IDM_STATS 的宏定义,数值大于 0x8000 即可,比如 4005。WndProc 的 WM_COMMAND 里注册一个 case:

case IDM_STATS: ShowStatistics(hwnd); break;

ShowStatistics 的实现逻辑很直白:GetWindowTextLengthW 拿总字符数;SendMessageW 发 EM_GETLINECOUNT 拿行数;如果要显示光标所在行列,再用 EM_GETSEL 结合 EM_LINEFROMCHAR 计算。最后用 MessageBoxW 拼结果弹窗。这套组合几乎可以平移到任何 Win32 文本编辑器上。

6.3 实现统计弹窗并回归验证

加完新功能,最怕资源编译不过,或者统计弹窗被自动换行影响,行数和预期对不上。验证顺序我一般固定走一遍:重新编译 rc,确认资源 ID 没冲突;打开一个已知行数的文件跑统计,与系统记事本状态栏对照;如果行数多出几条,多半是末端换行符被算成了新行,在统计函数里判断当前字符是否为 \n 结尾做一次修正。

这个 C++ 记事本被我最终改成了小组内部在用的日志查看器,保留编辑功能之外,加了行号、高亮和字符统计。这一轮改下来最深的感受是,动手前先把源码结构摸透,那份“窗口与业务分离”的组织确实救了我,否则在 main.cpp 堆到两千行时,改一个菜单就要全局搜一遍分发。从那以后我每次拿到单文件的 Win32 源码,都强制自己先拆模块再动功能,这个顺序能省掉一长串回头调试的时间。希望这篇解析帮你的 C++ 源代码跑起来,也祝你在二次开发时少踩几个当年让我踩到怀疑人生的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询