1. 程式人生 > >Qt 訊號和槽

Qt 訊號和槽

訊號和槽

訊號和槽用於物件間的通訊。訊號/槽機制是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);
        }
    }

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()訊號,但是因為 沒有槽被連線到bvalueChanged()訊號,所以沒有發生任何事 (訊號消失了)。

注意只有當v != val的時候setValue()函式才會設定這個值 並且發射訊號。這樣就避免了在迴圈連線的情況下(比如b.valueChanged() 和a.setValue()連線在一起)出現無休止的迴圈的情況。

這個例子說明了物件之間可以在互相不知道的情況下一起工作,只要在最初的時在 它們中間建立連線。

預處理程式改變或者移除了signalsslotsemit 這些關鍵字,這樣就可以使用標準的C++編譯器。

在一個定義有訊號和槽的類上執行moc。這樣就會生成 一個可以和其它物件檔案編譯和連線成引用程式的C++原始檔。

訊號

當物件的內部狀態發生改變,訊號就被髮射,在某些方面對於 物件代理或者所有者也許是很有趣的。只有定義了一個訊號的類和它的子類才能發射 這個訊號。

例如,一個列表框同時發射highlighted()activated()這 兩個訊號。絕大多數物件也許只對activated()這個訊號感興趣,但是有時 想知道列表框中的哪個條目在當前是高亮的。如果兩個不同的類對同一個訊號感興趣, 你可以把這個訊號和這兩個物件連線起來。

當一個訊號被髮射,它所連線的槽會被立即執行,就像一個普通函式呼叫一樣。 訊號/槽機制完全不依賴於任何一種圖形使用者介面的事件迴路。當所有的槽都返回後 emit也將返回。

如果幾個槽被連線到一個訊號,當訊號被髮射時,這些槽就會被按任意順序一個 接一個地執行。

訊號會由moc自動生成並且一定不要在.cpp檔案中 實現。它們也不能有任何返回型別(比如使用void)。

關於引數需要注意。我們的經驗顯示如果訊號和槽使用特殊的型別, 它們都可以多次使用。如果QScrollBar::valueChanged() 使用了一個特殊的型別,比如hypothetical QRangeControl::Range,它就只能被連 接到被設計成可以處理QRangeControl的槽。簡 單的和教程1的第5部分一樣的程式將是不可 能的。

當一個和槽連線的訊號被髮射的時候,這個操被呼叫。槽也是 普通的C++函式並且可以像它們一樣被呼叫;它們唯一的特點就是它們可以被訊號連 接。槽的引數不能含有預設值,並且和訊號一樣,為了槽的引數而使用自己特定的類 型是很不明智的。

因為槽就是普通成員函式,但卻有一點非常有意思的東西,它們也和普通成員函 數一樣有訪問許可權。一個槽的訪問許可權決定了誰可以和它相連:

一個public slots:區包含了任何訊號都可以相連的槽。這對於元件編 程來說非常有用:你生成了許多物件,它們互相併不知道,把它們的訊號和槽連線起 來,這樣資訊就可以正確地傳遞,並且就像一個鐵路模型,把它開啟然後讓它跑起來。

一個protected slots:區包含了之後這個類和它的子類的訊號才能連線 的槽。這就是說這些槽只是類的實現的一部分,而不是它和外界的介面。

一個private slots:區包含了之後這個類本身的訊號可以連線的槽。這 就是說它和這個類是非常緊密的,甚至它的子類都沒有獲得連線權利這樣的信任。

你也可以把槽定義為虛的,這在實踐中被發現也是非常有用的。

