Qt 5 C++ – Event và signal

4.4/5 - (12 votes)

Event (sự kiện) là một phần quan trọng của bất kỳ ứng dụng GUI nào. Tất cả các ứng dụng dạng GUI xử lý event trong suốt thời gian mà nó chạy. Event phần lớn được tạo ra bởi người dùng, nhưng cũng có lúc được tạo ra bởi chính ứng dụng. Có ba thành phần tham gia vào hệ thống event:

  • Event nguồn
  • Đối tượng event
  • Event đích

Event nguồn là đối tượng tạo ra sự thay đổi. Cứ có gì đó trong ứng dụng tạo ra sự thay đổi nào đó thì nó chính là event nguồn. Đối tượng event là chính bản thân cái event đó đã được mã hóa. Event đích là đối tượng sẽ xử lý event đó.

Nếu bạn từng lập trình GUI bằng API của window, bạn đã biết là một ứng dụng GUI sau khi đã hoàn tất các công việc khởi tạo biến, đăng ký ID với hệ điều hành…v.v. thì ứng dụng đó sẽ bắt đầu một vòng lặp vô tận, vòng lặp này liên tục lắng nghe và bắt các event đang diễn ra và gửi nó đến đối tượng để xử lý bằng câu lệnh switch(). Trong Qt cũng vậy, cứ mỗi lần gọi phương thức exec() của ứng dụng thì ứng dụng sẽ bắt đầu vòng lặp của nó, tuy nhiên chúng ta không trực tiếp làm việc với vòng lặp này, mà thay vào đó chúng ta dùng hệ thống Signal & Slot của Qt, đây là một phần mở rộng của C++ và cũng là thành phần đặc trưng của riêng Qt dùng để kiểm soát event.

Nói sơ qua về cách hoạt động của cơ chế Signal & Slot, một signal (tiếng việt là tín hiệu) sẽ được phát ra khi có sự kiện nào đó diễn ra, slot là phương thức của một đối tượng nào đó, mà cứ mỗi khi chỉ có đúng signal đó được phát ra thì slot này sẽ được thực thi.

Click

Ví dụ dưới đây là một chương trình đơn giản để mô tả cách hoạt động của hệ thống event. Chương trình hiển thị một button, cứ mỗi lần click button này thì thoát chương trình.

#pragma once

#include <QWidget>

class Click : public QWidget {
    
  public:
    Click(QWidget *parent = 0);
};
#include <QPushButton>
#include <QApplication>
#include <QHBoxLayout>
#include "click.h"

Click::Click(QWidget *parent)
    : QWidget(parent) {
       
  QHBoxLayout *hbox = new QHBoxLayout(this);
  hbox->setSpacing(5);
        
  QPushButton *quitBtn = new QPushButton("Quit", this);
  hbox->addWidget(quitBtn, 0, Qt::AlignLeft | Qt::AlignTop);

  connect(quitBtn, &QPushButton::clicked, qApp, &QApplication::quit);
}
connect(quitBtn, &QPushButton::clicked, qApp, &QApplication::quit);

Phương thức connect() có tác dụng kết nối signal của đối tượng này tới slot của đối tượng khác. Trong trường hợp này nó kết nối signal clicked của button tới slot quit của qApp. 

#include <QApplication>
#include "click.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Click window;
  
  window.resize(250, 150);
  window.setWindowTitle("Click");
  window.show();

  return app.exec();
}
Capture

Tương tác với bàn phím – KeyPress

Ta sẽ học cách tương tác với bàn phím qua ví dụ dưới đây.

#pragma once

#include <QWidget>

class KeyPress : public QWidget {

  public:
    KeyPress(QWidget *parent = 0);

  protected:
    void keyPressEvent(QKeyEvent * e);
};
#include <QApplication>
#include <QKeyEvent>
#include "keypress.h"

KeyPress::KeyPress(QWidget *parent)
    : QWidget(parent)
{ }

void KeyPress::keyPressEvent(QKeyEvent *event) {

   if (event->key() == Qt::Key_Escape) {  
       qApp->quit();
   } 
}

Thoát chương trình khi nhấn phím Escape.

void KeyPress::keyPressEvent(QKeyEvent *e) {

   if (e->key() == Qt::Key_Escape) {  
       qApp->quit();
   } 
}

