Jendela pada chapter lalu menumpuk dua widget secara vertikal, tetapi kita belum menjawab pertanyaan yang muncul di setiap aplikasi nyata: widget demikian banyaknya, dari mana posisi dan ukurannya ditentukan? Jawaban Qt adalah sistem layout — objek yang mengatur geometri widget secara otomatis berdasarkan hubungan antar widget, bukan angka piksel yang kita tebak. Chapter ini mengenalkan widget inti yang paling sering dipakai, alasan koordinat absolut ditinggalkan, serta aturan pembagian ruang lewat stretch factor; program teruji di akhir chapter menghitung distribusi lebar kolom sehingga matematika di balik layout bisa kita periksa angka per angka.
Widget Umum yang Sering Dipakai #
Semua yang tampak di jendela Qt adalah subclass QWidget, dan aplikasi nyata hampir selalu dibangun dari segelintir widget inti berikut.
| Widget | Peran | Sinyal yang sering dihubungkan |
|---|---|---|
| QLabel | menampilkan teks atau gambar (pasif) | tidak ada — tidak menerima input |
| QPushButton | memicu satu aksi | clicked |
| QLineEdit | input teks satu baris | textChanged, returnPressed |
| QListWidget | daftar item yang bisa dipilih | currentRowChanged, itemClicked |
Empat widget itu cukup untuk membangun form pencarian, panel konfigurasi, hingga daftar hasil; bila kebutuhan lebih spesifik muncul, Qt menyediakan turunannya — QSpinBox untuk angka, QCheckBox untuk pilihan on/off, QTableWidget untuk tabel. Sketsa berikut merangkai kelimanya dalam satu hierarki layout; perhatikan bahwa widget tidak pernah diberi posisi, cukup diserahkan ke layout.
auto* jendela = new QWidget; // wadah terluar
auto* kolom = new QVBoxLayout(jendela); // layout utama: vertikal
auto* barisAtas = new QHBoxLayout; // sub-layout: horizontal
barisAtas->addWidget(new QLineEdit); // kolom input: meregang
barisAtas->addWidget(new QPushButton("Cari")); // tombol: pas ukuran
kolom->addLayout(barisAtas); // sub-layout masuk kolom
auto* grid = new QGridLayout; // sel baris x kolom
grid->addWidget(new QLabel("Nama"), 0, 0);
grid->addWidget(new QLineEdit, 0, 1);
grid->addWidget(new QLabel("Stok"), 1, 0);
grid->addWidget(new QLineEdit, 1, 1);
kolom->addLayout(grid);
auto* daftar = new QListWidget; // mengisi sisa ruang
daftar->addItem("Aplikasi Desktop dengan C++");
kolom->addWidget(daftar);Tiga jenis layout terwakili di sana: QVBoxLayout menumpuk widget vertikal, QHBoxLayout berderet horizontal, dan QGridLayout menempatkan widget pada sel baris-kolom. Layout boleh bersarang — kolom utama berisi baris horizontal, grid, dan widget lain — sehingga struktur UI terbaca seperti struktur kode. addWidget() dan addLayout() sekaligus memindahkan kepemilikan widget ke layout dan pohon QObject parent-nya, meneruskan aturan ownership dari chapter sebelumnya.
Kenapa Layout, Bukan Koordinat Absolut #
Cara paling naif menata widget adalah memberi geometri piksel langsung — pada Qt, setGeometry(20, 40, 200, 30). Rencana itu pecah di empat situasi yang pasti dialami aplikasi nyata:
- pengguna mengubah ukuran jendela, sementara widget absolut diam di tempatnya;
- ukuran font berbeda antar sistem, sehingga teks terpotong di kotak yang terlalu kecil;
- terjemahan bahasa mengubah panjang label, dan widget bergeser menabrak widget lain;
- skala DPI antar layar berbeda, sehingga jarak piksel tidak bermakna sama.
Layout membalik kerjanya: kita menyatakan hubungan antar widget — di bawah, di samping, mengisi sisa ruang — dan layout yang menghitung geometri aktual setiap kali jendela berubah, memakai petunjuk ukuran dari setiap widget (sizeHint() dan minimumSizeHint()). Satu perhitungan menyelesaikan keempat masalah di atas sekaligus, karena hasilnya selalu dihitung ulang dari kondisi terkini.
Stretch Factor: Siapa Mendapat Sisa Ruang #
Widget jarang tepat memenuhi jendela, sehingga layout selalu punya pertanyaan tersisa: ruang berlebih diberikan kepada siapa? Jawabannya stretch factor — angka bobot yang menyatakan kehausan ruang. Widget dengan stretch 0 hanya menerima ukuran minimalnya; sisa ruang dibagi proporsional antar widget beregistrasi stretch, dan widget dengan stretch 3 menerima tiga kali porsi widget stretch 1. Pola paling umum terlihat di sketsa di atas: QLineEdit pencarian dengan stretch bawaan 1 di samping tombol stretch 0 — kotak teks meregang, tombol tetap pas.
Program Utama: Menghitung Distribusi Lebar #
Perhitungan layout bisa kita buka tutupnya tanpa Qt. Aturan ringkasnya tiga langkah: cicih total lebar minimal dan total spasi dari lebar jendela; bagi sisanya proporsional terhadap stretch factor; lalu bagikan sisa pembagian bulat satu piksel per kolom dari kiri agar totalnya pas. Program berikut menerapkan aturan itu pada tabel tiga kolom — Nama (stretch 3), Harga (stretch 1), Stok (stretch 1) — di dua lebar jendela, 640 dan 400 piksel, dan mencetak hasilnya sebagai tabel.
#include <iomanip> // std::setw, std::left
#include <iostream>
#include <numeric> // std::accumulate
#include <string>
#include <vector>
// Satu kolom layout: nama, prioritas ruang (stretch factor), dan lebar
// minimal yang tidak boleh dilanggar.
struct Kolom {
std::string nama;
int stretch;
int minimal;
};
// Menghitung lebar akhir tiap kolom dengan aturan ringkas layout Qt:
// 1. total spasi antar kolom dan lebar minimal masing-masing kolom
// dicicih lebih dulu dari lebar jendela.
// 2. sisa ruang bebas dibagi proporsional terhadap stretch factor.
// 3. sisa pembagian bulat dibagikan 1 piksel per kolom dari kiri,
// sehingga total lebar selalu pas dengan lebar jendela.
// Prasyarat: lebarJendela >= total minimal + total spasi.
std::vector<int> hitungLebar(const std::vector<Kolom>& kolom,
int lebarJendela, int spasi) {
const int totalMinimal = std::accumulate(
kolom.begin(), kolom.end(), 0,
[](int total, const Kolom& k) { return total + k.minimal; });
const int totalStretch = std::accumulate(
kolom.begin(), kolom.end(), 0,
[](int total, const Kolom& k) { return total + k.stretch; });
const int totalSpasi = spasi * static_cast<int>(kolom.size() - 1);
const int ruangBebas = lebarJendela - totalMinimal - totalSpasi;
std::vector<int> lebar(kolom.size());
int terpakai = 0;
for (std::size_t i = 0; i < kolom.size(); ++i) {
lebar[i] = kolom[i].minimal + ruangBebas * kolom[i].stretch / totalStretch;
terpakai += lebar[i];
}
// Bagikan sisa pembagian bulat agar totalnya pas.
int sisa = lebarJendela - totalSpasi - terpakai;
std::size_t idx = 0;
while (sisa > 0) {
lebar[idx % kolom.size()] += 1;
--sisa;
++idx;
}
return lebar;
}
// Mencetak tabel lebar untuk satu skenario jendela.
void tampilkanTabel(const std::vector<Kolom>& kolom,
const std::vector<int>& lebar,
int lebarJendela, int spasi) {
std::cout << "Jendela " << lebarJendela
<< " px, spasi antarkolom " << spasi << " px\n";
std::cout << std::left
<< std::setw(10) << "Kolom"
<< std::setw(10) << "Stretch"
<< std::setw(10) << "Minimal"
<< "Lebar" << "\n";
int totalStretch = 0;
int totalMinimal = 0;
int totalLebar = 0;
for (std::size_t i = 0; i < kolom.size(); ++i) {
std::cout << std::left
<< std::setw(10) << kolom[i].nama
<< std::setw(10) << kolom[i].stretch
<< std::setw(10) << kolom[i].minimal
<< lebar[i] << "\n";
totalStretch += kolom[i].stretch;
totalMinimal += kolom[i].minimal;
totalLebar += lebar[i];
}
std::cout << std::left
<< std::setw(10) << "Total"
<< std::setw(10) << totalStretch
<< std::setw(10) << totalMinimal
<< totalLebar << "\n";
const int jumlahSpasi = spasi * static_cast<int>(kolom.size() - 1);
std::cout << "Cek: " << totalLebar << " + " << jumlahSpasi
<< " px spasi = " << lebarJendela << " px\n\n";
}
int main() {
const std::vector<Kolom> kolom = {
{"Nama", 3, 80}, // stretch 3: kolom ini paling haus ruang
{"Harga", 1, 60},
{"Stok", 1, 60},
};
const int spasi = 8;
tampilkanTabel(kolom, hitungLebar(kolom, 640, spasi), 640, spasi);
tampilkanTabel(kolom, hitungLebar(kolom, 400, spasi), 400, spasi);
return 0;
}Output yang Dihasilkan:
Jendela 640 px, spasi antarkolom 8 px
Kolom Stretch Minimal Lebar
Nama 3 80 335
Harga 1 60 145
Stok 1 60 144
Total 5 200 624
Cek: 624 + 16 px spasi = 640 px
Jendela 400 px, spasi antarkolom 8 px
Kolom Stretch Minimal Lebar
Nama 3 80 191
Harga 1 60 97
Stok 1 60 96
Total 5 200 384
Cek: 384 + 16 px spasi = 400 pxMari kita bedah baris demi baris:
- Baris 9-13 (
Kolom): satu kolom dinyatakan dengan tiga data — nama, stretch factor, dan lebar minimal yang tidak boleh dilanggar. Struktur ini meniru peransizeHint()danminimumSizeHint()widget. - Baris 24-26 (
totalMinimal):std::accumulatemenjumlahkan lebar minimal seluruh kolom — pada skenario kita, 80 + 60 + 60 = 200 px. - Baris 30-31 (
totalSpasidanruangBebas): tiga kolom punya dua celah spasi, 2 x 8 = 16 px; ruang bebas jendela 640 px menjadi 640 - 200 - 16 = 424 px. - Baris 35-38 (distribusi proporsional): setiap kolom menerima minimalnya plus porsi
ruangBebas * stretch / totalStretch— Nama 424 x 3 / 5 = 254 px, Harga dan Stok masing-masing 424 x 1 / 5 = 84 px (pembagian bulat). - Baris 41-47 (sisa pembagian bulat): 254 + 84 + 84 = 422, tersisa 2 px dari 424; loop membagikannya satu piksel per kolom dari kiri, sehingga total selalu pas tanpa pecahan yang menggantung.
- Baris 52-83 (
tampilkanTabel): mencetak tabel lebar plus baris cek yang menjumlahkan kolom dan spasi; hasilnya harus kembali tepat ke lebar jendela. - Baris 85-95 (
main): fungsihitungLebar()dipanggil dua kali dengan lebar jendela berbeda — pembagian yang sama diterapkan ulang pada kondisi baru, dantampilkanTabel()mencetak setiap hasilnya.
Angka pada output membuktikan dua sifat layout. Pertama, proporsi terjaga: kolom Nama menerima sekitar 60% ruang bebas di kedua skenario, sesuai stretch 3 dari total 5. Kedua, minimal tidak pernah dilanggar: pada jendela 400 px, Harga dan Stok menyusut ke 97 dan 96 px — jauh dari batas 60 px — dan bila jendela menyusut lagi hingga menembus total minimal, layout Qt nyata akan meminta jendela tetap pada ukuran minimumnya alih-alih memangkas widget. Catatan kejujuran: mesin layout Qt sesungguhnya lebih bernuansa (beberapa tahap perhitungan dengan sizeHint, minimumSizeHint, dan kebijakan per widget), tetapi model tiga langkah di atas menangkap aturan stretch-nya dengan setia.
Sampai di sini UI kita responsif secara geometri, tetapi semua pekerjaannya masih ringan. Aplikasi nyata harus membaca berkas besar, memanggil jaringan, atau menghitung — dan itulah topik chapter berikutnya: menjaga event loop tetap hidup dengan melempar kerja berat ke thread lain, lalu melaporkan hasilnya kembali ke UI.
Referensi: doc.qt.io — Layout Management , doc.qt.io — QWidget