1、信號和槽 2、自定義信號槽 3、自定義信號槽需要註意的事項 4、信號槽的更多用法 5、案例代碼........ ...
C++_之Qt的信號和槽的詳解
1、概述
信號槽是 Qt 框架引以為豪的機制之一。所謂信號槽,實際就是觀察者模式。當某個事件發生之後,比如,按鈕檢測到自己被點擊了一下,它就會發出一個信號(signal)。這種發出是沒有目的的,類似廣播。如果有對象對這個信號感興趣,它就會使用連接(connect)函數,意思是,將想要處理的信號和自己的一個函數(稱為槽(slot))綁定來處理這個信號。也就是說,當信號發出時,被連接的槽函數會自動被回調。這就類似觀察者模式:當發生了感興趣的事件,某一個操作就會被自動觸發。(這裡提一句,Qt 的信號槽使用了額外的處理來實現,並不是 GoF 經典的觀察者模式的實現方式。)
信號和槽是Qt特有的信息傳輸機制,是Qt設計程式的重要基礎,它可以讓互不幹擾的對象建立一種聯繫。
槽的本質是類的成員函數,其參數可以是任意類型的。和普通C++成員函數幾乎沒有區別,它可以是虛函數;也可以被重載;可以是公有的、保護的、私有的、也可以被其他C++成員函數調用。唯一區別的是:槽可以與信號連接在一起,每當和槽連接的信號被髮射的時候,就會調用這個槽。
1.1對象樹(子對象動態分配空間不需要釋放)
參考連接:https://blog.csdn.net/fzu_dianzi/article/details/6949081
Qt提供了一種機制,能夠自動、有效的組織和管理繼承自QObject的Qt對象,這種機制就是對象樹。
Qt對象樹在用戶界面編程上是非常有用的。它能夠幫助程式員減輕記憶體泄露的壓力。
比如說當應用程式創建了一個具有父視窗部件的對象時,該對象將被加入父視窗部件的孩子列表。當應用程式銷毀父視窗部件時,其下的孩子列表中的對象將被一一刪除。這讓我們在編程時,能夠將主要精力放在系統的業務上,提高編程效率,同時也保證了系統的穩健性。
下麵筆者將簡單分析對象樹。
代碼驗證:
int main(int argc, char *argv[]) { QApplication app(argc, argv); QDialog *dlg = new QDialog(0); QPushButton *btn = new QPushButton(dlg); qDebug() << "dlg = " << dlg; qDebug() << "btn = " << btn; dlg->exec(); delete btn; qDebug() << "dlg = " << dlg; return 0; } dlg = QDialog(0x3ea1a0) btn = QPushButton(0x3ea228) /*關閉視窗後,dlg = QDialog(0x3ea1a0) 這說明關閉視窗,不會銷毀該視窗部件,而是將其隱藏起來。 我們在qDebug() << "dlg = " << dlg; 之後加上 qDebug() << "btn = " << btn; 明顯的,我們之前已經delete btn,btn指針沒有被賦值為0,這是編譯器決定的。 執行程式後,必然出現段錯誤。 2、 將程式稍微修改下。*/ int main(int argc, char *argv[]) { QApplication app(argc, argv); QDialog *dlg = new QDialog(0); QPushButton *btn = new QPushButton(dlg); qDebug() << "dlg = " << dlg; qDebug() << "btn = " << btn; dlg->exec(); delete dlg; qDebug() << "btn = " << btn; return 0; }
2、信號和槽
為了體驗一下信號槽的使用,我們以一段簡單的代碼說明:
Qt5 的書寫方式:(推薦的使用)★★★★★
#include <QApplication> #include <QPushButton> int main(int argc, char *argv[]) { QApplication app(argc, argv); QPushButton button("Quit"); QObject::connect(&button, &QPushButton::clicked, &app, &QApplication::quit); button.show(); return app.exec(); }
我們按照前面文章中介紹的在 Qt Creator 中創建工程的方法創建好工程,然後將main()函數修改為上面的代碼。點擊運行,我們會看到一個按鈕,上面有“Quit”字樣。點擊按鈕,程式退出。
connect()函數最常用的一般形式:
connect(sender, signal, receiver, slot);
參數:
sender:發出信號的對象
signal:發送對象發出的信號
receiver:接收信號的對象
slot:接收對象在接收到信號之後所需要調用的函數
信號槽要求信號和槽的參數一致,所謂一致,是參數類型一致。如果不一致,允許的情況是,槽函數的參數可以比信號的少,即便如此,槽函數存在的那些參數的順序也必須和信號的前面幾個一致起來。這是因為,你可以在槽函數中選擇忽略信號傳來的數據(也就是槽函數的參數比信號的少),但是不能說信號根本沒有這個數據,你就要在槽函數中使用(就是槽函數的參數比信號的多,這是不允許的)。
如果信號槽不符合,或者根本找不到這個信號或者槽函數,比如我們改成:
connect(&button, &QPushButton::clicked, &QApplication::quit2);
由於 QApplication 沒有 quit2 這樣的函數,因此在編譯時會有編譯錯誤:
'quit2' is not a member of QApplication
這樣,使用成員函數指針我們就不會擔心在編寫信號槽的時候出現函數錯誤。
Qt4 的書寫方式:
int main(int argc, char *argv[]) { QApplication a(argc, argv); QPushButton *button = new QPushButton("Quit"); connect(button, SIGNAL(clicked()), &a, SLOT(quit())); button->show(); return a.exec(); }
這裡使用了SIGNAL和SLOT這兩個巨集,將兩個函數名轉換成了字元串。註意到connect()函數的 signal 和 slot 都是接受字元串,一旦出現連接不成功的情況,Qt4是沒有編譯錯誤的(因為一切都是字元串,編譯期是不檢查字元串是否匹配),而是在運行時給出錯誤。這無疑會增加程式的不穩定性。
Qt5在語法上完全相容Qt4
小總結:
1>. 格式: connect(信號發出者對象(指針), &className::clicked, 信號接收者對象(指針), &classB::slot);
2>. 標準信號槽的使用:
connect(sender, &Send::signal, receiver, &Receiver::slot)
3、自定義信號槽
使用connect()可以讓我們連接系統提供的信號和槽。但是,Qt 的信號槽機制並不僅僅是使用系統提供的那部分,還會允許我們自己設計自己的信號和槽。
下麵我們看看使用 Qt 的信號槽,實現一個報紙和訂閱者的例子:
有一個報紙類Newspaper,有一個訂閱者類Subscriber。Subscriber可以訂閱Newspaper。這樣,當Newspaper有了新的內容的時候,Subscriber可以立即得到通知。
#include <QObject> ////////// newspaper.h ////////// class Newspaper : public QObject { Q_OBJECT public: Newspaper(const QString & name) : m_name(name) { } void send() { emit newPaper(m_name); } signals: void newPaper(const QString &name); private: QString m_name; }; ////////// reader.h ////////// #include <QObject> #include <QDebug> class Reader : public QObject { Q_OBJECT public: Reader() {} void receiveNewspaper(const QString & name) { qDebug() << "Receives Newspaper: " << name; } }; ////////// main.cpp ////////// #include <QCoreApplication> #include "newspaper.h" #include "reader.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); Newspaper newspaper("Newspaper A"); Reader reader; QObject::connect(&newspaper, &Newspaper::newPaper, &reader, &Reader::receiveNewspaper); newspaper.send(); return app.exec(); }
●首先看Newspaper這個類。這個類繼承了QObject類。只有繼承了QObject類的類,才具有信號槽的能力。所以,為了使用信號槽,必須繼承QObject。凡是QObject類(不管是直接子類還是間接子類),都應該在第一行代碼寫上Q_OBJECT。不管是不是使用信號槽,都應該添加這個巨集。這個巨集的展開將為我們的類提供信號槽機制、國際化機制以及 Qt 提供的不基於 C++ RTTI 的反射能力。
● Newspaper類的 public 和 private 代碼塊都比較簡單,只不過它新加了一個 signals。signals 塊所列出的,就是該類的信號。信號就是一個個的函數名,返回值是 void(因為無法獲得信號的返回值,所以也就無需返回任何值),參數是該類需要讓外界知道的數據。信號作為函數名,不需要在 cpp 函數中添加任何實現。
●Newspaper類的send()函數比較簡單,只有一個語句emit newPaper(m_name);。emit 是 Qt 對 C++ 的擴展,是一個關鍵字(其實也是一個巨集)。emit 的含義是發出,也就是發出newPaper()信號。感興趣的接收者會關註這個信號,可能還需要知道是哪份報紙發出的信號?所以,我們將實際的報紙名字m_name當做參數傳給這個信號。當接收者連接這個信號時,就可以通過槽函數獲得實際值。這樣就完成了數據從發出者到接收者的一個轉移。
● Reader類更簡單。因為這個類需要接受信號,所以我們將其繼承了QObject,並且添加了Q_OBJECT巨集。後面則是預設構造函數和一個普通的成員函數。Qt 5 中,任何成員函數、static 函數、全局函數和 Lambda 表達式都可以作為槽函數。與信號函數不同,槽函數必須自己完成實現代碼。槽函數就是普通的成員函數,因此作為成員函數,也會受到 public、private 等訪問控制符的影響。(如果信號是 private 的,這個信號就不能在類的外面連接,也就沒有任何意義。)
3.1自定義信號槽需要註意的事項
●發送者和接收者都需要是QObject的子類(當然,槽函數是全局函數、Lambda 表達式等無需接收者的時候除外);
●使用 signals 標記信號函數,信號是一個函數聲明,返回 void,不需要實現函數代碼;
●槽函數是普通的成員函數,作為成員函數,會受到 public、private、protected 的影響;
●使用 emit 在恰當的位置發送信號;
●使用QObject::connect()函數連接信號和槽。
●任何成員函數、static 函數、全局函數和 Lambda 表達式都可以作為槽函數
3.2信號槽的更多用法
● 一個信號可以和多個槽相連
如果是這種情況,這些槽會一個接一個的被調用,但是它們的調用順序是不確定的。
●多個信號可以連接到一個槽
只要任意一個信號發出,這個槽就會被調用。
●一個信號可以連接到另外的一個信號
當第一個信號發出時,第二個信號被髮出。除此之外,這種信號-信號的形式和信號-槽的形式沒有什麼區別。
●槽可以被取消鏈接
這種情況並不經常出現,因為當一個對象delete之後,Qt自動取消所有連接到這個對象上面的槽。
●使用Lambda 表達式
在使用 Qt 5 的時候,能夠支持 Qt 5 的編譯器都是支持 Lambda 表達式的。
我們的代碼可以寫成下麵這樣:
QObject::connect(&newspaper, static_cast<void (Newspaper:: *) (const QString &)>(&Newspaper::newPaper), [=](const QString &name) { /* Your code here. */ } );
在連接信號和槽的時候,槽函數可以使用Lambda表達式的方式進行處理。
4、Lambda表達式
C++11中的Lambda表達式用於定義並創建匿名的函數對象,以簡化編程工作。首先看一下Lambda表達式的基本構成:
[函數對象參數](操作符重載函數參數)mutable或exception ->返回值{函數體}
①函數對象參數;
[],標識一個Lambda的開始,這部分必須存在,不能省略。函數對象參數是傳遞給編譯器自動生成的函數對象類的構造函數的。函數對象參數只能使用那些到定義Lambda為止時Lambda所在作用範圍內可見的局部變數(包括Lambda所在類的this)。函數對象參數有以下形式:
▲空。沒有使用任何函數對象參數。
▲=。函數體內可以使用Lambda所在作用範圍內所有可見的局部變數(包括Lambda所在類的this),並且是值傳遞方式(相當於編譯器自動為我們按值傳遞了所有局部變數)。
▲&。函數體內可以使用Lambda所在作用範圍內所有可見的局部變數(包括Lambda所在類的this),並且是引用傳遞方式(相當於編譯器自動為我們按引用傳遞了所有局部變數)。
▲ this。函數體內可以使用Lambda所在類中的成員變數。
▲ a。將a按值進行傳遞。按值進行傳遞時,函數體內不能修改傳遞進來的a的拷貝,因為預設情況下函數是const的。要修改傳遞進來的a的拷貝,可以添加mutable修飾符。
▲ &a。將a按引用進行傳遞。
▲ a, &b。將a按值進行傳遞,b按引用進行傳遞。
▲ =,&a, &b。除a和b按引用進行傳遞外,其他參數都按值進行傳遞。
▲ &, a, b。除a和b按值進行傳遞外,其他參數都按引用進行傳遞。
int m = 0, n = 0; [=] (int a) mutable { m = ++n + a; }(4); [&] (int a) { m = ++n + a; }(4); [=,&m] (int a) mutable { m = ++n + a; }(4); [&,m] (int a) mutable { m = ++n + a; }(4); [m,n] (int a) mutable { m = ++n + a; }(4); [&m,&n] (int a) { m = ++n + a; }(4);
② 操作符重載函數參數;
標識重載的()操作符的參數,沒有參數時,這部分可以省略。參數可以通過按值(如:(a,b))和按引用(如:(&a,&b))兩種方式進行傳遞。
③ 可修改標示符;
mutable聲明,這部分可以省略。按值傳遞函數對象參數時,加上mutable修飾符後,可以修改按值傳遞進來的拷貝(註意是能修改拷貝,而不是值本身)。
④ 錯誤拋出標示符;
exception聲明,這部分也可以省略。exception聲明用於指定函數拋出的異常,如拋出整數類型的異常,可以使用throw(int)
⑤ 函數返回值;
->返回值類型,標識函數返回值的類型,當返回值為void,或者函數體中只有一處return的地方(此時編譯器可以自動推斷出返回值類型)時,這部分可以省略。
⑥ 是函數體;
{},標識函數的實現,這部分不能省略,但函數體可以為空。
總結:
案例代碼:
mainwidget.h
#ifndef MAINWIDGET_H #define MAINWIDGET_H #include <QWidget> #include <QPushButton> #include "subwidget.h" //子視窗頭文件 class MainWidget : public QWidget { Q_OBJECT public: MainWidget(QWidget *parent = 0); ~MainWidget(); public slots: void mySlot(); void changeWin(); void dealSub(); void dealSlot(int, QString); private: QPushButton b1; QPushButton *b2; QPushButton b3; SubWidget subWin; }; #endif // MAINWIDGET_H
subwidget.h
#ifndef SUBWIDGET_H #define SUBWIDGET_H #include <QWidget> #include <QPushButton> class SubWidget : public QWidget { Q_OBJECT public: explicit SubWidget(QWidget *parent = 0); void sendSlot(); signals: /* 信號必須有signals關鍵字來聲明 * 信號沒有返回值,但可以有參數 * 信號就是函數的聲明,只需聲明,無需定義 * 使用:emit mySignal(); * 信號可以重載 */ void mySignal(); void mySignal(int, QString); public slots: private: QPushButton b; }; #endif // SUBWIDGET_H
main.cpp
#include "mainwidget.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWidget w;//執行MainWidget的構造函數 w.show(); return a.exec(); }
mainwidget.cpp
#include "mainwidget.h" #include <QPushButton> #include <QDebug> //列印 MainWidget::MainWidget(QWidget *parent) : QWidget(parent) { b1.setParent(this); b1.setText("close"); b1.move(100, 100); b2 = new QPushButton(this); b2->setText("abc"); connect(&b1, &QPushButton::pressed, this, &MainWidget::close); /* &b1: 信號發出者,指針類型 * &QPushButton::pressed:處理的信號, &發送者的類名::信號名字 * this: 信號接收者 * &MainWidget::close: 槽函數,信號處理函數 &接收的類名::槽函數名字 * 發送-處理-接收-處理 */ /* 自定義槽,普通函數的用法 * Qt5:任意的成員函數,普通全局函數,靜態函數 * 槽函數需要和信號一致(參數,返回值) * 由於信號都是沒有返回值,所以,槽函數一定沒有返回值 */ connect(b2, &QPushButton::released, this, &MainWidget::mySlot); connect(b2, &QPushButton::released, &b1, &QPushButton::hide); /* 信號:簡訊 * 槽函數:接收簡訊的手機 */ setWindowTitle("老大"); //this->setWindowTitle("老大");//等價同上 b3.setParent(this); b3.setText("切換到子視窗"); b3.move(50, 50); //顯示子視窗 //subWin.show(); connect(&b3, &QPushButton::released, this, &MainWidget::changeWin); //處理子視窗的信號 // void (SubWidget::*funSignal)() = &SubWidget::mySignal; // connect(&subWin, funSignal, this, &MainWidget::dealSub); // void (SubWidget::*testSignal)(int, QString) = &SubWidget::mySignal; // connect(&subWin, testSignal, this, &MainWidget::dealSlot); //Qt4信號連接 //Qt4槽函數必須有slots關鍵字來修飾 connect(&subWin, SIGNAL(mySignal()), this, SLOT(dealSub()) ); connect(&subWin, SIGNAL(mySignal(int,QString)), this, SLOT(dealSlot(int,QString)) ); //缺點: SIGNAL SLOT 將函數名字 -> 字元串 不進行錯誤檢查 //Lambda表達式, 匿名函數對象 //C++11增加的新特性, 項目文件: CONFIG += C++11 //Qt配合信號一起使用,非常方便 QPushButton *b4 = new QPushButton(this); b4->setText("Lambda表達式"); b4->move(150, 150); int a = 10, b = 100; connect(b4, &QPushButton::clicked, // = :把外部所有局部變數、類中所有成員以值傳遞方式 // this: 類中所有成員以值傳遞方式 // & : 把外部所有局部變數, 引用符號 [=](bool isCheck) { qDebug() << isCheck; } ); resize(400, 300); } void MainWidget::dealSlot(int a, QString str) { // str.toUtf8() -> 位元組數組QByteArray // ……data() -> QByteArray -> char * qDebug() << a << str.toUtf8().data(); } void MainWidget::mySlot() { b2->setText("123"); } void MainWidget::changeWin() { //子視窗顯示 subWin.show(); //本視窗隱藏 this->hide(); } void MainWidget::dealSub() { //子視窗隱藏 subWin.hide(); //本視窗顯示 show(); } MainWidget::~MainWidget() { }
subwidget.cpp
#include "subwidget.h" SubWidget::SubWidget(QWidget *parent) : QWidget(parent) { this->setWindowTitle("小弟"); b.setParent(this); b.setText("切換到主視窗"); connect(&b, &QPushButton::clicked, this, &SubWidget::sendSlot); resize(400, 300); } void SubWidget::sendSlot() { emit mySignal(); emit mySignal(250, "我是子視窗"); }
SingnalAndSlot.pro
QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = 03_SignalAndSlot TEMPLATE = app SOURCES += main.cpp\ mainwidget.cpp \ subwidget.cpp HEADERS += mainwidget.h \ subwidget.h CONFIG += C++11
以上資料來源於互聯網和自己的見解。如有雷同,不勝榮幸。