プロジェクト管理ツール
タスク・進捗ステータス・ガントチャート表示ができるプロジェクト管理ツール。SQLiteとCanvas描画を学びます。
1. アプリ概要
タスク・進捗ステータス・ガントチャート表示ができるプロジェクト管理ツール。SQLiteとCanvas描画を学びます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★★ です。
このアプリは「データベース連携」カテゴリです。データベース連携 GUI は永続化と画面更新を結びつける典型例で、トランザクション・スキーマ設計・UI 反映の三点を一度に学べます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。
コピー&実行で動作確認 → コード解説で構造理解 → カスタマイズで応用、の 3 ステップで進めると、短時間で本質を押さえられます。
アプリを完成させた後は、自分の使い方に合わせて改造するのが学びを定着させる近道です。カスタマイズ章のアイデアを足がかりに、独自機能を一つ追加してみてください。
2. 機能一覧
- 「➕ 新規プロジェクト」ボタン(
_new_project())・「✏ プロジェクト編集」ボタン(_edit_project())・「🗑 削除」ボタン(_delete_project())・「➕ タスク追加」ボタン(_add_task())・「✏ 編集」ボタン(_edit_task())・「🗑 削除」ボタン(_delete_task())・「🔄 更新」ボタン(_refresh_tasks())・「🔄 ガント更新」ボタン(_draw_gantt())・「🔄 ダッシュボード更新」ボタン(_refresh_dashboard())・「保存」ボタン・「保存」ボタンで操作 - イベント
<<ComboboxSelected>>から_refresh_tasks()を実行 - マウス操作
<Double-1>から_edit_task()を実行 - イベント
<<ComboboxSelected>>から_draw_gantt()を実行 ttk.Treeviewによる表形式の一覧表示sqlite3.connect()による SQLite データベース接続と SQL 実行messagebox.askyesno()・messagebox.showwarning()によるダイアログ通知- ウィンドウはタイトル「プロジェクト管理ツール」・サイズ 1060x700 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は標準ライブラリの範囲で動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
import os
from datetime import datetime, date, timedelta
class App43:
"""プロジェクト管理ツール(ガントチャート付き)"""
DB_PATH = os.path.join(os.path.dirname(__file__), "projects.db")
STATUS_COLORS = {
"未着手": "#9e9e9e",
"進行中": "#2196f3",
"完了": "#4caf50",
"保留": "#ff9800",
"キャンセル": "#f44336",
}
def __init__(self, root):
self.root = root
self.root.title("プロジェクト管理ツール")
self.root.geometry("1060x700")
self.root.configure(bg="#f8f9fc")
self._init_db()
self._build_ui()
self._refresh_all()
def _init_db(self):
self.conn = sqlite3.connect(self.DB_PATH)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
start_date TEXT,
end_date TEXT,
status TEXT DEFAULT '進行中',
color TEXT DEFAULT '#2196f3'
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER,
name TEXT NOT NULL,
assignee TEXT,
start_date TEXT,
end_date TEXT,
status TEXT DEFAULT '未着手',
priority TEXT DEFAULT '中',
progress INTEGER DEFAULT 0,
notes TEXT,
FOREIGN KEY(project_id) REFERENCES projects(id)
)
""")
self.conn.commit()
if not self.conn.execute("SELECT 1 FROM projects").fetchone():
self._insert_sample_data()
def _insert_sample_data(self):
today = date.today()
p_id = self.conn.execute(
"INSERT INTO projects (name, description, start_date, end_date, status, color) "
"VALUES (?,?,?,?,?,?)",
("Webサイトリニューアル", "コーポレートサイトのリニューアル",
today.isoformat(),
(today + timedelta(days=60)).isoformat(),
"進行中", "#2196f3")).lastrowid
self.conn.commit()
tasks = [
("要件定義", "山田", today.isoformat(),
(today + timedelta(days=7)).isoformat(), "完了", "高", 100),
("デザイン", "鈴木", (today + timedelta(days=8)).isoformat(),
(today + timedelta(days=21)).isoformat(), "進行中", "高", 60),
("開発", "田中", (today + timedelta(days=22)).isoformat(),
(today + timedelta(days=50)).isoformat(), "未着手", "中", 0),
("テスト", "佐藤", (today + timedelta(days=51)).isoformat(),
(today + timedelta(days=58)).isoformat(), "未着手", "中", 0),
("リリース", "山田", (today + timedelta(days=59)).isoformat(),
(today + timedelta(days=60)).isoformat(), "未着手", "低", 0),
]
for task in tasks:
self.conn.execute(
"INSERT INTO tasks (project_id, name, assignee, start_date, "
"end_date, status, priority, progress) VALUES (?,?,?,?,?,?,?,?)",
(p_id,) + task)
self.conn.commit()
def _build_ui(self):
# ヘッダー
header = tk.Frame(self.root, bg="#1565c0", pady=8)
header.pack(fill=tk.X)
tk.Label(header, text="📋 プロジェクト管理ツール",
font=("Noto Sans JP", 13, "bold"),
bg="#1565c0", fg="white").pack(side=tk.LEFT, padx=12)
ttk.Button(header, text="➕ 新規プロジェクト",
command=self._new_project).pack(side=tk.RIGHT, padx=8)
notebook = ttk.Notebook(self.root)
notebook.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
# タスク一覧タブ
task_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(task_tab, text="📋 タスク一覧")
self._build_task_tab(task_tab)
# ガントチャートタブ
gantt_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(gantt_tab, text="📅 ガントチャート")
self._build_gantt_tab(gantt_tab)
# ダッシュボードタブ
dash_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(dash_tab, text="📊 ダッシュボード")
self._build_dashboard_tab(dash_tab)
self.status_var = tk.StringVar(value="プロジェクト管理ツール")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
# ── タスク一覧タブ ────────────────────────────────────────────
def _build_task_tab(self, parent):
# プロジェクト選択
proj_f = tk.Frame(parent, bg="#f8f9fc")
proj_f.pack(fill=tk.X, padx=8, pady=6)
tk.Label(proj_f, text="プロジェクト:", bg="#f8f9fc",
font=("Arial", 10)).pack(side=tk.LEFT)
self.sel_proj_var = tk.StringVar()
self.proj_cb = ttk.Combobox(proj_f, textvariable=self.sel_proj_var,
state="readonly", width=22)
self.proj_cb.pack(side=tk.LEFT, padx=6)
self.proj_cb.bind("<<ComboboxSelected>>",
lambda e: self._refresh_tasks())
ttk.Button(proj_f, text="✏ プロジェクト編集",
command=self._edit_project).pack(side=tk.LEFT, padx=4)
ttk.Button(proj_f, text="🗑 削除",
command=self._delete_project).pack(side=tk.LEFT, padx=4)
# タスクツリー
cols = ("name", "assignee", "start", "end", "status",
"priority", "progress")
self.task_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=16)
for c, h, w in [("name", "タスク名", 180), ("assignee", "担当者", 80),
("start", "開始日", 90), ("end", "終了日", 90),
("status", "状態", 80), ("priority", "優先度", 70),
("progress", "進捗%", 60)]:
self.task_tree.heading(c, text=h)
self.task_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.task_tree.yview)
self.task_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.task_tree.pack(fill=tk.BOTH, expand=True, padx=8)
self.task_tree.bind("<Double-1>", self._edit_task)
for status, color in self.STATUS_COLORS.items():
self.task_tree.tag_configure(status, foreground=color)
btn_f = tk.Frame(parent, bg="#f8f9fc")
btn_f.pack(fill=tk.X, padx=8, pady=4)
ttk.Button(btn_f, text="➕ タスク追加",
command=self._add_task).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="✏ 編集",
command=self._edit_task).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_task).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🔄 更新",
command=self._refresh_tasks).pack(side=tk.LEFT, padx=4)
def _refresh_tasks(self):
proj_name = self.sel_proj_var.get()
if not proj_name:
return
proj_row = self.conn.execute(
"SELECT id FROM projects WHERE name=?", (proj_name,)).fetchone()
if not proj_row:
return
proj_id = proj_row[0]
tasks = self.conn.execute(
"SELECT id, name, assignee, start_date, end_date, status, priority, progress "
"FROM tasks WHERE project_id=? ORDER BY start_date", (proj_id,)).fetchall()
self.task_tree.delete(*self.task_tree.get_children())
for row in tasks:
tid, name, assignee, start, end, status, priority, progress = row
self.task_tree.insert("", "end", iid=str(tid),
values=(name, assignee or "", start or "", end or "",
status, priority, f"{progress}%"),
tags=(status,))
# ── ガントチャートタブ ────────────────────────────────────────
def _build_gantt_tab(self, parent):
ctrl_f = tk.Frame(parent, bg="#f8f9fc")
ctrl_f.pack(fill=tk.X, padx=8, pady=6)
tk.Label(ctrl_f, text="プロジェクト:", bg="#f8f9fc",
font=("Arial", 10)).pack(side=tk.LEFT)
self.gantt_proj_var = tk.StringVar()
self.gantt_proj_cb = ttk.Combobox(ctrl_f, textvariable=self.gantt_proj_var,
state="readonly", width=22)
self.gantt_proj_cb.pack(side=tk.LEFT, padx=6)
self.gantt_proj_cb.bind("<<ComboboxSelected>>",
lambda e: self._draw_gantt())
ttk.Button(ctrl_f, text="🔄 ガント更新",
command=self._draw_gantt).pack(side=tk.LEFT, padx=4)
# キャンバス
self.gantt_canvas = tk.Canvas(parent, bg="white", highlightthickness=0)
h_sb = ttk.Scrollbar(parent, orient=tk.HORIZONTAL,
command=self.gantt_canvas.xview)
v_sb = ttk.Scrollbar(parent, orient=tk.VERTICAL,
command=self.gantt_canvas.yview)
self.gantt_canvas.configure(xscrollcommand=h_sb.set,
yscrollcommand=v_sb.set)
v_sb.pack(side=tk.RIGHT, fill=tk.Y)
h_sb.pack(side=tk.BOTTOM, fill=tk.X)
self.gantt_canvas.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
def _draw_gantt(self):
proj_name = self.gantt_proj_var.get()
if not proj_name:
return
proj_row = self.conn.execute(
"SELECT id, start_date, end_date FROM projects WHERE name=?",
(proj_name,)).fetchone()
if not proj_row:
return
proj_id, proj_start, proj_end = proj_row
tasks = self.conn.execute(
"SELECT name, assignee, start_date, end_date, status, progress "
"FROM tasks WHERE project_id=? ORDER BY start_date", (proj_id,)).fetchall()
if not tasks or not proj_start:
return
self.gantt_canvas.delete("all")
try:
start_date = datetime.fromisoformat(proj_start).date()
end_date = datetime.fromisoformat(proj_end).date()
except Exception:
return
total_days = (end_date - start_date).days + 1
LABEL_W = 150
DAY_W = 18
ROW_H = 32
HEADER_H = 50
today = date.today()
canvas_w = LABEL_W + total_days * DAY_W + 20
canvas_h = HEADER_H + len(tasks) * ROW_H + 20
self.gantt_canvas.configure(scrollregion=(0, 0, canvas_w, canvas_h))
# ヘッダー: 月/日
for d_offset in range(total_days):
d = start_date + timedelta(days=d_offset)
x = LABEL_W + d_offset * DAY_W
if d.day == 1 or d_offset == 0:
self.gantt_canvas.create_text(
x + 4, 10, text=f"{d.month}月", anchor="nw",
font=("Arial", 8, "bold"), fill="#333")
if d == today:
self.gantt_canvas.create_rectangle(
x, 0, x + DAY_W, canvas_h,
fill="#e3f2fd", outline="", stipple="gray25")
self.gantt_canvas.create_text(
x + DAY_W // 2, HEADER_H - 14,
text=str(d.day), font=("Arial", 7),
fill="#1976d2" if d.weekday() >= 5 else "#555")
self.gantt_canvas.create_line(
x, HEADER_H, x, canvas_h, fill="#e0e0e0")
self.gantt_canvas.create_line(
0, HEADER_H, canvas_w, HEADER_H, fill="#bbb", width=2)
# 今日ライン
if start_date <= today <= end_date:
today_x = LABEL_W + (today - start_date).days * DAY_W
self.gantt_canvas.create_line(
today_x, 0, today_x, canvas_h,
fill="#f44336", width=2, dash=(4, 2))
self.gantt_canvas.create_text(
today_x, 5, text="今日", fill="#f44336",
font=("Arial", 7, "bold"), anchor="n")
# タスクバー
for row_idx, (name, assignee, t_start, t_end, status, progress) in enumerate(tasks):
y = HEADER_H + row_idx * ROW_H
y_mid = y + ROW_H // 2
# ラベル
self.gantt_canvas.create_rectangle(
0, y, LABEL_W, y + ROW_H, fill="#f5f5f5", outline="#e0e0e0")
self.gantt_canvas.create_text(
8, y_mid, text=f"{name[:14]}", anchor="w",
font=("Arial", 9), fill="#333")
if assignee:
self.gantt_canvas.create_text(
8, y_mid + 11, text=f" {assignee}",
anchor="w", font=("Arial", 7), fill="#888")
if not t_start or not t_end:
continue
try:
task_start = datetime.fromisoformat(t_start).date()
task_end = datetime.fromisoformat(t_end).date()
except Exception:
continue
# バーの位置
bar_x1 = LABEL_W + max(0, (task_start - start_date).days) * DAY_W
bar_x2 = LABEL_W + min(total_days, (task_end - start_date).days + 1) * DAY_W
bar_y1 = y + 6
bar_y2 = y + ROW_H - 6
color = self.STATUS_COLORS.get(status, "#9e9e9e")
# 背景バー
self.gantt_canvas.create_rectangle(
bar_x1, bar_y1, bar_x2, bar_y2,
fill="#e0e0e0", outline="")
# 進捗バー
prog_w = int((bar_x2 - bar_x1) * progress / 100)
if prog_w > 0:
self.gantt_canvas.create_rectangle(
bar_x1, bar_y1, bar_x1 + prog_w, bar_y2,
fill=color, outline="")
self.gantt_canvas.create_rectangle(
bar_x1, bar_y1, bar_x2, bar_y2,
fill="", outline=color)
# 進捗%テキスト
if bar_x2 - bar_x1 > 30:
self.gantt_canvas.create_text(
(bar_x1 + bar_x2) // 2, (bar_y1 + bar_y2) // 2,
text=f"{progress}%", font=("Arial", 8), fill="white" if progress > 40 else "#333")
# ── ダッシュボードタブ ────────────────────────────────────────
def _build_dashboard_tab(self, parent):
self.dash_canvas = tk.Canvas(parent, bg="#f8f9fc", highlightthickness=0)
self.dash_canvas.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
ttk.Button(parent, text="🔄 ダッシュボード更新",
command=self._refresh_dashboard).pack(pady=4)
def _refresh_dashboard(self):
self.dash_canvas.delete("all")
projects = self.conn.execute(
"SELECT id, name, status, start_date, end_date FROM projects").fetchall()
if not projects:
self.dash_canvas.create_text(300, 200, text="プロジェクトがありません",
font=("Arial", 14), fill="#999")
return
x, y = 20, 20
for proj_id, name, status, start, end in projects:
tasks = self.conn.execute(
"SELECT status, COUNT(*) FROM tasks WHERE project_id=? "
"GROUP BY status", (proj_id,)).fetchall()
total = sum(c for _, c in tasks)
done = next((c for s, c in tasks if s == "完了"), 0)
pct = int(done / total * 100) if total else 0
color = self.STATUS_COLORS.get(status, "#9e9e9e")
# プロジェクトカード
self.dash_canvas.create_rectangle(
x, y, x + 280, y + 100, fill="white", outline="#ddd")
self.dash_canvas.create_rectangle(
x, y, x + 6, y + 100, fill=color, outline="")
self.dash_canvas.create_text(
x + 16, y + 16, text=name, anchor="nw",
font=("Arial", 12, "bold"), fill="#333")
self.dash_canvas.create_text(
x + 16, y + 36, text=f"期間: {start or '?'} 〜 {end or '?'}",
anchor="nw", font=("Arial", 9), fill="#666")
self.dash_canvas.create_text(
x + 16, y + 54, text=f"タスク: {done}/{total} 完了",
anchor="nw", font=("Arial", 9), fill="#666")
# プログレスバー
self.dash_canvas.create_rectangle(
x + 16, y + 72, x + 260, y + 86,
fill="#e0e0e0", outline="")
prog_w = int(244 * pct / 100)
if prog_w > 0:
self.dash_canvas.create_rectangle(
x + 16, y + 72, x + 16 + prog_w, y + 86,
fill=color, outline="")
self.dash_canvas.create_text(
x + 265, y + 79, text=f"{pct}%",
anchor="w", font=("Arial", 9, "bold"), fill=color)
x += 300
if x > 600:
x = 20
y += 120
# ── ダイアログ ────────────────────────────────────────────────
def _new_project(self):
self._project_dialog(None)
def _edit_project(self):
proj_name = self.sel_proj_var.get()
if proj_name:
row = self.conn.execute(
"SELECT id FROM projects WHERE name=?", (proj_name,)).fetchone()
if row:
self._project_dialog(row[0])
def _project_dialog(self, proj_id):
win = tk.Toplevel(self.root)
win.title("プロジェクト" + ("編集" if proj_id else "追加"))
win.geometry("380x280")
lbl_s = {"bg": "#f8f9fc", "font": ("Arial", 10)}
fields = {}
defaults = {}
if proj_id:
row = self.conn.execute(
"SELECT name, description, start_date, end_date, status, color "
"FROM projects WHERE id=?", (proj_id,)).fetchone()
if row:
defaults = dict(zip(
["name", "description", "start_date", "end_date", "status", "color"],
row))
today = date.today().isoformat()
for lbl, key, default in [
("名前:", "name", ""),
("説明:", "description", ""),
("開始日 (YYYY-MM-DD):", "start_date", today),
("終了日 (YYYY-MM-DD):", "end_date",
(date.today() + timedelta(days=30)).isoformat()),
("状態:", "status", "進行中"),
]:
row = tk.Frame(win, bg="#f8f9fc")
row.pack(fill=tk.X, padx=8, pady=3)
tk.Label(row, text=lbl, **lbl_s).pack(anchor="w")
var = tk.StringVar(value=defaults.get(key, default))
if key == "status":
cb = ttk.Combobox(row, textvariable=var,
values=list(self.STATUS_COLORS.keys()),
state="readonly", width=14)
cb.pack(anchor="w")
else:
ttk.Entry(row, textvariable=var, width=32).pack(anchor="w")
fields[key] = var
def save():
data = {k: v.get().strip() for k, v in fields.items()}
if not data["name"]:
return
if proj_id:
self.conn.execute(
"UPDATE projects SET name=?,description=?,start_date=?,"
"end_date=?,status=? WHERE id=?",
(data["name"], data["description"], data["start_date"],
data["end_date"], data["status"], proj_id))
else:
self.conn.execute(
"INSERT INTO projects (name,description,start_date,end_date,status) "
"VALUES (?,?,?,?,?)",
(data["name"], data["description"], data["start_date"],
data["end_date"], data["status"]))
self.conn.commit()
self._refresh_all()
win.destroy()
ttk.Button(win, text="保存", command=save).pack(pady=8)
def _add_task(self):
proj_name = self.sel_proj_var.get()
if not proj_name:
messagebox.showwarning("警告", "プロジェクトを選択してください")
return
row = self.conn.execute("SELECT id FROM projects WHERE name=?",
(proj_name,)).fetchone()
if row:
self._task_dialog(row[0], None)
def _edit_task(self, event=None):
sel = self.task_tree.selection()
if not sel:
return
proj_name = self.sel_proj_var.get()
proj_row = self.conn.execute("SELECT id FROM projects WHERE name=?",
(proj_name,)).fetchone()
if proj_row:
self._task_dialog(proj_row[0], int(sel[0]))
def _task_dialog(self, proj_id, task_id):
win = tk.Toplevel(self.root)
win.title("タスク" + ("編集" if task_id else "追加"))
win.geometry("380x380")
fields = {}
defaults = {}
if task_id:
row = self.conn.execute(
"SELECT name, assignee, start_date, end_date, status, priority, progress, notes "
"FROM tasks WHERE id=?", (task_id,)).fetchone()
if row:
defaults = dict(zip(
["name", "assignee", "start_date", "end_date",
"status", "priority", "progress", "notes"], row))
today = date.today().isoformat()
for lbl, key, default, choices in [
("タスク名:", "name", "", None),
("担当者:", "assignee", "", None),
("開始日:", "start_date", today, None),
("終了日:", "end_date", (date.today() + timedelta(days=7)).isoformat(), None),
("状態:", "status", "未着手", list(self.STATUS_COLORS.keys())),
("優先度:", "priority", "中", ["高", "中", "低"]),
("進捗(%):", "progress", "0", None),
]:
row = tk.Frame(win, bg="#f8f9fc")
row.pack(fill=tk.X, padx=8, pady=2)
tk.Label(row, text=lbl, bg="#f8f9fc", width=12, anchor="e").pack(side=tk.LEFT)
var = tk.StringVar(value=str(defaults.get(key, default)))
if choices:
ttk.Combobox(row, textvariable=var, values=choices,
state="readonly", width=12).pack(side=tk.LEFT, padx=4)
else:
ttk.Entry(row, textvariable=var, width=20).pack(side=tk.LEFT, padx=4)
fields[key] = var
def save():
data = {k: v.get().strip() for k, v in fields.items()}
if not data["name"]:
return
try:
progress = min(100, max(0, int(data.get("progress", 0))))
except ValueError:
progress = 0
if task_id:
self.conn.execute(
"UPDATE tasks SET name=?,assignee=?,start_date=?,end_date=?,"
"status=?,priority=?,progress=? WHERE id=?",
(data["name"], data["assignee"], data["start_date"],
data["end_date"], data["status"], data["priority"],
progress, task_id))
else:
self.conn.execute(
"INSERT INTO tasks (project_id,name,assignee,start_date,"
"end_date,status,priority,progress) VALUES (?,?,?,?,?,?,?,?)",
(proj_id, data["name"], data["assignee"], data["start_date"],
data["end_date"], data["status"], data["priority"], progress))
self.conn.commit()
self._refresh_tasks()
self._draw_gantt()
win.destroy()
ttk.Button(win, text="保存", command=save).pack(pady=8)
def _delete_project(self):
proj_name = self.sel_proj_var.get()
if not proj_name:
return
if messagebox.askyesno("確認", f"プロジェクト '{proj_name}' とそのタスクをすべて削除しますか?"):
row = self.conn.execute("SELECT id FROM projects WHERE name=?",
(proj_name,)).fetchone()
if row:
self.conn.execute("DELETE FROM tasks WHERE project_id=?", (row[0],))
self.conn.execute("DELETE FROM projects WHERE id=?", (row[0],))
self.conn.commit()
self._refresh_all()
def _delete_task(self):
sel = self.task_tree.selection()
if sel and messagebox.askyesno("確認", "タスクを削除しますか?"):
self.conn.execute("DELETE FROM tasks WHERE id=?", (int(sel[0]),))
self.conn.commit()
self._refresh_tasks()
self._draw_gantt()
def _refresh_all(self):
projects = self.conn.execute(
"SELECT name FROM projects ORDER BY id").fetchall()
names = [p[0] for p in projects]
self.proj_cb.configure(values=names)
self.gantt_proj_cb.configure(values=names)
if names:
if not self.sel_proj_var.get() or self.sel_proj_var.get() not in names:
self.sel_proj_var.set(names[0])
self.gantt_proj_var.set(names[0])
self._refresh_tasks()
self._draw_gantt()
self._refresh_dashboard()
n = len(names)
self.status_var.set(f"{n} プロジェクト")
if __name__ == "__main__":
root = tk.Tk()
app = App43(root)
root.mainloop()
5. コード解説
プロジェクト管理ツール(ガントチャート付き)のコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App43 クラスにアプリの全機能をまとめています(メソッド19個・全595行)。__init__ ではタイトル「プロジェクト管理ツール」とウィンドウサイズ 1060x700 を設定します。そのあと _init_db() → _build_ui() → _refresh_all() の順に呼び出して初期状態を作ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×7(ほかにループでも生成)・tk.Label×4(ほかにループでも生成)・ttk.Button×11・ttk.Notebook・ttk.Combobox×2(ほかにループでも生成)・ttk.Treeview・ttk.Scrollbar×3・tk.Canvas×2・tk.Toplevel×2・ttk.Entry(ループで生成) で構成しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「➕ 新規プロジェクト」ボタン(_new_project())・「✏ プロジェクト編集」ボタン(_edit_project())・「🗑 削除」ボタン(_delete_project())・「➕ タスク追加」ボタン(_add_task())・「✏ 編集」ボタン(_edit_task())・「🗑 削除」ボタン(_delete_task())・「🔄 更新」ボタン(_refresh_tasks())・「🔄 ガント更新」ボタン(_draw_gantt())・「🔄 ダッシュボード更新」ボタン(_refresh_dashboard())・「保存」ボタン・「保存」ボタンを command= で接続しています。また bind("<<ComboboxSelected>>", ...) から _refresh_tasks()、bind("<Double-1>", ...) から _edit_task()、bind("<<ComboboxSelected>>", ...) から _draw_gantt() を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
SQLite データベースの接続と実行
sqlite3.connect() でデータベースファイルに接続し、SQL の実行は execute()、確定は commit() で行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
クラス定数によるデータ定義
クラスの先頭でデータを定数として定義しています:DB_PATH = os.path.join(os.path.dirname(__file__), 'projects.db')。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception・ValueError を捕捉しています。あわせて messagebox.askyesno()・messagebox.showwarning() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app43.py と保存します。使うのは
datetime・os・sqlite3・tkinterだけなので、追加インストールは不要です。 -
2クラスの骨格を作る
App43クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("プロジェクト管理ツール")・geometry("1060x700")・configure(bg="#f8f9fc")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×7(ほかにループでも生成)・tk.Label×4(ほかにループでも生成)・ttk.Button×11・ttk.Notebook・ttk.Combobox×2(ほかにループでも生成)・ttk.Treeview・ttk.Scrollbar×3・tk.Canvas×2・tk.Toplevel×2・ttk.Entry(ループで生成) を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_new_project()・_edit_project()・_delete_project()・_add_task()・_edit_task()・_delete_task()・_refresh_tasks()・_draw_gantt()・_refresh_dashboard()・save・save)につなぎます。bind("<<ComboboxSelected>>", ...)・bind("<Double-1>", ...)・bind("<<ComboboxSelected>>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_draw_gantt()(115行)・_refresh_dashboard()(50行)・_refresh_tasks()(19行)など を実装します。SQL の実行結果はexecute()の戻り値から取り出します。 -
7動作確認する
python app43.pyで起動し、各ボタンが反応すること・データベースファイルを開いて内容が表示されること・マウス操作(クリック・ホイールなど)が効くことを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 クラス定数を書き換えて動きを変える
データはクラス定数(DB_PATH・STATUS_COLORS)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。
💡 配色をまとめて管理する
背景色 #f8f9fc をはじめ、色コードが各所に直書きされています。色を1つの辞書にまとめると、テーマの切り替えが1か所の変更で済むようになります。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ 「database is locked」というエラーが出る
原因:同じデータベースファイルを別のツール(DB Browser など)で開いたまま書き込むと、SQLite がファイルをロックしてこのエラーになります。
解決法:他のツールでの接続を閉じてから実行してください。書き込みは commit() で確定するまで反映されない点にも注意しましょう。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("1060x700") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。
-
課題1:遅れているタスクを赤字にする
終了日を過ぎているのに「完了」になっていないタスクを、タスク一覧で赤字表示にします。あわせて遅延件数をステータスバーに出してください。
期待結果:サンプルデータの「デザイン」の終了日を昨日に変えて「🔄 更新」を押すと、その行だけが赤字になり、ステータスバーが「遅延 1 件」になる。
合格条件
- 状態が「完了」のタスクは、終了日を過ぎても色が変わらない
- 終了日が空のタスクがあっても落ちない
- 遅延が0件のときは「遅延 0 件」と表示される
ヒント①(どこを触るか): 色分けは
_refresh_tasks()のtag_configureとinsert(..., tags=(status,))の組み合わせです。新しいタグ名(例overdue)を先にtag_configureで定義してから使います。
ヒント②(使うもの): 日付の比較はdatetime.fromisoformat(end).date() < date.today()(どちらもファイル先頭で import 済み)。起動直後は_refresh_all()が最後にステータスバーを「N プロジェクト」で上書きするので、確認は「🔄 更新」を押してから行ってください。
つまずきやすい点: 終了日は"2026-09-08"のような文字列で保存されているので、date.today()とそのまま比べるとTypeErrorになります。datetime.fromisoformat(end).date()で日付に直してから比較してください。終了日を空欄で保存したタスクはNULLではなく空文字""で入り、fromisoformat("")はValueErrorになるので、先にif not end: continueで弾くか try-except で囲みます。 -
課題2:担当者別の負荷をダッシュボードに出す
ダッシュボードのタブに「担当者ごとのタスク数と平均進捗」を追加します。
期待結果:サンプルデータのまま「🔄 ダッシュボード更新」を押すと、山田 2件・平均 50% / 鈴木 1件・60% / 田中 1件・0% / 佐藤 1件・0% が並ぶ。
合格条件
- 担当者を空欄で登録したタスクが「未割当」としてまとまる
- タスクを追加・削除して更新すると件数と平均が追従する
- 平均進捗が「50%」のように整数で表示される(50.0% にしない)
ヒント①(どこを触るか): 描画は
_refresh_dashboard()がself.dash_canvasに矩形とテキストを置いています。プロジェクトカードを描いたあとのyを進めて、その下に描き足します。
ヒント②(使うもの): 集計は SQL 1文で書けます:SELECT assignee, COUNT(*), AVG(progress) FROM tasks GROUP BY assignee。AVG()は小数を返すのでf"{avg:.0f}%"で丸めます。
つまずきやすい点: ダイアログで担当者を空欄のまま保存したタスクは、NULL ではなく空文字""として保存されます(_task_dialog()のsave()が.strip()した文字列を渡すため)。assignee or "未割当"のように空文字も拾う書き方にしてください。 -
課題3:新しいボタンを追加する
既存のボタンと同じ書き方で新しいボタンを1つ足し、押されたときに動くメソッドを自作してみましょう。ボタンの作成と
command=の接続がセットで身に付きます。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。