3.4. Các tín hiệu và các slot

Vậy là chúng ta đã quen dần với những nền tảng cơ bản để tạo ra các thành phần đồ họa. Trong bài trước chúng ta đã bắt đầu tạo ra 1 lớp riêng kế thừa từ QWidget để quản lý cửa sổ trong ứng dụng sau này của chúng ta.

Chúng ta sẽ đi tìm hiểu về cơ chế của tín hiệu (signal) và slot, 1 cơ chế của riêng Qt và đã trở thành 1 trong những điểm mạnh của thư viện này. Đây chính là kỹ thuật dùng để quản lý các sự kiện diễn ra bên trong các thành phần đồ họa. Ví dụ khi ấn 1 chiếc nút, chúng ta muốn 1 hàm nào đó được thực thi để phản ứng lại tác động này của người dùng. Việc này sẽ khiến tính tương tác của ứng dụng của chúng ta tăng lên rất nhiều.

Và tất cả những điều trên đây sẽ được trình bày trong bài học bên dưới này.

Khái niệm tín hiệu và slot

Nguyên lý khá đơn giản : 1 ứng dụng GUI linh hoạt sẽ cần biết tiếp nhận và phản ứng lại các sự kiện hoặc tác động từ phía người dùng.

Trong Qt, người ta nghĩ ra ý tưởng về khái niệm tín hiệu và slot. Để các bạn có thể hình dung cụ thể thì tiếp đây là định nghĩa của chúng.

  • Tín hiệu : là 1 thông điệp được 1 widget gửi đi khi có 1 sự kiện diễn ra. Ví dụ : 1 chiếc nút được bấm.
  • Slot : là hàm cần được gọi khi sự kiện diễn ra. Người ta thường nói là 1 tín hiệu gọi sử dụng 1 slot. Về bản chất thì các slot cũng vẫn là các phương thức của lớp. Khác biệt là nó có 1 liên kết với 1 tín hiệu. Ví dụ : slot quit() của QApplication sẽ thực hiện kết thúc chương trình.

Với Qt thì các tín hiệu và các slot được nhìn nhận như các thành phần riêng biệt bên cạnh các thuộc tính và phương thức truyền thống.

Dưới đây là hình ảnh thể hiện cách nhìn khác nhau về đối tượng khi sử dụng và không sử dụng Qt.

Trước Qt thì 1 đối tượng chỉ được cấu thành từ thuộc tính và phương thức. Qt thêm vào các thành phần được gọi là tín hiệu và slot dùng để quản lý các sự kiện diễn ra trong các widget.

Định nghĩa thì đã được nhắc đến ở trên. Trong Qt, chúng ta hay nghe nói về việc liên kết các tín hiệu và các slot với nhau.

Thật ra không có gì quá bí ẩn. Hãy tưởng tượng chúng ta có 2 đối tượng, mỗi cái đều có các thuộc tính, phương thức, tín hiệu và slot của riêng mình.

Trong hình vẽ thì tôi không thể hiện các phương thức và thuộc tính mà chú trọng vào các tín hiệu và slot. Như các bạn thấy thì tín hiệu 1 của đối tượng 1 được kết nối với slot 2 của đối tượng 2.

! Chúng ta hoàn toàn có thể kết nối 1 tín hiệu với nhiều slot. Như vậy, chúng ta có thể cùng lúc gọi nhiều xử lý hàm chỉ với 1 lần kích chuột. Ngoài ra, chúng ta cũng có thể kết nối 1 tín hiệu với 1 tín hiệu khác. Điều này nghĩa là tín hiệu của 1 chiếc nút có thể tạo ra tín hiệu trên 1 widget khác, và rồi tín hiệu mới này sẽ gọi các slot liên kết với nó (1 kiểu phản ứng dây chuyền vậy). Những trường hợp vừa nhắc đến trên đây thì khá đặc biệt nên tạm thời chúng ta sẽ không bàn đến chúng.

Kết nối 1 tín hiệu với 1 slot

Chúng ta hãy cùng thảo luận trên 1 tình huống cụ thể. Tôi có 2 đối tượng, 1 là nút bấm thuộc lớp QPushButton còn 1 là ứng dụng QApplication. Trong sơ đồ sau là những tín hiệu và slot thực tế đã được Qt tạo ra từ trước mà chúng ta có thể sử dụng.

