6.3. Widżety

1-okienkowa aplikacja prezentująca zastosowanie większości podstawowych widżetów dostępnych w bibliotece Qt5 obsługiwanej za pomocą wiązań PyQt5. Przykład ilustruje również techniki programowania obiektowego (ang. Object Oriented Programing).

Attention

Wymagana wiedza:

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

6.3.1. QPainter – podstawy rysowania

Zaczynamy od utworzenia głównego pliku o nazwie widzety.py w dowolnym katalogu za pomocą dowolnego edytora. Wstawiamy do niego poniższy kod:

Plik widzety.py. Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import unicode_literals
from PyQt5.QtWidgets import QApplication, QWidget
from gui import Ui_Widget


class Widgety(QWidget, Ui_Widget):
    """ Główna klasa aplikacji """

    def __init__(self, parent=None):
        super(Widgety, self).__init__(parent)
        self.setupUi(self)  # tworzenie interfejsu

if __name__ == '__main__':
    import sys

    app = QApplication(sys.argv)
    okno = Widgety()
    okno.show()

    sys.exit(app.exec_())

Podstawową klasą opisującą naszą aplikację będzie klasa Widgety. Umieścimy w niej głównie logikę aplikacji, czyli powiązania sygnałów i slotów (zob.: sygnały i sloty) oraz implementację tych ostatnich. Klasa ta dziedziczy z zaimportowanej z pliku gui.py klasy Ui_Widget i w swoim konstruktorze (def __init__(self, parent=None)) wywołuję odziedziczoną metodę self.setupUi(self), aby zbudować interfejs. Pozostała część pliku tworzy instancję aplikacji, instancję okna głównego, czyli klasy Widgety, wyświetla je i uruchamia pętlę zdarzeń.

Klasę Ui_Widget dla przejrzystości umieszczamy we wspomnianym pliku o nazwie gui.py. Tworzymy go i wstawiamy poniższy kod:

Plik gui.py. Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import unicode_literals
from PyQt5.QtGui import QPainter, QColor
from PyQt5.QtCore import QRect


class Ui_Widget(object):
    """ Klasa definiująca GUI """

    def setupUi(self, Widget):

        self.ksztalt = Ksztalty.Ellipse  # kształt do narysowania
        self.prost = QRect(1, 1, 101, 101)  # współrzędne prostokąta
        # kolor obramowania i wypełnienia w formacie RGB
        self.kolorO = QColor(0, 0, 0)
        self.kolorW = QColor(200, 30, 40)

        self.resize(102, 102)
        self.setWindowTitle('Widżety')

    def paintEvent(self, e):
        qp = QPainter()
        qp.begin(self)
        self.rysujFigury(e, qp)
        qp.end()

    def rysujFigury(self, e, qp):
        qp.setPen(self.kolorO)  # kolor obramowania
        qp.setBrush(self.kolorW)  # kolor wypełnienia
        qp.setRenderHint(QPainter.Antialiasing)  # wygładzanie kształtu

        if self.ksztalt == Ksztalty.Rect:
            qp.drawRect(self.prost)
        elif self.ksztalt == Ksztalty.Ellipse:
            qp.drawEllipse(self.prost)


class Ksztalty:
    """ Klasa pomocnicza, symuluje typ wyliczeniowy """
    Rect, Ellipse, Polygon, Line = range(4)

Klasa pomocnicza Ksztalty symulować będzie typ wyliczeniowy. Angielskie nazwy kształtów tworzą dane statyczne (zob. dana statyczna) klasy. Przypisujemy im kolejne wartości 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.

Określając rodzaj rysowanego kształtu, będziemy używali konstrukcji typu Ksztalty.Ellipse, tak jak w głównej metodzie klasy Ui_Widget o nazwie setupUi(). Definiujemy w niej zmienną wskazującą rysowany kształt (self.ksztalt = Ksztalty.Ellipse) oraz jego właściwości, czyli rozmiar, kolor obramowania i wypełnienia. Kolory opisujemy za pomocą klasy QColor, używając formatu RGB, np .: self.kolorW = QColor(200, 30, 40).

Za rysowanie każdego widżetu, w tym wypadku głównego okna, odpowiada funkcja 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 funkcję rysujFigury(), w której implementujemy właściwy kod rysujący.

Metody setPen() i setBrush() pozwalają ustawić kolor odpowiednio obramowania i wypełnienia. Po sprawdzeniu w instrukcji warunkowej rodzaju rysowanego kształtu wywołujemy odpowiednią metodę obiektu QPainter:

  • drawRect() – rysuje prostokąty,
  • drawEllipse() – rysuje elipsy.

Obydwie metody jako parametr przyjmują instancję klasy QRect: self.prost = QRect(1, 1, 101, 101). Pozwala ona opisywać prostokąt do narysowania albo służący do wpisania w niego elipsy. Jako argumenty konstruktora podajemy dwie pary współrzędnych. Pierwsza określa położenie lewego górnego, druga prawego dolnego rogu prostokąta.

Attention

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

Ćwiczenie

  • Przetestuj działanie aplikacji wydając w katalogu z plikami źródłowymi polecenie w terminalu: python widzety.py.
  • Spróbuj zmienić rodzaj rysowanej figury oraz kolory jej obramowania i wypełnienia.
../../_images/widzety00.png

6.3.2. Klasa Ksztalt

Przedstawiony wyżej sposób rysowania ma istotne ograniczenia. Przede wszystkim rysowanie odbywa się bezpośrednio w oknie głównym, co utrudnia umieszczanie innych widżetów. Po drugie nie ma wygodnego sposobu dodawania niezależnych od siebie kształtów. Aby poprawić te niedogodności, stworzymy swój widżet do rysowania, czyli klasę Ksztalt. Kod umieszczamy w osobnym pliku o nazwie ksztalt.py w katalogu z poprzednimi plikami. Jego zawartość jest następująca:

Plik ksztalty.py. Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# -*- coding: utf-8 -*-

from PyQt5.QtWidgets import QWidget
from PyQt5.QtGui import QPainter, QColor, QPolygon
from PyQt5.QtCore import QPoint, QRect, QSize


class Ksztalty:
    """ Klasa pomocnicza, symuluje typ wyliczeniowy """
    Rect, Ellipse, Polygon, Line = range(4)


class Ksztalt(QWidget):
    """ Klasa definiująca widget do rysowania kształtów """
    # współrzędne prostokąta i trójkąta
    prost = QRect(1, 1, 101, 101)
    punkty = QPolygon([
        QPoint(1, 101),  # punkt początkowy (x, y)
        QPoint(51, 1),
        QPoint(101, 101)])

    def __init__(self, parent, ksztalt=Ksztalty.Rect):
        super(Ksztalt, self).__init__(parent)

        # kształt do narysowania
        self.ksztalt = ksztalt
        # kolor obramowania i wypełnienia w formacie RGB
        self.kolorO = QColor(0, 0, 0)
        self.kolorW = QColor(255, 255, 255)

    def paintEvent(self, e):
        qp = QPainter()
        qp.begin(self)
        self.rysujFigury(e, qp)
        qp.end()

    def rysujFigury(self, e, qp):
        qp.setPen(self.kolorO)  # kolor obramowania
        qp.setBrush(self.kolorW)  # kolor wypełnienia
        qp.setRenderHint(QPainter.Antialiasing)  # wygładzanie kształtu

        if self.ksztalt == Ksztalty.Rect:
            qp.drawRect(self.prost)
        elif self.ksztalt == Ksztalty.Ellipse:
            qp.drawEllipse(self.prost)
        elif self.ksztalt == Ksztalty.Polygon:
            qp.drawPolygon(self.punkty)
        elif self.ksztalt == Ksztalty.Line:
            qp.drawLine(self.prost.topLeft(), self.prost.bottomRight())
        else:  # kształt domyślny Rect
            qp.drawRect(self.prost)

    def sizeHint(self):
        return QSize(102, 102)

    def minimumSizeHint(self):
        return QSize(102, 102)

    def ustawKsztalt(self, ksztalt):
        self.ksztalt = ksztalt
        self.update()

    def ustawKolorW(self, r=0, g=0, b=0):
        self.kolorW = QColor(r, g, b)
        self.update()

Najważniejsza metoda, tj. paintEvent(), w ogóle się nie zmienia. Natomiast funkcję rysujFigury() rozbudowujemy o możliwość rysowania kolejnych kształtów:

  • 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 naszej klasy;
  • 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 = QRect(1, 1, 101, 101).

