子窗口头文件
#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();
}
效果