Một bên là nút bấm m_nutBam còn bên kia là ứng dụng QApplication của chúng ta. Chúng ta có thể nối tín hiệu « nút được bấm » - clicked() với slot « đóng ứng dụng » - quit(). Thế là khi chúng ta ấn nút, ứng dụng sẽ kết thúc.

Để liên kết chúng với nhau, chúng ta cần dùng đến phương thức tĩnh của lớp QObject : connect().

Nguyên lý của phương thức connect()

connect() là 1 phương thức tĩnh, nghĩa là chúng ta hoàn toàn có thể gọi sử dụng nó mà không cần tạo ra đối tượng thực thể. Dành cho bạn nào còn chưa nhớ, để gọi phương thức tĩnh, cú pháp là tên của lớp, tiếp sau là dấu :: rồi tới tên của phương thức. Bởi phương thức này là của lớp QObject, chúng ta sẽ có :

QObject::connect();

Phương thức này nhận vào 4 tham số :

  • Con trỏ trỏ đến đối tượng phát tín hiệu
  • Tên của tín hiệu mà chúng ta muốn xử lý
  • Con trỏ trỏ đến đối tượng chứa slot xử lý
  • Tên của slot cần được thực hiện để đáp lại tín hiệu

Ngoài ra thì cũng tồn tại phương thức disconnect() dùng để ngắt kết nối giữa 2 đối tượng nhưng rất hiếm khi được sử dụng nên tôi sẽ không trình bày ở đây.

Sử dụng connect() để kết thúc ứng dụng

Hãy quay lại với ví dụ cụ thể của chúng ta.

#include "CuaSo.h"
CuaSo::CuaSo() : QWidget(){
    setFixedSize(300, 150);
    m_nutBam = new QPushButton("Đóng", this);
    m_ nutBam->setFont(QFont("Comic Sans MS", 14));
    m_ nutBam->move(110, 50);

    //Ket noi tin hieu an nut voi hanh dong ket thuc ung dung
    QObject::connect(m_nutBam, SIGNAL(clicked()), qApp, SLOT(quit()));
}

! connect() là 1 phương thức của lớp QObject. Bởi vì lớp CuaSo của chúng ta được kế thừa gián tiếp từ QObject nên lớp này cũng sẽ sở hữu phương thức connect().Việc đó nghĩa là trong trường hợp này, chúng ta hoàn toàn có thể bỏ tiền tố QObject:: trước tên của phương thức. Trong ví dụ thì tôi cố ý sử dụng cú pháp đầy đủ để nhận biết phương thức tĩnh nhưng việc này hoàn toàn không phải là bắt buộc.

Chúng ta hãy cùng chú ý kỹ hơn tới các tham số của connect() :

  • m_nutBam : con trỏ trỏ tới nút phát ra tín hiệu, không có gì đặc biệt.
  • SIGNAL(clicked()) : cách truyền tham số khá là kỳ lạ so với những trường hợp mà chúng ta hay thấy. Thực ra, SIGNAL() là 1 xử lý macro (1 đoạn mã nhỏ) của bộ tiền xử lý (preprocessor). Qt sẽ biến nó thành 1 đoạn mã C++ hợp cách mà trình biên dịch có thể hiểu được. Mục đích của kỹ thuật này là cho phép chúng ta viết 1 đoạn mã vừa ngắn vừa dễ hiểu. Không cần cố tìm cách hiểu cơ chế của xử lý này của Qt, đó không phải vấn đề của chúng ta.
  • qApp : con trỏ trỏ về đối tượng QApplication là chúng ta đã tạo ra trong main(). Con trỏ này từ đâu ra ? Trong thực tế, Qt tự động tạo ra 1 con trỏ tên là qApp trỏ về đối tượng QApplication mà chúng ta đã tạo ra. Con trỏ này được định nghĩa ở trong tiêu đề <QApplication> mà chúng ta thêm vào ở đầu tệp CuaSo.h.
  • SLOT(quit()) : slot cần được gọi để phản ứng là tín hiệu chiếc nút được bấm. Ở đây, SLOT() cũng là 1 macro mà Qt dùng đến để biến đổi thành đoạn mã mà trình biên dịch có khả năng hiểu được.