Konstruktor naszej klasy: __init__(self, parent, ksztalt=Ksztalty.Rect) – umożliwia opcjonalne przekazanie w drugim argumencie typu rysowanego kształtu. Domyślnie będzie to prostokąt. Zostanie on przypisany do atrybutu self.ksztalt. W konstruktorze definiujemy również domyślne kolory obramowania self.kolorO i wypełnienia self.kolorW.

Note

Warto zrozumieć różnicę pomiędzy zmiennymi klasy a zmiennymi instancji. Zmienne (właściwości) klasy, określane również jako dane statyczne, są wspólne dla wszystkich jej instancji. W naszej aplikacji zdefiniowaliśmy w ten sposób dostępne kształty, a także 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 rysować inną figurę zapamiętaną w zmiennej self.ksztalt. Zob.: Class and Instance Variables

Funkcje ustawKsztalt() i ustawKolorW() – jak wskazują nazwy – pozwalają modyfikować kształt i kolor wypełnienia obiektu kształtu już po jego utworzeniu jako instancji klasy. Metoda self.update() wymusza ponowne narysowanie kształtu.

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

Ponieważ wydzieliliśmy klasę opisującą kształty, plik gui.py możemy uprościć:

Plik gui.py. Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import unicode_literals
from ksztalty import Ksztalty, Ksztalt
from PyQt5.QtWidgets import QHBoxLayout


class Ui_Widget(object):
    """ Klasa definiująca GUI """

    def setupUi(self, Widget):

        # widget rysujący kształty, instancja klasy Ksztalt
        self.ksztalt = Ksztalt(self, Ksztalty.Polygon)

        # układ poziomy, zawiera: self.ksztalt
        ukladH1 = QHBoxLayout()
        ukladH1.addWidget(self.ksztalt)

        self.setLayout(ukladH1)  # przypisanie układu do okna głównego
        self.setWindowTitle('Widżety')

Tworzymy obiekt self.ksztalt jako instancję klasy Ksztalty() i ustawiamy kolor wypełnienia. Utworzony widżet dodajemy do poziomego układu ukladH1.addWidget(self.ksztalt), a układ przypisujemy do okna głównego self.setLayout(ukladH1).

Plik widzety.py pozostaje bez zmian, jego zadaniem jest uruchomienie aplikacji.

Ćwiczenie

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

Note

W kolejnych krokach będziemy umieszczać w oknie głównym widżety różnego typu. Kod tworzący te obiekty i ustawiający początkowe ich właściwości umieszczać będziemy w pliku gui.py w funkcji setupUi(). Dodając nowe widżety, musimy pamiętać o zaimportowaniu odpowiedniej klasy Qt na początku pliku. Informacje o importach będą umieszczone na początku każdej sekcji.

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

6.3.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 PyQt5.QtWidgets import QCheckBox, QButtonGroup, QVBoxLayout

Funkcja setupUi() przyjmuje następującą postać:

Plik gui.py. Kod nr
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    def setupUi(self, Widget):

        # widgety rysujące kształty, instancje klasy Ksztalt
        self.ksztalt1 = Ksztalt(self, Ksztalty.Polygon)
        self.ksztalt2 = Ksztalt(self, Ksztalty.Ellipse)
        self.ksztaltAktywny = self.ksztalt1

        # przyciski CheckBox ###
        uklad = QVBoxLayout()  # układ pionowy
        self.grupaChk = QButtonGroup()
        for i, v in enumerate(('Kwadrat', 'Koło', 'Trójkąt', 'Linia')):
            self.chk = QCheckBox(v)
            self.grupaChk.addButton(self.chk, i)
            uklad.addWidget(self.chk)
        self.grupaChk.buttons()[self.ksztaltAktywny.ksztalt].setChecked(True)
        # CheckBox do wyboru aktywnego kształtu
        self.ksztaltChk = QCheckBox('<=')
        self.ksztaltChk.setChecked(True)
        uklad.addWidget(self.ksztaltChk)

        # układ poziomy dla kształtów oraz przycisków CheckBox
        ukladH1 = QHBoxLayout()
        ukladH1.addWidget(self.ksztalt1)
        ukladH1.addLayout(uklad)
        ukladH1.addWidget(self.ksztalt2)
        # koniec CheckBox ###

        self.setLayout(ukladH1)  # przypisanie układu do okna głównego
        self.setWindowTitle('Widżety')

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

