简介:这份C++源代码资源实现了一个类似系统记事本的文本编辑程序,面向具备C++基础、希望从理论走向实践的编程学习者。项目围绕图形界面与文本处理展开,涵盖窗口创建、消息响应、文件读写等核心环节,适合作为课程设计或自学练手的参考案例。压缩包共21个文件,约64KB,以h头文件与cpp源文件为主体,另有ico图标、bmp位图、rc资源脚本及dia对话框布局等界面素材,并附dsp、dsw工程文件与可执行程序,完整呈现了一个小型MFC项目的组织结构。资源已有482人学习下载,读者可从中理解类与对象的封装设计、STL容器与字符串的运用,以及菜单、对话框等资源的定义方式,进而掌握C++ GUI编程的基本流程与项目搭建思路,为后续开发更复杂的桌面应用打下基础。
1. 从一份记事本源码说起:为什么我建议你亲手编译一遍
很多人第一次接触 C++ 桌面项目,都是从记事本这类小工具开始的。它看起来简单,但麻雀虽小五脏俱全:窗口创建、消息循环、菜单响应、文件读写、文本编辑控件、剪贴板操作,一个都不少。这份「记事本的 C++ 源代码」就是这样一个完整的 Windows 桌面应用,用纯 Win32 API 写成,没有 MFC、没有 Qt,编译出来就是一个能用的记事本程序。它适合谁?适合刚学完 C++ 语法、想看看真实项目长什么样的新手,也适合想复习 Win32 消息机制、准备做二次开发的老手。你拿到源码后,第一件事不该是读代码,而是先把它编译跑起来,看到窗口弹出来的那一刻,后面的一切才有意义。
2. 项目结构与技术选型:纯 Win32 API 的极简架构
2.1 为什么不用 MFC 或 Qt
这份源码最核心的技术决策就是:只用 Win32 API。这意味着你不需要安装任何额外的框架,只要有 Visual Studio 或者 MinGW,就能直接编译。MFC 虽然封装了窗口类,但隐藏了消息循环的细节;Qt 跨平台但引入了庞大的运行时。对于学习目的来说,Win32 API 是最透明的选择——每一个窗口句柄、每一条消息、每一次 GDI 调用都摆在明面上。
常见做法是:用WinMain作为入口,注册窗口类,创建主窗口,然后跑一个GetMessage/TranslateMessage/DispatchMessage循环。这份源码也是这个套路,没有例外。
2.2 目录结构一览
源码包解压后,通常是这样几个文件:
notepad/ ├── main.cpp // 入口,WinMain 和窗口过程 ├── resource.h // 资源 ID 定义 ├── notepad.rc // 资源脚本:菜单、加速键、图标 ├── notepad.ico // 程序图标 └── Makefile 或 .vcxproj // 构建脚本有些版本会把文件操作单独拆成file.cpp,编辑操作拆成edit.cpp,但核心逻辑都围绕一个WndProc展开。资源文件.rc很关键,菜单项和快捷键都在里面定义,改界面不用动 C++ 代码。
2.3 核心数据结构
整个程序围绕一个全局的HWND展开——主窗口句柄,以及一个子窗口句柄指向多行编辑控件。编辑控件负责所有文本显示和输入,主窗口只负责菜单命令和文件对话框。这种分工让代码非常清晰:编辑控件自己处理键盘输入、滚动、选择,主窗口只处理「新建」「打开」「保存」「另存为」这些命令。
// 全局变量:主窗口和编辑控件的句柄 HWND hMainWnd; // 主窗口 HWND hEdit; // 多行编辑控件 HINSTANCE hInst; // 当前实例句柄 // 窗口类名,注册和创建时都要用 const wchar_t CLASS_NAME[] = L"NotepadWndClass";参数说明:hEdit是CreateWindowEx创建EDIT类控件时返回的句柄,后续所有文本操作都通过SendMessage(hEdit, ...)完成。hInst在WinMain里拿到,注册窗口类时需要。
2.4 消息循环与窗口过程
Win32 程序的骨架就是消息循环。主窗口创建后,程序进入一个while循环,不断从消息队列取消息,翻译键盘消息,再分发到窗口过程。
// 标准 Win32 消息循环 MSG msg = {}; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); // 把 WM_KEYDOWN 翻译成 WM_CHAR DispatchMessage(&msg); // 分发给 WndProc } return (int)msg.wParam;逻辑说明:GetMessage在收到WM_QUIT时返回 0,循环结束。TranslateMessage负责把虚拟键消息转成字符消息,编辑控件才能收到正常输入的字符。DispatchMessage把消息送到注册时指定的WndProc函数。
窗口过程WndProc是一个巨大的switch,处理WM_CREATE、WM_COMMAND、WM_SIZE、WM_DESTROY等消息。其中WM_CREATE里创建编辑控件,WM_SIZE里调整编辑控件大小让它填满客户区,WM_COMMAND里根据菜单 ID 执行新建、打开、保存等操作。
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_CREATE: // 创建多行编辑控件,带滚动条 hEdit = CreateWindowEx(WS_EX_CLIENTEDGE, L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL, 0, 0, 0, 0, hwnd, (HMENU)ID_EDIT, hInst, nullptr); return 0; case WM_SIZE: // 编辑控件填满客户区 MoveWindow(hEdit, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE); return 0; case WM_COMMAND: switch (LOWORD(wParam)) { case ID_FILE_NEW: OnFileNew(hwnd); break; case ID_FILE_OPEN: OnFileOpen(hwnd); break; case ID_FILE_SAVE: OnFileSave(hwnd); break; case ID_FILE_EXIT: DestroyWindow(hwnd); break; } return 0; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wParam, lParam); }参数说明:WM_CREATE的lParam指向CREATESTRUCT,这里没用到。WM_SIZE的lParam低 16 位是宽度,高 16 位是高度,LOWORD和HIWORD宏负责提取。WM_COMMAND的LOWORD(wParam)是菜单项或控件的 ID,HIWORD(wParam)是通知码,对菜单来说通常是 0。
3. 核心功能实现:文件读写与编辑操作
3.1 新建与打开:对话框与文件流
「新建」最简单,把编辑控件内容清空即可。但要注意:清空之前应该检查是否有未保存的修改。常见做法是用EM_GETMODIFY消息查询编辑控件的修改标志,如果有修改就弹 MessageBox 问用户要不要保存。
void OnFileNew(HWND hwnd) { // 检查是否有未保存的修改 if (SendMessage(hEdit, EM_GETMODIFY, 0, 0)) { int ret = MessageBox(hwnd, L"是否保存当前更改?", L"记事本", MB_YESNOCANCEL); if (ret == IDCANCEL) return; if (ret == IDYES) OnFileSave(hwnd); } // 清空编辑控件 SetWindowText(hEdit, L""); // 清除修改标志 SendMessage(hEdit, EM_SETMODIFY, FALSE, 0); }逻辑说明:EM_GETMODIFY返回非零表示内容被改过。EM_SETMODIFY传FALSE清除标志。SetWindowText对编辑控件就是设置全部文本。
「打开」用GetOpenFileName弹出标准文件对话框,拿到路径后用CreateFile打开,读取到缓冲区,再SetWindowText塞进编辑控件。注意要处理 Unicode 编码,源码里用的是宽字符版本GetOpenFileNameW。
void OnFileOpen(HWND hwnd) { OPENFILENAME ofn = { sizeof(ofn) }; wchar_t szFile[MAX_PATH] = L""; ofn.hwndOwner = hwnd; ofn.lpstrFilter = L"文本文件 (*.txt)\0*.txt\0所有文件 (*.*)\0*.*\0"; ofn.lpstrFile = szFile; ofn.nMaxFile = MAX_PATH; ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST; if (!GetOpenFileName(&ofn)) return; // 用户取消 HANDLE hFile = CreateFile(szFile, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) { MessageBox(hwnd, L"无法打开文件", L"错误", MB_ICONERROR); return; } DWORD size = GetFileSize(hFile, nullptr); char* buf = new char[size + 1]; DWORD read = 0; ReadFile(hFile, buf, size, &read, nullptr); buf[read] = '\0'; CloseHandle(hFile); // 转成宽字符后设置到编辑控件 int wlen = MultiByteToWideChar(CP_UTF8, 0, buf, -1, nullptr, 0); wchar_t* wbuf = new wchar_t[wlen]; MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, wlen); SetWindowText(hEdit, wbuf); delete[] buf; delete[] wbuf; SendMessage(hEdit, EM_SETMODIFY, FALSE, 0); }参数说明:OPENFILENAME的lpstrFilter是双\0结尾的字符串对,每对是「显示名」和「模式」。CreateFile返回INVALID_HANDLE_VALUE表示失败。MultiByteToWideChar第一次调用传nullptr拿长度,第二次才真正转换。
3.2 保存与另存为:写入磁盘
保存逻辑和打开对称:如果当前有文件路径,直接写入;如果没有,走「另存为」弹保存对话框。文件路径用一个全局wchar_t数组记住。
wchar_t g_filePath[MAX_PATH] = L""; // 当前文件路径 void OnFileSave(HWND hwnd) { if (g_filePath[0] == L'\0') { OnFileSaveAs(hwnd); // 没有路径,走另存为 return; } // 获取编辑控件文本长度 int len = GetWindowTextLength(hEdit); wchar_t* wbuf = new wchar_t[len + 1]; GetWindowText(hEdit, wbuf, len + 1); // 转成 UTF-8 写入 int blen = WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, nullptr, 0, nullptr, nullptr); char* buf = new char[blen]; WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, buf, blen, nullptr, nullptr); HANDLE hFile = CreateFile(g_filePath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile != INVALID_HANDLE_VALUE) { DWORD written = 0; WriteFile(hFile, buf, blen - 1, &written, nullptr); // 不写结尾的 \0 CloseHandle(hFile); SendMessage(hEdit, EM_SETMODIFY, FALSE, 0); } delete[] wbuf; delete[] buf; }逻辑说明:GetWindowTextLength和GetWindowText是获取编辑控件文本的标准方式。WideCharToMultiByte把宽字符转成 UTF-8,这样保存的文件在别的编辑器里打开不会乱码。CREATE_ALWAYS表示覆盖已有文件。
3.3 编辑菜单:撤销、剪切、复制、粘贴
这些功能不需要自己实现,编辑控件内置了。主窗口只需要在菜单命令里转发消息给hEdit。
case ID_EDIT_UNDO: SendMessage(hEdit, WM_UNDO, 0, 0); break; case ID_EDIT_CUT: SendMessage(hEdit, WM_CUT, 0, 0); break; case ID_EDIT_COPY: SendMessage(hEdit, WM_COPY, 0, 0); break; case ID_EDIT_PASTE: SendMessage(hEdit, WM_PASTE, 0, 0); break; case ID_EDIT_SELECTALL: SendMessage(hEdit, EM_SETSEL, 0, -1); // 全选 break;参数说明:EM_SETSEL的wParam是起始位置,lParam是结束位置,传-1表示到文本末尾。这些消息由编辑控件自己处理,不需要额外代码。
3.4 字体与自动换行
记事本通常有个「格式」菜单,能切换自动换行和设置字体。自动换行通过切换编辑控件的WS_HSCROLL和ES_AUTOHSCROLL样式实现,需要重建控件或者用SetWindowLong改样式后刷新。
void ToggleWordWrap(HWND hwnd) { static bool wrapped = false; LONG style = GetWindowLong(hEdit, GWL_STYLE); if (wrapped) { style |= WS_HSCROLL | ES_AUTOHSCROLL; } else { style &= ~(WS_HSCROLL | ES_AUTOHSCROLL); } SetWindowLong(hEdit, GWL_STYLE, style); wrapped = !wrapped; // 强制重绘 InvalidateRect(hEdit, nullptr, TRUE); }逻辑说明:GetWindowLong/SetWindowLong读写窗口样式。去掉水平滚动样式后,编辑控件会自动换行。InvalidateRect触发重绘让改动生效。
4. 编译与运行:从源码到可执行文件
4.1 用 Visual Studio 编译
如果你有 Visual Studio,最简单的方式是新建一个「Windows 桌面应用程序」项目,把源码文件加进去,确保字符集设为 Unicode。资源文件.rc直接拖进项目,VS 会自动调用资源编译器。
常见做法是:项目属性里把「C/C++ → 预处理器 → 预处理器定义」加上UNICODE和_UNICODE,链接器里加上user32.lib、gdi32.lib、comdlg32.lib。这三个库分别对应窗口、图形、通用对话框。
4.2 用 MinGW 命令行编译
没有 VS 的话,MinGW-w64 也能编译。一条命令搞定:
# 编译资源文件 windres notepad.rc -O coff -o notepad.res # 编译并链接 g++ main.cpp notepad.res -o notepad.exe -mwindows -municode \ -luser32 -lgdi32 -lcomdlg32参数说明:-mwindows表示生成 GUI 程序,不弹控制台窗口。-municode启用 Unicode 入口点,对应wWinMain。-l指定链接库。windres是 MinGW 的资源编译器,把.rc编译成.res目标文件。
4.3 运行与验证
编译成功后双击notepad.exe,应该看到一个带菜单栏的窗口,中间是白色编辑区。测试步骤:输入文字,点「文件 → 保存」,选路径存成.txt,关掉程序,再打开,点「文件 → 打开」选刚才的文件,内容应该原样显示。如果中文乱码,检查保存时是否用了 UTF-8 编码。
提示:如果编译报错「无法解析的外部符号
WinMain」,说明入口点不对。Unicode 版本要用wWinMain,或者加-municode让链接器找wWinMain。
5. 避坑与常见问题排查
5.1 中文乱码:编码不统一
现象:打开或保存中文文本时显示乱码。原因:源码里混用了 ANSI 和 Unicode 函数,或者文件读写时编码转换不对。解决:统一用宽字符 API(CreateWindowExW、GetOpenFileNameW),文件读写时用MultiByteToWideChar/WideCharToMultiByte做 UTF-8 转换。编译时定义UNICODE和_UNICODE。
5.2 编辑控件不显示:忘记 WS_VISIBLE
现象:窗口出来了,但中间一片灰,没有编辑区。原因:CreateWindowEx创建编辑控件时漏了WS_VISIBLE样式,或者WM_SIZE里没调用MoveWindow导致控件大小为 0。解决:检查样式里有没有WS_VISIBLE,在WM_SIZE里用LOWORD(lParam)和HIWORD(lParam)设置宽高。
5.3 菜单点击没反应:WM_COMMAND 没处理
现象:菜单能点,但点了没任何反应。原因:WndProc里没有处理WM_COMMAND,或者菜单 ID 和resource.h里的定义对不上。解决:在switch (msg)里加case WM_COMMAND,用LOWORD(wParam)匹配 ID,确保.rc文件里的 ID 和resource.h一致。
5.4 程序关不掉:忘记 PostQuitMessage
现象:点关闭按钮,窗口消失了但进程还在任务管理器里。原因:WM_DESTROY里没有调用PostQuitMessage(0),消息循环收不到WM_QUIT。解决:在WM_DESTROY分支里加PostQuitMessage(0)。
5.5 文件保存后打不开:写入长度算错
现象:保存的.txt文件用别的编辑器打开是空的或者乱码。原因:WriteFile的长度参数传了blen而不是blen - 1,把字符串结尾的\0也写进去了;或者WideCharToMultiByte转换时长度算错。解决:写入时用blen - 1,转换时先用nullptr拿准确长度。
6. 二次开发:从记事本到自己的编辑器
拿到这份源码后,最自然的改造方向是加功能。比如加一个「查找替换」对话框,用FindText/ReplaceText或者自己写一个非模态对话框。核心思路是:在WndProc里处理WM_FIND消息,弹出对话框,拿到用户输入的查找词后,用SendMessage(hEdit, EM_SETSEL, start, end)选中匹配位置,再EM_SCROLLCARET滚动到可见区域。
另一个方向是加语法高亮。编辑控件本身不支持,常见做法是子类化编辑控件,拦截WM_PAINT自己绘制,或者换成RichEdit控件。RichEdit支持字符格式,可以用EM_SETCHARFORMAT给不同区域上色。改造步骤:把CreateWindowEx的类名从EDIT改成RICHEDIT50W,链接Msftedit.dll,然后按关键字位置设置格式。
还有一个实用改造是「最近打开文件」列表。用一个std::vector<std::wstring>存路径,每次打开或保存时更新,在「文件」菜单里动态插入菜单项。AppendMenu可以运行时加菜单,WM_COMMAND里根据 ID 范围判断是哪个最近文件。
// 动态添加最近文件菜单项 void UpdateRecentFilesMenu(HWND hwnd) { HMENU hMenu = GetMenu(hwnd); HMENU hFileMenu = GetSubMenu(hMenu, 0); // 删除旧的最近文件项(假设从 ID_RECENT_FIRST 开始) for (int i = 0; i < 5; i++) { DeleteMenu(hFileMenu, ID_RECENT_FIRST + i, MF_BYCOMMAND); } // 添加新的 for (size_t i = 0; i < g_recentFiles.size() && i < 5; i++) { AppendMenu(hFileMenu, MF_STRING, ID_RECENT_FIRST + i, g_recentFiles[i].c_str()); } }参数说明:GetMenu拿主菜单,GetSubMenu(hMenu, 0)拿第一个子菜单(文件菜单)。DeleteMenu按命令 ID 删除,AppendMenu追加。注意菜单项 ID 要避开已有的 ID 范围。
验证改造是否成功,我一般会走一遍完整流程:新建 → 输入中文 → 保存 → 关闭 → 重新打开 → 确认内容一致 → 测试新加的功能。这个流程能覆盖编码、文件读写、消息处理几个最容易翻车的地方。
从那以后我每次改 Win32 项目,都强制自己先跑一遍「新建-输入-保存-重开」的闭环,再动其他代码。希望帮到你。
本文还有配套的精品资源,点击获取