
1. 项目概述为什么选择Qt Creator打造记事本如果你刚接触C图形界面开发或者厌倦了控制台的黑白世界想快速做出一个能看能用的桌面软件那么用Qt Creator在半小时内做个记事本绝对是性价比最高的入门项目。这听起来像是个“玩具”但别小看它。一个记事本麻雀虽小五脏俱全它涉及窗口创建、菜单栏、工具栏、文本编辑、文件读写、快捷键绑定、状态栏提示等几乎所有桌面应用的核心模块。通过这个项目你能把Qt里最常用、最核心的部件Widgets和信号槽机制Signals Slots亲手摸一遍这种“从零到一”的完整感是看十遍教程都比不上的。网上关于Qt Creator和C的资料很多但要么是庞杂的官方文档让人望而生畏要么是零散的代码片段不成体系。我这个教程的目标很明确在30分钟内给你一个功能完整、代码清晰、可以直接编译运行的极简记事本程序。我会把每一步操作、每一行关键代码的意图都讲清楚让你不仅能把程序跑起来更能理解背后的逻辑。最终你会得到一个具备新建、打开、保存、另存为、退出等基本功能的记事本并且附上完整的、带详细注释的C源码你可以基于它进行任何扩展比如添加字体设置、查找替换甚至做成你自己的代码编辑器。2. 环境准备与项目创建第一步就走对工欲善其事必先利其器。第一步走对了后面就顺了。2.1 Qt Creator的安装与选择首先你需要Qt Creator。这里有个关键选择不要只下载Qt Creator而是要下载带有Qt库的完整安装包。Qt Creator只是一个集成开发环境IDE而Qt库才是我们写程序时真正调用的“武器库”。去哪里下直接访问Qt官网的下载页面。对于新手我强烈推荐选择开源版本。在安装时你会看到一个组件选择界面。这里务必勾选与你编译器匹配的Qt库比如Qt 6.5.3 MinGW 64-bit以及Qt Creator。MinGW是一个Windows下的GCC编译器套件对初学者最友好免去了配置MSVC的繁琐。版本选择新手建议选择最新的长期支持版本LTS如Qt 6.2 LTS或6.5 LTS。它们更稳定社区资源也更丰富。不用担心C标准Qt对现代C的支持很好。安装注意安装路径不要有中文和空格用默认的或简单的英文路径比如C:\Qt能避免99%的奇怪路径问题。安装完成后打开Qt Creator它的界面比较清爽。中间是代码编辑区左边是项目文件管理和打开的文件列表下面是编译输出和问题窗口。第一次使用它可能会让你配置一个Kit套件通常它会自动检测到你安装的Qt版本和编译器直接确认即可。2.2 创建你的第一个Qt Widgets项目打开Qt Creator点击左上角的“文件” - “新建文件或项目”。选择模板在弹出的窗口中选择Application-Qt Widgets Application然后点击“选择...”。这是创建带图形界面程序的标准模板。项目设置给项目起个名字比如SimpleNotepad。选择你的项目存放路径同样避免中文。在“构建系统”这一步选择qmake。qmake是Qt传统的项目构建工具简单直接适合我们这个入门项目。CMake更强大但配置稍复杂我们可以后续再学。类信息接下来会让你设置主窗口的类名。默认会生成一个MainWindow类我们保持默认即可。基类选择QMainWindow。QMainWindow是主窗口类天然提供了菜单栏、工具栏、状态栏和中心部件的布局框架正是我们做记事本所需要的。翻译文件关于翻译文件可以先取消勾选。我们的极简记事本暂时不需要多语言支持。套件选择最后一步选择你安装的Qt版本套件如 Desktop Qt 6.5.3 MinGW 64-bit然后点击“完成”。项目创建成功后你会在左侧的项目文件列表中看到几个核心文件SimpleNotepad.proqmake的项目配置文件管理着项目需要哪些模块、哪些源文件。main.cpp程序的入口创建并显示主窗口。mainwindow.h/mainwindow.cpp主窗口类的头文件和源文件我们大部分代码将在这里编写。mainwindow.ui这是一个XML格式的UI设计文件。你可以通过双击它以“所见即所得”的方式拖拽控件来设计界面。注意很多教程会一上来就教你在.ui文件里拖控件但对于理解Qt的代码结构我建议初学者先从纯代码开始。理解了代码如何创建界面后再用UI设计器来提高效率这样根基更牢。因此本教程我们将主要编辑.h和.cpp文件。3. 核心界面构建从空窗口到记事本雏形现在我们有了一个空白的窗口。让我们用代码把它“装修”成一个记事本的样子。3.1 设计主窗口布局与核心部件首先打开mainwindow.h文件。我们需要在MainWindow类中声明我们将要使用的界面部件。在private:区域下添加以下成员变量声明#ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow // 前置声明避免在头文件中包含大量头文件 class QTextEdit; class QAction; class QMenu; class QToolBar; class MainWindow : public QMainWindow { Q_OBJECT // 所有Qt窗口类都必须有的宏用于支持信号槽机制 public: MainWindow(QWidget *parent nullptr); ~MainWindow(); private: // 核心文本编辑区域 QTextEdit *textEdit; // 菜单栏动作Action QAction *newAct; QAction *openAct; QAction *saveAct; QAction *saveAsAct; QAction *exitAct; // 菜单栏 QMenu *fileMenu; // 工具栏 QToolBar *fileToolBar; // 私有函数用于初始化界面和连接信号槽 void createActions(); void createMenus(); void createToolBars(); void setupUI(); }; #endif // MAINWINDOW_H为什么这么设计QTextEdit这是Qt中功能强大的富文本编辑控件我们用它作为记事本的编辑区域。它自带滚动条、支持基本的文本格式完全满足需求。QAction动作。在Qt中一个功能如“新建”可以被同时添加到菜单项、工具栏按钮甚至快捷键上。QAction就是这个功能的抽象。我们创建“新建”、“打开”等动作然后分别把它们放到菜单和工具栏里。QMenu和QToolBar分别对应菜单栏和工具栏。QMainWindow已经为我们预留了放置它们的位置。3.2 实现界面初始化与部件创建接下来打开mainwindow.cpp文件开始实现构造函数和各个初始化函数。首先在文件顶部包含必要的头文件#include “mainwindow.h” #include QTextEdit #include QAction #include QMenu #include QToolBar #include QMessageBox #include QFileDialog #include QFile #include QTextStream #include QStatusBar现在实现构造函数和setupUI函数MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , textEdit(new QTextEdit(this)) // 初始化文本编辑框并指定父对象为this主窗口 { // 设置窗口标题和初始大小 setWindowTitle(tr(“简易记事本 - 未命名”)); // tr()函数用于后续国际化翻译养成好习惯 resize(800, 600); // 将QTextEdit设置为中心部件 setCentralWidget(textEdit); // 调用初始化函数 createActions(); createMenus(); createToolBars(); } MainWindow::~MainWindow() { // Qt的对象树机制会自动管理子对象内存这里通常无需手动释放 }关键点解析setCentralWidget(textEdit)这是QMainWindow的核心函数。它将textEdit这个部件设置为主窗口的中央区域使其占据除菜单栏、工具栏、状态栏之外的所有空间。new QTextEdit(this)在创建QTextEdit对象时将this即MainWindow对象作为父对象传递。这是Qt内存管理的关键——父对象销毁时会自动销毁其所有子对象极大减少了内存泄漏的风险。接着我们实现createActions函数来创建各个动作void MainWindow::createActions() { // 新建文件动作 newAct new QAction(tr(“新建”), this); // N 定义快捷键AltN newAct-setShortcut(QKeySequence::New); // 设置标准快捷键 CtrlN newAct-setStatusTip(tr(“创建一个新文件”)); // 鼠标悬停时状态栏提示 // 暂时不连接信号槽后续实现功能时再连接 // connect(newAct, QAction::triggered, this, MainWindow::newFile); // 打开文件动作 openAct new QAction(tr(“打开…”), this); openAct-setShortcut(QKeySequence::Open); openAct-setStatusTip(tr(“打开一个已存在的文件”)); // 保存文件动作 saveAct new QAction(tr(“保存”), this); saveAct-setShortcut(QKeySequence::Save); saveAct-setStatusTip(tr(“保存当前文件到磁盘”)); // 另存为动作 saveAsAct new QAction(tr(“另存为…”), this); saveAsAct-setShortcut(QKeySequence::SaveAs); saveAsAct-setStatusTip(tr(“将当前文件另存为新文件”)); // 退出动作 exitAct new QAction(tr(“退出”), this); exitAct-setShortcut(tr(“CtrlQ”)); // QKeySequence::Quit 在Windows上可能不是CtrlQ我们显式指定 exitAct-setStatusTip(tr(“退出应用程序”)); connect(exitAct, QAction::triggered, this, QWidget::close); // 连接到关闭窗口的槽函数 }动作设置详解tr(“新建”)tr()是Qt的翻译函数即使不做多语言也建议使用这是良好习惯。N表示在菜单显示时N字母会带下划线用户可以通过AltF打开文件菜单再按N来激活该动作。QKeySequence::New这是Qt定义的标准快捷键枚举。使用它而不是硬编码“CtrlN”的好处是Qt会根据不同操作系统平台自动适配比如在macOS上可能是CmdN。setStatusTip设置的状态提示文本会在用户将鼠标悬停在菜单项或工具栏按钮上时显示在主窗口底部的状态栏中。这是一个提升用户体验的小细节。然后实现createMenus和createToolBarsvoid MainWindow::createMenus() { // 获取主窗口自带的菜单栏并添加“文件”菜单 fileMenu menuBar()-addMenu(tr(“文件”)); fileMenu-addAction(newAct); fileMenu-addAction(openAct); fileMenu-addSeparator(); // 添加一个分隔线 fileMenu-addAction(saveAct); fileMenu-addAction(saveAsAct); fileMenu-addSeparator(); fileMenu-addAction(exitAct); } void MainWindow::createToolBars() { // 创建工具栏并添加常用动作 fileToolBar addToolBar(tr(“文件”)); fileToolBar-addAction(newAct); fileToolBar-addAction(openAct); fileToolBar-addAction(saveAct); // 工具栏通常不放“退出”按钮这里就不加了 }至此点击Qt Creator左下角的绿色三角形或按CtrlR编译并运行。你应该能看到一个带有菜单栏、工具栏和一个空白文本编辑区域的窗口了。菜单和工具栏的按钮暂时还点不了因为我们还没给它们连接具体的功能函数槽函数。4. 核心功能实现让记事本“活”起来界面有了现在我们来注入灵魂——实现文件操作功能。这涉及到Qt的另一个核心机制信号与槽。简单理解就是当一个事件发生如点击按钮发出triggered信号自动调用一个对应的处理函数槽函数。4.1 实现新建、打开与保存功能首先我们需要在mainwindow.h的类定义中private slots:区域如果没有就自己加一个声明我们的槽函数private slots: void newFile(); void openFile(); bool saveFile(); bool saveAsFile();同时我们需要一个成员变量来记录当前打开的文件路径private: // ... 之前声明的其他成员变量 QString currentFile; // 当前文件路径为空则表示是“未命名”的新文件然后在mainwindow.cpp中实现这些槽函数。我们先从newFile开始void MainWindow::newFile() { // 在新建前检查当前文档是否有未保存的修改 if (maybeSave()) { textEdit-clear(); // 清空文本编辑区 setCurrentFile(QString()); // 将当前文件设置为空未命名 } }这里调用了两个我们还没实现的函数maybeSave()和setCurrentFile()。maybeSave()用于在文档被修改且未保存时弹出提示框询问用户是否保存。setCurrentFile()是一个辅助函数用于更新currentFile变量和窗口标题。我们先实现maybeSave()和setCurrentFile()这两个辅助函数在mainwindow.cpp的类实现中添加为私有函数bool MainWindow::maybeSave() { // 如果文本被修改了 if (textEdit-document()-isModified()) { // 弹出标准询问对话框 QMessageBox::StandardButton ret; ret QMessageBox::warning(this, tr(“简易记事本”), tr(“文档已被修改。\n是否保存更改”), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); if (ret QMessageBox::Save) { return saveFile(); // 用户选择保存调用保存功能 } else if (ret QMessageBox::Cancel) { return false; // 用户选择取消返回false表示中断当前操作如新建、打开 } // 用户选择不保存继续执行 } return true; // 文档未修改或用户选择不保存返回true继续 } void MainWindow::setCurrentFile(const QString fileName) { currentFile fileName; // 根据文件名设置窗口标题 if (currentFile.isEmpty()) { setWindowTitle(tr(“简易记事本 - 未命名”)); textEdit-document()-setModified(false); // 新文件标记为未修改 } else { setWindowTitle(tr(“%1 - 简易记事本”).arg(QFileInfo(currentFile).fileName())); } }接下来实现openFile()void MainWindow::openFile() { // 打开前先询问是否保存当前修改 if (maybeSave()) { // 弹出文件选择对话框过滤文本文件 QString fileName QFileDialog::getOpenFileName(this, tr(“打开文件”), QDir::homePath(), // 默认从用户目录开始 tr(“文本文件 (*.txt);;所有文件 (*.*)”)); // 如果用户没有取消选择文件名非空 if (!fileName.isEmpty()) { loadFile(fileName); } } } bool MainWindow::loadFile(const QString fileName) { QFile file(fileName); // 以只读文本模式打开文件 if (!file.open(QFile::ReadOnly | QFile::Text)) { QMessageBox::warning(this, tr(“简易记事本”), tr(“无法打开文件 %1:\n%2.”) .arg(fileName) .arg(file.errorString())); return false; } // 使用QTextStream读取文件内容它自动处理编码默认UTF-8 QTextStream in(file); in.setCodec(“UTF-8”); // 显式设置为UTF-8编码确保中文不乱码 QApplication::setOverrideCursor(Qt::WaitCursor); // 将光标设置为等待状态 textEdit-setPlainText(in.readAll()); // 将文件全部内容读入文本编辑框 QApplication::restoreOverrideCursor(); // 恢复光标 setCurrentFile(fileName); // 更新当前文件信息 return true; }编码问题避坑文本文件编码是跨平台开发中一个常见坑。QTextStream默认使用系统本地编码在中文Windows上可能是GBK。为了更好的兼容性尤其是包含中文时我们显式调用setCodec(“UTF-8”)将其设置为UTF-8。保存时也要做同样处理。这是处理中文不乱码的关键一步。现在实现saveFile()和saveAsFile()bool MainWindow::saveFile() { // 如果当前文件没有路径即未命名的新文件则执行“另存为”流程 if (currentFile.isEmpty()) { return saveAsFile(); } else { return saveFileToDisk(currentFile); } } bool MainWindow::saveAsFile() { // 弹出“另存为”对话框 QString fileName QFileDialog::getSaveFileName(this, tr(“另存为”), QDir::homePath(), tr(“文本文件 (*.txt);;所有文件 (*.*)”)); if (fileName.isEmpty()) { return false; // 用户取消了保存 } return saveFileToDisk(fileName); } bool MainWindow::saveFileToDisk(const QString fileName) { QFile file(fileName); if (!file.open(QFile::WriteOnly | QFile::Text)) { QMessageBox::warning(this, tr(“简易记事本”), tr(“无法写入文件 %1:\n%2.”) .arg(fileName) .arg(file.errorString())); return false; } QTextStream out(file); out.setCodec(“UTF-8”); // 同样设置为UTF-8编码保存 out textEdit-toPlainText(); // 将文本编辑框的纯文本内容写入文件 file.close(); setCurrentFile(fileName); // 更新当前文件路径 statusBar()-showMessage(tr(“文件已保存”), 2000); // 在状态栏显示2秒的提示消息 return true; }4.2 连接信号与槽绑定功能功能函数都写好了现在需要把菜单/工具栏的动作QAction和这些槽函数连接起来。回到createActions()函数将之前注释掉的connect语句补全并添加其他动作的连接void MainWindow::createActions() { // … 之前的动作创建代码不变 // 连接信号与槽 connect(newAct, QAction::triggered, this, MainWindow::newFile); connect(openAct, QAction::triggered, this, MainWindow::openFile); connect(saveAct, QAction::triggered, this, MainWindow::saveFile); connect(saveAsAct, QAction::triggered, this, MainWindow::saveAsFile); // exitAct 已经在创建时连接了 }4.3 完善细节修改状态提示与关闭事件为了让体验更好我们还需要做两件事实时显示修改状态我们希望文档被修改后窗口标题能显示一个星号*作为提示。这可以通过连接QTextDocument的modificationChanged信号来实现。在MainWindow构造函数中添加MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , textEdit(new QTextEdit(this)) { // … 之前的设置代码 setCentralWidget(textEdit); // 连接文档修改状态变化信号更新窗口标题 connect(textEdit-document(), QTextDocument::modificationChanged, [this](bool changed) { QString title windowTitle(); if (changed !title.startsWith(‘*’)) { setWindowTitle(‘*’ title); } else if (!changed title.startsWith(‘*’)) { setWindowTitle(title.mid(1)); // 去掉开头的星号 } }); createActions(); createMenus(); createToolBars(); }这里使用了Lambda表达式来连接信号这是一个非常方便的特性。当文档的修改状态发生变化时这个Lambda函数会被调用根据changed参数在窗口标题前添加或移除星号。处理窗口关闭事件当用户点击窗口关闭按钮时应该和选择“退出”菜单一样检查文档是否需要保存。我们可以重写closeEvent函数。在mainwindow.h的类声明中添加protected: void closeEvent(QCloseEvent *event) override;然后在mainwindow.cpp中实现#include QCloseEvent // 记得在文件顶部包含这个头文件 void MainWindow::closeEvent(QCloseEvent *event) { if (maybeSave()) { event-accept(); // 用户确认保存或放弃接受关闭事件 } else { event-ignore(); // 用户取消了操作忽略关闭事件 } }5. 项目编译、运行与问题排查5.1 编译与运行现在所有核心代码已经完成。点击Qt Creator左下角的绿色“运行”按钮或按CtrlR。Qt Creator会自动执行qmake根据.pro文件生成Makefile和make编译链接的过程。第一次编译可能会稍慢。成功后你的极简记事本程序就会启动。你可以尝试输入一些文字。点击“文件”-“新建”会提示你是否保存。点击“文件”-“打开”选择一个文本文件。编辑后点击“保存”或“另存为”。修改内容后看窗口标题是否出现星号*。尝试直接点击窗口关闭按钮看是否会弹出保存提示。5.2 常见问题与解决方案速查表在实操过程中你可能会遇到以下问题。这里提供一个快速排查指南问题现象可能原因解决方案编译错误undefined reference to ‘vtable for MainWindow’1. 在头文件中声明了槽函数或使用了Q_OBJECT宏但未重新运行qmake。2. 槽函数声明了但未实现。1. 在Qt Creator中对项目右键 - “执行qmake”。2. 检查并实现所有声明的槽函数。程序运行后点击按钮无反应1. 信号与槽未正确连接。2. 连接语句写在了对象创建之前。1. 检查connect语句的参数是否正确特别是函数指针的写法。2. 确保connect在动作Action和主窗口对象都创建之后调用通常放在createActions函数末尾。打开或保存文件时中文乱码文件读写编码不一致。Windows记事本默认保存为ANSIGBK。在loadFile和saveFileToDisk函数中为QTextStream显式设置编码in.setCodec(“UTF-8”);和out.setCodec(“UTF-8”);。菜单或工具栏显示英文或乱码源代码文件本身的编码不是UTF-8。在Qt Creator中点击“编辑”-“Select Encoding”选择“UTF-8”并选择“按编码重新载入”。保存文件。确保项目所有源文件都是UTF-8编码。运行时弹出错误对话框提示“无法找到xxx.dll”程序依赖的Qt动态库DLL没有在系统路径中。1.开发环境运行在Qt Creator中运行是没问题的因为它配置了环境。2.独立发布程序需要将必要的Qt DLL文件如Qt6Core.dll,Qt6Gui.dll,Qt6Widgets.dll等复制到你的exe文件同级目录。可以使用Qt自带的windeployqt工具自动化完成。界面布局错乱控件大小位置不对可能是没有正确设置布局管理器Layout或者窗口大小改变时未适配。我们这个简单项目因为只用了setCentralWidget所以问题不大。对于复杂界面务必学习使用QHBoxLayout,QVBoxLayout,QGridLayout等布局管理器而不是用绝对坐标。快捷键不生效1. 快捷键被系统或其他应用占用。2. 设置快捷键的代码未生效。1. 检查系统快捷键冲突较少见。2. 确保setShortcut在动作被添加到菜单/工具栏之前调用。使用QKeySequence::StandardKey能最大程度保证跨平台一致性。5.3 功能扩展思路与代码优化你的30分钟极简记事本已经完成了核心功能。但它还有很大的扩展空间这里提供几个方向编辑功能添加“编辑”菜单实现撤销、重做、剪切、复制、粘贴、全选。QTextEdit本身已经内置了这些功能你只需要创建对应的QAction并连接到QTextEdit的相应槽即可例如QAction *cutAct editMenu-addAction(tr(“剪切”)); cutAct-setShortcut(QKeySequence::Cut); connect(cutAct, QAction::triggered, textEdit, QTextEdit::cut);格式设置添加“格式”菜单或工具栏按钮实现字体选择、加粗、斜体、下划线、颜色等。这需要用到QFontDialog,QColorDialog以及QTextEdit的QTextCursor来进行格式操作。查找替换实现一个查找替换对话框。你可以创建一个新的对话框类里面放输入框和按钮然后在主窗口中调用它。这涉及到模态对话框的使用和更复杂的文本操作。最近文件列表在“文件”菜单下动态显示最近打开的几个文件。这需要将文件路径保存到设置中如QSettings并在每次打开文件时更新这个列表。状态栏信息在状态栏实时显示光标所在的行号、列号或者字数统计。这可以通过连接QTextEdit的cursorPositionChanged()信号来实现。这个项目的完整源码我已经按照上述步骤整理好。你可以直接复制粘贴到你的Qt Creator项目中也可以作为一个起点添加你想要的任何功能。记住理解每一行代码为什么这么写比单纯能运行更重要。多尝试修改代码看看会发生什么这是学习编程最快的方式。