Qt信號槽機制


信號槽

信號槽被用於對象間的通訊。信號槽機制是 Qt 的核心機制,可能也是 Qt 與其他框架的最大區別。


簡介

GUI 編程中,當我們改變了一個組件,我們經常需要通知另外的一個組件。更一般地,我們希望任何類型的對象都能夠與另外的對象通訊。例如,如果用戶點擊了關閉按鈕,我們希望窗口的 close() 函數被調用。

早期工具庫對這種通訊使用回調實現。回調是一個指向一個函數的指針,所以如果你希望某種事件發生的時候,處理函數獲得通知,你就需要將指向另外函數的指針(也就是這個回調)傳遞給處理函數。這樣,處理函數就會在合適的時候調用回調函數。回調有兩個明顯的缺點:第一,它們不是類型安全的。我們不能保證處理函數傳遞給回調函數的參數都是正確的。第二,回調函數和處理函數緊密地耦合在一起,因爲處理函數必須知道哪一個函數被回調。


信號

當一個對象中某些可能會有別的對象關心的狀態被修改時,將會發出信號。只有定義了信號的類及其子類可以發出信號。

當一個信號被髮出時,連接到這個信號的槽立即被調用,就像一個普通的函數調用。當這種情況發生時,信號槽機制獨立於任何 GUI 事件循環。emit 語句之後的代碼將在所有的槽返回之後被執行。這種情況與使用連接隊列略有不同:使用連接隊列的時候,emit 語句之後的代碼將立即被執行,而槽在之後執行。

如果一個信號連接了多個槽,當信號發出時,這些槽將以連接的順序一個接一個地被執行。

信號由 [[moc]] 自動生成,並且不能夠在 .cpp 文件中被實現。它們不能有返回值(例如使用 void)。

使用參數的注意事項:我們的經驗是,不使用特定類型參數的信號和槽更容易被重用。如果 QScrollBar::valueChanged() 使用了特殊的類型,比如 QScrollBar::Range,那麼它就只能被 [[QScrollBar]] 的特定的槽連接到,不可能同時連接不同的輸入組件。


當連接到的信號發出時,槽就會被調用。槽是普通的 C++ 函數,能夠被正常的調用。它們的唯一特點是能夠與信號連接。

既然信號就是普通的成員函數,當它們像普通函數一樣調用的時候,遵循標準 C++ 的規則。但是,作爲槽,它們又能夠通過信號槽的連接被任何組件調用,不論這個組件的訪問級別。這意味着任意類的實例發出的信號,都可以使得不相關的類的私有槽被調用。

你也能把槽定義成虛的,這一點在實際應用中非常有用。

相比回調,信號槽要慢一些,因爲它們提供了更大的靈活性,雖然對於正式的應用而言,這一點微不足道。一般來說,發出一個連接到槽的信號,要比直接調用接收函數慢大約十倍。它的開銷主要在於定位連接對象,安全遍歷所有連接(例如,檢查信號發出期間,所有剩餘的接收者沒有被銷燬),以一種普適的方式掃描所有參數。雖然是個非虛函數調用聽起來很多,但是其開銷仍然比 new 或者 delete 小很多。只要你需要操作字符串,向量或者鏈表,這些操作背後都有着 new 或者 delete,信號槽的開銷只佔總開銷的很小一部分。

當你在槽中進行一次系統調用,或者間接調用十次函數,情況也是類似的。在 i586-500 上面,你可以向一個連接的接收者每秒發出 2,000,000 個信號,或者是向兩個接收者每秒發出 1,200,000 個信號。信號槽機制的簡單性和靈活性完全值得付出這些開銷,這些開銷用戶是很難注意到的。

注意,如果在基於 Qt 的應用程序中混用了定義有同名變量 signals 或者 slots 的庫的時候,可能會引起編譯器警告和錯誤。在這種情況下,使用 #undef 接觸預編譯符號。


元對象信息

元對象編譯器(moc)處理 C++ 文件中的類聲明,並且生成初始化元對象的 C++ 代碼。元對象包含有所有信號槽的名字,以及這些函數的指針。

