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

../../_images/kalkulator05.png

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:

Kod nr
 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
../../_images/kalkulator01.png

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

Kod nr
5from PyQt5.QtGui import QIcon
6from PyQt5.QtWidgets import QLabel, QGridLayout
Kod nr
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.

../../_images/kalkulator02.png

6.1.3. Interfejs

Dodamy teraz pozostałe widżety tworzące graficzny interfejs naszej aplikacji. Jak zwykle, zaczynamy od zaimportowania potrzebnych klas:

Kod nr
7from PyQt5.QtWidgets import QLineEdit, QPushButton, QHBoxLayout

Następnie przed instrukcją self.setLayout(ukladT) wstawiamy następujący kod:

Kod nr
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ź, a koniecBtn.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:

../../_images/kalkulator03.png

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:

Kod nr
8from PyQt5.QtWidgets import QMessageBox
9from PyQt5.QtCore import Qt

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

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

Kod nr
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:

Kod nr
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ę:

Kod nr
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.

../../_images/kalkulator04.png

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:

Kod nr
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:

Kod nr
 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.

../../_images/kalkulator05.png

Dopiszemy obsługę pozostałych działań. Instrukcję warunkową w funkcji dzialanie() rozbudowujemy następująco:

Kod nr
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ę.

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

6.1.6. Materiały

  1. Strona główna dokumentacji Qt5

  2. Lista klas Qt5

  3. PyQt5 Reference Guide

  4. Przykłady PyQt5

  5. Signals and slots

  6. Kody klawiszy

Źródła:


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:

2025-04-12 o 10:21 w Sphinx 7.3.7

Autorzy:

Patrz plik „Autorzy”