Slot quit() của QApplication là 1 slot được định nghĩa trước. Ngoài ra chúng ta còn có thể thấy 1 số slot khác tương tự như aboutQt(), vv…

Thường thường thì những slot được Qt tạo sẵn từ trước sẽ không đáp ứng đủ cho nhu cầu của chúng ta. Vậy nên trong phần tiếp theo của bài học, chúng ta sẽ tìm hiểu cách tạo ra slot của riêng mình.

Trước hết thì cứ kiểm tra đoạn mã bên trên đã.

Không có gì đặc biệt cho tới khi các bạn nhấn vào nút « Đóng ». Khi đó, chương trình sẽ kết thúc. Vậy là chúng ta đã lần đầu thành công kết nối 1 tín hiệu với 1 slot.

Các tham số trong tín hiệu và slot

Các bạn có thể thấy là phương thức connect() khá ấn tượng. Những thư viện khác, như WxWidget sử dụng khá nhiều macro và các cơ chế phức tạp như con trỏ hàm để đạt được những thành quả tương tự.

Tuy nhiên đó chỉ là 1 phần nhỏ trong những tính năng ưu việt mà Qt cung cấp cho chúng ta. Trong phần tiếp theo chúng ta sẽ thấy những tiện ích khác của Qt như là cho phép các tín hiệu và slot trao đổi tham số.

Tạo cửa sổ

Bước đầu, chúng ta sẽ thêm các widget khác vào trong cửa sổ của chúng ta. Bên cạnh đó chúng ta cũng có thể loại bỏ nút bấm lúc trước trong cửa sổ vì sẽ không cần dùng đến nó. Thay vào đó chúng ta sẽ thêm 2 loại widget khác :

  • QSlider : thanh chạy cho phép định ra 1 giá trị
  • QLCDNumber : khung hiển thị 1 giá trị số

Vì các bạn đã bắt đầu hình thành các thói quen tốt, tôi sẽ tiết kiệm thời gian và đưa ra luôn đoạn mã ví dụ. Trước hết là tệp tiêu đề.

#ifndef DEF_CUASO
#define DEF_CUASO
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QLCDNumber>
#include <QSlider>

class CuaSo : public QWidget{
    public:
      CuaSo();
    private:
      QLCDNumber *m_lcd;
      QSlider *m_thanhGiaTri;
};
#endif

Như các bạn có thể thấy thì nút bấm đã được thay thế bởi khung hiển thị LCD và thanh giá trị. Ở đây xin phép được nhắc lại 1 lần nữa, chúng ta không nên quên bao gồm tiêu đề của các lớp mà chúng ta sẽ sử dụng. Tôi vẫn bao gồm cả QPushButton nhưng nếu các bạn thấy sẽ không sử dụng lớp này nữa thì xóa dòng mã bao gồm của lớp này đi cũng không có vấn đề gì cả.

Dưới đây là tệp .cpp.

#include "CuaSo.h"
CuaSo::CuaSo() : QWidget(){
    setFixedSize(200, 100);
    m_lcd = new QLCDNumber(this);
    m_lcd->setSegmentStyle(QLCDNumber::Flat);
    m_lcd->move(50, 20);
    m_thanhGiaTri = new QSlider(Qt::Horizontal, this);
    m_thanhGiaTri->setGeometry(10, 60, 150, 20);
}

Không có gì quá bí ẩn trong đoạn mã trên vì đa phần là những câu lệnh chúng ta đã cùng xem qua. 1 số câu lệnh khá mới nhưng không mang ý nghĩa quá quan trọng, thuần túy thiên về việc trình bày : phương thức setSegmentStyle() của QLCDNumber dùng để thay đổi hình thức của khung hiển thị còn tham số trong phương thức khởi tạo của QSlider thì dùng để hiển thị thanh giá trị theo chiều ngang thay vì chiều dọc như mặc định của lớp này.

Và đây là kết quả của đoạn mã bên trên.

Liên kết các tham số

Công việc bây giờ là phải liên kết các widget lại với nhau và như thế, mọi thứ trở nên thú vị hơn nhiều. Ở đây, chúng ta sẽ thực hiện việc hiện thị lên khung LCD giá trị tương ứng của thanh chạy mục tiêu.

