[Qt及Qt Quick開發實戰精解] 第1章 多文檔編輯器


  這一章的例子是對《Qt Creator快速人門》基礎應用篇各章節知識的綜合應用, 也是一個規范的實例程序。之所以說其規范,是因為在這個程序中,我們對菜單什么時候可用/什么時候不可用、關閉程序時應該先保存已修改且尚未保存的文件等細節都做了嚴格的約束。而一個真正實用的應用程序,也就應該如此。

  本章應用了基礎篇的眾多知識點,但這里只是講解程序流程與框架,沒有涉及太多知識細節的講解。這個實例主要是對主窗口部件的應用,所以可以學完《Qt Creator快速入門》的前5章再來學習本章,這樣可以達到更好的效果。該實例是基於Qt中的MDI Example示例程序 的,它在Main Windows分類下。這個程序就是以QMainWindow類為主窗口,以QMdiArea類為多文檔區域,以QTextEdit類為子窗口部件,從而實現了一個多文檔 編輯器的應用。最終的運行效果如圖1-1所示。

![](https://img2018.cnblogs.com/blog/1075214/201812/1075214-20181216222023925-1822418411.png) 圖1-1 多文檔編輯器界面

1.1 界面設計

  先進行界面的設計,這里主要是對主窗口菜單欄和工具欄的設計。打開Qt Creator,創建新的項目。(項目源碼路徑:src\1 \1-1\myMdi)新建Qt Gui應用,項目名稱myMdi,類名默認為MainWindow,基類默認為QMainWindow都不做改動。 完成后雙擊mainwindow. ui文件進人設計模式,然后添加各個菜單,所有的菜單動作如圖1-2所示,最終的菜單欄和工具欄如圖1-3所示。設計菜單時,如果將來觸發這個菜單會彈出一個對話框進行詳細設置,那么就在這個菜單文本后面添加"..."號,例如這里的“打開文件”菜單和“另存為”菜單。這里還要注意,添加動作時,一定要使動作名稱和這里的Action編輯器中所使用的名稱保持一致,因為在后面的程序中還要用到它們。添加工具欄的工具是用鼠標把Action編輯器的Action拖動到工具欄做到的,圖片資源文件來自工程目錄下的image文件夾。

![](https://img2018.cnblogs.com/blog/1075214/201812/1075214-20181216220059022-463716138.png) 圖1-2 Action編輯器
![](https://img2018.cnblogs.com/blog/1075214/201812/1075214-20181216215920525-1231530281.png) 圖1-3 菜單欄與工具欄

  設計完菜單欄與工具欄后,向主窗口中心區域拖入一個MdiArea部件,並單擊主窗口界面,按下Ctrl + G快捷鍵,使其處於柵格布局之中。可以看一下對象列表窗口,確保MdiArea部件的objectName是mdiArea,而文件菜單、編輯菜單、窗口菜單和幫助菜單的objectName分別是menuF、menuE、menuW和menuH;如果不是,需 要在屬性欄中更改,因為后面的程序中要用到。

1.2 創建子窗口類

 

1.2.1 mdichild. h具體代碼及說明

  為了實現多文檔操作,需要向QMdiArea中添加子窗口,而為了可以更好地操作子窗口,必須子類化子窗口的中心部件。因為這里子窗口的中心部件使用了QTextEdit類,所以要實現自己的類,它必須繼承自QTextEdit,然后在其中添加我們的功能函數。
  (項目源碼路徑:src\1\1 -2\myMdi)往項目中添加新文件,模板選擇“C+ + 類”,類名為MdiChild,基類為QTextEdit,類型信息選擇“繼承自QWidget”。完成后在mdichild. h文件中添加代碼:

#include <QWidget>
#include <QTextEdit>

class MdiChild:public QTextEdit
{
Q_OBJECT

public:
    explicit MdiChild(QWidget *parent = nullptr);

    void newFile(); // 新建文件
    bool loadFile(const QString &filePath); // 加載文件
    bool save(); // 保存操作
    bool saveAs(); // 另存為操作
    bool saveFile(const QString &filePath); // 保存文件
    QString getFileNameFromPath(); // 從文件路徑中提取出文件名
    QString getCurFileName() { return curFile; } // 獲得返回的當前文件名稱(路徑)

protected:
    void closeEvent(QCloseEvent *event);    // 關閉事件

private slots:
    void documentWasModified(); //文檔被更改時,窗口顯示更改狀態標志

private:
    bool maybeSave();                       // 判斷是否需要保存
    void setCurrentFile(const QString &filePath);  // 設置當前文件

    QString curFile; // 保存新建文件時自動產生的當前文件路徑(名稱)
    bool isUnsaved_flag; //該標志位判斷文件是否為“未保存狀態”,若是,則打開文件對話框執行“另存為”操作,否則直接保存
};

  這里在頭文件中聲明了11個函數,定義了兩個變量。其中,currentFile()函數 返回當前的文件路徑,只有一行代碼,就直接在這里定義了。所以真正需要設計的只有10個函數,還有curFile與isUnsaved_flag兩個變量,分別用於保存當前文件的路徑和作為文件是否被保存過的標志。因為對於所有的應用程序,只有涉及新建、保存和關閉等操作時,都是使用的這些函數進行設置的,它們是一個整體,所以這里要將它們同時羅列出來。這些函數主要完成了下面幾個操作:

  1. 新建文件操作newFile()
    • 設置窗口編號;
    • 設置文件未被保存過“isUnsaved_flag = true;”;
    • 保存文件路徑,給curFile賦初值;
    • 設置子窗口標題;
    • 關聯文檔內容改變信號到顯示文檔更改狀態標志槽documentWasModified()。
  2. 加載文件操作loadFile()
    • 打開指定的文件,並讀取文件內容到編輯器;
    • 設置當前文件setCurrentFile(),該函數可以獲取文件路徑,完成文件和窗口狀態的設置;
    • 關聯文檔內容改變信號到顯示文檔更改狀態標志槽documentWasModified()。
  3. 保存操作save()
    • 如果文件沒有被保存過(用isUnsaved_flag判斷),執行另存為操作saveAs() ;
    • 否則直接保存文件saveFile(),該函數先打開指定文件,然后將編輯器的內容寫入該文件,最后設置當前文件setCurrentFile()。
  4. 另存為操作saveAs()
    • 從文件對話框獲取文件路徑;
    • 如果路徑不為空,則保存文件saveFile()。
  5. 關閉操作 closeEvent()
    • 如果maybeSave()函數返回為真,則關閉窗口。maybeSave()函數判斷文檔是否被更改過,如果被更改過,則彈出對話框,讓用戶選擇是否保存更改,或者取消關閉操作。如果用戶選擇保存更改,則返回保存操作save()的結果,如 果選擇取消,則返回false。否則,直接返回true。
    • 如果maybeSave()函數返回為假,則忽略該事件。
       

1.2.2 mdichild. cpp具體代碼及說明

  下面一次性貼出了mdichild.cpp中的所有代碼,沒有像書中一樣分步驟貼出,並進行說明:

#include "mdichild.h"
#include <QFile>
#include <QMessageBox>
#include <QTextStream>
#include <QApplication>
#include <QFileInfo>
#include <QFileDialog>
#include <QCloseEvent>
#include <QPushButton>

MdiChild::MdiChild(QWidget *parent) :
    QTextEdit(parent)
{
    // 這樣可以在子窗口關閉時銷毀這個類的對象
    setAttribute(Qt::WA_DeleteOnClose);

    // 初始isUntitled為true
    isUnsaved_flag = true;
}

// 新建文件
void MdiChild::newFile()
{
    // 設置窗口編號,因為窗口一直被保存,所以需要使用靜態變量
    static int windowNumber = 1; //窗口編號從1開始

    // 新建的文檔沒有被保存過
    isUnsaved_flag = true;

    // 將當前文件命名為:未命名文檔加窗口編號,窗口編號先使用再加1
    curFile = tr("未命名文檔%1.txt").arg(windowNumber++);

    // 設置窗口標題,使用[*]可以在文檔被更改后在文件名稱后才顯示”*“號
    setWindowTitle(curFile + "[*]" + tr(" - 多文檔編輯器"));

    // 當文檔內容被更改時發射contentsChanged()信號,執行documentWasModified()槽函數,在標題欄上顯示'*'
    connect(document(), SIGNAL(contentsChanged()),
            this, SLOT(documentWasModified()));
}

// 文檔被更改時,窗口顯示更改狀態標志
void MdiChild::documentWasModified()
{
    // 根據文檔的isModified()函數的返回值,判斷我們編輯器內容是否被更改了
    // 如果被更改了,參數為true,則setWindowModified()就會在設置了[*]號的地方顯示“*”號
    setWindowModified(document()->isModified()); //setWindowModified為庫函數
}

// 加載文件
bool MdiChild::loadFile(const QString &filePath)
{
    // 新建QFile對象
    QFile file(filePath);

    // 只讀方式打開文件,出錯則打開消息提示對話框,並返回false
    if (!file.open(QFile::ReadOnly | QFile::Text))
    {
        // %1和%2分別可以被后面的arg()中的fileName和file.errorString()代替
        QMessageBox::warning(this, tr("多文檔編輯器"),
                             tr("無法讀取文件 %1:\n%2.")
                             .arg(filePath).arg(file.errorString()));
        return false;
    }

    // 新建文本流對象
    QTextStream in(&file);

    // 設置鼠標狀態為等待狀態
    QApplication::setOverrideCursor(Qt::WaitCursor);

    // 讀取文件的全部文本內容,並添加到編輯器中
    setPlainText(in.readAll());

    // 恢復鼠標狀態
    QApplication::restoreOverrideCursor();

    // 設置當前文件
    setCurrentFile(filePath);

    // 文檔的“內容改變”信號,連接到“文檔改變槽”,即當文檔內容改變,則標題欄出現'*'
    connect(document(), SIGNAL(contentsChanged()),
            this, SLOT(documentWasModified()));

    return true;
}

// 設置當前文件,將加載文件的路徑保存到filePath中
void MdiChild::setCurrentFile(const QString &filePath)
{
    // canonicalFilePath()可以除去路徑中的符號鏈接,“.”和“..”等符號
    curFile = QFileInfo(filePath).canonicalFilePath();

    // 文件已經被保存過了
    isUnsaved_flag = false;

    // 文檔沒有被更改過
    document()->setModified(false);

    // 窗口不顯示被更改標志-'*'
    setWindowModified(false);

    // 設置窗口標題,userFriendlyCurrentFile()返回文件名
    setWindowTitle(getFileNameFromPath() + "[*]");
}

// 從文件路徑中提取出文件名
QString MdiChild::getFileNameFromPath()
{
    return QFileInfo(curFile).fileName(); // 從文件路徑中提取文件名
}

// 保存操作
bool MdiChild::save()
{
    if (isUnsaved_flag)
    { // 如果文件未被保存過,則執行另存為操作
        return saveAs();
    }
    else
    {
        return saveFile(curFile); //否則直接保存文件
    }
}

// 另存為操作
bool MdiChild::saveAs()
{
    // 獲取文件路徑,如果為空,則返回false
    QString filePath = QFileDialog::getSaveFileName(this, tr("另存為"),curFile);
    if (filePath.isEmpty())
        return false;

    return saveFile(filePath); // 否則保存文件
}

// 保存文件:本質是根據參數-硬盤文件路徑,打開硬盤文件然后寫入軟件上文檔數據
bool MdiChild::saveFile(const QString &filePath)
{
    QFile file(filePath);
    if (!file.open(QFile::WriteOnly | QFile::Text)) {
        QMessageBox::warning(this, tr("多文檔編輯器"),
                             tr("無法寫入文件 %1:\n%2.")
                             .arg(filePath).arg(file.errorString()));
        return false;
    }

    QTextStream out(&file);
    QApplication::setOverrideCursor(Qt::WaitCursor);
    out << toPlainText(); // 以純文本文件寫入
    QApplication::restoreOverrideCursor();

    setCurrentFile(filePath);
    return true;
}

//關閉事件
void MdiChild::closeEvent(QCloseEvent *event)
{
    if (maybeSave()) { // 如果maybeSave()函數返回true,則關閉窗口
        event->accept();
    } else {   // 用戶選擇不保存修改,maybeSave返回false,則這個事件會被忽略掉,什么都不做
        event->ignore();
    }
}

// 判斷是否需要保存: 在窗口關閉時判斷文件是否需要保存,並讓用戶選擇
bool MdiChild::maybeSave()
{
    // 如果文檔被更改過,彈出警告框,讓用戶做出選擇
    if (document()->isModified())
    {
        QMessageBox box;
        box.setWindowTitle(tr("多文檔編輯器"));
        box.setText(tr("是否保存對“%1”的更改?")
                    .arg(getFileNameFromPath()));
        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
}

  下面是對上面代碼的補充說明:

  • 新建文件函數( newFile() ):在設置窗口標題時添加了“[ * ]”字符,它可以保證編輯器內容被更改后,在相應位置顯示“* ”號。而判斷編輯器內容是否被更改,可以使用 QTextDocument 類對象的 isModified() 函數獲知,這里使用了 QTextEdit 類的 documen() 函數來獲取它的 QTextDocument 類對象。然后使用 setWindowModified() 函數設置窗口的更改狀態標志。如果參數為true,那么就會在標題中的設置了“[ * ]”號的地方顯示“* ”號,表示該文件已經被修改。
  • 加載文件函數( loadFile ):建立 QMessageBox 時使用了tr()函 數,其中的“1%”和“2%”分別可以被后面的 arg() 中的 fileName 和 file.errorString() 代替,這樣就可以在字符串中使用變量了。
  • 提取文件名函數( getFileNameFromPath() ):該函數是從文件路徑中提取出文件名,這樣使標題顯得更加清晰和友好,這就是這個函數名的含義。
  • 保存操作函數( save() ): 先使用 isUnsaved_flag 判斷文件是否被保存過,如果沒有,則要先進行另存為操作,如果已經保存過了,那么直接寫入文件就可以了。
  • 另存為操作函數( saveAs() ):先打開文件對話框,若不在"文件名"一欄修改,則會將從標題欄獲得的文件名稱作為默認保存名稱返回,賦給 fileName,如果文件名稱(路徑)不為空,則會再調用saveFile()。
  • 保存函數( saveFile() ): 這個函數進行文件的寫人操作,可以看到,它與 loadFile() 函數是相對應的兩個操作。
  • 關閉事件函數( closeEvent() ):這里的關閉事件函數會在窗口被關閉或者調用 close() 函數時執行。若maybeSave()返回true,則會執行 accept() 函數,那么窗口將會關閉, 這里需要說明,其實窗口關閉,默認只是將窗口隱藏起來了,並沒有將它銷毀掉。但是因為在前面的構造函數中使用了“setAttribute(Qt::WA_DeleteOnClose);”這行代碼,所以當關閉窗口時,它會被銷毀掉。而如果用戶選擇不保存修改,maybeSave()返回false,則會執行 ignore() 函數,那么這個事件會被忽略掉,什么都不做。
  • maybeSave()函數:它在窗口關閉時判斷文件是否需要保存,並讓用戶進行選擇。另外為了使警告框中的按鈕可以顯示中文,所以自定義了按鈕。
     

  這就是整個操作的過程,讀者可以好好分析一下各個函數及其聯系,因為以后寫相似的應用程序時,這幾個操作都是必須的,這里搞明白了,以后直接使用即可。下面對這個類進行簡單的測試。

  進入設計模式,在Action編輯器中“新建文件”動作上右擊,轉到它的觸發信號 triggered() 的槽,並更改如下:

// 新建文件菜單
void MainWindow::on_actionNew_triggered()
{
    // 創建MdiChild
    MdiChild *child = new MdiChild;
    // 多文檔區域添加子窗口
    ui->mdiArea->addSubWindow(child);
    // 新建文件
    child->newFile();
    // 顯示子窗口
    child->show();
}

  在這里新建了子窗口,並且以MdiChild類對象為中心部件,然后新建文件並且顯示出來。這里大家要在mainwindow. cpp文件中添加# include "mdichild. h”頭文 件。最后運行程序,然后按下Ctrl + N新建文件,並更改內容,然后進行關閉,看一下程序的運行效果。

  自己新加了一個思維導圖,如圖1-4所示

![](https://img2018.cnblogs.com/blog/1075214/201812/1075214-20181217113336735-1319133331.png) 圖1-4 MdiChild類-思維導圖

1.3 實現菜單的功能

   上一節建立了自己子窗口的中心部件MdiChild類,它繼承自QTextEdit類。 下面便可以使用這個類,來完成主窗口上的各個菜單的功能。
 

1.3.1 更新菜單狀態與新建文件操作

  (項目源碼路徑:srC\1\1-3\myMdi)首先更新菜單狀態,使一些菜單在開始時處於不可用狀態。然后再更改新建文件的操作。
 

1. 更新菜單狀態
  在 mainwindow. h 文件中添加如下代碼:

#include <QMainWindow>

//增加類MdiChild的前置聲明
class MdiChild;

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_actionNew_triggered();
    void updateMenus();           //更新菜單

private:
    Ui::MainWindow *ui;
    QAction *actionSeparator;    //分隔符
    MdiChild *activeMdiChild(); //活動窗口
};

  上面的 actionSeparator 動作用於創建一個間隔器,將來在“窗口”菜單中顯示子窗口列表時,可以用它與前面的菜單動作分隔開。下面在mainwindow. cpp文件中添加代碼:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mdichild.h"
#include <QMdiSubWindow>

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 = new MdiChild;
    // 多文檔區域添加子窗口
    ui->mdiArea->addSubWindow(child);
    // 新建文件
    child->newFile();
    // 顯示子窗口
    child->show();
}

void MainWindow::updateMenus() //更新菜單
{
    bool hasMdiChild = (activeMdiChild() != nullptr); //是否有活動窗口
    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::activeMdiChild() //活動窗口
{
    // 如果有活動窗口,則將其內的中心部件轉換為MdiChild類型
    if (QMdiSubWindow *activeSubWindow = ui->mdiArea->activeSubWindow())
        return qobject_cast<MdiChild *>(activeSubWindow->widget());
    return nullptr; // 沒有活動窗口,直接返回0
}
  • 構造函數: 初始化了 actionSeparator 動作,然后執行更新菜單函數,並關聯多文檔區域的活動子窗口信號到更新菜單槽上,每當更換活動子窗口時,都會更新菜單狀態。
  • updateMenus(): 在更新菜單函數中根據是否有活動子窗口,設置了各個菜單動作是否可用。這里剪切復制操作和撤銷恢復操作的設置還要進行特殊情況的判斷。
  • activeMdiChild(): 這個函數中使用了 QMdiArea 類的 activeSubWindow() 函數來獲得多文檔區域的活動子窗口,然后使用了 T qobjeCt_Cast ( QObject * object ) 函數來進行類型轉 換。這個函數是 QObject 類中的函數,它將 object 對象指針轉換為T類型的對象指 針,這里將活動窗口的中心部件 QWidget 類型指針轉換為 MdiChild 類型指針。這里的T類型必須是直接或者間接繼承自QObject類,而且在其定義中要有Q_OB- JECT宏變量。

  現在運行程序,效果如圖1-5所示。

![](https://img2018.cnblogs.com/blog/1075214/201812/1075214-20181217135458902-2120452163.png) 圖1-5 初始化菜單界面
 

2. 實現新建文件操作
  首先在 mainwindow. h 文件中添加 private slots:

    MdiChild *createMdiChild();	//創建子窗口

  然后在 mainwindow. cpp 文件中添加該槽的定義:

MdiChild * MainWindow::createMdiChild() //創建子窗口部件
{
    MdiChild *child = new MdiChild; //創建MdiChild部件
    ui->mdiArea->addSubWindow(child); //向多文檔區域添加子窗口,child為中心部件
    connect(child,SIGNAL(copyAvailable(bool)),ui->actionCut,
            SLOT(setEnabled(bool)));

    // 根據QTextEdit類的是否可以復制信號設置剪切復制動作是否可用
    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;
}

  在這個函數中創建了 MdiChild 部件,並將它作為子窗口的中心部件,然后添加到多文檔區域。下面關聯了編輯器的信號和我們的菜單動作,讓它們可以隨着文檔 的改變而改變狀態。最后返回了 MdiChild 對象指針。這里之所以要添加這樣一個 函數,是因為在下面的打開操作中還要使用到這個函數中的功能,所以將它們從新建 文件菜單的觸發信號槽中提取出來,另寫了這樣一個函數。下面更改在上一節中添加的新建文件菜單的觸發信號槽:

// 新建文件菜單
void MainWindow::on_actionNew_triggered()
{
    MdiChild *child = createMdiChild();    //創建MdiChild
    child->newFile();                      //新建文件
    child->show();                         //顯示子窗口
}

  因為添加子窗口的操作放到了 createMdiChild() 函數中進行,這里只需要調用這個函數就可以了。現在運行程序添加新文件,然后編輯,選中一些字符,可以看到工具欄中"剪切"、"復制"不再是灰色的,如圖1-6所示。但是,因為現在還沒有實現這些動作的功能,所以它們並不可用。

![](https://img2018.cnblogs.com/blog/1075214/201812/1075214-20181217140628315-442430069.png) 圖1-6 新建文件后菜單狀態

1.3.2 實現文件打開操作

  現在來實現打開文件菜單的功能。當要打開一個文件時,要先判斷這個文件是否已經被打開了,這樣就需要遍歷多文檔區域子窗口中的文件,如果發現該文件已經打開,則直接設置該子窗口為活動窗口;否則直接加載要打開的文件,並添加新的子窗口。
  (項目源碼路徑:src\1\1 - 5\myMdi)首先在 mainwindow. h 文件中先添加類的前置聲明 class QMdiSubWindow;然后添加 private 函數聲明:

    QMdiSubWindow *findMdiChild(const QString &filePath); // 查找子窗口

  再添加私有槽聲明private slots:

    void setActiveSubWindow(QWidget *window); // 設置活動子窗口

  現在從設計模式進入“打開文件”動作的觸發信號 triggered() 的槽,更改如下:

void MainWindow::on_actionOpen_triggered() // 打開文件菜單
{
    QString filePath = QFileDialog::getOpenFileName(this); // 獲取文件路徑
    if (!filePath.isEmpty()) 
    { 
        // 如果路徑不為空,則查看該文件是否已經打開
        QMdiSubWindow *existing = findMdiChild(filePath);
        if (existing) 
        { 
            // 如果已經存在,則將對應的子窗口設置為活動窗口
            ui->mdiArea->setActiveSubWindow(existing);
            return;
        }

        MdiChild *child = createMdiChild(); // 如果沒有打開,則新建子窗口
        if (child->loadFile(filePath)) 
        {
            ui->statusBar->showMessage(tr("打開文件成功"), 2000);
            child->show();
        }
        else 
        {
            child->close();
        }
    }
}

  下面是査找子窗口函數的實現:

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 nullptr;
}

  這個函數中使用了 foreach 語句來遍歷整個多文檔區域的所有子窗口,這個函數在《Qt Creator快速入門》的第7章容器類部分講到。下面是設置活動窗口的實現:

void MainWindow::setActiveSubWindow(QWidget *window) // 設置活動子窗口
{
    if (!window) // 如果傳遞了窗口部件,則將其設置為活動窗口
        return;
    ui->mdiArea->setActiveSubWindow(qobject_cast<QMdiSubWindow *>(window));
}

  這個函數的作用就是將傳遞過來的窗口部件設置為活動窗口。

1.3.3 添加子窗口列表

  現在為窗口菜單添加顯示子窗口列表的功能。我們想每添加一個子窗口就可以在窗口菜單中羅列出它的文件名,而且可以在這個列表中選擇一個子窗口,將它設置為活動窗口。這個看似很好實現,只要為窗口菜單添加菜單動作,然后關聯這個動作的觸發信號到設置活動窗口槽上就可以了。但是,如果有很多個子窗口怎么辦,難道要一個一個進行關聯嗎,那怎么獲知是哪個動作?其實,Qt中提供了一個信號映射器QSignalMapper類,它可以實現對多個相同部件的相同信號進行映射,為其添加字符串或者數值參數,然后再發射出去。
  (項目源碼路徑:src\1\1 - 5\myMdi)首先在 mainwindow. h 文件中添加類的前置聲明 class QSignalMapper;然后添加私有對象指針 private:

QSignalMapper *windowMapper;	// 信號映射器

  再添加私有槽聲明 private slots:

void updateWindowMenu();	// 更新窗口菜單

  下面到 mainwindow. cpp 文件中添加代碼。首先添加 #include 頭文件,然后在 MainWindow 的構造函數中添加如下代碼 ( 注意在最新的Qt中,QSignalMapper 這個類已經被棄用了)

    //注意在最新的Qt中,QSignalMapper 這個類已經被棄用了,沒有刪除是為了維護老代碼
    windowMapper = new QSignalMapper(this); // 創建信號映射器
    connect(windowMapper, SIGNAL(mapped(QWidget*)), // 映射器重新發射信號
            this, SLOT(setActiveSubWindow(QWidget*))); // 設置活動窗口
    updateWindowMenu();
    // 更新窗口菜單,並且設置當窗口菜單將要顯示的時候更新窗口菜單
    connect(ui->menuW,SIGNAL(aboutToShow()),this,SLOT(updateWindowMenu()));

  上面創建了信號映射器,並且將它的mappedO信號關聯到設置活動窗口槽上。然后更新窗口菜單,並且將窗口菜單的將要顯示信號關聯到我們的更新菜單梢上,這樣每當窗口菜單要顯示時都會更新窗口菜單,更新窗口菜單的代碼如下:

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;
        if (i < 9) 
        { 
            // 如果窗口數小於9,則設置編號為快捷鍵
            text = tr("&%1 %2").arg(i + 1)
                               .arg(child->getFileNameFromPath());
        } 
        else 
        {
            text = tr("%1 %2").arg(i + 1)
                              .arg(child->getFileNameFromPath());
        }
        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));

    }
}

  更新窗口菜單函數中,先清空了窗口菜單動作,然后再動態添加。這里遍歷了多文檔區域的各個子窗口,然后以它們中的文件名為文本創建了動作,並將這些動作添加到窗口菜單中。我們將動作的觸發信號關聯到信號映射器的 map() 槽上,然后設置了動作與其對應的子窗口之間的映射,這樣觸發菜單時就會執行 map() 函數,而它又會發射 mapped() 信號,這個 mapped() 函數會以子窗口部件為參數,因為在構造函數中設置了這個信號與 setActiveSubWindow() 函數的關聯,所以最終會執行設置活動子窗口函數,並且設置選擇的動作指定的子窗口為活動窗口。這時運行程序,效果如圖1 - 7所示。

![](https://img2018.cnblogs.com/blog/1075214/201812/1075214-20181217174241963-1064629944.png) 圖1-7 子窗口列表

1.3.4 實現其它功能

  下面來實現其他一些菜單的功能。因為在前面已經把核心的功能都實現了,而且像剪切、復制等常用功能,QTextEdit類已經提供了,所以這里只需要調用相應的函數即可。
  (項目源碼路徑:src\1\1-6\myMdi)只需要在設計模式,進入相應動作的觸發信號 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_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("歡迎訪問我們的網站:www.yafeilinux.com"));
}

