Niech Tego Dotkną! Tkinter Od Podstaw

Niech Tego Dotkną! Tkinter Od Podstaw

Programowanie w Python jest trochę niewdzięczne. Piszesz różne skrypty, logikę, ciekawe rozwiązania. Dla osób, które nie znają się na programowaniu, to będzie nic, póki nie zobaczą wizualnych efektów. Kiedy dla nas w dużej mierze interfejs oparty o wiersz poleceń (CLI) jest wystarczający, to są momenty gdy jedynym słusznym rozwiązaniem jest implementacja aplikacji okienkowej z graficznym interfejsem (GUI). Jest wiele różnych GUI, z których można korzystać w Python. Ponieważ stawiam duży nacisk na biblioteki wbudowane, omówimy bibliotekę Tkinter!

Dlaczego Tkinter

Jest wiele różnych bibliotek do tworzenia aplikacji okienkowych w Python. Do najczęściej wspominanych należy zdecydowanie PyQT. Innymi rozwiązaniami są także wxPython czy kivy. Problem z nimi jest taki, że posiadają wiele dodatkowych zależności, a co niektóre wymagają do pracy zewnętrznych narzędzi. Tkinter jest wbudowany, więc nie wymaga instalacji żadnych paczek Pythonowych do jego działania, i to jedna z jego największych niezaprzeczalnych zalet.

Najwięcej sprzeciwów względem to surowy wygląd, oraz ograniczenia. Trudno się nie zgodzić z tymi uwagami. O ile do większych aplikacji biznesowych zdecydowanie polecałbym użycie innych bibliotek, o tyle, aby stworzyć prosty interfejs graficzny do naszego kodu, nie ma lepszego rozwiązania. Dodatkowo wiedza wyniesiona z używania Tkinter pozwoli na łatwiejsze przejście na inne biblioteki do tworzenia GUI.

Podstawy

Aby uruchomić aplikację okienkową, wystarczy nam nie więcej niż trzy linie kodu. Po pierwsze importujemy klasę Tk z pakietu tkinter. Po drugie tworzymy obiekt klasy Tk. Jest to główna klasa aplikacji, element najwyższego rzędu. To do obiektu tej klasy będziemy dodawać elementy naszego interfejsu. Na koniec nie pozostało nic innego jak uruchomić naszą aplikację, wywołując funkcje mainloop na obiekcie klasy Tk.

from tkinter import Tk
root = Tk()
root.mainloop()

Puste okno aplikacji w tkinter

Skoro pierwszy krok mamy za sobą, czas na pewne udoskonalenia. Skupimy się na największych podstawach, aby jak najszybciej dojść do działającej aplikacji. Może się do tego przydać ustalenie nazwy naszej aplikacji poprzez metodę title, dzięki temu nazwiemy naszą aplikację App. Po drugie równie przydatne może okazać się ustawienie rozmiaru okna. Zrobimy to poprzez metodę geometry, która jako argument przyjmuje string w formacie SZEROKOŚĆxWYSOKOŚĆ, w poniższym przypadku będzie to 600px szerokości na 400px wysokości.

from tkinter import Tk
root = Tk()
root.title('App')
root.geometry("600x400")
root.mainloop()

Aplikacja o nazwie App, szerokości 600px i wysokości 400px

Skoro mamy już okno aplikacji, czas dodać do niej elementy!

Label

Aby dodać tekst w naszej aplikacji, skorzystamy z klasy Label. Większość obiektów w Tkinter jako pierwszy argument przyjmować będzie element, w którym dany obiekt zostanie umieszczony. Dlatego też, jako pierwszy argument przekazujemy obiekt naszego okna root.

Następnie definiujemy właściwości tego obiektu. Dla nas najistotniejszy to argument text, do którego przypiszemy wartość widoczną w oknie. Możemy zdefiniować także jego rozmiar za pomocą argumentu font. Dodatkowo za pomocą argumentu fg tekst ten możemy pokolorować. Możemy to zrobić za pomocą heksadecymalnej reprezentacji kolorów RGB (#RRGGBB), lub za pomocą słowa. W poniższym przykładzie będzie to napis Look at me! o kolorze niebieskim.

from tkinter import Tk, Label
root = Tk()
root.title("App")
root.geometry("200x200")
label = Label(root, text="Look at me!", font=30, fg="blue")
label.pack()
root.mainloop()

Ostatni element, to metoda pack. Używamy jej, aby ustalić miejsce naszego obiektu w aplikacji. Dla ułatwienia zostawimy na razie domyślną wartość TOP. Spowoduje to, że nasze obiekty będą wstawiane w jednej kolumnie od góry do dołu.

Aplikacja z tekstem Look at me!

Button

Skoro wiemy już jak pisać tekst w naszej aplikacji, czas na element sterujący. Będzie to przycisk, a więc zaimportujemy klasę Button. Tworzenie obiektu tej klasy wygląda podobnie do obiektu klasy Label. Jako pierwszy argument przekazujemy okno naszej aplikacji, a następnie cechy obiektu. Argument text już poznaliśmy. Width pozwoli ustawić szerokość naszego przycisku. Domyślnie szerokość dopasowywałaby się do szerokości naszego tekstu.

from tkinter import Tk, Button
def click_action():
print("Wow!")
root = Tk()
root.geometry("200x200")
click_button = Button(root, text="Click me!", width=8, command=click_action)
click_button.pack()
root.mainloop()

Najważniejszym argumentem jest tutaj command który służy do definiowania jaką komendę chcemy wywołać poprzez wciśnięcie przycisku. Przekazujemy tutaj funkcje bezargumentową. W poniższym przykładzie wyświetlać ona będzie napis Wow! w terminalu po każdorazowym wciśnięciu.

Aplikacja z przyciskiem Click me!

Konfiguracja

Cechy naszych obiektów możemy zmieniać także po ich utworzeniu. Robimy to poprzez metodę config. Jej argumenty są takie same, jak argumenty przy inicjalizacji obiektu. Poniższy przykład pokazuje inny sposób zdefiniowania komendy naszego przycisku. Definiujemy ją tutaj dopiero po utworzeniu obiektu.

from tkinter import Tk, Button
def click_action():
print("Wow!")
root = Tk()
root.geometry("200x200")
click_button = Button(root, text="Click me!", width=8)
click_button.pack()
click_button.config(command=click_action)
root.mainloop()

Do czego przydać nam może się taka metoda? Do modyfikacji obiektów w czasie działania aplikacji - czyli do aktualizacji tego, co w niej widzimy!

Komenda z argumentami

Teraz gdy możemy skonfigurować komendę po utworzeniu obiektu, możemy do tej komendy przekazać nasz obiekt. Ponieważ command musi być funkcją bezargumentową mamy dwa rozwiązania. Możemy zbudować funkcje, która przyjmie funkcje oraz jej argumenty, a na wyjściu zwróci funkcje bezargumentową wywołującą przekazaną przez nas funkcje z argumentami.

from tkinter import Tk, Button
def click_action(button):
button.config(text=f"Wow!")
def create_command(func, *args, **kwargs):
def command():
return func(*args, **kwargs)
return command
root = Tk()
root.geometry("200x200")
click_button = Button(root, text="Click me!", width=8)
click_button.pack()
click_button.config(command=create_command(click_action, click_button))
root.mainloop()

Drugim rozwiązaniem jest skorzystanie z funkcji lambda. W tym przypadku jako komendę przekazujemy funkcje lambda, która ukrywać będzie wywołanie funkcji click_action z obiektem przycisku jako argumentem. W środku tej funkcji także nastąpiła zmiana, po wciśnięciu przycisku zmienimy jego nazwę z Click me! na Wow!.

from tkinter import Tk, Button
def click_action(button):
button.config(text=f"Wow!")
root = Tk()
root.geometry("200x200")
click_button = Button(root, text="Click me!", width=8)
click_button.pack()
click_button.config(command=lambda: click_action(click_button))
root.mainloop()

Sposób, w jaki będziesz definiować komendy, jest zależny od Ciebie i od tego, które rozwiązanie jest dla Ciebie czytelniejsze i prostsze! Ja w kolejnych przykładach będę korzystać z opcji wykorzystującej funkcję anonimową lambda.

Aplikacja, która po kliknięciu przycisku zmienia jego nazwę na Wow!

Rozwinięcie komendy

Aby było ciekawiej, stwórzmy tutaj jakąś dodatkową logikę. Po pierwsze definiujemy zmienną clicks, która przechowywać będzie ilość kliknięć naszego przycisku. Następnie zmodyfikujemy naszą funkcję, aby z niej korzystała. Wykorzystamy do tego instrukcje global, która spowoduje, że w funkcji korzystać będziemy ze zmiennej globalnej. Następnie powiększymy ją o jeden, ponieważ udało nam się kliknąć przycisk! Na koniec nie pozostało nic innego, jak skorzystać z tej zmiennej w nazwie naszego przycisku. Zobaczmy, jak teraz zachowa się aplikacja.

from tkinter import Tk, Button
clicks = 0
def click_action(button):
global clicks
clicks += 1
button.config(text=f"Wow! x {clicks}")
root = Tk()
root.geometry("200x200")
click_button = Button(root, text="Click me!", width=8)
click_button.pack()
click_button.config(command=lambda: click_action(click_button))
root.mainloop()
![Stan aplikacji po kliknięciu raz w przycisk](/images/posts/006/button_click.png)
![Stan aplikacji po kliknięciu siedem razy w przycisk](/images/posts/006/button_click_7.png)

Układ

Domyślnie w Python elementy są układane jeden pod drugim. Na tym etapie nie będziemy się skupiać na zmianie tego zachowania. W myśl frontendową najpierw zróbmy, czego potrzebujemy, a potem przyjdzie czas na stylowanie!

Kolejność użycia metody pack ma w takim razie znaczenie. Na tej podstawie ustalona zostanie kolejność wystąpienia tych obiektów w naszej aplikacji. Ponieważ najpierw użyliśmy metody pack na obiekcie klasy Button, to przycisk zostanie zmapowany w aplikacji jako pierwszy, a nasz tekst jako drugi.

from tkinter import Tk, Label, Button
def click_action(button):
button.config(text=f"Wow!")
root = Tk()
root.geometry("200x200")
text_label = Label(root, text="Some text")
click_button = Button(root, text="Click me!", width=8)
click_button.pack()
text_label.pack()
click_button.config(command=lambda: click_action(click_button))
root.mainloop()

Aplikacja z przyciskiem i tekstem

Przypadek użycia Tkinter

Uważasz, że to za mało, aby tworzyć coś konkretnego? Udowodnię, że jest inaczej. Za pomocą wyłącznie powyższych elementów i właściwości zagramy w Kamień, papier i nożyce z komputerem! Zanim weźmiemy się do tworzenia gui w tkinter, zaimplementujmy rekwizyty, z których skorzystamy.

Logika

Po pierwsze, potrzebujemy prostej logiki do gry. Stwórzmy zatem listę możliwych ruchów.

available_choices = ["paper", "rock", "scissors"]

Przy prostym formacie rozgrywki mamy trzy opcje: wygramy, przegramy albo remis. Remis jest wtedy gdy użytkownik oraz komputer wybiorą ten sam symbol.

def play(player, cpu):
if player == cpu:
return None

Następnie mamy prostą zasadę zwycięstwa. Papier ma przewagę nad kamieniem, kamień nad nożyczkami, a te ostatnie nad papierem. Najłatwiej będzie opisać to za pomocą słownika. W każdym innym przypadku przegrywamy. Nasza funkcja wskazywać będzie status pierwszego gracza - czy wygrał (True), przegrał (False) lub zremisował (None).

available_choices = ["paper", "rock", "scissors"]
def play(player, cpu):
win_with = {"paper": "rock", "rock": "scissors", "scissors": "paper"}
if player == cpu:
return None
elif win_with[player] == cpu:
return True
else:
return False
import unittest
paper = "paper"
rock = "rock"
scissors = "scissors"
class PlayTest(unittest.TestCase):
def test_user_win(self):
self.assertTrue(play(paper, rock))
self.assertTrue(play(rock, scissors))
self.assertTrue(play(scissors, paper))
def test_cpu_win(self):
self.assertFalse(play(rock, paper))
self.assertFalse(play(scissors, rock))
self.assertFalse(play(paper, scissors))
def test_tie(self):
self.assertIsNone(play(rock, rock))
self.assertIsNone(play(paper, paper))
self.assertIsNone(play(scissors, scissors))
if __name__ == "__main__":
unittest.main()

Aby dodać losowości, skorzystamy z biblioteki random, a dokładniej metody choice, która zwracać będzie losowy element z kolekcji dostępnych symboli.

from random import choice
available_choices = ["paper", "rock", "scissors"]
cpu = choice(available_choices)

Klasa TK

Skoro logika jest gotowa, czas połączyć ją z GUI. Stwórzmy zatem okno aplikacji, oraz zdefiniujmy obiekt klasy Label, który zachęci gracza do gry, a następnie będzie informować o wyniku.

from tkinter import Tk, Label, Button
root = Tk()
root.title("Paper, Rock, Scissors")
root.geometry("300x150")
text_label = Label(root, font=40, text="Let's play paper, rock, scissors!")
text_label.pack()
root.mainloop()

Proste okno z zachętą do zagrania

Następnie dodajmy trzy przyciski. To z ich pomocą decydować będziemy o tym, jaki ruch wykonamy w tej grze!

from tkinter import Tk, Label, Button
root = Tk()
root.title("Paper, Rock, Scissors")
root.geometry("300x150")
text_label = Label(root, font=40, text="Let's play paper, rock, scissors!")
text_label.pack()
Button(
root, text="📃 Paper", font=40, width=10
).pack()
Button(
root, text="🤘 Rock", font=40, width=10
).pack()
Button(
root, text="✂️ Scissors", font=40, width=10
).pack()
root.mainloop()

Możecie zauważyć, że tekst naszych przycisków się rozjeżdża. Na ten moment zaakceptujemy tę niedoskonałość, poprawa ustawienia elementów czy tekstu to temat na osobny artykuł.

Okno z przyciskami wyboru symbolu przez gracza

Połączenie logiki z graficznym interfejsem

Nie przypisałem obiektów klasy Button do zmiennych, ponieważ w tej grze przy mojej implementacji nie będę potrzebować dalszego dostępu do tych obiektów. Czas spiąć logikę gry z GUI. Służyć do tego będzie funkcja play_cmd, która przyjmie symbol użytkownika. Za pomocą global uzyskamy dostęp do naszej labelki, której treść będziemy zmieniać w zależności od wyniku rozgrywki.

def play_cmd(player):
global text_label
cpu = choice(available_choices)
is_user_winner = play(player, cpu)
if is_user_winner is None:
text_label.config(text="Tie! Try again!", fg="blue")
elif is_user_winner:
text_label.config(text="You win... Let's play again", fg="green")
else:
text_label.config(text="I win, I win!", fg="red")

Po dopięciu powyższej funkcji do naszych przycisków, kod naszej gry wyglądać będzie następująco:

from random import choice
from tkinter import Tk, Label, Button
available_choices = ["paper", "rock", "scissors"]
def play(player, cpu):
win_with = {"paper": "rock", "rock": "scissors", "scissors": "paper"}
if player == cpu:
return None
elif win_with[player] == cpu:
return True
else:
return False
def play_cmd(player):
global text_label
cpu = choice(available_choices)
is_user_winner = play(player, cpu)
if is_user_winner is None:
text_label.config(text="Tie! Try again!", fg="blue")
elif is_user_winner:
text_label.config(text="You win... Let's play again", fg="green")
else:
text_label.config(text="I win, I win!", fg="red")
root = Tk()
root.title("Paper, Rock, Scissors")
root.geometry("300x150")
text_label = Label(root, font=40, text="Let's play paper, rock, scissors!")
text_label.pack()
Button(
root, text="📃 Paper", font=40, width=10, command=lambda: play_cmd("paper")
).pack()
Button(
root, text="🤘 Rock", font=40, width=10, command=lambda: play_cmd("rock")
).pack()
Button(
root, text="✂️ Scissors", font=40, width=10, command=lambda: play_cmd("scissors")
).pack()
root.mainloop()

Stan okna gry po wygranej komputera Stan okna gry po remisie Stan okna gry po wygranej użytkownika

Gra wygląda.. dość surowo. Ale działa! I to jest dla nas najważniejsze! Liczą się efekty, które potem można rozbudowywać i ulepszać. Aby potrenować wiedzę nabytą, możesz dodać statystykę zwycięstw, informacje o symbolu pokazanym przez komputer, lub rozszerzyć grę do wersji “Paper rock scissors lizard spock”!

Masz pomysł, jaką grę można by wykonać, wykorzystując powyższą wiedzę? Podziel się tym w komentarzach! W kolejnym artykule omówimy jak rozstawiać elementy w oknie, aby z pomocą tkinter móc zagrać z komputerem w kółko i krzyżyk :)