< Lập trình tân binh | 3.3. Tùy chỉnh các widget

3.3. Tùy chỉnh các widget

Cửa sổ tí hon với độc 1 chiếc nút mà chúng ta tạo ra trong bài trước chỉ là bước đầu tiên. Thực ra trong bài học đó, tôi đã dành thời gian chủ yếu để giải thích cách biên dịch chương trình hơn là tập trung vào nội dung của chương trình.

Vậy nên lúc này chúng ta sẽ nghiên cứu kỹ hơn về mặt nội dung của chương trình.

Chúng ta đã có 1 cái nút. Giờ sẽ cần làm gì để thay đổi kích thước cái nút đó ? Muốn thay đổi màu sắc, chữ viết trên nhãn hay vị trí của nút bên trong cửa sổ thì làm thế nào ?

Chúng ta sẽ cùng tập trung thảo luận về cách thao tác với các thuộc tính của widget nút bấm trong bài học này. Đương nhiên là ngoài nút bấm thì vẫn còn những widget khác như hộp chọn, danh sách thả, vv… Nhưng một khi chúng ta đã quen làm việc với các thuộc tính của 1 loại widget thì sẽ không khó khăn gì để biết cách thao tác những loại khác.

Cuối cùng chúng ta cũng sẽ nhắc lại 1 chút về tính kế thừa. Nó sẽ cho phép chúng ta tạo ra các widget của riêng mình kế thừa từ các widget có sẵn. Đây là 1 kỹ thuật vô cùng phổ biến chúng ta hay thấy trong các thư viện dùng để tạo GUI.

Thay đổi thuộc tính của 1 widget

Như tôi đã từng nói, nút bấm chính là 1 loại widget. Trong Qt, chúng ta tạo ra các nút bấm nhờ lớp QPushButton.

Chúng ta đều biết là 1 lớp đối tượng thì gồm 2 thành phần :

  • Các thuộc tính : các biến bên trong của lớp.
  • Các phương thức : các hàm bên trong của lớp.

Tính đóng gói yêu cầu là các thuộc tính của 1 lớp phải có quyền truy cập private và không thể bị truy cập và sử dụng trực tiếp từ người dùng bên ngoài.  Và bởi vì bản thân chúng ta cũng chỉ là “người dùng bên ngoài” đối với các lớp của Qt, chúng ta không còn lựa chọn nào khác ngoài sử dụng các phương thức truy cập (phương thức lấy và phương thức đặt) để thao tác với các thuộc tính này.

Các phương thức truy cập của Qt

Những lập trình viên đã tạo ra Qt là những người rất tuân thủ các quy tắc lập trình. Họ thậm chí còn phải tuân thủ những quy tắc này một cách nghiêm ngặt nhất nếu không thư viện khổng lồ mà họ tạo ra rất có nguy cơ biến thành 1 đống hỗn độn !

Vậy là trong Qt, với mỗi widget, chúng ta có thể tìm thấy các đặc điểm sau liên quan đến mỗi thuộc tính :

  • Bản thân thuộc tính được bảo về bởi quyền private, chúng ta không thể trực tiếp đọc hay thay đổi nó. Ví dụ : text.
  • Phương thức truy cập để đọc : 1 phương thức hằng có tên giống với tên của thuộc tính. Bởi vì nó là phương thức hằng nên các bạn có thể yên tâm là nó sẽ chỉ đọc chứ không thay đổi thuộc tính của lớp. Ví dụ : text().
  • Phương thức truy cập để thay đổi : 1 phương thức dùng để thay đổi thuộc tính. Tên của phương thức này bắt đầu bằng “set”, theo sau đó là tên của thuộc tính. Ví dụ : setText().

Kỹ thuật này mặc dù kiến trúc hơi nặng nề do phải tạo ra 2 phương thức cho mỗi thuộc tính nhưng lại giúp Qt đảm bảo giá trị chúng ta cấp cho các thuộc tính là hợp lệ. Ví dụ, nó sẽ tránh việc người dùng cung cấp giá trị thanh chạy tiến trình là 150% trong khi giá trị hợp lệ là giữa 0% và 100%.

Sau đây, chúng ta sẽ cùng tìm hiểu 1 số thuộc tính của các nút mà chúng ta được phép thao tác.

1 vài thuộc tính ví dụ của các nút bấm

Vậy là các bạn đều hiểu là mỗi widget, trong đó có các nút bấm, đều cho phép chúng ta thay đổi khá nhiều thuộc tính của chúng. Tuy nhiên chúng ta không thể nói hết trong 1 bài học, vậy nên trong bài học tới đây, tôi sẽ chỉ cho cách bạn cách để sử dụng tốt tài liệu kỹ thuật của Qt.

Trước mắt thì chúng ta cứ nói qua về những thuộc tính thông dụng nhất để các bạn có thể làm quen dần với cách sử dụng các phương thức truy cập.

text : văn bản

Đây có thể nói là 1 trong những thuộc tính quan trọng nhất : nó cho phép chúng ta thay đổi văn bản hiển thị bên trên nút bấm, hay chính là nhãn của chiếc nút đó.

Thực ra thì chúng ta có thể định nghĩa văn bản này vào thời điểm chúng ta tạo ra chiếc nút bởi phương thức khởi tạo của lớp này cho phép chúng ta cung cấp tham số là giá trị của nhãn của nút.

Tuy nhiên, vì 1 lý do nào đó mà chúng ta có thể muốn chương trình thay đổi giá trị đó trong khi thực thi. Vậy nên việc có thể truy cập tới thuộc tính này thông qua các phương thức truy cập trở nên khá hữu dụng.

