Programowanie aplikacji desktopowych · klasa III

Aplikacje okienkowe w tkinterze

od pustego okna do działającego przycisku

Dwie lekcje i jeden cel: zrozumieć, z czego składa się okno i dlaczego kliknięcie przycisku cokolwiek robi.

Rozdział 13, strony 148–157 · Python + tkinter · karty pracy: 01-okno i 09-tekst-i-przyciski
Lekcja 1 · Okno aplikacji · rozdz. 13.1–13.2

Konsola kontra okno

Aplikacja konsolowa

Rozmowa po kolei: program pyta, użytkownik odpowiada, program idzie dalej. Kolejność ustala programista.

Aplikacja okienkowa (GUI)

Wszystko widać naraz. Użytkownik klika, co chce i kiedy chce. Kolejność ustala użytkownik.

GUI — Graphical User Interface, po polsku graficzny interfejs użytkownika.
Stąd bierze się cała reszta rozdziału: skoro to użytkownik decyduje, program musi czekać i reagować na zdarzenia, zamiast wykonywać się od góry do dołu.
Lekcja 1 · Okno aplikacji · rozdz. 13.1–13.2

Z czego zbudowany jest interfejs

Okna

modalne i niemodalne

Przyciski

Button

Pola tekstowe

wpisywanie danych

Etykiety

Label — tekst do czytania

Listy i pola wyboru

wybór z gotowych opcji

Menu i paski

nawigacja po programie

Modalne i niemodalne — różnica, którą trzeba umieć nazwać. Okno modalne blokuje resztę programu, dopóki go nie zamkniesz (np. „Czy na pewno usunąć?”). Niemodalne pozwala pracować dalej w innych oknach.
Lekcja 1 · Okno aplikacji · rozdz. 13.1–13.2

Zdarzenia — i kto je obsługuje

Klawiatura

wciśnięcie klawisza, puszczenie klawisza

Mysz

kliknięcie, dwuklik, najechanie, przeciągnięcie

Dobra wiadomość: większości tych rzeczy nie pisze programista. Rysowanie przycisku, reakcja na najechanie, zaznaczanie tekstu — to robi za nas framework i system operacyjny. My podpinamy tylko to, co ma się stać po zdarzeniu.
Dalsze przykłady: Python, biblioteka tkinter, podejście obiektowe — z klasami i metodami, tak jak w rozdziale o programowaniu obiektowym.
Lekcja 1 · Okno aplikacji · rozdz. 13.1–13.2

Instalacja i sprawdzenie pipa

pip --version        # NAJPIERW to — do którego Pythona odwołuje się pip?
pip install tk       # instalacja (terminal, wiersz poleceń albo terminal IDE)
Po co sprawdzać pip --version przed instalacją. Na jednym komputerze bywa kilka Pythonów. Jeśli pip należy do Pythona 2, biblioteka trafi nie tam, gdzie piszesz program — i import się nie powiedzie. Wtedy używamy pip3.
Polecenie można wpisać w trzech miejscach: w terminalu systemu, w wierszu poleceń i w terminalu wbudowanym w środowisko IDE. Efekt ten sam.
Lekcja 1 · Okno aplikacji · rozdz. 13.1–13.2

Pierwszy program — trzy linie

import tkinter as tk   # wczytaj bibliotekę, nazywaj ją krótko: tk

okno = tk.Tk()         # stwórz obiekt okna — to jeszcze nie jest widoczne
okno.mainloop()        # pokaż okno i czekaj na użytkownika
Trzy linie, a okno już umie sporo: da się je przesuwać, zmieniać rozmiar, minimalizować, maksymalizować i zamknąć krzyżykiem. Tego nie pisaliśmy — to dał system.
Okno korzysta z bibliotek systemu operacyjnego, dlatego na Windowsie, Linuksie i macOS wygląda trochę inaczej. Ten sam kod, inny wygląd ramki.
Lekcja 1 · Okno aplikacji · rozdz. 13.1–13.2

mainloop() — dlaczego zawsze na końcu

mainloop() to pętla zdarzeń. Program wchodzi do niej i zostaje tam aż do zamknięcia okna: nasłuchuje kliknięć, odświeża widok, reaguje.

okno = tk.Tk()
okno.mainloop()        # ← program zatrzymuje się TUTAJ
okno.title("Za późno")   # ta linia wykona się dopiero po zamknięciu okna
Najczęstszy błąd początkującego. Wszystko, co ma być widoczne w oknie — tytuł, rozmiar, etykiety, przyciski — musi powstać przed mainloop(). Kod po tej linii wykona się dopiero wtedy, gdy okno zostanie zamknięte.
Lekcja 1 · Okno aplikacji · rozdz. 13.1–13.2

Tytuł i rozmiar okna

okno.title("Moja pierwsza aplikacja")
okno.geometry("300x400")

title()

napis na belce okna

geometry()

300 to szerokość, 400 to wysokość

