自訂 Signal 與 Slot


除了使用Qt現有元件預先定義好的Signal與Slot之外,您也可以定義自己物件的Signal與Slot,方式是繼承QObject或它的子類別(例如QWidget),以下直接使用實際例子來說明。

使用 Signal 與 Slot(使用拉桿改變LCD數字) 中,您直接使用拉桿來變動LCD數字顯示,假設現在您想要定義一個物件,當拉桿拉動時,必須通知該物件儲存拉桿的游標值,而物件儲存的游標值有變動時,LCD數字顯示也必須更新,這樣的一個物件不是圖形元件,它是個資料模型,用以儲存與圖形介面無關的資料。

您可以這麼定義一個Model類別:
  • Model.h
#ifndef MODEL_H
#define MODEL_H
#include <QObject>

class Model : public QObject {
Q_OBJECT

public:
Model() { m_value = 0; }

int value() const { return m_value; }

public slots:
void setValue(int);

signals:
void valueChanged(int);

private:
int m_value;
};

#endif

只有在MODEL_H名稱沒被定義過(#ifndef)的情況下,才會編譯以下的定義內容。這是個類別定義的技巧,可以避免類別定義的重覆,如果類別定義重覆,該段定義將不會被編譯:
#ifndef MODEL_H
#define MODEL_H

再來簡介一下Qt的Meta-Object System,它基於以下三個部份:
  • QObject類別
  • Q_OBJECT巨集
  • Meta-Object Compiler(moc)

Qt管理的物件必須繼承QObject類別,以提供Qt物件的Meta訊息,若要實作Signal與Slot機制,則必須包括Q_OBJECT巨集,moc會處理Qt的C++擴充(Meta-Object System),使用moc讀取C++標頭檔案,若發現類別定義中包括Q_OBJECT巨集,就會產生Qt meta-object相關的C++程式碼。

若您使用qmake來產生Makefile,若必要時,檔案中就會包括moc的使用,程式完成建置之後,會在release或debug目錄中,找到moc_Model.cpp,即為moc所提供的C++程式碼。

在Model中,自訂了Signal與Slot,slots與signals關鍵字其實是巨集,將被 展開為相關的程式碼,其中Slot定義setValue(int),將接收Signal傳來的整數資料,如果不想接受資料的話,int可以省去, Signal定義valueChanged(int),表示將發出的Signal會帶有一個整數:
public slots:
    void setValue(int);

signals:
     void valueChanged(int);

Signal與Slot的簽名是對應的,若Signal帶有參數,則對應的Slot也要帶有參數。

接著定義Model.cpp:
  • Model.cpp
#include "Model.h"

void Model::setValue(int value) {
if (value != m_value) {
m_value = value;
emit valueChanged(m_value);
}
}

Slot只是
一般的函式,可以由程式的其它部份直接呼叫,也可以連接至Signal,若有呼叫setValue(),程式執行到emit時,就會發出valueChanged()的Signal。

接著修改一下
使用 Signal 與 Slot(使用拉桿改變LCD數字) 中的範例:
  • main.cpp
#include <QApplication>
#include <QWidget>
#include <QSlider>
#include <QLCDNumber>
#include "Model.h"

int main(int argc, char *argv[]) {
QApplication app(argc, argv);

QWidget *parent = new QWidget;
parent->setWindowTitle("Signal & Slot");
parent->setMinimumSize(240, 140);
parent->setMaximumSize(240, 140);

QLCDNumber *lcd = new QLCDNumber(parent);
lcd->setGeometry(70, 20, 100, 30);

QSlider *slider = new QSlider(Qt::Horizontal, parent);
slider->setRange(0, 99);
slider->setValue(0);
slider->setGeometry(70, 70, 100, 30);

Model model;

QObject::connect(slider, SIGNAL(valueChanged(int)),
&model, SLOT(setValue(int)));
QObject::connect(&model, SIGNAL(valueChanged(int)),
lcd, SLOT(display(int)));

parent->show();

return app.exec();
}

您使用connect()連接QSlider的valueChanged() Signal及Model的setValue() Slot,所以拉動拉桿時,Model的m_value就會被設定為QSlider的游標值,而setValue()中使用了emit發出valueChanged() Signal,由於您將Model的valueChanged() Signal連接至QLCDNumber的display() Slot,所以LCD顯示數字也會改變。

一個Signal可以連至數個Slot,例如這個程式的Signal與Slot連接也可以改為,由QSlider同時發出Signal給Model及LCD顯示,執行結果不變:
QObject::connect(slider, SIGNAL(valueChanged(int)),
                 &model, SLOT(setValue(int)));
QObject::connect(slider, SIGNAL(valueChanged(int)),
                 lcd, SLOT(display(int)));    

一個Slot也可以被數個Signal連接,例如:
QObject::connect(slider, SIGNAL(valueChanged(int)),
                 &model, SLOT(setValue(int)));
QObject::connect(combox, SIGNAL(valueChanged(int)),
                 &model, SLOT(setValue(int)));

Signal與Slot的簽名基本上要相同,但若Signal的參數多於Slot的參數,則額外的參數會被Slot忽略。如果要斷開Signal與Slot的連接,則使用disconnect(),例如:
QObject::disconnect(slider, SIGNAL(valueChanged(int)),
                    &model, SLOT(setValue(int)));