Qt 示例之多文档编辑器

14 篇文章 0 订阅

子窗口头文件

#ifndef MINICHILD_H
#define MINICHILD_H

#include <QWidget>
#include <QTextEdit>
#include <QCloseEvent>
#include <QMenu>

class MiniChild : public QTextEdit
{
    Q_OBJECT
public:
    explicit MiniChild(QWidget *parent = nullptr);
    void newFile(); // 新建操作
    bool loadFile(const QString &fileName); // 加载文件
    bool save(); // 保存文件
    bool saveAs(); // 文件另存为
    bool saveFile(const QString &fileName); // 保存文件
    QString userFriendlyCurrentFile(); // 提取文件名
    QString currentFile() {return curFile;} // 返回当前文件路径

protected:
    void closeEvent(QCloseEvent *event) override; // 关闭事件
    void contextMenuEvent(QContextMenuEvent *e) override;

private slots:
    void documentWasModified(); // 文档被更改时, 窗口显示更改状态图标

private:
    bool maybeSave(); // 是否需要保存
    void setCurrentFile(const QString &fileName); // 设置当前文件
    QString curFile; // 保存当前文件路径
    bool isUntitled; // 作为当前文件是否被保存到硬盘上的标志
};

#endif // MINICHILD_H

 

子窗口实现文件

#include "minichild.h"

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

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

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


void MiniChild::documentWasModified()
{
    // 根据文档的isModified()的返回值,判断编辑器内容是否被更改了
    // 如果被更改了,就要再设置了[*]号的地方显示 * 号
    setWindowModified(document()->isModified());
}


bool MiniChild::loadFile(const QString &fileName)
{
    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;
}

void MiniChild::setCurrentFile(const QString &fileName)
{
    // canonicalFilePath() 可以除去路径中的符号链接, . 和 .. 等符号
    curFile = QFileInfo(fileName).canonicalFilePath();
    // 文件已经被保存过了
    isUntitled = false ;
    // 文档没有被更改过
    document()->setModified(false);
    setWindowTitle(userFriendlyCurrentFile() + "[*]");
}

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

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

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

bool MiniChild::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;
}

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

void MiniChild::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;
}

bool MiniChild::maybeSave()
{
    // 如果文件被更改过
    if (document()->isModified()) {
        QMessageBox box;
        box.setWindowTitle(tr("多文档编辑器"));
        box.setText(tr("是否保存对%1的更改?").arg(userFriendlyCurrentFile()));
        box.setIcon(QMessageBox::Warning);
        QPushButton *yesBtn = box.addButton(tr("是(&Y)"), QMessageBox::YesRole);
        box.addButton(tr("否(&N)"), QMessageBox::NoRole);
        QPushButton *cancenBtn = box.addButton(tr("取消"), QMessageBox::RejectRole);

        // 弹出对话框
        box.exec();

        // 如果用户选择是,则返回保存操作的结果,如果选择取消,则返回false
        if (box.clickedButton() == yesBtn) {
            return save();
        } else if (box.clickedButton() == cancenBtn) {
            return false;
        }
    }
    // 如果文档没有更改过,则直接返回true
    return true;
}

 

主页面头文件

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include "minichild.h"
#include <QMainWindow>
#include <QMdiSubWindow>
#include <QSignalMapper>
#include <QCloseEvent>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

protected:
    void closeEvent(QCloseEvent *event);

private slots:
    void updateMenus(); // 更新菜单
    MiniChild *createdMiniChild(); // 创建子窗口
    void setActiveSubWindow(QWidget *window); // 设置活动子窗口
    void updateWindowMenu(); //更新窗口菜单
    void showTextRowAndCol(); // 显示文本的行号和列号


    void on_actionNew_triggered();

    void on_actionOpen_triggered();

    void on_actionSave_triggered();

    void on_actionSaveAs_triggered();

    void on_actionUndo_triggered();

    void on_actionRedo_triggered();

    void on_actionCut_triggered();

    void on_actionCopy_triggered();

    void on_actionPaste_triggered();

    void on_actionClose_triggered();

    void on_actionQuit_triggered();

    void on_actionTile_triggered();

    void on_actionCascade_triggered();

