30分钟用Qt Creator打造C++桌面记事本:从零掌握图形界面开发
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.pro:qmake的项目配置文件,管理着项目需要哪些模块、哪些源文件。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 定义快捷键Alt+N newAct->setShortcut(QKeySequence::New); // 设置标准快捷键 Ctrl+N 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(“Ctrl+Q”)); // QKeySequence::Quit 在Windows上可能不是Ctrl+Q,我们显式指定 exitAct->setStatusTip(tr(“退出应用程序”)); connect(exitAct, &QAction::triggered, this, &QWidget::close); // 连接到关闭窗口的槽函数 }动作设置详解:
tr(“&新建”):tr()是Qt的翻译函数,即使不做多语言,也建议使用,这是良好习惯。&N表示在菜单显示时,N字母会带下划线,用户可以通过Alt+F(打开文件菜单)再按N来激活该动作。QKeySequence::New:这是Qt定义的标准快捷键枚举。使用它而不是硬编码“Ctrl+N”的好处是,Qt会根据不同操作系统平台自动适配(比如在macOS上可能是Cmd+N)。setStatusTip:设置的状态提示文本,会在用户将鼠标悬停在菜单项或工具栏按钮上时,显示在主窗口底部的状态栏中。这是一个提升用户体验的小细节。
然后,实现createMenus和createToolBars:
void 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左下角的绿色三角形(或按Ctrl+R)编译并运行。你应该能看到一个带有菜单栏、工具栏和一个空白文本编辑区域的窗口了。菜单和工具栏的按钮暂时还点不了,因为我们还没给它们连接具体的功能函数(槽函数)。
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左下角的绿色“运行”按钮(或按Ctrl+R)。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记事本默认保存为ANSI(GBK)。 | 在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项目中,也可以作为一个起点,添加你想要的任何功能。记住,理解每一行代码为什么这么写,比单纯能运行更重要。多尝试修改代码,看看会发生什么,这是学习编程最快的方式。