元對象還含有額外的信息,例如對象的類名。你也可以檢查一個對象是否QObject#inherits繼承自某一特定的類,例如:

if(widget->inherits("QAbstractButton")){
    QAbstractButton *button =static_cast<QAbstractButton *>(widget);
    button->toggle();
}

元對象信息也被用於 qobject_cast<T>(),這個宏類似於 QObject::inherits(),但是相對較少出現錯誤:

if(QAbstractButton *button = qobject_cast<QAbstractButton *>(widget))
    button->toggle();




信號和槽

Qt 中,我們有回調技術之外的選擇:信號槽。當特定事件發出時,一個信號會被髮出。Qt 組件有很多預定義的信號,同時,我們也可以通過繼承這些組件,添加自定義的信號。槽則能夠響應特定信號的函數。Qt 組件有很多預定義的槽,但是更常見的是,通過繼承組件添加你自己的槽,以便你能夠按照自己的方式處理信號。

wKioL1Q8j9nSrjGoAAG9D0asLi4532.jpg

信號槽機制是類型安全的:信號的簽名必須同接受該信號的槽的簽名一致(實際上,槽的參數個數可以比信號少,因爲槽能夠忽略信號定義的多出來的參數)。既然簽名都是兼容的,那麼編譯器就可以幫助我們找出不匹配的地方。信號和槽是鬆耦合的:發出信號的類不知道也不關心哪些槽連接到它的信號。Qt 的信號槽機制保證了,如果你把一個信號同一個槽連接,那麼在正確的時間,槽能夠接收到信號的參數並且被調用。信號和槽都可以有任意類型的任意個數的參數。它們全部都是類型安全的。

