6.2. 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).
Uwaga
Wymagana wiedza:
Znajomość Pythona w stopniu średnim.
Znajomość podstaw projektowania interfejsu z wykorzystaniem bibliotek Qt (zob. scenariusz Kalkulator).
6.2.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:
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4from __future__ import unicode_literals
5from PyQt5.QtWidgets import QApplication, QWidget
6from gui import Ui_Widget
7
8
9class Widgety(QWidget, Ui_Widget):
10 """ Główna klasa aplikacji """
11
12 def __init__(self, parent=None):
13 super(Widgety, self).__init__(parent)
14 self.setupUi(self) # tworzenie interfejsu
15
16if __name__ == '__main__':
17 import sys
18
19 app = QApplication(sys.argv)
20 okno = Widgety()
21 okno.show()
22
23 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:
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4from __future__ import unicode_literals
5from PyQt5.QtGui import QPainter, QColor
6from PyQt5.QtCore import QRect
7
8
9class Ui_Widget(object):
10 """ Klasa definiująca GUI """
11
12 def setupUi(self, Widget):
13
14 self.ksztalt = Ksztalty.Ellipse # kształt do narysowania
15 self.prost = QRect(1, 1, 101, 101) # współrzędne prostokąta
16 # kolor obramowania i wypełnienia w formacie RGB
17 self.kolorO = QColor(0, 0, 0)
18 self.kolorW = QColor(200, 30, 40)
19
20 self.resize(102, 102)
21 self.setWindowTitle('Widżety')
22
23 def paintEvent(self, e):
24 qp = QPainter()
25 qp.begin(self)
26 self.rysujFigury(e, qp)
27 qp.end()
28
29 def rysujFigury(self, e, qp):
30 qp.setPen(self.kolorO) # kolor obramowania
31 qp.setBrush(self.kolorW) # kolor wypełnienia
32 qp.setRenderHint(QPainter.Antialiasing) # wygładzanie kształtu
33
34 if self.ksztalt == Ksztalty.Rect:
35 qp.drawRect(self.prost)
36 elif self.ksztalt == Ksztalty.Ellipse:
37 qp.drawEllipse(self.prost)
38
39
40class Ksztalty:
41 """ Klasa pomocnicza, symuluje typ wyliczeniowy """
42 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.
Uwaga
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.

