6.1. Kalkulator¶
Prosta 1-okienkowa aplikacja ilustrująca podstawy tworzenia interfejsu graficznego i obsługi działań użytkownika za pomocą Pythona 3, PyQt5 i biblioteki Qt5. Przykład wprowadza również podstawy programowania obiektowego (ang. Object Oriented Programing).

6.1.1. Pokaż okno¶
Zaczynamy od utworzenia pliku o nazwie kalkulator.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 24 25 | #!/usr/bin/python3
# -*- coding: utf-8 -*-
from PyQt5.QtWidgets import QApplication, QWidget
class Kalkulator(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.interfejs()
def interfejs(self):
self.resize(300, 100)
self.setWindowTitle("Prosty kalkulator")
self.show()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
okno = Kalkulator()
sys.exit(app.exec_())
|
Import from __future__ import unicode_literals
ułatwi nam obsługę napisów zawierających
znaki narodowe, np. polskie “ogonki”.
Podstawą naszego programu będzie moduł PyQt5.QtWidgets
, z którego importujemy
klasy QApplication
i QWidget
– podstawową klasę wszystkich elementów interfejsu graficznego.
Wygląd okna naszej aplikacji definiować będziemy za pomocą klasy Kalkulator
dziedziczącej (zob. dziedziczenie) właściwości i metody z klasy QWidget (class Kalkulator(QWidget)
).
Instrukcja super(Kalkulator, self).__init__(parent)
zwraca nam klasę rodzica i wywołuje jego konstruktor.
Z kolei w konstruktorze naszej klasy wywołujemy metodę interfejs()
,
w której tworzyć będziemy GUI naszej aplikacji. Ustawiamy więc właściwości
okna aplikacji i jego zachowanie:
self.resize(300, 100)
– szerokość i wysokość okna;setWindowTitle("Prosty kalkulator")
) – tytuł okna;self.show()
– wyświetlenie okna na ekranie.
Informacja
Słowa self
używamy wtedy, kiedy odnosimy się do właściwości lub metod,
również odziedziczonych, jej instancji, czyli obiektów.
Słowo to zawsze występuje jako pierwszy parametr metod obiektu definiowanych
jako funkcje w definicji klasy. Zob. What is self?
Aby uruchomić program, tworzymy obiekt reprezentujący aplikację: app = QApplication(sys.argv)
.
Aplikacja może otrzymywać parametry z linii poleceń (sys.argv
). Tworzymy również
obiekt reprezentujący okno aplikacji, czyli instancję klasy Kalkulator: okno = Kalkulator()
.
Na koniec uruchamiamy główną pętlę programu (app.exec_()
), która rozpoczyna obsługę
zdarzeń (zob. główna pętla programu). Zdarzenia (np. kliknięcia) generowane są przez
system lub użytkownika i przekazywane do widżetów aplikacji, które mogą je obsługiwać.
Informacja
Jeżeli jakaś metoda, np. exec_()
, ma na końcu podkreślenie, to dlatego, że jej nazwa
pokrywa się z zarezerwowanym słowem kluczowym Pythona. Podkreślenie służy
ich rozróżnieniu.
Poprawne zakończenie aplikacji zapewniające zwrócenie informacji o jej stanie do systemu
zapewnia metoda sys.exit()
.
Przetestujmy kod. Program uruchamiamy poleceniem wydanym w terminalu w katalogu ze skryptem:
~$ python3 kalkulator.py

