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