ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++入门练手首选:从控制台到Win32图形界面的记事本完整开发实战

C++入门练手首选:从控制台到Win32图形界面的记事本完整开发实战 简介一套使用C语言实现的记事本程序完整源码面向刚接触GUI编程或想巩固C语法的学习者通过实际项目掌握文本编辑器的核心逻辑。压缩包共21个文件包含6个头文件、5个C源文件以及资源脚本、图标、工程配置等辅助文件头文件与源文件相互对应便于对照学习类声明与实现.rc资源文件则体现菜单和对话框的设计思路另附已编译的可执行程序可直接体验运行效果。资源包整体64KB轻量紧凑适合边读边改。目前已有480人学习浏览源码涵盖MFC窗口搭建、消息映射、文件打开与保存等关键知识点并拆分了关于对话框、工具栏等独立模块有助于理解C桌面应用从工程配置到界面交互的完整流程。通过分析其中的类分工和文件I/O实现还能掌握记事本类程序常见功能的编码方式具备较好的参考价值。 我这些年断断续续带过不少人入门C也帮很多读者改过项目如果让我挑一个最适合拿来练手的项目记事本绝对排在推荐榜前三。相比烂大街的学生管理系统、贪吃蛇记事本这个项目有几个天然优势它足够小一个周末就能写完它又足够完整文件读写、字符串处理、界面交互、状态管理全都要碰一遍最关键的是写完以后你手里就有一套能天天用的工具那种成就感是交完作业就扔的Demo给不了的。不过这里要先说清楚一个容易踩坑的问题网上搜C 记事本源代码会出来两种完全不同的东西。一种是纯控制台程序用标准库的iostream和string在黑色窗口里逐行输入输出另一种是用Win32 API或者Qt、MFC这类框架做的图形界面程序带菜单栏、工具栏、文本框长得和Windows自带的记事本几乎一模一样。这两种路线的技术栈差别很大学习目标也不一样。我建议两条路都走一遍先控制台版搞清楚核心逻辑再上Win32版把界面这层补齐这样你对C的全貌会有更实在的理解。下面我就按这个思路把整个项目的设计过程、核心代码和踩坑记录都拆开讲清楚。1. 记事本项目到底能练什么先搞明白学习目标再动手很多人一上来就急着找源码、抄代码文件夹里存了十几个版本的记事本项目最后连自己写的那版都跑不起来。这个问题的根源不是代码能力不够而是没想清楚写这个项目到底要练什么。我建议你在动手前先做一个清单把C的各个知识点和记事本的各个功能模块对应起来这样每写一段代码你都知道自己在练什么。拿记事本的功能来拆解功能模块涉及的C核心知识学习价值等级文本存储与追加string/vector/动态内存高几乎所有项目都用得到按行拆分文本string流、find/substr高锻炼字符串处理能力打开/保存文件fstream、文件流错误处理高I/O是基本功简单查找/替换字符串匹配、循环边界中算法思维入门统计字数/行数容器遍历、计数逻辑低但能快速获得正反馈命令行参数打开文件main函数参数、路径解析中了解操作系统接口把这张表格放在手边每写完一个功能就打个勾你会明显感觉到自己对C的控制力在上升。还有一个重要的认知记事本项目的核心不是把界面做得多漂亮而是文本数据的组织方式。你用什么数据结构来存每一行文字是vectorstring还是liststring删除一行、插入一行的代价分别是多少这些思考会直接影响你对STL容器的理解深度这个收获比你抄十遍源码都值钱。从环境上说Windows下用Visual Studio最省心官方社区版免费新建项目时选控制台应用或Windows桌面应用程序模板编译器、调试器、资源编辑器全给你配好了。如果想用VS Code写需要自己装MinGW或者MSVC工具链还要配置launch.json和tasks.json对于新手来说这一步本身就能劝退很多人。我的建议是先别折腾编辑器用最顺手的工具把语法和逻辑搞懂配置的事情等需要的时候再说。2. 控制台版记事本一个晚上就能写完的最小可用原型纯控制台版记事本虽然看起来很简陋但它的价值在于剥离了GUI的复杂性让你能把全部注意力放在文本处理和程序流程上。我把它定位成原型验证而不是最终成品。核心设计思路是做一个命令行交互式编辑器。启动后显示一个交互菜单支持新建、打开、显示、追加一行、删除一行、保存、退出这些基本操作。界面大概是这样的 迷你记事本 1. 新建文档 2. 打开文件 3. 查看内容 4. 追加一行 5. 删除一行 6. 保存到文件 7. 退出 请选择操作:这个界面的本质是一个while switch的命令循环。整个程序的状态就是一个vectorstring每个元素代表一行文本。选择对应的数字就执行对应的操作执行完以后回到菜单直到用户选择退出。文本存储直接用vectorstring就够了。这里有一个好习惯写一个MyNotepad类把数据和方法封装起来而不是全部堆在main函数里。类的大致骨架是这样的#include iostream #include fstream #include string #include vector #include algorithm class MyNotepad { private: std::vectorstd::string lines; // 每个元素存一行 std::string filename; // 当前文件名 public: void newFile() { lines.clear(); filename.clear(); std::cout 已新建空白文档。\n; } bool openFile(const std::string path) { std::ifstream fin(path); if (!fin) { std::cout 打开失败请检查文件路径。\n; return false; } lines.clear(); std::string line; while (std::getline(fin, line)) { lines.push_back(line); } filename path; std::cout 已打开: path 共 lines.size() 行。\n; return true; } bool saveFile(const std::string path) { std::ofstream fout(path); if (!fout) { std::cout 保存失败请检查目录权限。\n; return false; } for (size_t i 0; i lines.size(); i) { fout lines[i]; if (i 1 lines.size()) fout \n; // 避免文件末尾多一行空行 } filename path; std::cout 已保存: path \n; return true; } void showAll() const { if (lines.empty()) { std::cout 当前文档是空的。\n; return; } for (size_t i 0; i lines.size(); i) { std::cout i 1 : lines[i] \n; } std::cout 共 lines.size() 行。\n; } void appendLine(const std::string text) { lines.push_back(text); } bool deleteLine(size_t index) { if (index 0 || index lines.size()) { std::cout 行号越界删除失败。\n; return false; } lines.erase(lines.begin() index - 1); // 用户看到的行号从1开始 return true; } };这段代码里最需要留意的几个细节第一getline读出来的字符串不带换行符。你用std::getline(fin, line)读文件时每行的末尾换行会被丢弃所以后面的vectorstring里存的每一行都是干净的文本。保存的时候再把换行符加回去一行一行写出来。如果你用fin line这种形式会按空白字符分词遇到空格就断开完全不是记事本该有的行为。第二保存时末尾不要多写一个换行符。我上面的写法是判断i 1 lines.size()才追加\n这样保存出来的文件和记事本打开的效果完全一致。如果无脑在每个元素后面都加换行文件的最后会多出一个空行看起来很业余。第三文件路径里的编码问题。Windows的控制台默认代码页是GBK中文系统或者UTF-8新版系统可切换。如果你用中文字符串作为文件名去打开文件std::ifstream接收的是窄字符串在中文路径下可能出现乱码或者打不开的情况。这不是你代码写错了是Windows下窄字符串和宽字符串的经典坑。控制台版可以先不管这个等到Win32版我们统一用宽字符解决。主程序的命令循环也非常直观int main() { MyNotepad notepad; bool running true; int choice 0; while (running) { std::cout \n 迷你记事本 \n 1. 新建文档\n 2. 打开文件\n 3. 查看内容\n 4. 追加一行\n 5. 删除一行\n 6. 保存到文件\n 7. 退出\n 请选择操作: ; std::cin choice; std::cin.ignore(); // 吃掉回车否则会影响后面的 getline switch (choice) { case 1: notepad.newFile(); break; case 2: { std::string path; std::cout 请输入文件路径: ; std::getline(std::cin, path); notepad.openFile(path); break; } case 3: notepad.showAll(); break; case 4: { std::string text; std::cout 请输入要追加的内容: ; std::getline(std::cin, text); notepad.appendLine(text); break; } case 5: { size_t idx; std::cout 请输入要删除的行号: ; std::cin idx; notepad.deleteLine(idx); break; } case 6: { std::string path; std::cout 请输入保存路径: ; std::getline(std::cin, path); notepad.saveFile(path); break; } case 7: running false; break; default: std::cout 无效选项请重试。\n; } } return 0; }这里有一个特别容易坑到新手的点std::cin choice读完整数以后回车符还残留在输入缓冲区里。接下来如果你直接用std::getline去读路径第一行读到的会是一个空字符串——因为getline把那个残留的回车消费掉了。解决方案就是我在代码里写的std::cin.ignore();把缓冲区里的回车清掉。这个控制台版你一个晚上就能写完写完以后可以顺手扩展几个小功能按行号插入文本、清空文档、统计字符数。每加一个功能都会加深你对vector操作insert、erase、iteration的理解这个练习比刷十道算法题都来得扎实。3. 升级到Win32图形版窗口、消息循环和多行文本框的使用控制台版跑通以后就可以上Win32 API做真正的图形界面记事本了。为什么选Win32而不是Qt或者MFC因为Win32是Windows平台的底层接口你亲手注册窗口类、写消息循环、处理控件消息才能真正理解GUI程序运行的本质。有了这个底子以后再去学Qt、wxWidgets会非常快因为你知道那些框架在你背后都替你干了什么。先解释一下Win32程序的基本运行模型这个必须理解清楚否则你会一直处于抄代码能跑但改了不会调的状态。Win32 GUI程序的核心是消息驱动。程序启动后做的第一件事是注册窗口类然后创建主窗口接着进入一个叫GetMessage或UpdateWindow的循环。这个循环不断地从系统消息队列里取消息分发给窗口过程函数WndProc处理。所谓消息就是WM_COMMAND用户点了菜单、WM_PAINT窗口需要重绘、WM_SIZE窗口大小变了、WM_CLOSE用户点了关闭按钮这些。你的程序逻辑就分布在这些消息的处理分支里。多行文本框是Win32记事本的核心控件类名叫EDIT创建时加上ES_MULTILINE样式就变成多行文本编辑区。注意它的行为细节默认情况下EDIT控件不会自动换行需要加ES_AUTOVSCROLL和ES_AUTOHSCROLL来决定滚动行为。还有一个隐藏坑是如果你不加WS_VSCROLL和WS_HSCROLL样式即使内容超过了窗口范围也不会有滚动条出现。创建一个多行文本框的窗口代码大概是这样HWND hEdit CreateWindowExW( WS_EX_CLIENTEDGE, // 扩展样式使控件带边框 LEDIT, // 窗口类名 L, // 初始内容 WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL, 0, 0, 0, 0, // 初始位置和尺寸稍后在WM_SIZE里调整 hWnd, // 父窗口句柄 nullptr, // 菜单句柄这里不用 hInstance, // 应用实例句柄 nullptr );把文本框建立好以后程序的基本操作就变成了对文本框发送消息。比如读取内容用GetWindowTextW设置内容用SetWindowTextW选中全部用SendMessage(hEdit, EM_SETSEL, 0, -1, 0)。这些API配合起来整个记事本的读写逻辑就能搭起来了。菜单栏用LoadMenuW从资源里加载或者在代码里用CreateMenu和AppendMenuW动态创建。我建议前期先用动态创建的方式省得碰资源编译器。比如这样HMENU hMenuBar CreateMenu(); HMENU hFileMenu CreatePopupMenu(); AppendMenuW(hFileMenu, MF_STRING, ID_FILE_NEW, L新建(N)\tCtrlN); AppendMenuW(hFileMenu, MF_STRING, ID_FILE_OPEN, L打开(O)...\tCtrlO); AppendMenuW(hFileMenu, MF_SEPARATOR, 0, nullptr); AppendMenuW(hFileMenu, MF_STRING, ID_FILE_EXIT, L退出(X)); AppendMenuW(hMenuBar, MF_POPUP, (UINT_PTR)hFileMenu, L文件(F)); SetMenu(hWnd, hMenuBar);有了菜单以后在WndProc的WM_COMMAND分支里判断菜单ID分别处理打开、保存、新建逻辑。注意菜单ID要用#define定义成常量否则一堆魔法数字看代码会很痛苦。快捷键的实现也是通过菜单ID把菜单项文字里带上\tCtrlN只是显示效果真正的快捷键逻辑要靠TranslateAccelerator函数和加速键表。这个可以放到进阶阶段再搞前期能点菜单操作就行。4. 文件打开与保存的完整实现从对话框到编码处理的实战细节Win32记事本的打开和保存功能核心是两个APIGetOpenFileNameW和GetSaveFileNameW它们会弹出一个系统标准的文件选择对话框。这两个API的使用套路一样先初始化一个OPENFILENAMEW结构体设置父窗口句柄、文件类型过滤器、初始目录然后传一个wchar_t缓冲区接收用户选择的路径。一个常见错误是缓冲区大小没算好导致路径截断。我习惯用MAX_PATH260来定义缓冲区但现代Windows路径可以超过这个长度。稳妥的做法是把缓冲区设成32767这是Windows文件路径的实际上限。代码大概是wchar_t szFile[32768] L; OPENFILENAMEW ofn { sizeof(ofn) }; ofn.hwndOwner hWnd; ofn.lpstrFilter L文本文件 (*.txt)\0*.txt\0所有文件 (*.*)\0*.*\0; ofn.lpstrFile szFile; ofn.nMaxFile 32768; ofn.Flags OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST; ofn.lpstrDefExt Ltxt; if (GetOpenFileNameW(ofn)) { // 此时 szFile 里就是要打开的文件路径 }拿到路径后用CreateFileW配合ReadFile来读文件还是用C标准库的std::wifstream这个选择会直接影响你的编码处理复杂度。我的建议是在这个阶段直接用Windows API的ReadFile把整个文件读取到内存然后调用MultiByteToWideChar做编码转换再SetWindowTextW设置到文本框。为什么不能直接std::wifstream因为宽字符流的编码完全依赖系统的locale设置很不稳定。你在一台机器上正常换到另一台可能全部乱码。手写MultiByteToWideChar虽然代码长一点但你能精确控制编码行为这是正经做Windows桌面应用必须掌握的技能。保存的逻辑反过来先GetWindowTextW拿到文本框的宽字符内容再用WideCharToMultiByte转成UTF-8或者ANSI最后WriteFile写到磁盘。这里有一个产品细节要想清楚你的记事本要保存成什么编码Windows自带的记事本在新版本上默认保存UTF-8老版本默认ANSI。UTF-8跨平台兼容性好中文和英文混排没问题ANSI在中文系统下其实就是GBK兼容老软件更好但跨平台会乱码。我建议你这个项目里做一个简单但完整的编码选择菜单另存为时让用户选是UTF-8还是ANSI。实现方式不复杂就是调用编码转换时传不同的CodePage参数。这样写出来的记事本工具比Windows自带版本还灵活一点。打开文件时还有一个边界情况要处理好文件不存在、权限不足、文件被其他程序占用。很多新手用OpenFile或CreateFileW时不检查返回值后面一操作就崩溃。正确做法是拿到句柄后马上判断是不是INVALID_HANDLE_VALUE是的话用GetLastError拿到错误码弹一个友好的提示框。不要直接往崩溃的方向走。5. 剪贴板、查找替换和状态栏这三个细节决定了工具的完成度文本编辑器能打字能保存只能算一个Demo。真正让一个记事本像那么回事需要补齐一组基础交互——剪切/复制/粘贴、查找/替换、以及状态栏信息。这些功能的复杂程度不高但每一个都有一套固定的API用法非常值得亲手写一遍。剪贴板操作在Win32里有两种方式。一种是通过SendMessage直接给EDIT控件发WM_CUT、WM_COPY、WM_PASTE消息让控件自己去处理剪贴板。这是最省事的方式case ID_EDIT_COPY: SendMessage(hEdit, WM_COPY, 0, 0); break;另一种方式是直接操作Windows剪贴板先OpenClipboard再EmptyClipboard然后SetClipboardData放入数据最后CloseClipboard。这种方式适合你需要处理自定义格式数据的情况比如把选中文本转成HTML复制出去。对于记事本项目直接用控件消息就够了代码简洁还不用担心格式兼容。但这里有一个交互上的坑如果文本框里没有选中内容WM_CUT和WM_COPY不应该被触发否则用户会看到什么都没有复制的诡异现象。你需要维护一个当前是否选中文本的标志位在文本框的EN_SELCHANGE通知里实时更新然后在菜单判断EnableMenuItem。这个细节直接决定了你的记事本是否有专业感。查找替换的实现Win32控件本身提供了一套内置消息EM_FINDTEXT、EM_REPLACESEL。不过需要注意的是EM_FINDTEXT要求传入一个FINDTEXTEXW结构里面包含查找范围和查找字符串。如果你要支持从光标处向后查找区分大小写这些功能需要自己写一个查找对话框收集用户的输入然后向文本控件发送EM_FINDTEXTEXW消息。FINDTEXTEXW ft {0}; ft.chrg.cpMin searchFrom; // 从哪个字符位置开始 ft.chrg.cpMax editTextLength; // 到哪里结束 ft.lpstrText searchText; // 要找什么 LRESULT result SendMessageW(hEdit, EM_FINDTEXTEXW, FR_DOWN, (LPARAM)ft); if (result ! -1) { // 找到了结果在 ft.chrgText 里把这段文本选中 SendMessageW(hEdit, EM_SETSEL, ft.chrgText.cpMin, ft.chrgText.cpMax); }要注意EM_FINDTEXTEXW的返回值找到返回匹配起始位置的下标没找到返回-1。这是一个特别容易误判的地方很多新手以为返回0就是没找到结果文本开头匹配的时候永远认为没找到排查半天才发现是返回值判断错了。状态栏的实现通常是用一个STATUS类窗口CreateStatusWindow函数或者直接在主窗口底部画一个静态文本。最简单的做法是用CreateWindowExW再创建一个STATIC控件在WM_SIZE消息里调整它的位置让它固定在窗口底部。内容上显示行数、列数、字符数用SetWindowTextW更新。光标位置的变化来自文本框的EN_SELCHANGE通知里面有CHARRANGE结构体cpMin就是当前光标的位置通过计算该位置前面有几个换行符就能得到行号和列号。状态栏这一块做起来不难但很能锻炼你对控件尺寸和父窗口联动的理解。窗口大小一变编辑区和状态栏的尺寸都要跟着重新计算这里用GetClientRect拿到客户区大小然后各控件按自己占据的区域MoveWindow就对了。6. 我踩过的一些坑中文乱码、控件焦点和消息循环崩溃写一个记事本项目看似功能简单但真正把它跑稳会踩到不少坑。我把自己经历过的和帮别人排查过的问题归一下类列成清单你写的时候可以提前避开。第一类中文字符乱码。这是Win32记事本项目里最经典的问题十个人有九个会遇上。乱码的根源只有一个宽字符和窄字符混用。Windows GUI API的底层从NT时代起就是UnicodeUTF-16所有的窗口标题、控件文本、消息内容都是宽字符。但C标准库的fstream、std::cout默认走的是系统ANSI代码页。当你把窄字符串直接传给SetWindowTextW或者把GetWindowTextW读出来的宽字符串用输出到流里就会产生编码不匹配表现出来就是乱码。解决方案也很简单统一用宽字符版本的一切。项目的入口用wWinMain而不是main字符串字面量加L前缀文件操作统一用std::wifstream/std::wofstream一切和Windows API打交道的函数都选带W后缀的版本。这样做以后乱码基本能消灭九成。剩下的一成是代码页转换问题使用MultiByteToWideChar和WideCharToMultiByte时要指定正确的CodePage参数。第二类控件失去焦点导致快捷键失效。很多人做快捷键会直接在WndProc里处理WM_KEYDOWN但忘了当前焦点可能不在主窗口上按下CtrlS时事件根本到不了你的窗口。解决方法是使用加速键表Accelerator Table把快捷键和菜单ID绑定在消息循环里用TranslateAccelerator先拦截键盘消息。这样无论焦点在哪个控件上快捷键都能生效。这是Windows编程里一个很标准的模式记事本、画图、各种编辑器都这么干。第三类消息循环里操作窗口句柄时崩溃。菜单栏里的打开保存触发文件对话框时建议用模态对话框它会自动阻塞你的消息循环用户不关掉对话框就不能操作主窗口。有的新手在这里图省事直接在WM_COMMAND处理里写了一个循环等待用户输入。这个做法看起来没毛病实际上会让主窗口进入假死状态系统会给你弹程序无响应的提示。正确的做法是使用DialogBoxParamW或者CreateDialogParamW创建模态对话框让Windows自己去跑对话框的消息循环。第四类文件拖放功能。如果你想让用户把文件直接拖到窗口上打开需要在CreateWindowExW时加上WS_EX_ACCEPTFILES扩展样式然后处理WM_DROPFILES消息。这个功能我自己加过的次数太多了几乎每个写记事本的人最后都会提这个需求。DragQueryFileW获取文件名列表注意取完以后要用DragFinish释放资源否则内存会一直泄漏。这个坑也很常见Windows文档里有写但很容易被忽略。7. 写完之后怎么扩展撤销重做、语法高亮和多标签页一个基础版记事本写完程序能跑、能存、能查你已经达到中级C入门者的水平了。但如果想让这个项目真正体现你的能力或者作为求职作品展示我建议在它基础上再扩展三到四个进阶功能。每个功能都对应一个核心知识点做完一个你就上升一个台阶。功能一撤销/重做。这是一个数据结构的经典应用题。最常规的做法是维护两个操作栈一个是撤销栈一个是重做栈。每次编辑操作输入字符、删除、替换都把操作前的状态压入撤销栈重做时从撤销栈弹出来再压入重做栈这样撤销和重做就能互相切换。对于文本编辑器来说更实用的方案是存储操作的逆操作而不是存储整个文件快照——文件可能几十MB存快照内存很快就爆了。例如用户在位置100插入了abc那么逆操作就是删除位置100的3个字符。这个设计思路在各类编辑器、Drawing软件里都是通用的。功能二语法高亮。这个功能看起来高级本质是规划一个着色渲染机制。Win32自带的EDIT控件不具备高亮能力你需要换用RichEdit控件RICHEDIT类它为每个字符范围提供EM_SETCHARFORMAT消息来设置颜色和字体。实现时你的渲染流程是把全文拆成词法单元关键字、字符串、注释、普通文本然后给每个单元发格式消息。这个功能会逼着你学习词法分析虽然只是正则表达式级别的匹配但理解完以后编译器前端是怎么回事你心里就有底了。我当初做这个功能时顺带把TinyXML这种开源项目读了一遍收获比单看教科书大得多。功能三多标签页。当你打开多个文件时单窗口单编辑区就不够用了。Win32里做标签页有成熟的方案就是用TabControl控件类名SysTabControl32配合多个编辑区窗口。关键逻辑是切换标签时把当前编辑区的可见性关闭把目标标签对应的编辑区显示出来同时从逻辑上记住哪个文件对应哪个编辑句柄。这些映射关系用一个std::mapHWND, FileInfo就能搞定。写完以后再回头看你会明白为什么现代编辑器Qt Creator、VS Code动辄就是几个GB——它们的核心骨架也不过是这种标签面板的组合。功能四CtrlS即时保存。很多人在写项目时会把快捷键遗落到最后但其实这是每个软件的基本素质。做好加速键表之后你会发现保存这个动作变得特别自然用户按一下CtrlS文件就默默存好了不需要弹任何消息框。商品软件的用户体验就是一个个这样的细节堆起来的你的记事本地板和Windows记事本的差距也就在这些细节里。项目做到这个程度你就不是会写C语法的初学者了而是能用C完成一个完整产品的人。这两者之间的差别只有亲手把一个项目从空文件写到能用才能真正体会到。本文还有配套的精品资源点击获取
返回列表