Với mỗi thuộc tính, tài liệu của Qt sẽ nói cho chúng ta biết công dụng của thuộc tính này cũng như các phương thức truy cập để thao tác với nó.

Đầu tiên, chúng ta sẽ biết được kiểu dữ liệu của thuộc tính. Trong trường hợp của text thì nó thuộc lớp QString, giống như tất cả các thuộc tính khác dùng để lưu trữ văn bản trong Qt. Qt không sử dụng kiểu string cơ bản của C++ mà sử dụng 1 phiên bản của riêng nó để quản lý các chuỗi ký tự. Tóm lại thì QString là 1 bản cải tiến của string trong Qt.

Sau đó là 1 vài dòng về công dụng của thuộc tính mà chúng ta đang quan tâm. Cuối cùng, tài liệu cung cấp cho chúng ta tên của các phương thức truy cập để đọc và thay đổi thuộc tính. Trong trường hợp này là 2 phương thức sau :

  • QString text () const : phương thức lấy cho phép chúng ta đọc giá trị thuộc tính. Nó trả về kết quả là 1 đối tượng QString bởi vì đây là kiểu của thuộc tính này. Các bạn đều thấy từ khóa const để đảm bảm phương thức này không thay đổi giá trị của bất cứ thuộc tính nào.
  • void setText(const QString & text) : phương thức đặt dùng để thay đổi thuộc tính. Nó nhận vào 1 tham số là đoạn văn bản làm nhãn mà chúng ta muốn hiện lên trên chiếc nút.

! Về sau thì các bạn không cần mỗi lần đều phải giở lại tài liệu của Qt làm gì. Với các thuộc tính thì tên của các phương thức luôn tuân thủ quy tắc mà tôi đã nêu trên kía.

Dưới đây là 1 ví dụ thay đổi nhãn của nút sau khi tạo ra.

#include <QApplication>
#include <QPushButton> 
int main(int argc, char *argv[]){
    QApplication app(argc, argv); 
    QPushButton nutBam("Xin chào Tân Binh");
    nutBam.setText("Nút khẩn cấp !"); 
    nutBam.show(); 
    return app.exec();
}

! Các bạn hãy để ý là phương thức setText() yêu cầu tham số là 1 đối tương QString nhưng chúng ta lại chỉ cần cung cấp cho nó 1 chuỗi ký tự thường trong dấu ngoặc kép. Lý do rất đơn giản, bởi vì lớp QString hoạt động tương tự như string : những chuỗi ký tự trong ngoặc kép được tự động chuyển hóa thành đối tượng QString. Đây là 1 điều may mắn vì nó giúp chúng ta đỡ mất công phải tạo ra 1 đối tượng QString chỉ vì mục đích nhỏ này.

Và đây là kết quả của đoạn mã ví dụ.

Chúng ta có thể nhận ra được quá trình xử lý thông qua kết quả trên.

  1. Chúng ta tạo ra nút bấm thông qua phương thức khởi tạo và cho nó 1 cái nhãn « Xin chào Tân Binh »
  2. Chúng thay nhãn trên chiếc nút thành « Nút khẩn cấp ! »

Cuối cùng thì nhãn của chiếc nút là « Nút khẩn cấp » vì đây là nhãn cuối cùng mà chúng ta đưa cho nút và nó đã thay thế nhãn cũ. Xử lý trên cũng tương tự như xử lý khi chúng ta thực hiện :

int x = 1;
x = 2;
cout << x;

thì giá trị của x sẽ là 2.

Nên hiểu rằng là ví dụ chúng ta vừa thực hiện với chiếc nút thật ra hơi vô dụng bởi vì chúng ta có thể đưa nhãn « Nút khẩn cấp ! » cho nút từ khi khởi tạo và kết quả nhận được sẽ giống hệt nhau. Tuy nhiên, setText() vẫn cho chúng ta thấy khả năng của nó để có thể áp dụng trong những trường hợp khác. Ví dụ như khi người dùng cung cấp cho chúng ta tên họ thì chúng ta có thể dùng phương thức trên để in tên người dùng lên chiếc nút, vv…

toolTip : bong bóng thông tin

Đôi khi các bạn nhìn thấy một vài dòng văn bản nho nhỏ chú thích hay chỉ dẫn mỗi khi con chuột chỉ vào 1 thành phần widget. Đoạn văn bản đó được gọi là 1 bong bóng thông tin hay bong bóng chỉ dẫn.

Bong bóng này sẽ được định nghĩa dựa vào thuộc tính toolTip của widget. Để thay đổi văn bản trong bong bóng, chúng ta đơn giản chỉ cần sử dụng phương thức setToolTip(). Tổ chức của Qt giúp công việc của chúng ta dễ dàng hơn nhiều, phải không ?

#include <QApplication>
#include <QPushButton> 
int main(int argc, char *argv[]){
    QApplication app(argc, argv); 
    QPushButton nutBam("Nút khẩn cấp !");
    nutBam.setToolTip("Nút hướng dẫn"); 
    nutBam.show(); 
    return app.exec();
}

font : kiêu chữ, phông chữ

Với thuộc tính font, mọi thứ trở nên phức tạp hơn 1 chút. Thực ra thì cho tới giờ, chúng ta mới sử dụng các tham số là chuỗi ký tự được tự động chuyển đổi thành đối tượng QString nên mọi việc mới đơn giản như thế.