private:
    QAction *actionSeparator; // 间隔器
    MiniChild *activeMiniChild(); // 活动窗口
    QMdiSubWindow *findMiniChild(const QString &fileName);// 查找子窗口
    void readSettings(); // 读取窗口设置
    void writeSettings(); // 写入窗口设置
    void initWindow(); // 初始化窗口

    QSignalMapper *windowMapper; // 信号映射器
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

 

主页面实现文件

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include <QFileDialog>
#include <QSettings>
#include <QLabel>

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->menuWindow, SIGNAL(aboutToShow()), this, SLOT(updateWindowMenu()));

    // 初始化读取窗口设置
    readSettings();
}

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


void MainWindow::updateMenus()
{
    bool hasMiniChild = (activeMiniChild() != 0);
    ui->actionSave->setEnabled(hasMiniChild);
    ui->actionSaveAs->setEnabled(hasMiniChild);
    ui->actionPaste->setEnabled(hasMiniChild);
    ui->actionClose->setEnabled(hasMiniChild);
    ui->actionCloseAll->setEnabled(hasMiniChild);
    ui->actionTile->setEnabled(hasMiniChild);
    ui->actionCascade->setEnabled(hasMiniChild);
    ui->actionNext->setEnabled(hasMiniChild);
    ui->actionPrevious->setEnabled(hasMiniChild);

    // 设置间隔器是否显示
    actionSeparator->setVisible(hasMiniChild);
    // 有活动窗口且被选择的文本,剪切复制才可用
    bool hasSelection = (activeMiniChild() && activeMiniChild()->textCursor().hasSelection());
    ui->actionCut->setEnabled(hasSelection);
    ui->actionCopy->setEnabled(hasSelection);
    // 有活动窗口且文档有恢复操作时恢复动作可以
    ui->actionRedo->setEnabled(activeMiniChild() && activeMiniChild()->document()->isRedoAvailable());
    // 有活动窗口且文档有撤销操作时册小动作可以
    ui->actionUndo->setEnabled(activeMiniChild() && activeMiniChild()->document()->isUndoAvailable());
}