6.1.2. Widżety¶
Puste okno być może nie robi wrażenia, zobaczymy więc, jak tworzyć widżety (zob. widżet). Najprostszym przykładem będą etykiety.
Dodajemy wymagane importy i rozbudowujemy metodę interfejs()
:
5 6 | from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QLabel, QGridLayout
|
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | def interfejs(self):
# etykiety
etykieta1 = QLabel("Liczba 1:", self)
etykieta2 = QLabel("Liczba 2:", self)
etykieta3 = QLabel("Wynik:", self)
# przypisanie widgetów do układu tabelarycznego
ukladT = QGridLayout()
ukladT.addWidget(etykieta1, 0, 0)
ukladT.addWidget(etykieta2, 0, 1)
ukladT.addWidget(etykieta3, 0, 2)
# przypisanie utworzonego układu do okna
self.setLayout(ukladT)
self.setGeometry(20, 20, 300, 100)
self.setWindowIcon(QIcon('kalkulator.png'))
self.setWindowTitle("Prosty kalkulator")
self.show()
|
Dodawanie etykiet zaczynamy od utworzenia obiektów na podstawie odpowiedniej klasy,
w tym wypadku QtLabel. Do jej konstruktora
przekazujemy tekst, który ma się wyświetlać na etykiecie, np.: etykieta1 = QLabel("Liczba 1:", self)
.
Opcjonalny drugi argument wskazuje obiekt rodzica danej kontrolki.
Później tworzymy pomocniczy obiekt służący do rozmieszczenia etykiet w układzie
tabelarycznym: ukladT = QGridLayout()
. Kolejne etykiety dodajemy do niego za
pomocą metody addWidget()
. Przyjmuje ona nazwę obiektu oraz numer wiersza i kolumny
definiujących komórkę, w której znaleźć się ma obiekt. Zdefiniowany układ
(ang. layout) musimy powiązać z oknem naszej aplikacji: self.setLayout(ukladT)
.
Na koniec używamy metody setGeometry()
do określenia położenia okna aplikacji
(początek układu jest w lewym górnym rogu ekranu) i jego rozmiaru (szerokość, wysokość).
Dodajemy również ikonę pokazywaną w pasku tytułowym lub w miniaturze na pasku zadań:
self.setWindowIcon(QIcon('kalkulator.png'))
.
Informacja
Plik graficzny z ikoną musimy pobrać
i umieścić w katalogu
z aplikacją, czyli ze skryptem kalkulator.py
.
Przetestuj wprowadzone zmiany.

6.1.3. Interfejs¶
Dodamy teraz pozostałe widżety tworzące graficzny interfejs naszej aplikacji. Jak zwykle, zaczynamy od zaimportowania potrzebnych klas:
7 | from PyQt5.QtWidgets import QLineEdit, QPushButton, QHBoxLayout
|
Następnie przed instrukcją self.setLayout(ukladT)
wstawiamy następujący kod:
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 | # 1-liniowe pola edycyjne
self.liczba1Edt = QLineEdit()
self.liczba2Edt = QLineEdit()
self.wynikEdt = QLineEdit()
self.wynikEdt.readonly = True
self.wynikEdt.setToolTip('Wpisz <b>liczby</b> i wybierz działanie...')
ukladT.addWidget(self.liczba1Edt, 1, 0)
ukladT.addWidget(self.liczba2Edt, 1, 1)
ukladT.addWidget(self.wynikEdt, 1, 2)
# przyciski
dodajBtn = QPushButton("&Dodaj", self)
odejmijBtn = QPushButton("&Odejmij", self)
dzielBtn = QPushButton("&Mnóż", self)
mnozBtn = QPushButton("D&ziel", self)
koniecBtn = QPushButton("&Koniec", self)
koniecBtn.resize(koniecBtn.sizeHint())
ukladH = QHBoxLayout()
ukladH.addWidget(dodajBtn)
ukladH.addWidget(odejmijBtn)
ukladH.addWidget(dzielBtn)
ukladH.addWidget(mnozBtn)
ukladT.addLayout(ukladH, 2, 0, 1, 3)
ukladT.addWidget(koniecBtn, 3, 0, 1, 3)
|
Jak widać, dodawanie widżetów polega zazwyczaj na:
- utworzeniu obiektu na podstawie klasy opisującej potrzebny element interfejsu, np. QLineEdit – 1-liniowe pole edycyjne, lub QPushButton – przycisk;
- ustawieniu właściwości obiektu, np.
self.wynikEdt.readonly = True
umożliwia tylko odczyt tekstu pola,self.wynikEdt.setToolTip('Wpisz <b>liczby</b> i wybierz działanie...')
– ustawia podpowiedź, akoniecBtn.resize(koniecBtn.sizeHint())
– sugerowany rozmiar obiektu; - przypisaniu obiektu do układu – w powyższym przypadku wszystkie przyciski działań dodano
do układu horyzontalnego QHBoxLayout, ponieważ przycisków jest 4, a dopiero jego instancję do układu tabelarycznego:
ukladT.addLayout(ukladH, 2, 0, 1, 3)
. Liczby w tym przykładzie oznaczają odpowiednio wiersz i kolumnę, tj. komórkę, do której wstawiamy obiekt, a następnie ilość wierszy i kolumn, które chcemy wykorzystać.
Informacja
Jeżeli chcemy mieć dostęp do właściwości obiektów interfejsu w zasięgu całej klasy,
czyli w innych funkcjach, obiekty musimy definiować jako składowe klasy, a więc
poprzedzone słowem self
, np.: self.liczba1Edt = QLineEdit()
.
W powyższym kodzie, np. dodajBtn = QPushButton("&Dodaj", self)
, widać również, że tworząc obiekty można
określać ich rodzica (ang. parent), tzn. widżet nadrzędny, w tym wypadku self
, czyli okno główne
(ang. toplevel window). Bywa to przydatne zwłaszcza przy bardziej złożonych interfejsach.
Znak &
przed jakąś literą w opisie przycisków tworzy z kolei skrót klawiaturowy dostępny po naciśnięciu ALT + litera
.
Po uruchomieniu programu powinniśmy zobaczyć okno podobne do poniższego:

6.1.4. Zamykanie programu¶
Mamy okienko z polami edycyjnymi i przyciskami, ale kontrolki te na nic nie reagują. Nauczymy się więc obsługiwać poszczególne zdarzenia. Zacznijmy od zamykania aplikacji.
Na początku zaimportujmy klasę QMessageBox pozwalającą tworzyć komunikaty oraz przestrzeń nazw Qt zawierającą różne stałe:
8 9 | from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtCore import Qt
|
Dalej po instrukcji self.setLayout(ukladT)
w metodzie interfejs()
dopisujemy:
64 | koniecBtn.clicked.connect(self.koniec)
|
– instrukcja ta wiąże kliknięcie przycisku “Koniec” z wywołaniem metody koniec()
,
którą musimy dopisać na końcu klasy Kalkulator()
:
71 72 | def koniec(self):
self.close()
|
Funkcja koniec()
, obsługująca wydarzenie (ang. event) kliknięcia przycisku,
wywołuje po prostu metodę close()
okna głównego.
Informacja
Omówiony fragment kodu ilustruje mechanizm zwany sygnały i sloty (ang. signals & slots). Zapewnia on komunikację między obiektami. Sygnał powstaje w momencie wystąpienia jakiegoś wydarzenia, np. kliknięcia. Slot może z kolei być wbudowaną w Qt funkcją lub Pythonowym wywołaniem (ang. callable), np. klasą lub metodą.
Zamknięcie okna również jest rodzajem wydarzenia (QCloseEvent),
które można przechwycić. Np. po to, aby zapobiec utracie niezapisanych danych.
Do klasy Kalkulator()
dopiszmy następujący kod:
74 75 76 77 78 79 80 81 82 83 84 | def closeEvent(self, event):
odp = QMessageBox.question(
self, 'Komunikat',
"Czy na pewno koniec?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if odp == QMessageBox.Yes:
event.accept()
else:
event.ignore()
|
W nadpisanej metodzie closeEvent()
wyświetlamy użytkownikowi prośbę o potwierdzenie zamknięcia
za pomocą metody question()
(ang. pytanie) klasy QMessageBox.
Do konstruktora metody przekazujemy:
- obiekt rodzica –
self
oznacza okno główne; - tytuł kona dialogowego;
- komunikat dla użytkownika, np. pytanie;
- kombinację standardowych przycisków, np.
QMessageBox.Yes | QMessageBox.No
; - przycisk domyślny –
QMessageBox.No
.
Udzielona odpowiedź odp
, np. kliknięcie przycisku “Tak”, decyduje o zezwoleniu
na obsłużenie wydarzenia event.accept()
lub odrzuceniu go event.ignore()
.
Może wygodnie byłoby zamykać aplikację naciśnięciem klawisza ESC
?
Dopiszmy jeszcze jedną funkcję:
86 87 88 | def keyPressEvent(self, e):
if e.key() == Qt.Key_Escape:
self.close()
|
Podobnie jak w przypadku closeEvent()
tworzymy własną wersję funkcji
keyPressEvent obsługującej
naciśnięcia klawiszy QKeyEvent.
Sprawdzamy naciśnięty klawisz if e.key() == Qt.Key_Escape:
i zamykamy okno.
Przetestuj działanie aplikacji.

6.1.5. Działania¶
Kalkulator powinien liczyć. Zaczniemy od dodawania, ale na początku wszystkie
sygnały wygenerowane przez przyciski działań połączymy z jednym slotem.
Pod instrukcją koniecBtn.clicked.connect(self.koniec)
dodajemy:
65 66 67 68 | dodajBtn.clicked.connect(self.dzialanie)
odejmijBtn.clicked.connect(self.dzialanie)
mnozBtn.clicked.connect(self.dzialanie)
dzielBtn.clicked.connect(self.dzialanie)
|
Następnie zaczynamy implementację funkcji dzialanie()
. Na końcu klasy Kalkulator()
dodajemy:
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | def dzialanie(self):
nadawca = self.sender()
try:
liczba1 = float(self.liczba1Edt.text())
liczba2 = float(self.liczba2Edt.text())
wynik = ""
if nadawca.text() == "&Dodaj":
wynik = liczba1 + liczba2
else:
pass
self.wynikEdt.setText(str(wynik))
except ValueError:
QMessageBox.warning(self, "Błąd", "Błędne dane", QMessageBox.Ok)
|
Ponieważ jedna funkcja ma obsłużyć cztery sygnały, musimy znać źródło sygnału (ang. source),
czyli nadawcę (ang. sender): nadawca = self.sender()
.
Dalej rozpoczynamy blok try: except:
– użytkownik może wprowadzić błędne dane,
tj. pusty ciąg znaków lub ciąg, którego nie da się przekształcić na liczbę zmiennoprzecinkową (float()
).
W przypadku wyjątku, wyświetlamy ostrzeżenie o błędnych danych: QMessageBox.warning()
Jeżeli dane są liczbami, sprawdzamy nadawcę (if nadawca.text() == "&Dodaj":
)
i jeżeli jest to przycisk dodawania, obliczamy sumę wynik = liczba1 + liczba2
.
Na koniec wyświetlamy ją po zamianie na tekst (str()
) w polu tekstowym za pomocą
metody setText()
: self.wynikEdt.setText(str(wynik))
.
Sprawdź działanie programu.

Dopiszemy obsługę pozostałych działań. Instrukcję warunkową w funkcji dzialanie()
rozbudowujemy następująco:
103 104 105 106 107 108 109 110 111 112 113 114 115 | if nadawca.text() == "&Dodaj":
wynik = liczba1 + liczba2
elif nadawca.text() == "&Odejmij":
wynik = liczba1 - liczba2
elif nadawca.text() == "&Mnóż":
wynik = liczba1 * liczba2
else: # dzielenie
try:
wynik = round(liczba1 / liczba2, 9)
except ZeroDivisionError:
QMessageBox.critical(
self, "Błąd", "Nie można dzielić przez zero!")
return
|
Na uwagę zasługuje tylko dzielenie. Po pierwsze określamy dokładność dzielenia do 9
miejsc po przecinku round(liczba1 / liczba2, 9)
. Po drugie zabezpieczamy się
przed dzieleniem przez zero. Znów wykorzystujemy konstrukcję try: except:
,
w której przechwytujemy wyjątek ZeroDivisionError
i wyświetlamy odpowiednie ostrzeżenie.
Pozostaje przetestować aplikację.

Wskazówka
Jeżeli po zaimplementowaniu działań, aplikacja po uruchomieniu nie aktywuje kursora
w pierwszym polu edycyjnym, należy tuż przed ustawianiem właściwości okna głównego
(self.setGeometry()
) umieścić wywołanie self.liczba1Edt.setFocus()
,
które ustawia focus na wybranym elemencie.
6.1.6. Materiały¶
- Strona główna dokumentacji Qt5
- Lista klas Qt5
- PyQt5 Reference Guide
- Przykłady PyQt5
- Signals and slots
- Kody klawiszy
Ź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” |