Python Panoráma 5: Adatbázis interakció és UI építés17 perc olvasás

Az előző két részben jellemzően a webalkalmazások területén foglalatoskodtunk. Most azonban visszatérünk a “python-dúsabb” kódokhoz és megpróbálunk mélyebb és stabilabb tudásról tanúbizonyságot adni egy komplett kis könyvtár program keretében. A legtöbb alkalmazás, amelyet nap mint nap használunk alapvetően két jól elkülöníthető részből tevődik össze. Egyrészt áll egy frontendből (ez jelenti mindazt, amit a felhasználó közvetlenül, külsőleg lát a programból), másrészt pedig egy backend-ből, amely a tényleges mögöttes működését biztosítja a programnak (ez teremt kapcsolatot az adatbázissal és ebben találhatók azok a funkciók is, amelyek például a UI elemeihez kapcsolódnak). A front-end azért szükséges, hogy a felhasználó könnyedén tudja működtetni a programot, a backend pedig azért, hogy maga a program képes legyen műveletek végrehajtani és ne csak funkció nélküli gombok halmaza legyen az alkalmazás.

CÉL

A Python Panoráma ötödik részében nem lesz hiány kódírásból, ugyanis most külön-külön fogjuk megírni az alkalmazásunk front-end, illetve back-end részét. A végső cél pedig, hogy ezt a két scriptet összeolvasztva létrehozzunk egy egyszerűen kezelhető .exe fájlt, így feloldozva a megcélzott felhasználót a Python és a könyvtárak telepítési feladata alól. Így a programunk egyben lesz hordozható és felhasználóbarát is. De mit is tud majd pontosan a programunk?

Egy olyan könyvtár programot fogunk előállítani, amely képes lesz biztosítani a felhasználó számára, hogy könyveket kereshessen, adhasson hozzá a programhoz, törölhessen belőle vagy hogy éppen megváltoztathassa egy könyv paramétereit, például a szerző nevét. Nagyjából ezek azok a funkciók, amelyeket majd a back-end scriptünk fog tartalmazni.

MEGVALÓSÍTÁS

1. Front-end

A program elkészítése során először a frontend script megírását ajánlom, ugyanis azt követően már ki tud alakulni egy kép a fejünkben, hogy hogyan is kéne működnie a programunknak. Ebben a bemutatóban a tkinter python könyvtárat fogjuk használni a felhasználói felület megalkotásához. Ebből adódóan első lépésként importáljunk minden class-t a tkinter-ből a frontend.py scriptünkbe.

A könyvtár programunkhoz kapcsolódó funkciókat, azaz az új könyv hozzáadását, törlését stb. egy-egy elkülönített gombbal tudja majd irányítani a felhasználó. Emellett lehetősége lesz a négy mezőbe (Cím, Szerző, Kiadás éve, ISBN kód) a megfelelő inputot megadni a programunk számára. Továbbá a felhasználónak biztosítanunk kell egy lista elemet is, ahol láthatja például a keresés eredményeként kiadott könyveket, úgy hogy azok között egy scrollbar segítségével tudjon navigálni. Ha elképzeljük ezt egy ablakban akkor az így néz ki:

Ahogy láthatjuk szükségünk lesz egy két UI elemre. Előszőr egy ablakot kell megadnunk, majd ahhoz, hogy abba elemeket tudjunk illeszteni a tkinter Button, Entry, Listbox, illetve Label method-jeit kell használnunk. A tkinter-ben kétféle method alkalmazásával lehet elrendezni az egyes objektumokat. Az egyik a “.pack” a másik pedig a “.grid” method. Mi most a “.grid” method-öt fogjuk használni, amely egy rácsos szerkezetben helyezi el a gombokat, listákat és szövegdobozokat.

Összességében tehát egy UI elem előállításakor el kell mentenünk egy változóban a létrehozott elemet, mondjuk egy Button-t, meg kell határoznunk, hogy az adott elem melyik ablakhoz tartozik, hogy mi legyen a neve és hol helyezkedjen el az ablakon belül. Egy elem elhelyezkedését úgy adhatjuk meg, hogy megadjuk hányadik sorban és hányadik oszlopban legyen. Ebben az esetben például az “Add entry” gomb a harmadik oszlopban és a negyedik sorban helyezkedik el, mivel 0-tól kezdjük azok besorszámozását.

