Mechanizm sygnałów i slotów w Qt – Kurs Qt #3

Mechanizm sygnałów i slotów w Qt jest jednym z najbardziej cenionych i rozpoznawalnych funkcjonalności, które zawdzięczamy temu właśnie frameworkowi. Za tym mechanizmem stoi system Meta-Object Compiler będący przedmiotem poprzedniej części tego kursu Qt, ale sygnały i sloty są tematem tak istotnym, że zdecydowałem się na umieszczenie ich w oddzielnym wpisie. Gwarantuję Ci, że zdecydowanie polubisz sposób komunikacji między obiektami oferowany przez Qt.

Zanim zaczniesz

W ramach tego kursu Qt w pierwszej kolejności skupiamy się na tworzeniu interfejsu graficznego korzystając właśnie z tej technologii. Następnie to czytelnicy będą mogli zdecydować, w którą stronę pójdzie kurs. Dołożę wszelkich starań, aby nowe wpisy ukazywały się co czwartek. Jeżeli ten wpis jest pierwszym z tego kursu na jaki trafiasz to sugeruję, abyś zajrzał również do poprzednich. Miłej lektury!

  1. Aplikacje okienkowe. Czym jest Qt?
  2. Instalacja frameworka Qt
  3. Meta-Object Compiler i QObject

Mechanizm sygnałów i slotów – główna idea

Zarówno w przypadku pisania aplikacji okienkowych jak i komponentów na jej zapleczu, zdarza się, że zachodzi potrzeba opracowania metody komunikacji pomiędzy obiektami. Przykładowo w sytuacji, gdy użytkownik wciśnie przycisk START w menu gry chcielibyśmy aby to zdarzenie implikowało jakieś działanie, czyli żeby wywołana została jakaś metoda jak chociażby uruchomienie planszy z grą. Innym przykładem może być chociażby obiekt pingujący dostęp do serwera, gdzie po pomyślnym zapytaniu do serwera rozpoczęlibyśmy pobieranie dalszych danych.

W zwyczajnych warunkach, tj. bez Qt taka komunikacja odbywa się za pomocą callbacków lub w sposób bardziej „robaczany”, czyli poprzez trzymanie w klasie wskaźnika na obiekt, na rzecz, którego wywołać chcemy metodę. Oba sposoby niosą za sobą szereg możliwych problemów i są nieintuicyjne.

Jak to działa w Qt?

Framework Qt wychodzi naprzeciw dotychczasowym rozwiązaniom dostarczając mechanizm sygnałów i slotów, polegający na tym, że dany obiekt emituje sygnał, będący de facto metodą bez ciała. W tym samym obiekcie lub innym w zależności od naszych zamiarów implementujemy slot – metodę zawierającą instrukcje, które powinny zostać wykonane w przypadku jej wywołania. Zarówno sygnały jak i sloty mogą istnieć niezależnie od siebie. Oznacza to, że na jeden sygnał obiekt może reagować wieloma różnymi slotami oraz każdy slot może być wywołany w reakcji na wiele sygnałów pochodzących z różnych źródeł.

mechanizm sygnałów i slotów w qt

W dodatku mechanizm sygnałów i slotów w Qt niejako wymusza na nas zachowanie tej samej albo bardzo podobnej sygnatury sygnału i slotu. Właściwie to w momencie, gdy sygnał i slot są metodami to chodzi po prostu o to, że parametry sygnału muszą się zgadzać z parametrami slotu. Właściwie to liczba parametrów w slocie może być mniejsza niż liczba parametrów w sygnale, ale te początkowe muszą się zgadzać. Więcej zobaczymy na przykładach.

Deklaracja sygnału

Sygnały opierają się o system Meta-Object Compiler, czyli aby zadeklarować sygnał we własnej klasie to musi ona dziedziczyć po QObject oraz musimy ją dodatkowo udekorować makrem Q_OBJECT. Następnie dodajemy w klasie sekcję signals i w niej jak gdyby nigdy nic tworzymy deklarację metody. Metoda ta nie powinna niczego zwracać, dlatego stosujemy void. Jeżeli chodzi o ilość i typ parametrów to mamy pełną dowolność.

Załóżmy, że stworzymy klasę, która będzie kontrolować stan połączenia z serwerami binarnie.pl. Posiadać będzie metodę check(), która w przypadku, gdy połączenie się zmieni wyemituje sygnał accessChanged(bool access) jeżeli dotychczasowy dostęp się zmienił.

Deklarację sygnału należy zawrzeć w klasie w sekcji signals:

Następnie jeżeli chcemy sygnału wyemitować to w odpowiednim miejscu należy wywołać instrukcję emit accessChanged() z parametrem określającym zmieniony dostęp do serwera. Sygnał emitujemy w sytuacji, gdy dostęp się zmieni, tj. będzie inny niż dotychczasowy.

Na dobrą sprawę mógłbyś darować sobie pisanie „emit” przed wywołaniem sygnału, ale taki zabieg zwiększa czytelność kodu, ponieważ później patrząc w taki kod masz pewność, że wyemitowałeś sygnał a nie wywołałeś zwykłą metodę.

Połączenie sygnału z funkcją lambda

Jednym z pierwszych, mało inwazyjnych sposobów na wykorzystanie sygnału jest połączenie go z funkcją bądź wyrażeniem lambda. Wówczas za każdym razem, gdy sygnał zostanie wyemitowany to taka funkcja zostanie wywołana. Aby połączenie zrealizować należy skorzystać z poniższej metody:

Gdzie pierwszym parametrem jest wskazanie na obiekt emitujący sygnał, drugim jest funkcja zadeklarowana w definicji klasy tego obiektu jako sygnał, a trzecim parametrem jest właśnie wskazanie na funkcję/wyrażenie lambda.
Może to wyglądać na przykład w ten sposób:

Jak widzisz sygnał został połączony z wyrażeniem lambda wypisującym „Access changed to ” z wartością zmienionego dostępu w sytuacji, gdy ten faktycznie się zmieni. Dostęp ma szansę zmienić się 10 razy, bo tyle razy wywołujemy metodę checkAccess().

Deklaracja slotu

Załóżmy, że chcielibyśmy, aby nasz ConnectivityChecker sprawdzał dostęp nieustannie a nie tylko w momencie, gdy ręcznie wywołujemy metodę, która ma za zadanie to sprawdzić. W tym celu stworzymy timer, który co 2 sekundy będzie wywoływał funkcję checkAccess().

Skorzystamy z obiektu klasy QTimer, która emituje sygnał timeout() w momencie, gdy timer skończy odliczanie. Skoro mamy do czynienia z sygnałem to stworzymy również slot, który będzie wywoływany po każdej emisji tego sygnału i jego zadanie będzie sprawdzanie dostępności serwera.

Właściwie to mamy nawet taką metodę – checkAccess(). Zatem darujemy sobie pisanie kolejnej robiącej to samo. Wystarczy, że w definicji klasy umieścimy tą metodę w sekcji public slots lub private slots tak jak poniżej.

Połączenie sygnału ze slotem

Teraz slot checkAccess nie będzie już wywoływany z zewnątrz zatem możemy umieścić go w sekcji private slots. Niemniej w przypadku, gdy slot będzie wykorzystywany również na zewnątrz klasy umieść go w sekcji public slots.
Zauważ, że w tym samym czasie dodaliśmy nową składową klasy – QTimer *timer. Teraz zmienimy nieco domyślny konstruktor w ten sposób, aby przy utworzeniu nowej instancji obiektu ConnectivityChecker tworzona była również instancja timera domyślnie połączona sygnałem timeout z prywatnym slotem checkAccess().

Ot cała magia. Teraz wystarczy, że usuniesz pętlę używaną wcześniej i twój program będzie sprawdzał dostęp co dwie sekundy bez ustanku. Myślę, że kod jest zrozumiały.

Inna odsłona metody connect()


Parę słów o metodzie:

Z bardzo podobnej korzystaliśmy wcześniej łącząc sygnał accessChanged() z wyrażeniem lambda. Ogólnie rzecz biorąc metoda connect() jest przeciążona co sprowadza się do tego, że można ją używać na różne sposoby. Ten jest najpopularniejszy. Pierwsze dwa parametry odpowiadają za to samo co w wersji z functorem, natomiast dochodzi nam wskaźnik na odbiorcę sygnału, czyli w naszym przypadku na this -> obiekt ConnectivityChecker. Kolejny parametr to wskazanie na slot co czynimy w podobny sposób jak przy wskazaniu na sygnał.

Mechanizm sygnałów i slotów pomiędzy obiektami różnych klas

Klasę służącą sprawdzaniu połączenia z serwerami binarnie.pl już mamy. Załóżmy, że naszym celem było upewnienie się czy możemy spróbować otworzyć mój blog w przeglądarce 😉

Stwórzmy zatem klasę BrowserController wraz ze slotem za to odpowiadającym. Dodatkowo załóżmy, że klasa ta otworzy przeglądarkę z binarnie.pl tylko raz. Definicja klasy wyglądać może następująco:

Natomiast definicja metod klasy w ten sposób:

Zatem utworzyliśmy publiczny slot openBrowser() jednorazowo otwierający stronę binarnie.pl w przeglądarce w przypadku, gdy przekażemy jej wartość true. Teraz połączmy ten slot z sygnałem accessChanged() klasy ConnectivityChecker w funkcji main korzystając ze znanej nam już składni metody connect().

Teraz w momencie, gdy nawiążemy połączenie z serwerami otworzy się nasza domyślna przeglądarka na stronie binarnie.pl. Przy okazji widzimy, że ten sam sygnał może być podłączony do więcej niż tylko jednego slotu.

Przeciążenia funkcji connect()

Klasa QObject posiada 5 różnych wersji metody connect z uwagi na jej przeciążenia, więc nie zdziw się jeżeli w dokumentacji czy na StackOverflow zobaczysz inne sposoby na jej wywoływanie, niż w tym kursie. Oczywiście o wszystkich przeciążeniach przeczytasz w dokumentacji. Wykorzystywana składnia ze wskazaniami na sygnały i sloty jest obecnie zalecana.

Podsumowanie

Mam nadzieję, że po lekturze tej części kursu Qt wiesz już jak działa mechanizm sygnałów i slotów w tej technologii. W następnym wpisie bierzemy się za okienka, więc mam nadzieję, że zostaniesz ze mną 😊 Koniecznie daj znać co myślisz o kursie i czego jeszcze po nim oczekujesz.

Od razu chciałbym zaznaczyć, że wpis nie wyczerpuje tematu i w zależności od stopnia zaawansowania kursu i moich możliwości temat należy rozszerzyć.

You Might Also Like
Dodaj komentarz

Odwiedź Binarnie.pl na FB

Cześć, jeżeli publikowane treści okażą się dla Ciebie pomocne to zapraszam Cię do polubienia Binarnie.pl na Facebook'u 👍 Bądź pewien, że nic nowego Cię nie ominie 👨🏻‍💻


This will close in 20 seconds