Thuộc tính font phức tạp hơn vì bản thân nó chứa đựng 3 loại thông tin.

  • Tên của kiểu chữ được sử dụng (Times New Roman, Arial, Calibri, vv…)
  • Kích thước của chữ tính theo pixel (10, 12, 14, vv…)
  • Phong cách chữ (in đậm, nghiêng, vv…)

Nguyên mẫu của hàm setFont() là như sau :

void setFont ( const QFont & )

Điều này nghĩa là phương thức này nhận tham số là đối tương lớp QFont. Dựa theo các từ khóa sử dụng thì tham số của hàm này là 1 tham chiếu hằng, tức là tham số sẽ không cần phải được sao chép để truyền cho hàm nhưng đồng thời hàm cũng không thể thay đổi giá trị của tham số.

Vậy làm sao chúng ta có thể cung cấp 1 đối tượng QFont cho phương thức ? Câu trả lời quá đơn giản, chúng ta chỉ cần tạo ra đối tượng đó là được.

Tài liệu của Qt cho cung cấp cho chúng ta khá nhiều thông tin về lớp QFont. Cái chúng ta quan tâm là phương thức khởi tạo của lớp này. Các bạn có thể tự mình tìm hiểu tài liệu nếu muốn. Nếu không thì tôi sẽ  chép ra đây nguyên mẫu của phương thức này (thế nhưng đừng quá ỷ lại, lần sau có thể tôi sẽ không làm thế nữa đâu).

QFont(const QString & family, int pointSize = -1, int weight = -1, bool italic = false)

! Trong thực tế thì Qt hiếm khi chỉ cung cấp 1 phương thức khởi tạo duy nhất cho mỗi lớp mà thường tạo ra nhiều phương thức khởi tạo nạp chồng khác nhau. Đối với lớp QFont cũng thế nhưng phương thức mà tôi ghi ra cho các bạn là phương thức tạo chủ yếu và được sử dụng nhiều nhất.

Chỉ có duy nhất tham số đầu tiên là tham số bắt buộc, đó là tên kiểu chữ mà chúng ta muốn sử dụng. Những tham số khác thì như các bạn đã thấy, đều có giá trị mặc định nên chúng ta không bắt buộc phải truyền chúng cho phương thức.

Dưới đây là ý nghĩa của từng tham số dành cho các bạn quan tâm :

  • family :  tên kiểu chữ
  • pointSize : kích thước các ký tự tính theo pixel
  • weight : độ dày của nét chữ, cho phép chúng ta định nghĩa các mức độ in đậm nhất định. Giá trị hợp lệ của nó là nằm giữa 0 và 99 (từ nét mỏng nhất đến dày nhất). Các bạn có thể dùng hằng số QFont::Bold của lớp này để viết chữ in đậm, tương ứng với độ dày là 75. 
  • italic : tham số kiểu boolean, chỉ ra xem chữ viết có nghiêng không.

Hãy cùng làm vài ví dụ để làm quen nhé. Đầu tiên là tạo ra 1 đối tượng QFont.

QFont phongChu("Courier") ;

Tôi đặt tên đối tượng của tôi là phongChu. Tiếp đó tôi sẽ truyền đối tượng này làm tham số cho hàm setFont() của chiếc nút.

nutBam.setFont(phongChu);

Chúng ta cũng có thể rút gọn cách tôi vừa nói bên trên vào 1 câu lệnh nếu chúng ta không định sử dụng đối tượng QFont vừa được định nghĩa ở chỗ khác trong đoạn mã.

nutBam.setFont(QFont("Courier"));

2 câu lệnh lồng vào nhau. Chúng ta tạo ra đối tượng QFont ngay vào thời điểm nó được yêu cầu. Ngắn hơn và nhanh hơn trong những trường hợp đơn giản, cách viết này tránh cho chúng ta phải ngồi nghĩ ra tên cho những đối tượng chỉ cần dùng đến 1 lần.

Chúng ta có thể tiến thêm bước nữa, sử dụng 1 kiểu chữ khác, đồng thời cũng tăng kích thước các ký tự.

Các bạn cũng nên thử cả cách viết nghiêng và in đậm nữa.

cursor : con trỏ của chuột

Với thuộc tính cursor, chúng ta có thể thay đổi hình dạng của con chuột khi trỏ lên widget.

Cách đơn giản nhất là sử dụng 1 trong các hằng số định nghĩa trước trong danh sách dành cho con trỏ.  

nutBam.setCursor(Qt::PointingHandCursor);

Khi di chuột tới vị trí chiếc nút thì hình dạng của nó sẽ biến thành bàn tay trỏ vào nút bấm đó.

icon : hình ảnh biểu tượng

Sau tất cả những thứ chúng ta đã cùng tìm hiểu bên trên, việc thêm vào 1 hình ảnh biểu tượng trên nút có vẻ trở nên đơn giản : chúng ta sẽ sử dụng phương thức setIcon() với tham số là đối tượng QIcon.

Đối tượng QIcon có thể được tạo ra dễ dàng bằng cách gọi phương thức khởi tạo của lớp này với tham số là tên 1 tệp ảnh mà chúng ta muốn nạp.

Ví dụ nếu tôi muốn thêm 1 biểu tượng mặt cười lên chiếc nút của mình.

nutBam.setIcon(QIcon("smiley.png"));  

Tôi đã chọn 1 tệp ảnh PNG là 1 định dạng mà Qt có thể đọc được.