Egy adott elem hozzáadása tehát a következőképpen néz ki a front-end scriptünkben:
1. Gomb

b3=Button(window, text="Add entry", width=12)
b3.grid(row=4,column=3)

2. Címke

l1=Label(window,text="Title")
l1.grid(row=0,column=0)

3. Lista

list1=Listbox(window, height=6, width=35)
list1.grid(row=2,column=0, rowspan=6, columnspan=2)

4. Scrollbar

sb1=Scrollbar(window)
sb1.grid(row=2,column=2, rowspan=6)

5. Beviteli szövegdoboz

title_text=StringVar()
e1=Entry(window,textvariable=title_text)
e1.grid(row=0, column=1)

Ami az Entry elemhez tartozó kódrészlet esetében feltűnhet, az az hogy a method második argumentumában a beírt szöveget egy title_text nevű speciális stringváltozóban kell eltárolnunk. Ez azért szükséges, mert ezeket az értékeket még később használni fogjuk a back-end script függvényeinél.
Ha hozzáadtuk az összes UI elemet az ablakunkhoz, akkor valahogy így kell kinéznie a front-end scriptünknek:

from tkinter import *window=Tk()

window.wm_title("BookStore")

l1=Label(window,text="Title")
l1.grid(row=0,column=0)

l2=Label(window,text="Author")
l2.grid(row=0,column=2)

l3=Label(window,text="Year")
l3.grid(row=1,column=0)

l4=Label(window,text="ISBN")
l4.grid(row=1,column=2)

title_text=StringVar()
e1=Entry(window,textvariable=title_text)
e1.grid(row=0, column=1)

author_text=StringVar()
e2=Entry(window,textvariable=author_text)
e2.grid(row=0, column=3)

year_text=StringVar()
e3=Entry(window,textvariable=year_text)
e3.grid(row=1, column=1)

isbn_text=StringVar()
e4=Entry(window,textvariable=isbn_text)
e4.grid(row=1, column=3)

list1=Listbox(window, height=6, width=35)
list1.grid(row=2,column=0, rowspan=6, columnspan=2)

sb1=Scrollbar(window)
sb1.grid(row=2,column=2, rowspan=6)

list1.configure(yscrollcommand=sb1.set)
sb1.configure(command=list1.yview)

list1.bind('<<ListboxSelect>>',get_selected_row)

b1=Button(window, text="View all", width=12)
b1.grid(row=2,column=3)

b2=Button(window, text="Search entry", width=12)
b2.grid(row=3,column=3)

b3=Button(window, text="Add entry", width=12)
b3.grid(row=4,column=3)

b4=Button(window, text="Update selected", width=12)
b4.grid(row=5,column=3)

b5=Button(window, text="Delete selected", width=12)
b5.grid(row=6,column=3)

b6=Button(window, text="Close", width=12)
b6.grid(row=7,column=3)

window.mainloop()

Amit eddig még nem érintettünk az az, hogy a lista és a scrollbar összekapcsolása is szükséges. Ezt a configure method alkalmazásával oldottuk meg. Igazából ezzel el is a készült a programunk felhasználói felülete, azaz most már áttérhetünk a back-end script megírására.

2. Back-end

A back-end scriptünk fogja tartalmazni azokat a függvényeket, amelyek egy gomb megnyomásakor lefutnak. A legelső függvény, amit megírunk a connect() függvény lesz. Ez fogja biztosítani, hogy a programunk megfelelően tudjon kapcsolódni egy adatbázishoz, amelyre majd a többi függvény is hivatkozni fog. Mivel egy adatbázissal szeretnénk műveleteket végezni, így a kód legelején importáljuk script-ünkbe az sqlite3 python könyvtárat. Így képesek leszünk a “.db” fájlhoz kapcsolódni és SQL parancsokat végrehajtani a scriptünkön belül. A connect függvény, ahogy minden másik backend függvény esetében is van jellemzően négy, de legalább három olyan sor, amelyeknek mindig szerepelniük kell a függvényen belül. Ezek a következők:

