
简介这是一份用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 LNotepadClass; wc.hCursor LoadCursor(NULL, IDC_IBEAM); wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); RegisterClassEx(wc); // 2. 创建主窗口编辑控件在 WM_CREATE 里再建 HWND hWnd CreateWindowEx(0, LNotepadClass, 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, LEDIT, 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)是控件或菜单 IDHIWORD(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 \ -stdc17-mwindows告诉链接器生成 GUI 子系统程序不加的话会弹出一个黑色控制台窗口。-lcomctl32是通用控件库-lcomdlg32是通用对话框库。如果源码里用了std::filesystemMinGW 版本要够新否则得加-lstdcfs。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看中文是否乱码另存为后重新打开看内容是否一致按 CtrlZ 看撤销是否生效拖动窗口边缘改变大小看编辑区是否跟着缩放。这几步能覆盖消息循环、文件读写、控件布局三条主链路。如果某一步失败基本能定位到对应模块。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::vectorstd::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)\tCtrlN, ID_FILE_NEW MENUITEM 打开(O)...\tCtrlO, ID_FILE_OPEN MENUITEM 保存(S)\tCtrlS, ID_FILE_SAVE MENUITEM SEPARATOR MENUITEM 退出(X), ID_FILE_EXIT END POPUP 编辑(E) BEGIN MENUITEM 撤销(U)\tCtrlZ, ID_EDIT_UNDO MENUITEM 查找(F)...\tCtrlF, 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 小工具都强制把菜单和加速键放进资源文件不再用代码硬拼——这个习惯帮我省了很多返工时间。希望帮到你。本文还有配套的精品资源点击获取