程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> Signals & Slots(Qt5)

Signals & Slots(Qt5)

編輯:C++入門知識

>Signal-Slot的作用是對象間的通信; Signals-Slots機制是Qt的核心特性, 也可能是Qt和其他大多數框架提供的特性不同的部分;


介紹


>GUI編程中, 當我們改變了一個widget,經常希望另一個widget能被通知到; 通常我們希望各種對象間能互相通信. Example: 用戶點擊了CLOSE按鈕, 我們會想要讓window的close()函數被調用;

>老一點的toolkit包使用callback機制實現通信. callback是指向函數的指針, 如果你希望一個processing function能在一些事件上通知你, 需要傳遞一個函數指針到那個processing function; processing function會在何時的時候調用callback; Callbacks有兩個基本瑕疵: 第一, 不是類型安全的type-safe. 我們永遠不能確定processing function會用正確的參數來調用callback; 第二, callback和processing function有很強的耦合, processing function必須知道要調用哪一個callback;

Signals and Slots
>Qt使用Signals-Slots代替callback技術; signal在一個特定事件發生時被發出; Qt的widgets有很多預定義的signals, 我們可以自定義subclass來添加自己的signals; slot是一個函數, 接收到對應的signal時會被調用; 同樣, Qt有預定義的slots, 我們也可以自定義slots來處理相關的signals;

>signals-slots機制是type-safe的: signal的原型signature必須和接收信號的slot的signature一致;(slot的signature會短一些, 因為它可以忽略多余的參數) signature和編譯器兼容, 所以編譯器可以進行類型匹配; signal和slot是松耦合的: 一個類發出一個signal, 它不會去關心哪個slot接受到; 對於相關聯的signal和slot, Qt的signal-slot機制保證了slot會在合適的時間接收到signal的參數並被調用; Signal-slot可以傳遞任意個數和類型的參數; 完全type safe;


>所有繼承於QObject或Object的subclass的類都可以包含signal和slot; Signal在對象改變狀態時被發送, 對這個事件感興趣的對象可以處理這個Signal; 我們不知道也不關心是否有對象接收到這個發送的信號; 這是真正的信息封裝, 保證對象被當作軟件的組件來使用;


>Slots可以接收Signals, 他們也可以被當作普通的成員函數; 和Signal一樣, Slot也不用知道它是否和Signal連接起來了. 這樣保證了Qt能創建真正獨立的組件;


>你可以將任意多的signals連接到一個slot, 也可以將一個signal連接到任意多的slots; 甚至可以將一個signal連接到另一個signal(當第一個signal被發出時第二個signal會立即被發出)

Small Example

>基於QObject的類能發出signal告訴外界它的狀態改變了, valueChanged(), 同時有一個slot可以接收其他對象的signal; 所有包含signal-slot的類必須在類聲明的開始聲明Q_OBJECT宏, 並且(直接或間接)繼承自QObject類;


[cpp]
class Counter : public QObject 

    Q_OBJECT 
public: 
    Counter(QObject *parent = 0) { m_value = 0; } 
    int value() const { return m_value; } 
public slots: 
    void setValue(int value); 
signals: 
    void valueChanged(int newValue); 
private: 
    int m_value; 
}; 

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

[cpp]
//Slot由程序員實現  
void Counter::setValue(int value) 

    if (value != m_value) { 
        m_value = value; 
        //發出signal,傳遞新的參數值  
        emit valueChanged(value); 
    } 

//Slot由程序員實現
void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        //發出signal,傳遞新的參數值
        emit valueChanged(value);
    }
}>下面的代碼段創建2個Counter對象, 把第一個對象的valueChanged() signal關聯到第二個對象的setValue() slot;


[cpp]
Counter a, b; 
QObject::connect(&a, &Counter::valueChanged, 
                 &b, &Counter::setValue); 
a.setValue(12);     // a.value() == 12, b.value() == 12  
b.setValue(48);     // a.value() == 12, b.value() == 48 

Counter a, b;
QObject::connect(&a, &Counter::valueChanged,
                 &b, &Counter::setValue);
a.setValue(12);     // a.value() == 12, b.value() == 12
b.setValue(48);     // a.value() == 12, b.value() == 48
>對象a調用setValue(12), 會發送一個signal valueChanged(12), 對象b會在slot setVaule()接收到, 並調用這個slot; 對象b同樣會發出signal valueChanged(), 但是沒有slot和對象b的valueChanged()連接, signal會被忽略;

