1. 程式人生 > >QT事件 --接受和忽略函式的理解

QT事件 --接受和忽略函式的理解

 

//!!! Qt5
// ---------- custombutton.h ---------- //
class CustomButton : public QPushButton
{
    Q_OBJECT
public:
    CustomButton(QWidget *parent = 0);
private:
    void onButtonCliecked();
};

// ---------- custombutton.cpp ---------- //
CustomButton::CustomButton(QWidget *parent) :
    QPushButton(parent)
{
    connect(this, &CustomButton::clicked,
            this, &CustomButton::onButtonCliecked);
}

void CustomButton::onButtonCliecked()
{
    qDebug() << "You clicked this!";
}

// ---------- main.cpp ---------- //
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    CustomButton btn;
    btn.setText("This is a Button!");
    btn.show();

    return a.exec();
}

首先來看一段程式碼:如上

這是一段簡單的程式碼,經過我們前面一段時間的學習,我們已經能夠知道這段程式碼的執行結果:點選按鈕,會在控制檯打印出“You clicked this!”字串。這是我們前面介紹過的內容。下面,我們向CustomButton類新增一個事件函式:

// CustomButton
...
protected:
    void mousePressEvent(QMouseEvent *event);
...

// ---------- custombutton.cpp ---------- //
...
void CustomButton::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        qDebug() << "left";
    } else {
        QPushButton::mousePressEvent(event);
    }
}

我們重寫了CustomButtonmousePressEvent()函式,也就是滑鼠按下。在這個函式中,我們判斷如果滑鼠按下的是左鍵,則打印出來“left”字串,否則,呼叫父類的同名函式。編譯執行這段程式碼,當我們點選按鈕時,“You clicked this!”字串不再出現,只有一個“left”。也就是說,我們把父類的實現覆蓋掉了。由此可以看出,父類QPushButtonmousePressEvent()函式中肯定發出了clicked()訊號,否則的話,我們的槽函式怎麼會不執行了呢?這暗示我們一個非常重要的細節:當重寫事件回撥函式時,時刻注意是否需要通過呼叫父類的同名函式來確保原有實現仍能進行!

比如我們的CustomButton類,如果像我們這麼覆蓋函式,clicked()訊號永遠不會發生,你連線到這個訊號的槽函式也就永遠不會被執行。這個錯誤非常隱蔽,很可能會浪費你很多時間才能找到。因為這個錯誤不會有任何提示。這一定程度上說,我們的元件“忽略”了父類的事件,但這更多的是一種違心之舉,一種錯誤。

通過呼叫父類的同名函式,我們可以把 Qt 的事件傳遞看成鏈狀:如果子類沒有處理這個事件,就會繼續向其父類傳遞。Qt 的事件物件有兩個函式:accept()ignore()。正如它們的名字一樣,前者用來告訴 Qt,這個類的事件處理函式想要處理這個事件;後者則告訴 Qt,這個類的事件處理函式不想要處理這個事件。在事件處理函式中,可以使用isAccepted()來查詢這個事件是不是已經被接收了。具體來說:如果一個事件處理函式呼叫了一個事件物件的accept()函式,這個事件就不會被繼續傳播給其父元件;如果它呼叫了事件的ignore()函式,Qt 會從其父元件中尋找另外的接受者。

事實上,我們很少會使用accept()ignore()函式,而是像上面的示例一樣,如果希望忽略事件(所謂忽略,是指自己不想要這個事件),只要呼叫父類的響應函式即可。記得我們曾經說過,Qt 中的事件都是 protected 的,因此,重寫的函式必定存在著其父類中的響應函式,所以,這個方法是可行的。為什麼要這麼做,而不是自己去手動呼叫這兩個函式呢?因為我們無法確認父類中的這個處理函式有沒有額外的操作。如果我們在子類中直接忽略事件,Qt 會去尋找其他的接收者,該子類的父類的操作會被忽略(因為沒有呼叫父類的同名函式),這可能會有潛在的危險。為了避免自己去呼叫accept()ignore()函式,而是儘量呼叫父類實現,Qt 做了特殊的設計:事件物件預設是 accept 的,而作為所有元件的父類QWidget的預設實現則是呼叫ignore()。這麼一來,如果你自己實現事件處理函式,不呼叫QWidget的預設實現,你就等於是接受了事件;如果你要忽略事件,只需呼叫QWidget的預設實現。這一點我們前面已經說明。下面可以從程式碼級別來理解這一點,我們可以檢視一下QWidgetmousePressEvent()函式的實現:

void QWidget::mousePressEvent(QMouseEvent *event)
{
    event->ignore();
    if ((windowType() == Qt::Popup)) {
        event->accept();
        QWidget* w;
        while ((w = QApplication::activePopupWidget()) && w != this){
            w->close();
            if (QApplication::activePopupWidget() == w)
                w->hide(); // hide at least
        }
        if (!rect().contains(event->pos())){
            close();
        }
    }
}

這段程式碼在 Qt4 和 Qt5 中基本一致(區別在於activePopupWidget()一行,Qt4 的版本是qApp->activePopupWidget())。注意函式的第一個語句:event->ignore(),如果子類都沒有重寫這個函式,Qt 會預設忽略這個事件,繼續尋找下一個事件接收者。如果我們在子類的mousePressEvent()函式中直接呼叫了accept()或者ignore(),而沒有呼叫父類的同名函式,QWidget::mousePressEvent()函式中關於Popup判斷的那段程式碼就不會被執行,因此可能會出現預設其妙的怪異現象。