6.2.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:
1# -*- coding: utf-8 -*-
2
3from PyQt5.QtWidgets import QWidget
4from PyQt5.QtGui import QPainter, QColor, QPolygon
5from PyQt5.QtCore import QPoint, QRect, QSize
6
7
8class Ksztalty:
9 """ Klasa pomocnicza, symuluje typ wyliczeniowy """
10 Rect, Ellipse, Polygon, Line = range(4)
11
12
13class Ksztalt(QWidget):
14 """ Klasa definiująca widget do rysowania kształtów """
15 # współrzędne prostokąta i trójkąta
16 prost = QRect(1, 1, 101, 101)
17 punkty = QPolygon([
18 QPoint(1, 101), # punkt początkowy (x, y)
19 QPoint(51, 1),
20 QPoint(101, 101)])
21
22 def __init__(self, parent, ksztalt=Ksztalty.Rect):
23 super(Ksztalt, self).__init__(parent)
24
25 # kształt do narysowania
26 self.ksztalt = ksztalt
27 # kolor obramowania i wypełnienia w formacie RGB
28 self.kolorO = QColor(0, 0, 0)
29 self.kolorW = QColor(255, 255, 255)
30
31 def paintEvent(self, e):
32 qp = QPainter()
33 qp.begin(self)
34 self.rysujFigury(e, qp)
35 qp.end()
36
37 def rysujFigury(self, e, qp):
38 qp.setPen(self.kolorO) # kolor obramowania
39 qp.setBrush(self.kolorW) # kolor wypełnienia
40 qp.setRenderHint(QPainter.Antialiasing) # wygładzanie kształtu
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.Polygon:
47 qp.drawPolygon(self.punkty)
48 elif self.ksztalt == Ksztalty.Line:
49 qp.drawLine(self.prost.topLeft(), self.prost.bottomRight())
50 else: # kształt domyślny Rect
51 qp.drawRect(self.prost)
52
53 def sizeHint(self):
54 return QSize(102, 102)
55
56 def minimumSizeHint(self):
57 return QSize(102, 102)
58
59 def ustawKsztalt(self, ksztalt):
60 self.ksztalt = ksztalt
61 self.update()
62
63 def ustawKolorW(self, r=0, g=0, b=0):
64 self.kolorW = QColor(r, g, b)
65 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 atrybutpunkty
naszej klasy;qp.drawLine()
– pozwala narysować linię wyznaczoną przez współrzędne punktu początkowego i końcowego typuQPoint
; 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
.
Informacja
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ć:
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4from __future__ import unicode_literals
5from ksztalty import Ksztalty, Ksztalt
6from PyQt5.QtWidgets import QHBoxLayout
7
8
9class Ui_Widget(object):
10 """ Klasa definiująca GUI """
11
12 def setupUi(self, Widget):
13
14 # widget rysujący kształty, instancja klasy Ksztalt
15 self.ksztalt = Ksztalt(self, Ksztalty.Polygon)
16
17 # układ poziomy, zawiera: self.ksztalt
18 ukladH1 = QHBoxLayout()
19 ukladH1.addWidget(self.ksztalt)
20
21 self.setLayout(ukladH1) # przypisanie układu do okna głównego
22 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.

Informacja
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.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 PyQt5.QtWidgets import QCheckBox, QButtonGroup, QVBoxLayout
Funkcja setupUi()
przyjmuje następującą postać:
13 def setupUi(self, Widget):
14
15 # widgety rysujące kształty, instancje klasy Ksztalt
16 self.ksztalt1 = Ksztalt(self, Ksztalty.Polygon)
17 self.ksztalt2 = Ksztalt(self, Ksztalty.Ellipse)
18 self.ksztaltAktywny = self.ksztalt1
19
20 # przyciski CheckBox ###
21 uklad = QVBoxLayout() # układ pionowy
22 self.grupaChk = QButtonGroup()
23 for i, v in enumerate(('Kwadrat', 'Koło', 'Trójkąt', 'Linia')):
24 self.chk = QCheckBox(v)
25 self.grupaChk.addButton(self.chk, i)
26 uklad.addWidget(self.chk)
27 self.grupaChk.buttons()[self.ksztaltAktywny.ksztalt].setChecked(True)
28 # CheckBox do wyboru aktywnego kształtu
29 self.ksztaltChk = QCheckBox('<=')
30 self.ksztaltChk.setChecked(True)
31 uklad.addWidget(self.ksztaltChk)
32
33 # układ poziomy dla kształtów oraz przycisków CheckBox
34 ukladH1 = QHBoxLayout()
35 ukladH1.addWidget(self.ksztalt1)
36 ukladH1.addLayout(uklad)
37 ukladH1.addWidget(self.ksztalt2)
38 # koniec CheckBox ###
39
40 self.setLayout(ukladH1) # przypisanie układu do okna głównego
41 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
:
9class Widgety(QWidget, Ui_Widget):
10 """ Główna klasa aplikacji """
11
12 def __init__(self, parent=None):
13 super(Widgety, self).__init__(parent)
14 self.setupUi(self) # tworzenie interfejsu
15
16 # Sygnały i sloty ###
17 # przyciski CheckBox ###
18 self.grupaChk.buttonClicked[int].connect(self.ustawKsztalt)
19 self.ksztaltChk.clicked.connect(self.aktywujKsztalt)
20
21 def ustawKsztalt(self, wartosc):
22 self.ksztaltAktywny.ustawKsztalt(wartosc)
23
24 def aktywujKsztalt(self, wartosc):
25 nadawca = self.sender()
26 if wartosc:
27 self.ksztaltAktywny = self.ksztalt1
28 nadawca.setText('<=')
29 else:
30 self.ksztaltAktywny = self.ksztalt2
31 nadawca.setText('=>')
32 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.
Informacja
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.

