今日のPython「ToDoアプリGUIのバージョンアップ(ver2.5):データベースの検索機能追加」

2024年10月5日土曜日

Python Python_ToDoアプリ編 公開

t f B! P L

   今回もToDoアプリのGUI版をバージョンアップしたいと思います。


アプリ上の問題点

以前GUIでこのような「ToDoアプリ」を作りました。(参照:過去記事




レコードが増えてくると、一覧の中から対象となるレコードを探すのが困難になります。

これでは不便ですね。


改善対策

そこで、データベース内を単語検索し、該当する単語を含むレコードのみを表示するような機能を追加したいと思います。


では、始めます。

プログラム修正の概要

以前のバージョンからのプログラム修正内容を簡単に説明します。

まず、検索機能をget_search_todos関数として宣言します。

この関数にはテキストボックスに入力された単語でデータベース内を検索し、該当する単語を含むレコードを抽出する機能を持たせました。

テキストボックスが3つあり、それぞれのボックス内に入力された単語で検索する必要がありますのでif文で条件をわけて、検索するようにしました。



# 関数(データベースのデータ検索)
def get_search_todos(ToDo,date,id):
    # if文で条件分岐
    # 各テキストボックスに入力された条件で検索
    if ToDo != "" :
        with sqlite3.connect(db_path) as conn:
            cur = conn.cursor()
            # プレースホルダ
            query = '''SELECT * FROM myItem
                    where ToDo like ? '''
            cur.execute(query,('%'+ ToDo +'%',))
            rows = cur.fetchall()
            return rows

    elif date != "" :
        with sqlite3.connect(db_path) as conn:
            cur = conn.cursor()
            # プレースホルダ
            query = '''SELECT * FROM myItem
                    where date like ? '''
            cur.execute(query,('%'+ date +'%',))
            rows = cur.fetchall()
            return rows
    elif id != "" :
        with sqlite3.connect(db_path) as conn:
            cur = conn.cursor()
            #プレースホルダ
            query = '''SELECT * FROM myItem
                    where id like ? '''
            cur.execute(query,('%'+ id +'%',))
            rows = cur.fetchall()
            return rows
    # テキストボックスに入力がない場合は、全件表示
    else :
        with sqlite3.connect(db_path) as conn:
            cur = conn.cursor()
            cur.execute('select * from myItem')
            rows = cur.fetchall()
            return rows


SQL文で条件を絞って該当するレコードのみを抽出します。

特定の単語を含む文字列を検索するため、単語の前後に%を+で繋いで検索します。


cur.execute(query,('%'+ ToDo +'%',))

このとき、注意が必要なのは最後のカンマです。

無ければ、このようなエラーになります。

Incorrect number of bindings supplied. The current statement uses 1, and there are 3 supplied.

executeの第2引数はタプルだそうで、要素がひとつだけの時は、最後にカンマ(,)を記述する必要がある、とのこと。

以上の検索結果を関数view_search_todosで表示。



# 関数(リストボックスにToDoデータ検索結果表示)
def view_search_todos(tree,ToDo,date,id):
    #テキストボックスクリア
    id_text.delete(0, tk.END)
    ToDo_text.delete(0, tk.END)
    date_text.delete(0, tk.END)
    #リストボックスの表示クリア
    tree.delete(*tree.get_children())
    #レコードの検索結果をリストボックスに表示
    for row in get_search_todos(ToDo,date,id):
        tree.insert(parent='', index='end', iid=row ,values=(str(row[0]), str(row[1]) ,str(row[2])))



次に「検索」ボタンを追加します。



# ボタン(ToDoデータ検索)の設定
search_button = tk.Button(button_frame, text="検索", width=20, command=lambda: view_search_todos(tree,ToDo_text.get(),date_text.get(),id_text.get()))#
search_button.pack(side=tk.LEFT)



併せて、アプリ起動時に、TODO一覧を表示するように変更し、「表示」ボタンは削除することにしました。



#########################################
# GUI Treeviewにレコード一覧を初期表示  --- (5-6)
#########################################
view_todos(tree)




最後に、HELPメッセージの修正をします。

# 関数(HELPメッセージ表示)
def help_msg():

に以下の説明文を追加します。

        +"「検索」ボタン\n"
        +" 「ID」、「Todo内容」、「日付」欄に入力された単語で\n"
        +" データベースを検索し、その結果を表示します。\n"
        +"\n"

以上で修正は終了です。  

プログラム修正後の結果

では、実際にプログラムを実行して試してみます。


「検索」ボタンが追加されていますね。
「アプリ」という単語を「TODO内容」欄に入力して、ボタンを押すと



このように3件のみ表示されました。

まとめ

最後に


以上、GUI版「ToDoアプリ」の見直しをしてみました。
これからも見た目や、使い勝手をよくしていこうと思っています。

最後に、プログラム修正後の全サンプルコードを参考として掲載しておきます。

プログラム修正後の全サンプルコード





##########################################
# ToDoアプリGUI_ver2.5.py
# 2024/09/15
# -データベースの検索機能追加
# --「検索」ボタン設置
# --「view_search_todos」関数追加
# --「get_search_todos」関数追加
# -表示ボタンを削除し、更新ボタンに統合
# -アプリ起動時にTreeviewにレコード一覧を初期表示
##########################################

##########################################
# ライブラリをインポート --- (1)
##########################################
import os
import csv
import sqlite3
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
from tkinter import filedialog
#from pandas import DataFrame

#########################################
# 変数等を宣言 --- (2)
#########################################
db_path = "ToDo.sqlite3"
#csv_imp_path = "users.csv"
csv_exp_path = "exported_users.csv"


#########################################
# 関数を宣言 --- (3)
#########################################
# 関数(データベースの初期化)
def load_todos():
    if not os.path.exists(db_path):
        with sqlite3.connect(db_path) as conn:
          cur = conn.cursor()
          query = '''create table myItem(
                 id integer primary key autoincrement,
                 ToDo string,
                 date string
          )'''
          cur.execute(query)

# 関数(データのインポート)
def import_todos():
    conn = sqlite3.connect(db_path)
    cur = conn.cursor()

    #ファイルダイアログからインポート
    fTyp = [("","*")]
    iDir = os.path.abspath(os.path.dirname(__file__))
    csv_imp_path = tk.filedialog.askopenfilename(filetypes = fTyp,initialdir = iDir)

    with open(csv_imp_path, 'r') as f:
        reader = csv.reader(f)
        query = '''insert into myItem(
                 ToDo,
                 date
        ) values(?,?)'''
        for row in reader:
            cur.execute(query, ( row[1], row[2]))
    conn.commit()
    conn.close()

# 関数(データのエクスポート)  
def export_todos():
    conn = sqlite3.connect(db_path)
    cur = conn.cursor()
   # if not os.path.exists(csv_exp_path):
    with open(csv_exp_path, 'w') as f:
      writer = csv.writer(f)
      for row in cur.execute('SELECT * FROM myItem'):
            writer.writerow(row)
    conn.close()

# 関数(データベースからToDoデータ全件取得)
def get_todos():
    with sqlite3.connect(db_path) as conn:
        cur = conn.cursor()
        cur.execute('select * from myItem')
        rows = cur.fetchall()
        return rows
             
# 関数(リストボックスにToDoデータ全件表示)
def view_todos(tree):
    #テキストボックスクリア
    id_text.delete(0, tk.END)
    ToDo_text.delete(0, tk.END)
    date_text.delete(0, tk.END)
   
    tree.delete(*tree.get_children())
    for row in get_todos():
        tree.insert(parent='', index='end', iid=row ,values=(str(row[0]), str(row[1]) ,str(row[2])))

# 関数(データベースにデータ追加)
def add_todo(ToDo, date, id):
    with sqlite3.connect(db_path) as conn:
      cur = conn.cursor()
      query = '''insert into myItem(
                 ToDo,
                 date
      ) values(?,?)'''
      cur.execute(query,(ToDo,date))
      conn.commit()
     
      # 入力内容クリア
      clear_text()

# 関数(データベースのデータ削除)
def delete_todo(ToDo,date,id):
    with sqlite3.connect(db_path) as conn:
      cur = conn.cursor()
      query = 'delete from myItem where id = ?'
      cur.execute(query,[id])
      conn.commit()
     
      # 入力内容クリア      
      clear_text()

# 関数(データベースのデータ更新)
def update_todo(ToDo,date,id):
    with sqlite3.connect(db_path) as conn:
      cur = conn.cursor()
      query = '''update myItem set
                 ToDo = ?,
                 date = ?
                 where id = ?'''
      cur.execute(query,(ToDo,date,id))
      conn.commit()
     
      # 入力内容クリア
      clear_text()

# 関数(データベースのデータ検索)
def get_search_todos(ToDo,date,id):
    # if文で条件分岐
    # 各テキストボックスに入力された条件で検索
    if ToDo != "" :
        with sqlite3.connect(db_path) as conn:
            cur = conn.cursor()
            # プレースホルダ
            query = '''SELECT * FROM myItem
                    where ToDo like ? '''
            cur.execute(query,('%'+ ToDo +'%',))
            rows = cur.fetchall()
            return rows

    elif date != "" :
        with sqlite3.connect(db_path) as conn:
            cur = conn.cursor()
            # プレースホルダ
            query = '''SELECT * FROM myItem
                    where date like ? '''
            cur.execute(query,('%'+ date +'%',))
            rows = cur.fetchall()
            return rows
    elif id != "" :
        with sqlite3.connect(db_path) as conn:
            cur = conn.cursor()
            #プレースホルダ
            query = '''SELECT * FROM myItem
                    where id like ? '''
            cur.execute(query,('%'+ id +'%',))
            rows = cur.fetchall()
            return rows
    # テキストボックスに入力がない場合は、全件表示
    else :
        with sqlite3.connect(db_path) as conn:
            cur = conn.cursor()
            cur.execute('select * from myItem')
            rows = cur.fetchall()
            return rows

# 関数(リストボックスにToDoデータ検索結果表示)
def view_search_todos(tree,ToDo,date,id):
    #テキストボックスクリア
    id_text.delete(0, tk.END)
    ToDo_text.delete(0, tk.END)
    date_text.delete(0, tk.END)
    #リストボックスの表示クリア
    tree.delete(*tree.get_children())
    #レコードの検索結果をリストボックスに表示
    for row in get_search_todos(ToDo,date,id):
        tree.insert(parent='', index='end', iid=row ,values=(str(row[0]), str(row[1]) ,str(row[2])))



# 関数(テキストボックスの入力内容クリア)
def clear_text():

      view_todos(tree)

# 関数(TREEVIEW上で選択された行から情報を取得)
def select_record(event):
   
    # 選択行の判別
    record_id = tree.focus()
   
    # テキストボックス内の値を削除    
    id_text.delete( 0, tk.END )
    ToDo_text.delete( 0, tk.END )
    date_text.delete( 0, tk.END )
   
    # 選択行のレコードを取得
    record_values = tree.item(record_id, 'values')
   
    # テキストボックスに値を挿入
    id_text.insert( 0, record_values[0] )
    ToDo_text.insert( 0, record_values[1] )
    date_text.insert( 0, record_values[2] )
   
    #treeviewの選択解除
    tree.selection_remove(tree.selection())

# 関数(HELPメッセージ表示)
def help_msg():
    tk.messagebox.showinfo(title="HELP",
                              message="【操作説明】\n"

                              +"「追加」ボタン\n"
                              +" 「Todo内容」、「日付」欄に入力されたデータが\n"
                              +" データベースに保存されます。\n"
                              +"\n"
                              +"「削除」ボタン\n"
                              +" 「ID」欄に入力されたIDをもつデータが\n"
                              +" データベースから削除されます\n"
                              +"\n"
                              +"「更新」ボタン\n"
                              +" 登録済みのデータの内容を変更できます。\n"
                              +" データベースに保存されます。\n"
                              +"\n"
                              +"「検索」ボタン\n"
                              +" 「ID」、「Todo内容」、「日付」欄に入力された単語で\n"
                              +" データベースを検索し、その結果を表示します。\n"
                              +"\n"
                              )

#########################################
# データベースを呼び出し --- (4)
#########################################
todos = load_todos()

#########################################
# GUI メインウィンドウの設定 --- (5-1)
#########################################
# rootウィンドウ作成,(タイトル,サイズ)の設定
root = tk.Tk()
root.title("ToDoアプリGUI")
root.geometry("600x400")

# フレームの配置
view_frame = tk.Frame(root)
view_frame.pack(anchor=tk.CENTER)

text_frame = tk.Frame(root)
text_frame.pack(anchor=tk.W)

button_frame = tk.Frame(root)
button_frame.pack(anchor=tk.CENTER)

button2_frame = tk.Frame(root)
button2_frame.pack(anchor=tk.CENTER)

#########################################
# GUI フレーム(view)の設定 --- (5-2)
#########################################
# 列の識別名を指定
column = ('id', 'ToDo', 'date')

tree = ttk.Treeview(view_frame, columns=column)

# マウスで行を選択したときのイベント
tree.bind("<<TreeviewSelect>>", select_record)

# 列の設定
tree.column('#0',width=0, stretch='no')
tree.column('id', anchor='center', width=50)
tree.column('ToDo',anchor='w', width=300)
tree.column('date', anchor='center', width=80)

# 列の見出し設定
tree.heading('#0',text='')
tree.heading('id', text='ID',anchor='center')
tree.heading('ToDo', text='ToDo内容', anchor='center')
tree.heading('date',text='日付', anchor='center')

# ウィジェットの配置
tree.pack(anchor=tk.CENTER,pady=10)

#########################################
# GUI フレーム(text)の設定 --- (5-3) 
#########################################
# テキストボックス(ID)の設定
id_lbl1 = tk.Label(text_frame,text="ID:")
id_lbl1.pack(side=tk.LEFT)
id_text = tk.Entry(text_frame, width=5)
id_text.pack(side=tk.LEFT)

# テキストボックス(ToDo内容)の設定
add_lbl1 = tk.Label(text_frame,text="ToDo内容:")
add_lbl1.pack(side=tk.LEFT)
ToDo_text = tk.Entry(text_frame, width=50)
ToDo_text.pack(side=tk.LEFT)

# テキストボックス(日付)の設定
add_lbl2 = tk.Label(text_frame,text="日付:")
add_lbl2.pack(side=tk.LEFT)
date_text = tk.Entry(text_frame, width=15)
date_text.pack(side=tk.LEFT)

#########################################
# GUI フレーム(button)の設定  --- (5-4)
#########################################

# ボタン(ToDoデータ全件表示)の設定
# view_button = tk.Button(button_frame, text="表示", width=20, command=lambda: view_todos(tree))
# view_button.pack(side=tk.LEFT)

# ボタン(ToDoデータ追加)の設定
add_button = tk.Button(button_frame, text="追加", width=20, command=lambda: add_todo(ToDo_text.get(),date_text.get(),id_text.get()))
add_button.pack(side=tk.LEFT)

# ボタン(ToDoデータ削除)の設定
delete_button = tk.Button(button_frame, text="削除", width=20, command=lambda: delete_todo(ToDo_text.get(),date_text.get(),id_text.get()))
delete_button.pack(side=tk.LEFT)

# ボタン(ToDoデータ更新)の設定
update_button = tk.Button(button_frame, text="更新", width=20, command=lambda: update_todo(ToDo_text.get(),date_text.get(),id_text.get()))
update_button.pack(side=tk.LEFT)

# ボタン(ToDoデータ検索)の設定
search_button = tk.Button(button_frame, text="検索", width=20, command=lambda: view_search_todos(tree,ToDo_text.get(),date_text.get(),id_text.get()))#
search_button.pack(side=tk.LEFT)

#########################################
# GUI フレーム(button2)の設定  --- (5-5)
#########################################

# ボタン(データインポート)の設定
import_button = tk.Button(button2_frame, text="インポート", width=20, command=lambda: import_todos())
import_button.pack(side=tk.LEFT)

# ボタン(データエクスポート)の設定
export_button = tk.Button(button2_frame, text="エクスポート", width=20, command=lambda: export_todos())
export_button.pack(side=tk.LEFT)

# ボタン(HELPメッセージ表示)の設定
help_button = tk.Button(button2_frame, text="HELP", width=20, command=lambda: help_msg())
help_button.pack(side=tk.LEFT)

#########################################
# GUI Treeviewにレコード一覧を初期表示  --- (5-6)
#########################################
view_todos(tree)


#########################################
# イベントループ開始 --- (6)
#########################################
root.mainloop()




実行形式ファイル(exe)の作成

実行形式のファイルにする(exe化)場合は、こちらを参考にしてください(過去記事


ToDoアプリの更新履歴

Python_ToDoアプリ編として、こちらからご覧いただけます(更新履歴



このブログを検索

アーカイブ

カテゴリー

QooQ