< Lập trình tân binh | 3.7. Sắp xếp bố cục các widget

3.7. Sắp xếp bố cục các widget

Như các bạn đã biết, cửa sổ có thể chứa đủ loại widget khác nhau : các nút bấm, các trường văn bản hay các ô chọn, vv… Và thật sự, để sắp xếp các thành phần khác nhau 1 cách hợp lý nhất là cả 1 môn nghệ thuật cần nhiều thời gian để tập luyện. Thậm chí chúng ta nên tuân thủ theo các bước tiến hành nhất định nếu không muốn giao diện của chúng ta trở thành 1 bãi chiến trường hỗn loạn.

Vậy làm sao để bố trí hợp lý các widget ? Làm sao thay đổi kích thước cửa sổ cho hợp lý ? Làm sao khiến giao diện tự động tương thích với đủ loại màn hình với độ phân giải khác nhau ? Đó sẽ là những chủ đề chúng ta sẽ cùng bàn luận trong bài học này.

Để bắt đầu, các bạn cần biết là người ta thường chia ra 2 cách bố trí widget khác nhau :

  • Bố trí vị trí tuyệt đối : cách mà chúng ta vẫn làm trong những bài học trước, sử dụng các phương thức như setGeometry() hay move(), vv… Cách sắp xếp này rất chính xác vì chúng ta căn chuẩn đến từng điểm ảnh (pixel) để đặt ví trí cho các thành phần. Tuy nhiên cách này tồn tại 1 vài khuyết điểm mà chúng ta sẽ nhắc đến ở sau.
  • Bố trí vị trí tương đối : cách này linh hoạt hơn và được khuyến khích sử dụng bất cứ khi nào các bạn có thể. Đây là cách sắp xếp mà chúng ta sẽ tập trung nói tới trong bài học này.
Các khuyết điểm của sử dụng vị trí tuyệt đối

Chúng ta sẽ bắt đầu bằng việc thống nhất bắt đầu với cùng 1 đoạn mã nguồn nền tảng mà chúng ta sẽ sử dụng trong bài học này. Cùng với đó, chúng ta sẽ nhắc lại 1 chút về cách bố trí các widget sử dụng vị trí tuyệt đối – phương pháp mà chúng ta vẫn sử dụng trong các bài trước mà không quá đào sâu về nó.

Đoạn mã Qt khởi đầu

Trong các bài học trước, chúng ta đã tạo 1 dự án Qt bao gồm 3 tệp mã nguồn :

  • main.cpp : chứa hàm main, chịu trách nhiệm khởi động giao diện chính.
  • CuaSo.h : chứa định nghĩa của lớp CuaSo, là lớp được kế thừa QWidget.
  • CuaSo.cpp : chứa mã xử lý các phương thức như phương thức tạo, vv… của lớp CuaSo.

Đây là cấu trúc mà chúng ta sẽ sử dụng trong hầu hết các dự án Qt của chúng ta. Tuy vậy trong trường hợp này, chúng ta không cần sử dụng đến cấu trúc cồng kềnh như vậy. Vì thế tôi đề nghị chúng ta sẽ đơn giản hóa mọi thứ và quay về với những ví dụ như hồi đầu chúng ta tiếp xúc với Qt mà mã nguồn được chứa trong 1 tệp duy nhất : main.cpp.

Sau đây sẽ là đoạn mã nguồn nền tảng của chúng ta.

#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[]){
    QApplication app(argc, argv);
    QWidget cuaSo;
    QPushButton nutBam("Xin chào", &cuaSo);
    nutBam.move(70, 60);
    cuaSo.show();
    return app.exec();
}

Đơn giản ! Chương trình sẽ trả về cho chúng ta 1 cửa sổ trong đó chứa 1 nút bấm ở tạo độ điểm ảnh (70, 60).

Khuyết điểm của vị trí tuyệt đối

Trong đoạn mã trước đó, chúng ta đã cố định vị trí của chiếc nút với câu lệnh nutBam.move(70, 60);

Chiếc nút sẽ nắm ở vị trí chính xác 70 đơn vị điểm ảnh tính từ cạnh trái của cửa sổ và 60 đơn vị tính từ cạnh trên. Vấn đề là giao diện chúng ta đã tạo ra không hề linh hoạt chút nào, dễ thấy nhất khi chúng ta sử dụng con chuột để thu nhỏ kích thước cửa sổ. Vì chiếc nút cố định ở 1 chỗ, nếu kích thước cửa sổ quá nhỏ sẽ dẫn đến 1 phần hoặc toàn bộ chiếc nút bị che khuất.

