Qt 5 C++ – Cơ chế hoạt động của Signal và Slot

4.9/5 - (8 votes)

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()

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
4.9 8 votes
Article Rating
Subscribe
Thông báo cho tôi qua email khi
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

3 Comments
Inline Feedbacks
View all comments
nguyendung
3 năm trước

Bài viết rất hay ạ, mong ad viết thêm nhiều bài nữa

Trung
Trung
1 năm trước

Đọc dễ hiểu quá, cảm ơn admin rất rất nhiều ạ

Quang
Quang
1 năm trước

Dòng 23, 23, 24 của file main.cpp bạn viết thiếu & thì phải, mình thêm & mới run đc code

Last edited 1 năm trước by Quang