中級者向け No.089

コードスニペット管理

プログラムコードをSQLiteに保存・検索・コピーできるスニペット管理ツール。シンタックスハイライト付き。

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

1. アプリ概要

プログラムコードをSQLiteに保存・検索・コピーできるスニペット管理ツール。シンタックスハイライト付き。

このアプリはutilカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)、難易度は ★★★ です。

Pythonの豊富なライブラリを活用することで、実用的なアプリを短いコードで実装できます。ソースコードをコピーして実行し、仕組みを理解したうえでカスタマイズに挑戦してみてください。

GUIアプリ開発はプログラミングの楽しさを実感できる最も効果的な学習方法のひとつです。変数・関数・クラス・イベント処理などの重要な概念が自然と身につきます。

2. 機能一覧

  • コードスニペット管理のメイン機能
  • 直感的なGUIインターフェース
  • 入力値のバリデーション
  • エラーハンドリング
  • 結果の見やすい表示
  • クリア機能付き

3. 事前準備・環境

ℹ️
動作確認環境

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

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

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

4. 完全なソースコード

💡
コードのコピー方法

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

app089.py
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
import re
from datetime import datetime

DB_PATH = os.path.join(os.path.dirname(__file__), "snippets2.db")

LANGUAGES = ["Python", "JavaScript", "TypeScript", "HTML", "CSS",
             "SQL", "Bash", "Go", "Rust", "Java", "C", "C++",
             "Markdown", "JSON", "YAML", "その他"]


class App089:
    """コードスニペット管理 (app049 の拡張版)"""

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペット管理")
        self.root.geometry("1100x660")
        self.root.configure(bg="#0d1117")
        self._snippets = []     # list of rows
        self._current_id = None
        self._init_db()
        self._build_ui()
        self._load_snippets()

    def _init_db(self):
        conn = sqlite3.connect(DB_PATH)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS snippets (
                id       INTEGER PRIMARY KEY AUTOINCREMENT,
                title    TEXT NOT NULL,
                language TEXT DEFAULT 'Python',
                tags     TEXT DEFAULT '',
                code     TEXT DEFAULT '',
                note     TEXT DEFAULT '',
                favorite INTEGER DEFAULT 0,
                created  TEXT,
                updated  TEXT
            );
        """)
        # サンプル
        if conn.execute("SELECT COUNT(*) FROM snippets").fetchone()[0] == 0:
            samples = [
                ("リスト内包表記", "Python", "基礎,リスト",
                 "squares = [x**2 for x in range(10) if x % 2 == 0]",
                 "偶数の二乗リスト", 1),
                ("fetch + async/await", "JavaScript", "非同期,fetch",
                 "const res = await fetch('/api/data');\nconst json = await res.json();",
                 "非同期HTTP", 0),
                ("SELECT with JOIN", "SQL", "DB,JOIN",
                 "SELECT u.name, o.total\nFROM users u\nJOIN orders o ON o.user_id = u.id\nWHERE o.total > 1000;",
                 "ユーザー+注文JOIN", 1),
                ("dataclass", "Python", "クラス,typing",
                 "from dataclasses import dataclass\n\n@dataclass\nclass Point:\n    x: float\n    y: float",
                 "Python データクラス", 0),
                ("flexbox center", "CSS", "レイアウト",
                 ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}",
                 "Flexboxで中央揃え", 1),
            ]
            ts = datetime.now().isoformat()
            for t, l, tg, c, n, f in samples:
                conn.execute(
                    "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                    " VALUES (?,?,?,?,?,?,?,?)",
                    (t, l, tg, c, n, f, ts, ts))
        conn.commit()
        conn.close()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📎 コードスニペット管理",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#161b22", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#0d1117", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        self.search_var = tk.StringVar()
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT)
        self.search_var.trace_add("write", lambda *_: self._load_snippets())
        tk.Label(search_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="ALL")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["ALL"] + LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *_: self._load_snippets())
        self.fav_var = tk.BooleanVar()
        tk.Checkbutton(search_f, text="★ お気に入り", variable=self.fav_var,
                       bg="#0d1117", fg="#ccc", selectcolor="#161b22",
                       activebackground="#0d1117",
                       command=self._load_snippets).pack(side=tk.LEFT, padx=8)

        # メイン PanedWindow
        paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: スニペットリスト
        left = tk.Frame(paned, bg="#0d1117", width=320)
        paned.add(left, weight=0)

        # 操作ボタン
        btn_f = tk.Frame(left, bg="#0d1117")
        btn_f.pack(fill=tk.X, pady=2)
        tk.Button(btn_f, text="+ 新規", command=self._new,
                  bg="#1565c0", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#0d47a1", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="💾 保存", command=self._save_current,
                  bg="#2e7d32", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#1b5e20", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="🗑 削除", command=self._delete,
                  bg="#c62828", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#b71c1c", bd=0).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_f, text="📋 コピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=2)

        cols = ("fav", "title", "lang", "tags")
        self.tree = ttk.Treeview(left, columns=cols, show="headings", height=24)
        self.tree.heading("fav",   text="★")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.column("fav",   width=24,  anchor="center")
        self.tree.column("title", width=150, anchor="w")
        self.tree.column("lang",  width=70,  anchor="center")
        self.tree.column("tags",  width=70,  anchor="w")
        tsb = ttk.Scrollbar(left, command=self.tree.yview)
        self.tree.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.tag_configure("fav", foreground="#ffa726")

        # 右: 詳細エディター
        right = tk.Frame(paned, bg="#0d1117")
        paned.add(right, weight=1)

        # フォームヘッダー
        form_f = tk.Frame(right, bg="#0d1117")
        form_f.pack(fill=tk.X, pady=2)
        tk.Label(form_f, text="タイトル:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT)
        self.title_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.title_var,
                  width=28).pack(side=tk.LEFT, padx=4)

        tk.Label(form_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(form_f, textvariable=self.lang_var,
                     values=LANGUAGES, state="readonly",
                     width=12).pack(side=tk.LEFT)

        tk.Label(form_f, text="タグ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.tags_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.tags_var,
                  width=16).pack(side=tk.LEFT, padx=4)

        self.fav_btn_var = tk.BooleanVar()
        tk.Checkbutton(form_f, text="★ お気に入り",
                       variable=self.fav_btn_var,
                       bg="#0d1117", fg="#ffa726", selectcolor="#0d1117",
                       activebackground="#0d1117").pack(side=tk.LEFT, padx=8)

        # コードエディター (行番号付き)
        code_frame = tk.Frame(right, bg="#0d1117")
        code_frame.pack(fill=tk.BOTH, expand=True, pady=2)

        self.line_canvas = tk.Canvas(code_frame, bg="#161b22", width=40,
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(code_frame, bg="#161b22", fg="#c9d1d9",
                                  font=("Courier New", 10), relief=tk.FLAT,
                                  insertbackground="white", wrap=tk.NONE,
                                  tabs="4c", undo=True)
        xsb = ttk.Scrollbar(code_frame, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        ysb = ttk.Scrollbar(code_frame, command=self.code_text.yview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)
        self.code_text.bind("<KeyRelease>", self._update_lines)
        self.code_text.bind("<MouseWheel>", lambda e: self.root.after(10, self._update_lines))

        # ノート
        tk.Label(right, text="メモ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 8)).pack(anchor="w")
        self.note_text = tk.Text(right, height=3, bg="#161b22", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="white")
        self.note_text.pack(fill=tk.X)

        self.status_var = tk.StringVar(value="スニペットを選択してください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#21262d", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _update_lines(self, _=None):
        self.line_canvas.delete("all")
        i = self.code_text.index("@0,0")
        while True:
            dline = self.code_text.dlineinfo(i)
            if not dline:
                break
            y = dline[1]
            line_no = int(i.split(".")[0])
            self.line_canvas.create_text(
                2, y, anchor="nw", text=str(line_no),
                fill="#484f58", font=("Courier New", 10))
            i = self.code_text.index(f"{i}+1l")

    def _load_snippets(self):
        kw   = self.search_var.get().strip().lower()
        lang = self.lang_filter_var.get()
        fav  = self.fav_var.get()

        conn = sqlite3.connect(DB_PATH)
        query = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if kw:
            query += " AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ? OR LOWER(code) LIKE ?)"
            params += [f"%{kw}%"] * 3
        if lang != "ALL":
            query += " AND language = ?"
            params.append(lang)
        if fav:
            query += " AND favorite = 1"
        query += " ORDER BY updated DESC"
        rows = conn.execute(query, params).fetchall()
        conn.close()

        self._snippets = rows
        self.tree.delete(*self.tree.get_children())
        for row_id, title, language, tags, favorite in rows:
            tag = ("fav",) if favorite else ()
            self.tree.insert("", tk.END, text=str(row_id),
                              values=("★" if favorite else "", title, language, tags),
                              tags=tag)
        self.status_var.set(f"{len(rows)} 件")

    def _on_select(self, _=None):
        sel = self.tree.selection()
        if not sel:
            return
        row_id = int(self.tree.item(sel[0], "text"))
        conn = sqlite3.connect(DB_PATH)
        row = conn.execute(
            "SELECT id, title, language, tags, code, note, favorite FROM snippets WHERE id=?",
            (row_id,)).fetchone()
        conn.close()
        if not row:
            return
        self._current_id = row[0]
        self.title_var.set(row[1])
        self.lang_var.set(row[2])
        self.tags_var.set(row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert("1.0", row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert("1.0", row[5])
        self.fav_btn_var.set(bool(row[6]))
        self._update_lines()

    def _new(self):
        self._current_id = None
        self.title_var.set("新しいスニペット")
        self.lang_var.set("Python")
        self.tags_var.set("")
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_btn_var.set(False)
        self._update_lines()

    def _save_current(self):
        title = self.title_var.get().strip()
        if not title:
            messagebox.showwarning("警告", "タイトルを入力してください")
            return
        ts = datetime.now().isoformat()
        conn = sqlite3.connect(DB_PATH)
        if self._current_id is None:
            cur = conn.execute(
                "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, ts))
            self._current_id = cur.lastrowid
        else:
            conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,note=?,favorite=?,updated=? WHERE id=?",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, self._current_id))
        conn.commit()
        conn.close()
        self._load_snippets()
        self.status_var.set("保存しました")

    def _delete(self):
        if not self._current_id:
            return
        if not messagebox.askyesno("確認", "削除しますか?"):
            return
        conn = sqlite3.connect(DB_PATH)
        conn.execute("DELETE FROM snippets WHERE id=?", (self._current_id,))
        conn.commit()
        conn.close()
        self._current_id = None
        self._load_snippets()

    def _copy_code(self):
        code = self.code_text.get("1.0", tk.END).rstrip()
        self.root.clipboard_clear()
        self.root.clipboard_append(code)
        self.status_var.set("コードをクリップボードにコピーしました")


if __name__ == "__main__":
    root = tk.Tk()
    app = App089(root)
    root.mainloop()

5. コード解説

コードスニペット管理のコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。

クラス設計とコンストラクタ

App089クラスにアプリの全機能をまとめています。__init__でウィンドウ設定、_build_ui()でUI構築、process()でメイン処理を担当します。責任の分離により、コードが読みやすくなります。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
import re
from datetime import datetime

DB_PATH = os.path.join(os.path.dirname(__file__), "snippets2.db")

LANGUAGES = ["Python", "JavaScript", "TypeScript", "HTML", "CSS",
             "SQL", "Bash", "Go", "Rust", "Java", "C", "C++",
             "Markdown", "JSON", "YAML", "その他"]


class App089:
    """コードスニペット管理 (app049 の拡張版)"""

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペット管理")
        self.root.geometry("1100x660")
        self.root.configure(bg="#0d1117")
        self._snippets = []     # list of rows
        self._current_id = None
        self._init_db()
        self._build_ui()
        self._load_snippets()

    def _init_db(self):
        conn = sqlite3.connect(DB_PATH)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS snippets (
                id       INTEGER PRIMARY KEY AUTOINCREMENT,
                title    TEXT NOT NULL,
                language TEXT DEFAULT 'Python',
                tags     TEXT DEFAULT '',
                code     TEXT DEFAULT '',
                note     TEXT DEFAULT '',
                favorite INTEGER DEFAULT 0,
                created  TEXT,
                updated  TEXT
            );
        """)
        # サンプル
        if conn.execute("SELECT COUNT(*) FROM snippets").fetchone()[0] == 0:
            samples = [
                ("リスト内包表記", "Python", "基礎,リスト",
                 "squares = [x**2 for x in range(10) if x % 2 == 0]",
                 "偶数の二乗リスト", 1),
                ("fetch + async/await", "JavaScript", "非同期,fetch",
                 "const res = await fetch('/api/data');\nconst json = await res.json();",
                 "非同期HTTP", 0),
                ("SELECT with JOIN", "SQL", "DB,JOIN",
                 "SELECT u.name, o.total\nFROM users u\nJOIN orders o ON o.user_id = u.id\nWHERE o.total > 1000;",
                 "ユーザー+注文JOIN", 1),
                ("dataclass", "Python", "クラス,typing",
                 "from dataclasses import dataclass\n\n@dataclass\nclass Point:\n    x: float\n    y: float",
                 "Python データクラス", 0),
                ("flexbox center", "CSS", "レイアウト",
                 ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}",
                 "Flexboxで中央揃え", 1),
            ]
            ts = datetime.now().isoformat()
            for t, l, tg, c, n, f in samples:
                conn.execute(
                    "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                    " VALUES (?,?,?,?,?,?,?,?)",
                    (t, l, tg, c, n, f, ts, ts))
        conn.commit()
        conn.close()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📎 コードスニペット管理",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#161b22", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#0d1117", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        self.search_var = tk.StringVar()
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT)
        self.search_var.trace_add("write", lambda *_: self._load_snippets())
        tk.Label(search_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="ALL")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["ALL"] + LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *_: self._load_snippets())
        self.fav_var = tk.BooleanVar()
        tk.Checkbutton(search_f, text="★ お気に入り", variable=self.fav_var,
                       bg="#0d1117", fg="#ccc", selectcolor="#161b22",
                       activebackground="#0d1117",
                       command=self._load_snippets).pack(side=tk.LEFT, padx=8)

        # メイン PanedWindow
        paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: スニペットリスト
        left = tk.Frame(paned, bg="#0d1117", width=320)
        paned.add(left, weight=0)

        # 操作ボタン
        btn_f = tk.Frame(left, bg="#0d1117")
        btn_f.pack(fill=tk.X, pady=2)
        tk.Button(btn_f, text="+ 新規", command=self._new,
                  bg="#1565c0", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#0d47a1", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="💾 保存", command=self._save_current,
                  bg="#2e7d32", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#1b5e20", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="🗑 削除", command=self._delete,
                  bg="#c62828", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#b71c1c", bd=0).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_f, text="📋 コピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=2)

        cols = ("fav", "title", "lang", "tags")
        self.tree = ttk.Treeview(left, columns=cols, show="headings", height=24)
        self.tree.heading("fav",   text="★")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.column("fav",   width=24,  anchor="center")
        self.tree.column("title", width=150, anchor="w")
        self.tree.column("lang",  width=70,  anchor="center")
        self.tree.column("tags",  width=70,  anchor="w")
        tsb = ttk.Scrollbar(left, command=self.tree.yview)
        self.tree.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.tag_configure("fav", foreground="#ffa726")

        # 右: 詳細エディター
        right = tk.Frame(paned, bg="#0d1117")
        paned.add(right, weight=1)

        # フォームヘッダー
        form_f = tk.Frame(right, bg="#0d1117")
        form_f.pack(fill=tk.X, pady=2)
        tk.Label(form_f, text="タイトル:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT)
        self.title_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.title_var,
                  width=28).pack(side=tk.LEFT, padx=4)

        tk.Label(form_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(form_f, textvariable=self.lang_var,
                     values=LANGUAGES, state="readonly",
                     width=12).pack(side=tk.LEFT)

        tk.Label(form_f, text="タグ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.tags_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.tags_var,
                  width=16).pack(side=tk.LEFT, padx=4)

        self.fav_btn_var = tk.BooleanVar()
        tk.Checkbutton(form_f, text="★ お気に入り",
                       variable=self.fav_btn_var,
                       bg="#0d1117", fg="#ffa726", selectcolor="#0d1117",
                       activebackground="#0d1117").pack(side=tk.LEFT, padx=8)

        # コードエディター (行番号付き)
        code_frame = tk.Frame(right, bg="#0d1117")
        code_frame.pack(fill=tk.BOTH, expand=True, pady=2)

        self.line_canvas = tk.Canvas(code_frame, bg="#161b22", width=40,
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(code_frame, bg="#161b22", fg="#c9d1d9",
                                  font=("Courier New", 10), relief=tk.FLAT,
                                  insertbackground="white", wrap=tk.NONE,
                                  tabs="4c", undo=True)
        xsb = ttk.Scrollbar(code_frame, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        ysb = ttk.Scrollbar(code_frame, command=self.code_text.yview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)
        self.code_text.bind("<KeyRelease>", self._update_lines)
        self.code_text.bind("<MouseWheel>", lambda e: self.root.after(10, self._update_lines))

        # ノート
        tk.Label(right, text="メモ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 8)).pack(anchor="w")
        self.note_text = tk.Text(right, height=3, bg="#161b22", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="white")
        self.note_text.pack(fill=tk.X)

        self.status_var = tk.StringVar(value="スニペットを選択してください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#21262d", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _update_lines(self, _=None):
        self.line_canvas.delete("all")
        i = self.code_text.index("@0,0")
        while True:
            dline = self.code_text.dlineinfo(i)
            if not dline:
                break
            y = dline[1]
            line_no = int(i.split(".")[0])
            self.line_canvas.create_text(
                2, y, anchor="nw", text=str(line_no),
                fill="#484f58", font=("Courier New", 10))
            i = self.code_text.index(f"{i}+1l")

    def _load_snippets(self):
        kw   = self.search_var.get().strip().lower()
        lang = self.lang_filter_var.get()
        fav  = self.fav_var.get()

        conn = sqlite3.connect(DB_PATH)
        query = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if kw:
            query += " AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ? OR LOWER(code) LIKE ?)"
            params += [f"%{kw}%"] * 3
        if lang != "ALL":
            query += " AND language = ?"
            params.append(lang)
        if fav:
            query += " AND favorite = 1"
        query += " ORDER BY updated DESC"
        rows = conn.execute(query, params).fetchall()
        conn.close()

        self._snippets = rows
        self.tree.delete(*self.tree.get_children())
        for row_id, title, language, tags, favorite in rows:
            tag = ("fav",) if favorite else ()
            self.tree.insert("", tk.END, text=str(row_id),
                              values=("★" if favorite else "", title, language, tags),
                              tags=tag)
        self.status_var.set(f"{len(rows)} 件")

    def _on_select(self, _=None):
        sel = self.tree.selection()
        if not sel:
            return
        row_id = int(self.tree.item(sel[0], "text"))
        conn = sqlite3.connect(DB_PATH)
        row = conn.execute(
            "SELECT id, title, language, tags, code, note, favorite FROM snippets WHERE id=?",
            (row_id,)).fetchone()
        conn.close()
        if not row:
            return
        self._current_id = row[0]
        self.title_var.set(row[1])
        self.lang_var.set(row[2])
        self.tags_var.set(row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert("1.0", row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert("1.0", row[5])
        self.fav_btn_var.set(bool(row[6]))
        self._update_lines()

    def _new(self):
        self._current_id = None
        self.title_var.set("新しいスニペット")
        self.lang_var.set("Python")
        self.tags_var.set("")
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_btn_var.set(False)
        self._update_lines()

    def _save_current(self):
        title = self.title_var.get().strip()
        if not title:
            messagebox.showwarning("警告", "タイトルを入力してください")
            return
        ts = datetime.now().isoformat()
        conn = sqlite3.connect(DB_PATH)
        if self._current_id is None:
            cur = conn.execute(
                "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, ts))
            self._current_id = cur.lastrowid
        else:
            conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,note=?,favorite=?,updated=? WHERE id=?",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, self._current_id))
        conn.commit()
        conn.close()
        self._load_snippets()
        self.status_var.set("保存しました")

    def _delete(self):
        if not self._current_id:
            return
        if not messagebox.askyesno("確認", "削除しますか?"):
            return
        conn = sqlite3.connect(DB_PATH)
        conn.execute("DELETE FROM snippets WHERE id=?", (self._current_id,))
        conn.commit()
        conn.close()
        self._current_id = None
        self._load_snippets()

    def _copy_code(self):
        code = self.code_text.get("1.0", tk.END).rstrip()
        self.root.clipboard_clear()
        self.root.clipboard_append(code)
        self.status_var.set("コードをクリップボードにコピーしました")


if __name__ == "__main__":
    root = tk.Tk()
    app = App089(root)
    root.mainloop()

UIレイアウトの構築

LabelFrameで入力エリアと結果エリアを視覚的に分けています。pack()で縦に並べ、expand=Trueで結果エリアが画面いっぱいに広がるよう設定しています。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
import re
from datetime import datetime

DB_PATH = os.path.join(os.path.dirname(__file__), "snippets2.db")

LANGUAGES = ["Python", "JavaScript", "TypeScript", "HTML", "CSS",
             "SQL", "Bash", "Go", "Rust", "Java", "C", "C++",
             "Markdown", "JSON", "YAML", "その他"]


class App089:
    """コードスニペット管理 (app049 の拡張版)"""

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペット管理")
        self.root.geometry("1100x660")
        self.root.configure(bg="#0d1117")
        self._snippets = []     # list of rows
        self._current_id = None
        self._init_db()
        self._build_ui()
        self._load_snippets()

    def _init_db(self):
        conn = sqlite3.connect(DB_PATH)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS snippets (
                id       INTEGER PRIMARY KEY AUTOINCREMENT,
                title    TEXT NOT NULL,
                language TEXT DEFAULT 'Python',
                tags     TEXT DEFAULT '',
                code     TEXT DEFAULT '',
                note     TEXT DEFAULT '',
                favorite INTEGER DEFAULT 0,
                created  TEXT,
                updated  TEXT
            );
        """)
        # サンプル
        if conn.execute("SELECT COUNT(*) FROM snippets").fetchone()[0] == 0:
            samples = [
                ("リスト内包表記", "Python", "基礎,リスト",
                 "squares = [x**2 for x in range(10) if x % 2 == 0]",
                 "偶数の二乗リスト", 1),
                ("fetch + async/await", "JavaScript", "非同期,fetch",
                 "const res = await fetch('/api/data');\nconst json = await res.json();",
                 "非同期HTTP", 0),
                ("SELECT with JOIN", "SQL", "DB,JOIN",
                 "SELECT u.name, o.total\nFROM users u\nJOIN orders o ON o.user_id = u.id\nWHERE o.total > 1000;",
                 "ユーザー+注文JOIN", 1),
                ("dataclass", "Python", "クラス,typing",
                 "from dataclasses import dataclass\n\n@dataclass\nclass Point:\n    x: float\n    y: float",
                 "Python データクラス", 0),
                ("flexbox center", "CSS", "レイアウト",
                 ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}",
                 "Flexboxで中央揃え", 1),
            ]
            ts = datetime.now().isoformat()
            for t, l, tg, c, n, f in samples:
                conn.execute(
                    "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                    " VALUES (?,?,?,?,?,?,?,?)",
                    (t, l, tg, c, n, f, ts, ts))
        conn.commit()
        conn.close()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📎 コードスニペット管理",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#161b22", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#0d1117", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        self.search_var = tk.StringVar()
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT)
        self.search_var.trace_add("write", lambda *_: self._load_snippets())
        tk.Label(search_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="ALL")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["ALL"] + LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *_: self._load_snippets())
        self.fav_var = tk.BooleanVar()
        tk.Checkbutton(search_f, text="★ お気に入り", variable=self.fav_var,
                       bg="#0d1117", fg="#ccc", selectcolor="#161b22",
                       activebackground="#0d1117",
                       command=self._load_snippets).pack(side=tk.LEFT, padx=8)

        # メイン PanedWindow
        paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: スニペットリスト
        left = tk.Frame(paned, bg="#0d1117", width=320)
        paned.add(left, weight=0)

        # 操作ボタン
        btn_f = tk.Frame(left, bg="#0d1117")
        btn_f.pack(fill=tk.X, pady=2)
        tk.Button(btn_f, text="+ 新規", command=self._new,
                  bg="#1565c0", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#0d47a1", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="💾 保存", command=self._save_current,
                  bg="#2e7d32", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#1b5e20", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="🗑 削除", command=self._delete,
                  bg="#c62828", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#b71c1c", bd=0).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_f, text="📋 コピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=2)

        cols = ("fav", "title", "lang", "tags")
        self.tree = ttk.Treeview(left, columns=cols, show="headings", height=24)
        self.tree.heading("fav",   text="★")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.column("fav",   width=24,  anchor="center")
        self.tree.column("title", width=150, anchor="w")
        self.tree.column("lang",  width=70,  anchor="center")
        self.tree.column("tags",  width=70,  anchor="w")
        tsb = ttk.Scrollbar(left, command=self.tree.yview)
        self.tree.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.tag_configure("fav", foreground="#ffa726")

        # 右: 詳細エディター
        right = tk.Frame(paned, bg="#0d1117")
        paned.add(right, weight=1)

        # フォームヘッダー
        form_f = tk.Frame(right, bg="#0d1117")
        form_f.pack(fill=tk.X, pady=2)
        tk.Label(form_f, text="タイトル:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT)
        self.title_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.title_var,
                  width=28).pack(side=tk.LEFT, padx=4)

        tk.Label(form_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(form_f, textvariable=self.lang_var,
                     values=LANGUAGES, state="readonly",
                     width=12).pack(side=tk.LEFT)

        tk.Label(form_f, text="タグ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.tags_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.tags_var,
                  width=16).pack(side=tk.LEFT, padx=4)

        self.fav_btn_var = tk.BooleanVar()
        tk.Checkbutton(form_f, text="★ お気に入り",
                       variable=self.fav_btn_var,
                       bg="#0d1117", fg="#ffa726", selectcolor="#0d1117",
                       activebackground="#0d1117").pack(side=tk.LEFT, padx=8)

        # コードエディター (行番号付き)
        code_frame = tk.Frame(right, bg="#0d1117")
        code_frame.pack(fill=tk.BOTH, expand=True, pady=2)

        self.line_canvas = tk.Canvas(code_frame, bg="#161b22", width=40,
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(code_frame, bg="#161b22", fg="#c9d1d9",
                                  font=("Courier New", 10), relief=tk.FLAT,
                                  insertbackground="white", wrap=tk.NONE,
                                  tabs="4c", undo=True)
        xsb = ttk.Scrollbar(code_frame, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        ysb = ttk.Scrollbar(code_frame, command=self.code_text.yview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)
        self.code_text.bind("<KeyRelease>", self._update_lines)
        self.code_text.bind("<MouseWheel>", lambda e: self.root.after(10, self._update_lines))

        # ノート
        tk.Label(right, text="メモ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 8)).pack(anchor="w")
        self.note_text = tk.Text(right, height=3, bg="#161b22", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="white")
        self.note_text.pack(fill=tk.X)

        self.status_var = tk.StringVar(value="スニペットを選択してください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#21262d", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _update_lines(self, _=None):
        self.line_canvas.delete("all")
        i = self.code_text.index("@0,0")
        while True:
            dline = self.code_text.dlineinfo(i)
            if not dline:
                break
            y = dline[1]
            line_no = int(i.split(".")[0])
            self.line_canvas.create_text(
                2, y, anchor="nw", text=str(line_no),
                fill="#484f58", font=("Courier New", 10))
            i = self.code_text.index(f"{i}+1l")

    def _load_snippets(self):
        kw   = self.search_var.get().strip().lower()
        lang = self.lang_filter_var.get()
        fav  = self.fav_var.get()

        conn = sqlite3.connect(DB_PATH)
        query = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if kw:
            query += " AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ? OR LOWER(code) LIKE ?)"
            params += [f"%{kw}%"] * 3
        if lang != "ALL":
            query += " AND language = ?"
            params.append(lang)
        if fav:
            query += " AND favorite = 1"
        query += " ORDER BY updated DESC"
        rows = conn.execute(query, params).fetchall()
        conn.close()

        self._snippets = rows
        self.tree.delete(*self.tree.get_children())
        for row_id, title, language, tags, favorite in rows:
            tag = ("fav",) if favorite else ()
            self.tree.insert("", tk.END, text=str(row_id),
                              values=("★" if favorite else "", title, language, tags),
                              tags=tag)
        self.status_var.set(f"{len(rows)} 件")

    def _on_select(self, _=None):
        sel = self.tree.selection()
        if not sel:
            return
        row_id = int(self.tree.item(sel[0], "text"))
        conn = sqlite3.connect(DB_PATH)
        row = conn.execute(
            "SELECT id, title, language, tags, code, note, favorite FROM snippets WHERE id=?",
            (row_id,)).fetchone()
        conn.close()
        if not row:
            return
        self._current_id = row[0]
        self.title_var.set(row[1])
        self.lang_var.set(row[2])
        self.tags_var.set(row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert("1.0", row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert("1.0", row[5])
        self.fav_btn_var.set(bool(row[6]))
        self._update_lines()

    def _new(self):
        self._current_id = None
        self.title_var.set("新しいスニペット")
        self.lang_var.set("Python")
        self.tags_var.set("")
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_btn_var.set(False)
        self._update_lines()

    def _save_current(self):
        title = self.title_var.get().strip()
        if not title:
            messagebox.showwarning("警告", "タイトルを入力してください")
            return
        ts = datetime.now().isoformat()
        conn = sqlite3.connect(DB_PATH)
        if self._current_id is None:
            cur = conn.execute(
                "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, ts))
            self._current_id = cur.lastrowid
        else:
            conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,note=?,favorite=?,updated=? WHERE id=?",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, self._current_id))
        conn.commit()
        conn.close()
        self._load_snippets()
        self.status_var.set("保存しました")

    def _delete(self):
        if not self._current_id:
            return
        if not messagebox.askyesno("確認", "削除しますか?"):
            return
        conn = sqlite3.connect(DB_PATH)
        conn.execute("DELETE FROM snippets WHERE id=?", (self._current_id,))
        conn.commit()
        conn.close()
        self._current_id = None
        self._load_snippets()

    def _copy_code(self):
        code = self.code_text.get("1.0", tk.END).rstrip()
        self.root.clipboard_clear()
        self.root.clipboard_append(code)
        self.status_var.set("コードをクリップボードにコピーしました")


if __name__ == "__main__":
    root = tk.Tk()
    app = App089(root)
    root.mainloop()

イベント処理

ボタンのcommand引数でクリックイベントを、bind('')でEnterキーイベントを処理します。どちらの操作でも同じprocess()が呼ばれ、コードの重複を避けられます。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
import re
from datetime import datetime

DB_PATH = os.path.join(os.path.dirname(__file__), "snippets2.db")

LANGUAGES = ["Python", "JavaScript", "TypeScript", "HTML", "CSS",
             "SQL", "Bash", "Go", "Rust", "Java", "C", "C++",
             "Markdown", "JSON", "YAML", "その他"]


class App089:
    """コードスニペット管理 (app049 の拡張版)"""

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペット管理")
        self.root.geometry("1100x660")
        self.root.configure(bg="#0d1117")
        self._snippets = []     # list of rows
        self._current_id = None
        self._init_db()
        self._build_ui()
        self._load_snippets()

    def _init_db(self):
        conn = sqlite3.connect(DB_PATH)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS snippets (
                id       INTEGER PRIMARY KEY AUTOINCREMENT,
                title    TEXT NOT NULL,
                language TEXT DEFAULT 'Python',
                tags     TEXT DEFAULT '',
                code     TEXT DEFAULT '',
                note     TEXT DEFAULT '',
                favorite INTEGER DEFAULT 0,
                created  TEXT,
                updated  TEXT
            );
        """)
        # サンプル
        if conn.execute("SELECT COUNT(*) FROM snippets").fetchone()[0] == 0:
            samples = [
                ("リスト内包表記", "Python", "基礎,リスト",
                 "squares = [x**2 for x in range(10) if x % 2 == 0]",
                 "偶数の二乗リスト", 1),
                ("fetch + async/await", "JavaScript", "非同期,fetch",
                 "const res = await fetch('/api/data');\nconst json = await res.json();",
                 "非同期HTTP", 0),
                ("SELECT with JOIN", "SQL", "DB,JOIN",
                 "SELECT u.name, o.total\nFROM users u\nJOIN orders o ON o.user_id = u.id\nWHERE o.total > 1000;",
                 "ユーザー+注文JOIN", 1),
                ("dataclass", "Python", "クラス,typing",
                 "from dataclasses import dataclass\n\n@dataclass\nclass Point:\n    x: float\n    y: float",
                 "Python データクラス", 0),
                ("flexbox center", "CSS", "レイアウト",
                 ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}",
                 "Flexboxで中央揃え", 1),
            ]
            ts = datetime.now().isoformat()
            for t, l, tg, c, n, f in samples:
                conn.execute(
                    "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                    " VALUES (?,?,?,?,?,?,?,?)",
                    (t, l, tg, c, n, f, ts, ts))
        conn.commit()
        conn.close()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📎 コードスニペット管理",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#161b22", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#0d1117", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        self.search_var = tk.StringVar()
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT)
        self.search_var.trace_add("write", lambda *_: self._load_snippets())
        tk.Label(search_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="ALL")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["ALL"] + LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *_: self._load_snippets())
        self.fav_var = tk.BooleanVar()
        tk.Checkbutton(search_f, text="★ お気に入り", variable=self.fav_var,
                       bg="#0d1117", fg="#ccc", selectcolor="#161b22",
                       activebackground="#0d1117",
                       command=self._load_snippets).pack(side=tk.LEFT, padx=8)

        # メイン PanedWindow
        paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: スニペットリスト
        left = tk.Frame(paned, bg="#0d1117", width=320)
        paned.add(left, weight=0)

        # 操作ボタン
        btn_f = tk.Frame(left, bg="#0d1117")
        btn_f.pack(fill=tk.X, pady=2)
        tk.Button(btn_f, text="+ 新規", command=self._new,
                  bg="#1565c0", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#0d47a1", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="💾 保存", command=self._save_current,
                  bg="#2e7d32", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#1b5e20", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="🗑 削除", command=self._delete,
                  bg="#c62828", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#b71c1c", bd=0).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_f, text="📋 コピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=2)

        cols = ("fav", "title", "lang", "tags")
        self.tree = ttk.Treeview(left, columns=cols, show="headings", height=24)
        self.tree.heading("fav",   text="★")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.column("fav",   width=24,  anchor="center")
        self.tree.column("title", width=150, anchor="w")
        self.tree.column("lang",  width=70,  anchor="center")
        self.tree.column("tags",  width=70,  anchor="w")
        tsb = ttk.Scrollbar(left, command=self.tree.yview)
        self.tree.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.tag_configure("fav", foreground="#ffa726")

        # 右: 詳細エディター
        right = tk.Frame(paned, bg="#0d1117")
        paned.add(right, weight=1)

        # フォームヘッダー
        form_f = tk.Frame(right, bg="#0d1117")
        form_f.pack(fill=tk.X, pady=2)
        tk.Label(form_f, text="タイトル:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT)
        self.title_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.title_var,
                  width=28).pack(side=tk.LEFT, padx=4)

        tk.Label(form_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(form_f, textvariable=self.lang_var,
                     values=LANGUAGES, state="readonly",
                     width=12).pack(side=tk.LEFT)

        tk.Label(form_f, text="タグ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.tags_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.tags_var,
                  width=16).pack(side=tk.LEFT, padx=4)

        self.fav_btn_var = tk.BooleanVar()
        tk.Checkbutton(form_f, text="★ お気に入り",
                       variable=self.fav_btn_var,
                       bg="#0d1117", fg="#ffa726", selectcolor="#0d1117",
                       activebackground="#0d1117").pack(side=tk.LEFT, padx=8)

        # コードエディター (行番号付き)
        code_frame = tk.Frame(right, bg="#0d1117")
        code_frame.pack(fill=tk.BOTH, expand=True, pady=2)

        self.line_canvas = tk.Canvas(code_frame, bg="#161b22", width=40,
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(code_frame, bg="#161b22", fg="#c9d1d9",
                                  font=("Courier New", 10), relief=tk.FLAT,
                                  insertbackground="white", wrap=tk.NONE,
                                  tabs="4c", undo=True)
        xsb = ttk.Scrollbar(code_frame, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        ysb = ttk.Scrollbar(code_frame, command=self.code_text.yview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)
        self.code_text.bind("<KeyRelease>", self._update_lines)
        self.code_text.bind("<MouseWheel>", lambda e: self.root.after(10, self._update_lines))

        # ノート
        tk.Label(right, text="メモ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 8)).pack(anchor="w")
        self.note_text = tk.Text(right, height=3, bg="#161b22", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="white")
        self.note_text.pack(fill=tk.X)

        self.status_var = tk.StringVar(value="スニペットを選択してください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#21262d", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _update_lines(self, _=None):
        self.line_canvas.delete("all")
        i = self.code_text.index("@0,0")
        while True:
            dline = self.code_text.dlineinfo(i)
            if not dline:
                break
            y = dline[1]
            line_no = int(i.split(".")[0])
            self.line_canvas.create_text(
                2, y, anchor="nw", text=str(line_no),
                fill="#484f58", font=("Courier New", 10))
            i = self.code_text.index(f"{i}+1l")

    def _load_snippets(self):
        kw   = self.search_var.get().strip().lower()
        lang = self.lang_filter_var.get()
        fav  = self.fav_var.get()

        conn = sqlite3.connect(DB_PATH)
        query = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if kw:
            query += " AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ? OR LOWER(code) LIKE ?)"
            params += [f"%{kw}%"] * 3
        if lang != "ALL":
            query += " AND language = ?"
            params.append(lang)
        if fav:
            query += " AND favorite = 1"
        query += " ORDER BY updated DESC"
        rows = conn.execute(query, params).fetchall()
        conn.close()

        self._snippets = rows
        self.tree.delete(*self.tree.get_children())
        for row_id, title, language, tags, favorite in rows:
            tag = ("fav",) if favorite else ()
            self.tree.insert("", tk.END, text=str(row_id),
                              values=("★" if favorite else "", title, language, tags),
                              tags=tag)
        self.status_var.set(f"{len(rows)} 件")

    def _on_select(self, _=None):
        sel = self.tree.selection()
        if not sel:
            return
        row_id = int(self.tree.item(sel[0], "text"))
        conn = sqlite3.connect(DB_PATH)
        row = conn.execute(
            "SELECT id, title, language, tags, code, note, favorite FROM snippets WHERE id=?",
            (row_id,)).fetchone()
        conn.close()
        if not row:
            return
        self._current_id = row[0]
        self.title_var.set(row[1])
        self.lang_var.set(row[2])
        self.tags_var.set(row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert("1.0", row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert("1.0", row[5])
        self.fav_btn_var.set(bool(row[6]))
        self._update_lines()

    def _new(self):
        self._current_id = None
        self.title_var.set("新しいスニペット")
        self.lang_var.set("Python")
        self.tags_var.set("")
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_btn_var.set(False)
        self._update_lines()

    def _save_current(self):
        title = self.title_var.get().strip()
        if not title:
            messagebox.showwarning("警告", "タイトルを入力してください")
            return
        ts = datetime.now().isoformat()
        conn = sqlite3.connect(DB_PATH)
        if self._current_id is None:
            cur = conn.execute(
                "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, ts))
            self._current_id = cur.lastrowid
        else:
            conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,note=?,favorite=?,updated=? WHERE id=?",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, self._current_id))
        conn.commit()
        conn.close()
        self._load_snippets()
        self.status_var.set("保存しました")

    def _delete(self):
        if not self._current_id:
            return
        if not messagebox.askyesno("確認", "削除しますか?"):
            return
        conn = sqlite3.connect(DB_PATH)
        conn.execute("DELETE FROM snippets WHERE id=?", (self._current_id,))
        conn.commit()
        conn.close()
        self._current_id = None
        self._load_snippets()

    def _copy_code(self):
        code = self.code_text.get("1.0", tk.END).rstrip()
        self.root.clipboard_clear()
        self.root.clipboard_append(code)
        self.status_var.set("コードをクリップボードにコピーしました")


if __name__ == "__main__":
    root = tk.Tk()
    app = App089(root)
    root.mainloop()

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

tk.Textウィジェットをstate=DISABLED(読み取り専用)で作成し、更新時はNORMALに変更してinsert()で内容を書き込み、再びDISABLEDに戻します。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
import re
from datetime import datetime

DB_PATH = os.path.join(os.path.dirname(__file__), "snippets2.db")

LANGUAGES = ["Python", "JavaScript", "TypeScript", "HTML", "CSS",
             "SQL", "Bash", "Go", "Rust", "Java", "C", "C++",
             "Markdown", "JSON", "YAML", "その他"]


class App089:
    """コードスニペット管理 (app049 の拡張版)"""

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペット管理")
        self.root.geometry("1100x660")
        self.root.configure(bg="#0d1117")
        self._snippets = []     # list of rows
        self._current_id = None
        self._init_db()
        self._build_ui()
        self._load_snippets()

    def _init_db(self):
        conn = sqlite3.connect(DB_PATH)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS snippets (
                id       INTEGER PRIMARY KEY AUTOINCREMENT,
                title    TEXT NOT NULL,
                language TEXT DEFAULT 'Python',
                tags     TEXT DEFAULT '',
                code     TEXT DEFAULT '',
                note     TEXT DEFAULT '',
                favorite INTEGER DEFAULT 0,
                created  TEXT,
                updated  TEXT
            );
        """)
        # サンプル
        if conn.execute("SELECT COUNT(*) FROM snippets").fetchone()[0] == 0:
            samples = [
                ("リスト内包表記", "Python", "基礎,リスト",
                 "squares = [x**2 for x in range(10) if x % 2 == 0]",
                 "偶数の二乗リスト", 1),
                ("fetch + async/await", "JavaScript", "非同期,fetch",
                 "const res = await fetch('/api/data');\nconst json = await res.json();",
                 "非同期HTTP", 0),
                ("SELECT with JOIN", "SQL", "DB,JOIN",
                 "SELECT u.name, o.total\nFROM users u\nJOIN orders o ON o.user_id = u.id\nWHERE o.total > 1000;",
                 "ユーザー+注文JOIN", 1),
                ("dataclass", "Python", "クラス,typing",
                 "from dataclasses import dataclass\n\n@dataclass\nclass Point:\n    x: float\n    y: float",
                 "Python データクラス", 0),
                ("flexbox center", "CSS", "レイアウト",
                 ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}",
                 "Flexboxで中央揃え", 1),
            ]
            ts = datetime.now().isoformat()
            for t, l, tg, c, n, f in samples:
                conn.execute(
                    "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                    " VALUES (?,?,?,?,?,?,?,?)",
                    (t, l, tg, c, n, f, ts, ts))
        conn.commit()
        conn.close()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📎 コードスニペット管理",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#161b22", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#0d1117", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        self.search_var = tk.StringVar()
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT)
        self.search_var.trace_add("write", lambda *_: self._load_snippets())
        tk.Label(search_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="ALL")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["ALL"] + LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *_: self._load_snippets())
        self.fav_var = tk.BooleanVar()
        tk.Checkbutton(search_f, text="★ お気に入り", variable=self.fav_var,
                       bg="#0d1117", fg="#ccc", selectcolor="#161b22",
                       activebackground="#0d1117",
                       command=self._load_snippets).pack(side=tk.LEFT, padx=8)

        # メイン PanedWindow
        paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: スニペットリスト
        left = tk.Frame(paned, bg="#0d1117", width=320)
        paned.add(left, weight=0)

        # 操作ボタン
        btn_f = tk.Frame(left, bg="#0d1117")
        btn_f.pack(fill=tk.X, pady=2)
        tk.Button(btn_f, text="+ 新規", command=self._new,
                  bg="#1565c0", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#0d47a1", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="💾 保存", command=self._save_current,
                  bg="#2e7d32", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#1b5e20", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="🗑 削除", command=self._delete,
                  bg="#c62828", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#b71c1c", bd=0).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_f, text="📋 コピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=2)

        cols = ("fav", "title", "lang", "tags")
        self.tree = ttk.Treeview(left, columns=cols, show="headings", height=24)
        self.tree.heading("fav",   text="★")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.column("fav",   width=24,  anchor="center")
        self.tree.column("title", width=150, anchor="w")
        self.tree.column("lang",  width=70,  anchor="center")
        self.tree.column("tags",  width=70,  anchor="w")
        tsb = ttk.Scrollbar(left, command=self.tree.yview)
        self.tree.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.tag_configure("fav", foreground="#ffa726")

        # 右: 詳細エディター
        right = tk.Frame(paned, bg="#0d1117")
        paned.add(right, weight=1)

        # フォームヘッダー
        form_f = tk.Frame(right, bg="#0d1117")
        form_f.pack(fill=tk.X, pady=2)
        tk.Label(form_f, text="タイトル:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT)
        self.title_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.title_var,
                  width=28).pack(side=tk.LEFT, padx=4)

        tk.Label(form_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(form_f, textvariable=self.lang_var,
                     values=LANGUAGES, state="readonly",
                     width=12).pack(side=tk.LEFT)

        tk.Label(form_f, text="タグ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.tags_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.tags_var,
                  width=16).pack(side=tk.LEFT, padx=4)

        self.fav_btn_var = tk.BooleanVar()
        tk.Checkbutton(form_f, text="★ お気に入り",
                       variable=self.fav_btn_var,
                       bg="#0d1117", fg="#ffa726", selectcolor="#0d1117",
                       activebackground="#0d1117").pack(side=tk.LEFT, padx=8)

        # コードエディター (行番号付き)
        code_frame = tk.Frame(right, bg="#0d1117")
        code_frame.pack(fill=tk.BOTH, expand=True, pady=2)

        self.line_canvas = tk.Canvas(code_frame, bg="#161b22", width=40,
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(code_frame, bg="#161b22", fg="#c9d1d9",
                                  font=("Courier New", 10), relief=tk.FLAT,
                                  insertbackground="white", wrap=tk.NONE,
                                  tabs="4c", undo=True)
        xsb = ttk.Scrollbar(code_frame, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        ysb = ttk.Scrollbar(code_frame, command=self.code_text.yview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)
        self.code_text.bind("<KeyRelease>", self._update_lines)
        self.code_text.bind("<MouseWheel>", lambda e: self.root.after(10, self._update_lines))

        # ノート
        tk.Label(right, text="メモ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 8)).pack(anchor="w")
        self.note_text = tk.Text(right, height=3, bg="#161b22", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="white")
        self.note_text.pack(fill=tk.X)

        self.status_var = tk.StringVar(value="スニペットを選択してください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#21262d", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _update_lines(self, _=None):
        self.line_canvas.delete("all")
        i = self.code_text.index("@0,0")
        while True:
            dline = self.code_text.dlineinfo(i)
            if not dline:
                break
            y = dline[1]
            line_no = int(i.split(".")[0])
            self.line_canvas.create_text(
                2, y, anchor="nw", text=str(line_no),
                fill="#484f58", font=("Courier New", 10))
            i = self.code_text.index(f"{i}+1l")

    def _load_snippets(self):
        kw   = self.search_var.get().strip().lower()
        lang = self.lang_filter_var.get()
        fav  = self.fav_var.get()

        conn = sqlite3.connect(DB_PATH)
        query = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if kw:
            query += " AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ? OR LOWER(code) LIKE ?)"
            params += [f"%{kw}%"] * 3
        if lang != "ALL":
            query += " AND language = ?"
            params.append(lang)
        if fav:
            query += " AND favorite = 1"
        query += " ORDER BY updated DESC"
        rows = conn.execute(query, params).fetchall()
        conn.close()

        self._snippets = rows
        self.tree.delete(*self.tree.get_children())
        for row_id, title, language, tags, favorite in rows:
            tag = ("fav",) if favorite else ()
            self.tree.insert("", tk.END, text=str(row_id),
                              values=("★" if favorite else "", title, language, tags),
                              tags=tag)
        self.status_var.set(f"{len(rows)} 件")

    def _on_select(self, _=None):
        sel = self.tree.selection()
        if not sel:
            return
        row_id = int(self.tree.item(sel[0], "text"))
        conn = sqlite3.connect(DB_PATH)
        row = conn.execute(
            "SELECT id, title, language, tags, code, note, favorite FROM snippets WHERE id=?",
            (row_id,)).fetchone()
        conn.close()
        if not row:
            return
        self._current_id = row[0]
        self.title_var.set(row[1])
        self.lang_var.set(row[2])
        self.tags_var.set(row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert("1.0", row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert("1.0", row[5])
        self.fav_btn_var.set(bool(row[6]))
        self._update_lines()

    def _new(self):
        self._current_id = None
        self.title_var.set("新しいスニペット")
        self.lang_var.set("Python")
        self.tags_var.set("")
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_btn_var.set(False)
        self._update_lines()

    def _save_current(self):
        title = self.title_var.get().strip()
        if not title:
            messagebox.showwarning("警告", "タイトルを入力してください")
            return
        ts = datetime.now().isoformat()
        conn = sqlite3.connect(DB_PATH)
        if self._current_id is None:
            cur = conn.execute(
                "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, ts))
            self._current_id = cur.lastrowid
        else:
            conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,note=?,favorite=?,updated=? WHERE id=?",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, self._current_id))
        conn.commit()
        conn.close()
        self._load_snippets()
        self.status_var.set("保存しました")

    def _delete(self):
        if not self._current_id:
            return
        if not messagebox.askyesno("確認", "削除しますか?"):
            return
        conn = sqlite3.connect(DB_PATH)
        conn.execute("DELETE FROM snippets WHERE id=?", (self._current_id,))
        conn.commit()
        conn.close()
        self._current_id = None
        self._load_snippets()

    def _copy_code(self):
        code = self.code_text.get("1.0", tk.END).rstrip()
        self.root.clipboard_clear()
        self.root.clipboard_append(code)
        self.status_var.set("コードをクリップボードにコピーしました")


if __name__ == "__main__":
    root = tk.Tk()
    app = App089(root)
    root.mainloop()

例外処理とエラーハンドリング

try-exceptでValueErrorとExceptionを捕捉し、messagebox.showerror()でエラーメッセージを表示します。予期しないエラーも処理することで、アプリの堅牢性が向上します。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
import re
from datetime import datetime

DB_PATH = os.path.join(os.path.dirname(__file__), "snippets2.db")

LANGUAGES = ["Python", "JavaScript", "TypeScript", "HTML", "CSS",
             "SQL", "Bash", "Go", "Rust", "Java", "C", "C++",
             "Markdown", "JSON", "YAML", "その他"]


class App089:
    """コードスニペット管理 (app049 の拡張版)"""

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペット管理")
        self.root.geometry("1100x660")
        self.root.configure(bg="#0d1117")
        self._snippets = []     # list of rows
        self._current_id = None
        self._init_db()
        self._build_ui()
        self._load_snippets()

    def _init_db(self):
        conn = sqlite3.connect(DB_PATH)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS snippets (
                id       INTEGER PRIMARY KEY AUTOINCREMENT,
                title    TEXT NOT NULL,
                language TEXT DEFAULT 'Python',
                tags     TEXT DEFAULT '',
                code     TEXT DEFAULT '',
                note     TEXT DEFAULT '',
                favorite INTEGER DEFAULT 0,
                created  TEXT,
                updated  TEXT
            );
        """)
        # サンプル
        if conn.execute("SELECT COUNT(*) FROM snippets").fetchone()[0] == 0:
            samples = [
                ("リスト内包表記", "Python", "基礎,リスト",
                 "squares = [x**2 for x in range(10) if x % 2 == 0]",
                 "偶数の二乗リスト", 1),
                ("fetch + async/await", "JavaScript", "非同期,fetch",
                 "const res = await fetch('/api/data');\nconst json = await res.json();",
                 "非同期HTTP", 0),
                ("SELECT with JOIN", "SQL", "DB,JOIN",
                 "SELECT u.name, o.total\nFROM users u\nJOIN orders o ON o.user_id = u.id\nWHERE o.total > 1000;",
                 "ユーザー+注文JOIN", 1),
                ("dataclass", "Python", "クラス,typing",
                 "from dataclasses import dataclass\n\n@dataclass\nclass Point:\n    x: float\n    y: float",
                 "Python データクラス", 0),
                ("flexbox center", "CSS", "レイアウト",
                 ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}",
                 "Flexboxで中央揃え", 1),
            ]
            ts = datetime.now().isoformat()
            for t, l, tg, c, n, f in samples:
                conn.execute(
                    "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                    " VALUES (?,?,?,?,?,?,?,?)",
                    (t, l, tg, c, n, f, ts, ts))
        conn.commit()
        conn.close()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📎 コードスニペット管理",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#161b22", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#0d1117", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        self.search_var = tk.StringVar()
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT)
        self.search_var.trace_add("write", lambda *_: self._load_snippets())
        tk.Label(search_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="ALL")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["ALL"] + LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *_: self._load_snippets())
        self.fav_var = tk.BooleanVar()
        tk.Checkbutton(search_f, text="★ お気に入り", variable=self.fav_var,
                       bg="#0d1117", fg="#ccc", selectcolor="#161b22",
                       activebackground="#0d1117",
                       command=self._load_snippets).pack(side=tk.LEFT, padx=8)

        # メイン PanedWindow
        paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: スニペットリスト
        left = tk.Frame(paned, bg="#0d1117", width=320)
        paned.add(left, weight=0)

        # 操作ボタン
        btn_f = tk.Frame(left, bg="#0d1117")
        btn_f.pack(fill=tk.X, pady=2)
        tk.Button(btn_f, text="+ 新規", command=self._new,
                  bg="#1565c0", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#0d47a1", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="💾 保存", command=self._save_current,
                  bg="#2e7d32", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#1b5e20", bd=0).pack(side=tk.LEFT, padx=2)
        tk.Button(btn_f, text="🗑 削除", command=self._delete,
                  bg="#c62828", fg="white", relief=tk.FLAT,
                  font=("Arial", 9), padx=8, pady=3,
                  activebackground="#b71c1c", bd=0).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_f, text="📋 コピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=2)

        cols = ("fav", "title", "lang", "tags")
        self.tree = ttk.Treeview(left, columns=cols, show="headings", height=24)
        self.tree.heading("fav",   text="★")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.column("fav",   width=24,  anchor="center")
        self.tree.column("title", width=150, anchor="w")
        self.tree.column("lang",  width=70,  anchor="center")
        self.tree.column("tags",  width=70,  anchor="w")
        tsb = ttk.Scrollbar(left, command=self.tree.yview)
        self.tree.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.tag_configure("fav", foreground="#ffa726")

        # 右: 詳細エディター
        right = tk.Frame(paned, bg="#0d1117")
        paned.add(right, weight=1)

        # フォームヘッダー
        form_f = tk.Frame(right, bg="#0d1117")
        form_f.pack(fill=tk.X, pady=2)
        tk.Label(form_f, text="タイトル:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT)
        self.title_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.title_var,
                  width=28).pack(side=tk.LEFT, padx=4)

        tk.Label(form_f, text="言語:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(form_f, textvariable=self.lang_var,
                     values=LANGUAGES, state="readonly",
                     width=12).pack(side=tk.LEFT)

        tk.Label(form_f, text="タグ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.tags_var = tk.StringVar()
        ttk.Entry(form_f, textvariable=self.tags_var,
                  width=16).pack(side=tk.LEFT, padx=4)

        self.fav_btn_var = tk.BooleanVar()
        tk.Checkbutton(form_f, text="★ お気に入り",
                       variable=self.fav_btn_var,
                       bg="#0d1117", fg="#ffa726", selectcolor="#0d1117",
                       activebackground="#0d1117").pack(side=tk.LEFT, padx=8)

        # コードエディター (行番号付き)
        code_frame = tk.Frame(right, bg="#0d1117")
        code_frame.pack(fill=tk.BOTH, expand=True, pady=2)

        self.line_canvas = tk.Canvas(code_frame, bg="#161b22", width=40,
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(code_frame, bg="#161b22", fg="#c9d1d9",
                                  font=("Courier New", 10), relief=tk.FLAT,
                                  insertbackground="white", wrap=tk.NONE,
                                  tabs="4c", undo=True)
        xsb = ttk.Scrollbar(code_frame, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        ysb = ttk.Scrollbar(code_frame, command=self.code_text.yview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)
        self.code_text.bind("<KeyRelease>", self._update_lines)
        self.code_text.bind("<MouseWheel>", lambda e: self.root.after(10, self._update_lines))

        # ノート
        tk.Label(right, text="メモ:", bg="#0d1117", fg="#8b949e",
                 font=("Arial", 8)).pack(anchor="w")
        self.note_text = tk.Text(right, height=3, bg="#161b22", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="white")
        self.note_text.pack(fill=tk.X)

        self.status_var = tk.StringVar(value="スニペットを選択してください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#21262d", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _update_lines(self, _=None):
        self.line_canvas.delete("all")
        i = self.code_text.index("@0,0")
        while True:
            dline = self.code_text.dlineinfo(i)
            if not dline:
                break
            y = dline[1]
            line_no = int(i.split(".")[0])
            self.line_canvas.create_text(
                2, y, anchor="nw", text=str(line_no),
                fill="#484f58", font=("Courier New", 10))
            i = self.code_text.index(f"{i}+1l")

    def _load_snippets(self):
        kw   = self.search_var.get().strip().lower()
        lang = self.lang_filter_var.get()
        fav  = self.fav_var.get()

        conn = sqlite3.connect(DB_PATH)
        query = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if kw:
            query += " AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ? OR LOWER(code) LIKE ?)"
            params += [f"%{kw}%"] * 3
        if lang != "ALL":
            query += " AND language = ?"
            params.append(lang)
        if fav:
            query += " AND favorite = 1"
        query += " ORDER BY updated DESC"
        rows = conn.execute(query, params).fetchall()
        conn.close()

        self._snippets = rows
        self.tree.delete(*self.tree.get_children())
        for row_id, title, language, tags, favorite in rows:
            tag = ("fav",) if favorite else ()
            self.tree.insert("", tk.END, text=str(row_id),
                              values=("★" if favorite else "", title, language, tags),
                              tags=tag)
        self.status_var.set(f"{len(rows)} 件")

    def _on_select(self, _=None):
        sel = self.tree.selection()
        if not sel:
            return
        row_id = int(self.tree.item(sel[0], "text"))
        conn = sqlite3.connect(DB_PATH)
        row = conn.execute(
            "SELECT id, title, language, tags, code, note, favorite FROM snippets WHERE id=?",
            (row_id,)).fetchone()
        conn.close()
        if not row:
            return
        self._current_id = row[0]
        self.title_var.set(row[1])
        self.lang_var.set(row[2])
        self.tags_var.set(row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert("1.0", row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert("1.0", row[5])
        self.fav_btn_var.set(bool(row[6]))
        self._update_lines()

    def _new(self):
        self._current_id = None
        self.title_var.set("新しいスニペット")
        self.lang_var.set("Python")
        self.tags_var.set("")
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_btn_var.set(False)
        self._update_lines()

    def _save_current(self):
        title = self.title_var.get().strip()
        if not title:
            messagebox.showwarning("警告", "タイトルを入力してください")
            return
        ts = datetime.now().isoformat()
        conn = sqlite3.connect(DB_PATH)
        if self._current_id is None:
            cur = conn.execute(
                "INSERT INTO snippets (title,language,tags,code,note,favorite,created,updated)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, ts))
            self._current_id = cur.lastrowid
        else:
            conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,note=?,favorite=?,updated=? WHERE id=?",
                (title, self.lang_var.get(), self.tags_var.get(),
                 self.code_text.get("1.0", tk.END).rstrip(),
                 self.note_text.get("1.0", tk.END).rstrip(),
                 int(self.fav_btn_var.get()), ts, self._current_id))
        conn.commit()
        conn.close()
        self._load_snippets()
        self.status_var.set("保存しました")

    def _delete(self):
        if not self._current_id:
            return
        if not messagebox.askyesno("確認", "削除しますか?"):
            return
        conn = sqlite3.connect(DB_PATH)
        conn.execute("DELETE FROM snippets WHERE id=?", (self._current_id,))
        conn.commit()
        conn.close()
        self._current_id = None
        self._load_snippets()

    def _copy_code(self):
        code = self.code_text.get("1.0", tk.END).rstrip()
        self.root.clipboard_clear()
        self.root.clipboard_append(code)
        self.status_var.set("コードをクリップボードにコピーしました")


if __name__ == "__main__":
    root = tk.Tk()
    app = App089(root)
    root.mainloop()

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

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

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

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

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

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

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

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

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

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

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

    _execute()メソッドにメインロジックを実装します。

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

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

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

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

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

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。

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

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

💡 データの保存機能

処理結果をCSV・TXTファイルに保存する機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。

💡 設定ダイアログ

フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。

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

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

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

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

❌ ライブラリのインポートエラー

原因:必要なライブラリがインストールされていません。

解決法:pip install コマンドで必要なライブラリをインストールしてください。

❌ ウィンドウサイズが合わない

原因:画面解像度や表示スケールによって異なる場合があります。

解決法:root.geometry()で適切なサイズに調整してください。

9. 練習問題

アプリの理解を深めるための練習問題です。

  1. 課題1:機能拡張

    コードスニペット管理に新しい機能を1つ追加してみましょう。

  2. 課題2:UIの改善

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

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

    処理結果をファイルに保存する機能を追加しましょう。

🚀
次に挑戦するアプリ

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