! Cần chú ý là để đoạn mã có thể hoạt động được thì tệp ảnh cần nằm trong cùng thư mục với tệp thực thi hoặc là trong thư mục con (trong trường hợp đó thì chúng ta phải đưa ra đường dẫn tới tệp ảnh).

Trong Linux thì tệp này phải nằm trong thư mục HOME của bạn. Nếu các bạn muốn sử dụng đường dẫn của ứng dụng làm thư mục mặc định như trong Windows thì sẽ cần thay đổi câu lệnh 1 chút : QIcon(QCoreApplication::applicationDirPath() + "/smiley.png"); Với câu lệnh này thì xử lý sẽ tương tự như xử lý mặc định của Windows, tức là tệp ảnh cần nằm trong cùng thư mục của tệp thực thi hoặc thư mục con.

Nếu các bạn thành công thì đây sẽ là kết quả nhận được.

Qt và tính kế thừa

Một vài thuộc tính là đủ cho các bạn làm quen và nắm bắt được nguyên lý hoạt động rồi chứ. Bây giờ chúng ta sẽ quay về với những kiến thức quan trọng hơn 1 chút.

Tôi hy vọng là các bạn đã nắm khá vững về tính kế thừa vì nếu không thì chúng ta khó mà đi tiếp được. Nếu bạn nào thấy kiến thức về phần này vẫn chưa ổn, xin mời hãy đọc lại bài học về khái niệm này.

Hàng đống sự kế thừa

Kế thừa có thể được coi là khái niệm quan trọng nhất trong lập trình hướng đối tượng. Ý tưởng về việc tạo ra 1 lớp cơ bản, rồi tạo ra các lớp con kế thừa nó, và rồi chính các lớp con này lại có những lớp con của riêng mình chính là nguồn gốc tạo nên sức mạnh cho những thư viện như Qt.

Trong thực tế, hầu như mọi lớp trong Qt đều sử dụng đến tính kế thừa.

Gia phả các lớp được Qt cung cấp sẽ cho các bạn 1 khái niệm rõ ràng hơn về thứ mà chúng ta đang nói đến. Tất cả các lớp nằm ngoài cùng bên trái là các lớp cơ bản, còn các lớp dịch về bên phải chính là các lớp con, lớp cháu kế thừa từ các lớp cơ bản.

  • QAbstractExtensionFactory
    • QExtensionFactory
  • QAbstractExtensionManager
    • QExtensionManager

Những lớp như QAbstractExtensionFactory hay QAbstractExtensionManager được gọi là các lớp cơ bản và không có lớp mẹ. Trong khi đó thì QExtensionFactory QExtensionManager lại là các lớp con kế thừa từ các lớp vừa được nhắc đến bên trên.

Rất bình thường phải không ? Hãy chờ đến lúc các bạn nhìn đến lớp QObject. Thật không thể đếm hết là nó có bao nhiêu con cháu nữa. Thậm chí có những lúc chúng ta thấy đến 5 sự kế thừa liên tiếp nhau.

? Vậy làm sao chúng ta tìm ra những thứ muốn tìm trong đó ?

Đừng lo, các bạn sẽ nhanh chóng quen dần thôi. Tất cả sự kế thừa trong đó dùng để giúp chúng ta định hướng dễ hơn trong núi tài liệu đó. Nếu chúng không được thiết kế cẩn thận như thế, chúng ta sẽ rất dễ đi lạc và không thể thoát ra được.

QObject : lớp cơ bản không thể bỏ qua

QObject là 1 lớp cơ bản của tất cả các đối tượng Qt. Lớp này không đại diện 1 đối tượng nào cụ thể nhưng lại có chứa những tính năng cơ bản mà mọi lớp đều cần đến.

Nhiều bạn có thể thấy ngạc nhiên khi thấy 1 lớp cơ bản không biết làm 1 việc gì cụ thể. Thế nhưng đấy chính là thứ tạo nên sức mạnh cho thư viện. Ví dụ, trong lớp cơ bản QObject, chúng ta chỉ cần định nghĩa 1 lần phương thức lấy tên của đối tượng, vậy là tất cả các lớp con kế thừa nó sẽ tự động sở hữu phương thức này.

Ngoài ra, việc có 1 lớp cơ bản như QObject là rất cần thiết để thực hiện cơ chế sử dụng tín hiệu (signal) và slot mà chúng ta sẽ tìm hiểu trong bài học sau. Cơ chế này cho phép chúng ta gửi các tín hiệu giữa các đối tượng khác nhau.

Dành cho các bạn vẫn còn thấy mơ hồ, tôi có 1 sơ đồ mô tả việc thừa kế trong Qt có thể giúp các bạn có cái nhìn tổng thể hơn.

Trên đây chỉ là ví dụ 1 vài lớp. Chắc chắn là tôi không thể liệt kê tất cả ra đây nếu không thì chúng ta cần phải có 1 sơ đồ khổng lồ mới đủ.

Theo đó, chúng ta nhận thấy QObject là lớp mẹ được thừa kế bởi hầu hết các lớp khác. Như tôi đã nói, nó chứa 1 số tính năng mà tất cả các lớp đều cần.

1 số lớp khác như QSound, dùng để quản lý âm thanh, thì kế thừa trực tiếp từ lớp này.

Chúng ta ở đây thì chỉ quan tâm chủ yếu đến các thành phần đồ họa, hay chính là các widget. Lớp QWidget chính là lớp cơ bản của tất cả các widget và đây cũng là 1 lớp kế thừa trực tiếp từ QObject. Vì là lớp cơ bản, QWidget chứa nhiều thuộc tính chung được sử dụng bởi bất cứ widget nào.

  • Chiều dài
  • Chiều cao
  • Vị trí
  • Phông chữ được sử dụng
  • Con trỏ của chuột
  • vv…

Dựa vào tính chất của tính kế thừa, chúng ta sẽ chỉ phải định nghĩa những thuộc tính này 1 lần và tất cả các widget đều sẽ sở hữu chúng.

Các lớp trừu tượng

Các bạn có thể nhận thấy rằng trong sơ đồ của tôi thì lớp QAbstractButton có màu đỏ.

Đây là 1 trong rất nhiều các lớp trừu tượng của Qt mà các bạn có thể nhận ra thông qua tên của chúng vì luôn luôn chứa chữ « abstract ».

Khái niệm lớp trừu tượng này đã từng được nhắc đến trong các bài học trước. Tuy nhiên, dành cho bạn nào không nhớ thì đây là các lớp mà chúng ta không thể thực thể hóa để tạo ra các đối tượng. Những câu lệnh như sau sẽ khiến trình biên dịch báo lỗi :

QAbstractButton nutBam; // Khong the vi la lop truu tuong

? Tại sao lại cần tạo ra 1 lớp mà không thể tạo ra đối tượng từ nó ?

Những lớp kiểu này chỉ có tác dụng làm nền tảng cho các lớp con của nó. Ví dụ như QAbstractButton thì mang theo các tính chất chung của các widget loại nút bấm như :

  • text : văn bản hiển thị trên nút
  • icon : hình ảnh biểu tượng cạnh văn bản trên nút
  • down : nút có được ấn hay không
  • vv…

Và thế là rồi các lớp như QPushButton hay QCheckbox đều sẽ sở hữu các tính chất đó.

? Nếu như thế thì có phải là các lớp QObject và QWidget cũng có thể là các lớp trừu tượng vì chúng cũng chỉ được dùng làm các lớp nền tảng ?

Hoàn toàn đúng ! Tuy nhiên vì 1 số lý do thực tiễn nên những người xây dựng Qt không để chúng trở thành các lớp trừu tượng và chúng ta vẫn có thể thực thể hóa chúng.

Nhân tiện, theo các bạn, nếu chúng ta hiển thị 1 đối tượng QWidget thì chúng ta sẽ nhận được gì ? Chính là 1 cửa sổ ! Thực ra với Qt thì 1 widget mà không nằm trong 1 widget khác thì được coi như 1 cửa sổ. Điều này giải thích cho việc khi không biết thông tin rõ ràng về đối tượng widget, Qt sẽ quyết định ra 1 cửa sổ.

Widget chứa 1 widget khác

Chúng ta sẽ làm quen với 1 khái niệm quan trọng nhưng cũng khá đơn giản, đó là widget chứa hay widget công ten nơ.

Chứa và bị chứa

Các bạn cần biết là 1 widget có thể chứa trong nó 1 widget khác. Ví dụ như 1 cửa sổ QWidget có thể chứa các nút bấm QPushButton, ô chọn QCheckBox và thanh chạy tiến trình QProgressBar, vv…

Không có sự kế thừa nào ở đây hết, chỉ là khái niệm về vật chứa và vật bị chứa.

Trong hình thì chúng ta có thể thấy được 3 widget : 2 nút bấm thông thường và 1 vùng chứa được chia thành các tab.

Vùng chứa với các tab chính là 1 dạng widget chứa. Bản thân nó chứa bên trong các widget khác : 2 nút bấm, 1 ô chọn và 1 thanh chạy tiến trình.

Cứ thế, các widget sẽ được lồng vào nhau theo trình tự sau :

  • QWidget (cửa sổ)
    • QPushButton
    • QPushButton
    • QTabWidget (vùng chứa gồm các tab)
      • QPushButton
      • QPushButton
      • QCheckBox
      • QProgressBar

! Chú ý đừng nhầm lẫn giữa khái niệm chứa đựng và sự kế thừa. Ở đây chúng ta đang tìm hiểu việc 1 widget có thể chứa đựng 1 hay nhiều widget khác mà không liên qua đến việc chúng có phải là lớp mẹ với lớp con cháu của nhau không.

Tạo cửa sổ chứa 1 nút bấm

Chúng ta sẽ không ngay lập tức thử tạo ra 1 cửa sổ « phức tạp » như trên mà bắt đầu bằng 1 cửa sổ đơn giản với chỉ 1 chiếc nút.

? Chẳng phải chúng ta đã làm thế ở bên trên rồi sao ?

Hoàn toàn không giống nhau ! Trong phần bên trên, chúng ta chỉ hiển thị ra 1 chiếc nút. Chính Qt đã bổ sung thêm việc tạo ra 1 cửa sổ bởi chiếc nút của chúng ta không thể lơ lửng trên màn hình.

Bây giờ, chúng ta sẽ thực hiện chính quy hơn, đó là tạo ra cửa sổ trước rồi mới thêm chiếc nút. So với cách thức lúc trước chúng ta tạo ra chiếc nút, tiến hành theo trình tự mới này có hơn 1 số ưu điểm.

  • Ngoài chiếc nút ban đầu, chúng ta có thể thêm các widget khác vào trong cửa sổ.
  • Chúng ta có thể cố định vị trí cũng như kích thước chiếc nút. Lúc trước thì chiếc nút luôn có cùng kích thước với cửa sổ.

Đây là đoạn mã ví dụ :