Note setValue()只有在value != m_value的時候才會發出signal. 這樣可以防止signal-slot環形關聯導致無限循環的情況; (e.g. b.vauleChanged() 和 a.setVaule()互相關聯)

>默認情況下, 你創建一個信號關聯就應該有一個signal發出; 重復的關聯會有兩個或以上的signal發出; 你可以調用一個disconnect()打斷這些關聯; 使用Qt::UniqueConnect類型參數, 這樣只有在不重復的情況下, 信號關聯才會建立; 如果已經有了一個重復的關聯(同樣的object上的完全一樣的signal和完全一樣的slot), connect()會失敗並且返回false;

>這個例子說明對象間不需要知道對方的任何信息, 照樣可以互相通信; 為了實現通信機制我們只需要將對象互相關聯, 調用簡單的QObject::connect(), 或者使用uic的自動關聯特性(名字關聯 QMetaObject::connectSlotsByName(this));  


構建Example


>C++預處理會改變或者移除signal-slot和emit關鍵字, 這樣編譯器可以按照標准C++來處理代碼;


>對於包含signal-slot的類定義, 進行moc會產生一個C++源文件, 這個文件需要和其他的object文件一同編譯和鏈接; 如果你使用qmake, makefile規則中自動調用moc的部分會加到你工程中的makefile;

Signals

>Signals會在對象的內部狀態改變的時候被發出, 狀態的改變可能被對象的client或owner所注意; Signals是public的函數, 可以在任何地方被發出, 但是我們推薦只在定義signal的類或者子類中發signal;


>當一個signal被發出, 相關聯的slot一般會立即被執行, 就像普通函數的調用; 這個情況下, signal-slot機制完全獨立於任GUI的事件循環event loop; 在emit代碼段後面的代碼會在所有的slots都返回了以後被執行; 當使用queued connections的時候情況稍有不同, queued情況下在emit關鍵字後面的代碼還會繼續立即執行, slots則會在稍後執行;

>如果多個slots關聯到了一個signal上, 這些slots會一個接一個地被執行, 先後次序是按照它們被connected的次序來排列;

>Signals會被moc自動生成, 並且不能在cpp文件中實現. Signal不可以有返回值.(e.g. 使用void)

Note: 關於參數, 如果signal-slot不用特殊類型的參數, 他們可以更多地被重用; e.g. 如果QScrollBar::valueChanged()試圖使用一個特別類型, 假設是QScrollBar::Ranger, 那麼它只能被關聯到特別為QScrollBar設計的slots上了, 想要關聯到其他的input widget上基本不可能;

Slots

>Slot會在關聯的signal被發送的時候被調用; Slot是普通的C++函數, 可以被正常調用; 它們的特別之處只是可以和signal關聯起來;


>直接調用Slot時它就是普通的函數, 遵循一般的C++規則. 不過, 作為Slot, 忽略權限級別的話, 它可以被任何組件通過signal-slot關聯來激發. 這表示隨意一個類的實例發送了一個signal, 都可能激發其他不相關的類的實例的私有slot.

>Slot也可以定義成virtual的, 在實際使用中很有用;

>雖然在實際的應用程序中區別很小, 但是和callback相比, signal-slot會稍微慢一些, 因為它提供了更多的靈活性. 不考慮虛函數調用時, 普遍來說, 發出一個和一些slots相關聯的signal, 大約比直接調用接收函數receivers慢10倍. 主要的消耗是在: 安全地遍歷所有的connections, 鎖定關聯的對象(檢查後續的receivers沒有在發送的過程中被銷毀), 然後按照通用的方式安置參數; 如果有10個非虛函數被調用, 聽起來好像很多, 實際的損耗比任何new或delete操作要小很多. e.g. 當你在操作一個string, vector或list的時候, 如果需要new或delete, signal-slot的消耗只占了整個函數花費的效率很小的一個比例; 如果你調用系統函數或者間接調用10個函數, 情況是類似的; 簡單和靈活是signal-slot機制值得那一點小小消耗的理由;

Note 如果有其他庫定義的變量調用了signals和slots, 可能會在同時編譯Qt-based應用的時候導致編譯器的warnning和error. 解決的辦法是 #undef掉這些offending預編譯符號;

Meta-Object信息

