官方博客:https://www.yafeilinux.com/

Qt开源社区:https://www.qter.org/

参考书:《Qt 及 Qt Quick 开发实战精解》

Qt 项目实战 | 多界面文本编辑器

开发环境:Qt Creator 4.6.2 Based on Qt 5.9.6

在这里插入图片描述

界面设计

这里主要是对主窗口和工具栏的设计。

新建 Qt Gui 应用,项目名称 myMdi,类名默认 MainWindow,基类默认为 QMainWindow都不做改动。

添加资源文件 myImage.qrc:

在这里插入图片描述

双击 mainwindow.ui 进入设计模式,添加菜单:

在这里插入图片描述

菜单栏和工具栏:

在这里插入图片描述

文件子菜单,注意有2个分隔符:

在这里插入图片描述

编辑子菜单,注意有1个分隔符:

在这里插入图片描述

窗口子菜单,注意有2个分隔符:

在这里插入图片描述

帮助子菜单:

在这里插入图片描述

设计完菜单栏和工具栏后,向主窗口中心区域拖入一个 MDI Area 部件,并单击主窗口界面,按下 Ctrl + G,使其处于栅格布局。

在这里插入图片描述

确保 MDI Area 部件的 objectName 是 mdiArea,而文件菜单、编辑菜单、窗口菜单、帮助菜单的 objectName 分别是 menuF、menuE、menuW、menuH。

在这里插入图片描述

创建子窗口类

为了实现多文档操作,需要向 QMdiArea 中添加子窗口,我们需要子类化子窗口的中心部件。

新建C++类文件,类名为 MdiChild,基类为 QTextEdit,类型信息选择“继承自 QWidget”:

在这里插入图片描述

在 mdichild.h 添加代码:

#ifndef MDICHILD_H
#define MDICHILD_H

#include <QTextEdit>
#include <QWidget>

class MdiChild : public QTextEdit
{
    Q_OBJECT
private:
    QString curFile;  //当前文件路径
    bool isUntitled;  //作为当前文件是否被保存到硬盘的标志

    bool maybeSave();                              //是否需要保存
    void setCurrentFile(const Qstring& fileName);  //设置当前文件
protected:
    void closeEvent(QCloseEvent* event);  //关闭事件
public:
    explicit MdiChild(QWidget* parent = 0);
    void newFile();                            //新建文件
    bool loadFile(const Qstring& fileName);    //加载文件
    bool save();                               //保存操作
    bool saveAs();                             //另存为操作
    bool saveFile(const QString& fileName);    //保存文件
    QString userFriendlyCurrentFile();         //提取文件名
    QString currentFile() { return curFile; }  //返回当前文件路径
private slots:
    void documentWasModified();  //文档被更改时,窗口显示更改状态标志
};

#endif  // MDICHILD_H

在 mdichild.cpp 添加代码:

#include "mdichild.h"

#include <QApplication>
#include <QCloseEvent>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QPushButton>
#include <QTextStream>

// 是否需要保存
bool MdiChild::maybeSave()
{
    // 如果文档被更改过
    if (document()->isModified())
    {
        QMessageBox box;
        box.setWindowTitle(tr("多文档编辑器"));
        box.setText(tr("是否保存对“%1”的更改?").arg(userFriendlyCurrentFile()));
        box.setIcon(QMessageBox::Warning);
        // 添加按钮,QMessageBox::YesRole可以表明这个按钮的行为
        QPushButton* yesBtn = box.addButton(tr("是(&Y)"), QMessageBox::YesRole);
        box.addButton(tr("否(&N)"), QMessageBox::NoRole);
        QPushButton* cancelBtn = box.addButton(tr("取消"), QMessageBox::RejectRole);
        // 弹出对话框,让用户选择是否保存修改,或者取消关闭操作
        box.exec();
        if (box.clickedButton() == yesBtn)
        {
            // 如果用户选择是,则返回保存操作的结果
            return save();
        }
        else if (box.clickedButton() == cancelBtn)
        {
            // 如果选择取消,则返回false
            return false;
        }
    }
    return true;  // 如果文档没有更改过,则直接返回true
}

// 设置当前文件
void MdiChild::setCurrentFile(const QString& fileName)
{
    // canonicalFilePath()可以除去路径中的符号链接,“.”和“..”等符号
    curFile = QFileInfo(fileName).canonicalFilePath();
    // 文件已经被保存过了
    isUntitled = false;
    // 文档没有被更改过
    document()->setModified(false);
    // 窗口不显示被更改标志
    setWindowModified(false);
    // 设置窗口标题,userFriendlyCurrentFile() 函数返回文件名
    setWindowTitle(userFriendlyCurrentFile() + "[*]");
}

// 关闭操作,在关闭事件中执行
void MdiChild::closeEvent(QCloseEvent* event)
{
    if (maybeSave())
    {
        // 如果 maybeSave() 函数返回 true,则关闭窗口
        event->accept();
    }
    else
    {
        // 否则忽略该事件
        event->ignore();
    }
}

MdiChild::MdiChild(QWidget* parent) : QTextEdit(parent)
{
    // 设置在子窗口关闭时销毁这个类的对象
    setAttribute(Qt::WA_DeleteOnClose);
    // 初始 isUntitled 为 true
    isUntitled = true;
}