~$ python
>>> 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 zaznaczony być tylko jeden z nich. Tworzymy więc grupę logiczną dzięki klasie QButtonGroup. Do jej instancji dodajemy przyciski, oznaczając je kolejnymi indeksami: self.grupaChk.addButton(self.chk, i).

Kod self.grupaChk.buttons()[self.ksztaltAktywny.ksztalt].setChecked(True) zaznacza przycisk, który odpowiada aktualnemu kształtowi. Metoda buttons() zwraca nam listę przycisków. Ponieważ do oznaczania kształtów używamy kolejnych liczb całkowitych, możemy użyć ich jako indeksu.

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. Inicjujemy również odpowiednią zmienną: self.ksztaltAktywny = self.ksztalt1.

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

Teraz zajmiemy się obsługą sygnałów. W pliku widzety.py rozbudowujemy klasę Widgety:

Plik widzety.py. Kod nr
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Widgety(QWidget, Ui_Widget):
    """ Główna klasa aplikacji """

    def __init__(self, parent=None):
        super(Widgety, self).__init__(parent)
        self.setupUi(self)  # tworzenie interfejsu

        # Sygnały i sloty ###
        # przyciski CheckBox ###
        self.grupaChk.buttonClicked[int].connect(self.ustawKsztalt)
        self.ksztaltChk.clicked.connect(self.aktywujKsztalt)

    def ustawKsztalt(self, wartosc):
        self.ksztaltAktywny.ustawKsztalt(wartosc)

    def aktywujKsztalt(self, wartosc):
        nadawca = self.sender()
        if wartosc:
            self.ksztaltAktywny = self.ksztalt1
            nadawca.setText('<=')
        else:
            self.ksztaltAktywny = self.ksztalt2
            nadawca.setText('=>')
        self.grupaChk.buttons()[self.ksztaltAktywny.ksztalt].setChecked(True)

Na początku kliknięcie któregokolwiek z przycisków wyboru wiążemy z funkcją ustawKsztalt: self.grupaChk.buttonClicked[int].connect(self.ustawKsztalt). Zapis buttonClicked[int] oznacza, że dany sygnał może przekazać do slotu różne dane. W tym wypadku będzie to indeks klikniętego przycisku, czyli liczba całkowita. Gdybyśmy chcieli otrzymać tekst przycisku, użylibyśmy konstrukcji buttonClicked[str]. W slocie ustawKsztalt() otrzymaną wartość używamy do ustawienia rodzaju rysowanej figury za pomocą odpowiedniej metody klasy Ksztalt: self.ksztaltAktywny.ustawKsztalt(wartosc).

Kliknięcie przycisku wskazującego aktywną figurę obsługujemy w kodzie: self.ksztaltChk.clicked.connect(self.aktywujKsztalt). Tym razem funkcja aktywujKsztalt() dostaje wartość logiczną True lub False, która określa, czy przycisk został zaznaczony, czy nie. W zależności od tego ustawiamy jako aktywny odpowiedni obszar rysowania oraz tekst przycisku.

Note

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

Ćwiczenie

Jak zwykle uruchom kilkakrotnie aplikację. Spróbuj zmieniać inicjalne rodzaje domyślnych kształtów i kolory wypełnienia figur.
../../_images/widzety02.png

6.3.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ć.

Importy w pliku gui.py:

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

Teraz rozbudowujemy konstruktor klasy Ui_Widget. Po komentarzu # koniec CheckBox ### wstawiamy:

Plik gui.py. Kod nr
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
        # Slider i LCDNumber ###
        self.suwak = QSlider(Qt.Horizontal)
        self.suwak.setMinimum(0)
        self.suwak.setMaximum(255)
        self.lcd = QLCDNumber()
        self.lcd.setSegmentStyle(QLCDNumber.Flat)
        # układ poziomy (splitter) dla slajdera i lcd
        ukladH2 = QSplitter(Qt.Horizontal, self)
        ukladH2.addWidget(self.suwak)
        ukladH2.addWidget(self.lcd)
        ukladH2.setSizes((125, 75))

        # przyciski RadioButton ###
        self.ukladR = QHBoxLayout()
        for v in ('R', 'G', 'B'):
            self.radio = QRadioButton(v)
            self.ukladR.addWidget(self.radio)
        self.ukladR.itemAt(0).widget().setChecked(True)
        # grupujemy przyciski
        self.grupaRBtn = QGroupBox('Opcje RGB')
        self.grupaRBtn.setLayout(self.ukladR)
        self.grupaRBtn.setObjectName('Radio')
        self.grupaRBtn.setCheckable(True)
        # układ poziomy dla grupy Radio
        ukladH3 = QHBoxLayout()
        ukladH3.addWidget(self.grupaRBtn)
        # koniec RadioButton ###

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 tupli nazwy kanałów: self.radio = QRadioButton(v). Przyciski rozmieszczamy w poziomie (self.ukladR.addWidget(self.radio)).

Pierwszy z nich zaznaczamy: self.ukladR.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.grupaRBtn.setLayout(self.ukladR). Tego typu grupa zapewnia graficzną ramkę z przyciskiem aktywującym typu CheckBox, który domyślnie zaznaczamy: self.grupaRBtn.setCheckable(True). Za pomocą metody setObjectName() grupie nadajemy nazwę Radio.

Kończąc zmiany w interfejsie, tworzymy nowy pionowy układ dla elementów głównego okna aplikacji. Przedostatnią linię self.setLayout(ukladH1) zastępujemy poniższym kodem:

Plik gui.py. Kod nr
71
72
73
74
75
76
77
78
        # główny układ okna, pionowy ###
        ukladOkna = QVBoxLayout()
        ukladOkna.addLayout(ukladH1)
        ukladOkna.addWidget(ukladH2)
        ukladOkna.addLayout(ukladH3)

        self.setLayout(ukladOkna)  # przypisanie układu do okna głównego
        self.setWindowTitle('Widżety')

Ustawienia wstępne i obsługa zdarzeń

Importy w pliku widzety.py:

from PyQt5.QtGui import QColor

Dalej tworzymy dwie zmienne klasy Widgety:

Plik widzety.py. Kod nr
10
11
12
13
14
class Widgety(QWidget, Ui_Widget):
    """ Główna klasa aplikacji """

    kanaly = {'R'}  # zbiór kanałów
    kolorW = QColor(0, 0, 0)  # kolor RGB kształtu 1

Następnie uzupełniamy konstruktor (__init__()), a za nim dopisujemy dwie funkcje:

Plik widzety.py. Kod nr
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
        # Slider + przyciski RadioButton ###
        for i in range(self.ukladR.count()):
            self.ukladR.itemAt(i).widget().toggled.connect(self.ustawKanalRBtn)
        self.suwak.valueChanged.connect(self.zmienKolor)

    def ustawKanalRBtn(self, wartosc):
        self.kanaly = set()  # resetujemy zbiór kanałów
        nadawca = self.sender()
        if wartosc:
            self.kanaly.add(nadawca.text())

    def zmienKolor(self, wartosc):
        self.lcd.display(wartosc)
        if 'R' in self.kanaly:
            self.kolorW.setRed(wartosc)
        if 'G' in self.kanaly:
            self.kolorW.setGreen(wartosc)
        if 'B' in self.kanaly:
            self.kolorW.setBlue(wartosc)

        self.ksztaltAktywny.ustawKolorW(
            self.kolorW.red(),
            self.kolorW.green(),
            self.kolorW.blue())

Ze zmianą stanu przycisków Radio związany jest sygnał toggled. W pętli for i in range(self.ukladR.count()): wiążemy go dla każdego przycisku układu z funkcją ustawKanalRBtn(). Otrzymuje ona wartość logiczną. Zadaniem funkcji jest zresetowanie zbioru kolorów i dodanie do niego litery opisującej zaznaczony przycisk: self.kanaly.add(nadawca.text()).

Manipulowanie suwakiem wyzwala sygnał valueChanged, który łączymy ze slotem zmienKolor(): self.suwak.valueChanged.connect(self.zmienKolor). Do funkcji przekazywana jest wartość wybrana na suwaku, wyświetlamy ją w widżecie LCD: self.lcd.display(wartosc). Następnie sprawdzamy aktywne kanały w zbiorze kanałów i zmieniamy odpowiadającą im wartość składową w kolorze wypełnienia, np.: self.kolorW.setRed(wartosc). Na koniec przypisujemy otrzymany kolor wypełnienia aktywnemu kształtowi, osobno podając składowe RGB.

Przetestuj działanie aplikacji.

../../_images/widzety03.png

6.3.5. ComboBox i SpinBox

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

Importy w pliku gui.py:

from PyQt5.QtWidgets import QComboBox, QSpinBox

Po komentarzu # koniec RadioButton ### uzupełniamy kod funkcji setupUi():

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

Po utworzeniu obiektu listy za pomocą pętli for dodajemy kolejne elementy, czyli litery poszczególnych kanałów: self.listaRGB.addItem(v).

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

Obydwa widżety na razie wyłączamy metodą setEnabled(False). Umieszczamy jeden nad drugim, a ich układ dodajemy obok przycisków Radio, rozdzielając je odstępem 25 px: ukladH3.insertSpacing(1, 25).

W pliku widzety.py dodajemy do konstruktora kod przechwytujący 3 sygnały i dopisujemy dwie nowe funkcje:

Plik widzety.py. Kod nr
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
        # Lista ComboBox i SpinBox ###
        self.grupaRBtn.clicked.connect(self.ustawStan)
        self.listaRGB.activated[str].connect(self.ustawKanalCBox)
        self.spinRGB.valueChanged[int].connect(self.zmienKolor)

    def ustawStan(self, wartosc):
        if wartosc:
            self.listaRGB.setEnabled(False)
            self.spinRGB.setEnabled(False)
        else:
            self.listaRGB.setEnabled(True)
            self.spinRGB.setEnabled(True)
            self.kanaly = set()
            self.kanaly.add(self.listaRGB.currentText())

    def ustawKanalCBox(self, wartosc):
        self.kanaly = set()  # resetujemy zbiór kanałów
        self.kanaly.add(wartosc)

Po uruchomieniu aplikacji aktywna jest tylko grupa przycisków Radio. Kliknięcie tej grupy przechwytujemy: self.grupaRBtn.clicked.connect(self.ustawStan). Funkcja ustawStan() w zależności od zaznaczenia grupy lub jego braku wyłącza (setEnabled(False)) lub włącza (setEnabled(True)) widżety ComboBox i SpinBox. W tym drugim przypadku resetujemy zbiór kanałów i dodajemy do niego tylko kanał wybrany na liście: self.kanaly.add(self.listaRGB.currentText()).

Drugie wydarzenie, które obsłużymy, to wybranie nowego kanału z listy. Emitowany jest wtedy sygnał activated[str], który zawiera tekst wybranego elementu. W slocie ustawKanalCBox() tekst ten, czyli nazwę składowej koloru, dodajemy do zbioru kanałów.

Zmiana wartości w kontrolce SpinBox, czyli sygnał valueChanged[int], przekierowujemy do funkcji zmienKolor(), która obsługuje również zmiany wartości na suwaku.

Uruchom aplikację i sprawdź jej działanie.

../../_images/widzety04.png

6.3.6. Przyciski PushButton

Do tej pory można było zmieniać kolor każdego kanału składowego osobno. Dodamy teraz grupę przycisków typu QPushButton, które zachowywać się będą jak grupa przycisków wielokrotnego wyboru.

Importy w pliku gui.py:

from PyQt5.QtWidgets import QPushButton

Następnie po komentarzu # koniec ComboBox i SpinBox ### dopisujemy kod w funkcji setupUi():

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

Przyciski, jak poprzednio, tworzymy w pętli, podając w konstruktorze litery składowych koloru RGB: self.btn = QPushButton(v). Każdy przycisk przekształcamy na stanowy (może być trwale wciśnięty) za pomocą metody setCheckable(). Kolejne obiekty dodajemy do grupy logicznej typu QButtonGroup: self.grupaP.addButton(self.btn); oraz do układu poziomego. Układ przycisków dodajemy do ramki typu QGropBox z przyciskiem CheckBox: self.grupaPBtn.setCheckable(True). Na początku ramkę wyłączamy: self.grupaPBtn.setChecked(False).

Uwaga: na koniec musimy dodać grupę przycisków do głównego układu okna: ukladOkna.addWidget(self.grupaPBtn). Inaczej nie zobaczymy jej w oknie aplikacji!

W pliku widzety.py jak zwykle dopisujemy obsługę sygnałów w konstruktorze i jedną nową funkcję:

Plik widzety.py. Kod nr
32
33
34
35
36
37
38
39
40
41
42
        # przyciski PushButton ###
        for btn in self.grupaP.buttons():
            btn.clicked[bool].connect(self.ustawKanalPBtn)
        self.grupaPBtn.clicked.connect(self.ustawStan)

    def ustawKanalPBtn(self, wartosc):
        nadawca = self.sender()
        if wartosc:
            self.kanaly.add(nadawca.text())
        elif wartosc in self.kanaly:
            self.kanaly.remove(nadawca.text())

Pętla for btn in self.grupaP.buttons(): odczytuje kolejne przyciski z grupy grupaP, i kliknięcie każdego wiąże z nową funkcją: btn.clicked[bool].connect(self.ustawKanalPBtn). Zadaniem funkcji jest dodawanie kanału do zbioru, jeżeli przycisk został wciśnięty, i usuwanie ich ze zbioru w przeciwnym razie. Inaczej niż w poprzednich funkcjach, obsługujących przyciski Radio i listę ComboBox, nie resetujemy tu zbioru kanałów.

Przetestuj zmodyfikowaną aplikację.

../../_images/widzety05.png

6.3.7. QLabel i QLineEdit

Dodamy do aplikacji zestaw widżetów wyświetlających aktywne kanały jako etykiety typu QLabel oraz wartości składowych koloru jako 1-liniowe pola edycyjne typu QLineEdit.

Importy w pliku gui.py:

from PyQt5.QtWidgets import QLabel, QLineEdit

Następnie po komentarzu # koniec PushButton ### uzupełnij funkcję setupUi():

Plik gui.py. Kod nr
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
        # etykiety QLabel i pola QLineEdit ###
        ukladH4 = QHBoxLayout()
        self.labelR = QLabel('R')
        self.labelG = QLabel('G')
        self.labelB = QLabel('B')
        self.kolorR = QLineEdit('0')
        self.kolorG = QLineEdit('0')
        self.kolorB = QLineEdit('0')
        for v in ('R', 'G', 'B'):
            label = getattr(self, 'label' + v)
            kolor = getattr(self, 'kolor' + v)
            ukladH4.addWidget(label)
            ukladH4.addWidget(kolor)
            kolor.setMaxLength(3)
        # koniec QLabel i QLineEdit ###

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 wypadku 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: kolor.setMaxLength(3).

Uwaga: Pamiętajmy, że aby zobaczyć utworzone obiekty w oknie aplikacji, musimy dołączyć je do głównego układu okna: ukladOkna.addLayout(ukladH4).

W pliku widzety.py rozszerzamy konstruktor klasy Widgety i dodajemy funkcję informacyjną:

Plik widzety.py. Kod nr
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
        # etykiety QLabel i pola QEditLine ###
        for v in ('R', 'G', 'B'):
            kolor = getattr(self, 'kolor' + v)
            kolor.textEdited.connect(self.zmienKolor)

    def info(self):
        fontB = "QWidget { font-weight: bold }"
        fontN = "QWidget { font-weight: normal }"

        for v in ('R', 'G', 'B'):
            label = getattr(self, 'label' + v)
            kolor = getattr(self, 'kolor' + v)
            if v in self.kanaly:
                label.setStyleSheet(fontB)
                kolor.setEnabled(True)
            else:
                label.setStyleSheet(fontN)
                kolor.setEnabled(False)

        self.kolorR.setText(str(self.kolorW.red()))
        self.kolorG.setText(str(self.kolorW.green()))
        self.kolorB.setText(str(self.kolorW.blue()))

W pętli, podobnej jak w pliku interfejsu, sygnał zmiany tekstu pola typu QLineEdit wiążemy z dodaną wcześniej funkcją zmienKolor(). Będziemy mogli wpisywać w tych polach nowe wartości składowych koloru. Ale uwaga: do tej pory funkcja zmienKolor() otrzymywała wartości typu całkowitego z suwaka QSlider lub pola QSpinBox. Pole edycyjne zwraca natomiast tekst, który trzeba rzutować na typ całkowity. Dodaj więc na początku funkcji instrukcję: wartosc = int(wartosc).

