python tkinter,sqlite3でポケモン図鑑


##教材元
https://www.udemy.com/the-python-mega-course/learn/v4/content

##tkinterとsqlite3の練習に簡易ポケモン図鑑作った。
frontend.pyとbackend.pyの2つのファイルを作って、frontend.pyでbackedn.pyをimportする。
appとexeファイルにも変換した。
ポケモンのjsonファイル元：
https://github.com/kotofurumiya/pokemon_data


##backend.py 
sqlite3でdatabaseを作る。jsonファイルがロードされてない場合は、ロードしてポケモン図鑑のデータを登録する

```py
#backend.py
#trati特性の思考
#多分trait枠2つつくり、trait1,trait2のボックスどちらに入力しても、dbのtrait12どちらかに存在すればTrueにすればいい。
import sqlite3
import json

def connect():
    conn=sqlite3.connect("pokemon.db")
    cur=conn.cursor()#, evolve TEXT, type1 TEXT, type2 TEXT, trait TEXT, hidden TEXT, hp INTEGER, atk INTEGER, def INTEGER, spa INTEGER, spd INTEGER, spe INTEGER)"
    sql_statement="CREATE TABLE IF NOT EXISTS pokemon (id INTEGER PRIMARY KEY, no TEXT, name TEXT, evolve TEXT, type1 TEXT, type2 TEXT)"
    cur.execute(sql_statement)
    conn.commit()
    conn.close()

#ファイルを選択させて、jsonの絶対パスを受け取る。
#そのパスを使ってloadできる?
def load_json():
    conn=sqlite3.connect("pokemon.db")
    cur=conn.cursor()
    sql_statement="INSERT INTO pokemon VALUES (Null,?,?,?,?,?)"
    with open("pokemon_data.json") as data:
        cur.execute("Select * from pokemon")
        raws = cur.fetchall()
        if len(raws)==0:
            for i in json.load(data):
                sql_tuple=(str(i["no"]),i["name"],"True" if i["evolutions"] else "False",i["types"][0],i["types"][1])
                cur.execute(sql_statement, sql_tuple)
    conn.commit()
    conn.close()

def view():
    conn=sqlite3.connect("pokemon.db")
    cur=conn.cursor()
    cur.execute("Select * from pokemon")
    raws = cur.fetchall()
    conn.close()
    return raws
    
def insert(no, name, evolve, type1, type2):
    conn=sqlite3.connect("pokemon.db")
    cur=conn.cursor()
    sql_statement="insert into pokemon values (Null,?,?,?,?,?)"
    cur.execute(sql_statement, (no,name,evolve, type1, type2))
    conn.commit()
    conn.close()

#type1のサーチどうするんだ？
#type1のみならそれだけでいいか
def search(no="", name="", evolve="", type1="", type2=""):
    conn=sqlite3.connect("pokemon.db")
    cur=conn.cursor()
    sql_statement="select * from pokemon where no=? or name=? or evolve=? or (type1=? and type2=?)"
    if type1 or type2:
        #typeが2つため:2回実行
        cur.execute(sql_statement, (no,name,evolve,type1,type2))
        rows = cur.fetchall()
        cur.execute(sql_statement, (no,name,evolve,type2,type1))
        rows += cur.fetchall()
        conn.close()
        return rows
    cur.execute(sql_statement, (no,name,evolve,type1,type2))
    rows = cur.fetchall()
    conn.close()
    return rows

def update(id, no, name, evolve, type1, type2):
    conn=sqlite3.connect("pokemon.db")
    cur=conn.cursor()
    sql_statement="update pokemon set no=?, name=?, evolve=?, type1=?,type2=? where id=?"
    cur.execute(sql_statement, (no,name,evolve, type1, type2, id))
    conn.commit()
    conn.close()
    
def delete(id):
    conn=sqlite3.connect("pokemon.db")
    cur=conn.cursor()
    sql_statement="delete from pokemon where id=?"
    cur.execute(sql_statement, (id,))
    conn.commit()
    conn.close()
    
#importされたときに自動で下記も実行される。
connect()
load_json()

```
1. 最初にデータベースにテーブルを作り、カラムにデータを登録する際には指定されたデータ型である必要がある。(ただsqlite3はfrontend側から受け取った値が仮にint型でも、テーブル内部のcolumnがTEXT型だろうと、登録してくれる)
2. bool値のデータ型がsqlite3にはないっぽい？
3. cur.fetchall()はリストが返ってくる。リストの中の要素には検索されたrowがtupleで入ってる。
4. windowが開いたときにデータベースに接続して、windowを閉じるときにデータベースとの接続を切るようにしたほうがコードの量は減るよね。これ。半分くらいに多分なる。


##frontend.py

```py

#frontend.py

from tkinter import *
import backend

#command func
#wrap function。引数を渡せないため、wrapの中で実行させる

#<<ListboxSelect>>を受け取る。eventに入る
def get_selected_row(event):
    global selected_tuple
    try:
        index=list1.curselection()[0]
        selected_tuple=list1.get(index)
        #Entryは0,ENDなのか。Textは"1.0"か
        e1.delete(0,END)
        e2.delete(0,END)
        e3.delete(0,END)
        e4.delete(0,END)
        e5.delete(0,END)
        e1.insert(END, selected_tuple[1])
        e2.insert(END, selected_tuple[2])
        e3.insert(END, selected_tuple[3])
        e4.insert(END, selected_tuple[4])
        e5.insert(END, selected_tuple[5])
    except IndexError:
        pass
    

def view_command():
    list1.delete(0, END)
    for row in view():
        list1.insert(END, row)#(場所, value)
        #rowがendに追加される
        
def search_command():
    list1.delete(0,END)
    #これ空白だとNone渡されちゃうのか？するとちょっと不都合だな…
    for row in search(no.get(),name.get(),evolve.get(),type1.get(),type2.get()):
        list1.insert(END, row)

def add_command():
    insert(no.get(),name.get(),evolve.get(),type1.get(),type2.get())
    list1.delete(0, END)
    list1.insert(END, (no.get(),name.get(),evolve.get(),type1.get(),type2.get()))
    
#リストボックス内部の選択したもののidを取得する
def delete_command():
    delete(selected_tuple[0])
    view_command()
    
def update_command():
    update(selected_tuple[0],no.get(),name.get(),evolve.get(),type1.get(),type2.get())
    search_command()
    
def reset_command():
    e1.delete(0,END)
    e2.delete(0,END)
    e3.delete(0,END)
    e4.delete(0,END)
    e5.delete(0,END)
    
    
#window
window = Tk()

#title
window.wm_title("POKeMON Guide 861s")

#l = Label(window, text="")
l1 = Label(window, text="No")
l2 = Label(window, text="Name")
l3 = Label(window, text="Evolve")
l4 = Label(window, text="Type1")
l5 = Label(window, text="Type2")
#grid(row=0,column=0)
l1.grid(row=0,column=0)
l2.grid(row=1,column=0)
l3.grid(row=2,column=0)
l4.grid(row=3,column=0)
l5.grid(row=4,column=0)
#Entry
no=StringVar()
name=StringVar()
evolve=StringVar()
type1=StringVar()
type2=StringVar()
#Entry(window, textvariable=, width=10)
e1=Entry(window, textvariable=no, width=10)
e2=Entry(window, textvariable=name, width=10)
e3=Entry(window, textvariable=evolve, width=10)
e4=Entry(window, textvariable=type1, width=10)
e5=Entry(window, textvariable=type2, width=10)
#gird
e1.grid(row=0,column=1)
e2.grid(row=1,column=1)
e3.grid(row=2,column=1)
e4.grid(row=3,column=1)
e5.grid(row=4,column=1)
#listbox of output
list1 = Listbox(window, height=10, width=50)
list1.grid(row=0, column=2, rowspan=5)
#scrollbar
sb1 = Scrollbar(window)
sb1.grid(row=0, column=3, rowspan=5)
#scrollbar connect with listbox, y軸スクロール
list1.configure(yscrollcommand=sb1.set)
#y軸view will change
sb1.configure(command=list1.yview)

#event, bind(event, func)で→func(event)の形で実行される
list1.bind("<<ListboxSelect>>", get_selected_row)

#Button(window, text="", width=12, command=None)
b1=Button(window, text="View all", width=12, command=view_command)
b2=Button(window, text="Search entry", width=12, command=search_command)
b3=Button(window, text="Add entry", width=12, command=add_command)
b4=Button(window, text="Update", width=12, command=update_command)
b5=Button(window, text="Delete", width=12, command=delete_command)
b6=Button(window, text="Reset entry", width=12, command=reset_command)
b7=Button(window, text="Close", width=12, command=exit)
#grid(row=, column=3)
b1.grid(row=0, column=4)
b2.grid(row=1, column=4)
b3.grid(row=2, column=4)
b4.grid(row=3, column=4)
b5.grid(row=4, column=4)
b6.grid(row=5, column=4)
b7.grid(row=6, column=4)

window.mainloop()

#皮つくった
#sqlite3文つくった
#ボタン用funcを作る
#リストボックスへの選択イベントを関数と紐付け

'''
        print(list1.curselection())
        print(list1.curselection()[0])
        print(list1.get)
        print(selected_tuple)
(5,)#
5
<bound method Listbox.get of <tkinter.Listbox object .!listbox>>
(6, '5', 'リザード', 'True', 'ほのお', '')
'''
```

1.  `list1.configure(yscrollcommand=sb1.set)`はListboxのインスタンスlist1とScrollBarのインスタンスsb1を紐づけた。
2.  `sb1.configure(command=list1.yview)`でスクロールバーを動かすと、listboxのy軸（縦方向）の見るものが変わる。`configure`は全てのウィジェット(Text,Listbox,Entryなど)に共通で存在。動きを紐つけることができる
3.  Buttonを押したときの動作は`command=func`のfuncの部分に関数名を指定する。しかし、この際引数を指定できない。そのため引数な必要な場合、実際に実行した関数を別の関数で包んであげる(wrap_functionと呼ばれるっぽい)。この包んだ別の関数をButtonの`command=`に渡してやればいい。
4.  `bind(event, func)`で`func(event)`の形式で実行することができる。`<<ListboxSelect>>`はリストボックス内部で選択されたときのイベント。`get_selected_row()`の引数にこのイベントが渡される。参考元：https://python.keicode.com/advanced/tkinter-widget-listbox.php
5. `get_selected_row()`は自分で定義したもの。渡されたeventを元に、
`list1.curselection()`でリストボックス内部で選択されたrowのindex部分を含むtupleを返す。tupleなので`list1.curselection()[0]`で選択列のlistbox内部のindexを得られる。
6. `list1.get(index)`で対象のrowに存在する値をtupleで受け取れる。list1はListBoxのインスタンス。`list1.get()`のみだとListbox内部の値全てが返ってくる。
7. jupyterlabでこのコードを実行すると、`window.destroy`が働かない。.pyにしてterminalで動かせば普通に動く。


##おまけ　スタンドアローン化する

```
pip install pyinstaller
pyinstaller --onefile --windowed frontend.py 
```

pyinstallerがない場合`pip install pyinstaller`する。
backend.pyとfrontend.pyを同じディレクトリにおく。同ディレクトリ上で上記コードを実行すれば、appを作れる。以外の簡単なんやね。exeファイルも作れる。