#include <QApplication>
#include <QPushButton> 
int main(int argc, char *argv[]){
    QApplication app(argc, argv); 

    // Tao ra cua so
    QWidget cuaSo;
    cuaSo.setFixedSize(300, 150);

    // Tao ra nut bam nam ben trong cuaSo
    QPushButton nutBam("Nút khẩn cấp !", &cuaSo);
    nutBam.setFont(QFont("Comic Sans MS", 14));
    nutBam.setCursor(Qt::PointingHandCursor);

    cuaSo.show(); 

    return app.exec();
}

… và kết quả chúng ta nhận được.

Tiến trình xử lý của chương trình trên diễn ra như sau :

  1. Tạo ra 1 cửa sổ nhờ đối tượng QWidget.
  2. Thay đổi kích thước cửa sổ với phương thức setFixedSize(). Sau đây, kích thước cửa sổ sẽ không thay đổi được nữa.
  3. Tạo ra 1 nút bấm nhưng với phương thức khởi tạo khác với lần trước. Lần này phương thức tạo cần thêm 1 tham số là con trỏ trỏ về widget chứa chiếc nút (tức là cửa sổ mà chúng ta vừa tạo ra).
  4. Thay đổi 1 số tính chất của chiếc nút.
  5. Hiển thị cửa sổ và cả chiếc nút chứa trong nó.

Tất cả các widget đều chứa 1 phương thức tạo cho phép chỉ ra widget sẽ chứa đựng nó thông qua 1 tham số con trỏ. Trong ví dụ thì &cuaSo là tham số để xác định là cửa sổ vừa tạo sẽ là nơi chứa chiếc nút.

QPushButton nutBam("Nút khẩn cấp !", &cuaSo);

Nếu các bạn muốn đổi chỗ của chiếc nút trong cửa sổ, có thể sử dụng đến phương thức move().

nutBam.move(60, 50);

Ngoài ra các bạn cũng có thể sử dụng phương thức setGeometry() để xác định vị trí cũng như kích thước mong muốn cho chiếc nút. Phương thức này sẽ nhận vào 4 tham số là hoành độ, tung độ, chiều dài và chiều cao chiếc nút.

Tất cả các widget đều có thể chứa widget khác

Thậm chí điều này đúng cả với những chiếc nút.

Tất cả các widget đều có 1 phương thức khởi tạo nhận tham số là đối tượng chứa nó. Nếu chúng ta muốn chứa 1 chiếc nút trong 1 chiếc nút khác, chỉ cần truyền con trỏ tới chiếc nút chứa cho phương thức khởi tạo của chiếc nút thứ bị chứa là đủ.

#include <QApplication>
#include <QPushButton> 
int main(int argc, char *argv[]){
    QApplication app(argc, argv);
    QWidget cuaSo;
    cuaSo.setFixedSize(300, 150); 

    QPushButton nutBam1("Nút khẩn cấp !", &cuaSo);
    nutBam1.setFont(QFont("Comic Sans MS", 14));
    nutBam1.setCursor(Qt::PointingHandCursor);
    nutBam1.setGeometry(60, 50, 180, 70);
    // Tao chiec nut thu 2 bi chua trong chiec nut thu 1
    QPushButton nutBam2("Nút khác", &nutBam1);
    nutBam2.move(30, 15); 

    cuaSo.show(); 

    return app.exec();
}

Và kết quả trông khá là kỳ dị.

Điều này chứng tỏ rằng mỗi widget đều có khả năng chứa các widget khác dù là việc này không phải lúc nào cũng cho ra 1 kết quả dễ nhìn. Cái quan trọng các bạn cần là ghi nhớ rằng Qt khá linh động và cho phép chúng ta làm thế.

Những dòng lệnh include

Trong đoạn mã bên trên, chúng ta đã sử dụng khá nhiều lớp đối tượng như QWidget, QFontQIcon. Theo lẽ thường thì chúng ta sẽ cần phải bao gồm tệp tiêu đề của những lớp đó vào đầu tệp mã nguồn để trình biên dịch có thể nhận biết chúng.

#include <QApplication>
#include <QPushButton>
#include <QWidget>
#include <QFont>
#include <QIcon>

? Vậy lúc trước, khi chúng ta không hề bao gồm lớp QWidget mà vẫn thoải mái thao tác với đối tượng của lớp này, tại sao trình biên dịch lại không báo lỗi ?

Thật ra chúng ta đã may mắn là lớp QPushButton là lớp kế thừa từ QWidget, vậy nên bản thân lớp này sẽ bao gồm QWidget trong tệp tiêu đề của nó.

Về phần các lớp QFontQIcon thì cũng được bao gồm do lớp QPushButton có gián tiếp sử dụng các đối tượng của các lớp này.

Tóm lại thì ở đây, chúng ta đã may mắn là đoạn mã hoạt động bình thường. Nếu cẩn thận hơn thì trong mỗi đoạn mã, chúng ta sẽ cần phải bao gồm tất cả các lớp mà chúng ta cần sử dụng.

Khi thao tác với 1 lượng lớn mã nguồn, việc chúng ta quên bao gồm 1 hay 2 lớp cũng là rất bình thường. 1 giải pháp chắc chắn cho những người hay quên là thay vì bao gồm từng lớp, các bạn có thể bao gồm tất cả các lớp có trong module QWidgets.

#include <QtWidgets>

Tệp tiêu đề của QWidgets bản thân nó sẽ bao gồm tất cả các lớp khác của module GUI như QWidget, QPushButton, QFont, vv…

