在学习前面一些知识之后,开始进行第一个Qt实战任务,即完成一个多文档编辑器。首先先来看实验成果图,大概就是这么个多文档编辑器。
首先需要在设计模式里进行设计器的设置:
然后就是新建类MdiChild的声明和实现
mdichild.h
#ifndef MDICHILD_H
#define MDICHILD_H
#include
class MdiChild:public QTextEdit
{
Q_OBJECT
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;} //返回当前文件路径
protected:
void closeEvent(QCloseEvent *event); //关闭事件
void contextMenuEvent(QContextMenuEvent *e); //右键菜单事件
private slots:
void documentWasModified(); //文档被更改时,窗口显示更改状态标志
private:
bool maybeSave(); //是否需要保存
void setCurrentFile(const QString &fileName); //设置当前文件
QString curFile; //保存当前文件路径
bool isUntitled; //作为当前文件是否被保存到硬盘上的标志
};
#endif // MDICHILD_H
mdichild.cpp
#include "mdichild.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
MdiChild::MdiChild(QWidget *parent):QTextEdit (parent)
{
//设置在子窗口关闭时销毁这个类的对象
setAttribute(Qt::WA_DeleteOnClose);
//初始isUntitled为true
isUntitled = true;
}
/*
* 设置窗口编号
* 保存文件未被保存过“isUntitled = true”
* 保存文件路径,给curFile赋初值
* 设置子窗口标题
* 关联文档内容改变信号到显示文档更改状态
*/
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()));
}
/*
* 打开指定的文件,并读取文件内容到编辑器
* 设置当前文件setCurrentFile(),该函数可以获取文件路径,完成文件和窗口状态的设置
* 关联文档内容改变信号到显示文档更改状态槽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(documentWasChanged()));
return true;
}
bool MdiChild::save()
{
//如果文件未被保存过,则执行另存为操作,否则直接保存文件
if(isUntitled)
{
return saveAs();
}
else {
return saveFile(curFile);
}
}
bool MdiChild::saveAs()
{
QString fileName = QFileDialog::getSaveFileName(this,tr("另存为"),curFile);
//获取文件路径,如果为空,则返回false,否则保存文件
if(fileName.isEmpty())
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);
}
QString MdiChild::userFriendlyCurrentFile()
{
//从文件路径中提取文件名
return QFileInfo(curFile).fileName();
}
void MdiChild::closeEvent(QCloseEvent *event)
{
//如果maybeSave()函数返回true,则关闭窗口,否则忽略该事件
if(maybeSave())
{
event->accept();
}
else {
event->ignore();
}
}
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());
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;
}
void MdiChild::documentWasModified()
{
//根据文档的isModified()函数的返回值,判断编辑器内容是否被更改了
//如果被更改了,就要在设置[*]号的地方显示“*”号,这里会在窗口标题中显示
setWindowModified(document()->isModified());
}
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();
//如果用户选择是,则返回保存操作的结果;如果选择取消,则返回false
if(box.clickedButton() ==yesBtn)
return save();
else if(box.clickedButton() ==cancelBtn){
return false;
}
}
//如果文档没有更改过,则直接返回true;
return true;
}
void MdiChild::setCurrentFile(const QString &fileName)
{
//canonicalFilePath()可以除去路径中的符号链接,"."和".."等符号
curFile = QFileInfo(fileName).canonicalFilePath();
//文件已经被保存过了
isUntitled = false;
//文档没有被更改过
document()->setModified(false);
//窗口不显示被更改标志
setWindowModified(false);
//设置窗口标题,userFriendlyCurrentFile()返回文件名
setWindowTitle(userFriendlyCurrentFile()+"[*]");
}
然后是mainwindow类的声明与实现
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
class QMdiSubWindow;
class MdiChild;
class QSignalMapper;
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void updateMenus(); //更新菜单
MdiChild* createMdiChild(); //创建子窗口
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_actionCloseAll_triggered();
void on_actionAbout_triggered();
void on_actionAboutQt_triggered();
void on_actionExit_triggered();
private:
QMdiSubWindow *findMdiChild(const QString &fileName);//查找子窗口
void readSettings(); //读取窗口设置
void writeSettings(); //写入窗口设置
void initWindow(); //初始化窗口
protected:
void closeEvent(QCloseEvent* event); //关闭事件
private:
Ui::MainWindow *ui;
QAction *actionSeparator; //间隔器
MdiChild *activeMdiChild(); //活动窗口
QSignalMapper *windowMapper; //信号映射器
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include"mdichild.h"
#include
#include
#include
#include
#include
#include
#include
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
/*
* 初始化actionSeparator动作,然后执行更新菜单函数,并关联多文档区域的活动子窗口信号到更新菜单槽上
* 每当更换子窗口时,都会更新菜单状态
*/
//创建间隔期动作并在其中设置间隔期
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->menu_W,SIGNAL(aboutToShow()),this,SLOT(updateWindowMenu()));
readSettings(); //初始窗口时读取窗口设置信息
//初始化窗口
initWindow();
}
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);
//有活动窗口且有被选择的文本,剪切复制才可用
bool hasSelection =(activeMdiChild() &&activeMdiChild()->textCursor().hasSelection());
ui->actionCut->setEnabled(hasSelection);
ui->actionPaste->setEnabled(hasSelection);
//有活动窗口且有撤销操作时撤销动作可用
ui->actionUndo->setEnabled(activeMdiChild()&&activeMdiChild()->document()->isUndoAvailable());
//有活动窗口且文档有恢复操作时恢复动作可用
ui->actionRedo->setEnabled(activeMdiChild()&&activeMdiChild()->document()->isRedoAvailable());
}
/*
* 这个函数中创建了MdiChild部件,并将它作为子窗口的中心部件,然后添加到多文档区域。
* 下面关联了编辑器的信号和我们的菜单动作,让它们可以随着文档的改变而改变状态
* 最后返回了MdiChild对象指针。
* 之所以要添加这样一个函数,是因为在下面的打开操作中还要使用到这个函数中的功能,所以将它们从新建文件菜单的触发信号槽中提取出来,另写了这样一个函数
*/
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::setActiveSubWindow(QWidget *window)
{
//如果传递了窗口部件,则将其设置为活动窗口
if(!window)
return;
ui->mdiArea->setActiveSubWindow(qobject_cast<QMdiSubWindow*>(window));
}
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::updateWindowMenu()
{
//先清空菜单,然后在添加各个菜单动作
ui->menu_W->clear();
ui->menu_W->addAction(ui->actionClose);
ui->menu_W->addAction(ui->actionCloseAll);
ui->menu_W->addSeparator();
ui->menu_W->addAction(ui->actionTile);
ui->menu_W->addAction(ui->actionCascade);
ui->menu_W->addSeparator();
ui->menu_W->addAction(ui->actionNext);
ui->menu_W->addAction(ui->actionPrevious);
ui->menu_W->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->menu_W->addAction(text);
action->setCheckable(true);
//设置当前活动窗口动作为选中状态
action->setChecked(child ==activeMdiChild());
//关联动作的触发信号到信号映射器的map()槽,这个槽会发射mapped()信号
connect(action,SIGNAL(triggered()),windowMapper,SLOT(map()));
//将动作与相应的窗口部件进行映射
//在发射mappedd()信号时就会以这个窗口部件为参数
windowMapper->setMapping(action,windows.at(i));
}
}
void MainWindow::showTextRowAndCol()
{
//如果有活动窗口,则显示其中光标所在的位置
if(activeMdiChild())
{
//因为获取的行号和列号都是从0开始的,所以我们这里进行了加1
int rowNum = activeMdiChild()->textCursor().blockNumber()+1;
int colNum = activeMdiChild()->textCursor().blockNumber()+1;
ui->statusBar->showMessage(tr("%1行 %2列").arg(rowNum).arg(colNum),2000);
}
}
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;
}
void MainWindow::readSettings()
{
QSettings settings("yafeilinux","myMdi");
QPoint pos =settings.value("pos",QPoint(200,200)).toPoint();
QSize size =settings.value("size",QSize(400,200)).toSize();
move(pos);
resize(size);
}
void MainWindow::writeSettings()
{
QSettings settings("yafeilinux","myMdi");
//写入位置信息和大小
settings.setValue("pos",pos());
settings.setValue("size",size());
}
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("yafeilinux.com"));
//标签文字为富文本
label->setTextFormat(Qt::RichText);
//可以打开外部链接
label->setOpenExternalLinks(true);
ui->statusBar->addPermanentWidget(label);
ui->actionNew->setStatusTip(tr("创建一个文件"));
ui->actionOpen->setStatusTip(tr("打开一个文件"));
}
void MainWindow::closeEvent(QCloseEvent *event)
{
//先执行多文档区域的关闭操作
ui->mdiArea->closeAllSubWindows();
//如果还有窗口没有关闭,则忽略该事件
if(ui->mdiArea->currentSubWindow())
event->ignore();
else {
//在关闭前写入窗口设置
writeSettings();
event->accept();
}
}
/*
* 这个函数中使用了QMdiArea类的activeSubWindow()函数来获得多文档区域的活动子窗口,
* 然后使用了T qobject_cast()函数来进行类型转换。这个函数时QObject类的函数,它将object对象指针转换为T类型的对象指针
* 这里将活动窗口的中心部件QWidget类型指针转换为MdiChild类型的指针
* 这里的T类型必须是直接或者间接继承自QObject类,并且在其定义中要有Q_OBJECT宏变量
*/
MdiChild *MainWindow::activeMdiChild()
{
//如果有活动窗口,则将其内的中心部件转换为MdiChild类型,没有则直接返回0
if(QMdiSubWindow *activeSubWindow = ui->mdiArea->activeSubWindow())
return qobject_cast<MdiChild*>(activeSubWindow->widget());
return 0;
}
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_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_actionAbout_triggered()
{
QMessageBox::about(this,"关于",tr("致力于多文档编辑器普及工作"));
}
void MainWindow::on_actionAboutQt_triggered()
{
QMessageBox::about(this,"关于Qt",tr("Qt是一个1991年由Qt Company开发的跨平台C++图形用户界面应用程序开发框架。它既可以开发GUI程序,也可用于开发非GUI程序,比如控制台工具和服务器。Qt是面向对象的框架,使用特殊的代码生成扩展(称为元对象编译器(Meta Object Compiler, moc))以及一些宏,Qt很容易扩展,并且允许真正地组件编程。"));
}
void MainWindow::on_actionExit_triggered()
{
//等价于QApplication::closeAllWindows();
qApp->closeAllWindows();
}
最后是main函数
#include "mainwindow.h"
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}