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#!/usr/bin/python3
2# -*- coding: utf-8 -*-
3
4from PyQt5.QtWidgets import QApplication, QWidget
5
6
7class Kalkulator(QWidget):
8 def __init__(self, parent=None):
9 super().__init__(parent)
10
11 self.interfejs()
12
13 def interfejs(self):
14
15 self.resize(300, 100)
16 self.setWindowTitle("Prosty kalkulator")
17 self.show()
18
19
20if __name__ == '__main__':
21 import sys
22
23 app = QApplication(sys.argv)
24 okno = Kalkulator()
25 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()
:
5from PyQt5.QtGui import QIcon
6from PyQt5.QtWidgets import QLabel, QGridLayout
16 def interfejs(self):
17
18 # etykiety
19 etykieta1 = QLabel("Liczba 1:", self)
20 etykieta2 = QLabel("Liczba 2:", self)
21 etykieta3 = QLabel("Wynik:", self)
22
23 # przypisanie widgetów do układu tabelarycznego
24 ukladT = QGridLayout()
25 ukladT.addWidget(etykieta1, 0, 0)
26 ukladT.addWidget(etykieta2, 0, 1)
27 ukladT.addWidget(etykieta3, 0, 2)
28
29 # przypisanie utworzonego układu do okna
30 self.setLayout(ukladT)
31
32 self.setGeometry(20, 20, 300, 100)
33 self.setWindowIcon(QIcon('kalkulator.png'))
34 self.setWindowTitle("Prosty kalkulator")
35 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:
7from PyQt5.QtWidgets import QLineEdit, QPushButton, QHBoxLayout
Następnie przed instrukcją self.setLayout(ukladT)
wstawiamy następujący kod:
30 # 1-liniowe pola edycyjne
31 self.liczba1Edt = QLineEdit()
32 self.liczba2Edt = QLineEdit()
33 self.wynikEdt = QLineEdit()
34
35 self.wynikEdt.readonly = True
36 self.wynikEdt.setToolTip('Wpisz <b>liczby</b> i wybierz działanie...')
37
38 ukladT.addWidget(self.liczba1Edt, 1, 0)
39 ukladT.addWidget(self.liczba2Edt, 1, 1)
40 ukladT.addWidget(self.wynikEdt, 1, 2)
41
42 # przyciski
43 dodajBtn = QPushButton("&Dodaj", self)
44 odejmijBtn = QPushButton("&Odejmij", self)
45 dzielBtn = QPushButton("&Mnóż", self)
46 mnozBtn = QPushButton("D&ziel", self)
47 koniecBtn = QPushButton("&Koniec", self)
48 koniecBtn.resize(koniecBtn.sizeHint())
49
50 ukladH = QHBoxLayout()
51 ukladH.addWidget(dodajBtn)
52 ukladH.addWidget(odejmijBtn)
53 ukladH.addWidget(dzielBtn)
54 ukladH.addWidget(mnozBtn)
55
56 ukladT.addLayout(ukladH, 2, 0, 1, 3)
57 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:
8from PyQt5.QtWidgets import QMessageBox
9from 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 def koniec(self):
72 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 def closeEvent(self, event):
75
76 odp = QMessageBox.question(
77 self, 'Komunikat',
78 "Czy na pewno koniec?",
79 QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
80
81 if odp == QMessageBox.Yes:
82 event.accept()
83 else:
84 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 def keyPressEvent(self, e):
87 if e.key() == Qt.Key_Escape:
88 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 dodajBtn.clicked.connect(self.dzialanie)
66 odejmijBtn.clicked.connect(self.dzialanie)
67 mnozBtn.clicked.connect(self.dzialanie)
68 dzielBtn.clicked.connect(self.dzialanie)
Następnie zaczynamy implementację funkcji dzialanie()
. Na końcu klasy Kalkulator()
dodajemy:
94 def dzialanie(self):
95
96 nadawca = self.sender()
97
98 try:
99 liczba1 = float(self.liczba1Edt.text())
100 liczba2 = float(self.liczba2Edt.text())
101 wynik = ""
102
103 if nadawca.text() == "&Dodaj":
104 wynik = liczba1 + liczba2
105 else:
106 pass
107
108 self.wynikEdt.setText(str(wynik))
109
110 except ValueError:
111 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 if nadawca.text() == "&Dodaj":
104 wynik = liczba1 + liczba2
105 elif nadawca.text() == "&Odejmij":
106 wynik = liczba1 - liczba2
107 elif nadawca.text() == "&Mnóż":
108 wynik = liczba1 * liczba2
109 else: # dzielenie
110 try:
111 wynik = round(liczba1 / liczba2, 9)
112 except ZeroDivisionError:
113 QMessageBox.critical(
114 self, "Błąd", "Nie można dzielić przez zero!")
115 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
Ź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: