Qt:窗口与文件绑定
窗口与文件对应
- 一、单文档应用
- 1.1 核心概念与设计
- 1.2 实现步骤
- 1.3 完整示例代码
- 二、多文档应用(QMdiArea)
- 2.1 核心设计
- 2.2 实现步骤
- 2.3 完整示例代码
- 1. `documentwindow.h` (自定义子窗口类)
- 2. `documentwindow.cpp`
- 3. `mainwindow.h`
- 4. `mainwindow.cpp`
- 2.4 总结
- 三、多文档应用(QTabWidget)
- 3.1 核心设计
- 3.2 实现步骤
- 3.3 完整示例代码
- 1. `documentwidget.h` (自定义文档页面类)
- 2. `documentwidget.cpp`
- 3. `mainwindow.h`
- 4. `mainwindow.cpp`
- 3.4 总结
在 Qt 中将窗口与文件绑定,意味着一个窗口实例专门负责显示、编辑和保存一个特定的文件。
核心思想是:在窗口类中维护一个成员变量来存储当前绑定的文件路径(QString m_filePath
)。然后,围绕这个路径实现“新建”、“打开”、“保存”、“另存为”等功能。
一、单文档应用
1.1 核心概念与设计
-
文件路径成员变量 (
m_filePath
):- 在你的主窗口类(如
MainWindow
)中,添加一个私有成员变量QString m_filePath
。 - 这个变量是“绑定”的关键。当它为空时,表示窗口没有绑定任何文件(例如,一个新建的、未保存的文档)。当它有值时,表示窗口绑定了该路径下的文件。
- 在你的主窗口类(如
-
窗口标题 (
windowTitle
):- 动态更新窗口标题,以显示当前绑定的文件名和修改状态(例如:“document.txt - MyEditor [*]”)。
[*]
是一个特殊的占位符,当窗口有未保存的更改时,Qt 会自动显示一个*
。
- 动态更新窗口标题,以显示当前绑定的文件名和修改状态(例如:“document.txt - MyEditor [*]”)。
-
“脏”标志 (
isWindowModified
):- 使用
setWindowModified(true)
来标记窗口内容已被修改但尚未保存。这会触发标题栏的*
显示。 - 在保存文件后,调用
setWindowModified(false)
来清除标记。
- 使用
1.2 实现步骤
-
在窗口类中添加成员变量:
// mainwindow.h private:QString m_filePath; // 用于存储当前文件路径
-
实现“新建”功能:
- 清空 UI 内容(如
QTextEdit
)。 - 清空
m_filePath
。 - 重置窗口标题和“脏”标志。
- 清空 UI 内容(如
-
实现“打开”功能:
- 使用
QFileDialog::getOpenFileName()
获取用户选择的文件路径。 - 如果用户选择了文件,就读取文件内容并显示在 UI 上。
- 将
m_filePath
更新为所选路径。 - 更新窗口标题,并将“脏”标志设为
false
。
- 使用
-
实现“保存”功能:
- 如果
m_filePath
不为空(已绑定文件):直接将当前 UI 内容写入该路径的文件。 - 如果
m_filePath
为空(未绑定文件,即新建的文件):调用“另存为”功能。 - 保存成功后,将“脏”标志设为
false
。
- 如果
-
实现“另存为”功能:
- 使用
QFileDialog::getSaveFileName()
获取用户指定的新文件路径。 - 将当前 UI 内容写入该新路径的文件。
- 将
m_filePath
更新为这个新路径。 - 更新窗口标题,并将“脏”标志设为
false
。
- 使用
-
处理内容修改:
- 连接 UI 控件的
textChanged()
信号(或类似信号)到一个槽函数。 - 在该槽函数中,调用
setWindowModified(true)
。
- 连接 UI 控件的
-
处理关闭事件:
- 重写
closeEvent(QCloseEvent *event)
。 - 在关闭前检查
isWindowModified()
。如果为true
,弹出“是否保存”的提示对话框。 - 根据用户的选择(保存、不保存、取消),执行相应操作或取消关闭。
- 重写
1.3 完整示例代码
假设我们有一个简单的文本编辑器,主窗口包含一个 QTextEdit
(ui->textEdit
) 和标准的“文件”菜单。
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include <QCloseEvent>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();protected:// 重写关闭事件void closeEvent(QCloseEvent *event) override;private slots:// 菜单动作的槽函数void on_actionNew_triggered();void on_actionOpen_triggered();void on_actionSave_triggered();void on_actionSave_As_triggered();private:Ui::MainWindow *ui;QString m_filePath; // 当前绑定的文件路径// 辅助函数void setFilePath(const QString &path); // 设置文件路径并更新标题bool saveToFile(const QString &path); // 将内容保存到指定文件bool maybeSave(); // 检查并处理未保存的更改
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QFile>
#include <QTextStream>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);setFilePath(""); // 初始状态:无文件路径this->setWindowTitle("文本编辑器");// 连接文本框内容变化信号,以更新“脏”标志connect(ui->textEdit, &QTextEdit::textChanged, this, [this]() {this->setWindowModified(true);});
}MainWindow::~MainWindow()
{delete ui;
}// 设置文件路径并更新窗口标题
void MainWindow::setFilePath(const QString &path)
{m_filePath = path;QString title = m_filePath.isEmpty() ? "未命名" : QFileInfo(m_filePath).fileName();this->setWindowTitle(QString("%1 - 文本编辑器[*]").arg(title));this->setWindowModified(false); // 刚设置完路径,内容是“干净”的
}// 将当前内容保存到指定文件
bool MainWindow::saveToFile(const QString &path)
{QFile file(path);if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {QMessageBox::critical(this, "错误", "无法打开文件进行写入: " + file.errorString());return false;}QTextStream out(&file);out << ui->textEdit->toPlainText();file.close();setFilePath(path); // 更新路径和标题return true;
}// 检查并处理未保存的更改
bool MainWindow::maybeSave()
{if (!this->isWindowModified()) {return true; // 没有更改,直接关闭或继续}QMessageBox::StandardButton ret;QString message = m_filePath.isEmpty() ? "是否保存对“未命名”的更改?" :QString("是否保存对“%1”的更改?").arg(QFileInfo(m_filePath).fileName());ret = QMessageBox::warning(this, "文本编辑器", message,QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);if (ret == QMessageBox::Save) {return on_actionSave_triggered(); // 执行保存操作} else if (ret == QMessageBox::Cancel) {return false; // 取消关闭}// 如果是 Discard (不保存),则直接返回 true,允许关闭return true;
}// --- 槽函数实现 ---void MainWindow::on_actionNew_triggered()
{if (maybeSave()) { // 先检查是否需要保存当前文件ui->textEdit->clear();setFilePath("");}
}void MainWindow::on_actionOpen_triggered()
{if (maybeSave()) { // 先检查是否需要保存当前文件QString path = QFileDialog::getOpenFileName(this, "打开文件", "", "文本文件 (*.txt);;所有文件 (*)");if (!path.isEmpty()) {QFile file(path);if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {QMessageBox::critical(this, "错误", "无法打开文件: " + file.errorString());return;}QTextStream in(&file);ui->textEdit->setText(in.readAll());file.close();setFilePath(path);}}
}void MainWindow::on_actionSave_triggered()
{if (m_filePath.isEmpty()) {on_actionSave_As_triggered(); // 如果没有路径,则执行“另存为”} else {saveToFile(m_filePath);}
}void MainWindow::on_actionSave_As_triggered()
{QString path = QFileDialog::getSaveFileName(this, "另存为", "", "文本文件 (*.txt);;所有文件 (*)");if (!path.isEmpty()) {saveToFile(path);}
}// 处理关闭事件
void MainWindow::closeEvent(QCloseEvent *event)
{if (maybeSave()) {event->accept(); // 如果可以关闭(已保存或放弃更改)} else {event->ignore(); // 否则忽略关闭事件}
}
通过以上步骤,你就成功地将一个窗口与文件进行了绑定。这种模式是构建单文档应用程序的基石,它清晰地管理了文件的生命周期,并提供了专业的用户体验。对于多文档应用(MDI),逻辑是相似的,只是每个子窗口都各自维护自己的 m_filePath
和修改状态。
二、多文档应用(QMdiArea)
多文档应用(MDI)中实现窗口与文件的绑定,是单文档应用(SDI)中“窗口-文件绑定”概念的自然延伸。
核心区别在于:单文档应用中,一个应用程序实例只绑定一个文件;而多文档应用中,主窗口(QMainWindow
)可以包含多个子窗口(QMdiSubWindow
),每个子窗口都独立地与一个文件进行绑定。
因此,我们需要一个专门的子窗口类来封装文件路径、文件内容和相关操作。
2.1 核心设计
-
MainWindow
(主窗口):- 包含一个
QMdiArea
作为中央部件。 - 负责创建新的子窗口、打开文件(并在新子窗口中显示)、以及管理所有子窗口(如平铺、层叠)。
- “文件”菜单中的“新建”、“打开”、“保存全部”等动作属于主窗口。
- 包含一个
-
DocumentWindow
(文档窗口,自定义子窗口类):- 继承自
QMdiSubWindow
。 - 内部包含一个用于显示/编辑文件内容的控件,如
QTextEdit
。 - 核心:在这个类中维护与单个文件绑定所需的所有状态和逻辑。
QString m_filePath
:存储当前子窗口绑定的文件路径。isWindowModified()
/setWindowModified()
:管理文件的“脏”状态(是否已修改未保存)。save()
/saveAs()
/load()
:实现文件的加载、保存功能。updateWindowTitle()
:根据文件名和“脏”状态更新窗口标题。
- 继承自
2.2 实现步骤
-
创建
DocumentWindow
类:- 这是一个自定义的
QMdiSubWindow
,它将成为每个打开文件的容器。 - 它内部包含一个
QTextEdit
(或其他编辑器控件)。 - 它自身处理“保存”、“另存为”和“关闭前确认”等逻辑。
- 这是一个自定义的
-
修改
MainWindow
类:- “新建”动作:创建一个新的
DocumentWindow
实例,并将其添加到QMdiArea
。 - “打开”动作:弹出
QFileDialog
,获取文件路径后,创建一个新的DocumentWindow
实例,调用其load()
方法,并添加到QMdiArea
。 - “保存”/“另存为”动作:获取当前活动的
DocumentWindow
子窗口,并调用其save()
或saveAs()
方法。 - “关闭”动作:关闭当前活动的子窗口(
QMdiArea
会处理关闭事件)。 - “保存全部”动作:遍历
QMdiArea
中的所有子窗口,对每个DocumentWindow
调用save()
方法。
- “新建”动作:创建一个新的
2.3 完整示例代码
假设我们要创建一个多文档文本编辑器。
1. documentwindow.h
(自定义子窗口类)
#ifndef DOCUMENTWINDOW_H
#define DOCUMENTWINDOW_H#include <QMdiSubWindow>
#include <QTextEdit>class DocumentWindow : public QMdiSubWindow
{Q_OBJECTpublic:explicit DocumentWindow(QWidget *parent = nullptr);~DocumentWindow();bool loadFile(const QString &filePath);bool save();bool saveAs();QString currentFilePath() const;void setCurrentFilePath(const QString &path);protected:void closeEvent(QCloseEvent *event) override;private slots:void documentWasModified();private:bool saveToFile(const QString &filePath);void updateWindowTitle();bool maybeSave();QTextEdit *m_textEdit;QString m_filePath;
};#endif // DOCUMENTWINDOW_H
2. documentwindow.cpp
#include "documentwindow.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QFile>
#include <QTextStream>
#include <QCloseEvent>DocumentWindow::DocumentWindow(QWidget *parent) : QMdiSubWindow(parent)
{m_textEdit = new QTextEdit();setWidget(m_textEdit);setAttribute(Qt::WA_DeleteOnClose); // 关闭时自动删除connect(m_textEdit->document(), &QTextDocument::contentsChanged,this, &DocumentWindow::documentWasModified);setCurrentFilePath("");
}DocumentWindow::~DocumentWindow()
{// QMdiSubWindow 会自动删除其内部的 widget (m_textEdit)
}QString DocumentWindow::currentFilePath() const
{return m_filePath;
}void DocumentWindow::setCurrentFilePath(const QString &path)
{m_filePath = path;updateWindowTitle();m_textEdit->document()->setModified(false);
}bool DocumentWindow::loadFile(const QString &filePath)
{QFile file(filePath);if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {QMessageBox::critical(this, "错误", "无法打开文件: " + file.errorString());return false;}QTextStream in(&file);m_textEdit->setText(in.readAll());file.close();setCurrentFilePath(filePath);return true;
}bool DocumentWindow::save()
{if (m_filePath.isEmpty()) {return saveAs();} else {return saveToFile(m_filePath);}
}bool DocumentWindow::saveAs()
{QString filePath = QFileDialog::getSaveFileName(this, "另存为", "", "文本文件 (*.txt);;所有文件 (*)");if (filePath.isEmpty()) {return false;}return saveToFile(filePath);
}bool DocumentWindow::saveToFile(const QString &filePath)
{QFile file(filePath);if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {QMessageBox::critical(this, "错误", "无法打开文件进行写入: " + file.errorString());return false;}QTextStream out(&file);out << m_textEdit->toPlainText();file.close();setCurrentFilePath(filePath);return true;
}void DocumentWindow::updateWindowTitle()
{QString title = m_filePath.isEmpty() ? "未命名" : QFileInfo(m_filePath).fileName();setWindowTitle(QString("%1[*]").arg(title));
}void DocumentWindow::documentWasModified()
{setWindowModified(m_textEdit->document()->isModified());
}void DocumentWindow::closeEvent(QCloseEvent *event)
{if (maybeSave()) {event->accept();} else {event->ignore();}
}bool DocumentWindow::maybeSave()
{if (!isWindowModified()) {return true;}QMessageBox::StandardButton ret;QString message = m_filePath.isEmpty() ? "是否保存对“未命名”的更改?" :QString("是否保存对“%1”的更改?").arg(QFileInfo(m_filePath).fileName());ret = QMessageBox::warning(this, "多文档编辑器", message,QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);if (ret == QMessageBox::Save) {return save();} else if (ret == QMessageBox::Cancel) {return false;}return true; // Discard
}
3. mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include "documentwindow.h" // 包含自定义子窗口头文件QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:void on_actionNew_triggered();void on_actionOpen_triggered();void on_actionSave_triggered();void on_actionSave_As_triggered();void on_actionClose_triggered();void on_actionSave_All_triggered();private:Ui::MainWindow *ui;DocumentWindow* createNewDocumentWindow();DocumentWindow* activeDocumentWindow();
};
#endif // MAINWINDOW_H
4. mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMdiArea>
#include <QFileDialog>
#include <QMessageBox>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);this->setWindowTitle("多文档编辑器");// 假设 UI 文件中已经有一个 QMdiArea,其 objectName 为 mdiArea// 如果没有,需要手动创建并设置为中央部件// ui->mdiArea = new QMdiArea(this);// setCentralWidget(ui->mdiArea);
}MainWindow::~MainWindow()
{delete ui;
}DocumentWindow* MainWindow::createNewDocumentWindow()
{DocumentWindow *docWindow = new DocumentWindow();ui->mdiArea->addSubWindow(docWindow);docWindow->show();return docWindow;
}DocumentWindow* MainWindow::activeDocumentWindow()
{if (QMdiSubWindow *activeSubWindow = ui->mdiArea->activeSubWindow()) {return qobject_cast<DocumentWindow*>(activeSubWindow);}return nullptr;
}void MainWindow::on_actionNew_triggered()
{createNewDocumentWindow();
}void MainWindow::on_actionOpen_triggered()
{QString filePath = QFileDialog::getOpenFileName(this, "打开文件", "", "文本文件 (*.txt);;所有文件 (*)");if (!filePath.isEmpty()) {// 检查文件是否已在某个子窗口中打开foreach (QMdiSubWindow *subWindow, ui->mdiArea->subWindowList()) {DocumentWindow *docWindow = qobject_cast<DocumentWindow*>(subWindow);if (docWindow && docWindow->currentFilePath() == filePath) {ui->mdiArea->setActiveSubWindow(subWindow);return;}}// 如果文件未打开,则创建新窗口并加载DocumentWindow *newDocWindow = createNewDocumentWindow();newDocWindow->loadFile(filePath);}
}void MainWindow::on_actionSave_triggered()
{if (DocumentWindow *docWindow = activeDocumentWindow()) {docWindow->save();}
}void MainWindow::on_actionSave_As_triggered()
{if (DocumentWindow *docWindow = activeDocumentWindow()) {docWindow->saveAs();}
}void MainWindow::on_actionClose_triggered()
{ui->mdiArea->closeActiveSubWindow();
}void MainWindow::on_actionSave_All_triggered()
{foreach (QMdiSubWindow *subWindow, ui->mdiArea->subWindowList()) {DocumentWindow *docWindow = qobject_cast<DocumentWindow*>(subWindow);if (docWindow && docWindow->isWindowModified()) {docWindow->save();}}
}
2.4 总结
通过创建一个独立的 DocumentWindow
类来封装单个文件的所有行为,我们优雅地实现了多文档应用中“窗口-文件绑定”的需求。
- 职责分离:
MainWindow
负责全局管理,DocumentWindow
负责单个文件的生命周期。 - 高内聚:每个
DocumentWindow
对象都是一个自包含的、可独立运行的实体。 - 可扩展性:如果需要支持不同类型的文件(如图片、表格),只需创建不同的
DocumentWindow
子类即可。
这种设计模式是构建健壮、可维护的多文档应用程序的基石。
三、多文档应用(QTabWidget)
利用标签页(QTabWidget
)实现多文档应用并与文件进行绑定,是另一种非常流行且用户友好的方式。它比 QMdiArea
更简洁,适合一次只需要关注一个文档的场景。
核心思想与 MDI 类似:创建一个专门的 QWidget
子类来封装单个文件的所有状态和行为,然后将这个自定义的 QWidget
作为页面添加到 QTabWidget
中。
3.1 核心设计
-
MainWindow
(主窗口):- 包含一个
QTabWidget
作为中央部件。 - 负责创建新标签页、打开文件(在新标签页中显示)、关闭标签页等。
- “文件”菜单中的“新建”、“打开”、“保存全部”等动作属于主窗口。
- 包含一个
-
DocumentWidget
(文档页面,自定义QWidget
子类):- 继承自
QWidget
。 - 内部包含一个用于显示/编辑文件内容的控件,如
QTextEdit
。 - 核心:在这个类中维护与单个文件绑定所需的所有状态和逻辑。
QString m_filePath
:存储当前页面绑定的文件路径。isModified()
/setModified()
:管理文件的“脏”状态。save()
/saveAs()
/load()
:实现文件的加载、保存功能。getFileName()
:获取用于显示在标签页上的文件名。
- 继承自
3.2 实现步骤
-
创建
DocumentWidget
类:- 这是一个自定义的
QWidget
,它将成为每个文件的编辑器页面。 - 它内部包含一个
QTextEdit
。 - 它自身处理“保存”、“另存为”和“内容修改”等逻辑。
- 这是一个自定义的
-
修改
MainWindow
类:- 关键:将
QTabWidget
的tabCloseRequested(int index)
信号连接到一个槽函数,用于处理关闭标签页的请求。 - “新建”动作:创建一个新的
DocumentWidget
实例,并使用QTabWidget::addTab()
将其添加。 - “打开”动作:弹出
QFileDialog
,获取文件路径后,创建一个新的DocumentWidget
实例,调用其load()
方法,并添加到QTabWidget
。 - “保存”/“另存为”动作:获取当前活动标签页(
QTabWidget::currentWidget()
),将其强制转换为DocumentWidget
指针,并调用其save()
或saveAs()
方法。 - “关闭”动作:关闭当前活动的标签页。
- “保存全部”动作:遍历
QTabWidget
中的所有页面,对每个DocumentWidget
调用save()
方法。 - 监听
currentChanged(int index)
信号,根据当前页面的文件名更新主窗口标题。
- 关键:将
3.3 完整示例代码
假设我们要创建一个标签页式的多文档文本编辑器。
1. documentwidget.h
(自定义文档页面类)
#ifndef DOCUMENTWIDGET_H
#define DOCUMENTWIDGET_H#include <QWidget>
#include <QTextEdit>class DocumentWidget : public QWidget
{Q_OBJECTpublic:explicit DocumentWidget(QWidget *parent = nullptr);~DocumentWidget();bool loadFile(const QString &filePath);bool save();bool saveAs();QString currentFilePath() const;QString getFileName() const;bool isModified() const;signals:// 当文档状态改变时(如被修改),发射此信号,以便主窗口更新标签void documentStatusChanged();private slots:void documentWasModified();private:bool saveToFile(const QString &filePath);void setCurrentFilePath(const QString &path);QTextEdit *m_textEdit;QString m_filePath;
};#endif // DOCUMENTWIDGET_H
2. documentwidget.cpp
#include "documentwidget.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QFile>
#include <QTextStream>
#include <QVBoxLayout>DocumentWidget::DocumentWidget(QWidget *parent) : QWidget(parent)
{m_textEdit = new QTextEdit();QVBoxLayout *layout = new QVBoxLayout(this);layout->setContentsMargins(0, 0, 0, 0);layout->addWidget(m_textEdit);connect(m_textEdit->document(), &QTextDocument::contentsChanged,this, &DocumentWidget::documentWasModified);setCurrentFilePath("");
}DocumentWidget::~DocumentWidget()
{// QWidget 的析构函数会自动删除其子控件 (m_textEdit)
}QString DocumentWidget::currentFilePath() const
{return m_filePath;
}QString DocumentWidget::getFileName() const
{return m_filePath.isEmpty() ? "未命名" : QFileInfo(m_filePath).fileName();
}bool DocumentWidget::isModified() const
{return m_textEdit->document()->isModified();
}bool DocumentWidget::loadFile(const QString &filePath)
{QFile file(filePath);if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {QMessageBox::critical(this, "错误", "无法打开文件: " + file.errorString());return false;}QTextStream in(&file);m_textEdit->setText(in.readAll());file.close();setCurrentFilePath(filePath);return true;
}bool DocumentWidget::save()
{if (m_filePath.isEmpty()) {return saveAs();} else {return saveToFile(m_filePath);}
}bool DocumentWidget::saveAs()
{QString filePath = QFileDialog::getSaveFileName(this, "另存为", "", "文本文件 (*.txt);;所有文件 (*)");if (filePath.isEmpty()) {return false;}return saveToFile(filePath);
}bool DocumentWidget::saveToFile(const QString &filePath)
{QFile file(filePath);if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {QMessageBox::critical(this, "错误", "无法打开文件进行写入: " + file.errorString());return false;}QTextStream out(&file);out << m_textEdit->toPlainText();file.close();setCurrentFilePath(filePath);return true;
}void DocumentWidget::setCurrentFilePath(const QString &path)
{m_filePath = path;m_textEdit->document()->setModified(false);emit documentStatusChanged(); // 通知主窗口更新标签
}void DocumentWidget::documentWasModified()
{emit documentStatusChanged(); // 通知主窗口更新标签
}
3. mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include "documentwidget.h" // 包含自定义文档页面头文件QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();protected:void closeEvent(QCloseEvent *event) override;private slots:void on_actionNew_triggered();void on_actionOpen_triggered();void on_actionSave_triggered();void on_actionSave_As_triggered();void on_actionClose_triggered();void on_actionSave_All_triggered();// 自定义槽函数void onTabCloseRequested(int index);void updateTabTitle(int index);void updateWindowTitle();private:Ui::MainWindow *ui;DocumentWidget* createNewDocumentWidget();DocumentWidget* currentDocumentWidget();bool maybeSaveDocument(DocumentWidget *docWidget);void updateTabAppearance(DocumentWidget *docWidget);
};
#endif // MAINWINDOW_H
4. mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTabWidget>
#include <QFileDialog>
#include <QMessageBox>
#include <QCloseEvent>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);this->setWindowTitle("标签页式多文档编辑器");// 假设 UI 文件中已经有一个 QTabWidget,其 objectName 为 tabWidgetui->tabWidget->setTabsClosable(true); // 允许关闭标签ui->tabWidget->setMovable(true); // 允许拖动标签// 连接 QTabWidget 的信号connect(ui->tabWidget, &QTabWidget::tabCloseRequested, this, &MainWindow::onTabCloseRequested);connect(ui->tabWidget, &QTabWidget::currentChanged, this, &MainWindow::updateWindowTitle);// 初始创建一个空白文档on_actionNew_triggered();
}MainWindow::~MainWindow()
{delete ui;
}DocumentWidget* MainWindow::createNewDocumentWidget()
{DocumentWidget *docWidget = new DocumentWidget();int index = ui->tabWidget->addTab(docWidget, docWidget->getFileName());ui->tabWidget->setCurrentIndex(index);// 连接文档页面的信号,以更新标签标题和外观connect(docWidget, &DocumentWidget::documentStatusChanged, [this, docWidget]() {int index = ui->tabWidget->indexOf(docWidget);if (index != -1) {updateTabAppearance(docWidget);}});return docWidget;
}DocumentWidget* MainWindow::currentDocumentWidget()
{QWidget *currentWidget = ui->tabWidget->currentWidget();return qobject_cast<DocumentWidget*>(currentWidget);
}void MainWindow::updateTabAppearance(DocumentWidget *docWidget)
{int index = ui->tabWidget->indexOf(docWidget);if (index != -1) {QString title = docWidget->getFileName();if (docWidget->isModified()) {title += " *"; // 在标题后添加星号表示已修改}ui->tabWidget->setTabText(index, title);}
}void MainWindow::updateWindowTitle()
{DocumentWidget *docWidget = currentDocumentWidget();if (docWidget) {QString title = docWidget->getFileName();if (docWidget->isModified()) {title += " *";}this->setWindowTitle(QString("%1 - 标签页式多文档编辑器").arg(title));} else {this->setWindowTitle("标签页式多文档编辑器");}
}bool MainWindow::maybeSaveDocument(DocumentWidget *docWidget)
{if (docWidget && docWidget->isModified()) {QMessageBox::StandardButton ret;QString message = QString("是否保存对“%1”的更改?").arg(docWidget->getFileName());ret = QMessageBox::warning(this, "多文档编辑器", message,QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);if (ret == QMessageBox::Save) {return docWidget->save();} else if (ret == QMessageBox::Cancel) {return false;}// 如果是 Discard (不保存),则直接返回 true}return true;
}// --- 槽函数实现 ---void MainWindow::on_actionNew_triggered()
{createNewDocumentWidget();
}void MainWindow::on_actionOpen_triggered()
{QString filePath = QFileDialog::getOpenFileName(this, "打开文件", "", "文本文件 (*.txt);;所有文件 (*)");if (!filePath.isEmpty()) {// 检查文件是否已在某个标签页中打开for (int i = 0; i < ui->tabWidget->count(); ++i) {DocumentWidget *docWidget = qobject_cast<DocumentWidget*>(ui->tabWidget->widget(i));if (docWidget && docWidget->currentFilePath() == filePath) {ui->tabWidget->setCurrentIndex(i);return;}}// 如果文件未打开,则创建新标签页并加载DocumentWidget *newDocWidget = createNewDocumentWidget();newDocWidget->loadFile(filePath);}
}void MainWindow::on_actionSave_triggered()
{if (DocumentWidget *docWidget = currentDocumentWidget()) {docWidget->save();}
}void MainWindow::on_actionSave_As_triggered()
{if (DocumentWidget *docWidget = currentDocumentWidget()) {docWidget->saveAs();}
}void MainWindow::on_actionClose_triggered()
{onTabCloseRequested(ui->tabWidget->currentIndex());
}void MainWindow::on_actionSave_All_triggered()
{for (int i = 0; i < ui->tabWidget->count(); ++i) {DocumentWidget *docWidget = qobject_cast<DocumentWidget*>(ui->tabWidget->widget(i));if (docWidget && docWidget->isModified()) {docWidget->save();}}
}void MainWindow::onTabCloseRequested(int index)
{if (index >= 0) {DocumentWidget *docWidget = qobject_cast<DocumentWidget*>(ui->tabWidget->widget(index));if (maybeSaveDocument(docWidget)) {// 先从 tabWidget 中移除,再删除对象ui->tabWidget->removeTab(index);delete docWidget;}}
}void MainWindow::closeEvent(QCloseEvent *event)
{bool canClose = true;// 从后往前遍历,防止删除元素后索引混乱for (int i = ui->tabWidget->count() - 1; i >= 0; --i) {DocumentWidget *docWidget = qobject_cast<DocumentWidget*>(ui->tabWidget->widget(i));if (docWidget && !maybeSaveDocument(docWidget)) {canClose = false;// 即使一个取消,也要把之前“保存”或“不保存”的页面关闭// 所以这里不 break,而是继续,但标记最终不能关闭} else {ui->tabWidget->removeTab(i);delete docWidget;}}if (canClose) {event->accept();} else {event->ignore();}
}
3.4 总结
使用 QTabWidget
实现多文档应用,通过创建一个独立的 DocumentWidget
类来封装单个文件,同样实现了清晰的职责分离。
-
优点:
- 界面整洁:所有文档都在一个窗口内,通过标签页切换,不会相互遮挡。
- 用户体验现代:符合主流软件(如浏览器、代码编辑器)的操作习惯。
- 实现相对简单:
QTabWidget
的 API 非常直观。
-
缺点:
- 不支持同时查看多个文档的内容。
- 标签过多时,可能需要滚动标签栏。
这种设计模式是构建现代、高效的多文档应用程序的另一个绝佳选择,尤其适合内容编辑类应用。