6.6 多文件介面(Multiple Document Interface)

pamxy發表於2013-06-24

轉自:http://zlhwhj.blog.163.com/blog/static/253900342008125114147389/

一個主視窗中央區域內能夠提供多個文件的程式稱之為多文件程式,或者MDI程式。在Qt中,一個MDI程式是由QWorkspace類實現的,把QWorkspace做為中央控制元件,每一個文件視窗做為QWorkspace的子控制元件。
MDI程式的慣例是提供一個window選單,管理視窗的顯示方式和當前開啟的視窗列表。正在活動的視窗由選中記號標示。使用者可以點選window選單中視窗列表中的一個視窗把它啟用。
在這一節中,我們實現一個圖6.16所示的MDI編輯程式,介紹如何建立MDI程式,如何實現window選單。
Figure 6.16. The MDI Editor application

  

這個應用程式包含兩個類:MainWindowEditor類。程式中大部分程式碼和第一部分的Spreadsheet程式相似,這裡我們只介紹新增的程式碼。
Figure 6.17. The MDI Editor application's menus
 
首先看一下MainWindow類:
MainWindow::MainWindow()
{
    workspace = new QWorkspace;
    setCentralWidget(workspace);
    connect(workspace, SIGNAL(windowActivated(QWidget *)),
            this, SLOT(updateMenus()));
    createActions();
    createMenus();
    createToolBars();
    createStatusBar();
    setWindowTitle(tr("MDI Editor"));
    setWindowIcon(QPixmap(":/images/icon.png"));
}
在MainWindow的建構函式中,我們建立了一個QWorkSpace控制元件,並把這個控制元件做為一箇中央控制元件。連線QWorkSpace的windowActivated()訊號和updateMenus()函式,對window選單進行更新。
void MainWindow::newFile()
{
    Editor *editor = createEditor();
    editor->newFile();
    editor->show();
}
函式newFile()用來對應File|New選單,呼叫createEditor()私有函式建立一個子控制元件Editor。
Editor *MainWindow::createEditor()
{
    Editor *editor = new Editor;
    connect(editor, SIGNAL(copyAvailable(bool)),
            cutAction, SLOT(setEnabled(bool)));
    connect(editor, SIGNAL(copyAvailable(bool)),
            copyAction, SLOT(setEnabled(bool)));
    workspace->addWindow(editor);
    windowMenu->addAction(editor->windowMenuAction());
    windowActionGroup->addAction(editor->windowMenuAction());
    return editor;
}
函式createEditor()建立一個Editor控制元件,連線兩個訊號和槽,這保證了Edit|Cut選單和Edit|Copy選單的使能狀態依賴於是否有選中的文字,如果有選中的文字,Edit|Cut選單和Edit|Copy選單變為可用狀態。
因為是MDI程式,主視窗中可能有多個Editor控制元件。問題是當前活動的Editor視窗發出的copyAvailable(bool)訊號才能改變選單的狀態。實際上也只有當前活動的視窗能夠發出訊號,所以這個問題也不用考慮。
一旦新加了一個Editor控制元件,我們在Window選單中增加一個QAction啟用這個視窗。這個QAction是Editor類提供的,稍後會介紹。我們也向QActionGroup物件中新增了該Action,這樣QActionGroup保證了視窗選單中一次只有一項是選中的,即只有一個視窗是啟用的。
void MainWindow::open()
{
    Editor *editor = createEditor();
    if (editor->open()) {
        editor->show();
    } else {
        editor->close();
    }
}
函式open()對應選單File|Open。為新文件建立一個Editor,並在Editor上呼叫open()函式。這樣保證了檔案操作的實現是在Editor類中實現而非MainWindow類,因為每個Editor類需要維護自己的獨立狀態。
如果open()失敗,關閉Editor,錯誤的原因由Editor類已經告訴使用者。我們不必顯式的刪除Editor物件,在Editor的建構函式中,設定了Qt::WA_DeleteOn_Close屬性,在關閉的同時Editor會自動刪除自己。
void MainWindow::save()
{
    if (activeEditor())
        activeEditor()->save();
}
槽save()功能:如果有一活動Editor,則呼叫當前活動的Editor::save()。具體的實現操作也是在Editor中實現。
Editor *MainWindow::activeEditor()
{
    return qobject_cast<Editor *>(workspace->activeWindow());
}
函式activeEditor()返回當前活動的Editor型別的子視窗指標,如果沒有活動視窗,則返回一個空指標。
void MainWindow::cut()
{
    if (activeEditor())
        activeEditor()->cut();
}
槽cut()呼叫當前活動的Editor::cut(),copy(),paste()函式和cut()函式相同,在此略去不談。
void MainWindow::updateMenus()
{
    bool hasEditor = (activeEditor() != 0);
    bool hasSelection = activeEditor()
                        && activeEditor()->textCursor().hasSelection();
    saveAction->setEnabled(hasEditor);
    saveAsAction->setEnabled(hasEditor);
    pasteAction->setEnabled(hasEditor);
    cutAction->setEnabled(hasSelection);
    copyAction->setEnabled(hasSelection);
    closeAction->setEnabled(hasEditor);
    closeAllAction->setEnabled(hasEditor);
    tileAction->setEnabled(hasEditor);
    cascadeAction->setEnabled(hasEditor);
    nextAction->setEnabled(hasEditor);
    previousAction->setEnabled(hasEditor);
    separatorAction->setVisible(hasEditor);
    if (activeEditor())
        activeEditor()->windowMenuAction()->setChecked(true);
}
當啟用一個視窗或者關閉最後一個視窗時,呼叫updateMenus()槽更新選單,updateMenus()是槽函式,在MainWindow的建構函式呼叫了這個函式,使程式啟動時也能更新選單。
只要有一個活動視窗,大部分選單都是有意義的,如果沒有活動視窗,這些選單都被禁止。最後,呼叫QAction::setChecked()標示活動視窗。由於使用了QActionGroup,以前標示的活動視窗自動取消。
void MainWindow::createMenus()
{
    ...
    windowMenu = menuBar()->addMenu(tr("&Window"));
    windowMenu->addAction(closeAction);
    windowMenu->addAction(closeAllAction);
    windowMenu->addSeparator();
    windowMenu->addAction(tileAction);
    windowMenu->addAction(cascadeAction);
    windowMenu->addSeparator();
    windowMenu->addAction(nextAction);
    windowMenu->addAction(previousAction);
    windowMenu->addAction(separatorAction);
    ...
}
列出的這部分的createMenu()這段程式碼實現了window選單。這些QAction能夠很容易通過QWorkspace的成員函式實現,如,closeActiveWindow(),closeAllWindow(),tile(),cascade(),只要開啟一個新的子視窗,就在window選單中加一個Action。當使用者關閉一個視窗時,相應的window選單項就會刪除,這個Action會自動從Window選單中刪除。
void MainWindow::closeEvent(QCloseEvent *event)
{
    workspace->closeAllWindows();
    if (activeEditor()) {
        event->ignore();
    } else {
        event->accept();
    }
}
虛擬函式closeEvent()給每一個子視窗傳送關閉事件,關閉子視窗。如果還有一個子視窗,這很可能是因為使用者在“unsaved changes”訊息對話方塊中選擇了cancel按鈕,因此忽略這個關閉事件。如果沒有活動視窗,Qt關閉所有的視窗。如果我們不在MainWindow類中重寫closeEvent(),使用者就沒有機會儲存文件的改變。
以上是MainWindow部分的程式碼。接著看Editor類的實現。Editor類代表的是一個子視窗。它繼承自QTextEdit,基類中提供了文字編輯函式。Qt中的所有控制元件都可以做為一個獨立的視窗,因此也能做為MDI中的一個子視窗。
類定義如下:
class Editor : public QTextEdit
{
    Q_OBJECT
public:
    Editor(QWidget *parent = 0);
    void newFile();
    bool open();
    bool openFile(const QString &fileName);
    bool save();
    bool saveAs();
    QSize sizeHint() const;
    QAction *windowMenuAction() const { return action; }
protected:
    void closeEvent(QCloseEvent *event);
private slots:
    void documentWasModified();
private:
    bool okToContinue();
    bool saveFile(const QString &fileName);
    void setCurrentFile(const QString &fileName);
    bool readFile(const QString &fileName);
    bool writeFile(const QString &fileName);
    QString strippedName(const QString &fullFileName);
    QString curFile;
    bool isUntitled;
    QString fileFilters;
    QAction *action;
};
在Spreadsheet程式中的四個私有函式,也同樣出現在Editor類中:okToContinue(),saveFile(),setCurrentFile(),stripptedName()。
Editor::Editor(QWidget *parent)
    : QTextEdit(parent)
{
    action = new QAction(this);
    action->setCheckable(true);
    connect(action, SIGNAL(triggered()), this, SLOT(show()));
    connect(action, SIGNAL(triggered()), this, SLOT(setFocus()));
    isUntitled = true;
    fileFilters = tr("Text files (*.txt)\n"
                     "All files (*)");
    connect(document(), SIGNAL(contentsChanged()),
            this, SLOT(documentWasModified()));
    setWindowIcon(QPixmap(":/images/document.png"));
    setAttribute(Qt::WA_DeleteOnClose);
}
在建構函式中,我們首先建立一個QAction,把它新增到Window選單中表示一個editor,並把這個QAction發出的訊息triggered()和視窗的show(),setFocus()槽連線起來。
這個MDI程式允許使用者建立任意數量的Editor視窗,因此我們必須在新建時給文件一個預設的名字,這樣在儲存時才能把不同的文件區分開。通常的做法是用一個包含一個數字的名字,例如,document1.txt。變數isUntitled區分文件的名字是使用者輸入的還是程式自動生成的。
我們連線文件的contentsChanged()訊號和documentWasModified(),這個函式只是呼叫setWindowModified(true)。
最後設定屬性Qt::WA_DeleteOnClose,在關閉Editor視窗時自動刪除它,避免記憶體洩漏。
void Editor::newFile()
{
    static int documentNumber = 1;
    curFile = tr("document%1.txt").arg(documentNumber);
    setWindowTitle(curFile + "[*]");
    action->setText(curFile);
    isUntitled = true;
    ++documentNumber;
}
在newFile()函式中給新建的文件一個類似document1.txt的名字。這段程式碼放在了newFile()中而不是在建構函式中,是因為documentNumber是一個靜態的變數,在所有的Editor型別的物件中只有一個例項,我們不想呼叫open()函式來開啟一個已存在的文件時也浪費該數字,增加documentNumber的值。
主視窗標題中的[*]是一個位置標識號。在非Mac OS X平臺上表示文件有需要儲存。這個標識號在第三章也出現過。
bool Editor::open()
{
    QString fileName =
            QFileDialog::getOpenFileName(this, tr("Open"), ".",
                                         fileFilters);
    if (fileName.isEmpty())
        return false;
    return openFile(fileName);
}
函式open()通過使用openFile()開啟一個已經存在的檔案。
bool Editor::save()
{
    if (isUntitled) {
        return saveAs();
    } else {
        return saveFile(curFile);
    }
}
如果isUntitled為true,則呼叫函式saveAs()讓使用者給文件輸入一個名字,如果isUntitled為false,呼叫saveFile()函式。
void Editor::closeEvent(QCloseEvent *event)
{
    if (okToContinue()) {
        event->accept();
    } else {
        event->ignore();
    }
}
 
closeEvent()函式是重寫實現的,允許使用者儲存文件的改變。使用者是否儲存在okToContinue()函式中實現,彈出對話方塊“Do you want to save your changes?”,如果okToContinue()返回為true,接受這個關閉事件,否則,忽略這個事件,不關閉視窗。
 
void Editor::setCurrentFile(const QString &fileName)
{
    curFile = fileName;
    isUntitled = false;
    action->setText(strippedName(curFile));
 
    document()->setModified(false);
    setWindowTitle(strippedName(curFile) + "[*]");
    setWindowModified(false);
}
函式setCurrentFile()在openFile()和saveFile()中被呼叫,用以改變curFileisUtitled變數的值,設定視窗標題和子視窗對應的QAction的名稱,設定document()->setModified(false)。如果使用者改變了文件中的文字,QTextDocument會發出contentsChanged()訊號,把“modified”值為true。
 
QSize Editor::sizeHint() const
{
    return QSize(72 * fontMetrics().width('x'),
                 25 * fontMetrics().lineSpacing());
}
用字母“X”的寬度和一行字元的高度做參考,sizeHint()函式返回一個尺寸,QWorkspace用這個尺寸初始化視窗大小。
以下是這個函式的main.cpp檔案:
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QStringList args = app.arguments();
    MainWindow mainWin;
    if (args.count() > 1) {
        for (int i = 1; i < args.count(); ++i)
            mainWin.openFile(args[i]);
    } else {
        mainWin.newFile();
    }
    mainWin.show();
    return app.exec();
}
如果使用者在命令列上指定了一個檔案,則開啟這個檔案,否則新建一個空文件。Qt指定的選項如-style和-font自動由QApplication的建構函式從引數列表中刪除,因此如果在命令列上這樣寫:
mdieditor -style motif readme.txt
QApplication::arguments()返回一個包含兩個字串(mdieditor和readme.text)的QStringList,程式開啟文件readme.txt。
MDI是同時處理多個文件的一種方法。在Mac OS X上,較好的方法是使用多個頂層的視窗,在第三章“多文件”中有介紹。

相關文章