当前位置: 首页 > news >正文

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 核心概念与设计

  1. 文件路径成员变量 (m_filePath)

    • 在你的主窗口类(如 MainWindow)中,添加一个私有成员变量 QString m_filePath
    • 这个变量是“绑定”的关键。当它为空时,表示窗口没有绑定任何文件(例如,一个新建的、未保存的文档)。当它有值时,表示窗口绑定了该路径下的文件。
  2. 窗口标题 (windowTitle)

    • 动态更新窗口标题,以显示当前绑定的文件名和修改状态(例如:“document.txt - MyEditor [*]”)。[*] 是一个特殊的占位符,当窗口有未保存的更改时,Qt 会自动显示一个 *
  3. “脏”标志 (isWindowModified)

    • 使用 setWindowModified(true) 来标记窗口内容已被修改但尚未保存。这会触发标题栏的 * 显示。
    • 在保存文件后,调用 setWindowModified(false) 来清除标记。

1.2 实现步骤

  1. 在窗口类中添加成员变量

    // mainwindow.h
    private:QString m_filePath; // 用于存储当前文件路径
    
  2. 实现“新建”功能

    • 清空 UI 内容(如 QTextEdit)。
    • 清空 m_filePath
    • 重置窗口标题和“脏”标志。
  3. 实现“打开”功能

    • 使用 QFileDialog::getOpenFileName() 获取用户选择的文件路径。
    • 如果用户选择了文件,就读取文件内容并显示在 UI 上。
    • m_filePath 更新为所选路径。
    • 更新窗口标题,并将“脏”标志设为 false
  4. 实现“保存”功能

    • 如果 m_filePath 不为空(已绑定文件):直接将当前 UI 内容写入该路径的文件。
    • 如果 m_filePath 为空(未绑定文件,即新建的文件):调用“另存为”功能。
    • 保存成功后,将“脏”标志设为 false
  5. 实现“另存为”功能

    • 使用 QFileDialog::getSaveFileName() 获取用户指定的新文件路径。
    • 将当前 UI 内容写入该新路径的文件。
    • m_filePath 更新为这个新路径。
    • 更新窗口标题,并将“脏”标志设为 false
  6. 处理内容修改

    • 连接 UI 控件的 textChanged() 信号(或类似信号)到一个槽函数。
    • 在该槽函数中,调用 setWindowModified(true)
  7. 处理关闭事件

    • 重写 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 核心设计

  1. MainWindow (主窗口)

    • 包含一个 QMdiArea 作为中央部件。
    • 负责创建新的子窗口、打开文件(并在新子窗口中显示)、以及管理所有子窗口(如平铺、层叠)。
    • “文件”菜单中的“新建”、“打开”、“保存全部”等动作属于主窗口。
  2. DocumentWindow (文档窗口,自定义子窗口类)

    • 继承自 QMdiSubWindow
    • 内部包含一个用于显示/编辑文件内容的控件,如 QTextEdit
    • 核心:在这个类中维护与单个文件绑定所需的所有状态和逻辑。
      • QString m_filePath:存储当前子窗口绑定的文件路径。
      • isWindowModified() / setWindowModified():管理文件的“脏”状态(是否已修改未保存)。
      • save() / saveAs() / load():实现文件的加载、保存功能。
      • updateWindowTitle():根据文件名和“脏”状态更新窗口标题。

2.2 实现步骤

  1. 创建 DocumentWindow

    • 这是一个自定义的 QMdiSubWindow,它将成为每个打开文件的容器。
    • 它内部包含一个 QTextEdit(或其他编辑器控件)。
    • 它自身处理“保存”、“另存为”和“关闭前确认”等逻辑。
  2. 修改 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 核心设计

  1. MainWindow (主窗口)

    • 包含一个 QTabWidget 作为中央部件。
    • 负责创建新标签页、打开文件(在新标签页中显示)、关闭标签页等。
    • “文件”菜单中的“新建”、“打开”、“保存全部”等动作属于主窗口。
  2. DocumentWidget (文档页面,自定义 QWidget 子类)

    • 继承自 QWidget
    • 内部包含一个用于显示/编辑文件内容的控件,如 QTextEdit
    • 核心:在这个类中维护与单个文件绑定所需的所有状态和逻辑。
      • QString m_filePath:存储当前页面绑定的文件路径。
      • isModified() / setModified():管理文件的“脏”状态。
      • save() / saveAs() / load():实现文件的加载、保存功能。
      • getFileName():获取用于显示在标签页上的文件名。

3.2 实现步骤

  1. 创建 DocumentWidget

    • 这是一个自定义的 QWidget,它将成为每个文件的编辑器页面。
    • 它内部包含一个 QTextEdit
    • 它自身处理“保存”、“另存为”和“内容修改”等逻辑。
  2. 修改 MainWindow

    • 关键:将 QTabWidgettabCloseRequested(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 非常直观。
  • 缺点

    • 不支持同时查看多个文档的内容。
    • 标签过多时,可能需要滚动标签栏。

这种设计模式是构建现代、高效的多文档应用程序的另一个绝佳选择,尤其适合内容编辑类应用。

http://www.dtcms.com/a/469750.html

相关文章:

  • git常用命令大全
  • 算法 - 递归
  • 软考-系统架构设计师 系统架构评估详细讲解
  • Redis 黑马skyout
  • 【Unity】构建超实用的有限状态机管理类
  • redis基础命令和深入理解底层
  • Java中第三方报告库-Allure
  • 高端公司网站建设连云港做网站制作
  • Google 智能体设计模式:优先级排序
  • 网站做不做百度云加速手游代理平台哪个好
  • 【国内电子数据取证厂商龙信科技】邮件如何取证?
  • 手机网站模板 psd做网站建设分哪些类型
  • 做网站需要哪些框架网站没备案可以访问吗
  • Git下载和安装教程(附安装包)
  • go的学习2---》并发编程
  • 高端网站建设企业公司网页版qq空间登录入口官网
  • 麒麟系统安装达梦数据库遇到的问题
  • VScode怎么使用Jupyter并且设置内核
  • LwIP UDP RAW
  • VI-SLAM定位方案对比
  • TCP/IP 协议族—理论与实践(一)
  • 手持小风扇MCU方案,智能风扇方案设计开发
  • 网站设计深圳网站建设公司网页设计与制作100例怎么写
  • Linux -- 网络层
  • 建设班级网站 沟通无限网络黄页进入有限公司
  • Labview项目01:标准可配置序列测试框架
  • 拌合楼软件开发(23)监测客户端在线情况并联动企业微信提醒客户端离线和恢复
  • 雄安网建 网站建设莞城微信网站建设
  • 基于Python的交通数据分析应用-hadoop+django
  • [特殊字符] 教程|打造一个 Telegram 币圈波场交易记录检测机器人