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

../../_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
7
from PyQt5.QtWidgets import QLineEdit, QPushButton, QHBoxLayout

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

Kod nr
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ź, 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
8
9
from PyQt5.QtWidgets import QMessageBox
from 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
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:

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

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

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

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

../../_images/kalkulator05.png

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

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

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