void MainWindow::on_actionAboutQt_triggered() // 關於Qt菜單
{
    qApp->aboutQt(); // 這里的qApp是QApplication對象的全局指針,
                     // 這行代碼相當於QApplication::aboutQt();
}

1.4 完善程序功能

  應用程序在實現了主要的功能后,還要進行一些必要的設置,使它成為一個完善的應用程序。下面進行幾個方面的優化。
 

1.4.1 保存窗口設置

  我們都希望自己的應用程序很友好,那么能保存用戶對窗口的一些設置(比如大小、位置)等就顯得很必要了。Qt中的 QSettings 類提供了平台無關的永久保存應用程序設置的方法。
  (項目源碼路徑:src\1\1 - 7\myMdi)首先在 mainwindow. h 文件中添加 private 函數聲明:

    void readSetting();    //讀取窗口設置
    void writeSetting();    //寫入窗口設置

  然后再添加 protected 函數聲明:

    void closeEvent(QCloseEvent *event);  // 關閉事件

  然后到 mainwindow. cpp 添加代碼。先添加頭文件:

#include <QSettings>
#include <QCloseEvent>

  然后在 MainWindow 類的構造函數中添加代碼:

    readSettings(); // 初始窗口時讀取窗口設置信息

  下面是關閉事件處理函數的定義:

void MainWindow::closeEvent(QCloseEvent *event) // 關閉事件
{
    ui->mdiArea->closeAllSubWindows(); // 先執行多文檔區域的關閉操作
    if (ui->mdiArea->currentSubWindow()) {
        event->ignore(); // 如果還有窗口沒有關閉,則忽略該事件
    } else {
        writeSettings(); // 在關閉前寫入窗口設置
        event->accept();
    }
}

  可以看到我們是在構造窗口時進行窗口設置的讀取,在窗口的關閉事件中進行 窗口設置的寫入的。下面是窗口設置的寫人和讀取函數:

void MainWindow::writeSettings() // 寫入窗口設置
{
    QSettings settings("yafeilinux", "myMdi");
    settings.setValue("pos", pos());   // 寫入位置信息
    settings.setValue("size", size()); // 寫入大小信息
}

void MainWindow::readSettings() // 讀取窗口設置
{
    QSettings settings("yafeilinux", "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::on_actionExit_triggered() // 退出菜單
{
    qApp->closeAllWindows(); // 等價於QApplication::closeAllWindows();
}

  這里使用了 qApp 指針,它是 QApplication 對象的全局指針,因為在一個應用程序中只能定義一個 QApplication 對象。現在運行程序,改變窗口位置和大小,然后關閉程序,重新運行,測試一下運行效果。

1.4.3 其他功能

  最后還想要在狀態欄中可以顯示編輯器中光標所在的行號和列號,然后設置窗口的標題和狀態欄的一些顯示。首先在 mainwindow. h 文件中添加私有槽的聲明 private slots:

void showTextRowAndCol() j	//顯示文本的行號和列號

  然后添加一個私有函數的聲明private:

    void initWindow();	// 初始化窗口

  先來看顯示光標位置函數的定義:

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);
    }
}

  在這個函數中獲取了活動窗口中光標的位置,並在狀態欄中顯示,為了每次編輯 器中的光標位置變化時都可以調用這個函數,需要在 createMdiChild() 函數中的“return child;”一行代碼前添加一行代碼:

    // 每當編輯器中的光標位置改變,就重新顯示行號和列號
    connect(child,SIGNAL(cursorPositionChanged()),this,SLOT(showTextRowAndCol()));

  下面來看初始化窗口函數。首先添加頭文件 #include ,然后在 MainWindow 類的構造函數中調用這個函數,即在最后添加代碼:

    initWindow(); // 初始化窗口

  然后進行該函數的定義:

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=\"http://www.yafeilinux.com\">yafeilinux.com</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的介紹"));
}

  這里設置了窗口的標題和工具欄的標題,然后為多文檔區域設置了滾動條,添加了網站的鏈接。最后設置了各個動作的狀態提示信息,將鼠標移動到這些動作上時, 在狀態欄會顯示這些提示信息。最終運行效果如圖1 -9 所示。