Bản thân các lớp có sẵn trong Qt đã có sẵn những slot tự động xử lý các signal riêng biệt. Tuy nhiên ta có thể code lại các slot này, trong lập trình hướng đối tượng thì khái niệm này được gọi là Overriding. Trong ví dụ này mình override lại slot keyPressEvent của lớp QWidget, slot này nhận thông tin của signal trong lớp QKeyEvent.

#include <QApplication>
#include "keypress.h"

int main(int argc, char *argv[]) {

  QApplication app(argc, argv);  
    
  KeyPress window;
  
  window.resize(250, 150);
  window.setWindowTitle("Key press");
  window.show();

  return app.exec();
}

QMoveEvent

Lớp QMoveEvent chứa các thông tin về sự thay đổi vị trí của widget. QMoveEvent sẽ tự động được gửi tới widget như keyPress ở trên.

#pragma once

#include <QMainWindow>

class Move : public QWidget {

  Q_OBJECT

  public:
    Move(QWidget *parent = 0);
 
  protected:
    void moveEvent(QMoveEvent *e);
};
#include <QMoveEvent>
#include "move.h"

Move::Move(QWidget *parent)
    : QWidget(parent)
{ }

void Move::moveEvent(QMoveEvent *e) {

  int x = e->pos().x();
  int y = e->pos().y();
  
  QString text = QString::number(x) + "," + QString::number(y);

  setWindowTitle(text);
}

Trong đoạn code trên, chúng ta xử lý sự kiện di chuyển widget. Cứ mỗi lần chúng ta di chuyển cửa sổ, một event được gửi tới slot moveEvent, cũng giống như keyPress, slot này có sẵn trong Qt và chúng ta cũng override lại nó. Chúng ta lấy vị trí tọa độ mới của widget và đưa lên thanh tiêu đề.

int x = e->pos().x();
int y = e->pos().y();

Lấy thông tin tọa độ từ đối tượng QMoveEvent.

QString text = QString::number(x) + "," + QString::number(y);

Chuyển giá trị tọa độ từ số sang chuỗi.

setWindowTitle(text);

Dùng phương thức setWindowTitle() để đặt tiêu đề cho cửa sổ.

#include <QApplication>
#include "move.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Move window;
  
  window.resize(250, 150);
  window.setWindowTitle("Move");
  window.show();
  
  return app.exec();
}
Capture

Ngắt kết nối signal

Nếu có kết nối signal và slot thì Qt cũng cho phép chúng ta ngắt kết nối slot với các signal đó.

#pragma once

#include <QWidget>
#include <QPushButton>

class Disconnect : public QWidget {
    
  Q_OBJECT  

  public:
    Disconnect(QWidget *parent = 0);

  private slots:
    void onClick();
    void onCheck(int);

  private:
    QPushButton *clickBtn;
};

Ở trên chúng ta khai báo hai slot. Từ khóa slot thực chất không phải của C++, mà là phần mở rộng của C++ do Qt phát triển thêm. Chúng sẽ được dịch bởi Qt trước khi được dịch bởi C++. Để có thể dùng được signal và slot trong Qt thì chúng ta phải thêm macro Q_OBJECT vào đầu mỗi khai báo lớp.

#include <QTextStream>
#include <QCheckBox>
#include <QHBoxLayout>
#include "disconnect.h"

Disconnect::Disconnect(QWidget *parent)
    : QWidget(parent) {
        
  QHBoxLayout *hbox = new QHBoxLayout(this);
  hbox->setSpacing(5);        
        
  clickBtn = new QPushButton("Click", this);
  hbox->addWidget(clickBtn, 0, Qt::AlignLeft | Qt::AlignTop);

  QCheckBox *cb = new QCheckBox("Connect", this);
  cb->setCheckState(Qt::Checked);
  hbox->addWidget(cb, 0, Qt::AlignLeft | Qt::AlignTop);

  connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick);
  connect(cb, &QCheckBox::stateChanged, this, &Disconnect::onCheck);  
}

void Disconnect::onClick() {
    
  QTextStream out(stdout);
  out << "Button clicked" << endl;
}

void Disconnect::onCheck(int state) {
    
  if (state == Qt::Checked) {
    connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick);
  } else {
    disconnect(clickBtn, &QPushButton::clicked, this, 
        &Disconnect::onClick);
  }
}

Trong ví dụ này chúng ta tạo ra một button và một checkbox. Nếu tick vào checkbox thì cho connect button với slot onClick, ngược lại thì ngắt kết nối. Cứ mỗi lần bấm button thì in ra message, tuy nhiên message này hiển thị trên console vì thế nếu bạn nên chạy chương trình từ command promtp hoặc nhìn vào cửa sổ Application Output trên Qt Creator mới thấy message.

connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick);
connect(cb, &QCheckBox::stateChanged, this, &Disconnect::onCheck); 

Connect hai signal với hai slot.

void Disconnect::onClick() {
    
  QTextStream out(stdout);
  out << "Button clicked" << endl;
}

Nếu nhấn button thì in message.

void Disconnect::onCheck(int state) {
    
  if (state == Qt::Checked) {
    connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick);
  } else {
    disconnect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick);
  }
}

Slot onCheck() sẽ kết nối hoặc ngắt kết nối button với slot. Chúng ta dùng phương thức disconnect() để ngắt.

#include <QApplication>
#include "disconnect.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Disconnect window;
  
  window.resize(250, 150);
  window.setWindowTitle("Disconnect");
  window.show();
  
  return app.exec();
}

Capture

Timer

Timer trong Qt (hay các nền tảng khác) thương được dùng để code các công việc có tính chất lặp đi lặp lại. Trong ví dụ này ta dùng timer để hiển thị giờ hiện tại.

#pragma once

#include <QWidget>
#include <QLabel>

class Timer : public QWidget {

  public:
    Timer(QWidget *parent = 0);

  protected:
    void timerEvent(QTimerEvent *e);

  private:
    QLabel *label;
};
#include "timer.h"
#include <QHBoxLayout>
#include <QTime>

Timer::Timer(QWidget *parent)
    : QWidget(parent) {
        
  QHBoxLayout *hbox = new QHBoxLayout(this);
  hbox->setSpacing(5);               
           
  label = new QLabel("", this);
  hbox->addWidget(label, 0, Qt::AlignLeft | Qt::AlignTop);

  QTime qtime = QTime::currentTime();
  QString stime = qtime.toString();
  label->setText(stime);
   
  startTimer(1000);
}

void Timer::timerEvent(QTimerEvent *e) {
    
  Q_UNUSED(e);
  
  QTime qtime = QTime::currentTime();
  QString stime = qtime.toString();
  label->setText(stime);
}

Chúng ta hiển thị giờ từ hệ thống.

QTime qtime = QTime::currentTime();
QString stime = qtime.toString();
label->setText(stime);

Chúng ta lấy giờ hiện tại của hệ thống bằng phương thức currentTime(), sau đó chuyển nó sang kiểu chuỗi và đưa vào label.

startTimer(1000);

Phương thức startTimer() sẽ bắt đầu chu kỳ lặp của nó, cứ sau 1000ms nó sẽ phát một signal và signal đó sẽ được xử lý bởi slot timerEvent().

void Timer::timerEvent(QTimerEvent *e) {
    
  Q_UNUSED(e);
  
  QTime qtime = QTime::currentTime();
  QString stime = qtime.toString();
  label->setText(stime);
}

Để bắt được event từ timer, chúng ta phải override phương thức timerEvent().

#include <QApplication>
#include "timer.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Timer window;
  
  window.resize(250, 150);
  window.setWindowTitle("Timer");
  window.show();

  return app.exec();
}

Capture

0 0 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.

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

Hi !

Ad cho mình hỏi ngoài lề bài này chút với nhé
Mình không add được thư viện #include , các thư viện khác thì dùng bình thường, mình dùng QT 5.14.2

Thanks ad !

Quy
Quy
3 năm trước
Reply to  Quy

Là thư viện này ad nhé
#include

Quy
Quy
3 năm trước

Mình copy nên nó không hiện rõ, là thư viện #include

Thanks ad !

Quy
Quy
3 năm trước
Reply to  Phở Code

https://doc.qt.io/qt-5/qtest.html

Mình thấy ở hãng có, nhưng không hiểu sao add vào không được
Ad xem giúp mình với nhé

Thanks Ad !

Quy
Quy
3 năm trước

Thư viện QTest ad ah, hình như có dấu “<>” nên không show lên được, hihi

Quy
Quy
3 năm trước
Reply to  Phở Code

Ok rồi

Thank Ad nhiều nhé !