class CustomButton : public QPushButton
{
    Q_OBJECT
public:
    CustomButton(QWidget *parent) : QPushButton(parent)
    {
    }
protected:
    void mousePressEvent(QMouseEvent *event)
    {
        qDebug() << "CustomButton";
    }
};

class CustomButtonEx : public CustomButton
{
    Q_OBJECT
public:
    CustomButtonEx(QWidget *parent) : CustomButton(parent)
    {
    }
protected:
    void mousePressEvent(QMouseEvent *event)
    {
        qDebug() << "CustomButtonEx";
    }
};

class CustomWidget : public QWidget
{
    Q_OBJECT
public:
    CustomWidget(QWidget *parent) : QWidget(parent)
    {
    }
protected:
    void mousePressEvent(QMouseEvent *event)
    {
        qDebug() << "CustomWidget";
    }
};

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = 0) : QMainWindow(parent)
    {
        CustomWidget *widget = new CustomWidget(this);
        CustomButton *cbex = new CustomButton(widget);
        cbex->setText(tr("CustomButton"));
        CustomButtonEx *cb = new CustomButtonEx(widget);
        cb->setText(tr("CustomButtonEx"));
        QVBoxLayout *widgetLayout = new QVBoxLayout(widget);
        widgetLayout->addWidget(cbex);
        widgetLayout->addWidget(cb);
        this->setCentralWidget(widget);
    }
protected:
    void mousePressEvent(QMouseEvent *event)
    {
        qDebug() << "MainWindow";
    }
};

這段程式碼在一個MainWindow中添加了一個CustomWidget,裡面有兩個按鈕物件:CustomButtonCustomButtonEx。每一個類都重寫了mousePressEvent()函式。執行程式點選 CustomButtonEx,結果是CustomButtonEx

這是因為我們重寫了滑鼠按下的事件,但是並沒有呼叫父類函式或者顯式設定accept()ignore()。下面我們在CustomButtonExmousePressEvent()第一行增加一句event->accept(),重新執行,發現結果不變。正如我們前面所說,QEvent預設是accept的,呼叫這個函式並沒有什麼區別。然後我們將CustomButtonExevent->accept()改成event->ignore()。這次執行結果是

CustomButtonEx
CustomWidget

 

ignore()說明我們想讓事件繼續傳播,於是CustomButtonEx的父元件CustomWidget也收到了這個事件,所以輸出了自己的結果。同理,CustomWidget又沒有呼叫父類函式或者顯式設定accept()ignore(),所以事件傳播就此打住。這裡值得注意的是,CustomButtonEx的事件傳播給了父元件CustomWidget,而不是它的父類CustomButton事件的傳播是在元件層次上面的,而不是依靠類繼承機制。

接下來我們繼續測試,在CustomWidgetmousePressEvent()中增加QWidget::mousePressEvent(event)。這次的輸出是

CustomButtonEx
CustomWidget
MainWindow

如果你把QWidget::mousePressEvent(event)改成event->ignore(),結果也是一樣的。這正如我們前面說的,QWidget的預設是呼叫event->ignore()

在一個特殊的情形下,我們必須使用accept()ignore()函式,那就是視窗關閉的事件。對於視窗關閉QCloseEvent事件,呼叫accept()意味著 Qt 會停止事件的傳播,視窗關閉;呼叫ignore()則意味著事件繼續傳播,即阻止視窗關閉。回到我們前面寫的簡單的文字編輯器。我們在建構函式中新增如下程式碼:

...
textEdit = new QTextEdit(this);
setCentralWidget(textEdit);
connect(textEdit, &QTextEdit::textChanged, [=]() {
    this->setWindowModified(true);
});

setWindowTitle("TextPad [*]");
...

void MainWindow::closeEvent(QCloseEvent *event)
{
    if (isWindowModified()) {
        bool exit = QMessageBox::question(this,
                                      tr("Quit"),
                                      tr("Are you sure to quit this application?"),
                                      QMessageBox::Yes | QMessageBox::No,
                                      QMessageBox::No) == QMessageBox::Yes;
        if (exit) {
            event->accept();
        } else {
            event->ignore();
        }
    } else {
        event->accept();
    }
}

setWindowTitle()函式可以使用 [] 這種語法來表明,在視窗內容發生改變時(通過setWindowModified(true)函式通知),Qt 會自動在標題上面的 [] 位置替換成 * 號。我們使用 Lambda 表示式連線QTextEdit::textChanged()訊號,將windowModified設定為 true。然後我們需要重寫closeEvent()函式。在這個函式中,我們首先判斷是不是有過修改,如果有,則彈出詢問框,問一下是否要退出。如果使用者點選了“Yes”,則接受關閉事件,這個事件所在的操作就是關閉視窗。因此,一旦接受事件,視窗就會被關閉;否則視窗繼續保留。當然,如果視窗內容沒有被修改,則直接接受事件,關閉視窗。

                            -------------------------》》參考豆子老師的講解