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

news2024/11/14 9:19:25

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

  • Qt 项目实战 | 多界面文本编辑器
    • 界面设计
    • 创建子窗口类
    • 实现菜单的功能
      • 更新菜单状态与新建文件操作
      • 实现打开文件操作
      • 添加子窗口列表
      • 实现其他菜单功能
    • 完善程序功能
      • 保存窗口设置
      • 自定义右键菜单
      • 其他功能
    • 小结
      • 项目源码

官方博客: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(updateWindowMenu()));
    // 更新窗口菜单,并且设置当窗口菜单将要显示的时候更新窗口菜单
    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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1133704.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

初试Shiro

Shiro是一个用于身份验证、授权和会话管理的Java安全框架。它提供了一套易于使用的API&#xff0c;可以帮助开发人员构建安全性强大的应用程序。 环境准备 添加依赖 <dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</ar…

vueDay04——v-if else show

一、v-if的使用 我们可以像c语言一样去使用v-if结构 比如单用v-if&#xff0c;连用v-if v-else&#xff0c;或者是v-if v-else-if v-else 注意&#xff1a; 1.v-if v-else-if需要绑定值,而v-else不需要绑定值 2.if结构可以用在不同的标签类型之间 <div v-if"fir…

【MATLAB源码-第57期】基于matlab的IS95前向链路仿真,输出误码率曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 IS-95&#xff0c;也被称为cdmaOne&#xff0c;是第一代的CDMA&#xff08;Code Division Multiple Access&#xff0c;码分多址&#xff09;数字蜂窝通信标准。IS-95的全称是Interim Standard-95&#xff0c;最初由Qualcomm…

灵活、可用、高扩展,EasyMR 带来全新 Yarn 的队列管理功能及可视化配置

YARN&#xff08;Yet Another Resource Negotiator&#xff09;是 Hadoop 生态系统中的资源调度器&#xff0c;主要用于资源管理和作业调度。YARN 自身具备队列管理功能&#xff0c;通过对 YARN 资源队列进行配置和管理&#xff0c;实现集群资源的分配&#xff0c;以满足不同应…

Doceker-compose——容器群集编排管理工具

目录 Docker-compose 1、Docker-compose 的三大概念 2、YAML文件格式及编写注意事项 1&#xff09;使用 YAML 时需要注意下面事项 2&#xff09;ymal文件格式 3&#xff09;json格式 3、Docker Compose配置常用字段 4、Docker-compose的四种重启策略 5、Docker Compos…

[微服务]Spring Cloud Sleuth全链路追踪

Spring Cloud Sleuth全链路追踪 全链路追踪组件追踪的数据遵循的规则: 1,Span:基本单元; 执行一次服务调用就生成一个span,用于记录当时的情况 ,以一个64位ID作为唯一标识.span还有其他数据标识如摘要,时间戳信息,关键tag等; 2,Trace:一次请求; 以一个64位ID为唯一标识,可以…

各品牌PLC存储器寻址的规则

在PLC编程时&#xff0c;字节或多字节的变量一般支持绝对地址寻址&#xff08;比如&#xff0c;IW0、MD4等&#xff09;。要想正确寻址&#xff0c;则必须要搞清楚寻址的规则。目前常见的规则有两种&#xff1a;字节寻址和字寻址。下图清晰地表达了两种规则的编号情况&#xff…

AI:WEB 1 靶机

1.找ip地址 2.检测端口 3.上文件检索 发现是apache 的组件 4.漏洞扫描攻击 nikto -h 网站 扫描网站漏洞 目录爆破 5.利用发现敏感目录 6.登录 发现 都是403 使用上层发现ok 这是一个查询功能点 7.敏感目录 sql注入 检验 详细信息 检测到注入方式 查看数…

系统架构设计师之RUP软件开发生命周期

系统架构设计师之RUP软件开发生命周期

EasyAR使用