![](https://img2018.cnblogs.com/blog/1075214/201812/1075214-20181217203153112-846358107.png) 圖1-9 狀態欄提示信息

  到這里,整個程序就設計完成了。可以按照《Qt Creator快速人門》的第2章的內容給這個程序添加程序圖標,然后以 Release 的方式編譯程序,最后打包發布。

1.5 小結

  這個多文檔編輯器程序只是實現了編輯器的一些最基本功能,還有很多功能沒能實現,比如富文本的處理、打印和査找等。不過作為初學者的第一個綜合實例,它 已經包含了太多的知識點,如果要做一個功能強大的編輯器,那么這一章也許要寫幾百頁。如果還有興趣擴展這個程序,可以看一下Qt中提供的 Text Edit 示例,或者去qter網站上査看多文檔編輯器開源軟件,那個程序更加綜合。

  讀者應該認真學習這一章,不僅是學習其中的知識點,更多的是學習一種方法, 編寫綜合程序的方法。可以看到,我們程序的功能是一點一點加上去的,再龐大的程序也是將功能模塊一個個加上去的,不要設想一下子就寫出一個功能強大的應用程序。而且在程序編寫過程中,一定會出現各種問題,不要氣餒,不要煩躁,因為這是正常現象,學會多使用qDebug()函數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM