Q1、概述
信號槽是 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