? ? ? ? 也是好久沒有發帖子了,最近博主主要還是在邊學QT邊完成任務,所以進度很慢,但確實在這幾天對于QT自身槽和信號這類特殊的機制有了一定簡單的理解,所以還是想記錄下來,如果有初學者看到帖子對他有一定的幫助,那自然也會讓我感到十分榮幸
一. 什么是信號與槽
? ? ? ? 首先按照專業術語的描述是,QT的信號與槽機制是一種對象間通信機制。直觀的可以表現出一個對象發出信號,另一個對象通過槽函數響應這個信號,從而實現多模塊之間的解耦通信。這個也是屬于Qt框架最強大的特性之一,也是尤其適合GUI事件響應和模塊間協作。
? ? ? ? 通過舉一個簡單的例子,家門口有一個門鈴按鈕(就是等同于我們的信號),我們按了一下,屋里的電鈴聲就響了(槽函數)。
? ? ? ? 按門鈴 = 發出信號? 電鈴聲響了 = 槽函數被觸發 按下和響鈴之間 = connect()建立連接。
二. 信號/槽基本概念
? ? ? ? 信號(Signal)是QObject的子類在特定事件發生時自動發出的“通知”,就是一個事件的廣播。
?
? ? ? ? 而槽(Slot)可以與信號連接的普通成員函數,專門用于響應通知,也就是“接收廣播后做事”的函數
????????
? ? ? ? connect(),Qt提供的函數,用于建立信號與槽之間的綁定關系,“把信號線插進槽口”
? ? ? ? emit,是一個關鍵字,用于觸發自定義信號,也就是程序員手動“發出廣播”
? ? ? ? QObject,QT所有支持信號、槽的類的基類,信號槽必須用它派生出來的類
三. 廣播機制
? ? ? ? 在QT的信號與槽機制種,廣播機制指的是。信號一旦發出,系統會自動通知所有已連接的槽函數執行,無需顯示調用,且發送者不關心接收者的存在與行為。
? ? ? ? 這種機制體現了一種松耦合、發布-訂閱式的通信模型,是Qt框架中組件之間高效協作的核心基礎。
? ? ? ? 解耦(Decoupling),信號發送者和接收者之間并沒有直接依賴關系。發送者只負責發出信號,而不需要知道是否有接收者,更不需知道接收者是誰或要做什么。這樣可以使模塊之間保持獨立,便于維護或拓展。
? ? ? ? 一對多連接,一個信號可以同時連接多個槽函數。當信號被發出時,所有已經連接的槽將會被系統一次調用,按連接順序一次執行。這樣可以使一個事件可以觸發多個處理邏輯,從而實現模塊聯動
? ? ? ? 多對一連接:多個 不同的信號可以連接到同一個槽函數。當任意一個信號被發出時,該槽函數都會被執行。這使得不同來源的事件可以集中處理
? ? ? ? 自動調用:當信號發出時,所有與之連接的槽函數會被自動執行,對于我們開發者來說并不需要手動調用。Qt框架內部通過事件系統和元對象機制完成槽函數的自動觸發,我們只需要專注于邏輯實現
? ? ? ? 在Qt中信號是廣播式的,信號本身只是簡單的發出,并沒有指定特定的接收者。接收者(槽函數)會通過connect()來“訂閱”這個信號。
? ? ? ? 可以理解為信號式“廣播”出去的,它不會知道誰會接收到它,接收者通過connect()函數明確指定自己要接受哪個信號,所以如果沒有明確指定的接收者,那么這個函數就無法觸發任何行為,因為沒有對象響應找個信號
? ? ? ? 在代碼中可以這么理解,nullptr表示沒有指定接收者,那么即使按鈕被點擊,信號也不會觸發任何函數
// 如果沒有指定接收者,信號會發出,但沒有接收者來處理它
QObject::connect(&button, &QPushButton::clicked, nullptr, nullptr);
? ? ? ? 剛開始我將Qt的廣播形式和網絡廣播的形式弄混淆了,這里還是想做一個簡單的澄清,是兩個不同的概念
? ? ? ? 網絡廣播在網絡中,廣播通常指的是將信息發送到網絡上的所有設備,而不僅僅是發送給某一個指定的接收者。常見的網絡廣播是無差別地將信息發送給網絡上的所有設別,而接收這些廣播的設備通常是主動應答的,
? ? ? ? 例如UDP廣播,數據包被發送到一個特定的廣播地址(如255.255.255.255),網絡上的所有設備都有機會接收到這個廣播。然后,每個接收到廣播信息的設備可以選擇是否回應(例如一般就是發送一個響應報文)
????????
? ? ? ? 在Qt中,信號的“廣播”并不是指將消息發送給所有對象,而是一個信號可以連接到多個槽函數,因此多個接收者可以對同一個信號做出響應。信號本身并不主動去尋找接收者,而是通過connect()函數明確指定了哪些接收者應該處理這個信號
? ? ? ? 信號發出:信號并不是想網絡廣播那樣發送給所有設備,而是只從一個對象發出。可以想像是在說“我發生了某個事件,看下是否會有人響應我”
? ? ? ? “接收者”:只有通過connect()函數明確指定了接收者(即槽函數)后,信號才會被“接收”,并且接收者會執行相應的操作。這個過程并不需要接收者主動應答,而是由QT框架自動處理的
四. 信號與與槽的連接方式及適用場景
? ? ? ? 我們將他分為一下五種
????????系統信號(Qt自帶) + 系統槽(如QAPPlication::quit)這種組合方式可以快速調用系統功能。
? ? ? ? 信號部分是&QPushButton::clicked,這是Qt框架內置的標準信號,屬于系統控件QPushButton,當按鈕被點擊時自動發出。所有這是系統信號(也就是由Qt控件自帶)
? ? ? ? 槽部分 &QApplication::quit,這是Qt提供的一個系統級槽函數,作用是關閉應用程序主事件循環,相當于”退出應用“所以是系統槽(由Qt提供)
#include "mainwindow.h"#include <QApplication>
#include <QPushButton>int main(int argc, char *argv[])
{QApplication a(argc, argv);MainWindow w;QPushButton button("Quit",&w);button.move(100,100);QObject::connect(&button, &QPushButton::clicked,&a,&QApplication::quit);w.show();return a.exec();
}
? ? ? ? 系統信號 + 自定義槽函數 是目前較為常見的方式,如按鍵觸發邏輯。那么同樣的代碼邏輯如何在系統信號 + 自定義槽函數中體現。
? ? ? ? 在下面這段代碼中系統信號自然是,Qt自帶的信號,如QPushButton::clicked(),表示按鈕被點擊。
? ? ? ? 而自定義的槽函數,是我們寫在類中的處理函數,在這里是MainWindow::onButtonClicked()
? ? ? ? 所以簡單來理解“系統信號 + 自定義槽函數”就是——,按鈕這種系統組件發出信號,用戶自己寫的函數去接收并響應
? ? ? ??
? ? ? ? QpushButton內部有個信號:void clicked(bool checked = false);當我們用connect()把這個信號和函數onButtonClicked()連接在一起,當我們點擊按鈕的時候,按鈕會發出clickced信號(也就是前面提到的廣播機制)。Qt自動調用我們自定義的槽函數onButtonClicked來處理事件
//在MainWindow.cpp中MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);QPushButton *button =new QPushButton("Quit",this);button->move(100,100);connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);}MainWindow::~MainWindow()
{delete ui;
}void MainWindow::onButtonClicked()
{qDebug() << "按鈕被點擊";
}//在MainWindow.h中#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private:Ui::MainWindow *ui;private slots:void onButtonClicked();
};
#endif // MAINWINDOW_H
? ? ? ? 自定義信號 +?系統槽 一般適用于自定義行為影響系統行為
? ? ? ? 我們通過程序執行的順序來理解代碼,首先第一步顯示從連接按鈕點擊信號開始,對應mainwindows.cpp,connect(),當button被點擊的時候,調用MainWindow的onButtonclicked()方法。button是發出信號的對象,&QPushButton::Clicked()是系統信號(點擊按鈕后發出)。
? ? ? ? 但是這里就要有疑惑了,明明是使用我們的自定義信號,為什么還是是系統信號呢?先不著急我們先分析完,之后再進行解釋
? ? ? ? this是當前窗口對象(MainWindow),&Mainwindow::onButtonClicked是我們自定義的槽函數,響應按鈕點擊
? ? ? ? 在按鈕點擊過后,在void MainWindow這個我們自己編寫的槽函數中,在這個函數中我們有發出了另外一個信號quitApp(),這是我們在頭文件中signals:定義的自定義信號
? ? ? ? 也就是說,剛開始按鈕被點擊后發出系統信號,觸發了我們自己的函數onButtonClicked(自定義槽),然后onButtonClicked中又調用了emit quitApp(發出我們的自定義信號)
? ? ? ? 總結一下我們代碼中完成的工作是,我們點擊按鈕觸發系統信號,然后Qt自動調用我們定義的槽函數,我們的槽函數里發出quitApp(),里面是我們自定義的信號
//在mainwindow.h#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private:Ui::MainWindow *ui;private slots:void onButtonClicked();//自定義信號
signals:void quitApp();};
#endif // MAINWINDOW_H//在mainwindow.cpp中#include "mainwindow.h"
#include "ui_mainwindow.h"#include "QPushButton"
#include "QDebug"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);QPushButton *button =new QPushButton("Quit",this);button->move(100,100);connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);connect(this,&MainWindow::quitApp,qApp,&QApplication::quit);
}MainWindow::~MainWindow()
{delete ui;
}void MainWindow::onButtonClicked()
{emit quitApp();
}
? ? ? ? 現在我們來解釋一下,為什么整個程序執行的流程始終是系統信號。那為什么又還要發出自定義信號,博主后面了解到,Qt設計的很巧妙的地方就在于,信號鏈的“解耦”和“轉發”能力。
? ? ? ? 例如我們需要在點擊按鈕的時候退出程序,還想要加入記錄日志、播放音效、保存設置等,如果全部寫在onButtonclicked()里,那么這個槽函數就會越來越大,越來月耦合。
? ? ? ? 那么我們在遇到這樣的情況下就可以采取,在onButtonClicked()里發出一個自定義信號quitApp(),然后讓其他模塊自由決定是否接收這個信號,或者怎么處理信號
? ? ? ? 例如如下情況,把“按鈕點擊的事件”,拆成了“點擊后發出quitApp”信號,然后讓其他的函數也來訂閱這個信號
connect(this, &MainWindow::quitApp, qApp, &QApplication::quit); // 退出程序
connect(this, &MainWindow::quitApp, logger, &Logger::writeQuitLog); // 寫日志
connect(this, &MainWindow::quitApp, soundPlayer, &Player::playExitSound); // 播放音效
? ? ? ? 我們再舉一個實際的例子,如果我們把多個行為都寫再onButtonClicked中
void MainWindow::onButtonClicked() {qApp->quit(); // 退出程序settingsManager->save(); // 保存設置logger->log("用戶點擊退出"); // 寫日志soundPlayer->play("bye.wav"); // 播放音效
}
? ? ? ? 我們將所有的行為都寫在了一個函數中,主窗口知道太多別的類(Logger、Player、SettingManager),如果將來我們選喲將其中的功能進行改變,要修改的地方就有很多,而且假如其他的地方也需要退出行為,還要復制代碼到其他地方
// MainWindow.h
signals:void quitApp(); // ?? 只發信號,不處理具體細節
void MainWindow::onButtonClicked() {emit quitApp(); // ?? 你們誰愛處理誰處理,我不管
}
? ? ? ? 在程序初始化的時候就可以通過信號連接不同的行為,所以總結就是,如果在一個槽函數中做了太多和“自己職責無關的事”,就可以考慮廣播事件,讓其他人來聽
connect(mainWindow, &MainWindow::quitApp, qApp, &QApplication::quit);
connect(mainWindow, &MainWindow::quitApp, logger, &Logger::logQuit);
connect(mainWindow, &MainWindow::quitApp, settings, &Settings::save);
connect(mainWindow, &MainWindow::quitApp, soundPlayer, &Player::playExitSound);
? ? ? ? 自定義信號 自定義槽函數 一般常用于模塊解耦、自主通信,如果針對于自定義槽函數和信號,肯定會有更高的自由度,我們還是結合代碼來分析
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private:Ui::MainWindow *ui;private slots:void onButtonClicked(); //自定義槽(用于發送信號)void handClose(); //自定義槽(響應信號關閉窗口)//自定義信號
signals:void closeApp(); //自定義信號};
#endif // MAINWINDOW_H//在mainwindow.cpp中#include "mainwindow.h"
#include "ui_mainwindow.h"#include "QPushButton"
#include "QDebug"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);QPushButton *button =new QPushButton("Quit",this);button->move(100,100);connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);connect(this,&MainWindow::closeApp,this,&MainWindow::handClose);
}MainWindow::~MainWindow
()
{delete ui;
}void MainWindow::onButtonClicked()
{emit closeApp();
}void MainWindow::handClose(){qDebug() << "收到信號,關閉窗口";this->close();}
? ? ? ? 首先還是在頭文件總聲明一個自定義信號,以及private slots中全是自定義的槽函數,也是在頭文件中聲明。
? ? ? ? 然后在mainwindow中的構造函數中進行連接
,connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
//這行是系統信號(QPushButton::clicked)
//->自定義槽(onButtonClicked)作用是:當按鈕被點擊,就會自動調用onButtonClicked()這個槽函數
? ? ? ??connect(this, &MainWindow::closeApp, this, &MainWindow::handleClose);這行是自定義信號(closeApp)->自定義槽(handleClose)作用就是當你發出closeApp信號時,Qt就會自動去調用handleClose()這個函數。
? ? ? ? 然后在槽函數中發出信號,這個函數并不會直接關閉窗口。它只是發出一個“廣播”,說明我準備關閉了
void MainWindow::onButtonClicked()
{emit closeApp();
}
? ? ? ? 最后在響應的槽中執行操作
void MainWindow::handleClose()
{this->close(); // 真正關閉主窗口
}
? ? ? ? 以及最后任意信號 + Lambda表達式 通常簡潔高效,適合小型邏輯處理。這個機制是Qt信號槽機制中最靈活也是最現代的的用法之一,非常適合簡介邏輯、快速響應、小功能點,無需定義額外的槽函數,直接在connect()中用Lambda表達式表達處理邏輯
? ? ? ? Lambda是一種匿名函數(臨時的小函數),語法如下,一般用于只想快速響應一下,并不想為了這個操作特地去寫某個函數
[]() {// 執行的代碼
};
? ? ? ? 例如。如果只是臨時邏輯、簡單調試、小按鈕響應等首選Lambda,但是如果邏輯復雜、選喲復用、設計多個模塊之間的通信還是盡量選擇傳統的槽函數。但是如果在大型的項目中涉及到多個對象協作、跨模塊、需要事件驅動使用自定義信號 + 自定義槽
QPushButton *button = new QPushButton("點我", this);
button->move(100, 100);// 用 lambda 直接響應點擊
connect(button, &QPushButton::clicked, [](){qDebug() << "👋 Lambda:按鈕被點擊啦!";
});
? ? ? ? 博主去查了一下Lambda的具體原理,它是C++ 11引入的一種匿名函數(inline function)是臨時寫在某個地方的小函數,不需要在別的地方聲明或命名,基本語法如下
[capture](parameter_list) -> return_type {// function body
}
? ? ? ? 但大部分時候還是選擇寫成簡略的寫法
[]() {qDebug() << "Hello Lambda!";
};
????????