中級者向け No.49

コードスニペットマネージャー

よく使うコードスニペットを言語別・タグ別に保存・検索・コピーできる管理ツール。SQLiteとクリップボード操作を学びます。

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

1. アプリ概要

よく使うコードスニペットを言語別・タグ別に保存・検索・コピーできる管理ツール。SQLiteとクリップボード操作を学びます。

このアプリは中級カテゴリに分類される実践的な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. 完全なソースコード

💡
コードのコピー方法

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

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


class App49:
    """コードスニペットマネージャー"""

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

    SAMPLE_SNIPPETS = [
        ("リスト内包表記", "Python", "comprehension,list",
         "squares = [x**2 for x in range(10)]\neven = [x for x in range(20) if x % 2 == 0]"),
        ("fetch API", "JavaScript", "fetch,async,api",
         "const res = await fetch('https://api.example.com/data');\n"
         "const data = await res.json();\nconsole.log(data);"),
        ("flexbox中央揃え", "CSS", "flexbox,center,layout",
         ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}"),
        ("SELECT with JOIN", "SQL", "join,select,query",
         "SELECT u.name, o.amount\nFROM users u\n"
         "INNER JOIN orders o ON u.id = o.user_id\nWHERE o.amount > 1000;"),
        ("デコレーター", "Python", "decorator,functools",
         "import functools\n\ndef timer(func):\n    @functools.wraps(func)\n"
         "    def wrapper(*args, **kwargs):\n        import time\n"
         "        start = time.time()\n        result = func(*args, **kwargs)\n"
         "        print(f'{func.__name__}: {time.time()-start:.3f}s')\n"
         "        return result\n    return wrapper"),
        ("Goのgoroutine", "Go", "goroutine,channel",
         "ch := make(chan int)\ngo func() {\n    ch <- 42\n}()\n"
         "value := <-ch\nfmt.Println(value)"),
    ]

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペットマネージャー")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")

        db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                               "snippets.db")
        self._conn = sqlite3.connect(db_path)
        self._init_db()

        self._current_id = None
        self._fav_only = False
        self._build_ui()
        self._load_list()

    def _init_db(self):
        self._conn.execute("""
            CREATE TABLE IF NOT EXISTS snippets (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                language TEXT DEFAULT '',
                tags TEXT DEFAULT '',
                code TEXT DEFAULT '',
                note TEXT DEFAULT '',
                created_at TEXT,
                updated_at TEXT,
                favorite INTEGER DEFAULT 0
            )
        """)
        self._conn.commit()
        count = self._conn.execute(
            "SELECT COUNT(*) FROM snippets").fetchone()[0]
        if count == 0:
            now = datetime.now().isoformat(timespec="seconds")
            for title, lang, tags, code in self.SAMPLE_SNIPPETS:
                self._conn.execute(
                    "INSERT INTO snippets "
                    "(title,language,tags,code,created_at,updated_at)"
                    " VALUES (?,?,?,?,?,?)",
                    (title, lang, tags, code, now, now))
            self._conn.commit()

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#252526", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 コードスニペットマネージャー",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="+ 新規",
                   command=self._new_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="💾 保存",
                   command=self._save_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="🗑 削除",
                   command=self._delete_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="📋 コードコピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=4)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#1e1e1e", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        tk.Label(search_f, text="🔍", bg="#1e1e1e",
                 fg="#ccc").pack(side=tk.LEFT)
        self.search_var = tk.StringVar()
        self.search_var.trace_add("write", lambda *a: self._load_list())
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT, padx=4)
        tk.Label(search_f, text="言語:", bg="#1e1e1e", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="すべて")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["すべて"] + self.LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *a: self._load_list())
        self.fav_btn = ttk.Button(search_f, text="☆ お気に入り",
                                   command=self._toggle_fav_filter)
        self.fav_btn.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="#1e1e1e")
        paned.add(left, weight=1)
        self._build_list_panel(left)

        right = tk.Frame(paned, bg="#1e1e1e")
        paned.add(right, weight=2)
        self._build_edit_panel(right)

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

    def _build_list_panel(self, parent):
        tk.Label(parent, text="スニペット一覧", bg="#1e1e1e",
                 fg="#888", font=("Arial", 9)).pack(anchor="w", padx=4)

        cols = ("title", "lang", "tags", "fav")
        self.tree = ttk.Treeview(parent, columns=cols, show="headings",
                                  selectmode="browse")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.heading("fav",   text="★")
        self.tree.column("title", width=130, anchor="w")
        self.tree.column("lang",  width=80,  anchor="w")
        self.tree.column("tags",  width=100, anchor="w")
        self.tree.column("fav",   width=28,  anchor="center")

        sb = ttk.Scrollbar(parent, command=self.tree.yview)
        self.tree.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.bind("<Double-1>", lambda e: self._copy_code())

        self.count_lbl = tk.Label(parent, text="0 件", bg="#1e1e1e",
                                   fg="#555", font=("Arial", 8))
        self.count_lbl.pack(anchor="e", padx=4)

    def _build_edit_panel(self, parent):
        # フォーム
        form = tk.Frame(parent, bg="#252526", pady=6)
        form.pack(fill=tk.X, padx=4)

        r0 = tk.Frame(form, bg="#252526")
        r0.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r0, text="タイトル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.title_entry = ttk.Entry(r0)
        self.title_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
        self.fav_var = tk.IntVar()
        tk.Checkbutton(r0, variable=self.fav_var, bg="#252526",
                       text="★ お気に入り", fg="#ffd700",
                       selectcolor="#252526",
                       activebackground="#252526",
                       font=("Arial", 9)).pack(side=tk.LEFT, padx=4)

        r1 = tk.Frame(form, bg="#252526")
        r1.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r1, text="言語:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(r1, textvariable=self.lang_var,
                     values=self.LANGUAGES, width=14).pack(side=tk.LEFT, padx=4)
        self.lang_var.trace_add("write", lambda *a: self._highlight_code())

        tk.Label(r1, text="タグ(カンマ区切り):", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 2))
        self.tags_entry = ttk.Entry(r1)
        self.tags_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)

        # コードエディタ
        editor_f = tk.Frame(parent, bg="#1e1e1e")
        editor_f.pack(fill=tk.BOTH, expand=True, padx=4, pady=(4, 0))
        tk.Label(editor_f, text="コード:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")

        code_area = tk.Frame(editor_f, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

        self.line_canvas = tk.Canvas(code_area, width=36, bg="#0d1117",
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(
            code_area, bg="#0d1117", fg="#d4d4d4",
            font=("Courier New", 10), relief=tk.FLAT,
            insertbackground="#fff", selectbackground="#264f78",
            undo=True, wrap=tk.NONE, tabs=("4m",))
        ysb = ttk.Scrollbar(code_area, orient=tk.VERTICAL,
                             command=self.code_text.yview)
        xsb = ttk.Scrollbar(editor_f, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)

        self.code_text.bind("<KeyRelease>", self._on_code_change)

        # メモ
        note_f = tk.Frame(parent, bg="#1e1e1e")
        note_f.pack(fill=tk.X, padx=4, pady=4)
        tk.Label(note_f, text="メモ:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")
        self.note_text = tk.Text(note_f, height=3, bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="#fff")
        self.note_text.pack(fill=tk.X)

    # ── DB 操作 ───────────────────────────────────────────────────

    def _load_list(self):
        query = self.search_var.get().strip().lower()
        lang_f = self.lang_filter_var.get()

        sql = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if query:
            sql += (" AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ?"
                    " OR LOWER(code) LIKE ?)")
            params += [f"%{query}%"] * 3
        if lang_f != "すべて":
            sql += " AND language = ?"
            params.append(lang_f)
        if self._fav_only:
            sql += " AND favorite = 1"
        sql += " ORDER BY updated_at DESC"

        rows = self._conn.execute(sql, params).fetchall()
        self.tree.delete(*self.tree.get_children())
        for sid, title, lang, tags, fav in rows:
            self.tree.insert("", tk.END, iid=str(sid),
                              values=(title, lang, tags,
                                      "★" if fav else ""))
        self.count_lbl.config(text=f"{len(rows)} 件")

    def _on_select(self, event=None):
        sel = self.tree.selection()
        if not sel:
            return
        sid = int(sel[0])
        row = self._conn.execute(
            "SELECT id,title,language,tags,code,note,favorite"
            " FROM snippets WHERE id=?", (sid,)).fetchone()
        if not row:
            return
        self._current_id = row[0]
        self.title_entry.delete(0, tk.END)
        self.title_entry.insert(0, row[1])
        self.lang_var.set(row[2])
        self.tags_entry.delete(0, tk.END)
        self.tags_entry.insert(0, row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert(tk.END, row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert(tk.END, row[5] or "")
        self.fav_var.set(row[6])
        self._update_line_numbers()
        self._highlight_code()
        self.status_var.set(f"ID={sid}  {row[1]}  [{row[2]}]")

    def _new_snippet(self):
        self._current_id = None
        self.title_entry.delete(0, tk.END)
        self.lang_var.set("Python")
        self.tags_entry.delete(0, tk.END)
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_var.set(0)
        self._update_line_numbers()
        self.status_var.set("新規スニペット")
        self.title_entry.focus_set()

    def _save_snippet(self):
        title = self.title_entry.get().strip()
        if not title:
            messagebox.showerror("エラー", "タイトルを入力してください")
            return
        lang = self.lang_var.get()
        tags = self.tags_entry.get().strip()
        code = self.code_text.get("1.0", tk.END).rstrip("\n")
        note = self.note_text.get("1.0", tk.END).rstrip("\n")
        fav  = self.fav_var.get()
        now  = datetime.now().isoformat(timespec="seconds")

        if self._current_id is None:
            cur = self._conn.execute(
                "INSERT INTO snippets "
                "(title,language,tags,code,note,favorite,created_at,updated_at)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, lang, tags, code, note, fav, now, now))
            self._current_id = cur.lastrowid
            self._conn.commit()
            self.status_var.set(f"新規保存: {title}")
        else:
            self._conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,"
                "note=?,favorite=?,updated_at=? WHERE id=?",
                (title, lang, tags, code, note, fav, now, self._current_id))
            self._conn.commit()
            self.status_var.set(f"更新: {title}")

        self._load_list()
        try:
            self.tree.selection_set(str(self._current_id))
        except Exception:
            pass

    def _delete_snippet(self):
        if self._current_id is None:
            return
        title = self.title_entry.get().strip() or f"ID={self._current_id}"
        if not messagebox.askyesno("削除確認", f"「{title}」を削除しますか?"):
            return
        self._conn.execute("DELETE FROM snippets WHERE id=?",
                            (self._current_id,))
        self._conn.commit()
        self._current_id = None
        self._new_snippet()
        self._load_list()
        self.status_var.set("削除しました")

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

    def _toggle_fav_filter(self):
        self._fav_only = not self._fav_only
        self.fav_btn.config(text="★ お気に入り" if self._fav_only else "☆ お気に入り")
        self._load_list()

    # ── エディタ支援 ──────────────────────────────────────────────

    def _on_code_change(self, event=None):
        self._update_line_numbers()
        self._highlight_code()

    def _update_line_numbers(self):
        self.line_canvas.delete("all")
        try:
            line_num = int(self.code_text.index("@0,0").split(".")[0])
            while True:
                dline = self.code_text.dlineinfo(f"{line_num}.0")
                if dline is None:
                    break
                self.line_canvas.create_text(
                    32, dline[1] + dline[3] // 2,
                    text=str(line_num), anchor="e",
                    fill="#858585", font=("Courier New", 10))
                line_num += 1
        except Exception:
            pass

    def _highlight_code(self):
        lang = self.lang_var.get()
        for tag in ("kw", "str_", "cmt", "num", "bi"):
            self.code_text.tag_remove(tag, "1.0", tk.END)
        self.code_text.tag_configure("kw",   foreground="#569cd6")
        self.code_text.tag_configure("str_", foreground="#ce9178")
        self.code_text.tag_configure("cmt",  foreground="#6a9955")
        self.code_text.tag_configure("num",  foreground="#b5cea8")
        self.code_text.tag_configure("bi",   foreground="#4ec9b0")

        content = self.code_text.get("1.0", tk.END)

        if lang == "Python":
            kw_pat  = (r"\b(def|class|import|from|return|if|elif|else|for|"
                       r"while|try|except|finally|with|as|pass|break|continue|"
                       r"in|not|and|or|is|None|True|False|lambda|yield|async|"
                       r"await|raise|del|global|nonlocal)\b")
            bi_pat  = (r"\b(print|len|range|type|str|int|float|list|dict|set|"
                       r"tuple|bool|open|sum|max|min|sorted|enumerate|zip|map|"
                       r"filter|super|self)\b")
            str_pat = r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"#[^\n]*"
        elif lang in ("JavaScript", "TypeScript"):
            kw_pat  = (r"\b(const|let|var|function|return|if|else|for|while|"
                       r"class|new|import|export|from|await|async|try|catch|"
                       r"finally|typeof|instanceof|of|in|true|false|null|"
                       r"undefined|this|super)\b")
            bi_pat  = (r"\b(console|document|window|Array|Object|Promise|fetch|"
                       r"JSON|Math|Date|Error|parseInt|parseFloat|String|"
                       r"Boolean|Number|Map|Set)\b")
            str_pat = r'(`[^`]*`|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"//[^\n]*"
        elif lang == "SQL":
            kw_pat  = (r"\b(SELECT|FROM|WHERE|JOIN|INNER|LEFT|RIGHT|OUTER|ON|"
                       r"GROUP|BY|ORDER|HAVING|INSERT|UPDATE|DELETE|CREATE|"
                       r"TABLE|DROP|ALTER|AS|AND|OR|NOT|IN|LIKE|BETWEEN|"
                       r"EXISTS|DISTINCT|LIMIT|OFFSET|SET|VALUES|INTO)\b")
            bi_pat  = (r"\b(COUNT|SUM|AVG|MAX|MIN|COALESCE|NULLIF|CASE|WHEN|"
                       r"THEN|ELSE|END|NOW|DATE|CAST|CONVERT)\b")
            str_pat = r"'[^']*'"
            cmt_pat = r"--[^\n]*"
        else:
            kw_pat  = None
            bi_pat  = None
            str_pat = r'"[^"\n]*"|\'[^\'\n]*\''
            cmt_pat = r"//[^\n]*|#[^\n]*"

        flags = re.IGNORECASE if lang == "SQL" else 0

        def apply(pattern, tag):
            if not pattern:
                return
            for m in re.finditer(pattern, content, flags):
                s = f"1.0 + {m.start()} chars"
                e = f"1.0 + {m.end()} chars"
                self.code_text.tag_add(tag, s, e)

        apply(r"\b\d+(\.\d+)?\b", "num")
        apply(str_pat, "str_")
        apply(cmt_pat, "cmt")
        apply(bi_pat, "bi")
        apply(kw_pat, "kw")


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

5. コード解説

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

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

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

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


class App49:
    """コードスニペットマネージャー"""

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

    SAMPLE_SNIPPETS = [
        ("リスト内包表記", "Python", "comprehension,list",
         "squares = [x**2 for x in range(10)]\neven = [x for x in range(20) if x % 2 == 0]"),
        ("fetch API", "JavaScript", "fetch,async,api",
         "const res = await fetch('https://api.example.com/data');\n"
         "const data = await res.json();\nconsole.log(data);"),
        ("flexbox中央揃え", "CSS", "flexbox,center,layout",
         ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}"),
        ("SELECT with JOIN", "SQL", "join,select,query",
         "SELECT u.name, o.amount\nFROM users u\n"
         "INNER JOIN orders o ON u.id = o.user_id\nWHERE o.amount > 1000;"),
        ("デコレーター", "Python", "decorator,functools",
         "import functools\n\ndef timer(func):\n    @functools.wraps(func)\n"
         "    def wrapper(*args, **kwargs):\n        import time\n"
         "        start = time.time()\n        result = func(*args, **kwargs)\n"
         "        print(f'{func.__name__}: {time.time()-start:.3f}s')\n"
         "        return result\n    return wrapper"),
        ("Goのgoroutine", "Go", "goroutine,channel",
         "ch := make(chan int)\ngo func() {\n    ch <- 42\n}()\n"
         "value := <-ch\nfmt.Println(value)"),
    ]

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペットマネージャー")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")

        db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                               "snippets.db")
        self._conn = sqlite3.connect(db_path)
        self._init_db()

        self._current_id = None
        self._fav_only = False
        self._build_ui()
        self._load_list()

    def _init_db(self):
        self._conn.execute("""
            CREATE TABLE IF NOT EXISTS snippets (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                language TEXT DEFAULT '',
                tags TEXT DEFAULT '',
                code TEXT DEFAULT '',
                note TEXT DEFAULT '',
                created_at TEXT,
                updated_at TEXT,
                favorite INTEGER DEFAULT 0
            )
        """)
        self._conn.commit()
        count = self._conn.execute(
            "SELECT COUNT(*) FROM snippets").fetchone()[0]
        if count == 0:
            now = datetime.now().isoformat(timespec="seconds")
            for title, lang, tags, code in self.SAMPLE_SNIPPETS:
                self._conn.execute(
                    "INSERT INTO snippets "
                    "(title,language,tags,code,created_at,updated_at)"
                    " VALUES (?,?,?,?,?,?)",
                    (title, lang, tags, code, now, now))
            self._conn.commit()

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#252526", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 コードスニペットマネージャー",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="+ 新規",
                   command=self._new_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="💾 保存",
                   command=self._save_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="🗑 削除",
                   command=self._delete_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="📋 コードコピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=4)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#1e1e1e", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        tk.Label(search_f, text="🔍", bg="#1e1e1e",
                 fg="#ccc").pack(side=tk.LEFT)
        self.search_var = tk.StringVar()
        self.search_var.trace_add("write", lambda *a: self._load_list())
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT, padx=4)
        tk.Label(search_f, text="言語:", bg="#1e1e1e", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="すべて")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["すべて"] + self.LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *a: self._load_list())
        self.fav_btn = ttk.Button(search_f, text="☆ お気に入り",
                                   command=self._toggle_fav_filter)
        self.fav_btn.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="#1e1e1e")
        paned.add(left, weight=1)
        self._build_list_panel(left)

        right = tk.Frame(paned, bg="#1e1e1e")
        paned.add(right, weight=2)
        self._build_edit_panel(right)

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

    def _build_list_panel(self, parent):
        tk.Label(parent, text="スニペット一覧", bg="#1e1e1e",
                 fg="#888", font=("Arial", 9)).pack(anchor="w", padx=4)

        cols = ("title", "lang", "tags", "fav")
        self.tree = ttk.Treeview(parent, columns=cols, show="headings",
                                  selectmode="browse")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.heading("fav",   text="★")
        self.tree.column("title", width=130, anchor="w")
        self.tree.column("lang",  width=80,  anchor="w")
        self.tree.column("tags",  width=100, anchor="w")
        self.tree.column("fav",   width=28,  anchor="center")

        sb = ttk.Scrollbar(parent, command=self.tree.yview)
        self.tree.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.bind("<Double-1>", lambda e: self._copy_code())

        self.count_lbl = tk.Label(parent, text="0 件", bg="#1e1e1e",
                                   fg="#555", font=("Arial", 8))
        self.count_lbl.pack(anchor="e", padx=4)

    def _build_edit_panel(self, parent):
        # フォーム
        form = tk.Frame(parent, bg="#252526", pady=6)
        form.pack(fill=tk.X, padx=4)

        r0 = tk.Frame(form, bg="#252526")
        r0.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r0, text="タイトル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.title_entry = ttk.Entry(r0)
        self.title_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
        self.fav_var = tk.IntVar()
        tk.Checkbutton(r0, variable=self.fav_var, bg="#252526",
                       text="★ お気に入り", fg="#ffd700",
                       selectcolor="#252526",
                       activebackground="#252526",
                       font=("Arial", 9)).pack(side=tk.LEFT, padx=4)

        r1 = tk.Frame(form, bg="#252526")
        r1.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r1, text="言語:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(r1, textvariable=self.lang_var,
                     values=self.LANGUAGES, width=14).pack(side=tk.LEFT, padx=4)
        self.lang_var.trace_add("write", lambda *a: self._highlight_code())

        tk.Label(r1, text="タグ(カンマ区切り):", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 2))
        self.tags_entry = ttk.Entry(r1)
        self.tags_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)

        # コードエディタ
        editor_f = tk.Frame(parent, bg="#1e1e1e")
        editor_f.pack(fill=tk.BOTH, expand=True, padx=4, pady=(4, 0))
        tk.Label(editor_f, text="コード:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")

        code_area = tk.Frame(editor_f, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

        self.line_canvas = tk.Canvas(code_area, width=36, bg="#0d1117",
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(
            code_area, bg="#0d1117", fg="#d4d4d4",
            font=("Courier New", 10), relief=tk.FLAT,
            insertbackground="#fff", selectbackground="#264f78",
            undo=True, wrap=tk.NONE, tabs=("4m",))
        ysb = ttk.Scrollbar(code_area, orient=tk.VERTICAL,
                             command=self.code_text.yview)
        xsb = ttk.Scrollbar(editor_f, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)

        self.code_text.bind("<KeyRelease>", self._on_code_change)

        # メモ
        note_f = tk.Frame(parent, bg="#1e1e1e")
        note_f.pack(fill=tk.X, padx=4, pady=4)
        tk.Label(note_f, text="メモ:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")
        self.note_text = tk.Text(note_f, height=3, bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="#fff")
        self.note_text.pack(fill=tk.X)

    # ── DB 操作 ───────────────────────────────────────────────────

    def _load_list(self):
        query = self.search_var.get().strip().lower()
        lang_f = self.lang_filter_var.get()

        sql = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if query:
            sql += (" AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ?"
                    " OR LOWER(code) LIKE ?)")
            params += [f"%{query}%"] * 3
        if lang_f != "すべて":
            sql += " AND language = ?"
            params.append(lang_f)
        if self._fav_only:
            sql += " AND favorite = 1"
        sql += " ORDER BY updated_at DESC"

        rows = self._conn.execute(sql, params).fetchall()
        self.tree.delete(*self.tree.get_children())
        for sid, title, lang, tags, fav in rows:
            self.tree.insert("", tk.END, iid=str(sid),
                              values=(title, lang, tags,
                                      "★" if fav else ""))
        self.count_lbl.config(text=f"{len(rows)} 件")

    def _on_select(self, event=None):
        sel = self.tree.selection()
        if not sel:
            return
        sid = int(sel[0])
        row = self._conn.execute(
            "SELECT id,title,language,tags,code,note,favorite"
            " FROM snippets WHERE id=?", (sid,)).fetchone()
        if not row:
            return
        self._current_id = row[0]
        self.title_entry.delete(0, tk.END)
        self.title_entry.insert(0, row[1])
        self.lang_var.set(row[2])
        self.tags_entry.delete(0, tk.END)
        self.tags_entry.insert(0, row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert(tk.END, row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert(tk.END, row[5] or "")
        self.fav_var.set(row[6])
        self._update_line_numbers()
        self._highlight_code()
        self.status_var.set(f"ID={sid}  {row[1]}  [{row[2]}]")

    def _new_snippet(self):
        self._current_id = None
        self.title_entry.delete(0, tk.END)
        self.lang_var.set("Python")
        self.tags_entry.delete(0, tk.END)
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_var.set(0)
        self._update_line_numbers()
        self.status_var.set("新規スニペット")
        self.title_entry.focus_set()

    def _save_snippet(self):
        title = self.title_entry.get().strip()
        if not title:
            messagebox.showerror("エラー", "タイトルを入力してください")
            return
        lang = self.lang_var.get()
        tags = self.tags_entry.get().strip()
        code = self.code_text.get("1.0", tk.END).rstrip("\n")
        note = self.note_text.get("1.0", tk.END).rstrip("\n")
        fav  = self.fav_var.get()
        now  = datetime.now().isoformat(timespec="seconds")

        if self._current_id is None:
            cur = self._conn.execute(
                "INSERT INTO snippets "
                "(title,language,tags,code,note,favorite,created_at,updated_at)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, lang, tags, code, note, fav, now, now))
            self._current_id = cur.lastrowid
            self._conn.commit()
            self.status_var.set(f"新規保存: {title}")
        else:
            self._conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,"
                "note=?,favorite=?,updated_at=? WHERE id=?",
                (title, lang, tags, code, note, fav, now, self._current_id))
            self._conn.commit()
            self.status_var.set(f"更新: {title}")

        self._load_list()
        try:
            self.tree.selection_set(str(self._current_id))
        except Exception:
            pass

    def _delete_snippet(self):
        if self._current_id is None:
            return
        title = self.title_entry.get().strip() or f"ID={self._current_id}"
        if not messagebox.askyesno("削除確認", f"「{title}」を削除しますか?"):
            return
        self._conn.execute("DELETE FROM snippets WHERE id=?",
                            (self._current_id,))
        self._conn.commit()
        self._current_id = None
        self._new_snippet()
        self._load_list()
        self.status_var.set("削除しました")

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

    def _toggle_fav_filter(self):
        self._fav_only = not self._fav_only
        self.fav_btn.config(text="★ お気に入り" if self._fav_only else "☆ お気に入り")
        self._load_list()

    # ── エディタ支援 ──────────────────────────────────────────────

    def _on_code_change(self, event=None):
        self._update_line_numbers()
        self._highlight_code()

    def _update_line_numbers(self):
        self.line_canvas.delete("all")
        try:
            line_num = int(self.code_text.index("@0,0").split(".")[0])
            while True:
                dline = self.code_text.dlineinfo(f"{line_num}.0")
                if dline is None:
                    break
                self.line_canvas.create_text(
                    32, dline[1] + dline[3] // 2,
                    text=str(line_num), anchor="e",
                    fill="#858585", font=("Courier New", 10))
                line_num += 1
        except Exception:
            pass

    def _highlight_code(self):
        lang = self.lang_var.get()
        for tag in ("kw", "str_", "cmt", "num", "bi"):
            self.code_text.tag_remove(tag, "1.0", tk.END)
        self.code_text.tag_configure("kw",   foreground="#569cd6")
        self.code_text.tag_configure("str_", foreground="#ce9178")
        self.code_text.tag_configure("cmt",  foreground="#6a9955")
        self.code_text.tag_configure("num",  foreground="#b5cea8")
        self.code_text.tag_configure("bi",   foreground="#4ec9b0")

        content = self.code_text.get("1.0", tk.END)

        if lang == "Python":
            kw_pat  = (r"\b(def|class|import|from|return|if|elif|else|for|"
                       r"while|try|except|finally|with|as|pass|break|continue|"
                       r"in|not|and|or|is|None|True|False|lambda|yield|async|"
                       r"await|raise|del|global|nonlocal)\b")
            bi_pat  = (r"\b(print|len|range|type|str|int|float|list|dict|set|"
                       r"tuple|bool|open|sum|max|min|sorted|enumerate|zip|map|"
                       r"filter|super|self)\b")
            str_pat = r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"#[^\n]*"
        elif lang in ("JavaScript", "TypeScript"):
            kw_pat  = (r"\b(const|let|var|function|return|if|else|for|while|"
                       r"class|new|import|export|from|await|async|try|catch|"
                       r"finally|typeof|instanceof|of|in|true|false|null|"
                       r"undefined|this|super)\b")
            bi_pat  = (r"\b(console|document|window|Array|Object|Promise|fetch|"
                       r"JSON|Math|Date|Error|parseInt|parseFloat|String|"
                       r"Boolean|Number|Map|Set)\b")
            str_pat = r'(`[^`]*`|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"//[^\n]*"
        elif lang == "SQL":
            kw_pat  = (r"\b(SELECT|FROM|WHERE|JOIN|INNER|LEFT|RIGHT|OUTER|ON|"
                       r"GROUP|BY|ORDER|HAVING|INSERT|UPDATE|DELETE|CREATE|"
                       r"TABLE|DROP|ALTER|AS|AND|OR|NOT|IN|LIKE|BETWEEN|"
                       r"EXISTS|DISTINCT|LIMIT|OFFSET|SET|VALUES|INTO)\b")
            bi_pat  = (r"\b(COUNT|SUM|AVG|MAX|MIN|COALESCE|NULLIF|CASE|WHEN|"
                       r"THEN|ELSE|END|NOW|DATE|CAST|CONVERT)\b")
            str_pat = r"'[^']*'"
            cmt_pat = r"--[^\n]*"
        else:
            kw_pat  = None
            bi_pat  = None
            str_pat = r'"[^"\n]*"|\'[^\'\n]*\''
            cmt_pat = r"//[^\n]*|#[^\n]*"

        flags = re.IGNORECASE if lang == "SQL" else 0

        def apply(pattern, tag):
            if not pattern:
                return
            for m in re.finditer(pattern, content, flags):
                s = f"1.0 + {m.start()} chars"
                e = f"1.0 + {m.end()} chars"
                self.code_text.tag_add(tag, s, e)

        apply(r"\b\d+(\.\d+)?\b", "num")
        apply(str_pat, "str_")
        apply(cmt_pat, "cmt")
        apply(bi_pat, "bi")
        apply(kw_pat, "kw")


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

LabelFrameによるセクション分け

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

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


class App49:
    """コードスニペットマネージャー"""

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

    SAMPLE_SNIPPETS = [
        ("リスト内包表記", "Python", "comprehension,list",
         "squares = [x**2 for x in range(10)]\neven = [x for x in range(20) if x % 2 == 0]"),
        ("fetch API", "JavaScript", "fetch,async,api",
         "const res = await fetch('https://api.example.com/data');\n"
         "const data = await res.json();\nconsole.log(data);"),
        ("flexbox中央揃え", "CSS", "flexbox,center,layout",
         ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}"),
        ("SELECT with JOIN", "SQL", "join,select,query",
         "SELECT u.name, o.amount\nFROM users u\n"
         "INNER JOIN orders o ON u.id = o.user_id\nWHERE o.amount > 1000;"),
        ("デコレーター", "Python", "decorator,functools",
         "import functools\n\ndef timer(func):\n    @functools.wraps(func)\n"
         "    def wrapper(*args, **kwargs):\n        import time\n"
         "        start = time.time()\n        result = func(*args, **kwargs)\n"
         "        print(f'{func.__name__}: {time.time()-start:.3f}s')\n"
         "        return result\n    return wrapper"),
        ("Goのgoroutine", "Go", "goroutine,channel",
         "ch := make(chan int)\ngo func() {\n    ch <- 42\n}()\n"
         "value := <-ch\nfmt.Println(value)"),
    ]

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペットマネージャー")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")

        db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                               "snippets.db")
        self._conn = sqlite3.connect(db_path)
        self._init_db()

        self._current_id = None
        self._fav_only = False
        self._build_ui()
        self._load_list()

    def _init_db(self):
        self._conn.execute("""
            CREATE TABLE IF NOT EXISTS snippets (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                language TEXT DEFAULT '',
                tags TEXT DEFAULT '',
                code TEXT DEFAULT '',
                note TEXT DEFAULT '',
                created_at TEXT,
                updated_at TEXT,
                favorite INTEGER DEFAULT 0
            )
        """)
        self._conn.commit()
        count = self._conn.execute(
            "SELECT COUNT(*) FROM snippets").fetchone()[0]
        if count == 0:
            now = datetime.now().isoformat(timespec="seconds")
            for title, lang, tags, code in self.SAMPLE_SNIPPETS:
                self._conn.execute(
                    "INSERT INTO snippets "
                    "(title,language,tags,code,created_at,updated_at)"
                    " VALUES (?,?,?,?,?,?)",
                    (title, lang, tags, code, now, now))
            self._conn.commit()

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#252526", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 コードスニペットマネージャー",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="+ 新規",
                   command=self._new_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="💾 保存",
                   command=self._save_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="🗑 削除",
                   command=self._delete_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="📋 コードコピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=4)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#1e1e1e", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        tk.Label(search_f, text="🔍", bg="#1e1e1e",
                 fg="#ccc").pack(side=tk.LEFT)
        self.search_var = tk.StringVar()
        self.search_var.trace_add("write", lambda *a: self._load_list())
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT, padx=4)
        tk.Label(search_f, text="言語:", bg="#1e1e1e", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="すべて")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["すべて"] + self.LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *a: self._load_list())
        self.fav_btn = ttk.Button(search_f, text="☆ お気に入り",
                                   command=self._toggle_fav_filter)
        self.fav_btn.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="#1e1e1e")
        paned.add(left, weight=1)
        self._build_list_panel(left)

        right = tk.Frame(paned, bg="#1e1e1e")
        paned.add(right, weight=2)
        self._build_edit_panel(right)

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

    def _build_list_panel(self, parent):
        tk.Label(parent, text="スニペット一覧", bg="#1e1e1e",
                 fg="#888", font=("Arial", 9)).pack(anchor="w", padx=4)

        cols = ("title", "lang", "tags", "fav")
        self.tree = ttk.Treeview(parent, columns=cols, show="headings",
                                  selectmode="browse")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.heading("fav",   text="★")
        self.tree.column("title", width=130, anchor="w")
        self.tree.column("lang",  width=80,  anchor="w")
        self.tree.column("tags",  width=100, anchor="w")
        self.tree.column("fav",   width=28,  anchor="center")

        sb = ttk.Scrollbar(parent, command=self.tree.yview)
        self.tree.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.bind("<Double-1>", lambda e: self._copy_code())

        self.count_lbl = tk.Label(parent, text="0 件", bg="#1e1e1e",
                                   fg="#555", font=("Arial", 8))
        self.count_lbl.pack(anchor="e", padx=4)

    def _build_edit_panel(self, parent):
        # フォーム
        form = tk.Frame(parent, bg="#252526", pady=6)
        form.pack(fill=tk.X, padx=4)

        r0 = tk.Frame(form, bg="#252526")
        r0.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r0, text="タイトル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.title_entry = ttk.Entry(r0)
        self.title_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
        self.fav_var = tk.IntVar()
        tk.Checkbutton(r0, variable=self.fav_var, bg="#252526",
                       text="★ お気に入り", fg="#ffd700",
                       selectcolor="#252526",
                       activebackground="#252526",
                       font=("Arial", 9)).pack(side=tk.LEFT, padx=4)

        r1 = tk.Frame(form, bg="#252526")
        r1.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r1, text="言語:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(r1, textvariable=self.lang_var,
                     values=self.LANGUAGES, width=14).pack(side=tk.LEFT, padx=4)
        self.lang_var.trace_add("write", lambda *a: self._highlight_code())

        tk.Label(r1, text="タグ(カンマ区切り):", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 2))
        self.tags_entry = ttk.Entry(r1)
        self.tags_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)

        # コードエディタ
        editor_f = tk.Frame(parent, bg="#1e1e1e")
        editor_f.pack(fill=tk.BOTH, expand=True, padx=4, pady=(4, 0))
        tk.Label(editor_f, text="コード:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")

        code_area = tk.Frame(editor_f, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

        self.line_canvas = tk.Canvas(code_area, width=36, bg="#0d1117",
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(
            code_area, bg="#0d1117", fg="#d4d4d4",
            font=("Courier New", 10), relief=tk.FLAT,
            insertbackground="#fff", selectbackground="#264f78",
            undo=True, wrap=tk.NONE, tabs=("4m",))
        ysb = ttk.Scrollbar(code_area, orient=tk.VERTICAL,
                             command=self.code_text.yview)
        xsb = ttk.Scrollbar(editor_f, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)

        self.code_text.bind("<KeyRelease>", self._on_code_change)

        # メモ
        note_f = tk.Frame(parent, bg="#1e1e1e")
        note_f.pack(fill=tk.X, padx=4, pady=4)
        tk.Label(note_f, text="メモ:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")
        self.note_text = tk.Text(note_f, height=3, bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="#fff")
        self.note_text.pack(fill=tk.X)

    # ── DB 操作 ───────────────────────────────────────────────────

    def _load_list(self):
        query = self.search_var.get().strip().lower()
        lang_f = self.lang_filter_var.get()

        sql = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if query:
            sql += (" AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ?"
                    " OR LOWER(code) LIKE ?)")
            params += [f"%{query}%"] * 3
        if lang_f != "すべて":
            sql += " AND language = ?"
            params.append(lang_f)
        if self._fav_only:
            sql += " AND favorite = 1"
        sql += " ORDER BY updated_at DESC"

        rows = self._conn.execute(sql, params).fetchall()
        self.tree.delete(*self.tree.get_children())
        for sid, title, lang, tags, fav in rows:
            self.tree.insert("", tk.END, iid=str(sid),
                              values=(title, lang, tags,
                                      "★" if fav else ""))
        self.count_lbl.config(text=f"{len(rows)} 件")

    def _on_select(self, event=None):
        sel = self.tree.selection()
        if not sel:
            return
        sid = int(sel[0])
        row = self._conn.execute(
            "SELECT id,title,language,tags,code,note,favorite"
            " FROM snippets WHERE id=?", (sid,)).fetchone()
        if not row:
            return
        self._current_id = row[0]
        self.title_entry.delete(0, tk.END)
        self.title_entry.insert(0, row[1])
        self.lang_var.set(row[2])
        self.tags_entry.delete(0, tk.END)
        self.tags_entry.insert(0, row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert(tk.END, row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert(tk.END, row[5] or "")
        self.fav_var.set(row[6])
        self._update_line_numbers()
        self._highlight_code()
        self.status_var.set(f"ID={sid}  {row[1]}  [{row[2]}]")

    def _new_snippet(self):
        self._current_id = None
        self.title_entry.delete(0, tk.END)
        self.lang_var.set("Python")
        self.tags_entry.delete(0, tk.END)
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_var.set(0)
        self._update_line_numbers()
        self.status_var.set("新規スニペット")
        self.title_entry.focus_set()

    def _save_snippet(self):
        title = self.title_entry.get().strip()
        if not title:
            messagebox.showerror("エラー", "タイトルを入力してください")
            return
        lang = self.lang_var.get()
        tags = self.tags_entry.get().strip()
        code = self.code_text.get("1.0", tk.END).rstrip("\n")
        note = self.note_text.get("1.0", tk.END).rstrip("\n")
        fav  = self.fav_var.get()
        now  = datetime.now().isoformat(timespec="seconds")

        if self._current_id is None:
            cur = self._conn.execute(
                "INSERT INTO snippets "
                "(title,language,tags,code,note,favorite,created_at,updated_at)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, lang, tags, code, note, fav, now, now))
            self._current_id = cur.lastrowid
            self._conn.commit()
            self.status_var.set(f"新規保存: {title}")
        else:
            self._conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,"
                "note=?,favorite=?,updated_at=? WHERE id=?",
                (title, lang, tags, code, note, fav, now, self._current_id))
            self._conn.commit()
            self.status_var.set(f"更新: {title}")

        self._load_list()
        try:
            self.tree.selection_set(str(self._current_id))
        except Exception:
            pass

    def _delete_snippet(self):
        if self._current_id is None:
            return
        title = self.title_entry.get().strip() or f"ID={self._current_id}"
        if not messagebox.askyesno("削除確認", f"「{title}」を削除しますか?"):
            return
        self._conn.execute("DELETE FROM snippets WHERE id=?",
                            (self._current_id,))
        self._conn.commit()
        self._current_id = None
        self._new_snippet()
        self._load_list()
        self.status_var.set("削除しました")

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

    def _toggle_fav_filter(self):
        self._fav_only = not self._fav_only
        self.fav_btn.config(text="★ お気に入り" if self._fav_only else "☆ お気に入り")
        self._load_list()

    # ── エディタ支援 ──────────────────────────────────────────────

    def _on_code_change(self, event=None):
        self._update_line_numbers()
        self._highlight_code()

    def _update_line_numbers(self):
        self.line_canvas.delete("all")
        try:
            line_num = int(self.code_text.index("@0,0").split(".")[0])
            while True:
                dline = self.code_text.dlineinfo(f"{line_num}.0")
                if dline is None:
                    break
                self.line_canvas.create_text(
                    32, dline[1] + dline[3] // 2,
                    text=str(line_num), anchor="e",
                    fill="#858585", font=("Courier New", 10))
                line_num += 1
        except Exception:
            pass

    def _highlight_code(self):
        lang = self.lang_var.get()
        for tag in ("kw", "str_", "cmt", "num", "bi"):
            self.code_text.tag_remove(tag, "1.0", tk.END)
        self.code_text.tag_configure("kw",   foreground="#569cd6")
        self.code_text.tag_configure("str_", foreground="#ce9178")
        self.code_text.tag_configure("cmt",  foreground="#6a9955")
        self.code_text.tag_configure("num",  foreground="#b5cea8")
        self.code_text.tag_configure("bi",   foreground="#4ec9b0")

        content = self.code_text.get("1.0", tk.END)

        if lang == "Python":
            kw_pat  = (r"\b(def|class|import|from|return|if|elif|else|for|"
                       r"while|try|except|finally|with|as|pass|break|continue|"
                       r"in|not|and|or|is|None|True|False|lambda|yield|async|"
                       r"await|raise|del|global|nonlocal)\b")
            bi_pat  = (r"\b(print|len|range|type|str|int|float|list|dict|set|"
                       r"tuple|bool|open|sum|max|min|sorted|enumerate|zip|map|"
                       r"filter|super|self)\b")
            str_pat = r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"#[^\n]*"
        elif lang in ("JavaScript", "TypeScript"):
            kw_pat  = (r"\b(const|let|var|function|return|if|else|for|while|"
                       r"class|new|import|export|from|await|async|try|catch|"
                       r"finally|typeof|instanceof|of|in|true|false|null|"
                       r"undefined|this|super)\b")
            bi_pat  = (r"\b(console|document|window|Array|Object|Promise|fetch|"
                       r"JSON|Math|Date|Error|parseInt|parseFloat|String|"
                       r"Boolean|Number|Map|Set)\b")
            str_pat = r'(`[^`]*`|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"//[^\n]*"
        elif lang == "SQL":
            kw_pat  = (r"\b(SELECT|FROM|WHERE|JOIN|INNER|LEFT|RIGHT|OUTER|ON|"
                       r"GROUP|BY|ORDER|HAVING|INSERT|UPDATE|DELETE|CREATE|"
                       r"TABLE|DROP|ALTER|AS|AND|OR|NOT|IN|LIKE|BETWEEN|"
                       r"EXISTS|DISTINCT|LIMIT|OFFSET|SET|VALUES|INTO)\b")
            bi_pat  = (r"\b(COUNT|SUM|AVG|MAX|MIN|COALESCE|NULLIF|CASE|WHEN|"
                       r"THEN|ELSE|END|NOW|DATE|CAST|CONVERT)\b")
            str_pat = r"'[^']*'"
            cmt_pat = r"--[^\n]*"
        else:
            kw_pat  = None
            bi_pat  = None
            str_pat = r'"[^"\n]*"|\'[^\'\n]*\''
            cmt_pat = r"//[^\n]*|#[^\n]*"

        flags = re.IGNORECASE if lang == "SQL" else 0

        def apply(pattern, tag):
            if not pattern:
                return
            for m in re.finditer(pattern, content, flags):
                s = f"1.0 + {m.start()} chars"
                e = f"1.0 + {m.end()} chars"
                self.code_text.tag_add(tag, s, e)

        apply(r"\b\d+(\.\d+)?\b", "num")
        apply(str_pat, "str_")
        apply(cmt_pat, "cmt")
        apply(bi_pat, "bi")
        apply(kw_pat, "kw")


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

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

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

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


class App49:
    """コードスニペットマネージャー"""

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

    SAMPLE_SNIPPETS = [
        ("リスト内包表記", "Python", "comprehension,list",
         "squares = [x**2 for x in range(10)]\neven = [x for x in range(20) if x % 2 == 0]"),
        ("fetch API", "JavaScript", "fetch,async,api",
         "const res = await fetch('https://api.example.com/data');\n"
         "const data = await res.json();\nconsole.log(data);"),
        ("flexbox中央揃え", "CSS", "flexbox,center,layout",
         ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}"),
        ("SELECT with JOIN", "SQL", "join,select,query",
         "SELECT u.name, o.amount\nFROM users u\n"
         "INNER JOIN orders o ON u.id = o.user_id\nWHERE o.amount > 1000;"),
        ("デコレーター", "Python", "decorator,functools",
         "import functools\n\ndef timer(func):\n    @functools.wraps(func)\n"
         "    def wrapper(*args, **kwargs):\n        import time\n"
         "        start = time.time()\n        result = func(*args, **kwargs)\n"
         "        print(f'{func.__name__}: {time.time()-start:.3f}s')\n"
         "        return result\n    return wrapper"),
        ("Goのgoroutine", "Go", "goroutine,channel",
         "ch := make(chan int)\ngo func() {\n    ch <- 42\n}()\n"
         "value := <-ch\nfmt.Println(value)"),
    ]

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペットマネージャー")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")

        db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                               "snippets.db")
        self._conn = sqlite3.connect(db_path)
        self._init_db()

        self._current_id = None
        self._fav_only = False
        self._build_ui()
        self._load_list()

    def _init_db(self):
        self._conn.execute("""
            CREATE TABLE IF NOT EXISTS snippets (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                language TEXT DEFAULT '',
                tags TEXT DEFAULT '',
                code TEXT DEFAULT '',
                note TEXT DEFAULT '',
                created_at TEXT,
                updated_at TEXT,
                favorite INTEGER DEFAULT 0
            )
        """)
        self._conn.commit()
        count = self._conn.execute(
            "SELECT COUNT(*) FROM snippets").fetchone()[0]
        if count == 0:
            now = datetime.now().isoformat(timespec="seconds")
            for title, lang, tags, code in self.SAMPLE_SNIPPETS:
                self._conn.execute(
                    "INSERT INTO snippets "
                    "(title,language,tags,code,created_at,updated_at)"
                    " VALUES (?,?,?,?,?,?)",
                    (title, lang, tags, code, now, now))
            self._conn.commit()

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#252526", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 コードスニペットマネージャー",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="+ 新規",
                   command=self._new_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="💾 保存",
                   command=self._save_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="🗑 削除",
                   command=self._delete_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="📋 コードコピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=4)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#1e1e1e", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        tk.Label(search_f, text="🔍", bg="#1e1e1e",
                 fg="#ccc").pack(side=tk.LEFT)
        self.search_var = tk.StringVar()
        self.search_var.trace_add("write", lambda *a: self._load_list())
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT, padx=4)
        tk.Label(search_f, text="言語:", bg="#1e1e1e", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="すべて")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["すべて"] + self.LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *a: self._load_list())
        self.fav_btn = ttk.Button(search_f, text="☆ お気に入り",
                                   command=self._toggle_fav_filter)
        self.fav_btn.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="#1e1e1e")
        paned.add(left, weight=1)
        self._build_list_panel(left)

        right = tk.Frame(paned, bg="#1e1e1e")
        paned.add(right, weight=2)
        self._build_edit_panel(right)

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

    def _build_list_panel(self, parent):
        tk.Label(parent, text="スニペット一覧", bg="#1e1e1e",
                 fg="#888", font=("Arial", 9)).pack(anchor="w", padx=4)

        cols = ("title", "lang", "tags", "fav")
        self.tree = ttk.Treeview(parent, columns=cols, show="headings",
                                  selectmode="browse")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.heading("fav",   text="★")
        self.tree.column("title", width=130, anchor="w")
        self.tree.column("lang",  width=80,  anchor="w")
        self.tree.column("tags",  width=100, anchor="w")
        self.tree.column("fav",   width=28,  anchor="center")

        sb = ttk.Scrollbar(parent, command=self.tree.yview)
        self.tree.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.bind("<Double-1>", lambda e: self._copy_code())

        self.count_lbl = tk.Label(parent, text="0 件", bg="#1e1e1e",
                                   fg="#555", font=("Arial", 8))
        self.count_lbl.pack(anchor="e", padx=4)

    def _build_edit_panel(self, parent):
        # フォーム
        form = tk.Frame(parent, bg="#252526", pady=6)
        form.pack(fill=tk.X, padx=4)

        r0 = tk.Frame(form, bg="#252526")
        r0.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r0, text="タイトル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.title_entry = ttk.Entry(r0)
        self.title_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
        self.fav_var = tk.IntVar()
        tk.Checkbutton(r0, variable=self.fav_var, bg="#252526",
                       text="★ お気に入り", fg="#ffd700",
                       selectcolor="#252526",
                       activebackground="#252526",
                       font=("Arial", 9)).pack(side=tk.LEFT, padx=4)

        r1 = tk.Frame(form, bg="#252526")
        r1.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r1, text="言語:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(r1, textvariable=self.lang_var,
                     values=self.LANGUAGES, width=14).pack(side=tk.LEFT, padx=4)
        self.lang_var.trace_add("write", lambda *a: self._highlight_code())

        tk.Label(r1, text="タグ(カンマ区切り):", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 2))
        self.tags_entry = ttk.Entry(r1)
        self.tags_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)

        # コードエディタ
        editor_f = tk.Frame(parent, bg="#1e1e1e")
        editor_f.pack(fill=tk.BOTH, expand=True, padx=4, pady=(4, 0))
        tk.Label(editor_f, text="コード:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")

        code_area = tk.Frame(editor_f, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

        self.line_canvas = tk.Canvas(code_area, width=36, bg="#0d1117",
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(
            code_area, bg="#0d1117", fg="#d4d4d4",
            font=("Courier New", 10), relief=tk.FLAT,
            insertbackground="#fff", selectbackground="#264f78",
            undo=True, wrap=tk.NONE, tabs=("4m",))
        ysb = ttk.Scrollbar(code_area, orient=tk.VERTICAL,
                             command=self.code_text.yview)
        xsb = ttk.Scrollbar(editor_f, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)

        self.code_text.bind("<KeyRelease>", self._on_code_change)

        # メモ
        note_f = tk.Frame(parent, bg="#1e1e1e")
        note_f.pack(fill=tk.X, padx=4, pady=4)
        tk.Label(note_f, text="メモ:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")
        self.note_text = tk.Text(note_f, height=3, bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="#fff")
        self.note_text.pack(fill=tk.X)

    # ── DB 操作 ───────────────────────────────────────────────────

    def _load_list(self):
        query = self.search_var.get().strip().lower()
        lang_f = self.lang_filter_var.get()

        sql = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if query:
            sql += (" AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ?"
                    " OR LOWER(code) LIKE ?)")
            params += [f"%{query}%"] * 3
        if lang_f != "すべて":
            sql += " AND language = ?"
            params.append(lang_f)
        if self._fav_only:
            sql += " AND favorite = 1"
        sql += " ORDER BY updated_at DESC"

        rows = self._conn.execute(sql, params).fetchall()
        self.tree.delete(*self.tree.get_children())
        for sid, title, lang, tags, fav in rows:
            self.tree.insert("", tk.END, iid=str(sid),
                              values=(title, lang, tags,
                                      "★" if fav else ""))
        self.count_lbl.config(text=f"{len(rows)} 件")

    def _on_select(self, event=None):
        sel = self.tree.selection()
        if not sel:
            return
        sid = int(sel[0])
        row = self._conn.execute(
            "SELECT id,title,language,tags,code,note,favorite"
            " FROM snippets WHERE id=?", (sid,)).fetchone()
        if not row:
            return
        self._current_id = row[0]
        self.title_entry.delete(0, tk.END)
        self.title_entry.insert(0, row[1])
        self.lang_var.set(row[2])
        self.tags_entry.delete(0, tk.END)
        self.tags_entry.insert(0, row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert(tk.END, row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert(tk.END, row[5] or "")
        self.fav_var.set(row[6])
        self._update_line_numbers()
        self._highlight_code()
        self.status_var.set(f"ID={sid}  {row[1]}  [{row[2]}]")

    def _new_snippet(self):
        self._current_id = None
        self.title_entry.delete(0, tk.END)
        self.lang_var.set("Python")
        self.tags_entry.delete(0, tk.END)
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_var.set(0)
        self._update_line_numbers()
        self.status_var.set("新規スニペット")
        self.title_entry.focus_set()

    def _save_snippet(self):
        title = self.title_entry.get().strip()
        if not title:
            messagebox.showerror("エラー", "タイトルを入力してください")
            return
        lang = self.lang_var.get()
        tags = self.tags_entry.get().strip()
        code = self.code_text.get("1.0", tk.END).rstrip("\n")
        note = self.note_text.get("1.0", tk.END).rstrip("\n")
        fav  = self.fav_var.get()
        now  = datetime.now().isoformat(timespec="seconds")

        if self._current_id is None:
            cur = self._conn.execute(
                "INSERT INTO snippets "
                "(title,language,tags,code,note,favorite,created_at,updated_at)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, lang, tags, code, note, fav, now, now))
            self._current_id = cur.lastrowid
            self._conn.commit()
            self.status_var.set(f"新規保存: {title}")
        else:
            self._conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,"
                "note=?,favorite=?,updated_at=? WHERE id=?",
                (title, lang, tags, code, note, fav, now, self._current_id))
            self._conn.commit()
            self.status_var.set(f"更新: {title}")

        self._load_list()
        try:
            self.tree.selection_set(str(self._current_id))
        except Exception:
            pass

    def _delete_snippet(self):
        if self._current_id is None:
            return
        title = self.title_entry.get().strip() or f"ID={self._current_id}"
        if not messagebox.askyesno("削除確認", f"「{title}」を削除しますか?"):
            return
        self._conn.execute("DELETE FROM snippets WHERE id=?",
                            (self._current_id,))
        self._conn.commit()
        self._current_id = None
        self._new_snippet()
        self._load_list()
        self.status_var.set("削除しました")

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

    def _toggle_fav_filter(self):
        self._fav_only = not self._fav_only
        self.fav_btn.config(text="★ お気に入り" if self._fav_only else "☆ お気に入り")
        self._load_list()

    # ── エディタ支援 ──────────────────────────────────────────────

    def _on_code_change(self, event=None):
        self._update_line_numbers()
        self._highlight_code()

    def _update_line_numbers(self):
        self.line_canvas.delete("all")
        try:
            line_num = int(self.code_text.index("@0,0").split(".")[0])
            while True:
                dline = self.code_text.dlineinfo(f"{line_num}.0")
                if dline is None:
                    break
                self.line_canvas.create_text(
                    32, dline[1] + dline[3] // 2,
                    text=str(line_num), anchor="e",
                    fill="#858585", font=("Courier New", 10))
                line_num += 1
        except Exception:
            pass

    def _highlight_code(self):
        lang = self.lang_var.get()
        for tag in ("kw", "str_", "cmt", "num", "bi"):
            self.code_text.tag_remove(tag, "1.0", tk.END)
        self.code_text.tag_configure("kw",   foreground="#569cd6")
        self.code_text.tag_configure("str_", foreground="#ce9178")
        self.code_text.tag_configure("cmt",  foreground="#6a9955")
        self.code_text.tag_configure("num",  foreground="#b5cea8")
        self.code_text.tag_configure("bi",   foreground="#4ec9b0")

        content = self.code_text.get("1.0", tk.END)

        if lang == "Python":
            kw_pat  = (r"\b(def|class|import|from|return|if|elif|else|for|"
                       r"while|try|except|finally|with|as|pass|break|continue|"
                       r"in|not|and|or|is|None|True|False|lambda|yield|async|"
                       r"await|raise|del|global|nonlocal)\b")
            bi_pat  = (r"\b(print|len|range|type|str|int|float|list|dict|set|"
                       r"tuple|bool|open|sum|max|min|sorted|enumerate|zip|map|"
                       r"filter|super|self)\b")
            str_pat = r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"#[^\n]*"
        elif lang in ("JavaScript", "TypeScript"):
            kw_pat  = (r"\b(const|let|var|function|return|if|else|for|while|"
                       r"class|new|import|export|from|await|async|try|catch|"
                       r"finally|typeof|instanceof|of|in|true|false|null|"
                       r"undefined|this|super)\b")
            bi_pat  = (r"\b(console|document|window|Array|Object|Promise|fetch|"
                       r"JSON|Math|Date|Error|parseInt|parseFloat|String|"
                       r"Boolean|Number|Map|Set)\b")
            str_pat = r'(`[^`]*`|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"//[^\n]*"
        elif lang == "SQL":
            kw_pat  = (r"\b(SELECT|FROM|WHERE|JOIN|INNER|LEFT|RIGHT|OUTER|ON|"
                       r"GROUP|BY|ORDER|HAVING|INSERT|UPDATE|DELETE|CREATE|"
                       r"TABLE|DROP|ALTER|AS|AND|OR|NOT|IN|LIKE|BETWEEN|"
                       r"EXISTS|DISTINCT|LIMIT|OFFSET|SET|VALUES|INTO)\b")
            bi_pat  = (r"\b(COUNT|SUM|AVG|MAX|MIN|COALESCE|NULLIF|CASE|WHEN|"
                       r"THEN|ELSE|END|NOW|DATE|CAST|CONVERT)\b")
            str_pat = r"'[^']*'"
            cmt_pat = r"--[^\n]*"
        else:
            kw_pat  = None
            bi_pat  = None
            str_pat = r'"[^"\n]*"|\'[^\'\n]*\''
            cmt_pat = r"//[^\n]*|#[^\n]*"

        flags = re.IGNORECASE if lang == "SQL" else 0

        def apply(pattern, tag):
            if not pattern:
                return
            for m in re.finditer(pattern, content, flags):
                s = f"1.0 + {m.start()} chars"
                e = f"1.0 + {m.end()} chars"
                self.code_text.tag_add(tag, s, e)

        apply(r"\b\d+(\.\d+)?\b", "num")
        apply(str_pat, "str_")
        apply(cmt_pat, "cmt")
        apply(bi_pat, "bi")
        apply(kw_pat, "kw")


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

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

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

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


class App49:
    """コードスニペットマネージャー"""

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

    SAMPLE_SNIPPETS = [
        ("リスト内包表記", "Python", "comprehension,list",
         "squares = [x**2 for x in range(10)]\neven = [x for x in range(20) if x % 2 == 0]"),
        ("fetch API", "JavaScript", "fetch,async,api",
         "const res = await fetch('https://api.example.com/data');\n"
         "const data = await res.json();\nconsole.log(data);"),
        ("flexbox中央揃え", "CSS", "flexbox,center,layout",
         ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}"),
        ("SELECT with JOIN", "SQL", "join,select,query",
         "SELECT u.name, o.amount\nFROM users u\n"
         "INNER JOIN orders o ON u.id = o.user_id\nWHERE o.amount > 1000;"),
        ("デコレーター", "Python", "decorator,functools",
         "import functools\n\ndef timer(func):\n    @functools.wraps(func)\n"
         "    def wrapper(*args, **kwargs):\n        import time\n"
         "        start = time.time()\n        result = func(*args, **kwargs)\n"
         "        print(f'{func.__name__}: {time.time()-start:.3f}s')\n"
         "        return result\n    return wrapper"),
        ("Goのgoroutine", "Go", "goroutine,channel",
         "ch := make(chan int)\ngo func() {\n    ch <- 42\n}()\n"
         "value := <-ch\nfmt.Println(value)"),
    ]

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペットマネージャー")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")

        db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                               "snippets.db")
        self._conn = sqlite3.connect(db_path)
        self._init_db()

        self._current_id = None
        self._fav_only = False
        self._build_ui()
        self._load_list()

    def _init_db(self):
        self._conn.execute("""
            CREATE TABLE IF NOT EXISTS snippets (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                language TEXT DEFAULT '',
                tags TEXT DEFAULT '',
                code TEXT DEFAULT '',
                note TEXT DEFAULT '',
                created_at TEXT,
                updated_at TEXT,
                favorite INTEGER DEFAULT 0
            )
        """)
        self._conn.commit()
        count = self._conn.execute(
            "SELECT COUNT(*) FROM snippets").fetchone()[0]
        if count == 0:
            now = datetime.now().isoformat(timespec="seconds")
            for title, lang, tags, code in self.SAMPLE_SNIPPETS:
                self._conn.execute(
                    "INSERT INTO snippets "
                    "(title,language,tags,code,created_at,updated_at)"
                    " VALUES (?,?,?,?,?,?)",
                    (title, lang, tags, code, now, now))
            self._conn.commit()

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#252526", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 コードスニペットマネージャー",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="+ 新規",
                   command=self._new_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="💾 保存",
                   command=self._save_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="🗑 削除",
                   command=self._delete_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="📋 コードコピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=4)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#1e1e1e", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        tk.Label(search_f, text="🔍", bg="#1e1e1e",
                 fg="#ccc").pack(side=tk.LEFT)
        self.search_var = tk.StringVar()
        self.search_var.trace_add("write", lambda *a: self._load_list())
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT, padx=4)
        tk.Label(search_f, text="言語:", bg="#1e1e1e", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="すべて")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["すべて"] + self.LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *a: self._load_list())
        self.fav_btn = ttk.Button(search_f, text="☆ お気に入り",
                                   command=self._toggle_fav_filter)
        self.fav_btn.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="#1e1e1e")
        paned.add(left, weight=1)
        self._build_list_panel(left)

        right = tk.Frame(paned, bg="#1e1e1e")
        paned.add(right, weight=2)
        self._build_edit_panel(right)

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

    def _build_list_panel(self, parent):
        tk.Label(parent, text="スニペット一覧", bg="#1e1e1e",
                 fg="#888", font=("Arial", 9)).pack(anchor="w", padx=4)

        cols = ("title", "lang", "tags", "fav")
        self.tree = ttk.Treeview(parent, columns=cols, show="headings",
                                  selectmode="browse")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.heading("fav",   text="★")
        self.tree.column("title", width=130, anchor="w")
        self.tree.column("lang",  width=80,  anchor="w")
        self.tree.column("tags",  width=100, anchor="w")
        self.tree.column("fav",   width=28,  anchor="center")

        sb = ttk.Scrollbar(parent, command=self.tree.yview)
        self.tree.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.bind("<Double-1>", lambda e: self._copy_code())

        self.count_lbl = tk.Label(parent, text="0 件", bg="#1e1e1e",
                                   fg="#555", font=("Arial", 8))
        self.count_lbl.pack(anchor="e", padx=4)

    def _build_edit_panel(self, parent):
        # フォーム
        form = tk.Frame(parent, bg="#252526", pady=6)
        form.pack(fill=tk.X, padx=4)

        r0 = tk.Frame(form, bg="#252526")
        r0.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r0, text="タイトル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.title_entry = ttk.Entry(r0)
        self.title_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
        self.fav_var = tk.IntVar()
        tk.Checkbutton(r0, variable=self.fav_var, bg="#252526",
                       text="★ お気に入り", fg="#ffd700",
                       selectcolor="#252526",
                       activebackground="#252526",
                       font=("Arial", 9)).pack(side=tk.LEFT, padx=4)

        r1 = tk.Frame(form, bg="#252526")
        r1.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r1, text="言語:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(r1, textvariable=self.lang_var,
                     values=self.LANGUAGES, width=14).pack(side=tk.LEFT, padx=4)
        self.lang_var.trace_add("write", lambda *a: self._highlight_code())

        tk.Label(r1, text="タグ(カンマ区切り):", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 2))
        self.tags_entry = ttk.Entry(r1)
        self.tags_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)

        # コードエディタ
        editor_f = tk.Frame(parent, bg="#1e1e1e")
        editor_f.pack(fill=tk.BOTH, expand=True, padx=4, pady=(4, 0))
        tk.Label(editor_f, text="コード:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")

        code_area = tk.Frame(editor_f, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

        self.line_canvas = tk.Canvas(code_area, width=36, bg="#0d1117",
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(
            code_area, bg="#0d1117", fg="#d4d4d4",
            font=("Courier New", 10), relief=tk.FLAT,
            insertbackground="#fff", selectbackground="#264f78",
            undo=True, wrap=tk.NONE, tabs=("4m",))
        ysb = ttk.Scrollbar(code_area, orient=tk.VERTICAL,
                             command=self.code_text.yview)
        xsb = ttk.Scrollbar(editor_f, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)

        self.code_text.bind("<KeyRelease>", self._on_code_change)

        # メモ
        note_f = tk.Frame(parent, bg="#1e1e1e")
        note_f.pack(fill=tk.X, padx=4, pady=4)
        tk.Label(note_f, text="メモ:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")
        self.note_text = tk.Text(note_f, height=3, bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="#fff")
        self.note_text.pack(fill=tk.X)

    # ── DB 操作 ───────────────────────────────────────────────────

    def _load_list(self):
        query = self.search_var.get().strip().lower()
        lang_f = self.lang_filter_var.get()

        sql = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if query:
            sql += (" AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ?"
                    " OR LOWER(code) LIKE ?)")
            params += [f"%{query}%"] * 3
        if lang_f != "すべて":
            sql += " AND language = ?"
            params.append(lang_f)
        if self._fav_only:
            sql += " AND favorite = 1"
        sql += " ORDER BY updated_at DESC"

        rows = self._conn.execute(sql, params).fetchall()
        self.tree.delete(*self.tree.get_children())
        for sid, title, lang, tags, fav in rows:
            self.tree.insert("", tk.END, iid=str(sid),
                              values=(title, lang, tags,
                                      "★" if fav else ""))
        self.count_lbl.config(text=f"{len(rows)} 件")

    def _on_select(self, event=None):
        sel = self.tree.selection()
        if not sel:
            return
        sid = int(sel[0])
        row = self._conn.execute(
            "SELECT id,title,language,tags,code,note,favorite"
            " FROM snippets WHERE id=?", (sid,)).fetchone()
        if not row:
            return
        self._current_id = row[0]
        self.title_entry.delete(0, tk.END)
        self.title_entry.insert(0, row[1])
        self.lang_var.set(row[2])
        self.tags_entry.delete(0, tk.END)
        self.tags_entry.insert(0, row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert(tk.END, row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert(tk.END, row[5] or "")
        self.fav_var.set(row[6])
        self._update_line_numbers()
        self._highlight_code()
        self.status_var.set(f"ID={sid}  {row[1]}  [{row[2]}]")

    def _new_snippet(self):
        self._current_id = None
        self.title_entry.delete(0, tk.END)
        self.lang_var.set("Python")
        self.tags_entry.delete(0, tk.END)
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_var.set(0)
        self._update_line_numbers()
        self.status_var.set("新規スニペット")
        self.title_entry.focus_set()

    def _save_snippet(self):
        title = self.title_entry.get().strip()
        if not title:
            messagebox.showerror("エラー", "タイトルを入力してください")
            return
        lang = self.lang_var.get()
        tags = self.tags_entry.get().strip()
        code = self.code_text.get("1.0", tk.END).rstrip("\n")
        note = self.note_text.get("1.0", tk.END).rstrip("\n")
        fav  = self.fav_var.get()
        now  = datetime.now().isoformat(timespec="seconds")

        if self._current_id is None:
            cur = self._conn.execute(
                "INSERT INTO snippets "
                "(title,language,tags,code,note,favorite,created_at,updated_at)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, lang, tags, code, note, fav, now, now))
            self._current_id = cur.lastrowid
            self._conn.commit()
            self.status_var.set(f"新規保存: {title}")
        else:
            self._conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,"
                "note=?,favorite=?,updated_at=? WHERE id=?",
                (title, lang, tags, code, note, fav, now, self._current_id))
            self._conn.commit()
            self.status_var.set(f"更新: {title}")

        self._load_list()
        try:
            self.tree.selection_set(str(self._current_id))
        except Exception:
            pass

    def _delete_snippet(self):
        if self._current_id is None:
            return
        title = self.title_entry.get().strip() or f"ID={self._current_id}"
        if not messagebox.askyesno("削除確認", f"「{title}」を削除しますか?"):
            return
        self._conn.execute("DELETE FROM snippets WHERE id=?",
                            (self._current_id,))
        self._conn.commit()
        self._current_id = None
        self._new_snippet()
        self._load_list()
        self.status_var.set("削除しました")

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

    def _toggle_fav_filter(self):
        self._fav_only = not self._fav_only
        self.fav_btn.config(text="★ お気に入り" if self._fav_only else "☆ お気に入り")
        self._load_list()

    # ── エディタ支援 ──────────────────────────────────────────────

    def _on_code_change(self, event=None):
        self._update_line_numbers()
        self._highlight_code()

    def _update_line_numbers(self):
        self.line_canvas.delete("all")
        try:
            line_num = int(self.code_text.index("@0,0").split(".")[0])
            while True:
                dline = self.code_text.dlineinfo(f"{line_num}.0")
                if dline is None:
                    break
                self.line_canvas.create_text(
                    32, dline[1] + dline[3] // 2,
                    text=str(line_num), anchor="e",
                    fill="#858585", font=("Courier New", 10))
                line_num += 1
        except Exception:
            pass

    def _highlight_code(self):
        lang = self.lang_var.get()
        for tag in ("kw", "str_", "cmt", "num", "bi"):
            self.code_text.tag_remove(tag, "1.0", tk.END)
        self.code_text.tag_configure("kw",   foreground="#569cd6")
        self.code_text.tag_configure("str_", foreground="#ce9178")
        self.code_text.tag_configure("cmt",  foreground="#6a9955")
        self.code_text.tag_configure("num",  foreground="#b5cea8")
        self.code_text.tag_configure("bi",   foreground="#4ec9b0")

        content = self.code_text.get("1.0", tk.END)

        if lang == "Python":
            kw_pat  = (r"\b(def|class|import|from|return|if|elif|else|for|"
                       r"while|try|except|finally|with|as|pass|break|continue|"
                       r"in|not|and|or|is|None|True|False|lambda|yield|async|"
                       r"await|raise|del|global|nonlocal)\b")
            bi_pat  = (r"\b(print|len|range|type|str|int|float|list|dict|set|"
                       r"tuple|bool|open|sum|max|min|sorted|enumerate|zip|map|"
                       r"filter|super|self)\b")
            str_pat = r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"#[^\n]*"
        elif lang in ("JavaScript", "TypeScript"):
            kw_pat  = (r"\b(const|let|var|function|return|if|else|for|while|"
                       r"class|new|import|export|from|await|async|try|catch|"
                       r"finally|typeof|instanceof|of|in|true|false|null|"
                       r"undefined|this|super)\b")
            bi_pat  = (r"\b(console|document|window|Array|Object|Promise|fetch|"
                       r"JSON|Math|Date|Error|parseInt|parseFloat|String|"
                       r"Boolean|Number|Map|Set)\b")
            str_pat = r'(`[^`]*`|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"//[^\n]*"
        elif lang == "SQL":
            kw_pat  = (r"\b(SELECT|FROM|WHERE|JOIN|INNER|LEFT|RIGHT|OUTER|ON|"
                       r"GROUP|BY|ORDER|HAVING|INSERT|UPDATE|DELETE|CREATE|"
                       r"TABLE|DROP|ALTER|AS|AND|OR|NOT|IN|LIKE|BETWEEN|"
                       r"EXISTS|DISTINCT|LIMIT|OFFSET|SET|VALUES|INTO)\b")
            bi_pat  = (r"\b(COUNT|SUM|AVG|MAX|MIN|COALESCE|NULLIF|CASE|WHEN|"
                       r"THEN|ELSE|END|NOW|DATE|CAST|CONVERT)\b")
            str_pat = r"'[^']*'"
            cmt_pat = r"--[^\n]*"
        else:
            kw_pat  = None
            bi_pat  = None
            str_pat = r'"[^"\n]*"|\'[^\'\n]*\''
            cmt_pat = r"//[^\n]*|#[^\n]*"

        flags = re.IGNORECASE if lang == "SQL" else 0

        def apply(pattern, tag):
            if not pattern:
                return
            for m in re.finditer(pattern, content, flags):
                s = f"1.0 + {m.start()} chars"
                e = f"1.0 + {m.end()} chars"
                self.code_text.tag_add(tag, s, e)

        apply(r"\b\d+(\.\d+)?\b", "num")
        apply(str_pat, "str_")
        apply(cmt_pat, "cmt")
        apply(bi_pat, "bi")
        apply(kw_pat, "kw")


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

例外処理とmessagebox

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

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


class App49:
    """コードスニペットマネージャー"""

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

    SAMPLE_SNIPPETS = [
        ("リスト内包表記", "Python", "comprehension,list",
         "squares = [x**2 for x in range(10)]\neven = [x for x in range(20) if x % 2 == 0]"),
        ("fetch API", "JavaScript", "fetch,async,api",
         "const res = await fetch('https://api.example.com/data');\n"
         "const data = await res.json();\nconsole.log(data);"),
        ("flexbox中央揃え", "CSS", "flexbox,center,layout",
         ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}"),
        ("SELECT with JOIN", "SQL", "join,select,query",
         "SELECT u.name, o.amount\nFROM users u\n"
         "INNER JOIN orders o ON u.id = o.user_id\nWHERE o.amount > 1000;"),
        ("デコレーター", "Python", "decorator,functools",
         "import functools\n\ndef timer(func):\n    @functools.wraps(func)\n"
         "    def wrapper(*args, **kwargs):\n        import time\n"
         "        start = time.time()\n        result = func(*args, **kwargs)\n"
         "        print(f'{func.__name__}: {time.time()-start:.3f}s')\n"
         "        return result\n    return wrapper"),
        ("Goのgoroutine", "Go", "goroutine,channel",
         "ch := make(chan int)\ngo func() {\n    ch <- 42\n}()\n"
         "value := <-ch\nfmt.Println(value)"),
    ]

    def __init__(self, root):
        self.root = root
        self.root.title("コードスニペットマネージャー")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")

        db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                               "snippets.db")
        self._conn = sqlite3.connect(db_path)
        self._init_db()

        self._current_id = None
        self._fav_only = False
        self._build_ui()
        self._load_list()

    def _init_db(self):
        self._conn.execute("""
            CREATE TABLE IF NOT EXISTS snippets (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                language TEXT DEFAULT '',
                tags TEXT DEFAULT '',
                code TEXT DEFAULT '',
                note TEXT DEFAULT '',
                created_at TEXT,
                updated_at TEXT,
                favorite INTEGER DEFAULT 0
            )
        """)
        self._conn.commit()
        count = self._conn.execute(
            "SELECT COUNT(*) FROM snippets").fetchone()[0]
        if count == 0:
            now = datetime.now().isoformat(timespec="seconds")
            for title, lang, tags, code in self.SAMPLE_SNIPPETS:
                self._conn.execute(
                    "INSERT INTO snippets "
                    "(title,language,tags,code,created_at,updated_at)"
                    " VALUES (?,?,?,?,?,?)",
                    (title, lang, tags, code, now, now))
            self._conn.commit()

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#252526", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 コードスニペットマネージャー",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="+ 新規",
                   command=self._new_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="💾 保存",
                   command=self._save_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="🗑 削除",
                   command=self._delete_snippet).pack(side=tk.LEFT, padx=4)
        ttk.Button(header, text="📋 コードコピー",
                   command=self._copy_code).pack(side=tk.LEFT, padx=4)

        # 検索バー
        search_f = tk.Frame(self.root, bg="#1e1e1e", pady=4)
        search_f.pack(fill=tk.X, padx=8)
        tk.Label(search_f, text="🔍", bg="#1e1e1e",
                 fg="#ccc").pack(side=tk.LEFT)
        self.search_var = tk.StringVar()
        self.search_var.trace_add("write", lambda *a: self._load_list())
        ttk.Entry(search_f, textvariable=self.search_var,
                  width=28).pack(side=tk.LEFT, padx=4)
        tk.Label(search_f, text="言語:", bg="#1e1e1e", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.lang_filter_var = tk.StringVar(value="すべて")
        ttk.Combobox(search_f, textvariable=self.lang_filter_var,
                     values=["すべて"] + self.LANGUAGES,
                     state="readonly", width=12).pack(side=tk.LEFT)
        self.lang_filter_var.trace_add("write", lambda *a: self._load_list())
        self.fav_btn = ttk.Button(search_f, text="☆ お気に入り",
                                   command=self._toggle_fav_filter)
        self.fav_btn.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="#1e1e1e")
        paned.add(left, weight=1)
        self._build_list_panel(left)

        right = tk.Frame(paned, bg="#1e1e1e")
        paned.add(right, weight=2)
        self._build_edit_panel(right)

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

    def _build_list_panel(self, parent):
        tk.Label(parent, text="スニペット一覧", bg="#1e1e1e",
                 fg="#888", font=("Arial", 9)).pack(anchor="w", padx=4)

        cols = ("title", "lang", "tags", "fav")
        self.tree = ttk.Treeview(parent, columns=cols, show="headings",
                                  selectmode="browse")
        self.tree.heading("title", text="タイトル")
        self.tree.heading("lang",  text="言語")
        self.tree.heading("tags",  text="タグ")
        self.tree.heading("fav",   text="★")
        self.tree.column("title", width=130, anchor="w")
        self.tree.column("lang",  width=80,  anchor="w")
        self.tree.column("tags",  width=100, anchor="w")
        self.tree.column("fav",   width=28,  anchor="center")

        sb = ttk.Scrollbar(parent, command=self.tree.yview)
        self.tree.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        self.tree.bind("<Double-1>", lambda e: self._copy_code())

        self.count_lbl = tk.Label(parent, text="0 件", bg="#1e1e1e",
                                   fg="#555", font=("Arial", 8))
        self.count_lbl.pack(anchor="e", padx=4)

    def _build_edit_panel(self, parent):
        # フォーム
        form = tk.Frame(parent, bg="#252526", pady=6)
        form.pack(fill=tk.X, padx=4)

        r0 = tk.Frame(form, bg="#252526")
        r0.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r0, text="タイトル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.title_entry = ttk.Entry(r0)
        self.title_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
        self.fav_var = tk.IntVar()
        tk.Checkbutton(r0, variable=self.fav_var, bg="#252526",
                       text="★ お気に入り", fg="#ffd700",
                       selectcolor="#252526",
                       activebackground="#252526",
                       font=("Arial", 9)).pack(side=tk.LEFT, padx=4)

        r1 = tk.Frame(form, bg="#252526")
        r1.pack(fill=tk.X, padx=6, pady=2)
        tk.Label(r1, text="言語:", bg="#252526", fg="#ccc",
                 font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
        self.lang_var = tk.StringVar(value="Python")
        ttk.Combobox(r1, textvariable=self.lang_var,
                     values=self.LANGUAGES, width=14).pack(side=tk.LEFT, padx=4)
        self.lang_var.trace_add("write", lambda *a: self._highlight_code())

        tk.Label(r1, text="タグ(カンマ区切り):", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 2))
        self.tags_entry = ttk.Entry(r1)
        self.tags_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)

        # コードエディタ
        editor_f = tk.Frame(parent, bg="#1e1e1e")
        editor_f.pack(fill=tk.BOTH, expand=True, padx=4, pady=(4, 0))
        tk.Label(editor_f, text="コード:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")

        code_area = tk.Frame(editor_f, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

        self.line_canvas = tk.Canvas(code_area, width=36, bg="#0d1117",
                                      highlightthickness=0)
        self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)

        self.code_text = tk.Text(
            code_area, bg="#0d1117", fg="#d4d4d4",
            font=("Courier New", 10), relief=tk.FLAT,
            insertbackground="#fff", selectbackground="#264f78",
            undo=True, wrap=tk.NONE, tabs=("4m",))
        ysb = ttk.Scrollbar(code_area, orient=tk.VERTICAL,
                             command=self.code_text.yview)
        xsb = ttk.Scrollbar(editor_f, orient=tk.HORIZONTAL,
                             command=self.code_text.xview)
        self.code_text.configure(xscrollcommand=xsb.set,
                                  yscrollcommand=ysb.set)
        ysb.pack(side=tk.RIGHT, fill=tk.Y)
        self.code_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        xsb.pack(fill=tk.X)

        self.code_text.bind("<KeyRelease>", self._on_code_change)

        # メモ
        note_f = tk.Frame(parent, bg="#1e1e1e")
        note_f.pack(fill=tk.X, padx=4, pady=4)
        tk.Label(note_f, text="メモ:", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")
        self.note_text = tk.Text(note_f, height=3, bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), relief=tk.FLAT,
                                  insertbackground="#fff")
        self.note_text.pack(fill=tk.X)

    # ── DB 操作 ───────────────────────────────────────────────────

    def _load_list(self):
        query = self.search_var.get().strip().lower()
        lang_f = self.lang_filter_var.get()

        sql = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
        params = []
        if query:
            sql += (" AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ?"
                    " OR LOWER(code) LIKE ?)")
            params += [f"%{query}%"] * 3
        if lang_f != "すべて":
            sql += " AND language = ?"
            params.append(lang_f)
        if self._fav_only:
            sql += " AND favorite = 1"
        sql += " ORDER BY updated_at DESC"

        rows = self._conn.execute(sql, params).fetchall()
        self.tree.delete(*self.tree.get_children())
        for sid, title, lang, tags, fav in rows:
            self.tree.insert("", tk.END, iid=str(sid),
                              values=(title, lang, tags,
                                      "★" if fav else ""))
        self.count_lbl.config(text=f"{len(rows)} 件")

    def _on_select(self, event=None):
        sel = self.tree.selection()
        if not sel:
            return
        sid = int(sel[0])
        row = self._conn.execute(
            "SELECT id,title,language,tags,code,note,favorite"
            " FROM snippets WHERE id=?", (sid,)).fetchone()
        if not row:
            return
        self._current_id = row[0]
        self.title_entry.delete(0, tk.END)
        self.title_entry.insert(0, row[1])
        self.lang_var.set(row[2])
        self.tags_entry.delete(0, tk.END)
        self.tags_entry.insert(0, row[3])
        self.code_text.delete("1.0", tk.END)
        self.code_text.insert(tk.END, row[4])
        self.note_text.delete("1.0", tk.END)
        self.note_text.insert(tk.END, row[5] or "")
        self.fav_var.set(row[6])
        self._update_line_numbers()
        self._highlight_code()
        self.status_var.set(f"ID={sid}  {row[1]}  [{row[2]}]")

    def _new_snippet(self):
        self._current_id = None
        self.title_entry.delete(0, tk.END)
        self.lang_var.set("Python")
        self.tags_entry.delete(0, tk.END)
        self.code_text.delete("1.0", tk.END)
        self.note_text.delete("1.0", tk.END)
        self.fav_var.set(0)
        self._update_line_numbers()
        self.status_var.set("新規スニペット")
        self.title_entry.focus_set()

    def _save_snippet(self):
        title = self.title_entry.get().strip()
        if not title:
            messagebox.showerror("エラー", "タイトルを入力してください")
            return
        lang = self.lang_var.get()
        tags = self.tags_entry.get().strip()
        code = self.code_text.get("1.0", tk.END).rstrip("\n")
        note = self.note_text.get("1.0", tk.END).rstrip("\n")
        fav  = self.fav_var.get()
        now  = datetime.now().isoformat(timespec="seconds")

        if self._current_id is None:
            cur = self._conn.execute(
                "INSERT INTO snippets "
                "(title,language,tags,code,note,favorite,created_at,updated_at)"
                " VALUES (?,?,?,?,?,?,?,?)",
                (title, lang, tags, code, note, fav, now, now))
            self._current_id = cur.lastrowid
            self._conn.commit()
            self.status_var.set(f"新規保存: {title}")
        else:
            self._conn.execute(
                "UPDATE snippets SET title=?,language=?,tags=?,code=?,"
                "note=?,favorite=?,updated_at=? WHERE id=?",
                (title, lang, tags, code, note, fav, now, self._current_id))
            self._conn.commit()
            self.status_var.set(f"更新: {title}")

        self._load_list()
        try:
            self.tree.selection_set(str(self._current_id))
        except Exception:
            pass

    def _delete_snippet(self):
        if self._current_id is None:
            return
        title = self.title_entry.get().strip() or f"ID={self._current_id}"
        if not messagebox.askyesno("削除確認", f"「{title}」を削除しますか?"):
            return
        self._conn.execute("DELETE FROM snippets WHERE id=?",
                            (self._current_id,))
        self._conn.commit()
        self._current_id = None
        self._new_snippet()
        self._load_list()
        self.status_var.set("削除しました")

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

    def _toggle_fav_filter(self):
        self._fav_only = not self._fav_only
        self.fav_btn.config(text="★ お気に入り" if self._fav_only else "☆ お気に入り")
        self._load_list()

    # ── エディタ支援 ──────────────────────────────────────────────

    def _on_code_change(self, event=None):
        self._update_line_numbers()
        self._highlight_code()

    def _update_line_numbers(self):
        self.line_canvas.delete("all")
        try:
            line_num = int(self.code_text.index("@0,0").split(".")[0])
            while True:
                dline = self.code_text.dlineinfo(f"{line_num}.0")
                if dline is None:
                    break
                self.line_canvas.create_text(
                    32, dline[1] + dline[3] // 2,
                    text=str(line_num), anchor="e",
                    fill="#858585", font=("Courier New", 10))
                line_num += 1
        except Exception:
            pass

    def _highlight_code(self):
        lang = self.lang_var.get()
        for tag in ("kw", "str_", "cmt", "num", "bi"):
            self.code_text.tag_remove(tag, "1.0", tk.END)
        self.code_text.tag_configure("kw",   foreground="#569cd6")
        self.code_text.tag_configure("str_", foreground="#ce9178")
        self.code_text.tag_configure("cmt",  foreground="#6a9955")
        self.code_text.tag_configure("num",  foreground="#b5cea8")
        self.code_text.tag_configure("bi",   foreground="#4ec9b0")

        content = self.code_text.get("1.0", tk.END)

        if lang == "Python":
            kw_pat  = (r"\b(def|class|import|from|return|if|elif|else|for|"
                       r"while|try|except|finally|with|as|pass|break|continue|"
                       r"in|not|and|or|is|None|True|False|lambda|yield|async|"
                       r"await|raise|del|global|nonlocal)\b")
            bi_pat  = (r"\b(print|len|range|type|str|int|float|list|dict|set|"
                       r"tuple|bool|open|sum|max|min|sorted|enumerate|zip|map|"
                       r"filter|super|self)\b")
            str_pat = r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"#[^\n]*"
        elif lang in ("JavaScript", "TypeScript"):
            kw_pat  = (r"\b(const|let|var|function|return|if|else|for|while|"
                       r"class|new|import|export|from|await|async|try|catch|"
                       r"finally|typeof|instanceof|of|in|true|false|null|"
                       r"undefined|this|super)\b")
            bi_pat  = (r"\b(console|document|window|Array|Object|Promise|fetch|"
                       r"JSON|Math|Date|Error|parseInt|parseFloat|String|"
                       r"Boolean|Number|Map|Set)\b")
            str_pat = r'(`[^`]*`|"[^"\n]*"|\'[^\'\n]*\')'
            cmt_pat = r"//[^\n]*"
        elif lang == "SQL":
            kw_pat  = (r"\b(SELECT|FROM|WHERE|JOIN|INNER|LEFT|RIGHT|OUTER|ON|"
                       r"GROUP|BY|ORDER|HAVING|INSERT|UPDATE|DELETE|CREATE|"
                       r"TABLE|DROP|ALTER|AS|AND|OR|NOT|IN|LIKE|BETWEEN|"
                       r"EXISTS|DISTINCT|LIMIT|OFFSET|SET|VALUES|INTO)\b")
            bi_pat  = (r"\b(COUNT|SUM|AVG|MAX|MIN|COALESCE|NULLIF|CASE|WHEN|"
                       r"THEN|ELSE|END|NOW|DATE|CAST|CONVERT)\b")
            str_pat = r"'[^']*'"
            cmt_pat = r"--[^\n]*"
        else:
            kw_pat  = None
            bi_pat  = None
            str_pat = r'"[^"\n]*"|\'[^\'\n]*\''
            cmt_pat = r"//[^\n]*|#[^\n]*"

        flags = re.IGNORECASE if lang == "SQL" else 0

        def apply(pattern, tag):
            if not pattern:
                return
            for m in re.finditer(pattern, content, flags):
                s = f"1.0 + {m.start()} chars"
                e = f"1.0 + {m.end()} chars"
                self.code_text.tag_add(tag, s, e)

        apply(r"\b\d+(\.\d+)?\b", "num")
        apply(str_pat, "str_")
        apply(cmt_pat, "cmt")
        apply(bi_pat, "bi")
        apply(kw_pat, "kw")


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

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

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

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

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

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

    App49クラスを定義し、__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.50に挑戦しましょう。