Dwie pułapki w jednej linii. Argument geometry to tekst w cudzysłowie, nie dwie liczby. A między liczbami stoi mała litera x, nie znak mnożenia. geometry(300, 400) i geometry("300*400") to błędy.
Lekcja 1 · Okno aplikacji · rozdz. 13.1–13.2

resizable — blokada skalowania

okno.resizable(False, False)   # nie da się zmienić ani szerokości, ani wysokości
okno.resizable(True, False)    # szerokość tak, wysokość nie
Pierwszy argument to oś pozioma (szerokość), drugi pionowa (wysokość). Kolejność ta sama, co w geometry.
Sprawdź to sam — tego nie da się zapamiętać z wykładu. Ustaw (True, False), złap okno za bok i pociągnij. Potem złap za dolną krawędź. Opisanie, co się faktycznie stało, to jedno z zadań na karcie pracy.
Lekcja 1 · Okno aplikacji · rozdz. 13.1–13.2

Komentarze

# Autor: Jan Kowalski
# Program otwiera okno o stałym rozmiarze
import tkinter as tk
Linia zaczynająca się od # to komentarz. Python jej nie wykonuje — służy wyłącznie człowiekowi, który będzie czytał kod. W tym tobie za miesiąc.
Na karcie pracy wymagam komentarza przy każdej linii twojego kodu. Nie dla ozdoby: komentarz pokazuje, że rozumiesz, co napisałeś, a nie tylko przepisałeś.
Lekcja 1 · Okno aplikacji · rozdz. 13.1–13.2

Trzy sposoby importu

import tkinter                 # pełna nazwa przy każdym użyciu: tkinter.Tk()
import tkinter as tk           # skrót: tk.Tk()  ← tak robimy
from tkinter import *         # bez przedrostka: Tk()  ← odradzane
Dlaczego gwiazdka jest zła. Wciąga do programu wszystkie nazwy z biblioteki naraz. Jeśli twoja zmienna nazywa się tak samo jak coś z tkintera — jedna z nich zniknie, a błąd będzie trudny do znalezienia. Przy dużych bibliotekach to też niepotrzebne spowolnienie startu.
Dlaczego warto znać zapis as tk. Tak pisze większość poradników w internecie. Widząc tk.Button(...) od razu wiesz, skąd to pochodzi.
Praktyka 1 · ~35 min

Praktyka 1: twoje własne okno

To są dokładnie zadania z karty pracy 01-okno.

ZADANIE 1
  okno z tytułem: twoje imię i nazwisko
  rozmiar 420x280
  skalowanie zablokowane w OBU osiach
  import z aliasem tk
  każda linia opisana własnym komentarzem

ZADANIE 2 — eksperyment
  skopiuj program z zadania 1
  zmień TYLKO resizable na (True, False)
  uruchom, spróbuj rozciągnąć okno myszą w obie strony
  opisz, co faktycznie się stało
Jeśli program nie wystartował — zapisz treść błędu. Ostatnie pytanie karty brzmi: co poszło nie tak i co było przyczyną. Komunikat błędu to nie wstyd, tylko materiał.
Lekcja 2 · Tekst i przyciski · rozdz. 13.3–13.4

Tekst w oknie: StringVar i Label

tekst = tk.StringVar()        # pudełko na tekst, który może się zmieniać
tekst.set("Witaj")            # wkładamy do pudełka wartość początkową

label1 = tk.Label(okno, textvariable=tekst)
label1.grid(row=0, column=0)

StringVar

przechowuje tekst. Sam niczego nie wyświetla.

Label

wyświetla. Sam niczego nie przechowuje.

Dlaczego dwie linie, a nie jedna. StringVar() tworzy puste pudełko — bez set() etykieta pokaże pustkę. To częste pytanie na karcie pracy.
Lekcja 2 · Tekst i przyciski · rozdz. 13.3–13.4

textvariable — żywe połączenie

Parametr textvariable podpina etykietę do pudełka, a nie do konkretnego napisu. Zmieniasz zawartość pudełka — napis w oknie zmienia się sam.

tekst.set("Nowa treść")    # etykieta odświeża się natychmiast
Jest też prostsza droga: tk.Label(okno, text="Witaj") — napis wpisany na sztywno. Działa, ale nie da się go później zmienić w trakcie działania programu. Dlatego przy przyciskach używamy StringVar.
Podręcznik pokazuje pięć sposobów utworzenia etykiety. Na karcie pracy trzeba je wypisać — zajrzyj na stronę 152–153.
Lekcja 2 · Tekst i przyciski · rozdz. 13.3–13.4

grid — siatka okna

label1.grid(row=0, column=0)   # lewy górny róg
label2.grid(row=1, column=0)   # wiersz niżej
label3.grid(row=2, column=0)   # jeszcze niżej
Siatki nie trzeba konfigurować. tkinter sam dzieli okno na wiersze i kolumny w miarę potrzeb i dopasowuje ich rozmiar do zawartości.
Numerowanie zaczyna się od zera — jak indeksy list. row=0 to pierwszy wiersz, row=2 to trzeci. Element bez grid() nie pojawi się w oknie wcale, nawet jeśli go utworzyłeś.
Lekcja 2 · Tekst i przyciski · rozdz. 13.3–13.4

Kolory: bg i fg

tk.Label(okno, text="Uwaga", bg="black", fg="white")
tk.Label(okno, text="Alarm", bg="#ff0000", fg="#00ff00")

bg — background

kolor tła

fg — foreground

kolor napisu

Zapis szesnastkowy: #RRGGBB — po dwa znaki na czerwony, zielony i niebieski. #ff0000 to czysta czerwień, #00ff00 to czysta zieleń. Czyli w przykładzie wyżej: czerwone tło, zielony napis.
Lekcja 2 · Tekst i przyciski · rozdz. 13.3–13.4

Czcionki

font="none 16 bold"          # zapis tekstowy
font=("Arial", 16, "bold")      # zapis krotką

none / Arial

krój pisma — none oznacza domyślny

16

rozmiar w punktach

bold

styl: pogrubienie, kursywa, podkreślenie

Oba zapisy robią to samo. Krotka jest wygodniejsza, gdy nazwa czcionki składa się z kilku słów.
Lekcja 2 · Tekst i przyciski · rozdz. 13.3–13.4

Przycisk, który coś robi

przycisk = tk.Button(okno, text="Kliknij", command=aplikacja.zmien_tekst)
przycisk.grid(row=3, column=0)
Parametr command to sedno całej lekcji. Podajesz w nim nazwę metody bez nawiasów. Mówisz tym: „gdy ktoś kliknie — wywołaj to”. Samo kliknięcie obsługuje tkinter.
Najczęstszy błąd: command=zmien_tekst() z nawiasami. Wtedy Python wywoła metodę od razu, przy tworzeniu przycisku, i podstawi jej wynik. Przycisk po kliknięciu nie zrobi nic.
Lekcja 2 · Tekst i przyciski · rozdz. 13.3–13.4

Klasa Aplikacja — porządek w kodzie

class Aplikacja:
    def __init__(self, okno):      # konstruktor dostaje okno z zewnątrz
        self.tekst = tk.StringVar()
        self.tekst.set("Witaj")
        self.label = tk.Label(okno, textvariable=self.tekst)
        self.label.grid(row=0, column=0)

    def zmien_tekst(self):         # metoda = zachowanie aplikacji
        self.tekst.set("Kliknięto!")

okno = tk.Tk()
aplikacja = Aplikacja(okno)      # obiekt dostaje nasze okno
okno.mainloop()
Po co klasa, skoro działało i bez niej. Etykiety i ich teksty przestają być luźnymi zmiennymi — stają się polami obiektu. Dzięki self metoda zmien_tekst wie, którą etykietę ma zmienić, bez zmiennych globalnych.
Lekcja 2 · Tekst i przyciski · rozdz. 13.3–13.4

command z argumentem — dwa sposoby

command=self.zmien_tekst                       # metoda bez parametrów
command=lambda: self.zmien_tekst_w(label2)    # metoda z parametrem
Dlaczego w drugim przypadku potrzebna jest lambda. command oczekuje funkcji do wywołania później. Gdyby wpisać self.zmien_tekst_w(label2) wprost, Python wywołałby ją natychmiast. lambda opakowuje wywołanie i odkłada je na moment kliknięcia.
Dlatego dwie metody w przykładzie różnią się właśnie parametrami: jedna zna swoją etykietę na stałe, druga dostaje ją przy wywołaniu.
Lekcja 2 · Tekst i przyciski · rozdz. 13.3–13.4

set() czy config() — dwie drogi zmiany napisu

self.tekst.set("...")

zmienia zawartość pudełka StringVar. Odświeży się każda etykieta podpięta pod to pudełko przez textvariable.

label.config(text="...")

zmienia napis w jednej konkretnej etykiecie, bez pośrednika.

To wyjaśnia rysunki 13.10 i 13.11 z podręcznika: po kliknięciu pierwszego przycisku zmienia się napis podpięty pod StringVar, a drugi napis zostaje bez zmian — bo nie był z tym pudełkiem połączony.
Praktyka 2 · ~40 min

Praktyka 2: tekst i działający przycisk

Zadania z karty pracy 09-tekst-i-przyciski.

ZADANIE 1
  okno z tytułem: twoje imię, rozmiar 360x240
  trzy etykiety w trzech wierszach siatki
  druga etykieta: biały napis na czarnym tle

ZADANIE 2
  rozbuduj program o klasę Aplikacja
  konstruktor + metoda
  przycisk, który po kliknięciu zmienia tekst
  pierwszej etykiety na „Kliknięto!”
  wklej CAŁY plik
Kolejność, która oszczędza nerwy: najpierw okno, potem jedna etykieta, sprawdź. Potem kolejne dwie, sprawdź. Dopiero na końcu klasa i przycisk. Uruchamiaj po każdym kroku — wtedy wiesz, która zmiana coś zepsuła.