Druga nowa rzecz to funkcja informacyjna info(). Jej zadanie polega na wyróżnieniu aktywnych kanałów 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 omawiane już funkcje getattr() oraz setEnabled().

Na uwagę zasługują operacje na czcionce. Zmieniamy ją dzięki stylom CSS zdefiniowanym na początku funkcji pod nazwą fontB i fontN. Później przypisujemy je etykietom za pomocą metody setStyleSheet().

Na końcu omawianej funkcji do każdego pola edycyjnego wstawiamy aktualną wartość odpowiedniej składowej koloru przekształconą na tekst, np. self.kolorR.setText(str(self.kolorW.red())).

Wywołanie tej funkcji w postaci self.info() powinniśmy dopisać przynajmniej do funkcji zmienKolor().

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

../../_images/widzety06.png

6.3.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. Po pierwsze pola edycyjne QLineEdit dla składowych zielonej i niebieskiej powinny być na początku nieaktywne. Dodaj odpowiedni kod do pliku gui.py, wykorzystaj metodę setEnabled().
  2. Zaznaczenie jednej z grup przycisków powinno wyłączać drugą grupę. Jeżeli aktywujemy grupę Push dobrze byłoby zaznaczyć przycisk odpowiadający ostatniemu aktywnemu kanałowi. W tym celu trzeba uzupełnić funkcję ustawStan(). Spróbuj użyć poniższego kodu:
nadawca = self.sender()
if nadawca.objectName() == 'Radio':
    self.grupaPBtn.setChecked(False)
if nadawca.objectName() == 'Push':
    self.grupaRBtn.setChecked(False)
    for btn in self.grupaP.buttons():
        btn.setChecked(False)
        if btn.text() in self.kanaly:
            btn.setChecked(True)

Ponieważ w(y)łączanie ramek z przyciskami obsługujemy w jednym slocie, musimy wiedzieć, która ramka wysłała sygnał. Metoda self.sender() zwraca nam nadawcę, a za pomocą metody objectName() możemy odczytać jego nazwę.

Jeżeli ramką źródłową jest ta z przyciskami PushButton, w pętli for btn in self.grupaP.buttons(): na początku odznaczamy każdy przycisk po to, żeby zaznaczyć go, o ile wskazywany przez niego kanał jest w zbiorze.

  1. Stan pól edycyjnych powinien odpowiadać stanowi przycisków PushButton, wciśnięty przycisk to aktywne pole i odwrotnie. Dopisz odpowiedni kod do slotu ustawKanalPBtn(). Wykorzystaj funkcję getattr, aby uzyskać dostęp do właściwego pola edycyjnego.
  2. Funkcja zmienKolor() nie jest zabezpieczona przed błędnymi danymi wprowadzanymi do pól edycyjnych. Prześledź komunikaty w konsoli pojawiające się po wpisaniu wartości ujemnych, albo tekstu. Sytuacje takie można obsłużyć dopisując na początku funkcji np. taki kod:
try:
    wartosc = int(wartosc)
except ValueError:
    wartosc = 0
if wartosc > 255:
    wartosc = 255
  1. Jak zostało pokazane w aplikacji, nic nie stoi na przeszkodzie, żeby podobne sygnały obsługiwane były przez jeden slot. Niekiedy jednak wymaga to pewnych dodatkowych zabiegów. Można by na przykład spróbować połączyć sloty ustawKanalRBtn() i ustawKanalCBox() w jeden ustawKanal(), który mógłby zostać zaimplementowany tak:
def ustawKanal(self, wartosc):
    self.kanaly = set()  # resetujemy zbiór kanałów
    try:  # ComboBox
        if len(wartosc) == 1:
            self.kanaly.add(wartosc)
    except TypeError:  # RadioButton
        nadawca = self.sender()
        if wartosc:
            self.kanaly.add(nadawca.text())
  1. Dodaj dwa osobne przyciski, które umożliwią kopiowanie koloru i kształtu z jednej figury na drugą.

6.3.9. Materiały

  1. Qt Widgets
  2. Widgets Tutorial
  3. Layout Management

Źródła:


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:2017-09-08 o 19:38 w Sphinx 1.4.5
Autorzy:Patrz plik “Autorzy”