Qt入門(3)——訊號和槽

尹成發表於2014-09-04
訊號和槽用於物件間的通訊。訊號/槽機制是Qt的一箇中心特徵並且也許是Qt與其它工具包的最不相同的部分。
在圖形使用者介面程式設計中,我們經常希望一個視窗部件的一個變化被通知給另一個視窗部件。更一般地,我們希望任何一類的物件可以和其它物件進行通訊。例如,如果我們正在解析一個XML檔案,當我們遇到一個新的標籤時,我們也許希望通知列表檢視我們正在用來表達XML檔案的結構。
較老的工具包使用一種被稱作回撥的通訊方式來實現同一目的。回撥是指一個函式的指標,所以如果你希望一個處理函式通知你一些事件,你可以把另一個函式(回撥)的指標傳遞給處理函式。處理函式在適當的時候呼叫回撥。回撥有兩個主要缺點。首先他們不是型別安全的。我們從來都不能確定處理函式使用了正確的引數來呼叫回撥。其次回撥和處理函式是非常強有力地聯絡在一起的,因為處理函式必須知道要呼叫哪個回撥。
 
在Qt中我們有一種可以替代回撥的技術。我們使用訊號和槽。當一個特定事件發生的時候,一個訊號被髮射。Qt的視窗部件有很多預定義的訊號,但是我們總是可以通過繼承來加入我們自己的訊號。槽就是一個可以被呼叫處理特定訊號的函式。Qt的視窗部件又很多預定義的槽,但是通常的習慣是你可以加入自己的槽,這樣你就可以處理你所感興趣的訊號。
訊號和槽的機制是型別安全的:一個訊號的簽名必須與它的接收槽的簽名相匹配。(實際上一個槽的簽名可以比它接收的訊號的簽名少,因為它可以忽略額外的簽名。)因為簽名是一致的,編譯器就可以幫助我們檢測型別不匹配。訊號和槽是寬鬆地聯絡在一起的:一個發射訊號的類不用知道也不用注意哪個槽要接收這個訊號。Qt的訊號和槽的機制可以保證如果你把一個訊號和一個槽連線起來,槽會在正確的時間使用訊號的引數而被呼叫。訊號和槽可以使用任何數量、任何型別的引數。它們是完全型別安全的:不會再有回撥核心轉儲(core dump)。
從QObject類或者它的一個子類(比如QWidget類)繼承的所有類可以包含訊號和槽。當物件改變它們的狀態的時候,訊號被髮送,從某種意義上講,它們也許對外面的世界感興趣。這就是所有的物件通訊時所做的一切。它不知道也不注意無論有沒有東西接收它所發射的訊號。這就是真正的資訊封裝,並且確保物件可以用作一個軟體元件。

槽可以用來接收訊號,但它們是正常的成員函式。一個槽不知道它是否被任意訊號連線。此外,物件不知道關於這種通訊機制和能夠被用作一個真正的軟體元件。
你可以把許多訊號和你所希望的單一槽相連,並且一個訊號也可以和你所期望的許多槽相連。把一個訊號和另一個訊號直接相連也是可以的。(這時,只要第一個訊號被髮射時,第二個訊號立刻就被髮射。)
總體來看,訊號和槽構成了一個強有力的元件程式設計機制。 
 
一個最小的C++類宣告:
    class Foo
    {
    public:
        Foo();
        int value() const { return val; }
        void setValue( int );
    private:
        int val;
    };


一個小的Qt類如下:
    class Foo : public QObject
    {
        Q_OBJECT
    public:
        Foo();
        int value() const { return val; }
    public slots:
        void setValue( int );
    signals:
        void valueChanged( int );
    private:
        int val;
    };


這個類有同樣的內部狀態,和公有方法來訪問狀態,但是另外它也支援使用訊號和槽的元件程式設計:這個類可以通過發射一個訊號,valueChanged(),來告訴外面的世界它的狀態發生了變化,並且它有一個槽,其它物件可以傳送訊號給這個槽。
所有包含訊號和/或者槽的類必須在它們的宣告中提到Q_OBJECT。
槽可以由應用程式的編寫者來實現。這裡是Foo::setValue()的一個可能的實現:
    void Foo::setValue( int v )
    {
        if ( v != val ) {
            val = v;
            emit valueChanged(v);
        }
    }


這個類有同樣的內部狀態,和公有方法來訪問狀態,但是另外它也支援使用訊號和槽的元件程式設計:這個類可以通過發射一個訊號,valueChanged(),來告訴外面的世界它的狀態發生了變化,並且它有一個槽,其它物件可以傳送訊號給這個槽。
    所有包含訊號和/或者槽的類必須在它們的宣告中提到Q_OBJECT。
槽可以由應用程式的編寫者來實現。這裡是Foo::setValue()的一個可能的實現:
    void Foo::setValue( int v )
    {
        if ( v != val ) {
            val = v;
            emit valueChanged(v);
        }
    }


emit valueChanged(v)這一行從物件中發射valueChanged訊號。正如你所能看到的,你通過使用emit signal(arguments)來發射訊號。
下面是把兩個物件連線在一起的一種方法:
    Foo a, b;
    connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int)));
    b.setValue( 11 ); // a == undefined  b == 11
    a.setValue( 79 ); // a == 79         b == 79
    b.value();       


呼叫a.setValue(79)會使a發射一個valueChanged() 訊號,b將會在它的setValue()槽中接收這個訊號,也就是b.setValue(79) 被呼叫。接下來b會發射同樣的valueChanged()訊號,但是因為沒有槽被連線到b的valueChanged()訊號,所以沒有發生任何事(訊號消失了)。
注意只有當v != val的時候setValue()函式才會設定這個值並且發射訊號。這樣就避免了在迴圈連線的情況下(比如b.valueChanged() 和a.setValue()連線在一起)出現無休止的迴圈的情況。
這個例子說明了物件之間可以在互相不知道的情況下一起工作,只要在最初的時在它們中間建立連線。
預處理程式改變或者移除了signals、slots和emit 這些關鍵字,這樣就可以使用標準的C++編譯器。
在一個定義有訊號和槽的類上執行moc。這樣就會生成一個可以和其它物件檔案編譯和連線成引用程式的C++原始檔。

相關文章