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
實驗過程:
下面講的主要是一個個簡單功能的逐步實現過程的某些細節。
介面的設計:
在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))); //