conn=sqlite3.connect("books.db")
cur=conn.cursor()
conn.commit() # this is the row whish is not always necessary
conn.close()

Az első sorban kapcsolódunk az adatbázisunkhoz. A másodikban létrehozzuk a kurzor objektumot, amely segítségével majd SQL parancsokat tudunk kiadni. A harmadikban dokumentáljuk az adatbázison végzett változtatásokat (*: ez az a sor, amely nem mindig szükséges) A negyedik sorban pedig bezárjuk az adatbázissal létesített kapcsolatot. Most, hogy tisztáztuk ezeknek a soroknak lényegét értelmezzük egyesével azt is, hogy mit csinálnak az egyes függvények. Összesen hat függvényünk lesz: connect(), insert(), view(), search(), delete() és update().

1. connect()

def connect():
    conn=sqlite3.connect("books.db")
    cur=conn.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS book (id INTEGER PRIMARY KEY, title text, author text, year integer, isbn integer)")
    conn.commit()
    conn.close()

Ebben a függvényben azt vizsgáljuk meg, hogy létezik-e “book” nevezetű tábla az adatbázisunkban. Ha igen, akkor nem történik semmi érdekes. Viszont, ha nem létezik, akkor a program létrehoz egy book táblát a books adatbázisban. Ennek a táblának pedig az első oszlopa egy elsőleges kulcs (a könyvek azonosítására alkalmas id) lesz, a második a könyv címe és így tovább.

2. insert()

def insert(title, author, year, isbn):
    conn=sqlite3.connect("books.db")
    cur=conn.cursor()
    cur.execute("INSERT INTO book VALUES (NULL,?,?,?,?)",(title, author, year, isbn))
    conn.commit()
    conn.close()

Az insert függvény a books adatbázishoz történő csatlakozás után egy sort szúr be a book táblába, amely értékei az egyes változókban vannak eltárolva. Az SQLite esetében ezt kódrészletet úgy kell értelmezni, hogy a kérdőjelek helyére a második paraméterként megadott tuple megfelelő elemei kerülnek beírásra, pontosabban az egyes változók aktuális értékei.

3. view()

def view():
    conn=sqlite3.connect("books.db")
    cur=conn.cursor()
    cur.execute("SELECT * FROM book")
    rows=cur.fetchall()
    conn.close()
    return rows

A view függvény esetében a scriptünk a tábla összes mezőjét kiválasztja, majd azokat egy változóba helyezi át a “.fetchall” method segítségével és végül eredményként visszaadja azokat. Ebben az esetben nincs szükség a conn.comit() sorra ugyanis a táblánkból csak kiolvastunk adatok és nem módosítottuk azt.

4. search()

def search(title="", author="", year="", isbn=""):
    conn=sqlite3.connect("books.db")
    cur=conn.cursor()
    cur.execute("SELECT * FROM book WHERE title=? OR author=? OR year=? OR isbn=?", (title,author,year,isbn))
    rows=cur.fetchall()
    conn.close()
    return rows 

A search függvény csupán annyival különbözik a view függvénytől, hogy csak azokat a sorokat adja vissza, ahol az egyes paraméterek valamelyikének (azaz az oszlopérték) értéke megegyezik a változó aktuális értékével.

5. delete()

def delete(id):
    conn=sqlite3.connect("books.db")
    cur=conn.cursor()
    cur.execute("DELETE FROM book WHERE id=?",(id,))
    conn.commit()
    conn.close()

A delete függvény úgy módosítja a book táblát, hogy kitörli azt a könyvet amelyiknek az id-je megegyezik az id változóban tárolttal. Az id azért szükséges itt, mert ez alapján fogjuk meghatározni, hogy melyik könyvet törölje ki a program, ha a felhasználó kiválaszt egy adott könyvet a listából és a delete gombra nyom.

6. update()

def update(id,title,author,year,isbn):
    conn=sqlite3.connect("books.db")
    cur=conn.cursor()
    cur.execute("UPDATE book SET title=?, author=?, year=?, isbn=? WHERE id=?",(title,author,year,isbn,id))
    conn.commit()
    conn.close()

Az update függvény, részben hasonlóan a delete függvényhez, az id alapján fogja módosítani a táblát. Viszont az update függvény egy meglévő könyv adatait változtatja meg, majd azokra az értékekre, amik éppen a változók aktuális értékei. És hogy mi is lesz a változó aktuális értéke? Mindig az lesz, ami épp az Entry elemben szerepel, mint érték vagy az id esetében mindig az az id, ami az éppen kijelölt könyvhöz tartozik.

Ha ezeket összerakjuk, akkor meg is van a back-end scriptünk, ami innen letölthető egyébként.

Most viszont, hogy meg van mindkét scriptünk, már csak össze kell őket gyúrnunk. Mivel a backend függvényeit, akarjuk hozzárendelni a gombokhoz és más UI elemekhez, ezért importáljuk a mybackend.py scriptünket a frontend.py scriptünkbe. A felhasználói felület felokosítását kezdjük azzal, hogy a gombokhoz funkciókat rendelünk. Ezt úgy tehetjük meg, hogy az egyes UI elemek argumentumait kibővítjük a command=”függvényneve” résszel.

Először azt gondolnánk, hogy most már csak a back-end függvényeit kell a “függvényneve” helyre beilleszteni, de ez sajnos nem így van. Ahhoz, hogy a programunk megfelelően működjön külön definiálnunk kell a gombokhoz is függvényeket azért, mert a backend script függvényei még nem a megfelelő outputot nyújtják.

1. View all

def view_command():
    list1.delete(0,END) #deletes everything from the first row to last
    for row in mybackend.view():	 #iterates through the rows in the tuple given by mybackend.view()
        list1.insert(END,row)	 #inserts the given rows

Ha a Command Line-ban lefuttatjuk a mybackend.py script view függvényét, akkor láthatjuk, hogy az eredménye egy olyan lista, amelynek az elemei tuple-k. A tuple-k elemei pedig az id, könyv címe, szerző stb. Ahhoz, hogy ezeket a tuple-ket rendesen ki bírjuk íratni a Listbox-ba szükségünk van a view_command függvényre. Ez a függvény először megtisztítja a Listbox-ot majd a lista elemein végighaladva beilleszti azokat. Az END argumentum azt jelenti, hogy a soron következő könyv adatait mindig a Listbox sorok végére rakja.

2. Search entry

def search_command():
    list1.delete(0,END)
    for row in mybackend.search(title_text.get(),author_text.get(),year_text.get(),isbn_text.get()):
        list1.insert(END,row)

A view_command-hoz hasonlóan itt is ürítjük a listát és mindig a végére illesztünk be elemeket, de itt azon a könyvek között iterál a program, amelyek megfelelnek az adott feltételeknek. Ahogy még előbb említettem a title_text egy speciális stringváltozó, így a “.get” method-öt kell alkalmaznunk ahhoz, hogy egy egyszerű string-et kapjunk vissza belőle.

3. Add entry

def add_command():
    mybackend.insert(title_text.get(),author_text.get(),year_text.get(),isbn_text.get())
    list1.delete(0,END)
    list1.insert(END,(title_text.get(),author_text.get(),year_text.get(),isbn_text.get()))

Az add_command függvény esetében nincs sok dolgunk. Azonkívül, hogy végrehajtjuk a back-end script insert függvényét igazából csak azt kell megoldanunk, hogy a művelet eredménye a felhasználó számára is látható legyen, ezért a Listbox-ba kiírattatjuk a bevitt könyv paramétereit.

ID keresés kijelölés alapján

Ahhoz, hogy a Deletes selected és Update selected gombokhoz kapcsolódó függvényeket meg bírjuk írni szükségünk van egy olyan függvényre, amely visszaadja nekünk az éppen kijelölt elem paramétereit. Ez a függvényünk lesz a get_selected_row().

def get_selected_row(event):
    global selected_tuple
    index=list1.curselection()
    selected_tuple=list1.get(index)

Ez a függvény azt csinálja, hogy az aktuálisan kijelölt könyv indexét megkeresi és az ehhez az indexhez tartozó sor paramétereit egy listába rendezi. A selected_tuple változó globálissá tétele azért szükséges, mert lokális változóként nem tudnánk később a delete_command és update_command függvényeknél hivatkozni rá.

4. Delete selected

def delete_command():
    mybackend.delete(selected_tuple[0])
    view_command()

Most, hogy a get_selected_row függvény segítségével hozzájutottunk a kijelölt sor paramétereinek listájához nincs más dolgunk, mint annak első elemét venni, ami pont az id és végrehajtani a delete függvényt a back-end scriptből.

5. Update selected

def update_command():
    mybackend.update(selected_tuple[0], title_text.get(), author_text.get(), year_text.get(), isbn_text.get())
    view_command()

Ha ezeket mind egyesítjük akkor megkapjuk a végső front-end scriptünket.

TOVÁBBFEJLESZTÉS ÉS FELHASZNÁLÁS

A programunk már most is elég jól működik, azonban felhasználóbarát szempontból van még mit rajta csiszolni. Többek között nem lenne egy rossz dolog, ha a programunk egyből kiírná az éppen kiválasztott könyv paramétereit a szövegdobozokba. Továbbá, ha kipróbálgattuk a programunkat akkor észrevehettük, hogy mikor elindítjuk a programot és az első kattintásunk a Listbox-ot érinti, úgy a Command Line-ba megjelenik egy hiba kód, hogy a kijelölt elemből nem tudunk listát készíteni, ugyanis egyetlen eleme sincs. Ezeket a problémákat a get_selected_row függvényen belül tudjuk feloldani az alábbi módon:

def get_selected_row(event):
    try:
        global selected_tuple
        index=list1.curselection()
        selected_tuple=list1.get(index)
        e1.delete(0,END)
        e1.insert(END,selected_tuple[1])
        e2.delete(0,END)
        e2.insert(END,selected_tuple[2])
        e3.delete(0,END)
        e3.insert(END,selected_tuple[3])
        e4.delete(0,END)
        e4.insert(END,selected_tuple[4])
    except TclError:
        pass

Egy további nagy előrelépést jelentene, ha a programunkat hordozhatóvá tudnánk tenni és az is jó lenne, ha egy IT-ban kevésbé jártas emberkét nem rémítenénk meg egy hirtelen felugró Command Line ablakkal. Ezt pedig úgy tudjuk megoldani, hogy nyitunk egy Command Line ablakot a frontend és backend script-ünket tartalmazó mappában, letöltjük a pyinstaller-t a szokások pip paranccsal, majd beírjuk a következő parancsot:

pyinstaller --onefile --windowed frontend.py

Ha mindent jól csináltunk, akkor ezzel a sorral létrehoztunk egy frontend.exe fájlt, amelynek elindításakor már nem jelenik meg a Comman Line ablak. Csak arra kell figyelnünk, hogy a programunk képes legyen megtalálni a “books.db” adatbázist.

Ha esetleg a programunk átláthatóságán és struktúráján szeretnénk javítani, akkor pedig érdemes lehet megpróbálni átültetni ezt az egész programot egy objektum orientált programozási paradigmának megfelelő formába. Ennek a megoldása is elérhető az ehhez a részhez tartozó github repository-ban.

Ami a program felhasználási lehetőségeit illeti szerintem mindenképpen érdemes és tanulságos lehet az első részben készített interaktív szótárral való összedolgozása, de persze továbbfejlesztve akár hasznosíthatjuk kisebb logisztikai rendszerekhez vagy mondjuk számlaiktató rendszerekhez is.

Köszönöm, hogy végigolvastad a posztomat és ha érdekesnek találtad, akkor nézz bele a sorozat többi részébe is.

Gulácsy Dominik

About Dominik Gulácsy

Sophomore at Corvinus University of Budapest studying International Business who is motivated to use relevant academic knowledge to solve problems through optimisation. Dedicated to fully support the development of new business solutions in close collaboration with team members by IoT and data science applications. Gained experience in SQL and VBA but looking forward to learning more. A keen supporter of the circular economy.

View all posts by Dominik Gulácsy →

Leave a Reply

Your email address will not be published. Required fields are marked *