Trong tay của chúng ta có những tín hiệu và slot sau :

  • Tín hiệu valueChanged(int) của QSlider : tín hiệu được phát ra ngay khi chúng ta di chuyển con trỏ giá trị của thanh chạy. Điểm đặc biệt của tín hiệu này là nó trả về 1 tham số kiểu int (giá trị mới của thanh chạy).
  • Slot display(int) của QLCDNumber : hiển thị giá trị của tham số được truyền.
QObject::connect(m_thanhGiaTri, SIGNAL(valueChanged(int)), m_lcd, SLOT(display(int))) ;

Vậy là đủ để kết nối 2 widget trên. Thật kỳ lạ khi chúng ta chỉ cần cung cấp kiểu dữ liệu của tham số mà không cần đề cập đến tên của tham số đó khi thực hiện kết nối. Qt sẽ chịu trách nhiệm tự động truyền tham số tới đúng mục tiêu xác định.

Trong trường hợp này thì chỉ có 1 tham số nên là khá đơn giản. Tuy nhiên trong Qt, không có gì ngăn cấm việc chúng ta có thể truyền nhiều tham số cùng 1 lúc.

! Cần chú ý là kiểu dữ liệu của tham số được truyền đi cần phải hoàn toàn phù hợp. Các bạn sẽ không thể nào gửi đi 1 tín hiệu (int, double) tới 1 slot nhận vào kiểu (int, int). Đây là 1 trong những ưu thế của cơ chế tín hiệu và slot : đảm bảo kiểu dữ liệu của tham số. Tuy nhiên, trong 1 tín hiệu truyền đi vẫn có thể chứa nhiều tham số hơn số tham số nhận vào của 1 slot. Trong trường hợp đó, những tham số thừa ra sẽ bị slot xử lý bỏ qua.

Kết quả nhận được của đoạn mã trên là khung hiển thị sẽ chứa giá trị thay đổi tương ứng khi chúng ta di chuyển con trỏ của thanh chạy.

? Làm sao chúng ta biết được mỗi lớp sở hữu những tín hiệu hay slot nào ? Làm sao để xác định 1 tín hiệu sẽ gửi đi tham số là kiểu dữ liệu nào ?

Câu trả lời rất đơn giản : hãy đọc tài liệu kỹ thuật của Qt.

Nếu chúng ta tra cứu tài liệu thông tin về QLCDNumber, chúng ta sẽ thấy bên dưới những thuộc tính và phương thức quen thuộc sẽ là phần chứa thông tin về các slot và tín hiệu mà lớp này sở hữu.

! Các slot và các tín hiệu cũng có thể được kế thừa giống như các thuộc tính và phương thức. Điều này khá là hữu dụng khi chúng ta mới bắt đầu làm quen. Các bạn hẳn là cũng thấy được là ngoài các slot riêng của QLCDNumber thì cũng thừa hưởng rất nhiều slot vốn thuộc về QWidget và QObject. Thế nên đôi khi các bạn thấy người khác sử dụng 1 slot không nằm trong tài liệu của lớp, trước hết hãy kiểm tra tài liệu của các lớp mẹ vì có thể slot được sử dụng chính là 1 slot được thừa kế.

Bài tập

Để luyện tập, tôi đề nghị chúng ta sẽ thay đổi 1 chút đoạn mã bên trên. Tại sao không thử thay vì sử dụng 1 khung hiển thị số, chúng ta sẽ dùng 1 thanh chạy tiến trình để hiển thị kết quả!

1 vài gợi ý sau đây có thể sẽ khá hữu ích cho các bạn :

  • Thanh chạy tiến trình được quản lý bởi lớp QProgressBar.
  • Cần cung cấp các kích thước cho thanh chạy tiến trình nhờ phương thức setGeometry() mà chúng ta đã xem qua lúc trước.
  • Slot xử lý của QProgressBarsetValue(int), các bạn có thể dễ dàng tìm thấy thông tin về nó trong tài liệu của lớp.
  • Ngoài ra chúng ta cũng còn có thể thấy các slot khác như reset() cho phép đưa giá trị của thanh chạy tiến trình về 0. Tại sao không thử thêm vào 1 nút bấm cho phép chúng ta đưa giá trị của thanh chạy tiến trình về 0 trước khi bắt đầu?