>meta-object compiler(moc)會在一個C++文件中解析類的聲明並且生成C++代碼, 初始化meta-object. meta-object包含了所有signal和slot成員的名字, 還有指向這些函數的指針;

>meta-object還包含其他信息: 比如對象的類名. 你可以檢查對象是否繼承自某個特定的類; example:


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

if (widget->inherits("QAbstractButton")) {
    QAbstractButton *button = static_cast<QAbstractButton *>(widget);
    button->toggle();
}
>meta-object信息也可以被qobject_cast<T>()使用, 和QObject::inherits()類似, 但是更加安全less error-prone;


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

if (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget))
    button->toggle();
實際例子

>LcdNumber繼承自QObject, 通過QFrame和QWidget具備signal-slot機制; 某種程度上和內建的QLcdNumber類似;


>Q_OBJECT宏會被預編譯展開來去聲明多個被moc實現的成員函數; 如果你在編譯時遇到"undefined reference to vtable for LcdNumber"的錯誤, 你可能忘了先要運行moc命令, 把moc命令輸出的moc文件Include進來.


[cpp]
//略過moc不關心的一些析構函數和成員函數  
class LcdNumber : public QFrame 

    Q_OBJECT 
public: 
    LcdNumber(QWidget *parent = 0); 
signals: 
    void overflow(); 
public slots: 
    void display(int num); 
    void display(double num); 
    void display(const QString &str); 
    void setHexMode(); 
    void setDecMode(); 
    void setOctMode(); 
    void setBinMode(); 
    void setSmallDecimalPoint(bool point); 
}; 

//略過moc不關心的一些析構函數和成員函數
class LcdNumber : public QFrame
{
    Q_OBJECT
public:
    LcdNumber(QWidget *parent = 0);
signals:
    void overflow();
public slots:
    void display(int num);
    void display(double num);
    void display(const QString &str);
    void setHexMode();
    void setDecMode();
    void setOctMode();
    void setBinMode();
    void setSmallDecimalPoint(bool point);
};
>如果你繼承自QWidget, 基本上肯定需要在構造函數中加上parent參數, 把它傳遞給基類的;

>當LcdNumber被要求顯示一些非法的值時, 會發送overflow() signal;

>如果你不關心溢出, 或者知道不可能發生溢出, 你可以忽略overflow() signal; 可以不把它關聯到任何slot上;

>相反如果你想要在溢出時調用兩個不同的錯誤處理函數, 簡單地關聯到兩個不同的slots就行; Qt會按照關聯的次序調用兩個函數;

>Slot是被用來獲得其他widget狀態改變的信息的接收函數; 在示例代碼中, LcdNumber使用它去設置顯示的數字; 因為display()是類的一個接口, 所以這個slot設置為public;

>多個示例程序關聯QScrollBar的valueChanged() signal到了dispaly() slot, 因此LCD數字會不停得在scrollbar上顯示;

Note display()被重載overload了; 當你把一個signal和一個slot關聯起來, Qt會選擇適合的版本; 如果是使用callback, 你將不得不自己來找出5個不同的函數名字並且控制不同的類型;

具有默認參數的Signals和Slots


>signal和slot的原型可能包含了參數, 參數可能有默認值. 考慮QObject::destroyed():


[cpp]
void destroyed(QObject* = 0); 

void destroyed(QObject* = 0);
>當一個QObject被刪除, 它會發出QObject::destoryed() signal. 我們想要捕獲這個signal, 不論在哪我們可能有一個dangling reference指向被刪除的QObject, 這樣我們可以清除它; 一個合適的slot原型可能是:


[cpp]
void objectDestroyed(QObject* obj = 0); 

void objectDestroyed(QObject* obj = 0);
>有多種方式使用QObject::connect()來關聯signal-slot, 第一種是以函數指針:


[cpp]
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed); 

connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
>使用函數指針有很多優點. 首先, 允許編譯器檢查signal的參數是否和slot的參數兼容; 需要的話參數也能被編譯器隱式地轉換;

>你也可以使用C++11 lamdas表達式:

 

[cpp]
connect(sender, &QObject::destroyed, [=](){ this->m_objects.remove(sender); }); 

connect(sender, &QObject::destroyed, [=](){ this->m_objects.remove(sender); });
Note 如果你的編譯器不支持C++ 11可變參數模板 variadic templates, 這個語法只能在signal和slot具有小於或等於6個參數的情況下有效;