? Tại sao không ngăn người dùng thay đổi kích thước cửa sổ bằng phương thức setFixedSize() sử dụng trong bài trước ?

Đương nhiên là các bạn có thể làm như vậy. Đó là phương pháp khá phổ biến được sử dụng bởi những lập trình viên hay bố cục các widget dùng vị trí cố định. Tuy nhiên thì người dùng luôn thích việc có thể thay đổi kích thước cửa sổ. Vậy nên đấy chỉ có thể coi như phương án tạm thời.

Thêm vào đó, không phải lúc nào giải pháp trên cũng có thể giải quyết được vấn đề của chúng ta. Hãy thử lấy ví dụ trường hợp độ phân giải của máy người dùng thấp hơn phân giải của máy bạn. Các bạn quyết định là chiếc nút sẽ cách lề trái 1200 đơn vị do độ phân giải màn hình của bạn khá lớn (1600x1200). Đáng tiếc là máy tính của người dùng chỉ có độ phân giải 1024x768, vậy thì nguời đó sẽ không bao giờ có thể thấy chiếc nút do không thể tăng kích thước cửa sổ lên nữa. Vậy là phần mềm của chúng ta trở nên vô dụng với người dùng như vậy.

? Vậy đồng ý là phương pháp trên khá là tệ, chúng ta có thể làm gì khác ?

Không, tôi không hề nói là phương pháp sử dụng vị trí tuyệt đối là « tệ » bởi vì đôi khi chúng ta thật sự cần căn chính xác vị trí của 1 số widget. Các bạn có thể áp dụng phương pháp này trong những dự án nhất định.

Tuy nhiên, nếu không phải trường hợp bắt buộc, tôi đề nghị chúng ta sẽ sử dụng 1 cách thức sắp xếp khác : sử dụng ví trí tương đối.

Sử dụng vị trí tương đối nghĩa là chúng ta sẽ sắp xếp 1 widget nhờ các vị trí tương đối với các widget khác, kiểu như « nút 2 nằm bên phải nút 1 và phía trên nút 3 » vậy.

Các vị trí tương đối này được Qt quản lý bởi các lớp (layout). Chú ý là không nên nhầm lẫn với các lớp (class) mà chúng ta hay nhắc đến trước đây. Các lớp này bản chất chính là các widget chứa đựng. Chúng chính là đối tượng làm việc chính của chúng ta trong bài học này.

Cấu trúc các lớp quản lý lớp >__<

Để sắp xếp hợp lý các widget của giao diện, chúng ta sử dụng đến các lớp Qt chuyên quản lý việc xếp đặt. Qt cung cấp đủ loại lớp : có lớp quản lý việc sắp xếp widget theo chiều ngang, lớp khác lại cho phép sắp xếp theo chiều dọc, có lớp lại sắp xếp các thứ theo dạng mắt lưới, vv… Để giúp các bạn có cái nhìn rõ ràng hơn, dưới đây là sơ đồ minh họa 1 phần các lớp Qt.

Trên đây mà mối quan hệ kế thừa giữa các lớp quản lý việc sắp xếp bố cục. Tất cả chúng đều là các lớp con cháu của QLayout.

Mối quan hệ như trong sơ đồ là khá phổ biến trong Qt. QLayout là 1 lớp trừu tượng và nếu như các bạn còn nhớ rõ thì chúng ta sẽ không thể thực thể hóa nó được. Nhiệm vụ của nó là tập hợp tất cả các thuộc tính và phương thức mà bất cứ lớp quản lý lớp nào cũng phải có. Đây sẽ là 1 ví dụ tuyệt vời cho những điều mà tôi nói lúc trước về công dụng của tính kế thừa của Qt.

Ngoại trừ QStackedLayout (dùng để quản lý widget nằm trên nhiều lớp giao diện), chúng ta sẽ đề cập tới tất cả các lớp được liệt kê trên đây trong bài học này. Về phần QStackedLayout, làm việc với nó khá là phức tạp nên thông thường chúng ta sử dụng những widget có sử dụng lớp này như QWizard, vv…

Sắp xếp hàng dọc hoăc hàng ngang

Hãy bắt đầu ngay vào cách sắp xếp dễ dàng nhất và các bạn sẽ hiểu rõ hơn tại sao chúng ta lại cần đến tất cả những thứ này.

Chúng ta sẽ nghiên cứu cụ thể 2 lớp :

  • QHBoxLayout
  • QVBoxLayout

Đây là 2 lớp kế thừa trực tiếp từ QBoxLayout. Chúng có rất nhiều điểm tương đồng với nhau. Tài liệu của Qt có nhắc đến chúng với thuật ngữ « lớp tiện ích » nghĩa là các lớp giúp chúng ta tạo ra mọi thứ nhanh hơn dù về bản chất gần như giống hệt lớp mẹ. Vì vậy, chúng ta sẽ chỉ sử dụng 2 lớp này mà không dùng tới lớp mẹ QBoxLayout.

Sắp xếp theo hàng ngang

Cách sử dụng 1 lớp sắp xếp có thể được chia làm 3 bước :

  • Tạo ra các widget
  • Tạo ra lớp sắp xếp và đặt các widget vào trong đó
  • Yêu cầu cửa sổ sử dụng lớp sắp xếp vừa được tạo ra

Tạo các widget

Để minh họa cho phần kiến thức này, chúng ta sẽ tạo ra 1 vài nút bấm sử dụng QPushButton.

QPushButton *nutBam1 = new QPushButton("Chào");
QPushButton * nutBam2 = new QPushButton("mừng");
QPushButton * nutBam3 = new QPushButton("Tân binh");

Như các bạn thấy thì ở trên tôi đã sử dụng kỹ thuật con trỏ. Chúng ta cũng có thể đạt được mục đích tương tự mà không dùng đến con trỏ.

QPushButton nutBam1("Chào");
QPushButton nutBam2("mừng");
QPushButton nutBam3("Tân binh");

Phương pháp sau này có vẻ dễ dàng hơn nhưng theo phần trình bày tiếp theo, các bạn sẽ nhận ra rằng làm việc với con trỏ thì mang đến nhiều tiện ích hơn.

Khác biệt giữa 2 đoạn mã trên là trong phiên bản 1 thì các biến của chúng ta là các con trỏ trong khi các biến của phiên bản 2 là các đối tượng QPushButton. Như đã nói, dành cho phần tiếp theo, tôi sẽ sử dụng phiên bản mã nguồn trước.

Vậy là chúng ta đã có 3 chiếc nút. Tuy nhiên nếu các bạn chú ý hơn 1 chút thì sẽ nhận ra là chúng ta đã không sử dụng tham số để thông báo cửa sổ sẽ chứa chiếc nút mà chúng ta tạo ra như vẫn hay làm từ trước tới giờ.

QPushButton *nutBam1 = new QPushButton("Xin", &cuaSo);

Tôi đã cố tình thay đổi đoạn mã này bởi vì chúng ta sẽ không xếp chiếc nút trực tiếp vào trong cửa sổ mà sẽ thông qua 1 lớp sắp xếp.

Tạo các lớp sắp xếp và bố trí các widget

Tạo ra lớp sắp xếp ngang rất đơn giản, chúng ta chỉ việc sử dụng phương thức khởi tạo không tham số của lớp này.

QHBoxLayout *lop = new QHBoxLayout();

Việc tiếp theo của chúng ta là xếp những chiếc nút vào đó.

lop->addWidget(nutBam1);
lop->addWidget(nutBam2);
lop->addWidget(nutBam3);

Phương thức addWidget() đòi hỏi tham số là 1 con trỏ trỏ tới widget mà chúng ta muốn đặt vào trong lớp. Đây chính là lý do mà lúc trước tôi đã chọn sử dụng con trỏ (nếu không mỗi lần đoạn mã của chúng ta sẽ phải viết như sau : lop->addWidget(&nutBam1);).

Yêu cầu cửa sổ sử dụng lớp

Việc cuối cùng cần làm là đặt lớp vào trong cửa sổ bằng cách yêu cầu cửa sổ sử dụng lớp này để bố trí widget.

cuaSo.setLayout(lop);

Phương thức setLayout() yêu cầu tham số là 1 con trỏ trỏ đến lớp bố trí. Vậy là bây giờ cửa sổ sẽ chứa các widget tuân theo sự bố trí sẵn của lớp. Trong trường hợp của chúng ta, các widget được xếp thành hàng ngang.

Tóm lại, đây sẽ là đoạn mã kết quả của chúng ta trong main.cpp.

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

    QPushButton *nutBam1 = new QPushButton("Chào");
    QPushButton * nutBam2 = new QPushButton("mừng");
    QPushButton * nutBam3 = new QPushButton("Tân binh");

    QHBoxLayout *lop = new QHBoxLayout();
    lop->addWidget(nutBam1);
    lop->addWidget(nutBam2);
    lop->addWidget(nutBam3);

    cuaSo.setLayout(lop);   
    cuaSo.show();
    return app.exec();
}

Như thường lệ, chúng ta cần thêm #include <QHBoxLayout> vào đầu tệp để có thể sử dụng được lớp này.

Và đây là kết quả chúng ta sẽ nhận được khi sử dụng lớp sắp xếp theo chiều ngang.

Các nút tự động sắp xếp thành hàng ngang. Lợi ích của kỹ thuật này là phản ứng của các nút bấm trước tác động thay đổi kích thước cửa sổ từ phía người dùng.

Chúng ta hãy thử tăng chiều ngang cửa sổ. Các nút sẽ tự động điều chỉnh thích hợp.

Khi tăng chiều dọc, các bạn sẽ nhận thấy là những chiếc nút được tự động sắp xếp ở giữa khung cửa sổ.

Các bạn cũng có thể thử giảm kích thước cửa sổ và nhận ra rằng cửa sổ không thể giảm nhỏ nữa khi kích thước của những chiếc nút đạt đến tối thiểu. Việc này dẫn đến hiệu quả là những chiếc nút không thể biến mất như trong trường hợp dùng cách bố trí tuyệt đối.

Nói tóm lại, các cửa sổ sẽ chứa các lớp sắp xếp giao diện còn các lớp này sẽ chiu trách nhiệm chứa các widget.

Tôi có 1 sơ đồ minh họa đơn giản tặng các bạn.

Đấy là cách QHBoxLayout sắp xếp các widget theo chiều ngang. Tiếp đây, chúng ta sẽ tiếp xúc ngay với QVBoxlayout dùng để sắp xếp widget theo chiều dọc. Lớp này gần như hoàn toàn giống với lớp trước.

Sắp xếp theo hàng dọc

Để sắp xếp theo chiều dọc, chúng ta chỉ cần thay đổi QHBoxLayout thành QVBoxLayout là đủ.

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

    QPushButton *nutBam1 = new QPushButton("Chào");
    QPushButton * nutBam2 = new QPushButton("mừng");
    QPushButton * nutBam3 = new QPushButton("Tân binh");

    QVBoxLayout *lop = new QVBoxLayout();
    lop->addWidget(nutBam1);
    lop->addWidget(nutBam2);
    lop->addWidget(nutBam3);

    cuaSo.setLayout(lop);   
    cuaSo.show();
    return app.exec();
}

 Đừng quên bao gồm thêm QVBoxLayout vào đầu tệp.

Dưới đây là kết quả của đoạn mã trên.

Chúng ta đều thấy là những chiếc nút tự động điều chỉnh ứng với kích thước cửa sổ.

Tự động dọn dẹp các widget

? Bạn đã sử dụng new để phân bổ động mà không có lúc nào sử dụng delete để giải phóng bộ nhớ. Nếu các đối tượng bị xóa, liệu chúng có bị lưu lại trong bị nhớ ?

Tôi đã từng nói bới các bạn là Qt khá thông minh. Qt sẽ tự động gọi các phương thức hủy các lớp khi cửa sổ bị đóng và lớp sắp xếp bị hủy. Đấy là lúc Qt sử dụng lệnh delete.

Bây giờ, khi chúng ta đã quen dần với cách hoạt động của các lớp sắp xếp, chúng ta sẽ cùng nghiên cứu 1 kiểu lớp sắp xếp mãnh mẽ hơn nhưng cũng phức tạp hơn : QGridLayout.

Sắp xếp mắt lưới

Cách sắp xếp ngang và dọc khá dễ thao tác nhưng lại không cho phép chúng ta tạo ra những giao diện quá phức tạp. Và đấy là lúc chúng ta sẽ cần đến lớp sắp xếp lưới QGridLayout.

Các bạn có thể hình dung nó như 1 sự kết hợp của QHBoxLayoutQVBoxLayout, hình thành 1 dạng lưới với các dòng và các cột.

Sơ đồ lưới

Các bạn cần tưởng tượng cửa sổ giao diện của chúng ta là 1 lưới với vô hạn dòng và vô hạn cột.

 

Nếu muốn đặt 1 widget vào góc trên bên trái của giao diện, chúng ta sẽ đặt nó vào ô (0, 0), dưới 1 chút thì là (1, 0), vv…

Ứng dụng cơ bản của sắp xếp lưới

Hãy bắt đầu bằng những ứng dụng cơ bản của QGridLayout. Chúng ta sẽ thử dần từ những thứ đơn giản đến những thứ phức tạp.

Trong ví dụ dưới đây, chúng ta sẽ đặt 1 chiếc nút ở góc trên bên trái. Chiếc nút thứ 2 ở bên phải và nút thứ 3 sẽ ở bên dưới nút thứ 1. Tất cả thay đổi của chúng ta sẽ nằm trong phương thức addWidget(). Phương thức này có thể nhận thêm 2 thông số xác định tọa độ cho widget.

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

    QPushButton *nutBam1 = new QPushButton("Chào");
    QPushButton * nutBam2 = new QPushButton("mừng");
    QPushButton * nutBam3 = new QPushButton("Tân binh");

    QGridLayout *lop = new QGridLayout();
    lop->addWidget(nutBam1, 0, 0);
    lop->addWidget(nutBam2, 0, 1);
    lop->addWidget(nutBam3, 1, 0);

    cuaSo.setLayout(lop);   
    cuaSo.show();
    return app.exec();
}

Nếu các bạn đối chiếu với sơ đồ mà tôi đã cung cấp bên trên thì sẽ nhận ra các nút đều được đặt đúng tọa độ.

? Kích thước của lưới là vô hạn, vậy làm thể nào tôi có thể xếp 1 widget vào mắt lưới góc dưới bên phải đây ?

Qt tự nhận biết được widget nào nên được đặt ở góc dưới bên phải, đó là widget có các chỉ số tọa độ lớn nhất. Hãy kiểm nghiệm thử bằng cách thêm 1 nút bấm vào tọa độ (1, 1).

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

    QPushButton *nutBam1 = new QPushButton("Chào");
    QPushButton * nutBam2 = new QPushButton("mừng");
    QPushButton * nutBam3 = new QPushButton("Tân binh");
    QPushButton * nutBam4 = new QPushButton("^__^");

    QGridLayout *lop = new QGridLayout();
    lop->addWidget(nutBam1, 0, 0);
    lop->addWidget(nutBam2, 0, 1);
    lop->addWidget(nutBam3, 1, 0);
    lop->addWidget(nutBam4, 1, 1);

    cuaSo.setLayout(lop);   
    cuaSo.show();
    return app.exec();
}

Nếu muốn, các bạn thậm chí có thể đẩy nó thêm nữa về bên dưới bên phải, tách ra thành 1 dòng với 1 cột mới.

lop->addWidget(nutBam4, 2, 2);

Widget trải dài trên nhiều ô

Lợi ích của việc sắp xếp lưới là đôi khi, chúng ta có thể quyết định việc 1 widget chiếm nhiều mắt lưới liên tiếp. Thuật ngữ chuyên môn hay dùng cho việc này là sự trải rộng (spanning). Những bạn nào đã từng tiếp xúc với HTML thì đã quá quen thuộc với khái niệm này qua các thuộc tính rowspancolspan của các bảng trong HTML.

Để thực hiện việc này, chúng ta sẽ dùng 1 phiên bản ghi đè khác của phương thức addWidget(), cho phép cung cấp thêm 2 tham số rowSpan columnSpan.

  • rowSpan : số dòng mà widget sẽ sử dụng để hiển thị (mặc định là 1)
  • columnSpan : số cột mà widget sẽ sử dụng để hiển thị (mặc định là 1)

Hãy hình dung 1 widget được đặt ở góc trên bên trái, tọa độ (0, 0). Nếu chúng ta đưa cho nó giá trị rowSpan là 2, không gian mà nó chiếm dụng sẽ giống như trong hình vẽ sau.

Nếu thay vào đó chúng ta cung cấp giá trị columnSpan là 3 cho widget đó, chúng ta sẽ nhận được :

! Đến cuối cùng, không gian bị chiếm bởi 1 widget sẽ phụ thuộc vào bản chấn của widget đó và kích thước của lưới. Các bạn sẽ nhanh chóng quen với cơ chế hoạt động này qua việc thực hành nhiều hơn.