// 新建文件操作
void MdiChild::newFile()
{
    // 设置窗口编号,因为编号一直被保存,所以需要使用静态变量
    static int sequenceNumber = 1;
    // 新建的文档没有被保存过
    isUntitled = true;
    // 将当前文件命名为未命名文档加编号,编号先使用再加 1
    curFile = tr("未命名文档%1.txt").arg(sequenceNumber++);
    // 设置窗口标题,使用[*]可以在文档被更改后在文件名称后显示“*”号
    setWindowTitle(curFile + "[*]" + tr(" - 多文档编辑器"));
    // 当文档被更改时发射 contentsChanged() 信号,执行 documentWasModified() 槽函数
    connect(document(), SIGNAL(contentsChanged()), this, SLOT(documentWasModified()));
}

// 加载文件
bool MdiChild::loadFile(const QString& fileName)
{
    // 新建 QFile 对象
    QFile file(fileName);

    // 只读方式打开文件,出错则提示,并返回 false
    if (!file.open(QFile::ReadOnly | QFile::Text))
    {
        QMessageBox::warning(this, tr("多文档编辑器"),
                             tr("无法读取文件 %1:\n%2.").arg(fileName).arg(file.errorString()));
        return false;
    }
    // 新建文本流对象
    QTextStream in(&file);
    // 设置鼠标状态为等待状态
    QApplication::setOverrideCursor(Qt::WaitCursor);
    // 读取文件的全部文本内容,并添加到编辑器中
    setPlainText(in.readAll());
    // 恢复鼠标状态
    QApplication::restoreOverrideCursor();
    // 设置当前文件
    setCurrentFile(fileName);
    connect(document(), SIGNAL(contentsChanged()), this, SLOT(documentWasModified()));
    return true;
}

// 保存操作
bool MdiChild::save()
{
    if (isUntitled)
    {
        // 如果文件未被保存过,则执行另存为操作
        return saveAs();
    }
    else
    {
        // 否则直接保存文件
        return saveFile(curFile);
    }
}

// 另存为操作
bool MdiChild::saveAs()
{
    // 使用文件对话框获取文件路径
    QString fileName = QFileDialog::getSaveFileName(this, tr("另存为"), curFile);
    if (fileName.isEmpty())
    {
        // 如果文件路径为空,则返回 false
        return false;
    }
    // 否则保存文件
    return saveFile(fileName);
}

// 保存文件
bool MdiChild::saveFile(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);
    // 设置应用程序强制光标为等待旋转光标(设置鼠标状态为等待状态)
    QApplication::setOverrideCursor(Qt::WaitCursor);
    // 以纯文本文件写入
    out << toPlainText();
    // 恢复光标(恢复鼠标状态)
    QApplication::restoreOverrideCursor();
    setCurrentFile(fileName);
    return true;
}

// 提取文件名
QString MdiChild::userFriendlyCurrentFile()
{
    // 从文件路径中提取文件名
    return QFileInfo(curFile).fileName();
}

// 文档被更改时,窗口显示更改状态标志
void MdiChild::documentWasModified()
{
    // 根据文档的isModified()函数的返回值,判断编辑器内容是否被更改了
    // 如果被更改了,就要在设置了[*]号的地方显示“*”号,这里会在窗口标题中显示
    setWindowModified(document->isModified());
}

下面对这个类进行简单的测试。

在 mainwindow.cpp 中添加以下代码:

#include "mdichild.h"

因为程序中使用了中文,要在 main.cpp 文件中添加头文件和代码:

#include <QTextCodec>
// 解决 Qt 中文乱码问题
// QTextCodec::setCodecForLocale(QTextCodec::codecForLocale());
QTextCodec::setCodecForLocale(QTextCodec::codecForName("utf-8"));

转到设计模式,在 Action Editor 中“新建文件”动作上右击,转到它的触发信号 triggered() 的槽,并更改如下:

void MainWindow::on_actionNew_triggered()
{
    //创建 MdiChild
    MdiChild* child = new MdiChild;
    //多文档区域添加子窗口
    ui->mdiArea->addSubWindow(child);
    //新建文件
    child->newFile();
    //显示子窗口
    child->show();
}

测试结果:

在这里插入图片描述

打开多个界面也没有问题。

在这里插入图片描述

实现菜单的功能

更新菜单状态与新建文件操作

功能描述:

  • 更新菜单状态,使得一些菜单在开始时处于不可用状态。
  • 实现新建文件操作

更改 mainwindow.h:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

class MdiChild;
#include <QMainWindow>

namespace Ui
{
    class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
private:
    Ui::MainWindow* ui;
    QAction* actionSeparator;    // 间隔器
    MdiChild* activeMdiChild();  // 活动窗口

public:
    explicit MainWindow(QWidget* parent = 0);
    ~MainWindow();

private slots:
    void on_actionNew_triggered();  // 新建文件菜单
    void updateMenus();             // 更新菜单
    MdiChild* createMdiChild();     // 创建子窗口
};

#endif  // MAINWINDOW_H

更改mainwindow.cpp:

#include "mainwindow.h"

#include <QMdiSubWindow>

#include "mdichild.h"
#include "ui_mainwindow.h"

// 活动窗口
MdiChild* MainWindow::activeMdiChild()
{
    // 如果有活动窗口,则将其内的中心部件转换为 MdiChild 类型
    if (QMdiSubWindow* activeSubWindow = ui->mdiArea->activeSubWindow())
        return qobject_cast<MdiChild*>(activeSubWindow->widget());
    // 没有活动窗口,直接返回 0
    return 0;
}

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    // 创建间隔器动作并在其中设置间隔器
    actionSeparator = new QAction(this);
    actionSeparator->setSeparator(true);
    // 更新菜单
    updateMenus();
    // 当有活动窗口时更新菜单
    connect(ui->mdiArea, SIGNAL(subWindowActivated(QMdiSubWindow*)), this, SLOT(updateMenus()));
}

MainWindow::~MainWindow() { delete ui; }

void MainWindow::on_actionNew_triggered()
{
    // 创建 MdiChild
    MdiChild* child = createMdiChild();
    // 新建文件
    child->newFile();
    // 显示子窗口
    child->show();
}

// 更新菜单
void MainWindow::updateMenus()
{
    // 根据是否有活动窗口来设置各个动作是否可用
    bool hasMdiChild = (activeMdiChild() != 0);
    ui->actionSave->setEnabled(hasMdiChild);
    ui->actionSaveAs->setEnabled(hasMdiChild);
    ui->actionPaste->setEnabled(hasMdiChild);
    ui->actionClose->setEnabled(hasMdiChild);
    ui->actionCloseAll->setEnabled(hasMdiChild);
    ui->actionTile->setEnabled(hasMdiChild);
    ui->actionCascade->setEnabled(hasMdiChild);
    ui->actionNext->setEnabled(hasMdiChild);
    ui->actionPrevious->setEnabled(hasMdiChild);
    //设置间隔器是否显示
    actionSeparator->setVisible(hasMdiChild);
    // 有活动窗口且有被选择的文本,剪切复制才可用
    bool hasSelection = (activeMdiChild() && activeMdiChild()->textCursor().hasSelection());
    ui->actionCut->setEnabled(hasSelection);
    ui->actionCopy->setEnabled(hasSelection);
    // 有活动窗口且文档有撤销操作时,撤销动作可用
    ui->actionUndo->setEnabled(activeMdiChild() && activeMdiChild()->document()->isUndoAvailable());
    // 有活动窗口且文档有恢复操作时,恢复动作可用
    ui->actionRedo->setEnabled(activeMdiChild() && activeMdiChild()->document()->isRedoAvailable());
}

// 创建子窗口部件
MdiChild* MainWindow::createMdiChild()
{
    // 创建 MdiChild 部件
    MdiChild* child = new MdiChild;
    //向多文档区域添加子窗口,child 为中心部件
    ui->mdiArea->addSubWindow(child);
    // 根据 QTextEdit 类的是否可以复制信号设置剪切复制动作是否可用
    connect(child, SIGNAL(copyAvailable(bool)), ui->actionCut, SLOT(setEnabled(bool)));
    connect(child, SIGNAL(copyAvailable(bool)), ui->actionCopy, SLOT(setEnabled(bool)));
    // 根据 QTextDocument 类的是否可以撤销恢复信号设置撤销恢复动作是否可用
    connect(child->document(), SIGNAL(undoAvailable(bool)), ui->actionUndo, SLOT(setEnabled(bool)));
    connect(child->document(), SIGNAL(redoAvailable(bool)), ui->actionRedo, SLOT(setEnabled(bool)));
    return child;
}

测试:

在这里插入图片描述

在这里插入图片描述

可以看出,有子窗口时,保存和另存为图标是亮的。

在子窗口输入文字后,撤销图标变亮。

在这里插入图片描述

选中一些文字后,复制和粘贴图标变亮。

在这里插入图片描述

实现打开文件操作

功能描述:

实现打开文件操作。当要打开一个文件时,先判断这个文件是否已经被打开,这样就需要遍历多文档区域子窗口的文件,如果发现该文件已经打开,则直接设置该子窗口为活动窗口;否则加载要打开的文件,,并添加新的子窗口。

更新 mainwindow.h:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

class MdiChild;
class QMdiSubWindow;

#include <QMainWindow>

namespace Ui
{
    class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
private:
    Ui::MainWindow* ui;
    QAction* actionSeparator;                              // 间隔器
    MdiChild* activeMdiChild();                            // 活动窗口
    QMdiSubWindow* findMdiChild(const QString& fileName);  // 查找子窗口

public:
    explicit MainWindow(QWidget* parent = 0);
    ~MainWindow();

private slots:
    void on_actionNew_triggered();             // 新建文件菜单
    void on_actionOpen_triggered();            // 打开文件菜单
    void updateMenus();                        // 更新菜单
    MdiChild* createMdiChild();                // 创建子窗口
    void setActiveSubWindow(QWidget* window);  // 设置活动子窗口
};

#endif  // MAINWINDOW_H

更新 mainwindow.cpp:

