Mechanizm sygnałów i slotów w Qt to implementacja wzorca Observer dostarczona przez framework. Opiera się na metaprogramowaniu i funkcjach wirtualnych, a przy tym sam pilnuje czasu życia połączeń i sprawdza zgodność typów.

Różnice wobec ręcznego Observera

Aspekt Observer ręczny Qt Signals/Slots
Zarządzanie czasem życia Ręczne; ryzyko wiszących wskaźników Automatyczne; destruktor przerywa połączenia
Type safety Sprawdzanie w czasie działania Sprawdzanie w kompilacji (Qt 6.0+)
Deklaracja Funkcja wirtualna i kod szablonowy Słowa kluczowe signals: i slots:
Składnia połączenia attach(observer*) connect(sender, &Class::signal, receiver, &Class::slot)
Rozłączenie Wymaga wskaźnika do obserwatora disconnect() lub automatycznie przy niszczeniu
Elastyczność Każdy typ obserwatora Tylko klasy dziedziczące po QObject
Narzut callbacku Minimalny (vtable) Umiarkowany (string-based w Qt 5, kompilacja w Qt 6)

Jak działają sygnały i sloty

Sygnał deklaruje zdarzenie, czyli ogłasza, że coś się stało. Slot to funkcja, która na niego odpowiada. connect łączy jedno z drugim. Gdy sygnał zostanie wyemitowany, Qt wywołuje wszystkie połączone sloty.

Mermaid diagram

Emisja button.clicked() wywołuje wszystkie połączone sloty.

Kod (styl Qt 5)

#include <QObject>
#include <QApplication>
#include <iostream>

// ── Model (Subject/Emitter) ────────────────────────────────
class DataModel : public QObject {
    Q_OBJECT

private:
    int value_ = 0;

public:
    int get_value() const { return value_; }

    void set_value(int new_value) {
        if (value_ != new_value) {
            value_ = new_value;
            emit valueChanged(new_value);  // Sygnał emitowany tutaj
        }
    }

signals:  // Tylko deklaracja; bez implementacji
    void valueChanged(int new_value);
};

// ── View 1 (Observer) ──────────────────────────────────────
class DisplayWindow : public QObject {
    Q_OBJECT

public slots:  // Może być wywoływany z sygnałów
    void on_value_changed(int value) {
        std::cout << "[Display] Wartość: " << value << "\n";
    }
};

// ── View 2 (Observer) ──────────────────────────────────────
class LoggingWindow : public QObject {
    Q_OBJECT

public slots:
    void on_value_changed(int value) {
        std::cout << "[Logger] Wartość: " << value << "\n";
    }
};

// ── Alert System (Observer z parametrami) ─────────────────
class AlertWindow : public QObject {
    Q_OBJECT

private:
    int threshold_;

public:
    explicit AlertWindow(int threshold) : threshold_(threshold) {}

public slots:
    void on_value_changed(int value) {
        if (value > threshold_) {
            std::cout << "[Alert] OSTRZEŻENIE: " << value 
                      << " przekracza " << threshold_ << "\n";
        }
    }
};

// ── Użycie ────────────────────────────────────────────────
int main(int argc, char* argv[]) {
    QApplication app(argc, argv);

    DataModel model;
    DisplayWindow display;
    LoggingWindow logger;
    AlertWindow alert(50);

    // Połącz sygnały ze slotami
    // nadawca, &Class::signal, odbiornik, &Class::slot
    QObject::connect(&model, &DataModel::valueChanged, 
                     &display, &DisplayWindow::on_value_changed);

    QObject::connect(&model, &DataModel::valueChanged, 
                     &logger, &LoggingWindow::on_value_changed);

    QObject::connect(&model, &DataModel::valueChanged, 
                     &alert, &AlertWindow::on_value_changed);

    std::cout << "Połączono 3 sloty\n\n";

    std::cout << "Ustawianie wartości na 10:\n";
    model.set_value(10);

    std::cout << "\nUstawianie wartości na 60:\n";
    model.set_value(60);

    std::cout << "\nRozłączanie loggera:\n";
    QObject::disconnect(&model, &DataModel::valueChanged,
                        &logger, &LoggingWindow::on_value_changed);

    std::cout << "Ustawianie wartości na 20:\n";
    model.set_value(20);

    return 0;  // Obiekty niszczone; wszystkie połączenia oczyszczane automatycznie
}

Wyjście

Połączono 3 sloty

Ustawianie wartości na 10:
[Display] Wartość: 10
[Logger] Wartość: 10

Ustawianie wartości na 60:
[Display] Wartość: 60
[Logger] Wartość: 60
[Alert] OSTRZEŻENIE: 60 przekracza 50

Rozłączanie loggera:
Ustawianie wartości na 20:
[Display] Wartość: 20
[Alert] OSTRZEŻENIE: 20 przekracza 50

Właściwości sygnałów

class DataModel : public QObject {
    Q_OBJECT

signals:
    // Może przekazywać wiele parametrów dowolnego typu
    void valueChanged(int new_value);
    void stateChanged(int id, const QString& state);

    // Mogą nawet mieć parametry const
    void dataUpdated(const QByteArray& payload);

    // Wiele sygnałów z jednego obiektu
    void loading_started();
    void loading_finished(bool success);
    void progress(int percent);
};

Sygnały: - Nie mają implementacji. Kod generuje moc (Meta-Object Compiler). - Mogą mieć wiele parametrów dowolnego typu. - Mogą być połączone z wieloma slotami (jeden do wielu). - Mogą być połączone z innymi sygnałami (łańcuch sygnałów). - Nie zwracają wartości (fire-and-forget).

Właściwości slotów

class Observer : public QObject {
    Q_OBJECT

public slots:
    // Musi pasować do sygnatury sygnału (lub być konwertowany)
    void on_changed(int value) { }

    // Może mieć mniej parametrów niż sygnał (dodatkowe ignorowane)
    void on_state_changed() { }

    // Slot może być wywoływany z kodu
    void update_ui() { }

    // Sloty mogą wywoływać inne sloty i sygnały
    void refresh() { emit refreshed(); }

signals:
    void refreshed();
};

Sloty: - Są regularnymi funkcjami z implementacją. - Mogą mieć mniej parametrów niż sygnał (dodatkowe są ignorowane). - Można je wywołać bezpośrednio z kodu, w przeciwieństwie do sygnałów. - Same mogą emitować sygnały. - Są wirtualne, więc podklasy mogą je nadpisać.

Automatyczne zarządzanie czasem życia

W przeciwieństwie do ręcznego Observera Qt rozłącza połączenia samo, gdy obiekt jest niszczony:

{
    DataModel model;
    DisplayWindow* display = new DisplayWindow();

    QObject::connect(&model, &DataModel::valueChanged,
                     display, &DisplayWindow::on_value_changed);

    delete display;  // ✓ Wszystkie połączenia automatycznie usunięte
}

// display jest usunięty; valueChanged() już nie wywoła jego slotów
model.set_value(10);  // Bezpieczne! Brak wiszących wskaźników.

Qt śledzi relację rodzic-dziecko w QObject i rozłącza sygnały, gdy obiekt jest niszczony.

Składnia connect w Qt 5 i Qt 6

Qt 5 (string-based, sprawdzanie w czasie działania).

QObject::connect(&button, SIGNAL(clicked()),
                 &window, SLOT(on_button_clicked()));

Qt 6 (pointer-to-member, sprawdzanie w kompilacji).

QObject::connect(&button, &QPushButton::clicked,
                 &window, &Window::on_button_clicked);

Lepiej używać składni Qt 6, bo kompilator sprawdza, czy sygnał i slot istnieją i czy pasują do siebie.

Kiedy stosować sygnały i sloty

Dobre przypadki użycia - Aplikacje GUI: przyciski, menu, okna dialogowe. - Architektury model-widok: oddzielenie danych od prezentacji. - Systemy wtyczek: wtyczki emitują sygnały, a host się z nimi łączy. - Systemy sterowane zdarzeniami w aplikacji Qt. - Komunikacja między wątkami (Qt obsługuje ją automatycznie).

Kiedy tego unikać - Nie używasz Qt (brak QObject i moc). - Liczy się każda nanosekunda. - Potrzebujesz wartości zwracanych z obserwatorów (sygnały są fire-and-forget). - Callbacki są proste i jednorazowe. Lambdy z std::function są lżejsze.

Zalety

  • Automatyczne czyszczenie. Połączenie znika, gdy odbiornik jest niszczony.
  • Bezpieczeństwo typów (Qt 6). Kompilator sprawdza zgodność sygnału i slotu.
  • Deklaratywność. Składnia jest jasna, intencję widać od razu.
  • Bez ręcznej rejestracji. Nie ma listy obserwatorów do prowadzenia.
  • Sygnały między wątkami. Qt sam dostarcza je w bezpieczny sposób.
  • Elastyczne połączenia. Jeden sygnał może trafić do wielu slotów, wiele sygnałów do jednego slotu.
  • Debugowanie. Narzędzia Qt pokazują i śledzą połączenia.

Wady

  • Zależność od Qt. Działa tylko z QObject i moc.
  • Narzut. Więcej pracy w czasie działania i kompilacji niż przy zwykłych wywołaniach funkcji.
  • Brak wartości zwracanych. Sygnały są jednokierunkowe i fire-and-forget.
  • Funkcje wirtualne zostają. Sloty wywołują się wirtualnie.
  • Trzeba znać dokumentację. Bez zrozumienia moc i QObject się nie obejdzie.

Porównanie implementacji Observera

Podejście Type Safety Rozdzielenie Zarządzanie czasem życia Framework Złożoność
Oparte na interfejsie Częściowe Ręczne Ręczne Brak Niska
std::function Brak Dobre Ręczne Brak Średnia
Qt Signals/Slots Pełne (Qt 6) Dobre Automatyczne Wymaga Qt Średnia
System zdarzeń Pełne Dobre Ręczne Brak Wysoka

Podsumowanie

Sygnały i sloty to Observer z wbudowanymi zasadami czasu życia. Używaj ich w kodzie Qt. Poza Qt ich nie ma, a mniejszy mechanizm callbacków zwykle lepiej pasuje.

Następne kroki