1. 程式人生 > >Qt學習之路_11(簡易多文件編輯器)

Qt學習之路_11(簡易多文件編輯器)

前言:

  本文將介紹怎樣用Qt做一個簡單的多文件編輯器,該實驗的過程中主要涉及到Qt視窗的設計,選單欄(包括右擊選單),工具欄,狀態列,常見的文字檔案等操作。參考資料為網址上的一個例子:http://www.yafeilinux.com/

  本來是在ubuntu下做這個實驗的,可是一開始建立選單欄等時,裡面用的是中文,執行後中文就是不顯示.在網上找了2天的辦法,各種手段都試過了,什麼編碼方式啊,什麼實用QTextCodec這個類啊都試過了,沒成功。很不爽,暫時還是轉到windows下吧。在ubuntu下只是簡單的設計了該程式的介面,然後把那些程式碼弄到windows下後,開啟main.pp檔案後出現直接在main.cpp中出現 了 could not decode "main.cpp" with "System"-encoding

這種錯誤提示,發現在這個原始檔中不能輸入中文(比如說註釋的時候想用中文註釋).但是在同一個工程的其它cpp檔案中就可以輸入中文.看了下其它的cpp檔案的encoding也是System的.誤解,又是編碼問題!!

  實驗過程:

  下面講的主要是一個個簡單功能的逐步實現過程的某些細節。

  介面的設計

  在action編輯器中,Text欄中輸入文字內容,如果有快捷方式,也最好用括號將其註釋起來 ;Object Name中輸入目標名字,最好採用預設的action開頭命名;Tooltip是當滑鼠靠近該選單一會兒的時候會提示的文字,這裡一般與Text欄中除括號註釋外的相同;ShotCut一欄中直接用鍵盤按下快捷鍵一遍即可,注意按下Ctrl+Tab時顯示的為Ctrl+Tab,當按下Ctrl+Shift+Tab時顯示的是Ctrl+Shift+Backtab.;

  選單涉及完成後 如下圖所示:

   

    上面的used一欄不可用,當按住action編輯器中的每一欄,拖動到對應菜欄下的typehere了就變成打勾可用了。

  MyMdi文件類的建立:

  新建一個類,名字取為MyMdi,基類名為QTextEdit(注意,因為下拉列框中可選的基類有限,所以這裡可以自己輸入),型別資訊選擇繼承來自QWidget。