訊號和槽的機制是非常有效的,但是它不像“真正的”回撥那樣快。訊號和槽稍 微有些慢,這是因為它們所提供的靈活性,儘管在實際應用中這些不同可以被忽略。 通常,發射一個和槽相連的訊號,大約只比直接呼叫那些非虛擬函式呼叫的接收器慢 十倍。這是定位連線物件所需的開銷,可以安全地重複所有地連線(例如在發射期 間檢查併發接收器是否被破壞)並且可以按一般的方式安排任何引數。當十個非虛 函式呼叫聽起來很多時,舉個例子來說,時間開銷只不過比任何一個“new”或者 “delete”操作要少些。當你執行一個字串、向量或者列表操作時,需要“new”或者 “delete”,訊號和槽僅僅對一個完整函式呼叫地時間開銷中的一個非常小的部分負 責。無論何時你在一個槽中使用一個系統呼叫和間接地呼叫超過十個函式的時間是 相同的。在一臺i585-500機器上,你每秒鐘可以發射2,000,000個左右連線到一個 接收器上的訊號,或者發射1,200,000個左右連線到兩個接收器的訊號。訊號和槽 機制的簡單性和靈活性對於時間的開銷來說是非常值得的,你的使用者甚至察覺不出來。

元物件資訊

元物件編譯器(moc) 解析一個C++檔案中的類宣告並且生成初始化元物件的C++程式碼。元物件包括所有訊號 和槽函式的名稱,還有這些函式的指標。(要獲得更多的資訊,請看為什麼Qt不用模板來實現訊號和槽?

元物件包括一些額外的資訊,比如物件的類名稱。 你也可以檢查一個物件是否繼承了一個特定的類, 比如:

  if ( widget->inherits("QButton") ) {
        // 是的,它是一個Push Button、Radio Button或者其它按鈕。
  }

一個真實的例子

這是一個註釋過的簡單的例子(程式碼片斷選自qlcdnumber.h)。

    #include "qframe.h"
    #include "qbitarray.h"

    class QLCDNumber : public QFrame

QLCDNumber通過QFrameQWidget,還有#include這樣的相關宣告繼承了含有絕大 多數訊號/槽知識的QObject

    {
        Q_OBJECT

Q_OBJECT是由前處理器展開宣告幾個由moc來實現的成員函式,如果你得到了幾行 “virtual function QButton::className not defined”這樣的編譯器錯誤資訊,你也 許忘記執行moc或者忘記在連線命令中包含moc輸出。

    public:
        QLCDNumber( QWidget *parent=0, const char *name=0 );
        QLCDNumber( uint numDigits, QWidget *parent=0, const char *name=0 );

它並不和moc直接相關,但是如果你繼承了QWidget,你當然想在你的構造器中獲 得parentname這兩個引數,而且把它們傳遞到父類的構造器中。

一些解析器和成員函式在這裡省略掉了,moc忽略了這些成員函式。

    signals:
        void    overflow();

QLCDNumber被請求顯示一個不可能值時,它 發射一個訊號。

如果你沒有留意溢位,或者你認為溢位不會發生,你可以忽略overflow()訊號, 也就是說你可以不把它連線到任何一個槽上。

另一方面如果當數字溢位時,你想呼叫兩個不同的錯誤函式,很簡單地你可 以把這個訊號和兩個不同的槽連線起來。Qt將會兩個都呼叫(按任意順序)。

    public slots:
        void    display( int num );
        void    display( double num );
        void    display( const char *str );
        void    setHexMode();
        void    setDecMode();
        void    setOctMode();
        void    setBinMode();
        void    smallDecimalPoint( bool );

一個槽就是一個接收函式,用來獲得其它視窗部件狀態變或的資訊。QLCDNumber 使用它,就像上面的程式碼一樣,來設定顯示的數字。因為display()是這個 類和程式的其它的部分的一個介面,所以這個槽是公有的。

幾個例程把QScrollBar的newValue訊號連線到display槽,所以LCD數字可以繼續顯示滾動條的值。

請注意display()被過載了,當你把一個訊號和這個槽相連的時候,Qt將會選擇適 當的版本。如果使用回撥,你會發現五個不同的名字並且自己來跟蹤型別。

一些不相關的成員函式已經從例子中省略了。