void MainWindow::updateWindowMenu()
{
    // 先清空菜单,然后再添加各个菜单动作
    ui->menuWindow->clear();
    ui->menuWindow->addAction(ui->actionClose);
    ui->menuWindow->addAction(ui->actionCloseAll);
    ui->menuWindow->addSeparator();
    ui->menuWindow->addAction(ui->actionTile);
    ui->menuWindow->addAction(ui->actionCascade);
    ui->menuWindow->addSeparator();
    ui->menuWindow->addAction(ui->actionNext);
    ui->menuWindow->addAction(ui->actionPrevious);
    ui->menuWindow->addAction(actionSeparator);
    // 如果有活动窗口,则显示间隔器
    QList<QMdiSubWindow *> windows = ui->mdiArea->subWindowList();
    actionSeparator->setVisible(!windows.isEmpty());
    // 遍历各个子窗口
    for (int i = 0; i< windows.size(); ++i) {
        MiniChild *child = qobject_cast<MiniChild *>(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->menuWindow->addAction(text);
        action->setCheckable(true);
        // 设置当前活动窗口动作为选中状态
        action->setChecked(child == activeMiniChild());
        // 关联动作的触发信号映射器的map槽,这个槽会发射mapped信号
        connect(action, SIGNAL(triggered()), windowMapper, SLOT(map()));
        // 将动作与相应的窗口部件进行映射
        // 再发射mapped信号时就会以这个窗口部件为参数
        windowMapper->setMapping(action, windows.at(i));
    }

}

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

MiniChild *MainWindow::createdMiniChild()
{
    // 创建MiniChild部件
    MiniChild *child = new MiniChild;
    // 向多文档区域添加子窗口,child为中心部件
    ui->mdiArea->addSubWindow(child);
    // 根据QTextEdit类的是否可以复制信号设置剪切复制动作是否可用
    connect(child, SIGNAL(cutAvailable(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;

}


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

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

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

void MainWindow::writeSettings()
{
    QSettings settings("yafeilinux", "miniEditor");
    // 写入位置信息和大小信息
    settings.setValue("pos", pos());
    settings.setValue("size", size());
}

void MainWindow::readSettings()
{
    QSettings settings("yafeilinux", "miniEditor");
    QPoint pos = settings.value("pos", QPoint(200, 200)).toPoint();
    QSize size = settings.value("size", QSize(400, 400)).toSize();
    move(pos);
    resize(size);
}

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

        ui->statusbar->showMessage(tr("%1 行 %2 列").arg(rowNum).arg(colNum), 2000);
    }
}

void MainWindow::initWindow()
{
    setWindowTitle(tr("多文档编辑器"));
    // 在工具栏上右击时,可以关闭工具栏
    // ui->mainToolBar->setWindowTitle(tr("工具栏"));
    // 当多文档区域的内容超出可视化区域后,出现滚动条
    // setHorizontalScrollBarPolicy 此属性保存水平滚动条的策略
    // ScrollBarAsNeeded 当内容太大而无法容纳时,QAbstractScrollArea会显示一个滚动条
    ui->mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    // setVerticalScrollBarPolicy 此属性保存垂直滚动条的策略
    ui->mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);

    ui->statusbar->showMessage(tr("欢迎使用多文档编辑器"));

    QLabel *label = new QLabel(this);
    // Box QFrame在其内容周围绘制一个框
    // Sunken 框架和内容出现凹陷;使用当前颜色组的浅色和深色绘制三维凹陷线
    label->setFrameStyle(QFrame::Box | QFrame::Sunken);
    label->setText(tr("<a href=\"http:://www.qt.io\">qt.io</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->actionQuit->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"));
}

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

void MainWindow::on_actionOpen_triggered()
{
    // 获取文件路径
    QString fileName = QFileDialog::getOpenFileName();
    // 如果路径不为空, 则查看该文件是否已经打开
    if (!fileName.isEmpty()) {
        QMdiSubWindow *existing = findMiniChild(fileName);
        if(existing) {
            ui->mdiArea->setActiveSubWindow(existing);
            return ;
        }
        // 如果没有打开,则新建子窗口
        MiniChild *child = createdMiniChild();
        if (child->loadFile(fileName)) {
            ui->statusbar->showMessage(tr("打开文件成功"), 2000);
            child->show();
        } else {
            child->close();
        }
    }
}

void MainWindow::on_actionSave_triggered()
{
    if (activeMiniChild() && activeMiniChild()->save()) {
        ui->statusbar->showMessage(tr("文件保存成功"), 2000);
    }
}

void MainWindow::on_actionSaveAs_triggered()
{
    if (activeMiniChild() && activeMiniChild()->saveAs()) {
        ui->statusbar->showMessage(tr("文件保存成功"), 2000);
    }
}

void MainWindow::on_actionUndo_triggered()
{
    if (activeMiniChild()) activeMiniChild()->undo();
}

void MainWindow::on_actionRedo_triggered()
{
    if (activeMiniChild()) activeMiniChild()->redo();
}

void MainWindow::on_actionCut_triggered()
{
    if (activeMiniChild()) activeMiniChild()->cut();
}

void MainWindow::on_actionCopy_triggered()
{
    if (activeMiniChild()) activeMiniChild()->copy();
}

void MainWindow::on_actionPaste_triggered()
{
    if (activeMiniChild()) activeMiniChild()->paste();
}

void MainWindow::on_actionClose_triggered()
{
    ui->mdiArea->closeActiveSubWindow();
}

void MainWindow::on_actionQuit_triggered()
{
    qApp->closeAllWindows();
}

void MainWindow::on_actionTile_triggered()
{
    ui->mdiArea->tileSubWindows();
}

void MainWindow::on_actionCascade_triggered()
{
    ui->mdiArea->cascadeSubWindows();
}

 

效果

 

 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值