4.2. Widżety

Prosta 1-okienkowa aplikacja prezentująca większość podstawowych widżetów dostępnych w bibliotece Qt6 za pomocą Pythona 3 i biblioteki PyQt6. Przykład ilustruje również techniki programowania obiektowego (ang. Object Oriented Programing).

../../_images/widzety.png

W wybranym katalogu przygotuj środowisko wirtualne Pythona. Zainstaluj bibliotekę PyQt6 w aktywowanym środowisku:

(.venv) pip install pyqt6

Uwaga

Wymagana wiedza:

  • Znajomość Pythona w stopniu średnim.

  • Znajomość podstaw projektowania interfejsu z wykorzystaniem bibliotek Qt (zob. scenariusz Kalkulator).

  • Przedstawiona aplikacja składa się z 3 plików, które muszą być zapisane w tym samym katalogu, np. widzety.

4.2.1. QPainter – podstawy rysowania

Zaczynamy od utworzenia głównego pliku aplikacji o nazwie widzety.py. Wstawiamy do niego poniższy kod:

Plik widzety.py. Kod nr
 1from PyQt6.QtWidgets import QApplication, QWidget
 2from gui_z1 import UiWidget
 3
 4
 5class Widgety(QWidget, UiWidget):
 6    """ Główna klasa aplikacji """
 7
 8    def __init__(self):
 9        super().__init__()
10        self.setWindowTitle('Widżety')
11
12
13if __name__ == '__main__':
14    import sys
15    app = QApplication(sys.argv)
16    okno = Widgety()
17    okno.show()
18    sys.exit(app.exec())

Klasa Widgety posłuży do zdefiniowania głównego okna oraz logiki działania naszej aplikacji. Dziedziczy z klasy QWidget – podstawowej klasy biblioteki Qt, która jest bazą dla każdego elementu GUI, w tym okna głównego. Dziedziczy również z klasy UiWidget importowanej z pliku gui.py, w którym zdefiniujemy elementy interfejsu graficznego.

W konstruktorze klasy (__init()__) wywołujemy konstruktory klas rodziców (super().__init__()) oraz ustawiamy tytuł okna aplikacji (self.setWindowTitle('Widżety')).

Pozostały kod tworzy instancję aplikacji w oparciu o klasę QApplication, a także instancję okna głównego, czyli klasy Widgety, wyświetla je i uruchamia pętlę zdarzeń.

Kod klasy UiWidget umieszczamy we wspomnianym pliku o nazwie gui.py:

Plik gui.py. Kod nr
 1from ksztalty import Ksztalty, Ksztalt
 2from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout
 3
 4
 5class UiWidget:
 6    """ Klasa definiująca GUI """
 7
 8    def __init__(self):
 9
10        # widget definiujący kształt, instancja klasy Ksztalt
11        self.ksztalt1 = Ksztalt(None, Ksztalty.RECT)
12
13        # układ poziomy dla kształtów oraz przycisków CheckBox
14        uklad_h1 = QHBoxLayout()
15        uklad_h1.addWidget(self.ksztalt1)
16
17        # główny układ okna, pionowy
18        uklad_okna = QVBoxLayout()
19        uklad_okna.addLayout(uklad_h1)
20
21        # ustawienie głównego układu okna
22        self.setLayout(uklad_okna)

W konstruktorze klasy tworzymy widżet ksztalt1, który będzie mógł rysować figury geometryczne. Widżet jest instancję klasy Ksztalt zaimportowanej z pliku ksztalty.py. Z tego pliku importujemy również klasę Ksztalty, której właściwości oznaczają rysowane figury, w tym przypadku prostokąt: self.ksztalt1 = Ksztalt(None, Ksztalty.Rect)

Jeden widżet może zawierać wiele różnych elementów GUI, które trzeba w jakiś sposób porozmieszczać. Służą do tego układy graficzne (ang. layouts). Biblioteka Qt udostępnia układy:

Rysowany kształt dodajemy do układu poziomego za pomocą metody addWidget(). Następnie sam układ poziomy dodajemy do pionowego układu okna za pomocą metody addLayout(). Główny układ okna naszego widżetu ustawiamy w metodzie setLayout().

4.2.2. Klasa Ksztalt

W pliku ksztalty.py umieszczamy poniższy kod:

Plik ksztalty.py. Kod nr
 1from PyQt6.QtWidgets import QWidget
 2from PyQt6.QtGui import QPainter, QColor, QPolygon
 3from PyQt6.QtCore import QPoint, QRect, QSize
 4
 5
 6class Ksztalty:
 7    """ Klasa pomocnicza, symuluje typ wyliczeniowy """
 8    RECT, ELLIPSE, POLYGON, LINE = range(4)
 9
10
11class Ksztalt(QWidget):
12    """ Klasa definiująca widget do rysowania kształtów """
13    # współrzędne prostokąta i trójkąta
14    prost = QRect(1, 1, 101, 101)
15    punkty = QPolygon([
16        QPoint(1, 101),  # punkt początkowy (x, y)
17        QPoint(51, 1),
18        QPoint(101, 101)])
19
20    def __init__(self, parent, ksztalt=Ksztalty.RECT):
21        super().__init__(parent)
22
23        # kształt do narysowania
24        self.ksztalt = ksztalt
25
26        # kolor obramowania i wypełnienia w formacie RGB
27        self.kolor_o = QColor(0, 0, 0)
28        self.kolor_w = QColor(255, 255, 255)
29