因為我們在建立工程的時候,其主介面就是用的MainWindow這個類,這個類主要負責主介面的一些介面的佈局(比如選單欄,工具欄,狀態列等),顯示,退出和一些人機互動等。那麼我們新建的MyMdi這個類就不需要負責它在它的父視窗中的顯示退出等,只需負責自己視窗的佈局和介面顯示等。這種思想是說每個介面都單獨分離開來,只負責自己介面的實現和與它的子介面互動。

  好像window和widget不同,window為視窗,包括選單欄,工具欄,狀態列等,而widget一般不包括這些,只包括其文字欄。

  開啟檔案功能的實現:

  1. 單擊工具欄上的開啟檔案,則會彈出相應的對話方塊,選中所需要開啟的文字檔案。

  2. 如果該檔案已經被開啟過,則設定顯示該檔案對應的視窗為活動視窗。

  3. 如果該檔案沒有被開啟過,則新建一個視窗,該視窗貼在其父視窗中。且此時把檔案開啟,開啟成功則狀態列對應顯示成功資訊,否則輸出錯誤資訊。

  4. 過程3中開啟檔案是以只讀和文字方式開啟檔案的,開啟完後將文字內容顯示到視窗,並設定好檔案視窗的標題資訊等。

  5. 如果文字內容變化後但沒有儲存,則視窗的標題有*號在後面。

  新建檔案功能的實現:

  1. 單擊工具欄上的新建檔案,則新建立一個視窗物件,其類為MyMdi,本身具備輸入文字的功能,因為是繼承的QTextEdit。

  2. 設定好標題欄等資訊,且當有文字內容改變又沒有儲存的情況下則後面也一樣顯示*號。

  儲存檔案功能的實現:

  1. 如果是新建的檔案,單擊儲存時會自動跳到另存為那邊,即彈出一個另存為對話方塊,重新選擇儲存檔案的目錄和檔名。

  2. 如果是已經儲存過的檔案,比如說開啟的檔案,單擊選單欄下的儲存時,其內部執行的是用檔案流將開啟的檔案寫入到指定的檔名中。

  關閉視窗功能的實現:

  當單擊視窗右上角的關閉按鈕時,程式會自動執行該視窗的closeEvent()函式,所以如果我們在關閉視窗時需要某些功能,可以重寫這個函式。

  複製貼上剪下撤銷等功能實現:

  因為MyMdi這個類是繼承QTextEdit類的,所以這些方法都可以直接呼叫QTextEdit類裡面對應的方法就可以了。

  更新選單欄和工具欄功能的實現:

  選單欄中並不是所有的操作都是可用的,比如說複製,如果沒有活動視窗,或者即使有活動視窗但是沒有選中文字,則該操作不可以,同理,剪下也是一樣。

  另外,撤銷和恢復都是要經過系統判斷,當前是否可用執行這些操作,如果可以則這些操作對應的圖示為亮色,可用,否則為灰色不可用。

  狀態列的操作也是一樣,當有游標移動時,狀態列顯示的行列號值才會跟著變化。

  更新視窗子選單欄功能實現:

  當開啟多個文件時,視窗子選單下面會自動列出這些文件的名字,且作為一個組單獨用分隔符與上面的子選單隔開。我們可以在該選單欄下選擇一個文件,選完後該文件會被自動當做活動文件,且處於選中狀態。前9個文件可以用1~9這些數字做為快捷鍵。

  儲存視窗設定功能實現:

  如果軟體需要實現這一功能:當下次開啟時和上次該軟體關閉時的視窗大小,位置一樣。那麼我們就必須在每次關閉軟體時,保留好視窗大小,尺寸等資訊,當下次開啟該軟體時,重新讀取這些資訊並對視窗進行相應的設定。這裡需要用到QSettings這個類,該類是永久保存於平臺無關的應用程式的一些設定的類。在本程式中,關閉軟體時寫入視窗資訊,開啟軟體在建構函式中讀取該資訊並設定相應的視窗。

  自定義右鍵選單欄功能實現:

  預設的右鍵選單欄為英文的,我們這裡需要把它弄成中文的,只需在MyMdi這個類中重寫函式contextMenuEvent(QContextMenuEvent *event)即可。在該函式中,只需新建一個選單,然後動態為這個選單加入action,並且為每個action設定快捷鍵,同時也需要根據情況實現對應action是否可用。

  初始化視窗的實現:

  這一部分包括設定視窗標題,設定工具欄標題,設定水平垂直滾動條,在狀態列上加一個label,狀態列上顯示選單欄上各種action的提示資訊,雖然這個初始化視窗是在建構函式中呼叫的,但是這些設定在整個應用程式中都有效。

  實驗結果:

  本實驗的功能在上面幾個過程中已有實現,類似於windows下的記事本一樣。下面是其效果一張簡單的截圖:

  

  實驗主要部分程式碼即註釋(附錄有工程code下載連結):

mymdi.h:

複製程式碼
#ifndef MYMDI_H
#define MYMDI_H

#include <QTextEdit>

class MyMdi : public QTextEdit
{
    Q_OBJECT
public:
    explicit MyMdi(QWidget *parent = 0);
    void NewFile();
    bool LoadFile(const QString &file_name);
    QString CurrentFilePath();
    QString get_current_file_name();
    void SetCurrentFile(const QString &file_name);
    bool Save();
    bool SaveAs();
    bool SaveFile(const QString &file_name);//因為Save()和SaveAs()有很多共同的程式碼,所以最好單獨寫個函式供其呼叫。
    
signals:
    
public slots:

private:
    QString current_file_path_;//當前檔案的檔名
    bool is_saved_;  //檔案是否儲存標誌
    bool has_saved();
    void contextMenuEvent(QContextMenuEvent *event);

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

private slots:
    void DocumentWasModified();//當文件內容被改後所需執行的操作
    
};

#endif // MYMDI_H
複製程式碼