#include "mainwindow.h"

#include <QFileDialog>
#include <QMdiSubWindow>

#include "mdichild.h"
#include "ui_mainwindow.h"

// 活动窗口
MdiChild* MainWindow::activeMdiChild()
{
    // 如果有活动窗口,则将其内的中心部件转换为 MdiChild 类型
    if (QMdiSubWindow* activeSubWindow = ui->mdiArea->activeSubWindow())
        return qobject_cast<MdiChild*>(activeSubWindow->widget());
    // 没有活动窗口,直接返回 0
    return 0;
}

// 查找子窗口
QMdiSubWindow* MainWindow::findMdiChild(const QString& fileName)
{
    QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
    // 利用foreach语句遍历子窗口列表,如果其文件路径和要查找的路径相同,则返回该窗口
    foreach (QMdiSubWindow* window, ui->mdiArea->subWindowList())
    {
        MdiChild* mdiChild = qobject_cast<MdiChild*>(window->widget());
        if (mdiChild->currentFile() == canonicalFilePath)
            return window;
    }
    return 0;
}

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    // 创建间隔器动作并在其中设置间隔器
    actionSeparator = new QAction(this);
    actionSeparator->setSeparator(true);
    // 更新菜单
    updateMenus();
    // 当有活动窗口时更新菜单
    connect(ui->mdiArea, SIGNAL(subWindowActivated(QMdiSubWindow*)), this, SLOT(updateMenus()));
}

MainWindow::~MainWindow() { delete ui; }

// 新建文件菜单
void MainWindow::on_actionNew_triggered()
{
    // 创建 MdiChild
    MdiChild* child = createMdiChild();
    // 新建文件
    child->newFile();
    // 显示子窗口
    child->show();
}

// 打开文件菜单
void MainWindow::on_actionOpen_triggered()
{
    // 获取文件路径
    QString fileName = QFileDialog::getOpenFileName(this);
    // 如果路径不为空,则查看该文件是否已经打开
    if (!fileName.isEmpty())
    {
        QMdiSubWindow* existing = findMdiChild(fileName);
        // 如果已经存在,则将对应的子窗口设置为活动窗口
        if (existing)
        {
            ui->mdiArea->setActiveSubWindow(existing);
            return;
        }
        // 如果没有打开,则新建子窗口
        MdiChild* child = createMdiChild();
        if (child->loadFile(fileName))
        {
            ui->statusBar->showMessage(tr("打开文件成功"), 2000);
            child->show();
        }
        else
        {
            child->close();
        }
    }
}

// 更新菜单
void MainWindow::updateMenus()
{
    // 根据是否有活动窗口来设置各个动作是否可用
    bool hasMdiChild = (activeMdiChild() != 0);
    ui->actionSave->setEnabled(hasMdiChild);
    ui->actionSaveAs->setEnabled(hasMdiChild);
    ui->actionPaste->setEnabled(hasMdiChild);
    ui->actionClose->setEnabled(hasMdiChild);
    ui->actionCloseAll->setEnabled(hasMdiChild);
    ui->actionTile->setEnabled(hasMdiChild);
    ui->actionCascade->setEnabled(hasMdiChild);
    ui->actionNext->setEnabled(hasMdiChild);
    ui->actionPrevious->setEnabled(hasMdiChild);
    //设置间隔器是否显示
    actionSeparator->setVisible(hasMdiChild);
    // 有活动窗口且有被选择的文本,剪切复制才可用
    bool hasSelection = (activeMdiChild() && activeMdiChild()->textCursor().hasSelection());
    ui->actionCut->setEnabled(hasSelection);
    ui->actionCopy->setEnabled(hasSelection);
    // 有活动窗口且文档有撤销操作时,撤销动作可用
    ui->actionUndo->setEnabled(activeMdiChild() && activeMdiChild()->document()->isUndoAvailable());
    // 有活动窗口且文档有恢复操作时,恢复动作可用
    ui->actionRedo->setEnabled(activeMdiChild() && activeMdiChild()->document()->isRedoAvailable());
}

// 创建子窗口部件
MdiChild* MainWindow::createMdiChild()
{
    // 创建 MdiChild 部件
    MdiChild* child = new MdiChild;
    //向多文档区域添加子窗口,child 为中心部件
    ui->mdiArea->addSubWindow(child);
    // 根据 QTextEdit 类的是否可以复制信号设置剪切复制动作是否可用
    connect(child, SIGNAL(copyAvailable(bool)), ui->actionCut, SLOT(setEnabled(bool)));
    connect(child, SIGNAL(copyAvailable(bool)), ui->actionCopy, SLOT(setEnabled(bool)));
    // 根据 QTextDocument 类的是否可以撤销恢复信号设置撤销恢复动作是否可用
    connect(child->document(), SIGNAL(undoAvailable(bool)), ui->actionUndo, SLOT(setEnabled(bool)));
    connect(child->document(), SIGNAL(redoAvailable(bool)), ui->actionRedo, SLOT(setEnabled(bool)));
    return child;
}

// 设置活动子窗口
void MainWindow::setActiveSubWindow(QWidget* window)
{
    // 如果传递了窗口部件,则将其设置为活动窗口
    if (!window)
        return;
    ui->mdiArea->setActiveSubWindow(qobject_cast<QMdiSubWindow*>(window));
}

测试:

按下 Ctrl + O,选择要打开的文件。显示如下所示。

在这里插入图片描述

添加子窗口列表

功能描述:

每添加一个子窗口就可以在窗口菜单中罗列它的文件名,而且可以在这个列表中选择一个子窗口,将它设置为活动窗口。

更改 mainwindow.h:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

class MdiChild;
class QMdiSubWindow;
class QSignalMapper;

#include <QMainWindow>

namespace Ui
{
    class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
private:
    Ui::MainWindow* ui;
    QAction* actionSeparator;                              // 间隔器
    QSignalMapper* windowMapper;                           // 信号映射器
    MdiChild* activeMdiChild();                            // 活动窗口
    QMdiSubWindow* findMdiChild(const QString& fileName);  // 查找子窗口

public:
    explicit MainWindow(QWidget* parent = 0);
    ~MainWindow();

private slots:
    void on_actionNew_triggered();             // 新建文件菜单
    void on_actionOpen_triggered();            // 打开文件菜单
    void updateMenus();                        // 更新菜单
    MdiChild* createMdiChild();                // 创建子窗口
    void setActiveSubWindow(QWidget* window);  // 设置活动子窗口
    void updateWindowMenu();                   // 更新窗口菜单
};

#endif  // MAINWINDOW_H

更改 mainwindow.cpp:

#include "mainwindow.h"

#include <QFileDialog>
#include <QMdiSubWindow>
#include <QSignalMapper>

#include "mdichild.h"
#include "ui_mainwindow.h"

// 活动窗口
MdiChild* MainWindow::activeMdiChild()
{
    // 如果有活动窗口,则将其内的中心部件转换为 MdiChild 类型
    if (QMdiSubWindow* activeSubWindow = ui->mdiArea->activeSubWindow())
        return qobject_cast<MdiChild*>(activeSubWindow->widget());
    // 没有活动窗口,直接返回 0
    return 0;
}

// 查找子窗口
QMdiSubWindow* MainWindow::findMdiChild(const QString& fileName)
{
    QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
    // 利用foreach语句遍历子窗口列表,如果其文件路径和要查找的路径相同,则返回该窗口
    foreach (QMdiSubWindow* window, ui->mdiArea->subWindowList())
    {
        MdiChild* mdiChild = qobject_cast<MdiChild*>(window->widget());
        if (mdiChild->currentFile() == canonicalFilePath)
            return window;
    }
    return 0;
}

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 创建间隔器动作并在其中设置间隔器
    actionSeparator = new QAction(this);
    actionSeparator->setSeparator(true);
    // 更新菜单
    updateMenus();
    // 当有活动窗口时更新菜单
    connect(ui->mdiArea, SIGNAL(subWindowActivated(QMdiSubWindow*)), this, SLOT(updateMenus()));

    // 创建信号映射器
    windowMapper = new QSignalMapper(this);
    // 映射器重新发送信号,根据信号设置活动窗口
    connect(windowMapper, SIGNAL(mapped(QWidget*)), this, SLOT(setActiveSubWindow(QWidget*)));
    // 更新窗口菜单,并且设置当窗口菜单将要显示的时候更新窗口菜单
    updateWindowMenu();
    connect(ui->menuW, SIGNAL(aboutToShow()), this, SLOT(updateWindowMenu()));
}

MainWindow::~MainWindow() { delete ui; }

// 新建文件菜单
void MainWindow::on_actionNew_triggered()
{
    // 创建 MdiChild
    MdiChild* child = createMdiChild();
    // 新建文件
    child->newFile();
    // 显示子窗口
    child->show();
}

// 打开文件菜单
void MainWindow::on_actionOpen_triggered()
{
    // 获取文件路径
    QString fileName = QFileDialog::getOpenFileName(this);
    // 如果路径不为空,则查看该文件是否已经打开
    if (!fileName.isEmpty())
    {
        QMdiSubWindow* existing = findMdiChild(fileName);
        // 如果已经存在,则将对应的子窗口设置为活动窗口
        if (existing)
        {
            ui->mdiArea->setActiveSubWindow(existing);
            return;
        }
        // 如果没有打开,则新建子窗口
        MdiChild* child = createMdiChild();
        if (child->loadFile(fileName))
        {
            ui->statusBar->showMessage(tr("打开文件成功"), 2000);
            child->show();
        }
        else
        {
            child->close();
        }
    }
}

// 更新菜单
void MainWindow::updateMenus()
{
    // 根据是否有活动窗口来设置各个动作是否可用
    bool hasMdiChild = (activeMdiChild() != 0);
    ui->actionSave->setEnabled(hasMdiChild);
    ui->actionSaveAs->setEnabled(hasMdiChild);
    ui->actionPaste->setEnabled(hasMdiChild);
    ui->actionClose->setEnabled(hasMdiChild);
    ui->actionCloseAll->setEnabled(hasMdiChild);
    ui->actionTile->setEnabled(hasMdiChild);
    ui->actionCascade->setEnabled(hasMdiChild);
    ui->actionNext->setEnabled(hasMdiChild);
    ui->actionPrevious->setEnabled(hasMdiChild);
    //设置间隔器是否显示
    actionSeparator->setVisible(hasMdiChild);
    // 有活动窗口且有被选择的文本,剪切复制才可用
    bool hasSelection = (activeMdiChild() && activeMdiChild()->textCursor().hasSelection());
    ui->actionCut->setEnabled(hasSelection);
    ui->actionCopy->setEnabled(hasSelection);
    // 有活动窗口且文档有撤销操作时,撤销动作可用
    ui->actionUndo->setEnabled(activeMdiChild() && activeMdiChild()->document()->isUndoAvailable());
    // 有活动窗口且文档有恢复操作时,恢复动作可用
    ui->actionRedo->setEnabled(activeMdiChild() && activeMdiChild()->document()->isRedoAvailable());
}

// 创建子窗口部件
MdiChild* MainWindow::createMdiChild()
{
    // 创建 MdiChild 部件
    MdiChild* child = new MdiChild;
    //向多文档区域添加子窗口,child 为中心部件
    ui->mdiArea->addSubWindow(child);
    // 根据 QTextEdit 类的是否可以复制信号设置剪切复制动作是否可用
    connect(child, SIGNAL(copyAvailable(bool)), ui->actionCut, SLOT(setEnabled(bool)));
    connect(child, SIGNAL(copyAvailable(bool)), ui->actionCopy, SLOT(setEnabled(bool)));
    // 根据 QTextDocument 类的是否可以撤销恢复信号设置撤销恢复动作是否可用
    connect(child->document(), SIGNAL(undoAvailable(bool)), ui->actionUndo, SLOT(setEnabled(bool)));
    connect(child->document(), SIGNAL(redoAvailable(bool)), ui->actionRedo, SLOT(setEnabled(bool)));
    return child;
}

// 设置活动子窗口
void MainWindow::setActiveSubWindow(QWidget* window)
{
    // 如果传递了窗口部件,则将其设置为活动窗口
    if (!window)
        return;
    ui->mdiArea->setActiveSubWindow(qobject_cast<QMdiSubWindow*>(window));
}

// 更新窗口菜单
void MainWindow::updateWindowMenu()
{
    // 先清空菜单,然后再添加各个菜单动作
    ui->menuW->clear();
    ui->menuW->addAction(ui->actionClose);     // 关闭
    ui->menuW->addAction(ui->actionCloseAll);  // 关闭所有窗口
    ui->menuW->addSeparator();                 // 分隔符
    ui->menuW->addAction(ui->actionTile);      // 平铺
    ui->menuW->addAction(ui->actionCascade);   // 层叠
    ui->menuW->addSeparator();                 // 分隔符
    ui->menuW->addAction(ui->actionNext);      // 下一个
    ui->menuW->addAction(ui->actionPrevious);  // 前一个
    ui->menuW->addAction(actionSeparator);
    // 如果有活动窗口,则显示间隔器
    QList<QMdiSubWindow*> windows = ui->mdiArea->subWindowList();
    actionSeparator->setVisible(!windows.isEmpty());
    // 遍历各个子窗口
    for (int i = 0; i < windows.size(); i++)
    {
        MdiChild* child = qobject_cast<MdiChild*>(windows.at(i)->widget());
        QString text;
        // 如果窗口数小于 9,则设置编号为快捷键
        if (i < 9)
        {
            text = tr("&%1 %2").arg(i + 1).arg(child->userFriendlyCurrentFile());
        }
        else
        {
            text = tr("%1 %2").arg(i + 1).arg(child->userFriendlyCurrentFile());
        }
        // 添加动作到菜单
        QAction* action = ui->menuW->addAction(text);
        // 设置动作可以选择
        action->setCheckable(true);
        // 设置当前活动窗口动作为选中状态
        action->setChecked(child == activeMdiChild());
        // 关联动作的触发信号到信号映射器的 map() 槽函数上,这个函数会发射 mapped() 信号
        connect(action, SIGNAL(triggered()), windowMapper, SLOT(map()));
        // 将动作与相应的窗口部件进行映射,在发射 mapped() 信号时就会以这个窗口部件为参数
        windowMapper->setMapping(action, windows.at(i));
    }
}

测试:

在这里插入图片描述

实现其他菜单功能

进入设计模式,点击其他 action 的槽,更新它们的 triggered() 代码:

// 保存菜单
void MainWindow::on_actionSave_triggered()
{
    if (activeMdiChild() && activeMdiChild()->save())
        ui->statusBar->showMessage(tr("文件保存成功"), 2000);
}

// 另存为菜单
void MainWindow::on_actionSaveAs_triggered()
{
    if (activeMdiChild() && activeMdiChild()->saveAs())
        ui->statusBar->showMessage(tr("文件保存成功"), 2000);
}

// 退出菜单
void MainWindow::on_actionExit_triggered()
{
    // 这里的 qApp 是 QApplication 对象的全局指针
    qApp->quit();  // 等效于 qApp->exit(0);
    // 这行代码相当于 QApplication::quit();
}

// 撤销菜单
void MainWindow::on_actionUndo_triggered()
{
    if (activeMdiChild())
        activeMdiChild()->undo();
}

// 恢复菜单
void MainWindow::on_actionRedo_triggered()
{
    if (activeMdiChild())
        activeMdiChild()->redo();
}

// 剪切菜单
void MainWindow::on_actionCut_triggered()
{
    if (activeMdiChild())
        activeMdiChild()->cut();
}

// 复制菜单
void MainWindow::on_actionCopy_triggered()
{
    if (activeMdiChild())
        activeMdiChild()->copy();
}

// 粘贴菜单
void MainWindow::on_actionPaste_triggered()
{
    if (activeMdiChild())
        activeMdiChild()->paste();
}

// 关闭菜单
void MainWindow::on_actionClose_triggered() { ui->mdiArea->closeActiveSubWindow(); }

// 关闭所有窗口菜单
void MainWindow::on_actionCloseAll_triggered() { ui->mdiArea->closeAllSubWindows(); }

// 平铺菜单
void MainWindow::on_actionTile_triggered() { ui->mdiArea->tileSubWindows(); }

// 层叠菜单
void MainWindow::on_actionCascade_triggered() { ui->mdiArea->cascadeSubWindows(); }

// 下一个菜单
void MainWindow::on_actionNext_triggered() { ui->mdiArea->activateNextSubWindow(); }

// 前一个菜单
void MainWindow::on_actionPrevious_triggered() { ui->mdiArea->activatePreviousSubWindow(); }

// 关于菜单
void MainWindow::on_actionAbout_triggered() { QMessageBox::about(this, tr("关于本软件"), tr("开发者:UestcXiye")); }

// 关于 Qt 菜单
void MainWindow::on_actionAboutQt_triggered()
{
    // 这里的 qApp 是 QApplication 对象的全局指针
    qApp->aboutQt();
    // 这行代码相当于 QApplication::aboutQt();
}

测试:

在这里插入图片描述

完善程序功能

保存窗口设置

QSettings 类提供平台无关的永久保存应用程序设置的方法。

在 mainwindow.h 文件新增私有函数声明:

void readSettings();   // 读取窗口设置
void writeSettings();  // 写入窗口设置

再新增 protected 函数声明:

void closeEvent(QCloseEvent* event);  // 关闭事件

在 mainwindow.cpp 文件添加代码:

#include <QSettings>
#include <QCloseEvent>

MainWindow 类的构造函数添加代码:

// 初始窗口时读取窗口设置信息
readSettings();

三个函数的实现:

// 读取窗口设置
void MainWindow::readSettings()
{
    QSettings settings("uestc_xiye", "myMdi");
    QPoint pos = settings.value("pos", QPoint(200, 200)).toPoint();
    QSize size = settings.value("size", QSize(400, 400)).toSize();
    move(pos);
    resize(size);
}

// 写入窗口设置
void MainWindow::writeSettings()
{
    QSettings settings("uestc_xiye", "myMdi");
    // 写入位置信息
    settings.setValue("pos", pos());
    // 写入大小信息
    settings.setValue("size", size());
}

// 关闭事件
void MainWindow::closeEvent(QCloseEvent* event)
{
    // 先执行多文档区域的关闭操作
    ui->mdiArea->closeAllSubWindows();
    // 如果还有窗口没有关闭,则忽略该事件
    if (ui->mdiArea->currentSubWindow())
    {
        event->ignore();
    }
    else
    {
        // 在关闭前写入窗口设置
        writeSettings();
        event->accept();
    }
}

自定义右键菜单

右键菜单默认是英文的,我们需要重新实现 QTextEdit 类的上下文菜单事件。

首先在 mdichild.h 中添加头文件:

#include <QMenu>

新增 protected 函数:

void contextMenuEvent(QContextMenuEvent* e);  // 右键菜单事件

函数定义:

// 右键菜单事件
void MdiChild::contextMenuEvent(QContextMenuEvent* e)
{
    // 创建菜单,并向其中添加动作
    QMenu* menu = new QMenu;
    QAction* undo = menu->addAction(tr("撤销(&U)"), this, SLOT(undo()), QKeySequence::Undo);
    undo->setEnabled(document()->isUndoAvailable());
    QAction* redo = menu->addAction(tr("恢复(&R)"), this, SLOT(redo()), QKeySequence::Redo);
    redo->setEnabled(document()->isRedoAvailable());
    menu->addSeparator();
    QAction* cut = menu->addAction(tr("剪切(&T)"), this, SLOT(cut()), QKeySequence::Cut);
    cut->setEnabled(textCursor().hasSelection());
    QAction* copy = menu->addAction(tr("复制(&C)"), this, SLOT(copy()), QKeySequence::Copy);
    copy->setEnabled(textCursor().hasSelection());
    menu->addAction(tr("粘贴(&P)"), this, SLOT(paste()), QKeySequence::Paste);
    QAction* clear = menu->addAction(tr("清空"), this, SLOT(clear()));
    clear->setEnabled(!document()->isEmpty());
    menu->addSeparator();
    QAction* select = menu->addAction(tr("全选"), this, SLOT(selectAll()), QKeySequence::SelectAll);
    select->setEnabled(!document()->isEmpty());
    // 获取鼠标的位置,然后在这个位置显示菜单
    menu->exec(e->globalPos());
    // 最后销毁这个菜单
    delete menu;
}

测试:

在这里插入图片描述

其他功能

功能描述:

  • 在状态栏中显示编辑器中光标所在的行号和列号
  • 设置窗口的标题和状态栏的一些显示

在 mainwindow.h 添加私有槽声明:

void showTextRowAndCol(); // 显示文本的行号和列号

添加私有函数声明:

void initWindow(); // 初始化窗口

修改函数:

// 创建子窗口部件
MdiChild* MainWindow::createMdiChild()
{
    // 创建 MdiChild 部件
    MdiChild* child = new MdiChild;
    //向多文档区域添加子窗口,child 为中心部件
    ui->mdiArea->addSubWindow(child);
    // 根据 QTextEdit 类的是否可以复制信号设置剪切复制动作是否可用
    connect(child, SIGNAL(copyAvailable(bool)), ui->actionCut, SLOT(setEnabled(bool)));
    connect(child, SIGNAL(copyAvailable(bool)), ui->actionCopy, SLOT(setEnabled(bool)));
    // 根据 QTextDocument 类的是否可以撤销恢复信号设置撤销恢复动作是否可用
    connect(child->document(), SIGNAL(undoAvailable(bool)), ui->actionUndo, SLOT(setEnabled(bool)));
    connect(child->document(), SIGNAL(redoAvailable(bool)), ui->actionRedo, SLOT(setEnabled(bool)));
    // 每当编辑器中的光标位置改变,就重新显示行号和列号
    connect(child, SIGNAL(cursorPositionChanged()), this, SLOT(showTextRowAndCol()));
    return child;
}

函数定义:

// 显示文本的行号和列号
void MainWindow::showTextRowAndCol()
{
    // 如果有活动窗口,则显示其中光标所在的位置
    if (activeMdiChild())
    {
        // 因为获取的行号和列号都是从 0 开始的,所以我们这里进行了加 1
        int rowNum = activeMdiChild()->textCursor().blockNumber() + 1;
        int colNum = activeMdiChild()->textCursor().columnNumber() + 1;
        ui->statusBar->showMessage(tr("%1行 %2列").arg(rowNum).arg(colNum), 2000);
    }
}

下面来看初始化窗口函数。

添加头文件:

#include <QLabel>

定义初始化函数:

// 初始化窗口
void MainWindow::initWindow()
{
    setWindowTitle(tr("多文档编辑器"));
    // 在工具栏上单击鼠标右键时,可以关闭工具栏
    ui->mainToolBar->setWindowTitle(tr("工具栏"));
    // 当多文档区域的内容超出可视区域后,出现滚动条
    ui->mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    ui->mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    ui->statusBar->showMessage(tr("欢迎使用多文档编辑器"));
    QLabel* label = new QLabel(this);
    label->setFrameStyle(QFrame::Box | QFrame::Sunken);
    label->setText(tr("<a href=\"https://blog.csdn.net/ProgramNovice\">CSDN 博客</a>"));
    // 标签文本为富文本
    label->setTextFormat(Qt::RichText);
    // 可以打开外部链接
    label->setOpenExternalLinks(true);
    ui->statusBar->addPermanentWidget(label);
    ui->actionNew->setStatusTip(tr("创建一个文件"));
    // 设置其他动作的状态提示
    ui->actionOpen->setStatusTip(tr("打开一个已经存在的文件"));
    ui->actionSave->setStatusTip(tr("保存文档到硬盘"));
    ui->actionSaveAs->setStatusTip(tr("以新的名称保存文档"));
    ui->actionExit->setStatusTip(tr("退出应用程序"));
    ui->actionUndo->setStatusTip(tr("撤销先前的操作"));
    ui->actionRedo->setStatusTip(tr("恢复先前的操作"));
    ui->actionCut->setStatusTip(tr("剪切选中的内容到剪贴板"));
    ui->actionCopy->setStatusTip(tr("复制选中的内容到剪贴板"));
    ui->actionPaste->setStatusTip(tr("粘贴剪贴板的内容到当前位置"));
    ui->actionClose->setStatusTip(tr("关闭活动窗口"));
    ui->actionCloseAll->setStatusTip(tr("关闭所有窗口"));
    ui->actionTile->setStatusTip(tr("平铺所有窗口"));
    ui->actionCascade->setStatusTip(tr("层叠所有窗口"));
    ui->actionNext->setStatusTip(tr("将焦点移动到下一个窗口"));
    ui->actionPrevious->setStatusTip(tr("将焦点移动到前一个窗口"));
    ui->actionAbout->setStatusTip(tr("显示本软件的介绍"));
    ui->actionAboutQt->setStatusTip(tr("显示Qt的介绍"));
}

最后在 MainWindow 类的构造函数添加:

// 初始化窗口
initWindow();

小结

程序最终演示:

在这里插入图片描述

项目源码

CSDN:Multi-document Editor.zip

GitHub:UestcXiye/Multi-document-Editor

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