简介:这是一份用C++编写的记事本程序完整源代码,面向希望从理论走向实践的C++初学者与进阶开发者,帮助其理解图形界面应用从设计到编译运行的全过程。压缩包共21个文件,约64KB,以6个h头文件、5个cpp源文件为核心,辅以ico图标、bmp位图、rc资源脚本、dsp与dsw工程文件及可执行程序,覆盖界面资源、工程配置与业务逻辑各层面。项目围绕主对话框、关于对话框及文本编辑处理展开,涉及MFC或WinAPI窗口创建、消息响应、文件读写与面向对象封装等知识点,读者可借此梳理一个C++ GUI项目的目录组织与模块划分方式。目前已有482人学习下载,适合作为课程设计或自学练手的参考范例,对照源码调试运行,能更直观地掌握文本编辑器的实现思路与常见排错方法。
1. 从一份记事本源码说起:为什么我建议你亲手跑一遍
很多人第一次接触 C++ 桌面开发,都是从记事本这类小工具开始的。它看起来简单,但真正拆开一份记事本的 C++ 源代码,你会发现里面藏着窗口消息循环、文本编辑控件、文件读写、菜单加速键、查找替换、字体设置这一整套东西。它不像算法题那样只考一个点,而是把 Win32 API 或 Qt 的常用套路串成了一条完整链路。这份资源适合两类人:一类是刚学完 C++ 语法、想找个能跑起来的完整项目练手的新手;另一类是想快速回顾桌面端事件驱动模型、准备做二次开发的老手。下面我就按“能干什么、怎么跑、怎么改”的顺序,把这份源码拆开讲清楚。
2. 项目定位与技术选型:它到底是不是你该下载的那一份
2.1 记事本源码的典型功能边界
一份完整的记事本 C++ 源代码,通常不会只实现一个能打字的窗口。它至少包含这些功能模块:主窗口与菜单栏、文本编辑区、文件新建/打开/保存/另存为、撤销与重做、剪切复制粘贴、查找与替换、字体与自动换行、状态栏行列号显示。有些版本还会加上最近打开文件列表、打印预览、编码切换。这些功能听起来基础,但每一个都对应着 Win32 或 Qt 里一个具体的 API 调用或信号槽连接。
从项目定位上看,它属于“单文档界面”应用,没有复杂的多线程和网络通信,适合用来理解事件驱动编程的基本骨架。如果你正在找的是那种带数据库、带网络请求的完整业务系统,这份源码可能偏轻;但如果你想搞懂一个 Windows 桌面程序从WinMain到消息循环再到窗口过程函数的完整生命周期,它反而是最干净的样本。
2.2 技术栈判断:Win32 API 还是 Qt
拿到一份记事本 C++ 源代码,第一件事是看它依赖什么。常见做法有两种:一种是纯 Win32 API 加标准 C++,用CreateWindowEx创建编辑控件,用GetOpenFileName调文件对话框;另一种是基于 Qt 的QMainWindow加QTextEdit,用信号槽处理菜单动作。两种路线各有各的适用场景。
纯 Win32 版本的好处是零第三方依赖,一个.cpp加一个.h就能编译,适合理解 Windows 消息机制。缺点是代码里到处是HWND、WPARAM、LPARAM,新手容易看晕。Qt 版本的好处是跨平台、代码可读性高,connect一行就能把菜单项和槽函数绑起来,但需要先装 Qt 开发环境。
判断方法很简单:打开源码目录,看有没有.pro文件或CMakeLists.txt里出现Qt5::Widgets、Qt6::Widgets。如果有,就是 Qt 路线;如果只有.vcxproj或Makefile里出现user32.lib、gdi32.lib,那就是 Win32 路线。我一般会先看main函数或WinMain的签名,这是最直接的区分点。
2.3 目录结构长什么样
一份组织得比较清楚的记事本源码,目录通常不会太深。常见结构是:
notepad-cpp/ ├── src/ │ ├── main.cpp # 程序入口,WinMain 或 main │ ├── MainWindow.cpp # 主窗口创建与消息处理 │ ├── MainWindow.h │ ├── FileManager.cpp # 文件打开/保存逻辑 │ ├── FileManager.h │ ├── EditOperations.cpp # 查找替换、剪贴板 │ └── resource.rc # 菜单、图标、加速键资源 ├── include/ ├── build/ └── README.md这个结构不是固定的,但核心思路是把窗口逻辑、文件操作、编辑操作分开。如果你拿到的源码把所有代码都塞在一个main.cpp里,那说明它更偏向教学演示,改起来会累一些,但跑起来反而更简单。看目录的时候重点找resource.rc或.ui文件,菜单和快捷键的定义基本都在那里。
2.4 运行前需要准备什么
Win32 路线的话,Windows 上装 Visual Studio 的“使用 C++ 的桌面开发”工作负载就够了,编译时链接user32.lib、gdi32.lib、comdlg32.lib。如果用的是 MinGW,命令行g++ main.cpp -o notepad.exe -mwindows -lcomdlg32也能出结果。Qt 路线则需要先装 Qt Creator 或命令行 qmake/cmake,再配置好对应的 Kit。
这里有个容易翻车的点:有些源码用了#pragma comment(lib, "comctl32.lib")来简化链接,但 MinGW 不认这个指令,换编译器时要在构建脚本里手动补上-lcomctl32。我一般会先看 README 里有没有写构建命令,没有的话就按“先试 IDE 工程文件,再试命令行”的顺序来。
3. 核心模块拆解:从消息循环到文件落盘的完整链路
3.1 程序入口与消息循环
Win32 记事本的骨架就在WinMain里。它做三件事:注册窗口类、创建主窗口、跑消息循环。下面这段是典型写法:
// main.cpp - Win32 记事本入口 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // 1. 注册窗口类,指定窗口过程函数 WndProc WNDCLASSEX wc = { sizeof(WNDCLASSEX) }; wc.lpfnWndProc = WndProc; // 消息处理回调 wc.hInstance = hInstance; wc.lpszClassName = L"NotepadClass"; wc.hCursor = LoadCursor(NULL, IDC_IBEAM); wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); RegisterClassEx(&wc); // 2. 创建主窗口,编辑控件在 WM_CREATE 里再建 HWND hWnd = CreateWindowEx(0, L"NotepadClass", L"记事本", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL); ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); // 3. 消息循环:从队列取消息,翻译后分发 MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); // 处理键盘字符消息 DispatchMessage(&msg); // 交给 WndProc } return (int)msg.wParam; }这段代码的关键参数:lpfnWndProc指向窗口过程函数,所有菜单点击、键盘输入、窗口重绘都会走到那里;hCursor设成IDC_IBEAM是为了让鼠标在编辑区显示为文本光标;hbrBackground用系统窗口色,避免自己管理画刷。消息循环里的TranslateMessage不能省,否则WM_CHAR不会产生,编辑框就收不到字符。
3.2 窗口过程与编辑控件创建
WndProc是消息处理的核心。记事本里最关键的几条消息是WM_CREATE、WM_COMMAND、WM_SIZE和WM_SETFOCUS。
// MainWindow.cpp - 窗口过程函数 LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { static HWND hEdit; // 编辑控件句柄,静态保存 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, GetModuleHandle(NULL), NULL); break; case WM_SIZE: // 窗口大小变化时,编辑控件铺满客户区 MoveWindow(hEdit, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE); break; case WM_COMMAND: // 菜单和加速键都走这里,按 wParam 低字分派 switch (LOWORD(wParam)) { case ID_FILE_NEW: DoFileNew(hWnd, hEdit); break; case ID_FILE_OPEN: DoFileOpen(hWnd, hEdit); break; case ID_FILE_SAVE: DoFileSave(hWnd, hEdit); break; case ID_EDIT_FIND: DoFindReplace(hWnd, hEdit); break; } break; case WM_SETFOCUS: SetFocus(hEdit); // 窗口获得焦点时把光标交给编辑区 break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, msg, wParam, lParam); } return 0; }这里有几个参数值得注意:ES_MULTILINE让编辑框支持多行,ES_AUTOVSCROLL让内容超出时自动滚动,WS_EX_CLIENTEDGE加一圈凹陷边框。WM_SIZE里用LOWORD和HIWORD从lParam取出客户区宽高,这是 Win32 里传两个 16 位值的经典做法。WM_COMMAND的LOWORD(wParam)是控件或菜单 ID,HIWORD(wParam)是通知码,菜单项一般不用管通知码。
3.3 文件打开与保存:对话框和编码处理
文件操作是记事本里最容易出兼容问题的地方。打开文件用GetOpenFileName,保存用GetSaveFileName,读写用CreateFile或标准fopen都行。下面这段用 Win32 通用对话框:
// FileManager.cpp - 打开文件并读入编辑框 void DoFileOpen(HWND hWnd, HWND hEdit) { OPENFILENAME ofn = { sizeof(OPENFILENAME) }; wchar_t szFile[MAX_PATH] = { 0 }; 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, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { MessageBox(hWnd, L"文件打开失败", L"错误", MB_ICONERROR); return; } DWORD dwSize = GetFileSize(hFile, NULL); char* pBuf = new char[dwSize + 1]; DWORD dwRead = 0; ReadFile(hFile, pBuf, dwSize, &dwRead, NULL); pBuf[dwRead] = '\0'; CloseHandle(hFile); // 转成宽字符后设置到编辑框 int wlen = MultiByteToWideChar(CP_ACP, 0, pBuf, -1, NULL, 0); wchar_t* pWide = new wchar_t[wlen]; MultiByteToWideChar(CP_ACP, 0, pBuf, -1, pWide, wlen); SetWindowText(hEdit, pWide); delete[] pBuf; delete[] pWide; }lpstrFilter里的\0分隔和末尾双\0是 Win32 对话框的固定格式,写错了过滤器就不显示。MultiByteToWideChar这里用CP_ACP表示按系统默认编码解析,如果源码要支持 UTF-8,就得改成CP_UTF8,并且保存时反向用WideCharToMultiByte。我一般会在打开和保存时都加一个编码判断,避免中文乱码这个玄学问题。
3.4 查找替换与剪贴板操作
查找替换在 Win32 里可以用EM_FINDTEXTEX消息配合FINDTEXTEX结构,也可以自己把编辑框内容取出来用std::wstring::find做。剪贴板则用OpenClipboard、SetClipboardData、GetClipboardData这一套。下面是一个查找的简化实现:
// EditOperations.cpp - 在编辑框中查找文本 bool FindTextInEdit(HWND hEdit, const wchar_t* target, bool matchCase) { if (!target || !*target) return false; // 取编辑框全部内容 int len = GetWindowTextLength(hEdit); wchar_t* buf = new wchar_t[len + 1]; GetWindowText(hEdit, buf, len + 1); std::wstring content(buf); std::wstring keyword(target); delete[] buf; // 大小写敏感处理 if (!matchCase) { std::transform(content.begin(), content.end(), content.begin(), towlower); std::transform(keyword.begin(), keyword.end(), keyword.begin(), towlower); } size_t pos = content.find(keyword); if (pos == std::wstring::npos) return false; // 选中匹配到的文本 SendMessage(hEdit, EM_SETSEL, pos, pos + keyword.size()); SendMessage(hEdit, EM_SCROLLCARET, 0, 0); return true; }EM_SETSEL的两个参数是起始和结束字符索引,EM_SCROLLCARET让编辑框滚动到选中位置。这里用std::wstring而不是裸指针,是为了避免手动管理内存。注意towlower对宽字符逐个转换,比std::tolower更适合 Windows 的wchar_t。
4. 编译与运行:从源码到可执行文件的完整步骤
4.1 Visual Studio 路线
如果你拿到的源码带.sln或.vcxproj,直接双击打开,确认解决方案平台是 x64 或 Win32,然后按 F7 生成。常见问题是字符集设置:项目属性里“字符集”要选“使用 Unicode 字符集”,否则L"..."宽字符串和CreateWindowEx的宽字符版本会对不上。如果报LNK2019未解析外部符号,检查链接器输入里有没有comctl32.lib、comdlg32.lib、user32.lib。
4.2 MinGW 命令行路线
没有 IDE 工程文件时,用命令行编译更直接:
# 在源码目录下执行,-mwindows 去掉控制台窗口 g++ src/main.cpp src/MainWindow.cpp src/FileManager.cpp \ src/EditOperations.cpp \ -o notepad.exe \ -mwindows -lcomctl32 -lcomdlg32 -lgdi32 -luser32 \ -std=c++17-mwindows告诉链接器生成 GUI 子系统程序,不加的话会弹出一个黑色控制台窗口。-lcomctl32是通用控件库,-lcomdlg32是通用对话框库。如果源码里用了std::filesystem,MinGW 版本要够新,否则得加-lstdc++fs。
4.3 Qt 路线的构建方式
如果源码是 Qt 版本,看有没有.pro文件。有的话:
qmake notepad.pro make # Linux/macOS mingw32-make # Windows MinGW如果是 CMake 工程:
mkdir build && cd build cmake .. -DCMAKE_PREFIX_PATH=/path/to/Qt cmake --build .Qt 版本常见问题是找不到Qt5Widgets,需要在CMAKE_PREFIX_PATH里指向 Qt 安装目录。跑起来后如果界面是空的,检查main.cpp里有没有QApplication app(argc, argv);和return app.exec();。
4.4 运行后的验证清单
程序跑起来后,按这个顺序验证:新建文件输入文字,看编辑框是否正常响应;打开一个 UTF-8 的 txt,看中文是否乱码;另存为后重新打开,看内容是否一致;按 Ctrl+Z 看撤销是否生效;拖动窗口边缘改变大小,看编辑区是否跟着缩放。这几步能覆盖消息循环、文件读写、控件布局三条主链路。如果某一步失败,基本能定位到对应模块。
5. 避坑与常见问题:编译和运行时的血泪记录
5.1 中文乱码:现象、原因与解决
现象是打开中文 txt 后显示成问号或方块。原因通常是源码用CP_ACP读取,但文件实际是 UTF-8 编码,或者反过来。解决方法是统一编码:在打开和保存时都用CP_UTF8,并在文件开头写 BOM 或不写 BOM 保持一致。我一般会在FileManager里加一个DetectEncoding函数,先读前三个字节判断有没有 BOM。
5.2 链接错误 LNK2019:未解析的外部符号
现象是编译通过但链接失败,提示WinMain或CreateWindowExW找不到。原因是字符集不匹配或库没链接。如果错误里带W后缀,说明代码用了宽字符版本,但项目字符集设成了多字节。解决是把项目属性改成“使用 Unicode 字符集”,或者在代码里显式用CreateWindowExA和char字符串。
5.3 编辑框不显示或无法输入
现象是窗口出来了,但编辑区一片空白,打字没反应。原因可能是WM_CREATE里CreateWindowEx返回了 NULL,通常是窗口类名写错或hInstance传错。另一个可能是WM_SETFOCUS没把焦点交给编辑框。解决是在WM_CREATE后加SetFocus(hEdit),并检查CreateWindowEx的返回值,失败时用GetLastError看错误码。
5.4 菜单点击没反应
现象是菜单能弹出,但点了“打开”没动静。原因是WM_COMMAND里LOWORD(wParam)的 ID 和资源文件里定义的 ID 对不上。解决是打开resource.h或.rc文件,核对ID_FILE_OPEN的值,确保switch里的 case 和它一致。如果用了加速键表,还要检查TranslateAccelerator有没有在消息循环里调用。
5.5 程序退出后进程残留
现象是关闭窗口后任务管理器里还有进程。原因是WM_DESTROY里漏了PostQuitMessage(0),或者有子线程没退出。解决是确认WM_DESTROY分支存在,并且所有CreateThread或std::thread都有对应的退出条件。记事本一般没有多线程,这个问题多半是消息循环写成了while(1)而不是while(GetMessage(...))。
6. 二次开发改造:从记事本到自己的小工具
6.1 加一个“最近打开文件”列表
改造思路是在FileManager里维护一个std::vector<std::wstring>,每次成功打开或保存后把路径插到最前面,去重后保留最近 10 条。菜单里加一个ID_FILE_RECENT子菜单,在WM_INITMENUPOPUP里动态填充。关键代码是:
// 在 WM_INITMENUPOPUP 中动态生成最近文件菜单 case WM_INITMENUPOPUP: { HMENU hMenu = (HMENU)wParam; if (GetMenuItemID(hMenu, 0) == ID_FILE_RECENT_BASE) { // 清空旧项 while (GetMenuItemCount(hMenu) > 0) DeleteMenu(hMenu, 0, MF_BYPOSITION); // 重新填充 for (size_t i = 0; i < g_recentFiles.size(); ++i) { AppendMenu(hMenu, MF_STRING, ID_FILE_RECENT_BASE + i, g_recentFiles[i].c_str()); } } break; }ID_FILE_RECENT_BASE是一个起始 ID,比如 5000,每个文件项用5000 + i区分。点击后在WM_COMMAND里判断wParam是否落在[5000, 5000 + n)区间,取出对应路径再调用打开逻辑。这个改造能让你理解动态菜单和 ID 区间管理。
6.2 换成 Qt 版本需要改什么
如果想把 Win32 版本重写成 Qt,核心是把WndProc里的switch拆成一个个槽函数,把CreateWindowEx换成new QTextEdit,把GetOpenFileName换成QFileDialog::getOpenFileName。信号槽的连接写在MainWindow构造函数里:
// Qt 版本的主窗口构造 MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { textEdit = new QTextEdit(this); setCentralWidget(textEdit); QMenu* fileMenu = menuBar()->addMenu(tr("文件")); QAction* openAct = fileMenu->addAction(tr("打开")); connect(openAct, &QAction::triggered, this, &MainWindow::openFile); resize(800, 600); } void MainWindow::openFile() { QString path = QFileDialog::getOpenFileName(this, tr("打开文件")); if (path.isEmpty()) return; QFile file(path); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { textEdit->setPlainText(QString::fromUtf8(file.readAll())); } }Qt 版本的好处是编码处理简单,QString::fromUtf8直接搞定;坏处是打包体积大,需要带 Qt 运行库。我一般会先用 Win32 版本理解消息机制,再用 Qt 版本做实际工具,两者互补。
6.3 验证改造是否成功的三个方法
改完之后别急着说“好了”,按这三步验证:第一,功能回归,把新建、打开、保存、查找、替换、撤销各走一遍,看有没有改坏;第二,边界测试,打开一个 10MB 的大文件,看会不会卡死或内存暴涨;第三,异常测试,打开一个不存在的文件、一个没有权限的目录,看错误提示是否友好。我习惯在每次改完FileManager后,强制用 UTF-8 和 GBK 两种编码各存一次再打开,确认没有乱码才收工。
6.4 一个具体技巧:用资源文件管理菜单和加速键
很多新手会把菜单用代码一行行AppendMenu拼出来,改起来很痛苦。更好的做法是在.rc文件里定义菜单和加速键表:
// resource.rc - 菜单和加速键定义 IDR_MAINMENU MENU BEGIN POPUP "文件(&F)" BEGIN MENUITEM "新建(&N)\tCtrl+N", ID_FILE_NEW MENUITEM "打开(&O)...\tCtrl+O", ID_FILE_OPEN MENUITEM "保存(&S)\tCtrl+S", ID_FILE_SAVE MENUITEM SEPARATOR MENUITEM "退出(&X)", ID_FILE_EXIT END POPUP "编辑(&E)" BEGIN MENUITEM "撤销(&U)\tCtrl+Z", ID_EDIT_UNDO MENUITEM "查找(&F)...\tCtrl+F", ID_EDIT_FIND END END IDR_MAINACCEL ACCELERATORS BEGIN "N", ID_FILE_NEW, VIRTKEY, CONTROL "O", ID_FILE_OPEN, VIRTKEY, CONTROL "S", ID_FILE_SAVE, VIRTKEY, CONTROL "Z", ID_EDIT_UNDO, VIRTKEY, CONTROL "F", ID_EDIT_FIND, VIRTKEY, CONTROL END然后在WinMain里用LoadMenu和LoadAccelerators加载,消息循环里加TranslateAccelerator。这样改快捷键不用动 C++ 代码,只改.rc文件重新编译就行。从那以后我每次做 Win32 小工具,都强制把菜单和加速键放进资源文件,不再用代码硬拼——这个习惯帮我省了很多返工时间。希望帮到你。
本文还有配套的精品资源,点击获取