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 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:
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.
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 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 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 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.

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 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
:
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.
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.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:
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:
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:
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:
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.

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 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:
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.

6.2.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()
:
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ę:
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ę.

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 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ą:
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.

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: | 2022-05-22 o 19:52 w Sphinx 1.5.3 |
---|---|
Autorzy: | Patrz plik “Autorzy” |