所有繼承自 QObject 或者它的一個子類(例如 QWidget都可以包含信號槽。信號在對象改變其狀態,並且這個狀態可能有別的對象關心時被髮出。這就是這個對象爲和別的對象交互所做的所有工作。它並不知道也不關心有沒有別的對象正在接收它發出的信號。這是真正的信息封裝,保證了這個對象能夠成爲一個組件。

槽能夠被用於接收信號,也能夠像普通函數一樣使用。正如一個對象並不知道究竟有沒有別的對象正在接收它的信號一樣,一個槽也不知道有沒有信號與它相連。這保證了使用 Qt 可以創建真正相互獨立的組件。

你可以將任意多個信號連接到同一個槽上,也可能將一個信號連接任意多個槽。同時,也能夠直接將一個信號與另一個信號相連(這會使第一個信號發出時,馬上發出第二個信號)。

總值,信號槽建立起一種非常強大的組件編程機制。


一個簡單的例子

一個最簡單的 C++ 類:

class Counter
{
public:
    Counter(){ m_value =0;}
 
    int value()const{return m_value;}
    void setValue(int value);
 
private:
    int m_value;
};

一個基於 QObject 的最簡單的類:

#include<QObject>
 
class Counter :public QObject
{
    Q_OBJECT
 
public:
    Counter(){ m_value =0;}
 
    int value()const{return m_value;}
 
public slots:
    void setValue(int value);
 
signals:
    void valueChanged(int newValue);
 
private:
    int m_value;
};

繼承了 QObject 的版本有着類似的私有屬性,提供了訪問器訪問這些屬性。但是它使用信號槽實現了組件編程。這個類可以在它的私有屬性改變時通過發出信號 valueChanged() 通知外界,同樣,它也有一個槽,用於接收別的對象發出的信號。

所有包含信號槽的類都必須在聲明的上部含有 Q_OBJECT 宏。它們也必須繼承(直接或間接)自 QObject

槽由應用程序編程人員實現。下面是一個 Counter::setValue() 的可能的實現:

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

emit 那一行發出了valueChanged() 信號,將新的值作爲信號的參數。

在下面的代碼片段中,我們創建了兩個 Counter 對象,使用 QObject::connect() 函數將第一個對象的 valueChanged() 信號同第二個對象的 setValue() 槽連接起來。

Counter a, b;
QObject::connect(&a, SIGNAL(valueChanged(int)),
                 &b, SLOT(setValue(int)));
 
a.setValue(12);     // a.value() == 12, b.value() == 12
b.setValue(48);     // a.value() == 12, b.value() == 48

調用 a.setValue(12) 導致發出valueChanged(12) 信號。這個信號將被 b setValue() 槽接收到,也就是說,b.setValue(12) 將被調用。而當 b 發出相同的valueChanged() 信號時,因爲沒有槽連接到 b valueChanegd() 信號上面,因此該信號將被忽略。

注意,當且僅當 value != m_value 的時候,setValue() 才設置新的值,並且發出信號。這避免的無限循環的出現(否則的話,如果 b.valueChanged() 再與a.setValue() 相連,就會出現無限循環)。

默認情況下,一個連接發出一個信號,重複的連接將發出兩個信號。你可以用過調用 disconnect() 函數來去除這些連接。如果你傳入 Qt::UniqueConnection 類型,則當連接不是重複的時候纔會被建立。如果已經存在重複的連接(所謂重複的連接,指的是同一個對象的相同的信號連接到相同的槽上面),這個連接將會失敗,connect 返回 false

這個例子解釋了對象可以在一起工作,而不需要知道彼此的任何信息。爲了達到這一目的,只需要將它們連接起來,而這一操作通過簡單地調用 QObject::connect() 函數,或者是 uic 自動連接特性即可實現。


構建例子

C++ 預處理器將使用標準 C++ signalsslots emit 關鍵字替換掉。

通過對含有信號槽的類運行 moc,將生成一個供應用程序中其他目標文件編譯、鏈接的標準 C++ 源文件。如果你使用 qmakemakefile 將自動添加運行 moc 的規則。



一個真實的例子

這裏有一個組件信號槽的例子:

#ifndefLCDNUMBER_H
#defineLCDNUMBER_H
 
#include<QFrame>
 
class LcdNumber :public QFrame
{
    Q_OBJECT

LcdNumber 通過 QFrame  QWidget 繼承了 QObject,信號槽的很大一部分代碼都是在這裏實現的。這個類很像內置的 QLCDNumber 組件。

O_OBJECT 宏由預處理器展開,聲明一些有 moc 實現的成員函數。如果你出現了編譯器錯誤,“undefined reference to vtable for LcdNumber,你可能忘記[[運行 moc]],或者是忘記連接 moc 的輸出文件了。

public:
    LcdNumber(QWidget *parent =0);

或者這個看起來同 moc 沒有什麼關係,但是如果你繼承了 QWidget,你應該在你的構造函數中提供一個 parent 參數,並且將其傳遞給父類的構造函數。

這裏忽略了一些析構函數和成員函數。moc 會忽略成員函數。

signals:
    void overflow();

當出現溢出值的時候,LcdNumber 發出一個信號。

如果你不關心溢出,或者是你知道溢出不可能發生,你只需要忽略掉 overflow() 信號,例如,不要把它與任何槽連接。

另一方面,如果你需要在數值溢出時調用兩個不同的錯誤函數,只需要簡單的將這個信號同兩個不同的槽連接即可。Qt 將調用這兩個函數(以任意的順序)。

public slots:
    void display(int num);
    void display(double num);
    void display(const QString &str);
    void setHexMode();
    void setDecMode();
    void setOctMode();
    void setBinMode();
    voidsetSmallDecimalPoint(bool point);
};

槽就是能夠在別的組件改變狀態時接收到信息的接收函數。正如上面的代碼說明的那樣,LcdNumber 使用槽來顯示數字。因爲 display() 是程序其他部分的接口,因此槽是公有的。

在這個例子裏面,程序將 QScrollBar  valueChanged() 信號同 display() 槽連接在一起,於是這個 LCD 數字就可以連續地顯示滾動條的數值了。

注意 display() 被重載了,當你向這個槽連接信號的時候, Qt 會自動選擇合適的版本。如果使用回調的話,你不得不選擇五個不同的名字,並且注意它們的類型。

這個例子已經省略了許多不相關的函數。


具有默認參數的信號和槽

信號和槽的簽名可以包含參數,而這些參數也可以有默認值。請看 QObject::destroyed()

void destroyed(QObject*=0);

當一個 QObject 被刪除的時候,它就會發出 QObject::destroyed() 信號。不論我們在哪裏用到了這個 QObject 的引用,我們都希望能夠捕獲這個信號,以便做出清理。一個合適的槽可能是:

void objectDestroyed(QObject* obj =0);

爲了將信號與槽連接起來,我們使用 QObject::connect() 函數以及 SIGNAL()  SLOT() 宏。是否需要在 SIGNAL()  SLOT() 宏中包含參數的規則是,如果參數具有默認值,則傳遞給 SIGNAL() 宏的參數數目不能比傳遞給 SLOT() 的參數數目少。

以下這些都能夠正確工作:

connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

但是下面的就不能:

connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));