>還有一個方法是使用SIGNAL和SLOT宏. 關於是否在SIGNAL()和SLOT()宏中引入參數; 如果參數有默認值, 規則是傳到SIGNAL()中的函數原型的參數個數必須少於傳到SLOT()中的函數原型;

[cpp]
//以下這些都能工作  
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(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

[cpp]
//這個無法工作,因為slot預期的是接收一個參數QObject,這個signal不會發出參數, connection會報錯;  
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*))); 

//這個無法工作,因為slot預期的是接收一個參數QObject,這個signal不會發出參數, connection會報錯;
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));Note 當使用宏的QObject::connect()重載, 編譯器不會檢查signal-slot的參數;

更多Signal-Slot的使用

>如果你想得到發送signal的sender的信息, Qt提供了QObject::sender()函數, 返回一個指向發送signal的對象的指針;


>當遇到很多signals關聯到一個相同的slot的情況, 並且這個slot需要對每個signal作出不同的處理時, 可以用QSignalMapper類;

>假設你有三個push buttons, 用來決定打開哪種文件: Tax File, Accounts File, Report File.

>為了打開正確的文件, 使用QSignalMapper::setMapping()把所有的clicked() signals和QSignalMapper對象map起來; 然後把文件的QPushButton::clicked() signal和QSignalMapper::map() slot關聯起來;

[cpp]
signalMapper = new QSignalMapper(this); 
signalMapper->setMapping(taxFileButton, QString("taxfile.txt")); 
signalMapper->setMapping(accountFileButton, QString("accountsfile.txt")); 
signalMapper->setMapping(reportFileButton, QString("reportfile.txt")); 
connect(taxFileButton, &QPushButton::clicked, 
    signalMapper, &QSignalMapper::map); 
connect(accountFileButton, &QPushButton::clicked, 
    signalMapper, &QSignalMapper::map); 
connect(reportFileButton, &QPushButton::clicked, 
    signalMapper, &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, &QPushButton::clicked,
    signalMapper, &QSignalMapper::map);
connect(accountFileButton, &QPushButton::clicked,
    signalMapper, &QSignalMapper::map);
connect(reportFileButton, &QPushButton::clicked,
    signalMapper, &QSignalMapper::map);
>最後, 把mapped() signal和不同文件打開時調用的readFile() slot關聯起來, 不同的按鈕按下會打開不同的文件;[cpp] view plaincopyprint?connect(signalMapper, SIGNAL(mapped(QString)),this, SLOT(readFile(QString))); 

connect(signalMapper, SIGNAL(mapped(QString)),this, SLOT(readFile(QString)));>Qt可以使用第三方3rd Party signal-slot機制. 你甚至可以在一個項目裡同時使用兩種機制. 需要做的就是把下面一行代碼加到qmake項目文件(.pro)中.

Qt使用第三方Signals-Slots

[cpp]
CONFIG += no_keywords 

CONFIG += no_keywords>這行代碼告訴Qt不要去定義moc關鍵字signals, slots, emit, 因為這些名字會被第三方庫使用. e.g, Boost. 在定義了no_keywords標簽的情況下繼續使用Qt, 把源代碼中Qt關鍵字簡單地替換成相應的Qt宏: Q_SIGNALS(or Q_SIGNAL), Q_SLOTS(Q_SLOT)和Q_EMIT;


在Qt5中處理signals-slots的重載


>新的Qt5的語法為了解釋和關聯正確的重載函數會進行顯式地轉換. ClassA定義了兩個重載函數作為signal;

[cpp]
class ClassA : public QObject 

    Q_OBJECT 
...    
signals: 
    void mySignal(double d); 
    void mySignal(QString s); 
... 
}; 

class ClassA : public QObject
{
    Q_OBJECT
...  
signals:
    void mySignal(double d);
    void mySignal(QString s);
...
};>假設有個ClassB的實例b, 它的slot有一個QString參數, 那麼將ClassA的實例a上重載的第二個signal與其關聯的正確方式是:[cpp] view plaincopyprint?connect(&a, static_cast<void (ClassA::*)(QString)>(&ClassA::mySignal), &b, &ClassB::mySlot); 

connect(&a, static_cast<void (ClassA::*)(QString)>(&ClassA::mySignal), &b, &ClassB::mySlot);>如果有多個重載的slot, 用同樣的方式來connect;

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved