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.
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
QObjectimoc. - 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
mociQObjectsię 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
- Zorientowany obiektowo system zdarzeń. Zdarzenia zorientowane obiektowo dla aplikacji spoza Qt, z hierarchiami bezpiecznymi typowo.
- Zarządzanie czasem życia. Strategie zarządzania czasem życia dla ręcznych implementacji Observera.