精通Qt4编程
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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文本编辑器