mymdi.cpp:

複製程式碼
#include "mymdi.h"
#include <QFile>
#include <QMessageBox>
#include <QTextStream>
#include <QApplication>
#include <QFileInfo>
#include <QFileDialog>
#include <QPushButton>
#include <QCloseEvent>
#include <QMenu>

MyMdi::MyMdi(QWidget *parent) :
    QTextEdit(parent)//因為MyMdi是繼承QTextEdit類的,所以它本身就是一個文字編輯類,可以編輯文字
{
    setAttribute(Qt::WA_DeleteOnClose);//加入了這句程式碼後,則該視窗呼叫close()函式不僅僅是隱藏視窗而已,同時也被銷燬
    is_saved_ = false;
}

void MyMdi::NewFile()
{
    static int sequence_number = 1;
    is_saved_ = false;
    current_file_path_ = tr("未命名文件%1.txt").arg(sequence_number++);
    setWindowTitle(current_file_path_ + "[*]");//設定文件預設標題,“[*]”在預設情況下是什麼都不顯示的,只有當呼叫setWindowModified()
                                                //函式的時候,會自動在由“[*]”的地方加上“*”,後面的文字會自動後移
    connect(document(), SIGNAL(contentsChanged()), this, SLOT(DocumentWasModified()));//文件內容發生改變時,
                                                                                     //觸發槽函式DocumentWasModified().
}

QString MyMdi::CurrentFilePath()
{
    return current_file_path_;//current_file_path_是私有變數,對外隱藏起來了,但是CurrentFilePath()是公有成員函式,顯示出現
}

//設定當前檔案的一些資訊,比如說視窗標題,該檔案的路徑名等
void MyMdi::SetCurrentFile(const QString &file_name)
{
    current_file_path_ = QFileInfo(file_name).canonicalFilePath();//得到解釋過後的絕對路徑名
    is_saved_ = true;//設定為被儲存過,因為該函式是被LoadFile()函式呼叫的,所以肯定可以被當做是儲存過的了
    document()->setModified(false);//文件沒有被改過
    setWindowModified(false);//視窗不顯示被更改的標誌
    setWindowTitle(get_current_file_name() + "[*]");//設定視窗標題
}

bool MyMdi::LoadFile(const QString &file_name)
{
    QFile file(file_name);//建立需開啟的檔案物件
    if(!file.open(QFile::ReadOnly | QFile::Text))
        {
            //開啟失敗時,輸出錯誤資訊
            QMessageBox::warning(this, "多文件編輯器", tr("無法讀取檔案 %1:\n%2").arg(file_name).arg(file.errorString()));
            return false;
        }
    QTextStream in(&file);//文字流
    QApplication::setOverrideCursor(Qt::WaitCursor);//設定整個應用程式的游標形狀為等待形狀,因為如果檔案的內容非常多時可以提醒使用者
    setPlainText(in.readAll());//讀取文字流中的所有內容,並顯示在其窗體中
    QApplication::restoreOverrideCursor();//恢復開始時的游標狀態
    SetCurrentFile(file_name);//設定標題什麼的
    //注意這裡發射訊號用的是contentsChanged(),而不是contentsChange().
    connect(document(), SIGNAL(contentsChanged()), this, SLOT(DocumentWasModified()));

    return true;
}

QString MyMdi::get_current_file_name()
{
    return QFileInfo(current_file_path_).fileName();//從當前檔案路徑名中提取其檔名
}

void MyMdi::DocumentWasModified()
{
    setWindowModified(document()->isModified());//“*”顯示出來

}

bool MyMdi::has_saved()
{
    if(document()->isModified())
        {
            QMessageBox box;
            box.setWindowTitle(tr("多文件編輯器"));
            box.setText(tr("是否儲存對%1的更改?").arg(get_current_file_name()));
            box.setIcon(QMessageBox::Warning);//警告圖示
            //下面是訊息box上新增3個按鈕,分別為yes,no,cancel
            QPushButton *yes_button = box.addButton(tr(""), QMessageBox::YesRole);
            QPushButton *no_button = box.addButton(tr(""), QMessageBox::NoRole);
            QPushButton *cancel_button = box.addButton(tr("取消"), QMessageBox::RejectRole);
            box.exec();//在這裡等待使用者選擇3個按鈕中的一個
            if(box.clickedButton() == yes_button)
                return Save();
            else if(box.clickedButton() == no_button)
                return true;//不用儲存,直接關掉
            else if(box.clickedButton() == cancel_button)
                return false;//什麼都不做

        }
    return true;//要麼已經儲存好了,要麼根本就沒更改過其內容
}

bool MyMdi::Save()
{
    if(is_saved_)//已經儲存過至少一次後,則說明檔案的檔名等已經弄好了,直接儲存內容即可。
        return SaveFile(current_file_path_);
    else return SaveAs();//第一次儲存時,需要呼叫SaveAs
}

bool MyMdi::SaveAs()
{
    //返回的名字file_name是自己手動輸入的名字,或者直接採用的是預設的名字
    QString file_name = QFileDialog::getSaveFileName(this, tr("另存為"), current_file_path_);
    if(file_name.isEmpty())
        return false;

    return SaveFile(file_name);
}

bool MyMdi::SaveFile(const QString &file_name)
{
    QFile file(file_name);
    //即使是寫入文字,也得將文字先開啟
    if(!file.open(QFile::WriteOnly | QFile::Text))
        {
            QMessageBox::warning(this, "多文件編輯器", tr("無法寫入檔案 %1:\n%2").arg(file_name).arg(file.errorString()));
            return false;
        }
    QTextStream out(&file);
    QApplication::setOverrideCursor(Qt::WaitCursor);
    out << toPlainText();//以純文字方式寫入,核心函式
    QApplication::restoreOverrideCursor();
    //返回之前,也將該檔案的標題,路徑名等設定好。
    SetCurrentFile(file_name);
    return true;
}

void MyMdi::contextMenuEvent(QContextMenuEvent *event)
{
    QMenu *menu = new QMenu;

    //QKeySequence類是專門封裝快捷鍵的,這裡使用的是預設的快捷鍵操作,其快捷鍵位"&"號後面那個字母
    QAction *undo = menu->addAction(tr("撤銷(&U)"), this, SLOT(undo()), QKeySequence::Undo);//直接呼叫槽函式undo()
    undo->setEnabled(document()->isUndoAvailable());//因為該類是一個widget,所以可以直接使用document()函式

    QAction *redo = menu->addAction(tr("恢復(&A)"), 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());

    menu -> addAction(tr("貼上&P"), this, SLOT(paste()), QKeySequence::Paste);

    QAction *clear = menu->addAction(tr("清空"), this, SLOT(clear()));
    clear->setEnabled(!document()->isEmpty());//文字內容非空時就可以清除

    menu->addSeparator();//增加分隔符

    QAction *select_all = menu->addAction(tr("全選"), this, SLOT(selectAll()), QKeySequence::SelectAll);
    select_all->setEnabled(!document()->isEmpty());

    menu->exec(event->globalPos());//獲取滑鼠位置,並顯示選單

    delete menu;//銷燬這個選單
}

//該函式是頂層視窗被關閉時發出的事件,是關閉視窗自帶的關閉符號X
void MyMdi::closeEvent(QCloseEvent *event)//要記得加入 #include <QCloseEvent>
{
    if(has_saved())
        event->accept();//儲存完畢後直接退出程式
    else
        event->ignore();

}
複製程式碼

mainwindow.h:

複製程式碼
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
//#include "mymdi.h"
#include <QAction>

class MyMdi;
class QMdiSubWindow;//加入一個類相當於加入一個頭檔案?
class QSignalMapper;//這是個跟訊號發射相關的類

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
    
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    
private slots:
 //   void set_active_sub_window(QWidget *window);
    MyMdi *CreateMyMdi();
    void set_active_sub_window(QWidget *window);
    void UpdateMenus();
    void ShowTextRowCol();
    void UpdateWindowMenu();
    void closeEvent(QCloseEvent *event);

    void on_actionNew_triggered();

    void on_actionOpen_triggered();

    void on_actionExit_triggered();

    void on_actionSave_triggered();

    void on_actionSaveAs_triggered();

    void on_actionCut_triggered();

    void on_actionCopy_triggered();

    void on_actionPaste_triggered();

    void on_actionUndo_triggered();

    void on_actionRedo_triggered();

    void on_actionClose_triggered();

    void on_actionCloseAll_triggered();

    void on_actionTile_triggered();

    void on_actionCascade_triggered();

    void on_actionNext_triggered();

    void on_actionPrevious_triggered();

    void on_actionAbout_triggered();

    void on_actionAboutQt_triggered();