Za pomocą klasy Ksztalty symulujemy typ wyliczeniowy, tzn. angielskim nazwom kształtów, które będą dostępne jako dane statyczne klasy, przypisujemy kolejne liczby całkowite zaczynając od 0. Kształty, które będziemy rysowali, to:

  • RECT – prostokąt, wartość 0;

  • ELLIPSE – elipsa, w tym koło, wartość 1;

  • POLYGON – linia łamana zamknięta, np. trójkąt, wartość 2;

  • LINE – linia łącząca dwa punkty, wartość 3.

Klasa Ksztalt dziedziczy z klasy QWidget i pozwoli na rysowanie zdefiniowanych w klasie Ksztalty figur. W konstruktorze definiujemy właściwości obiektu, który ma być rysowany:

  • self.ksztalt – rysowana figura wskazana w parametrze ksztalt, której domyślna wartość to Ksztalty.RECT,

  • self.kolor_o, self.kolor_w – kolory obramowania i wypełnienia.

Kolory tworzymy za pomocą klasy QColor, używając formatu RGB, np .: QColor(0, 0, 0).

Do klasy Ksztalt dodajemy metody odpowiedzialne za rysowanie:

Plik ksztalty.py. Kod nr
30    def paintEvent(self, e):
31        qp = QPainter()
32        qp.begin(self)
33        self.rysuj_figury(qp)
34        qp.end()
35
36    def rysuj_figury(self, qp):
37        qp.setPen(self.kolor_o)  # kolor obramowania
38        qp.setBrush(self.kolor_w)  # kolor wypełnienia
39        # wygładzanie kształtu
40        qp.setRenderHint(QPainter.RenderHint.Antialiasing)
41
42        if self.ksztalt == Ksztalty.RECT:
43            qp.drawRect(self.prost)
44        elif self.ksztalt == Ksztalty.ELLIPSE:
45            qp.drawEllipse(self.prost)
46        elif self.ksztalt == Ksztalty.LINE:
47            qp.drawLine(self.prost.topLeft(), self.prost.bottomRight())
48        elif self.ksztalt == Ksztalty.POLYGON:
49            qp.drawPolygon(self.punkty)
50

Za rysowanie każdego widżetu odpowiada metoda paintEvent(). Nadpisujemy ją. Tworzymy instancję klasy QPainter umożliwiającej rysowanie różnych kształtów (qp = QPainter()). Między metodami begin() i end() wywołujemy metodę rysuj_figury(), w której implementujemy kod rysujący poszczególne kształty.

Metoda rysuj_figury() otrzymuje obiekt klasy QPainter. Jego metody setPen() i setBrush() pozwalają ustawić kolor odpowiednio obramowania i wypełnienia. Następnie w instrukcji warunkowej sprawdzamy rodzaj rysowanego kształtu i wywołujemy metodę rysującą odpowiednią figurę:

  • drawRect() – rysuje prostokąt,

  • drawEllipse() – rysuje elipsę (koło),

  • qp.drawLine() – pozwala narysować linię wyznaczoną przez współrzędne punktu początkowego i końcowego typu QPoint; nasza klasa wykorzystuje tu współrzędne lewego górnego (self.prost.topLeft()) i prawego dolnego (self.prost.bottomRight()) rogu domyślnego prostokąta prost,

  • drawPolygon() – pozwala rysować wielokąty, jako argument podajemy listę typu QPolygon punktów typu QPoint opisujących współrzędne kolejnych wierzchołków; domyślne współrzędne zdefiniowane zostały jako atrybut punkty klasy Ksztalty,

Informacja

Każdy rysowany kształt wpisany jest w prostokąt zdefiniowany jako właściwość statyczna klasy Ksztalt: prost = QRect(1, 1, 101, 101). Obiekt ten jest instancją klasy QRect. Dwie pierwsze wartości to współrzędne lewego górnego, a dwie następne prawego dolnego rogu prostokąta w 2-wymiarowym układzie współrzędnych.

Początek układu współrzędnych, w odniesieniu do którego definiujemy w Qt pozycję widżetów czy punkty opisujące kształty, znajduje się w lewym górnym rogu obiektu rodzica, np. głównego okna aplikacji.

Informacja

Warto zrozumieć różnicę pomiędzy zmiennymi klasy a zmiennymi instancji. Zmienne (właściwości, atrybuty) klasy, określane również jako dane statyczne, są wspólne dla wszystkich jej instancji. W naszej aplikacji zdefiniowaliśmy w ten sposób zmienne prost i punkty klasy Ksztalt.

Zmienne instancji natomiast są inne dla każdego obiektu. Definiujemy je w konstruktorze, używając słowa self. Np. każda instancja klasy Ksztalt może mieć inną wartość właściwości self.ksztalt.

Zob.: Class and Instance Variables

Ćwiczenie

  • Uruchom skrypt widzety.py.

  • Spróbuj zmienić rodzaj rysowanej figury oraz kolory jej obramowania i wypełnienia.

../../_images/widzety00.png

Być może zauważysz, że po uruchomieniu naszego skryptu rozmiar okna aplikacji nie jest dopasowany do rozmiaru rysowanej figury. Spróbujemy to zmienić uzupełniając kod klasy Ksztalt:

Plik ksztalty.py. Kod nr
51    def sizeHint(self):
52        return QSize(102, 102)
53
54    def minimumSizeHint(self):
55        return QSize(102, 102)
56
57    def ustaw_ksztalt(self, ksztalt):
58        self.ksztalt = ksztalt
59        self.update()
60
61    def ustaw_kolor_w(self, r=0, g=0, b=0):
62        self.kolor_w = QColor(r, g, b)
63        self.update()

W nadpisanych metodach sizeHint() i minimumSizeHint() określamy sugerowany i minimalny rozmiar naszego kształtu. Są one niezbędne, aby układy graficzne (ang. layouts), w których umieścimy kształty, zarezerwowały odpowiednio dużo miejsca na ich wyświetlenie.

Kod uzupełniliśmy również o dwie metody ustaw_ksztalt() i ustaw_kolor_w(), które przydadzą się nam dalej. Jak wskazują nazwy – pozwolą one zmieniać kształt i jego kolor wypełnienia już po utworzeniu obiektu. Metoda self.update() wymusi ponowne narysowanie kształtu.

Ćwiczenie

  • Ponownie przetestuj działanie aplikacji, spróbuj zmienić rodzaj rysowanej figury oraz kolor jej wypełnienia.

../../_images/widzety01.png

Informacja

W kolejnych krokach będziemy dodawać widżety różnego typu. Kod tworzący odpowiednie obiekty i ustawiający ich początkowe właściwości dopisywać będziemy w pliku gui.py w konstruktorze klasy UiWidget. Dodając widżety, musimy pamiętać o zaimportowaniu odpowiedniej klasy z PyQt6.QtWidgets na początku pliku.

Kod wiążący sygnały ze slotami umieścimy w pliku widzety.py, w konstruktorze klasy Widgety. Sloty implementować będziemy jako funkcje tej klasy.

4.2.3. Przyciski CheckBox

Wykorzystując klasę Ksztalt utworzymy kolejny obiekt do rysowania figur. Dodamy także przyciski typu QCheckBox umożliwiające zmianę rodzaju wyświetlanej figury.

Importy w pliku gui.py:

from PyQt6.QtWidgets import QCheckBox, QButtonGroup

Klasa UiWidget przyjmuje następującą postać:

Plik gui.py. Kod nr
 6class UiWidget:
 7    """ Klasa definiująca GUI """
 8
 9    def __init__(self):
10
11        # widget definiujący kształt, instancja klasy Ksztalt
12        self.ksztalt1 = Ksztalt(None, Ksztalty.RECT)
13        self.ksztalt2 = Ksztalt(None, Ksztalty.ELLIPSE)
14        self.ksztalt_aktywny = self.ksztalt1
15
16        # przyciski CheckBox
17        uklad_chk = QVBoxLayout()  # układ pionowy
18        self.grupa_chk = QButtonGroup()
19        for i, v in enumerate(('Kwadrat', 'Koło', 'Trójkąt', 'Linia')):
20            self.chk = QCheckBox(v)
21            self.grupa_chk.addButton(self.chk, i)
22            uklad_chk.addWidget(self.chk)
23        przyciski = self.grupa_chk.buttons()
24        przyciski[self.ksztalt_aktywny.ksztalt].setChecked(True)
25
26        # przycisk CheckBox do wyboru aktywnego kształtu
27        self.ksztalt_chk = QCheckBox('<=')
28        self.ksztalt_chk.setChecked(True)
29        uklad_chk.addWidget(self.ksztalt_chk)
30
31        # układ poziomy dla kształtów oraz przycisków CheckBox
32        uklad_h1 = QHBoxLayout()
33        uklad_h1.addWidget(self.ksztalt1)
34        uklad_h1.addLayout(uklad_chk)
35        uklad_h1.addWidget(self.ksztalt2)
36        # koniec CheckBox
37
38        # główny układ okna, pionowy
39        uklad_okna = QVBoxLayout()
40        uklad_okna.addLayout(uklad_h1)
41
42        # ustawienie głównego układu okna
43        self.setLayout(uklad_okna)

Dodajemy drugi obiekt self.ksztalt2, domyślnie rysujący elipsę. Definiujemy też dodatkową właściwość self.ksztalt_aktywny, która przechowywała będzie aktualnie wybrany kształt, tzn. albo ksztal1 (domyślnie) albo ksztalt2.

Do tworzenia przycisków typu CheckBox wykorzystujemy pętlę for, która odczytuje z krotki kolejne indeksy i etykiety przycisków. Jeśli masz wątpliwości, jak to działa, przetestuj następujący kod w konsoli Pythona:

>>> for i, v in enumerate(('Kwadrat', 'Koło', 'Trójkąt', 'Linia')):
...   print(i, v)

Odczytane etykiety przekazujemy do konstruktora: self.chk = QCheckBox(v).

Przyciski wyboru kształtu działać mają na zasadzie wyłączności, w danym momencie powinien być zaznaczony tylko jeden z nich. Tworzymy więc grupę logiczną grupa_chk na podstawie klasy QButtonGroup. Do grupy dodajemy przyciski, oznaczając je kolejnymi indeksami: self.grupa_chk.addButton(self.chk, i).

Metoda buttons() zwraca listę przycisków, którą zapisujemy w zmiennej przyciski. Przycisk odpowiadający aktualnemu kształtowi wskazujemy przez indeks self.ksztalt_aktywny.ksztalt i wywołujemy metodę setChecked(True), która go zaznacza.

Poza pętlą tworzymy jeszcze jeden przycisk (self.ksztaltChk = QCheckBox("<=")), niezależny od powyższej grupy. Jego stan wskazuje aktywny kształt. Domyślnie go zaznaczamy: self.ksztaltChk.setChecked(True), co oznacza, że aktywną figurą będzie pierwszy kształt.

Wszystkie elementy interfejsu umieszczamy w układzie poziomym o nazwie uklad_h1. Po lewej stronie znajdzie się ksztalt1, w środku układ przycisków wyboru, a po prawej ksztalt2.

4.2.3.1. Obsługa sygnałów

Teraz zajmiemy się obsługą sygnałów. Przypomnijmy, że są to wydarzenia zachodzące w obrębie okna naszej aplikacji (ruch myszy, kliknięcia, naciśnięcia klawiszy itp.) przechwytywane przez główną pętlę zdarzeń naszej aplikacji. Do ich obsługi używamy slotów, czyli funkcji, w tym wypadku będą to metody klasy Widgety.

W pliku widzety.py rozbudowujemy klasę Widgety:

Plik widzety.py. Kod nr
 5class Widgety(QWidget, UiWidget):
 6    """ Główna klasa aplikacji """
 7
 8    def __init__(self):
 9        super().__init__()
10        self.setWindowTitle('Widżety')
11
12        # Sygnały i sloty
13        # przyciski CheckBox
14        self.grupa_chk.buttonClicked.connect(self.ustaw_ksztalt)
15        self.ksztalt_chk.clicked.connect(self.aktywuj_ksztalt)
16
17    def ustaw_ksztalt(self):
18        self.ksztalt_aktywny.ustaw_ksztalt(self.grupa_chk.checkedId())
19
20    def aktywuj_ksztalt(self, wartosc):
21        nadawca = self.sender()
22        if wartosc:
23            self.ksztalt_aktywny = self.ksztalt1
24            nadawca.setText('<=')
25        else:
26            self.ksztalt_aktywny = self.ksztalt2
27            nadawca.setText('=>')
28        przyciski = self.grupa_chk.buttons()
29        przyciski[self.ksztalt_aktywny.ksztalt].setChecked(True)
30

Grupa przycisków grupa_chk po kliknięciu emituje sygnał buttonClicked()`. Przekazujemy jego obsługę do slotu (metody klasy Widgety) ustaw_ksztalt().

W slocie ustaw_ksztalt() używamy metody o tej samej nazwie klasy Ksztalt do ustawienia nowej figury do narysowania. Jako argument przekazujemy identyfikator klikniętego przycisku odczytywany za pomocą metody checkedId(). Jest to liczba całkowita, która wskazuje jedną z figur zdefiniowanych w klasie Ksztalty.

Przypomnijmy (zob. wyżej), że metoda ustaw_ksztalt() z klasy Kształt aktualizuje identyfikator figury i wywołuje metodę update(), która wywołuje metodę paintEvent(), a ta metodę rysuj_figury(), która rysuje nową figurę.

Kliknięcie przycisku checkbox wskazującego aktywną figurę obsługujemy za pomocą slotu aktywuj_ksztalt(). Jej zadaniem jest ustawienie pierwszego lub drugiego kształtu jako aktywnego. Jeżeli przekazany do slotu argument wartosc będzie miał wartość True, co oznacza, że checkbox został zaznaczony, aktywujemy ksztalt1, w przeciwnym razie ksztalt2. Zmieniamy również odpowiednio tekst wyświetlany przy przycisku.

Informacja

Warto zapamiętać, jak uzyskać dostęp do obiektu nadawcy, który wygenerował dany sygnał. W odpowiednim slocie używamy kodu self.sender().

Ćwiczenie

Uruchom kilkakrotnie aplikację. Spróbuj zmieniać inicjalne rodzaje domyślnych kształtów i kolory wypełnienia figur.

../../_images/widzety02.png

4.2.4. Slider i przyciski RadioButton

Możemy już manipulować rodzajami rysowanych kształtów na obydwu obszarach rysowania. Spróbujemy teraz dodać widżety pozwalające je kolorować.

W pliku gui.py dodajemy importy:

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QSlider, QLCDNumber, QSplitter
from PyQt6.QtWidgets import QRadioButton, QGroupBox

Rozbudowujemy konstruktor klasy UiWidget. Po komentarzu # koniec CheckBox wstawiamy:

Plik gui.py. Kod nr
41        # Slider i LCDNumber
42        self.suwak = QSlider(Qt.Orientation.Horizontal)
43        self.suwak.setMinimum(0)
44        self.suwak.setMaximum(255)
45        self.lcd = QLCDNumber()
46        self.lcd.setSegmentStyle(QLCDNumber.SegmentStyle.Flat)
47
48        # układ poziomy (splitter) dla slajdera i lcd
49        uklad_h2 = QSplitter(Qt.Orientation.Horizontal, self)
50        uklad_h2.addWidget(self.suwak)
51        uklad_h2.addWidget(self.lcd)
52        uklad_h2.setSizes((125, 75))
53
54        # przyciski RadioButton
55        self.uklad_r = QHBoxLayout()
56        for v in 'RGB':
57            self.radio = QRadioButton(v)
58            self.uklad_r.addWidget(self.radio)
59        self.uklad_r.itemAt(0).widget().setChecked(True)
60
61        # grupujemy przyciski
62        self.grupa_rbb = QGroupBox('Opcje RGB')
63        self.grupa_rbb.setLayout(self.uklad_r)
64        self.grupa_rbb.setObjectName('Radio')
65        self.grupa_rbb.setCheckable(True)
66
67        # układ poziomy dla grupy Radio
68        uklad_h3 = QHBoxLayout()
69        uklad_h3.addWidget(self.grupa_rbb)
70        # koniec RadioButton
71

Do zmiany wartości składowych kolorów RGB wykorzystamy instancję klasy QSlider, czyli popularny suwak, w tym wypadku poziomy. Po utworzeniu obiektu, ustawiamy za pomocą metod setMinimum() i setMaximum() zakres zmienianych wartości <0-255>.

Następnie tworzymy instancję klasy QLCDNumber, którą wykorzystamy do wyświetlania wartości wybranej za pomocą suwaka. Obydwa obiekty dodajemy do poziomego układu, rozdzielając je instancją typu QSplitter. Obiekt tez pozwala płynnie zmieniać rozmiar otaczających go widżetów.

Przyciski typu RadioButton posłużą nam do wskazywania kanału koloru RGB, którego wartość chcemy zmienić. Tworzymy je w pętli, wykorzystując odczytane z ciągu znaków 'RGB' nazwy kanałów: self.radio = QRadioButton(v). Przyciski rozmieszczamy w układzie poziomym (self.uklad_r.addWidget(self.radio)).

Pierwszy z nich zaznaczamy: self.uklad_r.itemAt(0).widget().setChecked(True). Metoda itemAt(0) zwraca nam pierwszy element danego układu jako typ QLayoutItem. Kolejna metoda widget() przekształca go w obiekt typu QWidget, dzięki czemu możemy wywoływać jego metody.

Układ przycisków dodajemy do grupy typu QGroupBox: self.grupa_rbb.setLayout(self.uklad_r). Tego typu grupa zapewnia graficzną ramkę z przyciskiem aktywującym typu CheckBox, który domyślnie zaznaczamy: self.grupa_rbb.setCheckable(True). Za pomocą metody setObjectName() grupie nadajemy nazwę Radio. Grupę dodajemy do układu poziomego.

Wszystkie dodane powyżej widżety zostały umieszczone w układach poziomych, które należy dodać do głównego układu okna. Dopisz przed wywołaniem metody setLayout() odpowiedni kod:

Plik gui.py. Kod nr
uklad_okna.addWidget(uklad_h2)
uklad_okna.addLayout(uklad_h3)

4.2.4.1. Obsługa sygnałów

W pliku widzety.py dodajemy importy:

from PyQt6.QtGui import QColor
from PyQt6.QtWidgets import QRadioButton

Uzupełniamy konstruktor (__init__()) klasy Widgety:

Plik widzety.py. Kod nr
18
19        self.kanaly = {'R'}  # zbiór kanałów
20        self.kolor_w = QColor(0, 0, 0)  # kolor RGB kształtu 1
21
22        # Slider + przyciski RadioButton
23        for i in range(self.uklad_r.count()):
24            self.uklad_r.itemAt(i).widget().toggled.connect(self.ustaw_kanal)
25        self.suwak.valueChanged.connect(self.zmien_kolor)

Zmiana stanu przycisku RadioButton emituje sygnał toggled. W pętli for i in range(self.uklad_r.count()): wiążemy ten sygnał dla każdego przycisku układu ze slotem ustaw_kanal().

Przesuwanie suwaka wyzwala sygnał valueChanged, który łączymy ze slotem zmien_kolor().

Do klasy Widget dodajemy teraz wspomniane sloty i metodę pomocniczą:

Plik widzety.py. Kod nr
41    def ustaw_kanal(self, wartosc=''):
42        nadawca = self.sender()
43        if isinstance(nadawca, QRadioButton) and wartosc:
44            # nadawca to QRadioButton
45            self.kanaly = set()  # resetujemy zbiór kanałów
46            kanal = nadawca.text()
47            self.kanaly.add(kanal)
48            self.wypisz_kanal(kanal, self.suwak)
49
50    def wypisz_kanal(self, kanal, widzet):
51        if kanal == 'R':
52            widzet.setValue(self.kolor_w.red())
53        elif kanal == 'G':
54            widzet.setValue(self.kolor_w.green())
55        else:
56            widzet.setValue(self.kolor_w.blue())
57
58    def zmien_kolor(self, wartosc):
59        self.lcd.display(wartosc)
60        if 'R' in self.kanaly:
61            self.kolor_w.setRed(wartosc)
62        if 'G' in self.kanaly:
63            self.kolor_w.setGreen(wartosc)
64        if 'B' in self.kanaly:
65            self.kolor_w.setBlue(wartosc)
66        self.ksztalt_aktywny.ustaw_kolor_w(
67            self.kolor_w.red(),
68            self.kolor_w.green(),
69            self.kolor_w.blue())
70

Metoda ustaw_kanal() służy do zapisania w zbiorze kanałów self.kanaly litery oznaczającej wybrany kanał. Kanał można wybrać za pomocą różnych widżetów, dlatego na początku w zmiennej nadawca zapisujemy obiekt nadawcy. Warunek isinstance(nadawca, QRadioButton) and wartosc sprawdza za pomocą funkcji wbudowanej isinstance(nadawca, QRadioButton), czy nadawcą jest przycisk RadioButton i jeżeli tak, czy parametr wartosc ma wartość True, co oznacza, że przycisk jest zaznaczony. Jeżeli zostanie spełniony, do zresetowanego wcześniej zbioru kanałów dodajemy literę wybranego kanału: self.kanaly.add(nadawca.text()). Następnie wywołujemy metodę wypisz_kanal().

Zadaniem metody wypisz_kanal() jest ustawienie wartości kanału przekazanego w parametrze kanal w widżecie przekazanym w parametrze widzet za pomocą metody setValue(). Przekazny kanał wykkrywamy w złożonej instrukcji warunkowej, składową koloru odczytujemy za pomocą odpowiednich metod, np.: self.suwak.setValue(self.kolor_w.red()).

Metoda zmien_kolor() wywoływana jest po zmianie wartości, tj. liczby z zakresu <0; 255>, za pomocą suwaka. Wartość wyświetlamy w widżecie LCD: self.lcd.display(wartosc). Następnie sprawdzamy, który ze zmienianych kanałów znajduje się w zbiorze kanały i aktualizujemy jego wartość w kolorze wypełnienia, np.: self.kolor_w.setRed(wartosc).

Na koniec składowe koloru wypełnienia kolor_w przekazujemy do metody ustaw_kolor_w() aktywnego kształtu. Przypomnijmy, żę metoda ta zdefiniowana w pliku ksztalty.py aktualizuje kolor kształtu i wymusza jego ponowne rysowanie.

Przetestuj działanie aplikacji.

../../_images/widzety03.png

4.2.5. ComboBox i SpinBox

Modyfikowane kanały koloru można również wybierać z rozwijalnej listy typu QComboBox, a ich wartości ustawiać za pomocą widżetu QSpinBox.

W pliku gui.py dodajemy importy:

from PyQt6.QtWidgets import QComboBox, QSpinBox

Po komentarzu # koniec RadioButton uzupełniamy konstruktor klasy UiWidget:

Plik gui.py. Kod nr
73        # Lista ComboBox i SpinBox
74        self.lista_rgb = QComboBox()
75        for v in 'RGB':
76            self.lista_rgb.addItem(v)
77        self.lista_rgb.setEnabled(False)
78        # SpinBox
79        self.spin_rgb = QSpinBox()
80        self.spin_rgb.setMinimum(0)
81        self.spin_rgb.setMaximum(255)
82        self.spin_rgb.setEnabled(False)
83        # układ pionowy dla ComboBox i SpinBox
84        uklad_v1 = QVBoxLayout()
85        uklad_v1.addWidget(self.lista_rgb)
86        uklad_v1.addWidget(self.spin_rgb)
87        # do układu poziomego grupy Radio dodajemy układ ComboBox i SpinBox
88        uklad_h3.insertSpacing(1, 25)
89        uklad_h3.addLayout(uklad_v1)
90        # koniec ComboBox i SpinBox
91

Do listy utworzonej na podstawie klasy ComboBox dodajemy za pomocą pętli for litery poszczególnych kanałów: self.lista_rgb.addItem(v).

Obiekt typu SpinBox podobnie jak Slider wymaga ustawienia zakresu wartości <0-255>. Stosujemy takie same metody, jak wcześniej, tj. setMinimum() i setMaximum().

Obydwa widżety na początku wyłączamy metodą setEnabled(False). Umieszczamy jeden nad drugim w pionowym układzie uklad_v1, a układ dodajemy obok przycisków Radio uklad_h3.addLayout(uklad_v1), oddzielając go odstępem 25 px: uklad_h3.insertSpacing(1, 25).

4.2.5.1. Obsługa sygnałów

W pliku widzety.py dodajemy import:

from PyQt6.QtWidgets import QRadioButton

Do konstruktora dodajemy kod przechwytujący 3 sygnały:

Plik widzety.py. Kod nr
27        # Lista ComboBox i SpinBox
28        self.grupa_rbb.clicked.connect(self.ustaw_stan)
29        self.lista_rgb.currentTextChanged.connect(self.ustaw_kanal)
30        self.spin_rgb.valueChanged.connect(self.zmien_kolor)
31

Pierwszy sygnał, tj. kliknięcie przycisku CheckBox grupy przycisków RadioButton wiążemy ze slotem ustaw_stan():

Plik widzety.py. Kod nr
81
82    def ustaw_stan(self, wartosc):
83        if wartosc:
84            # włączone przyciski RadioButton
85            self.lista_rgb.setEnabled(False)
86            self.spin_rgb.setEnabled(False)
87        else:
88            # włączona lista ComboBox
89            self.lista_rgb.setEnabled(True)
90            self.spin_rgb.setEnabled(True)
91            self.kanaly = set()
92            self.kanaly.add(self.lista_rgb.currentText())
93            self.wypisz_kanal(wartosc, self.spin_rgb)
94

Jeżeli metoda ustaw_stan() w parametrze wartosc otrzyma True, tzn. przycisk jest zaznaczony, wyłączamy widżety ComboBox i SpinBox (setEnabled(False)). W przeciwnym razie je włączamy (setEnabled(True)), a także resetujemy zbiór kanałów i dodajemy do niego kanał wybrany na liście: self.kanaly.add(self.lista_rgb.currentText()). Na koniec ustawiamy wartość aktywnego kanału w obiekcie SpinBox.

Zmianę kanału na liście ComboBox, tj. sygnał currentTextChanged obsługujemy za pomocą dodanej wcześniej metody ustaw_kanal(), która przyjmuje następującą postać:

Plik widzety.py. Kod nr
46    def ustaw_kanal(self, wartosc=''):
47        nadawca = self.sender()
48        if isinstance(nadawca, QRadioButton) and wartosc:
49            # nadawca to QRadioButton
50            self.kanaly = set()  # resetujemy zbiór kanałów
51            kanal = nadawca.text()
52            self.kanaly.add(kanal)
53            self.wypisz_kanal(kanal, self.suwak)
54        elif isinstance(nadawca, QComboBox):
55            # nadawca to QComboBox
56            self.kanaly = set()  # resetujemy zbiór kanałów
57            self.kanaly.add(wartosc)
58            self.wypisz_kanal(wartosc, self.spin_rgb)
59

Dodajemy warunek isinstance(nadawca, QComboBox) sprawdzający, czy nadawcą jest obiekt typu QComboBox. Jeżeli tak, resetujemy zbiór kanałów i dodajemy literę wybranego kanału: self.kanaly.add(wartosc). Na koniec ustawiamy wartość tego kanału w obiekcie SpinBox: self.wypisz_kanal(wartosc, self.spin_rgb).

Informacja

Slot ustaw_kanal() w przypadku sygnału toogled obiektu typu QRadioButton otrzymuje w argumencie wartosc wartość True lub False w zależności od tego, czy przycisk jest zaznaczony czy nie. W przypadku sygnału currentTextChanged obiektu typu QComboBox argument wartosc zawiera literę wybranego kanału.

Zmiana wartości w kontrolce SpinBox, czyli sygnał valueChanged, przekierowujemy do dodanego wcześniej slotu zmien_lolor(), który obsługuje również zmiany wartości na suwaku.

Uruchom aplikację i sprawdź jej działanie.

../../_images/widzety04.png

4.2.6. Przyciski PushButton

Za pomocą dodanych do tej pory widżetów możemy zmieniać kolor każdego kanału składowego osobno. Dodamy teraz możliwość zmiany koloru kilku kanałów jednocześnie. Użyjemy grupy przycisków typu QPushButton.

W pliku gui.py dodajemy importy:

from PyQt6.QtWidgets import QPushButton

Następnie po komentarzu # koniec ComboBox i SpinBox dopisujemy kod w konstruktorze klasy UiWidget:

Plik gui.py. Kod nr
 93        # przyciski PushButton
 94        uklad_pb = QHBoxLayout()
 95        self.grupa_pb = QButtonGroup()
 96        for v in 'RGB':
 97            self.btn = QPushButton(v)
 98            self.btn.setCheckable(True)
 99            self.grupa_pb.addButton(self.btn)
100            uklad_pb.addWidget(self.btn)
101        self.grupa_pb.setExclusive(False)
102        # grupujemy przyciski
103        self.grupa_pbb = QGroupBox('Przyciski RGB')
104        self.grupa_pbb.setLayout(uklad_pb)
105        self.grupa_pbb.setObjectName('Push')
106        self.grupa_pbb.setCheckable(True)
107        self.grupa_pbb.setChecked(False)
108        # koniec PushButton
109

Przyciski tworzymy podobnie jak wcześniej w pętli za pomocą instrukcji: self.btn = QPushButton(v). Każdy przycisk przekształcamy na stanowy, tj. taki który może być trwale wciśnięty, za pomocą metody setCheckable(True). Następnie przycisk dodajemy do grupy typu QButtonGroup, która umożliwi zaznaczenie (wciśnięcie przycisku): self.grupa_pb.addButton(self.btn). Każdy przycisk dodawany jest również do układu poziomego uklad_pb.

Wywołanie po pętli metody grupy przycisków setExclusive(False) umożliwi zaznaczanie (wciskanie) wielu przycisków na raz, czyli odwrotnie niż w przypadku grupy przycisków CheckBox.

Układ przycisków dodajemy do ramki typu QGroupBox z przyciskiem CheckBox: self.grupa_pbb.setCheckable(True). Na początku ramkę wyłączamy: self.grupaPBtn.setChecked(False).

Ramkę z przyciskami musimy dodać do głównego układu okna za pomocą metody addWidget(). Kod powinien wyglądać następująco:

Plik gui.py. Kod nr
110        # główny układ okna, pionowy
111        uklad_okna = QVBoxLayout()
112        uklad_okna.addLayout(uklad_h1)
113        uklad_okna.addWidget(uklad_h2)
114        uklad_okna.addLayout(uklad_h3)
115        uklad_okna.addWidget(self.grupa_pbb)
116

4.2.6.1. Obsługa sygnałów

W pliku widzety.py dodajemy import:

from PyQt6.QtWidgets import QPushButton

Obsługę sygnałów dopisujemy w konstruktorze:

Plik widzety.py. Kod nr
33        # przyciski PushButton
34        for btn in self.grupa_pb.buttons():
35            btn.clicked.connect(self.ustaw_kanal)
36        self.grupa_pbb.clicked.connect(self.ustaw_stan)
37

W pętli odczytujemy kolejne przyciski z grupy grupa_pb zwracane przez metodę buttons() i kliknięcie każdego wiążemy ze slotem ustaw_kanal().

Kod metody ustaw_kanal() uzupełniamy:

Plik widzety.py. Kod nr
65        elif isinstance(nadawca, QPushButton):
66            if wartosc:
67                self.kanaly.add(nadawca.text())
68            elif nadawca.text() in self.kanaly:
69                self.kanaly.remove(nadawca.text())
70

Po wykryciu, że nadawca sygnału jest obiektem typu QPushButton, sprawdzamy, czy przycisk został wciśnięty, tj. argument wartosc ustawiony jest na True. Jeżeli tak, odpowiedni kanał zostanie dodany do zbioru self.kanaly, a w przeciwnym razie zostanie ze zbioru usunięty.

Inaczej niż w poprzednich metodach, obsługujących przyciski Radio i listę ComboBox, nie resetujemy tu zbioru kanałów.

Przetestuj zmodyfikowaną aplikację.

../../_images/widzety05.png

4.2.7. QLabel i QLineEdit

Dodamy do aplikacji zestaw widżetów typu QLineEdit, tzn. 1-liniowych pól edycyjnych. Pola będą oznaczone etykietami typu QLabel i będą umożliwiały ustawienia składowych koloru wypełnienia aktywnego kształtu.

W pliku gui.py dodajemy importy:

from PyQt6.QtWidgets import QLabel, QLineEdit

Następnie po komentarzu # koniec PushButton uzupełnij konstruktor klasy UiWidget:

Plik gui.py. Kod nr
111        # etykiety QLabel i pola QLineEdit
112        uklad_h4 = QHBoxLayout()
113        self.label_r = QLabel('R')
114        self.label_g = QLabel('G')
115        self.label_b = QLabel('B')
116        self.edit_r = QLineEdit('0')
117        self.edit_g = QLineEdit('0')
118        self.edit_b = QLineEdit('0')
119        for v in 'rgb':
120            label = getattr(self, 'label_' + v)
121            edit = getattr(self, 'edit_' + v)
122            edit.setObjectName('edit_' + v)
123            edit.setMaxLength(3)
124            uklad_h4.addWidget(label)
125            uklad_h4.addWidget(edit)
126        # koniec QLabel i QLineEdit
127
128        # główny układ okna, pionowy

Zaczynamy od utworzenia trzech etykiet i trzech pól edycyjnych dla każdego kanału. W pętli wykorzystujemy funkcję Pythona getattr(obiekt, nazwa), która potrafi zwrócić podany jako nazwa atrybut obiektu. W tym przypadku kolejne etykiety i pola edycyjne, które umieszczamy obok siebie w poziomie. Przy okazji ograniczamy długość wpisywanego w pola edycyjne tekstu do 3 znaków: edit.setMaxLength(3).

Układ uklad_h4 trzeba jeszcze dodać do głównego układu okna:

Plik gui.py. Kod nr
uklad_okna.addLayout(uklad_h4)

4.2.7.1. Obsługa sygnałów

W pliku widzety.py dodajemy import:

from PyQt6.QtWidgets import QLineEdit

Obsługę sygnałów dopisujemy w konstruktorze:

Plik widzety.py. Kod nr
39        # etykiety QLabel i pola QEditLine
40        for v in 'rgb':
41            edit = getattr(self, 'edit_' + v)
42            edit.editingFinished.connect(self.ustaw_kanal)
43            edit.editingFinished.connect(self.zmien_kolor)
44

W pętli, podobnej jak w pliku interfejsu, sygnał zakończenia edycji tekstu w polu typu QLineEdit wiążemy z dodanymi wcześniej slotami ustaw_kanal i zmien_kolor(). Będziemy mogli wpisywać w tych polach nowe wartości składowych koloru.

Uzupełniamy metodę ustaw_kanal():

Plik widzety.py. Kod nr
77        elif isinstance(nadawca, QLineEdit):
78            self.kanaly = set()  # resetujemy zbiór kanałów
79            kanal = nadawca.objectName()[-1].upper()
80            self.kanaly.add(kanal)
81

Jeżeli nadawca jest obiektem typu QLineEdit, odczytujemy ostatnią literę z jego nazwy i zamieniamy na wielką: nadawca.objectName()[-1].upper(). Litera ta oznacza edytowany kanał, który dodajemy do zbioru kanałów.

Następnie zmieniamy metodę zmien_kolor(), która do tej pory otrzymywała wartości typu całkowitego z suwaka QSlider lub pola QSpinBox. Pole edycyjne zwraca liczbę, ale w postaci tekstu, który trzeba zamienić na typ całkowity. Dodajemy więc na początku metody instrukcję:

Plik widzety.py. Kod nr
90    def zmien_kolor(self, wartosc=0):
91        if isinstance(self.sender(), QLineEdit):
92            wartosc = int(self.sender().text())
93        self.lcd.display(wartosc)

Natomiast na końcu omawianej metody umieszczamy wywołanie nowej metody: self.info().

Kod metody info() dopisujemy do klasy Widgety:

Plik widzety.py. Kod nr
119    def info(self):
120        font_b = "QWidget { font-weight: bold }"
121        font_n = "QWidget { font-weight: normal }"
122
123        for v in 'rgb':
124            label = getattr(self, 'label_' + v)
125            if v.upper() in self.kanaly:
126                label.setStyleSheet(font_b)
127            else:
128                label.setStyleSheet(font_n)
129
130        self.edit_r.setText(str(self.kolor_w.red()))
131        self.edit_g.setText(str(self.kolor_w.green()))
132        self.edit_b.setText(str(self.kolor_w.blue()))
133

Jej zadanie polega na wyróżnieniu kanałów znajdujących się w zbiorze kanaly poprzez pogrubienie czcionki etykiet i uaktywnieniu odpowiednich pól edycyjnych. Jeżeli kanał jest nieaktywny, ustawiamy normalną czcionkę etykiety i wyłączamy pole edycji. Wszystko dzieje się w pętli wykorzystującej omawianą już funkcję getattr() do uzyskania dostępu do kolejnych obiektów. Na końcu metody wartości poszczególnych kanałów koloru wpisujemy do odpowiednich pól edycyjnych.

Informacja

Typ czcionki zmieniamy z pomocą stylów CSS zdefiniowanym na początku funkcji pod nazwą font_b i font_n. Później przypisujemy je etykietom za pomocą metody setStyleSheet().

Wprowadź omówione zmiany i przetestuj działanie aplikacji.

../../_images/widzety06.png

4.2.8. Dodatki

Nasza aplikacja działa, ale można dopracować w niej kilka szczegółów. Poniżej zaproponujemy kilka zmian, które potraktować należy jako zachętę do samodzielnych ćwiczeń i przeróbek.

  1. Pola edycyjne QLineEdit dla składowych zielonej i niebieskiej powinny być na początku nieaktywne.

  2. Zaznaczenie jednej z grup widżetów powinno wyłączać inne grupy, tj. w danym momencie powinna być aktywna albo grupa przycisków Radio albo lista Combo albo grupa przycisków Push z polami edycyjnymi.

  3. Jeżeli aktywujemy grupę Push, należy zaznaczyć (wcisnąć) przycisk odpowiadający ostatniemu aktywnemu kanałowi.

  4. Stan pól edycyjnych powinien odpowiadać stanowi przycisków Push, wciśnięty przycisk to aktywne pole i odwrotnie.

  5. Funkcja zmien_kolor() nie jest zabezpieczona przed błędnymi danymi wprowadzanymi do pól edycyjnych.

  6. Dodaj dwa osobne przyciski, które umożliwią kopiowanie koloru i kształtu z jednej figury na drugą.

  7. Dodaj etykietę lub pole edycyjne, które będzie wyświetlało aktualnie ustawiony kolor dla aktywnego kształtu w formacie szesnastkowym.

4.2.9. Materiały

  1. Qt Widgets

  2. Widgets Tutorial

  3. Layout Management


Licencja Creative Commons Materiały Python 101 udostępniane przez Centrum Edukacji Obywatelskiej na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowa.

Utworzony:

2026-04-19 o 17:42 w Sphinx 7.3.7

Autorzy:

Robert Bednarz