Tuy nhiên cần chú ý là khi làm như thế, chúng ta có nguy cơ khiến trình biên dịch hoạt động chậm đi rất nhiều do phải bao gồm thêm cả những lớp không cần thiết.

Kế thừa 1 widget

Tóm lại, đến lúc này, chúng ta đã biết 1 số thứ về  các widget :

  • Đọc và thay đổi các thuộc tính của widget thông qua 1 số ví dụ các thuộc tính của các nút bấm.
  • Hiểu biết về cấu trúc của Qt và mối liên hệ kế thừa giữa các lớp.
  • Khái niệm về việc 1 widget được chứa trong 1 widget khác qua ví dụ về việc tạo 1 nút bấm bên trong 1 cửa sổ.

Trong phần tiếp theo này, chúng ta sẽ cùng đi sâu hơn vào tùy chỉnh các widget bằng cách tạo ra 1 loại widget mới.

Thực ra cũng không hẳn là 1 loại hoàn toàn mới mà chúng ta sẽ tạo ra 1 lớp kế thừa từ QWidget và dùng để mô tả cửa sổ của chúng ta. Có thể ban đầu các bạn sẽ thây là việc tạo ra 1 lớp của riêng để quản lý cửa sổ có vẻ khiến đoạn mã trở nên khá nặng nề, thế nhưng đây gần như là kỹ thuật chúng ta sẽ sử dụng với tất cả các thành phần của GUI vì nó cho phép tùy chỉnh các thành phần này ở mức độ lớn nhất có thể.

Sơ đồ dưới đây mô tả cái mà chúng ta muốn thực hiện.

Đương nhiên, mỗi khi tạo thêm 1 lớp, chúng ta sẽ cần thêm 2 tệp :

  • CuaSo.h : tệp định nghĩa của lớp
  • CuaSo.cpp : tệp chứa mã xử lý của các phương thức của lớp

CuaSo.h

Đây là mã ví dụ của tệp CuaSo.h

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

class CuaSo : public QWidget {// Ke thua lop QWidget
  public:
    CuaSo();

  private:
    QPushButton *m_nutBam;
};
#endif
#ifndef DEF_CUASO
#define DEF_CUASO
// Ma xu ly
#endif

Những dòng lệnh này thì các bạn đã quá quen thuộc rồi. Nó giúp cho trình biên dịch tránh phải bao gồm nhiều lần cùng 1 tệp tiêu đề và dễ dẫn đến lỗi.

#include <QApplication>
#include <QWidget>
#include <QPushButton>

Bởi vì chúng ta kế thừa từ lớp QWidget nên sẽ cần bao gồm định nghĩa của lớp này vào tệp làm việc. Tương tự, chúng ta cũng cần bao gồm QPushButton vì sẽ làm việc với các đối tượng của lớp này.

Về phần QApplication thì chúng ta sẽ có lúc dùng tới, tôi sẽ giải thích sau.