Chúc các bạn vui vẻ với bài tập này.

Tự tạo các tín hiệu và các slot

Phần tiếp theo đây khá mà thú vị nhưng cũng đòi hỏi ở chúng ta sự tinh tế, đấy là phần nói về cách mà chúng ta tự tạo ra các tín hiệu và slot cho riêng mình.

Trong thực tế, các tín hiệu và slot tạo sẵn về cơ bản là đủ để đáp ứng các nhu cầu thông thường. Thế nhưng khi chúng ta làm việc với các dự án thực, không thiếu trường hợp chúng ta sẽ phải thốt lên khi slot mà chúng ta cần không tồn tại. Đấy chính là lúc chúng ta nhất thiết phải tạo ra các widget chuyên dụng của riêng mình.

! Để có thể tạo ra các tín hiệu và slot riêng cho lớp, yêu cầu là lớp phải được bắt nguồn trực tiếp hoặ gián tiếp từ QObject. Ví dụ như lớp CuaSo của chúng ta kế thừa từ QWidget là 1 lớp con của QObject. Vậy nên chúng ta sẽ có quyền tạo ra thêm các slot và tín hiệu cho lớp cửa sổ.

Chúng ta sẽ bắt đầu với việc tạo ra các slot rồi mới xem cách để tạo ra các tín hiệu sau.

Tự tạo 1 slot riêng

Trước hết cần phải nhắc lại là 1 slot thật ra chỉ là 1 phương thức, chỉ khác là nó có thể kết nối đến các tín hiệu. Vậy nên chúng ta sẽ chỉ cần tạo ra 1 phương thức mới, tuy nhiên cần tuân thủ 1 số quy tắc.

Để tập luyện thì chúng ta sẽ cần 1 trường hợp mà slot chúng ta cần hoàn toàn không tồn tại. Tôi đề nghị chúng ta sẽ thực hành trên 1 ví dụ trong đó giá trị của thanh chạy giá trị sẽ quyết định kích thước của cửa sổ của chúng ta.

Chúng ta muốn là tín hiệu valueChange(int) của QSlider có thể kết nối đến 1 slot của cửa sổ. Tiếp theo slot này sẽ có nhiêm vụ thay đổi chiều rộng của cửa sổ này. Bởi vì slot thayDoiChieuRong không hề tồn tại trong lớp QWidget, chúng ta sẽ cần phải tự tạo ra nó.

Tệp tiêu đề (CuaSo.h)

Chúng ta sẽ bắt đầu với tệp tiêu đề của lớp CuaSo.

Bất cứ khi nào chúng ta định nghĩa thêm 1 tín hiệu hay 1 slot cho lớp, nhất thiết phải định nghĩa 1 đoạn xử lý macro trong tệp tiêu đề của lớp đó.

Xử lý macro này sẽ tên là Q_OBJECT và phải được đặt ở đầu của khai báo lớp.

class CuaSo : public QWidget{
    Q_OBJECT

    public:
      CuaSo();
    private:
      QSlider *m_thanhGiaTri;
};

Tới lúc này thì trong lớp của chúng ta mới có 1 thuộc tính và 1 phương thức tạo.

Xử lý macro Q_OBJECT sẽ thông báo cho trình biên dịch chuẩn bị để tiếp nhận từ khóa slot. Bắt đầu từ đây, chúng ta có thể tự định nghĩa các slot cho riêng mình.

class CuaSo : public QWidget{
    Q_OBJECT

    public:
      CuaSo();
    public slots:
      void thayDoiChieuRong(int chieuRong);
    private:
      QSlider *m_thanhGiaTri;
};

Các bạn hẳn đã nhìn ra là chúng ta đã thêm 1 phần mới vào cho lớp : public slot. Tôi muốn tất cả các slot của tôi đều có quyền truy cập public. Chúng ta cũng có thể tao quyền truy cập private cho chúng nhưng kiểu gì thì Qt cũng phải có thể truy cập được từ bên ngoài để có thể gọi slot này từ bất cứ widget nào.

Trừ những chú ý bên trên thì nguyên mẫu của slot hoàn toàn tương tự như nguyên mẫu của 1 hàm bình thường. Chúng ta chỉ cần thêm mã xử lý của slot vào tệp .cpp là đủ.

Mã xử lý (CuaSo.cpp)

void CuaSo::thayDoiChieuRong(int chieuRong){
    setFixedSize(chieuRong, 100);
}

Đoạn mã xử lý không có gì quá đặc biệt. Phương thức chỉ đơn giản là sử dụng giá trị nhận vào từ tham số để thay đổi kích thước của cửa sổ thông qua phương thức setFixedSize().

Liên kết

Cuối cùng thì chúng ta chỉ còn việc kết nối tín hiệu của thanh chạy với slot mà chúng ta vừa tạo ra cho cửa sổ bên trong phương thức khởi tạo của cửa sổ này.

CuaSo::CuaSo() : QWidget(){
    setFixedSize(200, 100);
    m_thanhGiaTri = new QSlider(Qt::Horizontal, this);
    m_thanhGiaTri->setRange(200, 600);
    m_thanhGiaTri->setGeometry(10, 60, 150, 20);
    QObject::connect(m_thanhGiaTri, SIGNAL(valueChanged(int)), this, SLOT(thayDoiChieuRong(int)));
}

Tôi áp dụng 1 số thay đổi nhỏ trên thanh chạy giá trị của chúng ta để nó chỉ có thể mang các giá trị nằm giữa 200 và 600 nhờ phương thức setRange(). Bằng cách này, chúng ta có thể chắc chắn là cửa sổ của chúng ta có chiều rộng không nhỏ hơn 200 cũng như không thể vượt quá 600.

Liên kết được thiết lập giữa tín hiệu valueChange(int) của QSlider và slot thayDoiChieuRong(int) của CuaSo. Ở đây chúng ta lại thấy sự tồn tại của this là khá cần thiết vì chúng ta cần 1 con trỏ trỏ đến bản thân đối tượng và this đơn giản hóa việc này.

Dưới đây là sơ đồ mô tả sự kết nối trên.

Chúng ta sẽ nhận được kết quả như sau.

Bây giờ các bạn đã có thể thoải mái chơi đùa với việc thay đổi chiều rộng của cửa sổ nhờ thanh chạy rồi.

Bài tập : thay đổi chiều cao cửa sổ

Lại 1 bài tập nho nhỏ dành cho những người lười embarassed.

Thật là khó chịu khi chúng ta chỉ có thể thay đổi chiều rộng mà không phải là cả chiều cao của cửa sổ. Vậy nên bài tập của chúng ta là thêm vào slot cho phép chúng ta nắm quyền thay đổi chiều cao cửa sổ qua 1 thanh chạy thứ 2.

Các bạn cần phải đạt được 1 kết quả tương tự như sau :

Tự tạo tín hiệu riêng

Việc tự tạo tín hiệu riêng thì hiếm gặp hơn việc tự tạo slot riêng, thế nhưng không phải là không tồn tại.

Chúng ta sẽ cùng thực hiện xử lý như sau :  Khi thanh chạy ngang đạt tới giá trị tối đa là 600, cửa sổ sẽ gửi đi tín hiệu kichThuocToiDa để thông báo rằng nó đã đạt tới kích thước tối đa. Tiếp đó chúng ta sẽ cần phải nối tín hiệu này với 1 slot khác để kiểm nghiệm hoạt động của nó.

Tệp tiêu đề (CuaSo.h)

class CuaSo : public QWidget{
    Q_OBJECT

    public:
      CuaSo();
    public slots:
      void thayDoiChieuRong(int chieuRong);
    signals:
      void kichThuocToiDa();
    private:
      QSlider * m_thanhGiaTri;
};

Chúng ta đã thêm vào phần khai báo signals. Các tín hiệu cũng được thể hiện dưới dạng các phương thức (tương tự như các slot). Khác biệt là chúng sẽ không có mã xử lý trong tệp .cpp. Trong thực tế, Qt sẽ tạo ra đoạn xử lý này giúp chúng ta. Vậy nên nếu các bạn cố ý tự định nghĩa xử lý cho tín hiệu, các bạn sẽ nhận được thông báo lỗi “Multiple definition of ...”.

1 tín hiệu truyền đi có thể chứa 1 hay nhiều tham số. Trong trường hợp của ví dụ này thì nó không truyền đi tham số nào cả. Tín hiệu luôn luôn phải trả về kiểu void.

Phát đi tín hiệu (CuaSo.cpp)

Sau khi định nghĩa tín hiệu, đến 1 lúc chúng ta cần phải phát nó đi.

Làm sao chúng ta biết lúc nào thì cửa sổ đạt tới kích thước giới hạn? Thật ra rất đơn giản, chúng ta sẽ xác định được điều đó trong slot thayDoiChieuRong! Chúng ta chỉ cần kiểm tra xem slot có nhận vào giá trị tối đa (600) hay không và tùy theo đó phát đi tín hiệu

void CuaSo::thayDoiChieuRong(int chieuRong){
    setFixedSize(chieuRong, 100);
    if (chieuRong == 600){
        emit kichThuocToiDa();
    }
}

Để phát đi tín hiệu thì chúng ta cần sử dụng đến từ khóa emit. Lại 1 lần nữa, đây là 1 khái niệm được Qt tạo ra mà không phải trong bản thân ngôn ngữ C++. Lợi ích của nó đương nhiên là khiến cho đoạn mã thêm dễ hiểu.

! Ở đây thì tín hiệu không mang theo tham số nào cả. Thế nhưng nếu các bạn muốn thêm tham số vào thì cũng rất dễ dàng, chỉ cần phát tín hiệu đi theo cú pháp như sau : emit tinHieu(thamSo1, thamSo2, …);

Liên kết

Để kiểm tra tín hiệu thì chúng ta cần liên kết nó với 1 slot có sẵn. Các bạn có thể sử dụng bất cứ slot nào mà chúng ta có thể dễ dàng kiểm tra. Tôi thì đề nghị sử dụng slot của qApp dùng để tạo ra xử lý kết thúc ứng dụng.

Tôi đồng ý là việc này không mang ý nghĩa gì lớn, thế nhưng là 1 cách vô cùng dễ dàng để kiểm tra hoạt động của tín hiệu. Còn nhiều cơ hội để tạo ra những thứ có ích hơn ở phía sau.

Như vậy, trong phương thức khởi tạo của CuaSo, chúng ta cần thêm dòng lệnh sau.

QObject::connect(this, SIGNAL(kichThuocToiDa()), qApp, SLOT(quit()));

Bây giờ chúng ta đã có thể dễ dàng kiểm tra kết quả : chương trình sẽ kết thúc khi kích thước của cửa sổ đạt giá trị tối đa.

Và đây là trình tự xử lý đã diễn ra.

  1. Tín hiệu valueChanged của thanh giá trị gọi slot thayDoiChieuRong của cửa sổ.
  2. Slot thực hiện các xử lý của nó (tăng chiều rộng của cửa sổ) và kiểm tra giá trị xem cửa sổ đã đạt kích thước tối đa chưa. Nếu đã đạt tới kích thước tối đa, tín hiệu kichThuocToiDa sẽ được truyền đi.
  3. Tín hiệu kichThuocToiDa kích hoạt slot quit của ứng dụng, dẫn đến việc ứng dụng kết thúc.

Và đấy là cách thanh giá trị có thể kích phát chuỗi hành động dẫn đến kết quả cuối cùng là kết thúc chương trình. Chuỗi hành động sẽ càng ngày càng phức tạp tùy vào chương trình mà bạn muốn viết. Bây giờ đến lượt các bạn tự nghĩ ra và tạo ra các tín hiệu và slot cho riêng mình.

Tóm tắt bài học :
  • Qt cung cấp cơ chế quản lý sự kiện thông qua các tín hiệu và các slot để liên kết các widget với nhau. 1 widget có thể gửi đi 1 tín hiệu và rồi tín hiệu này sẽ được nhận và xử lý bởi 1 slot của widget khác. Ví dụ chúng ta có thể nối tín hiệu “nút được bấm” với slot “đóng cửa sổ”.
  • Các tín hiệu cũng như các slot có thể coi là 1 loại thành phần mới của lớp bên cạnh các thuộc tính và phương thức truyền thống.
  • Liên kết giữa các thành phần này được tạo ra nhờ phương thức tĩnh connect().
  • Chúng ta có thể tự tạo các tín hiệu và slot của riêng mình.