6.2.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()
:
72 # Lista ComboBox i SpinBox ###
73 self.listaRGB = QComboBox(self)
74 for v in ('R', 'G', 'B'):
75 self.listaRGB.addItem(v)
76 self.listaRGB.setEnabled(False)
77 # SpinBox
78 self.spinRGB = QSpinBox()
79 self.spinRGB.setMinimum(0)
80 self.spinRGB.setMaximum(255)
81 self.spinRGB.setEnabled(False)
82 # układ pionowy dla ComboBox i SpinBox
83 uklad = QVBoxLayout()
84 uklad.addWidget(self.listaRGB)
85 uklad.addWidget(self.spinRGB)
86 # do układu poziomego grupy Radio dodajemy układ ComboBox i SpinBox
87 ukladH3.insertSpacing(1, 25)
88 ukladH3.addLayout(uklad)
89 # 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:
28 # Lista ComboBox i SpinBox ###
29 self.grupaRBtn.clicked.connect(self.ustawStan)
30 self.listaRGB.activated[str].connect(self.ustawKanalCBox)
31 self.spinRGB.valueChanged[int].connect(self.zmienKolor)
32
33 def ustawStan(self, wartosc):
34 if wartosc:
35 self.listaRGB.setEnabled(False)
36 self.spinRGB.setEnabled(False)
37 else:
38 self.listaRGB.setEnabled(True)
39 self.spinRGB.setEnabled(True)
40 self.kanaly = set()
41 self.kanaly.add(self.listaRGB.currentText())
42
43 def ustawKanalCBox(self, wartosc):
44 self.kanaly = set() # resetujemy zbiór kanałów
45 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.

6.2.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()
:
110 # etykiety QLabel i pola QLineEdit ###
111 ukladH4 = QHBoxLayout()
112 self.labelR = QLabel('R')
113 self.labelG = QLabel('G')
114 self.labelB = QLabel('B')
115 self.kolorR = QLineEdit('0')
116 self.kolorG = QLineEdit('0')
117 self.kolorB = QLineEdit('0')
118 for v in ('R', 'G', 'B'):
119 label = getattr(self, 'label' + v)
120 kolor = getattr(self, 'kolor' + v)
121 ukladH4.addWidget(label)
122 ukladH4.addWidget(kolor)
123 kolor.setMaxLength(3)
124 # 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ą:
36 # etykiety QLabel i pola QEditLine ###
37 for v in ('R', 'G', 'B'):
38 kolor = getattr(self, 'kolor' + v)
39 kolor.textEdited.connect(self.zmienKolor)
40
41 def info(self):
42 fontB = "QWidget { font-weight: bold }"
43 fontN = "QWidget { font-weight: normal }"
44
45 for v in ('R', 'G', 'B'):
46 label = getattr(self, 'label' + v)
47 kolor = getattr(self, 'kolor' + v)
48 if v in self.kanaly:
49 label.setStyleSheet(fontB)
50 kolor.setEnabled(True)
51 else:
52 label.setStyleSheet(fontN)
53 kolor.setEnabled(False)
54
55 self.kolorR.setText(str(self.kolorW.red()))
56 self.kolorG.setText(str(self.kolorW.green()))
57 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.

6.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.
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()
.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.
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.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
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()
iustawKanalCBox()
w jedenustawKanal()
, 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())
Dodaj dwa osobne przyciski, które umożliwią kopiowanie koloru i kształtu z jednej figury na drugą.
6.2.9. Materiały
Źródła:
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:
2025-04-12 o 10:21 w Sphinx 7.3.7
- Autorzy: