Pythonで作る!土日祝色分け対応スケジュール管理アプリ(Tkinter+SQLite)

プログラミング

こんにちは!
今回はPythonのGUIライブラリ「Tkinter」と軽量データベース「SQLite」を使って、土日は色分けしたスケジュール管理アプリを作る方法をご紹介します。


目次

  • アプリの概要
  • 使用ライブラリ
  • コード解説
  • 使い方
  • まとめ

アプリの概要

このアプリは、

  • 日付選択カレンダーから予定を追加・編集・削除できる
  • 土曜日は青色、日曜日は赤色、今日の日は緑色で色分け
  • 予定はSQLiteに保存して管理

といった機能を備えています。


使用ライブラリ

  • Python標準の tkinter(GUI構築用)
  • sqlite3(データ保存用)
  • datetimecalendar(日付操作用)

コード解説

以下に全コードを掲載しています。詳しい解説は後述します。

import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
from datetime import datetime, date
import calendar

DB_NAME = "schedule.db"
TIME_OPTIONS = [f"{h:02d}:{m:02d}" for h in range(24) for m in (0,30)]

def init_db():
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
title TEXT NOT NULL,
start TEXT,
end TEXT,
location TEXT,
notes TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
conn.close()

def get_events_for_date(d: date):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, title, start, end, location, notes FROM events WHERE date=? ORDER BY start", (d.isoformat(),))
rows = c.fetchall()
conn.close()
return rows

def add_event(d: date, title, start, end, location, notes):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("INSERT INTO events (date, title, start, end, location, notes) VALUES (?, ?, ?, ?, ?, ?)",
(d.isoformat(), title, start, end, location, notes))
conn.commit()
conn.close()

def update_event(event_id, title, start, end, location, notes):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("UPDATE events SET title=?, start=?, end=?, location=?, notes=? WHERE id=?",
(title, start, end, location, notes, event_id))
conn.commit()
conn.close()

def delete_event(event_id):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM events WHERE id=?", (event_id,))
conn.commit()
conn.close()

def search_events(keyword):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
kw = f"%{keyword}%"
c.execute("SELECT id, date, title, start, end, location FROM events WHERE title LIKE ? OR notes LIKE ? ORDER BY date, start", (kw, kw))
rows = c.fetchall()
conn.close()
return rows

class DatePicker(ttk.Frame):
def __init__(self, master, callback):
super().__init__(master)
self.callback = callback
self.now = date.today()
self.current_year = self.now.year
self.current_month = self.now.month

header = ttk.Frame(self)
header.pack(fill="x", pady=4)

self.prev_btn = ttk.Button(header, text="<", width=3, command=self.prev_month)
self.prev_btn.pack(side="left")
self.title_lbl = ttk.Label(header, text="", width=18, anchor="center")
self.title_lbl.pack(side="left", padx=6)
self.next_btn = ttk.Button(header, text=">", width=3, command=self.next_month)
self.next_btn.pack(side="left")

self.days_frame = ttk.Frame(self)
self.days_frame.pack()

self.build_calendar()

def build_calendar(self):
for w in self.days_frame.winfo_children():
w.destroy()

self.title_lbl.config(text=f"{self.current_year} / {self.current_month:02d}")

days = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]
for i, d in enumerate(days):
lbl = ttk.Label(self.days_frame, text=d, borderwidth=0, padding=2)
lbl.grid(row=0, column=i, padx=2, pady=1)

cal = calendar.Calendar(firstweekday=0)
month_days = cal.monthdayscalendar(self.current_year, self.current_month)

for r, week in enumerate(month_days, start=1):
for c, day in enumerate(week):
if day == 0:
btn = ttk.Label(self.days_frame, text="", width=4)
else:
style_name = "TButton"
if c == 5: # 土曜
style_name = "Saturday.TButton"
elif c == 6: # 日曜
style_name = "Sunday.TButton"

btn = ttk.Button(self.days_frame, text=str(day), width=4,
style=style_name,
command=lambda d=day: self.on_day_selected(d))
if (self.current_year, self.current_month, day) == (self.now.year, self.now.month, self.now.day):
btn.config(style="Today.TButton")
btn.grid(row=r, column=c, padx=2, pady=2)

def on_day_selected(self, day):
d = date(self.current_year, self.current_month, day)
self.callback(d)

def prev_month(self):
if self.current_month == 1:
self.current_month = 12
self.current_year -= 1
else:
self.current_month -= 1
self.build_calendar()

def next_month(self):
if self.current_month == 12:
self.current_month = 1
self.current_year += 1
else:
self.current_month += 1
self.build_calendar()

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("スケジュール管理アプリ")
self.geometry("900x520")
self.style = ttk.Style(self)
self.style.configure("Today.TButton", foreground="green")
self.style.configure("Saturday.TButton", foreground="blue")
self.style.configure("Sunday.TButton", foreground="red")

self.selected_date = date.today()

left = ttk.Frame(self, padding=8)
left.pack(side="left", fill="y")

self.datepicker = DatePicker(left, self.on_date_selected)
self.datepicker.pack()

add_quick = ttk.Button(left, text="今日に予定追加", command=lambda: self.open_add_dialog(self.selected_date))
add_quick.pack(pady=8, fill="x")

right = ttk.Frame(self, padding=8)
right.pack(side="left", fill="both", expand=True)

top_row = ttk.Frame(right)
top_row.pack(fill="x")
self.date_label = ttk.Label(top_row, text=self.selected_date.isoformat(), font=("Arial", 14))
self.date_label.pack(side="left")

search_frame = ttk.Frame(top_row)
search_frame.pack(side="right")
self.search_var = tk.StringVar()
search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=30)
search_entry.pack(side="left", padx=4)
search_entry.bind("<Return>", lambda e: self.on_search())
search_btn = ttk.Button(search_frame, text="検索", command=self.on_search)
search_btn.pack(side="left")

columns = ("id", "title", "start", "end", "location")
self.tree = ttk.Treeview(right, columns=columns, show="headings", selectmode="browse")
self.tree.heading("title", text="タイトル")
self.tree.heading("start", text="開始")
self.tree.heading("end", text="終了")
self.tree.heading("location", text="場所")
self.tree.column("id", width=0, stretch=False)
self.tree.pack(fill="both", expand=True, pady=8)
self.tree.bind("<Double-1>", self.on_edit_from_tree)

btns = ttk.Frame(right)
btns.pack(fill="x", pady=4)
ttk.Button(btns, text="追加", command=lambda: self.open_add_dialog(self.selected_date)).pack(side="left", padx=4)
ttk.Button(btns, text="編集", command=self.on_edit_selected).pack(side="left", padx=4)
ttk.Button(btns, text="削除", command=self.on_delete_selected).pack(side="left", padx=4)
ttk.Button(btns, text="今日へ", command=self.goto_today).pack(side="right", padx=4)

self.refresh_list()

def on_date_selected(self, d: date):
self.selected_date = d
self.date_label.config(text=self.selected_date.isoformat())
self.refresh_list()

def refresh_list(self):
for i in self.tree.get_children():
self.tree.delete(i)
rows = get_events_for_date(self.selected_date)
for r in rows:
event_id, title, start, end, location, notes = r
self.tree.insert("", "end", values=(event_id, title, start or "", end or "", location or ""))

def open_add_dialog(self, d: date):
dlg = EventDialog(self, "予定を追加", d)
self.wait_window(dlg)
if dlg.result:
title, start, end, location, notes = dlg.result
if not title.strip():
messagebox.showwarning("入力エラー", "タイトルは必須です。")
return
add_event(d, title, start, end, location, notes)
self.refresh_list()

def on_edit_from_tree(self, event):
self.on_edit_selected()

def on_edit_selected(self):
sel = self.tree.selection()
if not sel:
messagebox.showinfo("情報", "編集する予定を選んでください。")
return
item = self.tree.item(sel)
event_id = item["values"][0]
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT date, title, start, end, location, notes FROM events WHERE id=?", (event_id,))
row = c.fetchone()
conn.close()
if not row:
messagebox.showerror("エラー", "予定が見つかりませんでした。")
return
d_str, title, start, end, location, notes = row
d = datetime.fromisoformat(d_str).date()
dlg = EventDialog(self, "予定を編集", d, (event_id, title, start, end, location, notes))
self.wait_window(dlg)
if dlg.result:
title, start, end, location, notes = dlg.result
update_event(event_id, title, start, end, location, notes)
self.refresh_list()

def on_delete_selected(self):
sel = self.tree.selection()
if not sel:
messagebox.showinfo("情報", "削除する予定を選んでください。")
return
item = self.tree.item(sel)
event_id = item["values"][0]
if messagebox.askyesno("削除確認", "選択した予定を削除しますか?"):
delete_event(event_id)
self.refresh_list()

def on_search(self):
kw = self.search_var.get().strip()
if not kw:
messagebox.showinfo("検索", "検索キーワードを入力してください。")
return
rows = search_events(kw)
win = tk.Toplevel(self)
win.title(f"検索結果: {kw}")
tree = ttk.Treeview(win, columns=("id","date","title","start","end","location"), show="headings")
tree.heading("date", text="日付")
tree.heading("title", text="タイトル")
tree.heading("start", text="開始")
tree.heading("end", text="終了")
tree.heading("location", text="場所")
tree.pack(fill="both", expand=True)
for r in rows:
event_id, d_str, title, start, end, location = r
tree.insert("", "end", values=(event_id, d_str[:10], title, start or "", end or "", location or ""))

def goto_today(self):
self.datepicker.current_year = date.today().year
self.datepicker.current_month = date.today().month
self.datepicker.build_calendar()
self.on_date_selected(date.today())

class EventDialog(tk.Toplevel):
def __init__(self, parent, title, d: date, edit_data=None):
super().__init__(parent)
self.transient(parent)
self.title(title)
self.parent = parent
self.result = None
self.date = d

frm = ttk.Frame(self, padding=12)
frm.pack(fill="both", expand=True)

ttk.Label(frm, text=f"日付: {self.date.isoformat()}").grid(row=0, column=0, columnspan=2, sticky="w", pady=2)

ttk.Label(frm, text="タイトル *").grid(row=1, column=0, sticky="e", pady=2)
self.title_var = tk.StringVar(value=(edit_data[1] if edit_data else ""))
ttk.Entry(frm, textvariable=self.title_var, width=40).grid(row=1, column=1, sticky="w", pady=2)

ttk.Label(frm, text="開始 (HH:MM)").grid(row=2, column=0, sticky="e", pady=2)
self.start_var = tk.StringVar(value=(edit_data[2] if edit_data else ""))
self.start_combo = ttk.Combobox(frm, textvariable=self.start_var, values=TIME_OPTIONS, width=18)
self.start_combo.grid(row=2, column=1, sticky="w", pady=2)

ttk.Label(frm, text="終了 (HH:MM)").grid(row=3, column=0, sticky="e", pady=2)
self.end_var = tk.StringVar(value=(edit_data[3] if edit_data else ""))
self.end_combo = ttk.Combobox(frm, textvariable=self.end_var, values=TIME_OPTIONS, width=18)
self.end_combo.grid(row=3, column=1, sticky="w", pady=2)

ttk.Label(frm, text="場所").grid(row=4, column=0, sticky="e", pady=2)
self.loc_var = tk.StringVar(value=(edit_data[4] if edit_data else ""))
ttk.Entry(frm, textvariable=self.loc_var, width=40).grid(row=4, column=1, sticky="w", pady=2)

ttk.Label(frm, text="メモ").grid(row=5, column=0, sticky="ne", pady=2)
self.notes_txt = tk.Text(frm, width=40, height=6)
self.notes_txt.grid(row=5, column=1, sticky="w", pady=2)
if edit_data and edit_data[5]:
self.notes_txt.insert("1.0", edit_data[5])

btns = ttk.Frame(frm)
btns.grid(row=6, column=0, columnspan=2, pady=8)
ttk.Button(btns, text="保存", command=self.on_ok).pack(side="left", padx=6)
ttk.Button(btns, text="キャンセル", command=self.on_cancel).pack(side="left")

self.grab_set()
self.protocol("WM_DELETE_WINDOW", self.on_cancel)
self.resizable(False, False)
self.wait_visibility()
self.focus_set()

def on_ok(self):
title = self.title_var.get().strip()
start = self.start_var.get().strip()
end = self.end_var.get().strip()
location = self.loc_var.get().strip()
notes = self.notes_txt.get("1.0", "end").strip()
for t in (start, end):
if t:
try:
datetime.strptime(t, "%H:%M")
except ValueError:
messagebox.showwarning("入力エラー", f"時刻の形式はHH:MMです。入力値: {t}")
return
self.result = (title, start, end, location, notes)
self.destroy()

def on_cancel(self):
self.result = None
self.destroy()

if __name__ == "__main__":
init_db()
app = App()
app.mainloop()

主なポイント

  • DatePickerクラス
    月カレンダーを表示し、土曜・日曜ボタンは色分けスタイルを設定しています。
    今日の日付ボタンは別スタイルで緑色に表示。
  • Appクラス
    メインアプリケーション。画面を左右に分けてカレンダー表示と予定リスト・検索機能を配置。
    予定の追加・編集はダイアログを利用。
  • SQLite連携
    予定データはSQLiteのeventsテーブルに保存。タイトル・開始時間・終了時間・場所・メモを管理。

使い方

  1. アプリを起動するとカレンダーが表示されます。
  2. 日付をクリックすると右側にその日の予定一覧が表示されます。
  3. 「追加」ボタンまたは「今日に予定追加」ボタンで予定を追加可能。
  4. 予定を選択して「編集」またはダブルクリックで編集、削除ボタンで削除できます。
  5. 土日は曜日ごとに色が違うので見やすい!

まとめ

今回はPythonのTkinterでカレンダー+予定管理アプリを作り、土日色分けの見た目を改善しました。
SQLiteで保存するので予定データも永続化。簡単にカスタマイズもできるのでぜひチャレンジしてみてください!

タイトルとURLをコピーしました