今日のPython「ToDoアプリGUIのバージョンアップ(ver2.8):メニューバーを追加」

2024年11月2日土曜日

Python Python_ToDoアプリ編 公開

t f B! P L

はじめに

このブログでは、現在、Pythonで、ToDoアプリの製作を進めています。アプリと言ってもまだまだ、実用に耐えうる状態では無く、全く手探りの状態で、コツコツと機能の追加等を行っています。

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

これまでの経緯はPython_ToDoアプリ編として、こちらからご覧いただけますので宜しければご覧下さい。(→更新履歴


現在のアプリの状態と問題点

今の「ToDoアプリ」は、このような画面になっています。

今回、気になった点は、画面のボタン配置です。
現在は様々なボタンが画面上に並列に並んでいます。このままボタンを追加していくと画面がボタンだらけになってしまいます。
これらを綺麗に整理してスッキリしたいと思います。

改善対策

そこで、メニューバーを追加することにしました。一般的なアプリでは、画面の上部にメニューバーがあって、クリックするとプルダウンメニューが表示されます。あれを採用したいと思います。

今配置しているボタンの中では、インポート、エクスポート、HELPをメニューバーに移動させるのが良いと思います。

では、始めます。

プログラム修正の概要

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

まず、メインウィンドウであるrootウィンドウにメニューバーを追加します。

これで画面の上部にメニューバーが設置されます。




# menubarの設置 ver2.8
menubar = tk.Menu(root)
root.config(menu=menubar)



次にメニューバーのプルダウン項目を設定します。今回は、ファイルとヘルプという2つのカテゴリーのボタンを設定し、それらをクリックするとプルダウンメニューが表示され、それぞれインポート、エクスポート等の詳細項目が選択できるという設定です。




#########################################
# GUI メニューバー(menubar)の設定 --- (5-1-2)
#########################################
# ファイルメニューの設定置 ver2.8
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="インポート", command=lambda:import_todos())
file_menu.add_command(label="エクスポート", command=lambda:export_todos())
file_menu.add_separator()
file_menu.add_command(label="終了", command=root.quit)
menubar.add_cascade(label="ファイル", menu=file_menu)

# ヘルプメニューの設定
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label='操作方法', command=lambda:help_msg())

menubar.add_cascade(label="ヘルプ", menu=help_menu)




最後に今配置済みのボタンに該当する箇所を削除します。

以上で終了です。

 

プログラム修正後の結果

では、実際にプログラムを実行して試してみます。
メニューバーが追加されています。



思っていたよりも簡単に出来ました。

最後に


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

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

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

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






##########################################
# ToDoアプリGUI_ver2.8.py
# 2024/09/15
# -メニューバーを追加

##########################################

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