Trong ví dụ dưới đây, chúng ta hãy thử cho chiều rộng của nút “Tân binh” chiếm cứ 2 cột của cửa sổ.

lop->addWidget(nutBam3, 1, 0, 1, 2);

2 tham số cuối cùng lần lượt là rowSpancolumnSpan. Giá trị của rowSpan là mặc định nhưng của columnSpan thì đã tang thành 2. Vậy nên kết quả chúng ta nhận được sẽ có khác với bên trên.

! Hãy thử với columnSpan = 3 và các bạn sẽ thấy… không có gì khác biệt ! Đây chính là điều mà tôi đã nhắc đến trong chú ý trước. Chúng ta sẽ cần phải có thêm 1 widget nữa trên hàng thứ nhất nếu muốn giá trị columnSpan này có tác dụng.

Tôi nghĩ các bạn đừng nên ngại thử nghiệm nhiều lần để hiểu rõ cách thức hoạt động của sự trải rộng trong thiết kế giao diện Qt. Chậm mà chắc vẫn tốt hơn.

Sắp xếp đơn mẫu

Lớp sắp xếp QFormLayout là 1 lớp sắp xếp đặc biệt dành riêng để thiết kế các mẫu đơn điện tử.

Chúng ta có thể hiểu đơn giản 1 mẫu đơn là 1 chuỗi liên tiếp các trường với tên của trường và khung giá trị của trường tương ứng.

Thông thường, để viết chữ vào trong giao diện, chúng ta sử dụng đến widget QLabel để tạo ra các nhãn.

Lợi ích của lớp QFormLayout mà chúng ta sẽ sử dụng đó là nó sẽ tự động tạo ra các QLabel cho chúng ta.

! Hãy chú ý rằng việc sắp xếp của QFormLayout chẳng khác gì so với 1 QGridLayout với 2 cột và vô số dòng. Trong thực tế, QFormLayout vốn chính là 1 phiên bản đặc biệt của QGridLayout nhưng thích ứng tốt hơn với các hệ điều hành khác nhau.

Cách sử dụng QFormLayout rất đơn giản. Khác biệt ở đây là chúng ta không sử dụng addWidget() mà sẽ sử dụng phương thức addRow() cần 2 tham số.

  • Văn bản in trên nhãn.
  • Con trỏ trỏ tới trường của đơn mẫu

Để đơn giản, chúng ta sẽ thử tạo ra đơn mẫu với 3 trường đều được nhập giá trị nhờ QLineEdit. Sau đó, chúng ta sẽ thêm nó vào đơn mẫu nhờ addRow().

#include <QApplication>
#include <QLineEdit>
#include <QFormLayout>
int main(int argc, char *argv[]){
    QApplication app(argc, argv);
    QWidget cuaSo;

    QLineEdit *ho = new QLineEdit();
    QLineEdit *ten  = new QLineEdit();
    QLineEdit *tuoi = new QLineEdit();

    QFormLayout *lop = new QFormLayout();
    lop->addRow("Họ", ho);
    lop->addRow("Tên", ten);
    lop->addRow("Tuổi", tuoi);

    cuaSo.setLayout(lop);
    cuaSo.show();
    return app.exec();
}

Thật tuyệt chứ hả ?

Chúng ta thậm chí có thể định nghĩa những phím tắt tương ứng dẫn tới khung điền giá trị của các trường bằng các thêm vào dấu & trước chữ cái mà các bạn muốn biến thành phím tắt.

lop->addRow("&Họ", ho);
lop->addRow("&Tên", ten);
lop->addRow("T&uổi", tuoi);

Thế là chúng ta đã có các phím tắt tương ứng cho từng trường : “h” cho họ, “t” cho tên và “u” cho tuổi. Cách sử dụng các phím tắt này thay đổi tùy hệ điều hành. Trong Windows, chúng ta cần ấn Alt kết hợp với phím tắt để sử dụng nó. Nếu các bạn giữ phím Alt thì sẽ thấy các phím tắt được gạch chân như sau.

! Chúng ta rất hay gặp ký tự & dùng trong thiết kế giao diện để tạo ra các phím tắt. Thường hay gặp nhất là trên thanh công cụ. Vì thế, nếu các bạn muốn viết “&” trong 1 cái nhãn thì chúng ta bắt buộc phải thay thế bằng &&. Ví dụ “Người đẹp && Quái vật” sẽ hiển thị thành “Người đẹp & Quái vật” như ta mong muốn.

Kết hợp nhiều lớp sắp xếp

Trước khi kết thúc bài học này, chúng ta sẽ cùng tìm hiểu 1 kiến thức quan trọng cuối cùng về kết hợp các lớp, 1 tính năng mạnh mẽ cho phép thiết kế giao diện thiên biến vạn hóa.

Hãy bắt đầu bằng việc trả lời 1 câu hỏi.

? Các lớp sắp xếp có vẻ hay nhưng hơi bị hạn chế thì phải ? Làm sao có thể tạo ra các giao diện phức tạp chỉ với QVBoxLayout hay thậm chí là QGridLayout ?

Đúng là nếu chỉ có thể sắp xếp các widget chồng lên nhau hay dù là có thể xếp thành lưới thì giao diện thiết kế ra vẫn còn rất hạn chế. Nhưng các bạn không cần quá lo lắng, phép màu sẽ xuất hiện khi chúng ta bắt đầu biết kết hợp các lớp với nhau, nghĩa là đặt lớp này vào trong lớp khác.

Ví dụ cụ thể

Chúng ta hãy cùng nghiên cứu lại đơn mẫu đơn giản mà chúng ta đã tạo ra lúc trước. Thêm vào đó, chúng ta sẽ thêm 1 nút “Thoát” ở bên dưới mẫu đơn.

Trước tiên chúng ta phải tạo ra 1 QVBoxLayout và đặt vào đó mẫu đơn và rồi đến chiếc nút.

Chúng ta thấy là QVBoxLayout theo thứ tự chứa 2 thành phần :

  • 1 QFormLayout (chứa trong nó 1 vài widget khác)
  • 1 nút bấm QPushButton

Vậy là 1 lớp hoàn toàn có thể chứa 1 lớp khác.

Sử dụng addLayout

Để lồng thêm 1 lớp vào bên trong lớp khác, chúng ta có thể sử dụng addLayout() thay vì addWidget().

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

    QLineEdit *ho = new QLineEdit();
    QLineEdit *ten  = new QLineEdit();
    QLineEdit *tuoi = new QLineEdit();

    QFormLayout *lop = new QFormLayout();
    lop->addRow("&Họ", ho);
    lop->addRow("&Tên", ten);
    lop->addRow("T&uổi", tuoi);

    QVBoxLayout *lopChinh = new QVBoxLayout();
    lopChinh->addLayout(lop);
    QPushButton *nutThoat = new QPushButton("Thoát");
    QWidget::connect(nutThoat, SIGNAL(clicked()), &app, SLOT(quit()));
    lopChinh->addWidget(nutThoat);

    cuaSo.setLayout(lopChinh);
    cuaSo.show();
    return app.exec();
}

Các bạn hãy để ý rằng tôi tạo ra mọi thứ từ trong ra ngoài : trước hết là tạo những widget và lớp ở trong rồi mới tạo ra lớp bên ngoài và thêm chúng vào đó. Và đây là kết quả cuối cùng chúng ta nhận được.

Mọi thứ không hiện ra rõ ràng nhưng chúng ta vẫn có thể hình dung ra được cấu trúc của giao diện.

Tóm tắt bài học :
  • Có 2 cách sắp xếp các widget trong giao diện : sử dụng vị trí tuyệt đối đo bằng đơn vị điểm ảnh hoặc sử dụng vị trí tương đối với các lớp sắp xếp.
  • Nên dùng cách sắp xếp sử dụng vị trí tương đối bất cứ lúc nào có thể. Lợi ích của phương pháp này là các widget tự động phân bố trong không gian giao diện và thích ứng với kích thước giao diện.
  • Có nhiều lớp sắp xếp khác nhau để tổ chức các widget : QVBoxLayout (xếp dọc), QHBoxLayout (xếp ngang), QGridLayout (xếp dạng lưới) và QFormLayout (dùng cho mẫu đơn).
  • Các lớp sắp xếp có thể long vào nhau, nghĩa là lớp này chứa bên trong lớp kia. Việc này giúp chúng ta sắp xếp các widget 1 cách chính xác nhất có thể nhưng không làm mất đi tính linh hoạt vốn có của phương pháp sử dụng ví trí tương đối.