Lekcja 10 - programowanie obiektowe (notatki)

Tkinter (cd.)
Frame (czyli ramka)
Komponent, którego nie widzimy - jego główną funkcją jest zgrupowanie elementów ze sobą. Wpisujemy go jako rodzica konkretnych elementów, to umożliwia nam dokonanie zmian na większej ilości komponentów oraz wybieranie pomiędzy rozpakowywaniem packiem a gridem (w obrębie jednego rodzica używa się tylko jednego sposobu).
import tkiner as tk
okno = tk.Tk()
okno.title("Jakis tytuł")
ramka = tk.Frame(okno, padx = 10, pady=10)
#padding - odstępy pomiędzy komponentami
ramka.pack()
przycisk = tk.Button(ramka, text="To przycisk")
przycisk.pack()
ramka2 = tk.Frame(okno, padx = 10, pady=10)
ramka2.pack()
przycisk2 = tk.Button(ramka2, text="Poza ramką")
przycisk2.grid(row=0, column=0)
okno.mainloop()
Pack
Wygodny sposób rozmieszczania półautomatycznego, można podać dodatkowe parametry:
Metoda fill rozciąga dany element na szerokość jego rodzica.
okno.geometry("800x800")
pole_tekstowe = tk.Entry(ramka, width=100)
pole_tekstowe.pack()
przycisk = tk.Button(okno, text="To przycisk")
przycisk.pack(fill="x")
Metoda side przenosi komponent w odpowiednie miejsce: góra (top), dół (bottom), lewo (left) czy prawo (right).
okno.geometry("800x800")
przycisk = tk.Button(okno, text="To przycisk")
przycisk.pack(side="bottom")
Grid
Ułatwia rozmieszczenie komponentów gdy efekt jaki chcemy osiągnąć jest bardziej skomplikowany. Metoda ta w pewien sposób nakłada "niewidzialną siatkę" na okno i umożliwia wstawienie danego komponentu do odpowiedniej kratki lub kratek.
etykieta1 = tk.Label(ramka, text="etykieta1")
etykieta1.grid(row=0, column=0)
wprowadz1 = tk.Entry(ramka, width=20)
wprowadz1.grid(row=0, column=1)
etykietaxx = tk.Label(ramka, text="etykietaXX")
etykietaxx.grid(row=0, column=2)
etykieta2 = tk.Label(ramka, text="etykieta2")
etykieta2.grid(row=1, column=0)
wprowadz2 = tk.Entry(ramka, width=20)
wprowadz2.grid(row=1, column=1)
etykieta3 = tk.Label(ramka, text="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
etykieta3.grid(row=0, column=0, columnspan=3)
#columnspan - dodatkowy parametr nakazujący zając 3 komórki. Odpowiednikiem scalania komórek wzdłuż kolumny jest rowspan.
wprowadz3 = tk.Entry(ramka, width=20)
wprowadz3.grid(row=3, column=1)
CustomTkinter
Zmodyfikowana biblioteka tkintera, która umożliwia nadanie naszej aplikacji bardziej nowoczesnego wyglądu.
import tkinter as tk
import qrcode
import customtkinter as ctk
from PIL import ImageTk
def generuj_qr():
global qkod, obrazek
dane = pole_tekstowe.get()
qrkod = qrcode.make(dane)
obrazek = ImageTk.PhotoImage(qrkod)
pic_label.configure(image=obrazek)
ctk.set_appearance_mode("light")
okno = ctk.CTk()
okno.title("QR Code Maker")
okno.configure(padx=50, pady=50)
etykieta = ctk.CTkLabel(okno, text="Podaj linka: ")
etykieta.pack()
pole_tekstowe = ctk.CTkEntry(okno, width=50)
pole_tekstowe.pack()
przycisk = ctk.CTkButton(okno, text="OK", command=generuj_qr)
przycisk.pack()
pic_label = ctk.CTkLabel(okno, text="")
pic_label.pack()
okno.mainloop()
Programowanie obiektowe
Funkcje - czasowniki
Własne funkcje deklarujemy za pomocą def, według wzoru:
def nasza_nazwa_funkcji (parametry dla funkcji)
#blok kodu
return zwracana_wartość
Nie każda funkcja zwróci nam jakąś wartość.
Dobrze w przypadku parametrów funkcji podać jakiego typu elementu się tam spodziewamy tj. stringa, liczby całkowitej itd. Choć nie ma takiego obowiązku lepiej to robić ponieważ eliminuje to dużą część błędów (program zasygnalizuje błąd, gdy wprowadzone później dane są innego typu). Mile widziane jest też podawanie co zwróci dana funkcja (po strzałeczce), nawet jeśli nic nie zwróci (wtedy podajemy "None").
def podnies_do_szescianu(liczba:int)-> int:
szescian = liczba * liczba * liczba
return szescian
dwa_do_szesc = podnies_do_szescianu(2)
print(dwa_do_szesc)
Nazwy zamknięte w funkcji są niezależne od innych elementów w aplikacji, dlatego nawet jeśli nazwy zmiennych się powtarzają to się nie nadpiszą:
tekst = "Alice"
def drukuj_3razy(tekst:str):
print(tekst*3)
drukuj_3razy("Bob")
Wydrukuje się: BobBobBob
Podobnie w tym wypadku:
zmienna1 = 1
zmienna2 = 2
zmienna3 = 55
def drukuj_cos(zmienna3)
zmienna3 =+ 1
print(zmienna3)
Wydrukuje się 55
Jeśli chcemy wewnątrz funkcji skorzystać ze zmiennej spoza funkcji, można przekazać ją jako parametr:
ksiazka_kontaktowa = {"Stefan" : "discord:stefox", "Ala" : "234567890"}
def drukuj_kontakty(kontakty:dict):
for imie, kontakt in kontakty.items():
print(f"Imię: {imie}")
print(f"Dane kontaktowe: {kontakt}")
print("""
*****************
""")
drukuj_kontakty(ksiazka_kontaktowa)
lub wciągnąć ją jako globalną zmienną:
def generuj_qr():
global obrazek
dane = pole_url.get()
qrkod = qrcode.make(dane)
obrazek = ImageTk.PhotoImage(qrkod)
pic_label.config(image=obrazek)
Odradza się stosowanie tej metody.
Do bardziej przydatnych własnych funkcji należy czyszczenie ekranu, zgodne ze wzorem:
from os import system, name
def czysc_ekran() -> None:
if name == "nt": #sprawdza na jakim programie działa użytkownik
_ = system("cls") #jeśli na windowsie, wykonuje polecenie cls
else:
_ = system("clear") #jeśli na macu itd. clear
Klasy obiektów
Klasa to ogólny opis cech lub czynności, które dany obiekt posiada lub wykonuje, dzięki czemu można zaklasyfikować go do jakiejś wspólnej grupy. Na przykład jeśli spotykamy obiekt, który:
#Ma zespół atrybutów:
łapy: 4
ogon: 1
maja_pazury: True
ostre_zeby: True
#wykonuje czynności:
miauczy
mruczy
Możemy przyjąć, że należy do klasy kotów 😉
Do obiektów należących do jednej klasy, można zastosować jeden sposób postępowania, w ten sposób eliminuje się powtarzanie i ułatwia się pracę w zespołach (przypisanie jednej osoby do pracy nad konkretną klasą).
Deklarowanie klasy
Deklarowanie klasy przebiega zgodnie z pewnym schematem. Wpierw po słowie class podajemy nazwę klasy, zgodnie ze standardami powinien być to rzeczownik zaczynający się od dużej litery, na końcu zaś pusty nawias okrągły.
menu = {"sałatka" : 12.5, "sok" : 8.1, "burito" : 25.2,}
class Zamowienie():
Teraz trzeba zastosować funkcję, która jest wykonywana podczas tworzenia obiektu. Zgodnie ze wzorem def __init__ (parametry). Init - umożliwia przypisanie obiektowi jego indywidualnych cech. Pamiętaj, przed i po init podwójna podłoga!!! Następnie w kolejnych linijkach po wcięciu podajemy parametry, które mamy zastosować - poprzedzając je self - umożliwiające stworzenie cech indywidualnych danego obiektu:
def __init__(self, stolik: int, potrawy:list) - > None:
self.nr_stolika = stolik
self.potrawy = potrawy
self.czy_oplacone = False
W powyższym przykładzie cechami Zamówienia są numer stolika, jakie potrawy przyniesiono i czy zapłacono za posiłek.
Ponadto Zamówienie powiązane jest też z pewną funkcjonalnością w tym wypadku obliczaniem rachunku oraz przyjmowaniem zapłaty stąd kolejne funkcje przypisane do tej klasy. Takie funkcje wbudowane w daną klasę to metody, powinny one korzystać z indywidualnych cech obiektu (stąd self).
def oblicz_rachunek(self) -> float:
rachunek = 0
for potrawa in self.potrawy:
rachunek += menu[potrawa]
return rachunek
def przyjmij_oplate(self):
self.czy_oplacone = True
Teraz już mogę wykonywać metody przypisane do tej konkretnej klasy:
zam1 = Zamowienie(1, ["sałatka", "burito"])
print(zam1.oblicz_rachunek())
print(zam1.potrawy)
Uwaga! Jeśli nie zastosujemy w metodzie self, sugeruje to, że daną metodę lepiej z klasy wyjąć.
Ćwiczenie
W ten sposób można stworzyć aplikację do gry RPG, gdzie klasą będą postacie, posiadające zestaw cech: imię, profesję, punkty życia, magii i siły.
class PostacGracza():
def __init__(self, imie:str, profesja:str, hp:int = 100, mana:int =100, sila:int) -> None:
self.imie = imie
self.profesja = profesja
self.hp = hp #punkty życia, jeśli użytkownik nie poda inaczej będzie miał 100.
self.mana = mana #punkty magii
self.sila = sila #punkty siły
Można przetestować działanie:
gracz1 = PostacGracza(imie="Fizban", profesja="mag", mana=200, sila=10,)
print(f"{gracz1.imie} - pkt. życia: {gracz1.hp}")
print(f"{gracz1.imie} - siła magii: {gracz1.mana}")
print(f"{gracz1.imie} - profesja: {gracz1.profesja}")
gracz2 = PostacGracza(imie="Taz", profesja="łotrzyk", sila=15,)
print(f"{gracz2.imie} - pkt. życia: {gracz2.hp}")
print(f"{gracz2.imie} - siła magii: {gracz2.mana}")
print(f"{gracz2.imie} - profesja: {gracz2.profesja}")
Stwórz metody przypisane do klasy. które wyświetlają powitanie dla danego gracza, liczą ilość obrażeń jakie dana postać może zadać w walce wręcz i odejmującą punkty życia:
def powitaj(self):
print(f"Witaj {self.imie}.")
def zadaj_obrazenia(self) -> int:
return self.sila * 2
def otrzymaj_obrazenia(self, ilosc_obrazen:int)
self.hp-=ilosc_obrazen
Zadanie domowe
class Wojownik():
def __init__(self, imie:str, hp:int = 200, mana:int = 100, sila:int = 30, zrecznosc:int = 100, wytrzymalosc:int =30,) -> None:
self.imie = imie
self.hp = hp
self.mana = mana
self.sila = sila
self.zrecznosc = zrecznosc
self.wytrzymalosc = wytrzymalosc
def paruj_ciosy(self):
print("Sparowano cios. Zredukowano obrażenia")
def wyswietl_karte_postaci(self):
print(f"""-----------------
Karta postaci
-----------------------
IMIĘ {self.imie}
RASA/KLAN/TYP Wojownik
-----------------------
SIŁA {self.sila}
ZRĘCZNOŚĆ {self.zrecznosc}
WYTRZYMAŁOŚĆ {self.wytrzymalosc}
ŻYWOTNOŚĆ {self.hp}
MAGIA {self.mana}
""")
gracz1 = Wojownik(imie="Boreasz")
gracz1.wyswietl_karte_postaci()
class Mag():
def __init__(self, imie:str, hp:int = 200, mana:int = 400, sila:int = 10, zrecznosc:int = 100, wytrzymalosc:int =10,) -> None:
self.imie = imie
self.hp = hp
self.mana = mana
self.sila = sila
self.zrecznosc = zrecznosc
self.wytrzymalosc = wytrzymalosc
def rzuc_kule_ognia(self):
print("Rzucono kulę ognia")
def wyswietl_karte_postaci(self):
print(f"""-----------------
Karta postaci
-----------------------
IMIĘ {self.imie}
RASA/KLAN/TYP Mag
-----------------------
SIŁA {self.sila}
ZRĘCZNOŚĆ {self.zrecznosc}
WYTRZYMAŁOŚĆ {self.wytrzymalosc}
ŻYWOTNOŚĆ {self.hp}
MAGIA {self.mana}
""")
gracz2 = Mag(imie="Merlin")
gracz2.wyswietl_karte_postaci()