class CuaSo : public QWidget {// Ke thua lop QWidget

Nếu các bạn đã nắm vững về lập trình hướng đối tượng thì không khó để nhận ra cú pháp kế thừa, ở đây cho phép chúng ta kế thừa lớp QWidget. Và thế là đương nhiên lớp của chúng ta sẽ sở hữu tất cả các thuộc tính và phương thức của QWidget.

public:
  CuaSo();

private:
  QPushButton *m_nutBam;

Nội dụng của lớp khá đợn giản : nguyên mẫu của 1 phương thức khởi tạo và 1 thuộc tính m_nutBam. Chú ý là thuộc tính của chúng ta là 1 con trỏ nên sẽ cần phải sử dụng đến phép toán new để phân bổ động tạo ra đối tượng đó. Thuộc tính này sẽ có quyền truy cập private tuân thủ theo tính đóng gói.

CuaSo.cpp

Bởi vì chúng ta chỉ có 1 phương thức là phương thức khởi tạo vậy nên tệp .cpp cũng sẽ không quá dài.

#include "CuaSo.h"

CuaSo::CuaSo() : QWidget(){
    setFixedSize(300, 150);

    // Tao ra nut bam
    m_nutBam = new QPushButton("Nút khẩn cấp !", this);
    m_nutBam->setFont(QFont("Comic Sans MS", 14));
    m_nutBam->setCursor(Qt::PointingHandCursor);
    m_nutBam->move(60, 50);
}

Hầu như những câu lệnh trên đều đã được nhắc đến đâu đó trong phần trước của giáo trình nên các bạn sẽ không thấy lạ lẫm gì với chúng.

#include "CuaSo.h"

Đương nhiên việc bao gồm tên tiêu đề có chứa định nghĩa của lớp là bắt buộc.

CuaSo::CuaSo() : QWidget(){

Tên của lớp được viết trước tên của phương thức để giúp cho trình biên dịch phân biệt nếu có nhiều phương thức cùng tên. Tiếp theo đó là cú pháp của tính kế thừa. Trong 1 số trường hợp khác, chúng ta có thể truyền thêm tham số cho phương thức khởi tạo của lớp QWidget nhưng ở đây, chúng ta sẽ dùng phương thức khởi tạo mặc định.

setFixedSize(300, 150);

Câu lệnh chúng ta đã sử dụng ở trên để cố định kích thước của cửa sổ và không cho phép thay đổi. Khác biệt là chúng ta không cần viết cuaSo.setFixedSize(300, 150); như bên trên vì chúng ta đang nằm bên trong mã xử lý của lớp. Đây đơn giản là cách 1 lớp sử dụng 1 phương thức thuộc về nó (phương thức setFixedSize() được thừa kế từ QWidget).

Không cần quá lo lắng, các bạn sẽ nhanh chóng quen dần với cách viết này thôi vì tính thực dụng của nó quá lớn.

m_nutBam = new QPushButton("Nút khẩn cấp !", this);

Có thể coi là dòng lệnh quan trọng nhất trong các xử lý của phương thức này. Chính trong câu lệnh này chúng ta tạo ra chiếc nút bấm. Thực ra trong tệp tiêu đề thì chúng ta đã khai báo con trỏ nhưng không cho nó trỏ đến bất cứ đâu cả. Chính phép toán new ở đây đã gọi phương thức khởi tạo của QPushButton và trả về 1 địa chỉ gán cho con trỏ.

Chúng ta gặp lại từ khóa this mà tôi từng có dịp nhắc đến với các bạn trước đây. Đây là lúc chúng ta thấy được vai trò của nó trong thực tế.

Nếu các bạn còn nhớ thì tham số thứ 2 chúng ta truyền cho phương thức khởi tạo của 1 widget là 1 con trỏ hướng về widget bao chứa nó. Lúc trước ở trong main() thì mọi thứ khá dễ dàng, chúng ta chỉ việc truyền cho tham số này đối tượng cuaSo là được. Thế nhưng bây giờ chúng ta đang ở bên trong lớp CuaSo, vậy thì để cửa sổ có thể nói cho chiếc nút biết là nó chính là widget bao chứa nút, nó sẽ cần 1 con trỏ trỏ đến bản thân, chính là this.

main.cpp

Cuối cùng, chương trình nào cũng không thể chạy nếu thiếu hàm main(). Thú vị là thường thì chương trình càng lớn, khả năng là hàm main() của nó càng đơn giản cool.

#include <QApplication>
#include "CuaSo.h" 
int main(int argc, char *argv[]){
    QApplication app(argc, argv); 
    CuaSo cuaSo;
    cuaSo.show(); 
    return app.exec();
}

Chúng ta chỉ cần đơn giản bao gồm QApplicationCuaSo.h.

Mã xử lý thì chỉ gồm 2 câu lệnh tạo ra cửa sổ và hiển thị nó ra màn hình. Khi đối tượng cuaSo được tạo ra, phương thức khởi tạo của nó sẽ được gọi và trong đó sẽ tạo ra tất cả các widget được chứa bên trong nó (trong trường hợp này là chiếc nút bấm).

Tự động hủy các widget con nằm bên trong

? Chúng ta đã phân bổ động để tạo ra đối tượng QPushButton trong phương thức khởi tạo lớp CuaSo nhưng không hề giải phóng nó với phép toán delete ?

Đúng vậy, 1 đối tượng được tạo ra bởi new luôn cần phải được giải phóng bằng delete ở đâu đó. Bình thường thì chúng ta sẽ phải viết phương thức hủy của lớp CuaSo.

CuaSo::~CuaSo(){
    delete m_nutBam;
}

Tuy nhiên, Qt đã giúp chúng ta tự động xóa chiếc nút khi đối tượng cuaSo bị xóa ở cuối hàm main().

Trong thực tế, khi chúng ta xóa 1 đối tượng widget bao chứa, Qt sẽ tự động xóa tất cả các thành phần con chứa bên trong đối tượng đó. Đây là 1 trong những ích lợi của việc thông báo cho QPushButtonCuaSo chính là đối tượng bao chứa nó. Khi cửa sổ bị xóa thì chiếc nút cũng được Qt quản lý giải quyết luôn.

Nhờ tính năng này của Qt mà chúng ta không cần phải lo lắng bỏ quên các phép toán delete mỗi khi hủy 1 đối tượng.

Biên dịch

Đây là kết quả nhận được sau khi biên dịch thành công chương trình.

Vâng, tất cả những thứ lằng nhằng bên trên chúng ta đã thực hiện chỉ để nhận được 1 kết quả hoàn toàn tương tự như lúc trước. Nhưng thật ra thì không chỉ có vậy. Chúng ta đã tạo nên nền tảng cơ bản cho loại cửa sổ của riêng chúng ta. Việc này giúp cho chúng ta trong tương lai có thể dễ dàng thêm vào các widget nếu muốn cũng như quản lý phản ứng của những widget này với các tác động của người dùng.

Tất cả những thứ đó chúng ta sẽ cùng tìm hiểu trong bài học sau.

! Để luyện tập tí chút giãn gân giãn cốt, các bạn có thể thử cải tiến 1 chút chương trình của chúng ta, ví dụ như them 1 phương thức khởi tạo nữa cho phép người dùng truyền vào tham số xác định kích thước cửa sổ, vv…

Tóm tắt bài học :
  • Trong cấu trúc mỗi lớp Qt đều có các phương thức truy cập cho phép chúng ta thao tác với các thuộc tính của lớp.
  • Tài liệu hướng dẫn của Qt cung cấp đầy đủ thông tin chúng ta cần biết về cách thao tác với các widget.
  • Qt sử dụng rất nhiều tính kế thừa trong cấu trúc của thư viện. Đa phần các lớp được kế thừa từ 1 lớp tên là QObject.
  • Trong Qt, chúng ta luôn luôn có thể chứa 1 widget bên trong 1 widget khác.
  • Để tùy chỉnh ở mức độ cao nhất, chúng ta nên tự tạo ra lớp riêng để quản lý từng widget. Các tính năng của lớp của chúng ta có thể có được bằng cách kế thừa các lớp cơ bản sẵn có.