中級者向け No.43

プロジェクト管理ツール

タスク・マイルストーン・ガントチャート表示ができるプロジェクト管理ツール。SQLiteとCanvas描画を学びます。

🎯 難易度: ★★★ 📦 ライブラリ: tkinter(標準ライブラリ) ⏱️ 制作時間: 30〜90分

1. アプリ概要

タスク・マイルストーン・ガントチャート表示ができるプロジェクト管理ツール。SQLiteとCanvas描画を学びます。

このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★★ です。

Pythonでは tkinter を使うことで、クロスプラットフォームなGUIアプリを簡単に作成できます。このアプリを通じて、ウィジェットの配置・イベント処理・データ管理など、GUI開発の実践的なスキルを習得できます。

ソースコードは完全な動作状態で提供しており、コピーしてそのまま実行できます。まずは実行して動作を確認し、その後コードを読んで仕組みを理解していきましょう。カスタマイズセクションでは機能拡張のアイデアも紹介しています。

GUIアプリ開発は、プログラミングの楽しさを実感できる最も効果的な学習方法のひとつです。アプリを作ることで、変数・関数・クラス・イベント処理など、プログラミングの重要な概念が自然と身についていきます。このアプリをきっかけに、オリジナルアプリの開発にも挑戦してみてください。

2. 機能一覧

  • プロジェクト管理ツールのメイン機能
  • 直感的なGUIインターフェース
  • 入力値のバリデーション
  • エラーハンドリング
  • 結果の見やすい表示
  • キーボードショートカット対応

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.10 以上 / Windows・Mac・Linux すべて対応

以下の環境で動作確認しています。

  • Python 3.10 以上
  • OS: Windows 10/11・macOS 12+・Ubuntu 20.04+

4. 完全なソースコード

💡
コードのコピー方法

右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。

app43.py
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="", radius=0)
            # 進捗バー
            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クラスにアプリの全機能をまとめています。__init__メソッドでウィンドウの基本設定を行い、_build_ui()でUI構築、process()でメイン処理を担当します。この分離により、各メソッドの責任が明確になりコードが読みやすくなります。

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="", radius=0)
            # 進捗バー
            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()

LabelFrameによるセクション分け

ttk.LabelFrame を使うことで、入力エリアと結果エリアを視覚的に分けられます。padding引数でフレーム内の余白を設定し、見やすいレイアウトを実現しています。

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="", radius=0)
            # 進捗バー
            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()

Entryウィジェットとイベントバインド

ttk.Entryで入力フィールドを作成します。bind('', ...)でEnterキー押下時に処理を実行できます。これにより、マウスを使わずキーボードだけで操作できるUXが実現できます。

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="", radius=0)
            # 進捗バー
            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()

Textウィジェットでの結果表示

結果表示にはtk.Textウィジェットを使います。state=tk.DISABLEDでユーザーが直接編集できないようにし、表示前にNORMALに切り替えてからinsert()で内容を更新します。

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="", radius=0)
            # 進捗バー
            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()

例外処理とmessagebox

try-except で ValueError と Exception を捕捉し、messagebox.showerror() でユーザーにわかりやすいエラーメッセージを表示します。入力バリデーションは必ず実装しましょう。

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="", radius=0)
            # 進捗バー
            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()

6. ステップバイステップガイド

このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。

  1. 1
    ファイルを作成する

    新しいファイルを作成して app43.py と保存します。

  2. 2
    クラスの骨格を作る

    App43クラスを定義し、__init__とmainloop()の最小構成を作ります。

  3. 3
    タイトルバーを作る

    Frameを使ってカラーバー付きのタイトルエリアを作ります。

  4. 4
    入力フォームを実装する

    LabelFrameとEntryウィジェットで入力エリアを作ります。

  5. 5
    処理ロジックを実装する

    _calculate()メソッドに計算・処理ロジックを実装します。

  6. 6
    結果表示を実装する

    TextウィジェットかLabelに結果を表示する_show_result()を実装します。

  7. 7
    エラー処理を追加する

    try-exceptとmessageboxでエラーハンドリングを追加します。

7. カスタマイズアイデア

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。

💡 ダークモードを追加する

bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。

💡 データのエクスポート機能

計算結果をCSV・TXTファイルに保存するエクスポート機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。

💡 入力履歴機能

以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。

8. よくある問題と解決法

❌ 日本語フォントが表示されない

原因:システムに日本語フォントが見つからない場合があります。

解決法:font引数を省略するかシステムに合ったフォントを指定してください。

❌ ウィンドウのサイズが変更できない

原因:resizable(False, False)が設定されています。

解決法:resizable(True, True)に変更してください。

9. 練習問題

アプリの理解を深めるための練習問題です。難易度順に挑戦してみてください。

  1. 課題1:機能拡張

    プロジェクト管理ツールに新しい機能を1つ追加してみましょう。どんな機能があると便利か考えてから実装してください。

  2. 課題2:UIの改善

    色・フォント・レイアウトを変更して、より使いやすいUIにカスタマイズしてみましょう。

  3. 課題3:保存機能の追加

    入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のNo.44に挑戦しましょう。