因爲槽需要接收一個 QObject 的參數,而信號並沒有發出這個參數。這個連接將出現一個運行時錯誤。


信號槽的高級使用

有些情況下,你可能需要知道信號發送者的信息,Qt 提供了 QObject::sender() 函數,其返回值是發送這個信號的對象的指針。

QSignalMapper 類可以用於解決很多信號連接到同一個槽,並且這個槽需要針對每一個信號做出不同的處理。

假如你有三個按鈕,“Tax File”“Accounts File”“ReportFile”,來決定你將要打開哪一個文件。

爲了打開正確的文件,你可以使用 QSignalMapper::setMapper() 函數,將所有的 clicked() 信號映射到 QSignalMapper 對象。然後你將 QPushButton::clicked() 信號連接到 QSignalMapper::map()槽上面。

signalMapper =new QSignalMapper(this);
signalMapper->setMapping(taxFileButton,QString("taxfile.txt"));
signalMapper->setMapping(accountFileButton,QString("accountsfile.txt"));
signalMapper->setMapping(reportFileButton,QString("reportfile.txt"));
 
connect(taxFileButton,SIGNAL(clicked()),
    signalMapper, SLOT (map()));
connect(accountFileButton,SIGNAL(clicked()),
    signalMapper, SLOT (map()));
connect(reportFileButton,SIGNAL(clicked()),
    signalMapper, SLOT (map()));

然後,你將 mapped() 信號同 readFile() 槽連接到一起。這個槽則通過查看哪一個按鈕被點擊,來打開不同的文件。

connect(signalMapper,SIGNAL(mapped(const QString &)),
     this, SLOT(readFile(const QString &)));


混合使用 Qt 和第三方信號槽

混合使用 Qt 同第三方庫的信號槽機制也是可能的。你甚至可以在用一個項目中同時使用這兩種機制。只需要在你的 qmake 文件(.pro)中添加下面一行:

CONFIG += no_keywords

這句告訴 Qt 不要定義 moc 關鍵字 signalsslots  emit,因爲這些名字可能被第三方庫使用,例如 Boost。爲了在定義了 no_keywords 標記之後繼續使用 Qt 的信號槽,需要將你的代碼中的所有 moc 關鍵字替換爲 Qt 的宏 Q_SIGNALS(或者是 Q_SIGNAL),Q_SLOTS(或者是 Q_SLOT),以及 Q_EMIT


轉載處:http://qtdocs.sourceforge.net/index.php/信號槽



應注意的問題

信號與槽機制是比較靈活的,但有些侷限性我們必須瞭解,這樣在實際的使用過程中做到有的放矢,避免產生一些錯誤。下面就介紹一下這方面的情況。

1 .信號與槽的效率是非常高的,但是同真正的回調函數比較起來,由於增加了靈活性,因此在速度上還是有所損失,當然這種損失相對來說是比較小的,通過在一臺 i586-133 的機器上測試是 10 微秒(運行 Linux),可見這種機制所提供的簡潔性、靈活性還是值得的。但如果我們要追求高效率的話,比如在實時系統中就要儘可能的少用這種機制。

2 .信號與槽機制與普通函數的調用一樣,如果使用不當的話,在程序執行時也有可能產生死循環。因此,在定義槽函數時一定要注意避免間接形成無限循環,即在槽中再次發射所接收到的同樣信號。例如 , 在前面給出的例子中如果在 mySlot() 槽函數中加上語句 emit mySignal() 即可形成死循環。

3 .如果一個信號與多個槽相聯繫的話,那麼,當這個信號被髮射時,與之相關的槽被激活的順序將是隨機的。

4. 宏定義不能用在 signal 和 slot 的參數中。

既然 moc 工具不擴展 #define,因此,在 signals 和 slots 中攜帶參數的宏就不能正確地工作,如果不帶參數是可以的。例如,下面的例子中將帶有參數的宏 SIGNEDNESS(a) 作爲信號的參數是不合語法的:

						 #ifdef ultrix 
    #define SIGNEDNESS(a) unsigned a 
    #else 
    #define SIGNEDNESS(a) a 
    #endif 
    class Whatever : public QObject 
    { 
    [...] 
    signals: 
        void someSignal( SIGNEDNESS(a) ); 
    [...] 
    };

5. 構造函數不能用在 signals 或者 slots 聲明區域內。

的確,將一個構造函數放在 signals 或者 slots 區內有點不可理解,無論如何,不能將它們放在 private slots、protected slots 或者 public slots 區內。下面的用法是不合語法要求的:

						 class SomeClass : public QObject 
    { 
        Q_OBJECT 
    public slots: 
        SomeClass( QObject *parent, const char *name ) 
            : QObject( parent, name ) {}  // 在槽聲明區內聲明構造函數不合語法
    [...] 
    };

6. 函數指針不能作爲信號或槽的參數。

例如,下面的例子中將 void (*applyFunction)(QList*, void*) 作爲參數是不合語法的:

						 class someClass : public QObject 
    { 
        Q_OBJECT 
    [...] 
    public slots: 
        void apply(void (*applyFunction)(QList*, void*), char*); // 不合語法
    };

你可以採用下面的方法繞過這個限制:

      typedef void (*ApplyFunctionType)(QList*, void*); 
    class someClass : public QObject 
    { 
        Q_OBJECT 
    [...] 
    public slots: 
        void apply( ApplyFunctionType, char *); 
    };

7. 信號與槽不能有缺省參數。

既然 signal->slot 綁定是發生在運行時刻,那麼,從概念上講使用缺省參數是困難的。下面的用法是不合理的:

						 class SomeClass : public QObject 
    { 
        Q_OBJECT 
    public slots: 
        void someSlot(int x=100); // 將 x 的缺省值定義成 100,在槽函數聲明中使用是錯誤的
    };

8. 信號與槽也不能攜帶模板類參數。

如果將信號、槽聲明爲模板類參數的話,即使 moc 工具不報告錯誤,也不可能得到預期的結果。 例如,下面的例子中當信號發射時,槽函數不會被正確調用:

						 [...] 
   public slots: 
       void MyWidget::setLocation (pair<int,int> location); 
   [...] 
   public signals: 
       void MyObject::moved (pair<int,int> location);

但是,你可以使用 typedef 語句來繞過這個限制。如下所示:

						 typedef pair<int,int> IntPair; 
   [...] 
   public slots: 
       void MyWidget::setLocation (IntPair location); 
   [...] 
   public signals: 
       void MyObject::moved (IntPair location);

這樣使用的話,你就可以得到正確的結果。

9. 嵌套的類不能位於信號或槽區域內,也不能有信號或者槽。

例如,下面的例子中,在 class B 中聲明槽 b() 是不合語法的,在信號區內聲明槽 b() 也是不合語法的。

						 class A 
    { 
        Q_OBJECT 
    public: 
        class B 
    { 
        public slots:   // 在嵌套類中聲明槽不合語法
            void b(); 
        [....] 
        }; 
    signals: 
        class B 
    { 
        // 在信號區內聲明嵌套類不合語法
        void b(); 
        [....] 
        }: 
    };

10. 友元聲明不能位於信號或者槽聲明區內。

相反,它們應該在普通 C++ 的 private、protected 或者 public 區內進行聲明。下面的例子是不合語法規範的:

						 class someClass : public QObject 
    { 
        Q_OBJECT 
    [...] 
    signals: // 信號定義區
        friend class ClassTemplate; // 此處定義不合語法
    };


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章