private:
    Ui::MainWindow *ui;

    QAction *actionSeparator;
    QMdiSubWindow *FindMdiChild(const QString &file_name);//查詢子視窗
    MyMdi *GetActiveWindow();
    QSignalMapper *window_mapper;
    void read_settings();
    void write_settings();
    void init_window();
};

#endif // MAINWINDOW_H
複製程式碼

mainwindow.cpp:

複製程式碼
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mymdi.h"
#include <QFileDialog>
#include <QMdiSubWindow>
#include <QDebug>
#include <QSignalMapper>
#include <QSettings>
#include <QCloseEvent>
#include <QLabel>
#include <QMessageBox>
#include <QMenu>

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

    window_mapper = new QSignalMapper(this);//建立訊號發生器
    connect(window_mapper, SIGNAL(mapped(QWidget*)), this, SLOT(set_active_sub_window(QWidget*)));//通過訊號發生器設定活動視窗

    UpdateWindowMenu();//更新視窗子選單
    connect(ui->menuW, SIGNAL(aboutToShow()), this, SLOT(UpdateWindowMenu()));//當視窗子選單將要出現時,就觸發更新視窗子選單

    read_settings();//因為在退出視窗時,執行了write_settings()函式,即儲存了退出視窗時的視窗位置,尺寸等資訊。因此下次開啟該程式時,其位置尺寸
                    //等資訊會保留
    init_window();//初始化視窗
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_actionNew_triggered()
{
//    MyMdi *new_mdi = new MyMdi();
//   ui->mdiArea->addSubWindow(new_mdi);

    /*為什麼不能使用上面的方法呢?因為上面的方法沒有涉及到文件內容改變時,比如選中了文字,有過撤銷操作等。
     即使我們又UpdateMenus()函式,但是關聯它的connect函式的訊號為當有新的活動窗口出現時,所以一旦新
     的活動窗口出現後,後面該文件內容的改變就不會觸發選單欄和工具欄對應action的變化了。
    */
    MyMdi *new_mdi = CreateMyMdi();
    new_mdi->NewFile();//新建檔案
    new_mdi->show();
}

void MainWindow::on_actionOpen_triggered()
{
    QString file_name = QFileDialog::getOpenFileName(this);//手動選擇需要開啟的檔案,其實返回的file_name是包含路徑名的檔名
    if(!file_name.isEmpty())
        {
            QMdiSubWindow *existing_window = FindMdiChild(file_name);
            if(existing_window) //如果該檔案對應視窗已經開啟
            {
                set_active_sub_window(existing_window);//設定該視窗為活動視窗,雖然set_active_sub_window是該類的成員函式,但是不能使用
                                                        //ui->來呼叫,冒失ui->呼叫的都是跟介面相關自動生成的一些量
                return ;
            }
            MyMdi *open_window = CreateMyMdi();//否則新建子視窗,且加入到多文件容器中
            if(open_window->LoadFile(file_name))
                {
                    ui->statusBar->showMessage(tr("開啟檔案成功"), 2000);//狀態列顯示開啟檔案成功,持續2秒
                    open_window->show();
                }
            else
                {
                    open_window->close();//打不開該檔案時,則銷燬新建的視窗
                }
        }
}

MyMdi* MainWindow::CreateMyMdi()
{
    MyMdi *child = new MyMdi();
    ui->mdiArea->addSubWindow(child);

    //根據是否可複製來設定剪下複製動作是否可用
    connect(child, SIGNAL(copyAvailable(bool)), ui->actionCopy, SLOT(setEnabled(bool)));
    connect(child, SIGNAL(copyAvailable(bool)), ui->actionCut, SLOT(setEnabled(bool)));

    //