4.1. Kalkulator

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

../../_images/kalkulator05.png

Bibliotekę Pyqt6 instalujemy w środowisku wirtualnym Pythona za pomocą polecenia:

(.venv) pip install pyqt6

4.1.1. Pokaż okno

Zaczynamy od utworzenia pliku o nazwie kalkulator.py i wstawiamy do niego poniższy kod:

Kod nr
 1from PyQt6.QtWidgets import QApplication, QWidget
 2
 3
 4class Kalkulator(QWidget):
 5    def __init__(self, parent=None):
 6        super().__init__(parent)
 7        self.interfejs()
 8
 9    def interfejs(self):
10
11        self.resize(300, 100)
12        self.setWindowTitle("Prosty kalkulator")
13        self.show()
14
15
16if __name__ == '__main__':
17    import sys
18    app = QApplication(sys.argv)
19    okno = Kalkulator()
20    sys.exit(app.exec())

Podstawą naszego programu będzie moduł PyQt6.QtWidgets, z którego importujemy klasy QApplication – obsługa aplikacji, 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. Konstruktor naszej klasy na początku wywołuje konstruktor klasy bazowej: super(self).__init__(parent). Następnie wywołujemy metodę interfejs(), w której tworzyć będziemy GUI naszej aplikacji. Ustawiamy 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łowo self to konwencjonalna nazwa używana wtedy, kiedy odnosimy się do właściwości lub metod obiektu utworzonego jako instancja jakiejś klasy. Słowo to zawsze występuje jako pierwszy parametr metod definiowanych jako funkcje klasy. Zob. What is self?

Instrukcja app = QApplication(sys.argv) – tworzy obiekt aplikacji. Argument sys.argv wskazuje, że aplikacja może otrzymywać parametry z linii poleceń. W instrukcji okno = Kalkulator() tworzymy okno aplikacji, czyli obiekt będący instancją klasy 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 są do aplikacji, która może je obsługiwać.

Poprawne zakończenie aplikacji zapewniające zwrócenie informacji o jej stanie do systemu, co zapewnia wywołanie sys.exit().

Przetestujmy kod. Program uruchamiamy poleceniem wydanym w terminalu z aktywnym środowiskiem wirtualnym w katalogu ze skryptem:

~$ python3 kalkulator.py
../../_images/kalkulator01.png

4.1.2. Etykiety

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():

Kod nr
1from PyQt6.QtWidgets import QApplication, QWidget
2from PyQt6.QtGui import QIcon
3from PyQt6.QtWidgets import QLabel, QGridLayout
Kod nr
12    def interfejs(self):
13
14        # etykiety
15        etykieta_1 = QLabel("Liczba 1:", self)
16        etykieta_2 = QLabel("Liczba 2:", self)
17        etykieta_3 = QLabel("Wynik:", self)
18
19        # przypisanie widgetów do układu tabelarycznego
20        uklad_t = QGridLayout()
21        uklad_t.addWidget(etykieta_1, 0, 0)
22        uklad_t.addWidget(etykieta_2, 0, 1)
23        uklad_t.addWidget(etykieta_3, 0, 2)
24
25        # przypisanie utworzonego układu do okna
26        self.setLayout(uklad_t)
27
28        # ustawienia rozmiaru, ikony i tytułu okna
29        self.setGeometry(20, 20, 300, 100)
30        self.setWindowIcon(QIcon('kalkulator.png'))
31        self.setWindowTitle("Prosty kalkulator")

Dodawanie widżetów 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.: etykieta_1 = QLabel("Liczba 1:", self). Opcjonalny drugi argument, omówione wyżej słowo self, wskazuje obiekt rodzica, w tym przypadku okno, w którym ją umieszczamy.

Informacja

Tworzenie widżetów z argumentem self umożliwia dostęp do ich właściwości w zasięgu całej klasy, czyli w innych metodach.

Do rozmieszczania widżetów w oknie służą tzw. układy (ang. layout). Tworzymy więc układ tabelaryczny: uklad_t = QGridLayout() – i dodajemy do niego obiekty (etykiety) za pomocą metody addWidget(). Jako argumenty podajemy nazwę obiektu oraz numer wiersza i kolumny definiujących komórkę, w której znaleźć się ma obiekt. Numeracja wierszy i kolumn zaczyna się od zera. Zdefiniowany układ przypisujemy do okna nadrzędnego: self.setLayout(uklad_t).

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 ze skryptem kalkulator.py.

Przetestuj wprowadzone zmiany.

../../_images/kalkulator02.png

4.1.3. Pola edycyjne i przyciski

Dodamy teraz pozostałe widżety tworzące graficzny interfejs naszej aplikacji, czyli pola edycyjne (zob. QLineEdit) i przyciski (zob. QPushButton). Jak zwykle zaczynamy od zaimportowania potrzebnych klas:

Kod nr
4from PyQt6.QtWidgets import QLineEdit, QPushButton, QHBoxLayout

Dodatkowa klasa QHBoxLayout umożliwi nam rozmieszczenie przycisków w układzie poziomym.

Następnie przed instrukcją self.setLayout(uklad_t) w metodzie interfejs() wstawiamy następujący kod:

Kod nr
28        # 1-liniowe pola edycyjne
29        self.liczba_1 = QLineEdit()
30        self.liczba_2 = QLineEdit()
31        self.wynik = QLineEdit()
32
33        # właściwości dodawanych pól
34        self.wynik.readonly = True
35        self.wynik.setToolTip('Wpisz <b>liczby</b> i wybierz działanie...')
36
37        uklad_t.addWidget(self.liczba_1, 1, 0)
38        uklad_t.addWidget(self.liczba_2, 1, 1)
39        uklad_t.addWidget(self.wynik, 1, 2)
40
41        # przyciski
42        prz_dodaj = QPushButton("&Dodaj", self)
43        prz_odejmij = QPushButton("&Odejmij", self)
44        prz_dziel = QPushButton("&Mnóż", self)
45        prz_mnoz = QPushButton("D&ziel", self)
46        prz_koniec = QPushButton("&Koniec", self)
47
48        uklad_h = QHBoxLayout()
49        uklad_h.addWidget(prz_dodaj)
50        uklad_h.addWidget(prz_odejmij)
51        uklad_h.addWidget(prz_dziel)
52        uklad_h.addWidget(prz_mnoz)
53
54        uklad_t.addLayout(uklad_h, 2, 0, 1, 3)
55        uklad_t.addWidget(prz_koniec, 3, 0, 1, 3)
56
57        # przypisanie utworzonego układu do okna

Jak widać, dodawanie widżetów polega zazwyczaj na:

  • utworzeniu obiektu na podstawie klasy opisującej potrzebny element interfejsu, np. liczba_1 = QLineEdit(self) lub prz_dodaj = QPushButton("&Dodaj", self);

  • ustawieniu właściwości obiektu, np. self.wynik.readonly = True umożliwia tylko odczyt tekstu pola, self.wynik.setToolTip('Wpisz <b>liczby</b> i wybierz działanie...') – definiuje podpowiedź, która pojawia się po najechaniu kursorem na widżet;

  • przypisaniu obiektu do układu – ponieważ mamy 4 przyciski działań, tworzymy układ horyzontalny (zob. QHBoxLayout): uklad_h = QHBoxLayout(), dodajemy do niego przyciski za pomocą metody addWidget() i dodajemy go do układu tabelarycznego: uklad_t.addLayout(uklad_h, 2, 0, 1, 3). Argumenty liczbowe w metodzie addLayout() oznaczają odpowiednio wiersz i kolumnę, tj. komórkę, do której wstawiamy obiekt, oraz liczbę wierszy i kolumn, które chcemy wykorzystać.

Informacja

Znak & przed jakąś literą w opisie przycisków tworzy skrót klawiaturowy dostępny po naciśnięciu ALT + litera.

Po uruchomieniu programu powinniśmy zobaczyć okno podobne do poniższego:

../../_images/kalkulator03.png

4.1.4. Obsługa zdarzeń

Mamy okno z polami edycyjnymi i przyciskami, ale kontrolki te na nic nie reagują. Nauczymy się więc obsługiwać poszczególne zdarzenia.

Na początku skryptu importujemy klasę QMessageBox pozwalającą tworzyć okna z komunikatami oraz przestrzeń nazw Qt zawierającą różne stałe:

Kod nr
5from PyQt6.QtWidgets import QMessageBox
6from PyQt6.QtCore import Qt

Dalej w metodzie interfejs() po instrukcji self.setLayout(uklad_t) dopisujemy:

Kod nr
60        prz_koniec.clicked.connect(self.koniec)
61        prz_dodaj.clicked.connect(self.dzialanie)
62        prz_odejmij.clicked.connect(self.dzialanie)
63        prz_mnoz.clicked.connect(self.dzialanie)
64        prz_dziel.clicked.connect(self.dzialanie)
65

Jednym ze zdarzeń jest kliknięcie przycisku (clicked), które wiążemy za pomocą metody conect() z metodami koniec() i dzialanie(). Metody te trzeba dodać do klasy Kalkulator(). Zacznijmy od metody koniec():

Kod nr
73    def koniec(self):
74        self.close()
75

Metoda koniec() wywołana po kliknięciu przycisku „Koniec” wywołuje metodę close() okna głównego, co powoduje jego zamknięcie.

Informacja

Omówiony fragment kodu ilustruje mechanizm zwany w bibliotece Qt 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ą.

4.1.5. Działania

Ze zdarzeniem kliknięcia przycisków działań powiązana została metoda działania(). Dodajmy ją więc do klasy Kalkulator():

Kod nr
 76    def dzialanie(self):
 77
 78        nadawca = self.sender()
 79
 80        try:
 81            liczba1 = float(self.liczba_1.text())
 82            liczba2 = float(self.liczba_2.text())
 83
 84            if nadawca.text() == "&Dodaj":
 85                wynik = liczba1 + liczba2
 86            elif nadawca.text() == "&Odejmij":
 87                wynik = liczba1 - liczba2
 88            elif nadawca.text() == "&Mnóż":
 89                wynik = liczba1 * liczba2
 90            else:  # dzielenie
 91                try:
 92                    wynik = round(liczba1 / liczba2, 9)
 93                except ZeroDivisionError:
 94                    QMessageBox.critical(
 95                        self, "Błąd", "Nie można dzielić przez zero!")
 96                    return
 97
 98            self.wynik.setText(str(wynik))
 99
100        except ValueError:
101            QMessageBox.warning(self, "Błąd", "Błędne dane", QMessageBox.StandardButton.Ok)

Ponieważ jedna metoda ma obsłużyć cztery sygnały, musimy znać źródło sygnału (ang. source), czyli nadawcę (ang. sender), w tym przypadku przycisk, który został kliknięty: nadawca = self.sender().

W bloku try: przekształcamy wprowadzone w polach tekstowych liczby na typ zmiennoprzecinkowy. Operacja ta z powodu błędnych danych może zwrócić wyjątek, który przechwytujemy w klauzuli except ValueError i wyświetlamy ostrzeżenie: QMessageBox.warning().

Jeżeli dane są poprawne, w złożonej instrukcji warunkowej identyfikujemy kliknięty poprzez umieszczony na nim komunikat, np. if nadawca.text() == "&Dodaj":. Następnie wykonujemy odpowiednie dla danego przycisku działanie.

Warto zwrócić uwagę, że w przypadku dzielenia ponownie korzystamy z przechwytywania wyjątków, tym razem do obsługi ewentualnego błędu dzielenia przez zero (ZeroDivisionError).

../../_images/kalkulator06.png

Instrukcja round(liczba1 / liczba2, 9) zaokrągla wynik dzielenia do 9 miejsc po przecinku.

Na koniec zamieniamy uzyskany wynik na ciąg znaków i wypisujemy w polu tekstowym za pomocą metody setText(): self.wynikEdt.setText(str(wynik)).

Sprawdź działanie programu.

../../_images/kalkulator05.png

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.liczba_1.setFocus(), które ustawia focus na wybranym elemencie.

4.1.6. Zamykanie aplikacji

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 kolejna metodę:

Kod nr
103    def closeEvent(self, event):
104        msg_box = QMessageBox()
105        msg_box.setWindowTitle('Komunikat')
106        msg_box.setText('Czy na pewno koniec?')
107        msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
108        msg_box.setDefaultButton(QMessageBox.StandardButton.No)
109        odp = msg_box.exec()
110
111        if odp == QMessageBox.StandardButton.Yes.value:
112            event.accept()
113        else:
114            event.ignore()
115

W nadpisanej metodzie closeEvent() wyświetlamy użytkownikowi prośbę o potwierdzenie zamknięcia za pomocą okna dialogowego utworzonego za pomocą klasy QMessageBox.

../../_images/kalkulator04.png

Używamy następujących metod:

  • setWindowTitle() – do ustawienia tytuł okna,

  • setText() – do podania komunikatu dla użytkownika, w tym przypadku pytania,

  • setStandardButtons – do zdefiniowania dostępnych przycisków,

  • setDefaultButton – do wskazania przycisku domyślnego.

Jeżeli użytkownik kliknie przycisk „Yes” (if odp == QMessageBox.StandardButton.Yes.value:), zezwalamy na dalszą obsługę zdarzenia event.accept(), w przeciwnym razie odrzucamy je event.ignore().

4.1.7. Naciśnięcie klawisza

Może wygodnie byłoby zamykać aplikację naciśnięciem klawisza ESC? Dopiszmy jeszcze jedną metodę w klasie Kalkulator:

Kod nr
116    def keyPressEvent(self, e):
117        if e.key() == Qt.Key.Key_Escape:
118            self.close()
119

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 jeżeli jest to ESC, zamykamy okno.

4.1.8. Materiały

  1. Strona główna dokumentacji Qt6

  2. Lista klas Qt6

  3. PyQt6 Reference Guide

  4. Przykłady PyQt6

  5. Signals and slots

  6. Kody klawiszy


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:

2026-04-19 o 17:41 w Sphinx 7.3.7

Autorzy:

Robert Bednarz