Trong bài này chúng ta sẽ tìm hiểu về cơ chế hoạt động của Signal và Slot
Trong lập trình GUI thì có một thứ rất quan trọng đó là sự kiện (event), khi một sự kiện nào đó xảy ra thì sẽ có các đối tượng xử lý sự kiện đó. Chẳng hạn như khi click vào nút X trên góc cửa sổ thì thoát chương trình. Qt xử lý sự kiện bằng cách tạo ra Signal và Slot. Trong một số bài trước chúng ta đã tìm hiểu sơ qua về cơ chế này, trong bài này chúng ta sẽ tìm hiểu kỹ hơn.
Signal
Signal tiếng Việt có nghĩa là tín hiệu. Trong Qt, khi một sự kiện nào đó xảy ra, một signal sẽ được phát đi giống như đài truyền hình phát sóng vậy, thực ra nó chỉ là một phương thức của một lớp nhưng không có phần thân hàm {}
. Các lớp Widget có sẵn trong Qt có rất nhiều signal được định nghĩa sẵn, và chúng ta cũng có thể viết các signal riêng cho các lớp của chúng ta. Signal không có kiểu trả về, kiểu trả về của signal luôn luôn là void
.
Slot
Slot chẳng qua cũng là một phương thức bình thường của một lớp, các phương thức này sẽ được gọi khi có một signal nào đó được phát đi. Cũng giống như signal, các lớp Widget trong Qt cũng có sẵn rất nhiều slot và chúng ta cũng có thể viết slot cho lớp của riêng chúng ta.
Connect
Signal và slot được kết nối qua từng đối tượng (chứ không phải qua từng lớp như nhiều bạn vẫn nghĩ). Tức là chúng ta chỉ có kết nối đối tượng này với đối tượng kia chứ không kết nối lớp này với lớp kia, giả sử chúng ta có đối tượng object1, object2
thuộc lớp A và object3
thuộc lớp B thì chúng ta chỉ có thể kết nối object1->object2, object1->object3
hoặc object3->object2
chứ không kết nối lớp A đến lớp B.
Khi kết nối như vậy thì một đối tượng sẽ làm vai trò phát signal, một đối tượng sẽ nhận signal. Đối tượng phát signal có thể phát các signal và cứ mỗi lần phát như vậy thì đối tượng nhận signal tương ứng sẽ thực thi slot của đối tượng đó.
Một signal có thể kết nối đến nhiều slot và một slot có thể kết nối đến nhiều signal.
Slot sẽ được gọi khi có signal tương ứng được phát ra, nhưng vì slot cũng là một phương thức bình thường như bao phương thức khác nên chúng ta cũng có thể gọi chúng như gọi phương thức bình thường vậy.
Tham số của signal phải ít hơn hoặc bằng tham số của slot. Khi một signal được phát đi, nó sẽ mang theo dữ liệu là các tham số của nó, và slot nhận signal này sẽ nhận các tham số đó thông qua tham số của nó. Thứ tự các tham số của signal và slot phải giống nhau, chẳng hạn như signal gửi 1 int, sau đó là 1 string thì slot cũng phải nhận 1 int rồi mới tới string.
Một signal cũng có thể kết nối đến một signal khác, tức là như thế sẽ phát ra 2 signal.
(ảnh : Qt)
Ví dụ
Chúng ta sẽ viết 2 lớp, một lớp phát signal và một lớp có slot nhận signal.
Emitter
#include <QObject> #include <QString> class Emitter : public QObject { Q_OBJECT public: Emitter(); void emitSignal1(); void emitSignal2(int); void emitSignal3(QString, int); void setName(QString); signals: void signal1(); void signal2(int); void signal3(QString, int); };
Lớp Emitter
là lớp phát signal, trong đó có 3 signal khác nhau.
class Emitter : public QObject { Q_OBJECT ... }
Để một lớp có thể sử dụng signal và slot thì lớp đó phải kế thừa từ lớp QObject
và ở đầu lớp bạn phải khai báo macro Q_OBJECT
.
signals: void signal1(); void signal2(int); void signal3(QString, int);
Chúng ta dùng từ khóa signals
để báo cho Qt biết phương thức nào là một signal. Các signal sẽ có kiểu trả về là void, và cũng có các tham số đi kèm như một phương thức bình thường.
#include "emitter.h" Emitter::Emitter() {} void Emitter::emitSignal1() { emit signal1(); } void Emitter::emitSignal2(int arg) { emit signal2(arg); } void Emitter::emitSignal3(QString arg1, int arg2) { emit signal3(arg1, arg2); } void Emitter::setName(QString arg) { this->setObjectName(arg); }
Các signal sẽ được gọi trong các phương thức khác nhau.
void Emitter::emitSignal1() { emit signal1(); }
Để phát các signal thì bạn dùng từ khóa emit
.
void Emitter::setName(QString arg) { this->setObjectName(arg); }
Lớp QObject có sẵn một thuộc tính là objectName
dùng để đặt tên cho đối tượng, thuộc tính này có giá trị rỗng, chúng ta có thể đặt giá trị cho thuộc tính này thông qua phương thức setObjectName()
.
Receiver
#include <QString> #include <QObject> #include <QDebug> #include <iostream> #include "emitter.h" class Receiver : public QObject { Q_OBJECT public: Receiver(); public slots: void receiveSignal1(); void receiveSignal2(int); void receiveSignal3(QString, int); };
Chúng ta viết lớp Receiver có 3 slot xử lý từng signal khác nhau.
public slots: void receiveSignal1(); void receiveSignal2(int); void receiveSignal3(QString, int);
Các slot cũng chỉ là các phương thức bình thường. Khi khai báo bạn phải thêm từ khóa slots
ở phía trước.
#include "receiver.h" Receiver::Receiver() { } void Receiver::receiveSignal1() { std::cout << "Signal 1 received!\n"; } void Receiver::receiveSignal2(int arg) { std::cout << "Signal 2 came with an integer: " << arg << "\n"; } void Receiver::receiveSignal3(QString arg1, int arg2) { std::cout << "Signal 3 from " << QObject::sender()->objectName().toStdString() << ": " << arg1.toStdString() << " " << QString::number(arg2).toStdString() << "\n"; }
Tham số đi kèm với signal cũng sẽ là tham số được truyền vào lời gọi các phương thức slot tương ứng.
QObject::sender()->objectName().toStdString()
Ngoài ra bạn có thể lấy con trỏ tham chiếu đến đối tượng đã gửi signal thông qua phương thức QObject::sender()
. Phương thức objectName()
lấy thuộc tính objectName.
arg1.toStdString()
Vì std::cout
là đối tượng xuất trong thư viện C++ chuẩn, không liên quan gì đến Qt cả nên chúng ta phải chuyển đối kiểu dữ liệu từ QString
của Qt sang std::string
của C++ bằng phương thức toStdString()
.
Main
#include <QCoreApplication> #include <QObject> #include "emitter.h" #include "receiver.h" int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); Emitter e1, e2; Receiver r1, r2; e1.setName("Emitter 1"); e2.setName("Emitter 2"); QObject::connect(&e1, SIGNAL(signal1()), &r1, SLOT(receiveSignal1())); e1.emitSignal1(); QObject::connect(&e2, Emitter::signal2, &r2, Receiver::receiveSignal2); e2.emitSignal2(3); QObject::connect(&e1, Emitter::signal3, &r1, Receiver::receiveSignal3); QObject::connect(&e2, Emitter::signal3, &r1, Receiver::receiveSignal3); e1.emitSignal3("Current year is", 2016); e2.emitSignal3("Sex is", 0); return a.exec(); }
Trong hàm main() chúng ta khai báo các đối tượng Emitter và Receiver để kết nối.
QObject::connect(&e1, SIGNAL(signal1()), &r1, SLOT(receiveSignal1())); ... QObject::connect(&e2, Emitter::signal2, &r2, Receiver::receiveSignal2); ...
Để kết nối một signal của một đối tượng với một slot của một đối tượng khác thì chúng ta dùng phương thức QObject::connect()
, phương thức này có rất nhiều override khác nhau vì Qt đã được phát triển từ rất lâu rồi. Ở trên là 2 cú pháp dùng trong phương thức này.
Signal 1 received! Signal 2 came with an integer: 3 Signal 3 from Emitter 1 : Current year is 2016 Signal 3 from Emitter 2 : Sex is 0