EazyAR后台管理&#xff0c;云定位服务 建模 需要自行拍摄360度视频&#xff0c;后台上传&#xff0c;由EazyAR工作人员完成构建。 标注数据 需要在unity安装EazyAR插件&#xff0c;在unity场景编辑后&#xff0c;上传标注数据。 uinity标注数据 微信小程序中使用&#x…

Kali Linux 安装使用远程桌面连接远程服务器

1. 安装远程桌面连接软件 目前为止&#xff0c;我们已经可以通过其它机器以远程桌面的方式连接 Kali Linux&#xff0c;但想要使用 Kali Linux 远程连接其它机器还无法做到&#xff0c;下面就看看如何实现。 更新软件源列表&#xff1b; $ sudo apt-get update $ sudo apt-g…

二、可行性分析与需求分析

文章目录 概念考点练习题一、可行性分析与需求分析1.可行性分析的任务2.可行性研究3.甘特图4.数据流图5.数据字典数据字典的内容 6.需求分析7. 实体联系ER图8. 状态转换图 二、练习题 概念考点练习题 一、可行性分析与需求分析 1.可行性分析的任务 用最小的代价在尽可能短的时…

Xilinx MicroBlaze定时器中断无法返回主函数问题解决

最近在使用Xilinx 7系列FPGA XC7A100T时&#xff0c;运行MicroBlaze软核处理器&#xff0c;添加了AXI TIMER IP核&#xff0c;并使能定时器溢出中断&#xff0c;发现定时器触发中断后&#xff0c;无法返回主函数的问题&#xff0c;最后发现修改编译器优化等级就正常了。 FPGA型…

量子力学的基础公设

量子力学的基础公设 - 知乎

MySQL---JDBC编程

文章目录 什么是JDBC&#xff1f;JDBC的工作原理JDBC的使用添加依赖创建数据源DataSource创建数据库连接Connection创建操作命令Statement执行SQL指令释放资源 通过JDBC演示CRUD新增查询修改删除 什么是JDBC&#xff1f; JDBC&#xff1a;Java Database Connectivity&#xff…

10000阅读量感言

目录 前言 10000阅读量 回忆 感谢 结尾 前言 2023年10月25日20点26分&#xff0c;我终于突破10000阅读量了&#xff01;为了记录下来&#xff0c;我写下了这篇博客。 10000阅读量 以上是我卡点截的一张图&#xff0c;我当时看到自己10000阅读量还是很惊喜的&#xff0c;还…

算法训练营第一天 704 .二分查找、27.移除元素

算法训练营第一天 | 704 .二分查找、27.移除元素 &#xff08; 一 &#xff09;、704 二分查找 题目链接&#xff1a;https://leetcode.cn/problems/binary-search/description/ 解题思路&#xff1a; ​ 数组 nums 是有序排列的&#xff0c;二分查找每次都是对半查询&…

【离散数学必刷题】命题逻辑(第一章 左孝凌)刷完包过!

复习16题&#xff1a; 【1】下列哪个语句是真命题&#xff08;&#xff09; A、今天天气真好&#xff01; B、我正在说谎。 C、如果7 2 10 &#xff0c;那么4 6 5。 D、如果7 2 9 &#xff0c; 则 4 6 5。 对于A&#xff0c;只有具有确定真值的陈述句才是命题&#xf…

TotalFinder v1.15.1(Finder代替工具)

TotalFinder是一款功能强大的文件管理工具&#xff0c;为Mac用户提供了一整套增强的文件管理功能和工具&#xff0c;能够更高效地管理和组织文件。 TotalFinder的主要特点包括&#xff1a; 为Finder添加了选项卡功能&#xff0c;允许多个文件夹在同一个窗口中同时打开。用户可…

一文精通C++ -- 继承

前言&#xff1a;继承是C类和对象三大特性中关键的一环&#xff0c;上承封装&#xff0c;下接多态&#xff0c;C中的继承是一种面向对象编程的概念&#xff0c;它允许一个类&#xff08;称为子类或派生类&#xff09;继承另一个类&#xff08;称为父类或基类&#xff09;的属性…