Chapter sebelumnya berakhir pada fungsi exec(): antarmuka sudah tampil, antrean kejadian berjalan, dan kita menunggu pengguna bertindak. Pertanyaannya kemudian: saat tombol ditekan, bagaimana kejadian itu sampai ke kode kita? Qt menjawabnya dengan mekanisme yang menjadi identitas framework ini — signals dan slots. Chapter ini membedah jendela pertama, kontrak signal-slot, dan dua fondasi teknisnya (moc dan ownership QObject), lalu membuktikan inti mekanismenya dengan program mandiri tanpa Qt.

Jendela Pertama dengan QApplication dan QWidget #

Setiap program Qt berangkat dari dua objek. QApplication adalah satu objek per program yang memiliki event loop, hubungan dengan kejadian OS, dan pengaturan global aplikasi. QWidget adalah induk semua yang tampak — tombol, label, jendela utama — semuanya subclass dari class yang sama. Tiga langkah membentuk program GUI minimal: buat QApplication, tampilkan widget dengan show(), lalu serahkan kendali pada app.exec().

Sketsa berikut menambahkan satu langkah yang menjadi inti chapter ini: connect() menautkan sinyal clicked milik tombol ke fungsi simpan() milik jendela.

cpp Sketsa Qt — connect sinyal ke slot (ILUSTRATIF: Qt tidak terinstal di sandbox praktikum, bentuk API untuk proyek nyata)
class JendelaUtama : public QWidget {
    Q_OBJECT   // makro wajib agar moc menghasilkan kode meta-object

public:
    explicit JendelaUtama(QWidget* induk = nullptr) : QWidget(induk) {
        auto* kolom = new QVBoxLayout(this);
        tombol_ = new QPushButton("Simpan", this);  // this: parent widget
        label_  = new QLabel("Siap", this);
        kolom->addWidget(tombol_);
        kolom->addWidget(label_);

        // sinyal clicked -> slot simpan(): pemeriksaan saat kompilasi
        connect(tombol_, &QPushButton::clicked, this, &JendelaUtama::simpan);
    }

private slots:
    void simpan() { label_->setText("Dokumen tersimpan"); }

private:
    QPushButton* tombol_ = nullptr;
    QLabel* label_ = nullptr;
};

int main(int argc, char* argv[]) {
    QApplication app(argc, argv);
    JendelaUtama jendela;
    jendela.show();
    return app.exec();
}

Saat app.exec() berjalan, satu thread UI berputar mengambil kejadian. Klik pada tombol_ memancarkan sinyal clicked, dan connect() di constructor telah menautkan sinyal itu ke simpan() — label berganti teks tanpa satu baris pun dari kita yang memeriksa klik. Perhatikan pula Q_OBJECT dan private slots:: keduanya bukan C++ murni; pembahasannya menyusul di bagian moc.

Build system Qt mengurus langkah-langkah khas toolkit ini secara otomatis:

bash Sesi build Qt — ILUSTRATIF: perintah untuk proyek nyata, tidak dieksekusi di sandbox
cmake -B build -S .        # CMake mendeteksi Qt dan menyiapkan langkah moc
cmake --build build        # moc, kompilasi C++, link dalam satu perintah
./build/hello-qt           # jendela muncul; klik tombol mengubah label

Signals dan Slots: Kontrak Pengirim-Penerima #

Signal adalah pemberitahuan yang dipancarkan objek saat keadaannya berubah — clicked pada tombol, textChanged pada QLineEdit, valueChanged pada slider. Slot adalah member function biasa yang siap bereaksi. connect() menautkan keduanya tanpa mengikat class pengirim dan penerima secara langsung: tombol tidak tahu siapa yang mendengarkan sinyalnya, dan penerima tidak tahu dari mana sinyal datang.

Pemisahan itu disebut loose coupling, dan bayarannya terasa saat aplikasi berkembang: satu sinyal boleh terhubung ke banyak slot, satu slot boleh menerima sinyal dari banyak sumber, dan keduanya boleh berada di file yang berbeda tanpa include silang. Sejak Qt5, connect() memakai pointer ke member function sehingga ketidakcocokan sinyal dan slot tertangkap compiler — bukan saat program berjalan. Inti mekanismenya, bagaimanapun, sederhana: tabel sinyal berisi daftar fungsi yang harus dipanggil saat emit dieksekusi, dan tabel itu bisa kita bangun dengan pustaka standar.

Program Utama: Signals-Slots Mini #

Program berikut merekonstruksi inti signal-slot: class Sumber menyimpan registry slot per nama sinyal, fungsi hubungkan() mendaftarkan slot (analogi connect), dan fungsi emit() memanggil semuanya berurutan (analogi emit Qt). Dua penerima — StatusBar dan Logger — tidak saling mengenal dan tidak mengenal Sumber lebih dari kontrak std::function.

cpp
#include <functional>    // std::function
#include <iostream>
#include <map>           // std::map: tabel sinyal terurut, iterasi pasti
#include <string>
#include <utility>       // std::move
#include <vector>

// Sumber sinyal mini: menyimpan daftar slot per nama sinyal, lalu
// memanggil semuanya saat emit() dieksekusi. Sumber tidak tahu apa pun
// tentang penerima — inilah loose coupling antara pengirim dan penerima.
class Sumber {
public:
    using Slot = std::function<void(const std::string&)>;

    // Mendaftarkan slot pada satu sinyal; satu sinyal boleh punya banyak slot.
    void hubungkan(const std::string& namaSinyal, Slot slot) {
        tabel_[namaSinyal].push_back(std::move(slot));
    }

    // Analogi emit pada Qt: semua slot sinyal ini dipanggil berurutan.
    void emit(const std::string& namaSinyal, const std::string& pesan) {
        auto it = tabel_.find(namaSinyal);
        if (it == tabel_.end()) {
            return;  // sinyal tanpa slot: tidak terjadi apa pun
        }
        for (const Slot& slot : it->second) {
            slot(pesan);
        }
    }

private:
    std::map<std::string, std::vector<Slot>> tabel_;
};

// Penerima 1: layaknya widget status bar dengan slot tampilkan().
class StatusBar {
public:
    void tampilkan(const std::string& pesan) {
        std::cout << "[status bar] " << pesan << "\n";
    }
};

// Penerima 2: layaknya komponen logger dengan slot catat().
class Logger {
public:
    void catat(const std::string& pesan) {
        std::cout << "[log] aksi pengguna: \"" << pesan << "\"\n";
    }
};

int main() {
    Sumber tombol;
    StatusBar statusBar;
    Logger logger;

    // Satu sinyal dihubungkan ke dua slot milik dua objek berbeda.
    tombol.hubungkan("klik",
        [&statusBar](const std::string& pesan) { statusBar.tampilkan(pesan); });
    tombol.hubungkan("klik",
        [&logger](const std::string& pesan) { logger.catat(pesan); });

    std::cout << "emit pertama:\n";
    tombol.emit("klik", "Simpan Dokumen");

    std::cout << "emit kedua:\n";
    tombol.emit("klik", "Buka Berkas");

    std::cout << "emit sinyal tanpa slot:\n";
    tombol.emit("hover", "kursor di atas tombol");
    std::cout << "(sinyal hover tanpa slot: emit kembali tanpa efek)\n";
    return 0;
}

Output yang Dihasilkan:

text
emit pertama:
[status bar] Simpan Dokumen
[log] aksi pengguna: "Simpan Dokumen"
emit kedua:
[status bar] Buka Berkas
[log] aksi pengguna: "Buka Berkas"
emit sinyal tanpa slot:
(sinyal hover tanpa slot: emit kembali tanpa efek)

Mari kita bedah baris demi baris:

  1. Baris 13 (using Slot): kontrak slot dinyatakan satu kali — fungsi apa pun yang menerima const std::string& bisa menjadi slot. Pada Qt, kontrak ini diperiksa compiler lewat pointer member function.
  2. Baris 16-18 (hubungkan): slot disimpan ke tabel_[namaSinyal]; operator [] pada std::map otomatis membuat vektor kosong bila sinyal belum ada. Analogi connect().
  3. Baris 21-29 (emit): mencari daftar slot sinyal, lalu memanggil semuanya berurutan. Bila sinyal tidak punya slot sama sekali, fungsi langsung kembali — perhatikan perilaku ini pada baris terakhir output.
  4. Baris 32 (tabel_): seluruh mekanisme berdiri di atas satu std::map<std::string, std::vector<Slot>>. Kita memilih std::map (bukan unordered_map) agar urutan sinyal pasti, sesuai kebiasaan course saat mencetak isi container.
  5. Baris 36-49 (penerima): StatusBar dan Logger adalah class biasa tanpa pewarisan bersama — penerima sinyal tidak perlu induk class tertentu.
  6. Baris 57-60 (penghubungan): satu sinyal "klik" dihubungkan ke dua slot milik dua objek berbeda lewat lambda yang menangkap referensi. Tidak ada #include silang antar class penerima.
  7. Baris 63 dan 66 (emit): setiap pemanggilan emit("klik", ...) memicu dua slot berurutan — status bar lebih dulu, lalu log, sesuai urutan pendaftaran.
  8. Baris 69 (sinyal tanpa slot): sinyal "hover" tidak pernah dihubungkan; emit mencari daftarnya, menemukan akhir tabel, dan kembali tanpa efek. Program tetap jalan normal — pemancar tidak pernah bergantung pada keberadaan penerima.

Output mengonfirmasi kontraknya: dua slot terpanggil berurutan untuk setiap emit, dan sinyal tanpa penerima tidak menimbulkan galat. Pada Qt nyata, mekanisme yang sama ditambah tiga kemampuan yang belum kita tiru — pemeriksaan tipe saat kompilasi, koneksi lintas thread (queued connection, dibahas chapter Event dan Threading UI), dan pemutusan koneksi otomatis saat salah satu objek dihapus. Kemampuan ketiga itulah yang menuntut fondasi berikutnya: Qt tahu hubungan antar objek karena setiap QObject tercatat pada induknya.

Moc dan Ownership Pohon QObject #

Moc: Meta-Object Compiler #

Q_OBJECT bukan kata kunci C++, melainkan makro; begitu pula signals dan slots. Sintaks tambahan itu diproses oleh moc (Meta-Object Compiler): tool yang membaca header berisi Q_OBJECT, lalu menghasilkan file C++ lain (moc_*.cpp) berisi tabel metadata class — daftar sinyal dan slot beserta cara memanggilnya. Berkat tabel itu, connect() dapat memeriksa kecocokan sinyal-slot saat kompilasi dan emit dapat mengantar argumen lintas thread. Kita tidak pernah menjalankan moc manual: qmake dan CMake memanggilnya otomatis setiap build, sebagaimana compiler memanggil preprocessor.

Ownership: Pohon Parent-Anak #

Qt mengelola memori widget lewat pohon kepemilikan: setiap QObject menerima parent di constructor-nya, dan destructor parent menghapus semua anak secara otomatis. Itulah alasan sketsa di atas menulis new QPushButton("Simpan", this) — widget diletakkan di heap, tetapi this (jendela) menjadi parent-nya; ketika jendela dihapus, seluruh tombol dan label ikut dihapus tanpa satu pun delete dari kita. Pola ini menyambung dengan RAII yang kita kenal di Section Manajemen Memori: kepemilikan diserahkan ke objek yang masa hidupnya paling panjang, dan pembersihan tetap otomatis — hanya saja penjaganya adalah pohon QObject, bukan unique_ptr.

Jendela, sinyal, dan slot sudah lengkap; tinggal satu pertanyaan klasik aplikasi desktop — widget sebanyak itu diletakkan di mana dan berapa ukurannya? Chapter berikutnya menjawab lewat sistem layout, dan kita akan menghitung sendiri matematika pembagian ruangnya sampai ke sisa piksel terakhir.

Referensi: doc.qt.io — Signals & Slots , doc.qt.io — QObject , doc.qt.io — moc