5.4 工作空间部件QWorkspace
工作空间部件QWorkspace提供了一种多文档界面(Multiple Document Interface,MDI)应用程序的实现方法,它可以包容、管理多个子窗口。在一个这样的MDI应用程序中,工作空间部件是作为主窗口的中心部件来使用的,它的子窗口具有和一般的顶层窗口一样的行为模式,比如可以显示、隐藏、最大化和设置窗口标题等。
下面,通过使用工作空间部件来实现第4章的简单文本编辑器的多文档功能。
为了保证工作空间部件的子窗口在关闭时能够保存文档的数据,在不使用事件过滤器(事件过滤器将在第12章“事件处理”中讲述)的情况下,必须要定义一个继承自QTextEdit的自定义窗口部件来实现编辑器的功能,并重写虚函数closeEvent()。
第4章已经详细描述了简单文本编辑器的实现,尽管本节的例子中自定义了一个CMDITextEdit类,但一些基本的函数功能还是相同的。在此主要解释它们的不同点。
首先,看一下自定义类CMDITextEdit的头文件mditextedit.h。
// chapter05/workspace/src/mditextedit.h. #ifndef _MDITEXTEDIT_H_ #define _MDITEXTEDIT_H_ #include <QTextEdit> class CMDITextEdit : public QTextEdit { Q_OBJECT public: CMDITextEdit(bool , const QString &); virtual ~CMDITextEdit(); bool loadFile(const QString &fileName); bool save(); bool saveAs(); bool saveFile(const QString &fileName); QString fileName(); static int count; protected: void closeEvent(QCloseEvent *event); private: bool maybeSave(); void setCurrentFile(const QString &fileName); QString curFile; bool isUntitled; enum{MaxRecentFiles = 9}; signals: void updateRecentFiels(); void counted(int); private slots: void doModified(); }; #endif
自定义类CMDITextEdit继承自QTextEdit,实现文本的编辑以及状态的保存。
公有函数fileName()获取当前文档的文件名字。
公有静态变量count对新建(或打开)的文档进行计数。
信号updateRecentFiles()用于打开或第一次保存时,以通知应用程序更新最近文件列表。
信号counted()通知应用程序在状态栏显示新的文档计数。
下面看一下它的实现文件mditextedit.cpp。
// chapter05/workspace/src/mditextedit.cpp. #include <QtGui> #include "mditextedit.h" int CMDITextEdit::count = 0; CMDITextEdit::CMDITextEdit(bool untitled, const QString& fileName) : isUntitled(untitled), curFile(fileName) { setAttribute(Qt::WA_DeleteOnClose); setWindowTitle(fileName + "[*]"); connect(document(), SIGNAL(contentsChanged()), this, SLOT(doModified())); }
实现文件mditextedit.cpp中,必须首先要对静态成员变量count进行初始化。
类CMDITextEdit的构造函数有两个形参untitled和fileName,第一个参数告诉新建的CMDITextEdit对象新文档是否保存过;参数fileName指定了新建文档的名字。
函数setAttribute()继承自QWidget类,设置窗口部件对象的属性为Qt::WA_DeleteOnClose,即当窗口部件接受关闭事件的时候,销毁掉该窗口部件对象。
CMDITextEdit::~CMDITextEdit() { emit counted(--count); }
在析构函数中,对文档计数变量进行减1操作,并发送信号counted(),以通知应用程序在主窗口的状态栏上显示打开的文档总数。
QString CMDITextEdit::fileName() { return curFile; }
函数fileName()返回当前文档的文件名字。当用户打开文件时,将该函数返回的文件名与用户打开的文件名进行比较,如果相同则不再需要打开该文件。
bool CMDITextEdit::save() { if (isUntitled) { saveAs(); } else { saveFile(curFile); document()->setModified(false); setWindowModified(false); } }
函数save()完成文档的保存。如果文档从来没有保存过,执行另存为操作;否则直接保存并设置底层文档以及文本编辑框窗口部件的状态。
bool CMDITextEdit::saveAs() { QString fileName = QFileDialog::getSaveFileName(this, tr("另存为"), curFile); if (!fileName.isEmpty()) { saveFile(fileName); setCurrentFile(fileName); } }
函数saveAs()完成文档的另存为操作。
bool CMDITextEdit::loadFile(const QString &fileName) { QFile file(fileName); 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); setText(in.readAll()); QApplication::restoreOverrideCursor(); setCurrentFile(fileName); return true; }
函数loadFile()完成文件的加载,并设置当前文本编辑框窗口的状态。
bool CMDITextEdit::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(); return true; }
函数saveFile()实现文本编辑框窗口的文档保存。
void CMDITextEdit::closeEvent(QCloseEvent *event) { if (maybeSave()) event->accept(); else event->ignore(); }
重写函数closeEvent()接收窗口部件的“关闭”事件,如果文档还没有保存,则提示用户保存文件;否则直接关闭该文档的文本编辑框窗口。
void CMDITextEdit::doModified() { setWindowModified(document()->isModified()); }
槽函数doModified()接收文本编辑框窗口的textChanged()信号,设置窗口的编辑状态。
bool CMDITextEdit::maybeSave() { if(document()->isModified()) { QMessageBox box; box.setWindowTitle(tr("警告")); box.setIcon(QMessageBox::Warning); box.setText(tr("文档没有保存,是否保存?")); box.setStandardButtons(QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); switch(box.exec()) { case QMessageBox::Yes : save(); return true; case QMessageBox::No : return true; case QMessageBox::Cancel : return false; } } else return true; }
一般当用户退出文本编辑器应用程序或关闭某个文本编辑框窗口时,调用函数maybeSave(),提示用户是否保存文档:
● 用户选择是,则保存文档,返回“true”(通知调用函数接受“关闭”事件);
● 用户选择否,不保存文档,返回“true”(通知调用函数接受“关闭”事件);
● 用户选择取消,不保存文件,返回“false”(通知调用函数忽略“关闭”事件)。
void CMDITextEdit::setCurrentFile(const QString &fileName) { curFile = QFileInfo(fileName).canonicalFilePath(); isUntitled = false; setWindowTitle(curFile + "[*]"); document()->setModified(false); setWindowModified(false); QSettings settings("709", "MDI example"); QStringList files = settings.value("recentFiles").toStringList(); files.removeAll(fileName); files.prepend(fileName); while (files.size() > MaxRecentFiles) files.removeLast(); settings.setValue("recentFiles", files); emit updateRecentFiels(); }
函数setCurrentFile()完成最近文件列表的更新和保存,并发送信号updateRecentFiels()通知应用程序更新文件菜单中的最近文件列表的内容。
主窗口类CMainWindow和前面第4章简单文本编辑器的主窗口基本相同,不同的是添加了工作空间部件作为主窗口的中心部件,以实现文本编辑器的多文档功能。
主窗口类CMainWindow的定义文件mainwindow.h内容如下。
// chapter05/workspace/src/mainwindow.h. #ifndef _MAINWINDOW_H_ #define _MAINWINDOW_H_ #include "ui_mainwindow.h" class QLabel; class QWorkspace; class QSignalMapper; class CMDITextEdit; class CMainWindow : public QMainWindow, public Ui::MainWindow { Q_OBJECT public: CMainWindow(QWidget* = 0); private: QDockWidget* dockWidget; QLabel* label1; QLabel* label2; enum{MaxRecentFiles = 9}; QAction* recentFileActs[MaxRecentFiles]; QAction* separatorAct; QWorkspace* workspace; void iniDockWidget(); void iniStatusBar(); void iniConnect(); CMDITextEdit* activeWindow(); CMDITextEdit* createMDITextEdit(bool, const QString&);
在类CMainWindow的私有区,新增了必要的公共成员变量和两个成员函数。
成员变量workspace声明了一个工作空间部件QWorkspace的对象指针。
成员函数activeWindow()获取当前应用程序的活动文本编辑框窗口;createMDITextEdit()窗口创建新的文本编辑框窗口。
private slots: void doNew(); void doOpen(); void doClose(); void doSave(); void doASave(); void doQuit(); void doUndo(); void doCut(); void doCopy(); void doPast(); void doAll(); void doFind(); void openRecentFile(); void updateRecentFiles(); void updateMenu(); void showCount(int); }; #endif
在类CMainWindow的私有槽函数声明区,去掉了一些不再使用的槽函数,新加了两个槽函数:updateMenu()在多文档切换的时候更新菜单的状态;showCount(int)在主窗口的状态栏显示文档的总个数。
下面是主窗口类CMainWindow的实现文件mainwindow.cpp的内容。
// chapter05/workspace/src/mainwindow.cpp. #include <QtGui> #include <QDebug> #include <QtCore> #include "mainwindow.h" #include "findfileform.h" #include "mditextedit.h" CMainWindow::CMainWindow(QWidget* parent) : QMainWindow(parent) { setupUi(this); workspace = new QWorkspace; setCentralWidget(workspace); connect(workspace, SIGNAL(windowActivated(QWidget *)), this, SLOT(updateMenu())); iniDockWidget(); iniStatusBar(); iniConnect(); updateRecentFiles(); updateMenu(); showMaximized(); showCount(0); }
构造函数完成应用程序主窗口及其状态的初始化。
主窗口的中心部件初始化为一个工作空间部件。当用户激活工作空间部件的某个子窗口时,工作空间部件将会发出信号QWorkSpace::windowActivated(),信号的参数是被激活的子窗口指针。此处,将该信号关联到槽函数updateMenu(),当新的子窗口被激活时,完成菜单的更新。
void CMainWindow::iniDockWidget() { CFindFileForm* findFileForm = new CFindFileForm; dockWidget = new QDockWidget(tr("查找文件"), this); dockWidget->setAllowedAreas(Qt::RightDockWidgetArea); dockWidget->setFeatures(QDockWidget::AllDockWidgetFeatures); dockWidget->setFloating(false); dockWidget->setWidget(findFileForm); dockWidget->setVisible(false); addDockWidget(Qt::RightDockWidgetArea, dockWidget); }
函数iniDockWidget()完成对锚接部件的初始化,该函数的实现基本上没有变化。
void CMainWindow::iniStatusBar() { QStatusBar* bar = statusBar(); label1 = new QLabel; label1->setMinimumSize(200, 25); label1->setFrameShadow(QFrame::Sunken); label1->setFrameShape(QFrame::WinPanel); label2 = new QLabel; label2->setMinimumSize(200, 25); label2->setFrameShadow(QFrame::Sunken); label2->setFrameShape(QFrame::WinPanel); bar->addWidget(label1); bar->addWidget(label2); }
函数iniStatusBar()完成对状态栏窗口部件的初始化。
void CMainWindow::iniConnect() { connect(actNew, SIGNAL(triggered()), this, SLOT(doNew())); connect(actOpen, SIGNAL(triggered()), this, SLOT(doOpen())); connect(actClose, SIGNAL(triggered()), this, SLOT(doClose())); connect(actSave, SIGNAL(triggered()), this, SLOT(doSave())); connect(actASave, SIGNAL(triggered()), this, SLOT(doASave())); connect(actQuit, SIGNAL(triggered()), this, SLOT(doQuit())); connect(actUndo, SIGNAL(triggered()), this, SLOT(doUndo())); connect(actCut, SIGNAL(triggered()), this, SLOT(doCut())); connect(actCopy, SIGNAL(triggered()), this, SLOT(doCopy())); connect(actPaste, SIGNAL(triggered()), this, SLOT(doPast())); connect(actAll, SIGNAL(triggered()), this, SLOT(doAll())); connect(actFind, SIGNAL(triggered()), this, SLOT(doFind())); separatorAct = menu_F->insertSeparator(actQuit); separatorAct->setVisible(false); for (int i = 0; i < 9; ++i) { recentFileActs[i] = new QAction(this); menu_F->insertAction(separatorAct, recentFileActs[i]); recentFileActs[i]->setVisible(false); connect(recentFileActs[i], SIGNAL(triggered()), this, SLOT(openRecentFile())); } }
函数iniConnect()完成信号和槽的关联。
void CMainWindow::doFind() { dockWidget->show(); dockWidget->setFloating(false); }
槽函数doFind()控制锚接部件的显隐。
void CMainWindow::doNew() { static int sequenceNum = 1; CMDITextEdit* textEdit = createMDITextEdit(true, tr("untitled%1").arg(sequenceNum++)); textEdit->setVisible(true); }
槽函数doNew()响应用户的“新建”操作,创建文本编辑框窗口并进行显示。
局部静态变量sequenceNum记录文档的序列号,作为新建文档的标题的一部分。注意,局部静态变量的初始化是在第一次使用的时候,当再次使用的时候不再进行初始化。
void CMainWindow::doOpen() { QString fileName = QFileDialog::getOpenFileName(this); if (!fileName.isEmpty()) { QWidgetList list = workspace->windowList(); for(int i=0; i<list.count(); i++) { CMDITextEdit* textEdit = qobject_cast<CMDITextEdit*>(list[i]); if(textEdit->fileName() == fileName) { workspace->setActiveWindow(textEdit); return ; } } CMDITextEdit* textEdit = createMDITextEdit(false, fileName); if (textEdit->loadFile(fileName)) { label2->setText(tr("已经读取")); } textEdit->setVisible(true); } }
槽函数doOpen()创建一个新的文本编辑框窗口,并在新建的窗口中打开一个文本文档。函数QWorkspace::setActiveWindow()设置工作空间部件的活动子窗口。
void CMainWindow::doClose() { activeWindow()->close(); }
槽函数doClose()响应用户“关闭”当前文本编辑框窗口的操作,调用文本编辑框窗口的“关闭”槽函数。
void CMainWindow::doSave() { activeWindow()->save(); }
槽函数doSave()响应用户的“保存”操作,调用活动文本编辑框窗口的“保存”函数。
void CMainWindow::doASave() { activeWindow()->saveAs(); }
槽函数doASave()响应用户的“另存为”操作,调用活动文本编辑框窗口的“另存为”函数。
void CMainWindow::doQuit() { QWidgetList list = workspace->windowList(); for(int i=0; i<list.count(); i++) { CMDITextEdit* textEdit = qobject_cast<CMDITextEdit*>(list[i]); textEdit->close(); } qApp->quit(); }
槽函数doQuit()响应用户的“退出”文本编辑器操作,对当前打开的所有文档进行检查,对还没有保存的文档进行“保存”操作提示(比较完善的做法是,重写CMainWindow的closeEvent()函数,以同样的方法处理主窗口的“关闭”操作)。
下面几个槽函数完成活动文本编辑框窗口的编辑操作,包括撤消、剪切、复制、粘贴和选择全部。
void CMainWindow::doUndo() { activeWindow()->undo(); } void CMainWindow::doCut() { activeWindow()->cut(); } void CMainWindow::doCopy() { activeWindow()->copy(); } void CMainWindow::doPast() { activeWindow()->paste(); } void CMainWindow::doAll() { activeWindow()->selectAll(); } void CMainWindow::openRecentFile() { QAction *action = qobject_cast<QAction *>(sender()); if (action) { QString fileName = action->data().toString(); CMDITextEdit* textEdit = createMDITextEdit(false, fileName); textEdit->loadFile(fileName); textEdit->setVisible(true); } }
槽函数openRecentFile()完成用户的打开文件菜单中的最近文件列表文件的操作,创建新的文本编辑框窗口并加载指定的文件。
void CMainWindow::updateRecentFiles() { QSettings settings("709", "MDI example"); QStringList files = settings.value("recentFiles").toStringList(); int numRecentFiles = qMin(files.size(), (int)9); for (int i = 0; i < numRecentFiles; ++i) { QString text = tr("&%1 %2").arg(i + 1).arg(files[i]); recentFileActs[i]->setText(text); recentFileActs[i]->setData(files[i]); recentFileActs[i]->setVisible(true); } for (int i = numRecentFiles; i < 9; ++i) recentFileActs[i]->setVisible(false); separatorAct->setVisible(numRecentFiles > 0); }
槽函数updateRecentFiles()完成应用程序主窗口的文件菜单的最近文件列表的更新,与第4 章具有相同标签的函数不同的是,此处的updateRecentFiles()是一个槽,它接收来自CMDITextEdit对象的信号updateRecentFiles()。
CMDITextEdit* CMainWindow::activeWindow () { CMDITextEdit* textEdit = qobject_cast<CMDITextEdit*>(workspace->activeWindow()); return textEdit; }
函数activeWindow ()获取并返回工作空间部件的当前活动窗口。
函数QWorkspace::activeWindow()获取工作空间部件的当前活动的子窗口。
void CMainWindow::updateMenu() { bool hasChild = (activeWindow() != 0); actSave->setEnabled(hasChild); actASave->setEnabled(hasChild); actPaste->setEnabled(hasChild); actClose->setEnabled(hasChild); actUndo->setEnabled(hasChild); actCut->setEnabled(hasChild); bool selected = (activeWindow() && activeWindow()->textCursor().hasSelection()); actCut->setEnabled(selected); actCopy->setEnabled(selected); }
函数updateMenu()响应QWorkspace的信号QWorkspace::windowActivated(),完成应用程序主菜单状态的更新。当工作空间部件的一个子窗口被激活的时候,将会发出QWorkspace::windowActivated()信号。
函数QTextCursor::hasSelection()判断文本编辑框的文本是否有被选中,如果有返回true;否则返回false。
CMDITextEdit* CMainWindow::createMDITextEdit(bool untitled, const QString& fileName) { CMDITextEdit* textEdit = new CMDITextEdit(untitled, fileName); workspace->addWindow(textEdit); connect(textEdit, SIGNAL(updateRecentFiels()), this, SLOT(updateRecentFiles())); connect(textEdit, SIGNAL(copyAvailable(bool)), actCut, SLOT(setEnabled(bool))); connect(textEdit, SIGNAL(copyAvailable(bool)), actCopy, SLOT(setEnabled(bool))); connect(textEdit, SIGNAL(counted(int)), this, SLOT(showCount(int))); showCount(++textEdit->count); return textEdit; }
函数createMDITextEdit()创建一个新的文本编辑框CMDITextEdit对象,并将该对象添加到工作空间部件QWorkspace中。它的第一个参数untitled指定了新文档的状态,如果untitled = true,表示新的文档从来没有被保存过;而当untitled = false时,表示新的文档曾经被保存过(比如文档的“打开”操作)。第二个参数指定了文件名。
函数QWorkspace::addWindow()将新建的CMDITextEdit对象添加到工作空间部件中。
void CMainWindow::showCount(int count) { label1->setText(tr("文档总数:%1").arg(count)); }
槽函数showCount()更新状态栏中标签窗口部件的显示内容,设置文档总数。
现在,创建一个新的KDevelop工程workspace,创建主程序文件(同第4章的KDevelop工程“designmainwindow”的主程序完全相同),并将在第4 章引用的查找文件类CFindFileForm(包括ui文件)、Qt设计器中绘制的主窗口的ui文件(包括资源文件及其资源)加入到工程中。最后,将上述类CMDITextEdit和CMainWindow的定义文件和实现文件加入到工程中。
编译运行应用程序,运行界面如图5-19所示。
图5-19 QWorkspace实现MDI文本编辑器