#########################################
# 変数等を宣言 --- (2)
#########################################
db_path = "ToDo_column4.sqlite3"
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,
                 status 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,
                 status
        ) values(?,?,?)'''
        for row in reader:
            cur.execute(query, ( row[1], row[2],row[3]))
    conn.commit()
    conn.close()
    # ツリービュー再表示
    view_todos(tree)

# 関数(データのエクスポート)  
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()

# 関数(テキストボックスの入力内容クリア)
def clear_text(tree):
    id_text.delete(0, tk.END)
    ToDo_text.delete(0, tk.END)
    date_text.delete(0, tk.END)
    combobox.selection_clear()#★
    combobox.set('')#★


# 関数(データベースから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):
    #テキストボックスクリア
    clear_text(tree)
    #ツリービュークリア
    tree.delete(*tree.get_children())
    #1行毎に色違いで表示
    i=0
    for row in get_todos():
        tree.insert(parent='', index='end', tags=i, iid=row ,values=(str(row[0]), str(row[1]) ,str(row[2]),str(row[3])))
        if i % 2 == 0:
            # tagが偶数(レコードは奇数)の場合のみ、背景色の設定
            tree.tag_configure(i,background="#e6e6fa")#'yellow'
        i+=1

# 関数(データベースにデータ追加)
def add_todo(ToDo, date,status, id):
    with sqlite3.connect(db_path) as conn:
      cur = conn.cursor()
      query = '''INSERT INTO myItem(
                 ToDo,
                 date,
                 status
                ) values(?,?,?)'''
      cur.execute(query,(ToDo,date,status))
      conn.commit()
      # ツリービュー再表示
      view_todos(tree)

# 関数(データベースのデータ削除)
def delete_todo(ToDo,date,status,id):
    with sqlite3.connect(db_path) as conn:
      cur = conn.cursor()
      query = 'DELETE FROM myItem WHERE id = ?'
      cur.execute(query,[id])
      conn.commit()
      # ツリービュー再表示    
      view_todos(tree)

# 関数(データベースのデータ更新)
def update_todo(ToDo,date,status,id):
    with sqlite3.connect(db_path) as conn:
      cur = conn.cursor()
      query = '''UPDATE myItem
                 SET ToDo = ?,
                     date = ?,
                     status = ?
                 WHERE id = ?'''
      cur.execute(query,(ToDo,date,status,id))
      conn.commit()
      # ツリービュー再表示
      view_todos(tree)
   
# 関数(データベースのデータ検索)
def get_search_todos(ToDo,date,status,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 status != "" :
        with sqlite3.connect(db_path) as conn:
            cur = conn.cursor()
            # プレースホルダ
            query = '''SELECT * FROM myItem
                       WHERE status
                       LIKE ? '''
            cur.execute(query,('%'+ status +'%',))
            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,status,id):
    # テキストボックスクリア
    clear_text(tree)
    # ツリービュークリア
    tree.delete(*tree.get_children())
    # レコードの検索結果をリストボックスに表示
    i=0
    for row in get_search_todos(ToDo,date,status,id):
        tree.insert(parent='', index='end', tags=i, iid=row ,values=(str(row[0]), str(row[1]) ,str(row[2]) ,str(row[3])))
        if i % 2 == 0:
           # tagが偶数(レコードは奇数)の場合のみ、背景色の設定
           tree.tag_configure(i,background="#e6e6fa")#'yellow'
        i+=1

# 関数(TREEVIEW上で選択された行から情報を取得)
def select_record(event):
    # 選択行の判別
    record_id = tree.focus()
    #テキストボックスクリア
    clear_text(tree)
    # 選択行のレコードを取得
    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] )
    combobox.set(record_values[3] )#★
    #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")

# menubarの設置 ver2.8
menubar = tk.Menu(root)
root.config(menu=menubar)

# フレームの配置
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 メニューバー(menubar)の設定 --- (5-1-2)
#########################################
# ファイルメニューの設定置 ver2.8
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="インポート", command=lambda:import_todos())
file_menu.add_command(label="エクスポート", command=lambda:export_todos())
file_menu.add_separator()
file_menu.add_command(label="終了", command=root.quit)
menubar.add_cascade(label="ファイル", menu=file_menu)

# ヘルプメニューの設定
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label='操作方法', command=lambda:help_msg())

menubar.add_cascade(label="ヘルプ", menu=help_menu)

#########################################
# GUI フレーム(view)の設定 --- (5-2)
#########################################
# ツリービューの列の識別名を指定
column = ('id', 'ToDo', 'date','status')
# ツリービュー初期化
tree = ttk.Treeview(view_frame, columns=column)
# ツリービューの列の設定
tree.column('#0',width=0, stretch='no')
tree.column('id', anchor='e', width=50) # 右よせ
tree.column('ToDo',anchor='w', width=300) # 左よせ
tree.column('date', anchor='e', width=80) # 右よせ
tree.column('status', anchor='w', 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.heading('status',text='状況', anchor='center')
# ツリービューの行をマウスで選択したときのイベント
tree.bind("<<TreeviewSelect>>", select_record)
# ウィジェットの配置
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=30)
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)

#コンボボックス(状況)の設定
add_lbl3 = tk.Label(text_frame,text="状況:")
add_lbl3.pack(side=tk.LEFT)

values = [" ", "完了", "進行中", "未着手"] # 表示する選択肢
combobox = ttk.Combobox(text_frame, values=values)
combobox.state(['readonly']) # 選択のみ可
combobox.config(width=10) # 表示幅
combobox.pack(side=tk.LEFT)

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

# ボタン(ToDoデータ追加)の設定
add_button = tk.Button(button_frame, text="追加", width=20, command=lambda: add_todo(ToDo_text.get(),date_text.get(),combobox.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(),combobox.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(),combobox.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(),combobox.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()





このブログを検索

アーカイブ

カテゴリー

QooQ