中級者向け No.50

マルチタブ簡易IDE

タブ付きエディタ・コード実行・出力パネル・ファイルツリーを統合した簡易IDE。集大成プロジェクトです。

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

1. アプリ概要

タブ付きエディタ・コード実行・出力パネル・ファイルツリーを統合した簡易IDE。集大成プロジェクトです。

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

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

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

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

2. 機能一覧

  • マルチタブ簡易IDEのメイン機能
  • 直感的なGUIインターフェース
  • 入力値のバリデーション
  • エラーハンドリング
  • 結果の見やすい表示
  • キーボードショートカット対応

3. 事前準備・環境

ℹ️
動作確認環境

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

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

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

4. 完全なソースコード

💡
コードのコピー方法

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

app50.py
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import re
import sys
import subprocess
import threading
from datetime import datetime


class App50:
    """マルチタブ簡易IDE"""

    TAB_UNTITLED = "無題"

    PYTHON_KW = (
        r"\b(def|class|import|from|return|if|elif|else|for|while|"
        r"try|except|finally|with|as|pass|break|continue|in|not|and|or|is|"
        r"None|True|False|lambda|yield|async|await|raise|del|global|nonlocal)\b"
    )
    PYTHON_BUILTINS = (
        r"\b(print|len|range|type|str|int|float|list|dict|set|tuple|bool|"
        r"open|sum|max|min|sorted|enumerate|zip|map|filter|super|self|input|"
        r"abs|round|isinstance|hasattr|getattr|setattr|vars|dir|help)\b"
    )
    PYTHON_STR = (
        r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|f"[^"]*"|f\'[^\']*\'|'
        r'"[^"\n]*"|\'[^\'\n]*\')'
    )
    PYTHON_CMT = r"#[^\n]*"
    PYTHON_NUM = r"\b\d+(\.\d+)?\b"
    PYTHON_DECO = r"@\w+"

    SAMPLE_CODE = '''\
# 簡易IDE サンプル — Fibonacci
def fibonacci(n):
    """フィボナッチ数列を返す"""
    a, b = 0, 1
    result = []
    for _ in range(n):
        result.append(a)
        a, b = b, a + b
    return result

if __name__ == "__main__":
    nums = fibonacci(10)
    print("Fibonacci:", nums)
    print("Sum:", sum(nums))
'''

    def __init__(self, root):
        self.root = root
        self.root.title("簡易IDE")
        self.root.geometry("1200x760")
        self.root.configure(bg="#1e1e1e")

        self._tabs = {}      # tab_id -> {"path", "editor", "modified"}
        self._proc = None
        self._file_tree_path = None

        self._build_ui()
        self._new_tab()

    # ── UI構築 ────────────────────────────────────────────────────

    def _build_ui(self):
        # メニューバー
        menubar = tk.Menu(self.root, bg="#252526", fg="#ccc",
                          activebackground="#094771",
                          activeforeground="#fff", bd=0)
        self.root.configure(menu=menubar)

        file_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="ファイル", menu=file_menu)
        file_menu.add_command(label="新規タブ (Ctrl+T)",   command=self._new_tab)
        file_menu.add_command(label="開く (Ctrl+O)",       command=self._open_file)
        file_menu.add_command(label="保存 (Ctrl+S)",       command=self._save_file)
        file_menu.add_command(label="名前を付けて保存",     command=self._save_as)
        file_menu.add_separator()
        file_menu.add_command(label="タブを閉じる (Ctrl+W)", command=self._close_tab)
        file_menu.add_separator()
        file_menu.add_command(label="終了", command=self.root.quit)

        run_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                           activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="実行", menu=run_menu)
        run_menu.add_command(label="▶ 実行 (F5)",   command=self._run_code)
        run_menu.add_command(label="⏹ 停止",        command=self._stop_code)

        edit_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="編集", menu=edit_menu)
        edit_menu.add_command(label="検索/置換 (Ctrl+H)", command=self._show_search)

        # キーバインド
        self.root.bind("<Control-t>", lambda e: self._new_tab())
        self.root.bind("<Control-o>", lambda e: self._open_file())
        self.root.bind("<Control-s>", lambda e: self._save_file())
        self.root.bind("<Control-w>", lambda e: self._close_tab())
        self.root.bind("<F5>",        lambda e: self._run_code())

        # ツールバー
        toolbar = tk.Frame(self.root, bg="#2d2d2d", pady=4)
        toolbar.pack(fill=tk.X)
        for text, cmd in [("📄 新規", self._new_tab),
                           ("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("▶ 実行", self._run_code),
                           ("⏹ 停止", self._stop_code)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="#ccc", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=2,
                      activebackground="#505050", bd=0).pack(
                side=tk.LEFT, padx=2)

        tk.Label(toolbar, text="Python インタープリタ:", bg="#2d2d2d",
                 fg="#888", font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 2))
        self.python_path_var = tk.StringVar(value=sys.executable)
        ttk.Entry(toolbar, textvariable=self.python_path_var,
                  width=30).pack(side=tk.LEFT)

        self.run_status = tk.Label(toolbar, text="", bg="#2d2d2d",
                                    fg="#4fc3f7", font=("Arial", 9))
        self.run_status.pack(side=tk.RIGHT, padx=8)

        # メインエリア: ファイルツリー | タブエディタ
        main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        main.pack(fill=tk.BOTH, expand=True)

        # 左: ファイルツリー
        tree_frame = tk.Frame(main, bg="#252526", width=180)
        main.add(tree_frame, weight=0)
        self._build_file_tree(tree_frame)

        # 右: エディタ + ターミナル
        right = tk.Frame(main, bg="#1e1e1e")
        main.add(right, weight=1)

        editor_paned = ttk.PanedWindow(right, orient=tk.VERTICAL)
        editor_paned.pack(fill=tk.BOTH, expand=True)

        # タブエディタ
        editor_area = tk.Frame(editor_paned, bg="#1e1e1e")
        editor_paned.add(editor_area, weight=3)
        self._build_editor_area(editor_area)

        # ターミナル出力
        term_area = tk.Frame(editor_paned, bg="#0d1117")
        editor_paned.add(term_area, weight=1)
        self._build_terminal(term_area)

        # ステータスバー
        self.status_var = tk.StringVar(value="準備完了")
        sb = tk.Frame(self.root, bg="#007acc", pady=2)
        sb.pack(fill=tk.X, side=tk.BOTTOM)
        self.status_lbl = tk.Label(sb, textvariable=self.status_var,
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8), anchor="w", padx=8)
        self.status_lbl.pack(side=tk.LEFT)
        self.cursor_lbl = tk.Label(sb, text="行 1, 列 1",
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8))
        self.cursor_lbl.pack(side=tk.RIGHT, padx=8)

    def _build_file_tree(self, parent):
        hdr = tk.Frame(parent, bg="#252526")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="📁 エクスプローラー", bg="#252526",
                 fg="#ccc", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=4, pady=4)
        ttk.Button(hdr, text="…",
                   command=self._open_folder).pack(side=tk.RIGHT, padx=2)

        self.file_tree = ttk.Treeview(parent, show="tree", selectmode="browse")
        fsb = ttk.Scrollbar(parent, command=self.file_tree.yview)
        self.file_tree.configure(yscrollcommand=fsb.set)
        fsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.file_tree.pack(fill=tk.BOTH, expand=True)
        self.file_tree.bind("<Double-1>", self._on_tree_double_click)

    def _build_editor_area(self, parent):
        # タブバー
        self.notebook = ttk.Notebook(parent)
        self.notebook.pack(fill=tk.BOTH, expand=True)
        self.notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)

        # 検索バー(非表示状態で配置)
        self.search_bar = tk.Frame(parent, bg="#252526", pady=4)
        tk.Label(self.search_bar, text="検索:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.search_entry = ttk.Entry(self.search_bar, width=20)
        self.search_entry.pack(side=tk.LEFT, padx=2)
        tk.Label(self.search_bar, text="置換:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.replace_entry = ttk.Entry(self.search_bar, width=20)
        self.replace_entry.pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="次へ",
                   command=self._find_next).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="置換",
                   command=self._replace_one).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="全置換",
                   command=self._replace_all).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="✕",
                   command=self._hide_search).pack(side=tk.LEFT, padx=4)
        self._search_visible = False

    def _build_terminal(self, parent):
        hdr = tk.Frame(parent, bg="#1a1a2e")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="ターミナル", bg="#1a1a2e",
                 fg="#4fc3f7", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=6, pady=2)
        ttk.Button(hdr, text="クリア",
                   command=self._clear_terminal).pack(side=tk.RIGHT, padx=4)

        self.terminal = tk.Text(parent, bg="#0d1117", fg="#d4d4d4",
                                 font=("Courier New", 9), relief=tk.FLAT,
                                 state=tk.DISABLED, wrap=tk.WORD)
        tsb = ttk.Scrollbar(parent, command=self.terminal.yview)
        self.terminal.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.terminal.pack(fill=tk.BOTH, expand=True)

        self.terminal.tag_configure("err",  foreground="#f48771")
        self.terminal.tag_configure("info", foreground="#4fc3f7")
        self.terminal.tag_configure("ok",   foreground="#4ec9b0")

    # ── タブ管理 ──────────────────────────────────────────────────

    def _new_tab(self, path=None, content=None):
        frame = tk.Frame(self.notebook, bg="#1e1e1e")

        # 行番号キャンバス + エディタ
        code_area = tk.Frame(frame, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

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

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

        # 初期コンテンツ
        initial = content if content is not None else self.SAMPLE_CODE
        editor.insert("1.0", initial)

        label = os.path.basename(path) if path else self.TAB_UNTITLED
        self.notebook.add(frame, text=f"  {label}  ")
        tab_id = frame

        self._tabs[id(tab_id)] = {
            "path": path,
            "editor": editor,
            "line_canvas": line_canvas,
            "modified": False,
            "frame": frame,
        }

        # イベント
        def on_key(e, tid=id(tab_id)):
            info = self._tabs.get(tid)
            if info and not info["modified"]:
                info["modified"] = True
                idx = self.notebook.index(frame)
                cur_text = self.notebook.tab(idx, "text")
                if not cur_text.startswith("●"):
                    self.notebook.tab(idx, text="● " + cur_text.strip())
            self._update_line_numbers(tid)
            self._highlight(tid)
            self._update_cursor_pos(e.widget)

        editor.bind("<KeyRelease>", on_key)
        editor.bind("<ButtonRelease-1>",
                    lambda e: self._update_cursor_pos(e.widget))

        self.notebook.select(frame)
        self._update_line_numbers(id(tab_id))
        self._highlight(id(tab_id))

    def _current_tab(self):
        try:
            frame = self.notebook.nametowidget(self.notebook.select())
            return id(frame), self._tabs.get(id(frame))
        except Exception:
            return None, None

    def _on_tab_changed(self, event=None):
        _, info = self._current_tab()
        if info:
            path = info["path"] or self.TAB_UNTITLED
            self.status_var.set(path)

    def _close_tab(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["modified"]:
            name = os.path.basename(info["path"]) if info["path"] else self.TAB_UNTITLED
            ans = messagebox.askyesnocancel("保存確認",
                                             f"「{name}」は変更されています。保存しますか?")
            if ans is None:
                return
            if ans:
                self._save_file()
        frame = info["frame"]
        self.notebook.forget(frame)
        del self._tabs[tid]

    # ── ファイル操作 ──────────────────────────────────────────────

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            with open(path, encoding="utf-8") as f:
                content = f.read()
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        self._new_tab(path=path, content=content)

    def _save_file(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["path"] is None:
            self._save_as()
            return
        self._do_save(tid, info, info["path"])

    def _save_as(self):
        tid, info = self._current_tab()
        if info is None:
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".py",
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        self._do_save(tid, info, path)

    def _do_save(self, tid, info, path):
        content = info["editor"].get("1.0", tk.END)
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        info["path"] = path
        info["modified"] = False
        idx = self.notebook.index(info["frame"])
        self.notebook.tab(idx, text=f"  {os.path.basename(path)}  ")
        self.status_var.set(f"保存: {path}")

    # ── ファイルツリー ────────────────────────────────────────────

    def _open_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self._file_tree_path = folder
            self._refresh_file_tree()

    def _refresh_file_tree(self):
        folder = self._file_tree_path
        if not folder:
            return
        self.file_tree.delete(*self.file_tree.get_children())
        root_node = self.file_tree.insert(
            "", tk.END, text=os.path.basename(folder),
            values=[folder], open=True)
        try:
            for name in sorted(os.listdir(folder)):
                full = os.path.join(folder, name)
                if os.path.isfile(full):
                    self.file_tree.insert(root_node, tk.END,
                                           text=name, values=[full])
        except Exception:
            pass

    def _on_tree_double_click(self, event=None):
        sel = self.file_tree.selection()
        if not sel:
            return
        vals = self.file_tree.item(sel[0], "values")
        if not vals:
            return
        path = vals[0]
        if os.path.isfile(path):
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self._new_tab(path=path, content=content)
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    # ── 実行 ──────────────────────────────────────────────────────

    def _run_code(self):
        _, info = self._current_tab()
        if info is None:
            return
        code = info["editor"].get("1.0", tk.END)
        path = info["path"]

        # 保存済みならそのファイルを実行、未保存なら一時保存
        if path and not info["modified"]:
            run_path = path
        else:
            import tempfile
            tmp = tempfile.NamedTemporaryFile(
                suffix=".py", delete=False, mode="w", encoding="utf-8")
            tmp.write(code)
            tmp.close()
            run_path = tmp.name

        self._term_write(f"▶ 実行: {os.path.basename(run_path)}\n", "info")
        self.run_status.config(text="実行中...")

        def target():
            try:
                py = self.python_path_var.get() or sys.executable
                self._proc = subprocess.Popen(
                    [py, run_path],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                    text=True, encoding="utf-8", errors="replace")
                stdout, stderr = self._proc.communicate(timeout=60)
                rc = self._proc.returncode
                if stdout:
                    self.root.after(0, self._term_write, stdout, "")
                if stderr:
                    self.root.after(0, self._term_write, stderr, "err")
                msg = f"✔ 終了 (コード {rc})" if rc == 0 else f"✖ エラー終了 (コード {rc})"
                tag = "ok" if rc == 0 else "err"
                self.root.after(0, self._term_write, msg + "\n", tag)
                self.root.after(0, self.run_status.config, {"text": ""})
            except subprocess.TimeoutExpired:
                self._proc.kill()
                self.root.after(0, self._term_write,
                                "⚠ タイムアウト (60秒)\n", "err")
            except Exception as e:
                self.root.after(0, self._term_write, str(e) + "\n", "err")
            finally:
                self._proc = None

        threading.Thread(target=target, daemon=True).start()

    def _stop_code(self):
        if self._proc:
            try:
                self._proc.kill()
            except Exception:
                pass
            self._term_write("⏹ 強制停止\n", "err")

    # ── ターミナル ────────────────────────────────────────────────

    def _term_write(self, text, tag=""):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.insert(tk.END, text, tag)
        self.terminal.see(tk.END)
        self.terminal.configure(state=tk.DISABLED)

    def _clear_terminal(self):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.delete("1.0", tk.END)
        self.terminal.configure(state=tk.DISABLED)

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

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

    def _highlight(self, tid):
        info = self._tabs.get(tid)
        if not info:
            return
        editor = info["editor"]

        for tag in ("kw", "str_", "cmt", "num", "deco", "bi"):
            editor.tag_remove(tag, "1.0", tk.END)
        editor.tag_configure("kw",   foreground="#569cd6")
        editor.tag_configure("str_", foreground="#ce9178")
        editor.tag_configure("cmt",  foreground="#6a9955")
        editor.tag_configure("num",  foreground="#b5cea8")
        editor.tag_configure("deco", foreground="#dcdcaa")
        editor.tag_configure("bi",   foreground="#4ec9b0")

        content = editor.get("1.0", tk.END)

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

        apply(self.PYTHON_NUM,     "num")
        apply(self.PYTHON_STR,     "str_")
        apply(self.PYTHON_CMT,     "cmt")
        apply(self.PYTHON_DECO,    "deco")
        apply(self.PYTHON_BUILTINS,"bi")
        apply(self.PYTHON_KW,      "kw")

    def _update_cursor_pos(self, widget):
        try:
            pos = widget.index(tk.INSERT)
            line, col = pos.split(".")
            self.cursor_lbl.config(text=f"行 {line}, 列 {int(col)+1}")
        except Exception:
            pass

    # ── 検索・置換 ────────────────────────────────────────────────

    def _show_search(self):
        if not self._search_visible:
            self.search_bar.pack(fill=tk.X, before=self.notebook)
            self._search_visible = True
            self.search_entry.focus_set()

    def _hide_search(self):
        if self._search_visible:
            self.search_bar.pack_forget()
            self._search_visible = False

    def _find_next(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        if not query:
            return
        editor.tag_remove("search_hl", "1.0", tk.END)
        editor.tag_configure("search_hl", background="#4d4000",
                              foreground="#fff")
        start = editor.index(tk.INSERT)
        pos = editor.search(query, start, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.tag_add("search_hl", pos, end_pos)
            editor.mark_set(tk.INSERT, end_pos)
            editor.see(pos)

    def _replace_one(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        pos = editor.search(query, tk.INSERT, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.delete(pos, end_pos)
            editor.insert(pos, replacement)

    def _replace_all(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        content = editor.get("1.0", tk.END)
        count = content.count(query)
        if count:
            new_content = content.replace(query, replacement)
            editor.delete("1.0", tk.END)
            editor.insert("1.0", new_content)
            self.status_var.set(f"{count} 件置換しました")


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

5. コード解説

マルチタブ簡易IDEのコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。

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

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

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import re
import sys
import subprocess
import threading
from datetime import datetime


class App50:
    """マルチタブ簡易IDE"""

    TAB_UNTITLED = "無題"

    PYTHON_KW = (
        r"\b(def|class|import|from|return|if|elif|else|for|while|"
        r"try|except|finally|with|as|pass|break|continue|in|not|and|or|is|"
        r"None|True|False|lambda|yield|async|await|raise|del|global|nonlocal)\b"
    )
    PYTHON_BUILTINS = (
        r"\b(print|len|range|type|str|int|float|list|dict|set|tuple|bool|"
        r"open|sum|max|min|sorted|enumerate|zip|map|filter|super|self|input|"
        r"abs|round|isinstance|hasattr|getattr|setattr|vars|dir|help)\b"
    )
    PYTHON_STR = (
        r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|f"[^"]*"|f\'[^\']*\'|'
        r'"[^"\n]*"|\'[^\'\n]*\')'
    )
    PYTHON_CMT = r"#[^\n]*"
    PYTHON_NUM = r"\b\d+(\.\d+)?\b"
    PYTHON_DECO = r"@\w+"

    SAMPLE_CODE = '''\
# 簡易IDE サンプル — Fibonacci
def fibonacci(n):
    """フィボナッチ数列を返す"""
    a, b = 0, 1
    result = []
    for _ in range(n):
        result.append(a)
        a, b = b, a + b
    return result

if __name__ == "__main__":
    nums = fibonacci(10)
    print("Fibonacci:", nums)
    print("Sum:", sum(nums))
'''

    def __init__(self, root):
        self.root = root
        self.root.title("簡易IDE")
        self.root.geometry("1200x760")
        self.root.configure(bg="#1e1e1e")

        self._tabs = {}      # tab_id -> {"path", "editor", "modified"}
        self._proc = None
        self._file_tree_path = None

        self._build_ui()
        self._new_tab()

    # ── UI構築 ────────────────────────────────────────────────────

    def _build_ui(self):
        # メニューバー
        menubar = tk.Menu(self.root, bg="#252526", fg="#ccc",
                          activebackground="#094771",
                          activeforeground="#fff", bd=0)
        self.root.configure(menu=menubar)

        file_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="ファイル", menu=file_menu)
        file_menu.add_command(label="新規タブ (Ctrl+T)",   command=self._new_tab)
        file_menu.add_command(label="開く (Ctrl+O)",       command=self._open_file)
        file_menu.add_command(label="保存 (Ctrl+S)",       command=self._save_file)
        file_menu.add_command(label="名前を付けて保存",     command=self._save_as)
        file_menu.add_separator()
        file_menu.add_command(label="タブを閉じる (Ctrl+W)", command=self._close_tab)
        file_menu.add_separator()
        file_menu.add_command(label="終了", command=self.root.quit)

        run_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                           activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="実行", menu=run_menu)
        run_menu.add_command(label="▶ 実行 (F5)",   command=self._run_code)
        run_menu.add_command(label="⏹ 停止",        command=self._stop_code)

        edit_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="編集", menu=edit_menu)
        edit_menu.add_command(label="検索/置換 (Ctrl+H)", command=self._show_search)

        # キーバインド
        self.root.bind("<Control-t>", lambda e: self._new_tab())
        self.root.bind("<Control-o>", lambda e: self._open_file())
        self.root.bind("<Control-s>", lambda e: self._save_file())
        self.root.bind("<Control-w>", lambda e: self._close_tab())
        self.root.bind("<F5>",        lambda e: self._run_code())

        # ツールバー
        toolbar = tk.Frame(self.root, bg="#2d2d2d", pady=4)
        toolbar.pack(fill=tk.X)
        for text, cmd in [("📄 新規", self._new_tab),
                           ("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("▶ 実行", self._run_code),
                           ("⏹ 停止", self._stop_code)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="#ccc", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=2,
                      activebackground="#505050", bd=0).pack(
                side=tk.LEFT, padx=2)

        tk.Label(toolbar, text="Python インタープリタ:", bg="#2d2d2d",
                 fg="#888", font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 2))
        self.python_path_var = tk.StringVar(value=sys.executable)
        ttk.Entry(toolbar, textvariable=self.python_path_var,
                  width=30).pack(side=tk.LEFT)

        self.run_status = tk.Label(toolbar, text="", bg="#2d2d2d",
                                    fg="#4fc3f7", font=("Arial", 9))
        self.run_status.pack(side=tk.RIGHT, padx=8)

        # メインエリア: ファイルツリー | タブエディタ
        main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        main.pack(fill=tk.BOTH, expand=True)

        # 左: ファイルツリー
        tree_frame = tk.Frame(main, bg="#252526", width=180)
        main.add(tree_frame, weight=0)
        self._build_file_tree(tree_frame)

        # 右: エディタ + ターミナル
        right = tk.Frame(main, bg="#1e1e1e")
        main.add(right, weight=1)

        editor_paned = ttk.PanedWindow(right, orient=tk.VERTICAL)
        editor_paned.pack(fill=tk.BOTH, expand=True)

        # タブエディタ
        editor_area = tk.Frame(editor_paned, bg="#1e1e1e")
        editor_paned.add(editor_area, weight=3)
        self._build_editor_area(editor_area)

        # ターミナル出力
        term_area = tk.Frame(editor_paned, bg="#0d1117")
        editor_paned.add(term_area, weight=1)
        self._build_terminal(term_area)

        # ステータスバー
        self.status_var = tk.StringVar(value="準備完了")
        sb = tk.Frame(self.root, bg="#007acc", pady=2)
        sb.pack(fill=tk.X, side=tk.BOTTOM)
        self.status_lbl = tk.Label(sb, textvariable=self.status_var,
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8), anchor="w", padx=8)
        self.status_lbl.pack(side=tk.LEFT)
        self.cursor_lbl = tk.Label(sb, text="行 1, 列 1",
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8))
        self.cursor_lbl.pack(side=tk.RIGHT, padx=8)

    def _build_file_tree(self, parent):
        hdr = tk.Frame(parent, bg="#252526")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="📁 エクスプローラー", bg="#252526",
                 fg="#ccc", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=4, pady=4)
        ttk.Button(hdr, text="…",
                   command=self._open_folder).pack(side=tk.RIGHT, padx=2)

        self.file_tree = ttk.Treeview(parent, show="tree", selectmode="browse")
        fsb = ttk.Scrollbar(parent, command=self.file_tree.yview)
        self.file_tree.configure(yscrollcommand=fsb.set)
        fsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.file_tree.pack(fill=tk.BOTH, expand=True)
        self.file_tree.bind("<Double-1>", self._on_tree_double_click)

    def _build_editor_area(self, parent):
        # タブバー
        self.notebook = ttk.Notebook(parent)
        self.notebook.pack(fill=tk.BOTH, expand=True)
        self.notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)

        # 検索バー(非表示状態で配置)
        self.search_bar = tk.Frame(parent, bg="#252526", pady=4)
        tk.Label(self.search_bar, text="検索:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.search_entry = ttk.Entry(self.search_bar, width=20)
        self.search_entry.pack(side=tk.LEFT, padx=2)
        tk.Label(self.search_bar, text="置換:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.replace_entry = ttk.Entry(self.search_bar, width=20)
        self.replace_entry.pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="次へ",
                   command=self._find_next).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="置換",
                   command=self._replace_one).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="全置換",
                   command=self._replace_all).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="✕",
                   command=self._hide_search).pack(side=tk.LEFT, padx=4)
        self._search_visible = False

    def _build_terminal(self, parent):
        hdr = tk.Frame(parent, bg="#1a1a2e")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="ターミナル", bg="#1a1a2e",
                 fg="#4fc3f7", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=6, pady=2)
        ttk.Button(hdr, text="クリア",
                   command=self._clear_terminal).pack(side=tk.RIGHT, padx=4)

        self.terminal = tk.Text(parent, bg="#0d1117", fg="#d4d4d4",
                                 font=("Courier New", 9), relief=tk.FLAT,
                                 state=tk.DISABLED, wrap=tk.WORD)
        tsb = ttk.Scrollbar(parent, command=self.terminal.yview)
        self.terminal.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.terminal.pack(fill=tk.BOTH, expand=True)

        self.terminal.tag_configure("err",  foreground="#f48771")
        self.terminal.tag_configure("info", foreground="#4fc3f7")
        self.terminal.tag_configure("ok",   foreground="#4ec9b0")

    # ── タブ管理 ──────────────────────────────────────────────────

    def _new_tab(self, path=None, content=None):
        frame = tk.Frame(self.notebook, bg="#1e1e1e")

        # 行番号キャンバス + エディタ
        code_area = tk.Frame(frame, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

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

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

        # 初期コンテンツ
        initial = content if content is not None else self.SAMPLE_CODE
        editor.insert("1.0", initial)

        label = os.path.basename(path) if path else self.TAB_UNTITLED
        self.notebook.add(frame, text=f"  {label}  ")
        tab_id = frame

        self._tabs[id(tab_id)] = {
            "path": path,
            "editor": editor,
            "line_canvas": line_canvas,
            "modified": False,
            "frame": frame,
        }

        # イベント
        def on_key(e, tid=id(tab_id)):
            info = self._tabs.get(tid)
            if info and not info["modified"]:
                info["modified"] = True
                idx = self.notebook.index(frame)
                cur_text = self.notebook.tab(idx, "text")
                if not cur_text.startswith("●"):
                    self.notebook.tab(idx, text="● " + cur_text.strip())
            self._update_line_numbers(tid)
            self._highlight(tid)
            self._update_cursor_pos(e.widget)

        editor.bind("<KeyRelease>", on_key)
        editor.bind("<ButtonRelease-1>",
                    lambda e: self._update_cursor_pos(e.widget))

        self.notebook.select(frame)
        self._update_line_numbers(id(tab_id))
        self._highlight(id(tab_id))

    def _current_tab(self):
        try:
            frame = self.notebook.nametowidget(self.notebook.select())
            return id(frame), self._tabs.get(id(frame))
        except Exception:
            return None, None

    def _on_tab_changed(self, event=None):
        _, info = self._current_tab()
        if info:
            path = info["path"] or self.TAB_UNTITLED
            self.status_var.set(path)

    def _close_tab(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["modified"]:
            name = os.path.basename(info["path"]) if info["path"] else self.TAB_UNTITLED
            ans = messagebox.askyesnocancel("保存確認",
                                             f"「{name}」は変更されています。保存しますか?")
            if ans is None:
                return
            if ans:
                self._save_file()
        frame = info["frame"]
        self.notebook.forget(frame)
        del self._tabs[tid]

    # ── ファイル操作 ──────────────────────────────────────────────

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            with open(path, encoding="utf-8") as f:
                content = f.read()
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        self._new_tab(path=path, content=content)

    def _save_file(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["path"] is None:
            self._save_as()
            return
        self._do_save(tid, info, info["path"])

    def _save_as(self):
        tid, info = self._current_tab()
        if info is None:
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".py",
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        self._do_save(tid, info, path)

    def _do_save(self, tid, info, path):
        content = info["editor"].get("1.0", tk.END)
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        info["path"] = path
        info["modified"] = False
        idx = self.notebook.index(info["frame"])
        self.notebook.tab(idx, text=f"  {os.path.basename(path)}  ")
        self.status_var.set(f"保存: {path}")

    # ── ファイルツリー ────────────────────────────────────────────

    def _open_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self._file_tree_path = folder
            self._refresh_file_tree()

    def _refresh_file_tree(self):
        folder = self._file_tree_path
        if not folder:
            return
        self.file_tree.delete(*self.file_tree.get_children())
        root_node = self.file_tree.insert(
            "", tk.END, text=os.path.basename(folder),
            values=[folder], open=True)
        try:
            for name in sorted(os.listdir(folder)):
                full = os.path.join(folder, name)
                if os.path.isfile(full):
                    self.file_tree.insert(root_node, tk.END,
                                           text=name, values=[full])
        except Exception:
            pass

    def _on_tree_double_click(self, event=None):
        sel = self.file_tree.selection()
        if not sel:
            return
        vals = self.file_tree.item(sel[0], "values")
        if not vals:
            return
        path = vals[0]
        if os.path.isfile(path):
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self._new_tab(path=path, content=content)
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    # ── 実行 ──────────────────────────────────────────────────────

    def _run_code(self):
        _, info = self._current_tab()
        if info is None:
            return
        code = info["editor"].get("1.0", tk.END)
        path = info["path"]

        # 保存済みならそのファイルを実行、未保存なら一時保存
        if path and not info["modified"]:
            run_path = path
        else:
            import tempfile
            tmp = tempfile.NamedTemporaryFile(
                suffix=".py", delete=False, mode="w", encoding="utf-8")
            tmp.write(code)
            tmp.close()
            run_path = tmp.name

        self._term_write(f"▶ 実行: {os.path.basename(run_path)}\n", "info")
        self.run_status.config(text="実行中...")

        def target():
            try:
                py = self.python_path_var.get() or sys.executable
                self._proc = subprocess.Popen(
                    [py, run_path],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                    text=True, encoding="utf-8", errors="replace")
                stdout, stderr = self._proc.communicate(timeout=60)
                rc = self._proc.returncode
                if stdout:
                    self.root.after(0, self._term_write, stdout, "")
                if stderr:
                    self.root.after(0, self._term_write, stderr, "err")
                msg = f"✔ 終了 (コード {rc})" if rc == 0 else f"✖ エラー終了 (コード {rc})"
                tag = "ok" if rc == 0 else "err"
                self.root.after(0, self._term_write, msg + "\n", tag)
                self.root.after(0, self.run_status.config, {"text": ""})
            except subprocess.TimeoutExpired:
                self._proc.kill()
                self.root.after(0, self._term_write,
                                "⚠ タイムアウト (60秒)\n", "err")
            except Exception as e:
                self.root.after(0, self._term_write, str(e) + "\n", "err")
            finally:
                self._proc = None

        threading.Thread(target=target, daemon=True).start()

    def _stop_code(self):
        if self._proc:
            try:
                self._proc.kill()
            except Exception:
                pass
            self._term_write("⏹ 強制停止\n", "err")

    # ── ターミナル ────────────────────────────────────────────────

    def _term_write(self, text, tag=""):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.insert(tk.END, text, tag)
        self.terminal.see(tk.END)
        self.terminal.configure(state=tk.DISABLED)

    def _clear_terminal(self):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.delete("1.0", tk.END)
        self.terminal.configure(state=tk.DISABLED)

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

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

    def _highlight(self, tid):
        info = self._tabs.get(tid)
        if not info:
            return
        editor = info["editor"]

        for tag in ("kw", "str_", "cmt", "num", "deco", "bi"):
            editor.tag_remove(tag, "1.0", tk.END)
        editor.tag_configure("kw",   foreground="#569cd6")
        editor.tag_configure("str_", foreground="#ce9178")
        editor.tag_configure("cmt",  foreground="#6a9955")
        editor.tag_configure("num",  foreground="#b5cea8")
        editor.tag_configure("deco", foreground="#dcdcaa")
        editor.tag_configure("bi",   foreground="#4ec9b0")

        content = editor.get("1.0", tk.END)

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

        apply(self.PYTHON_NUM,     "num")
        apply(self.PYTHON_STR,     "str_")
        apply(self.PYTHON_CMT,     "cmt")
        apply(self.PYTHON_DECO,    "deco")
        apply(self.PYTHON_BUILTINS,"bi")
        apply(self.PYTHON_KW,      "kw")

    def _update_cursor_pos(self, widget):
        try:
            pos = widget.index(tk.INSERT)
            line, col = pos.split(".")
            self.cursor_lbl.config(text=f"行 {line}, 列 {int(col)+1}")
        except Exception:
            pass

    # ── 検索・置換 ────────────────────────────────────────────────

    def _show_search(self):
        if not self._search_visible:
            self.search_bar.pack(fill=tk.X, before=self.notebook)
            self._search_visible = True
            self.search_entry.focus_set()

    def _hide_search(self):
        if self._search_visible:
            self.search_bar.pack_forget()
            self._search_visible = False

    def _find_next(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        if not query:
            return
        editor.tag_remove("search_hl", "1.0", tk.END)
        editor.tag_configure("search_hl", background="#4d4000",
                              foreground="#fff")
        start = editor.index(tk.INSERT)
        pos = editor.search(query, start, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.tag_add("search_hl", pos, end_pos)
            editor.mark_set(tk.INSERT, end_pos)
            editor.see(pos)

    def _replace_one(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        pos = editor.search(query, tk.INSERT, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.delete(pos, end_pos)
            editor.insert(pos, replacement)

    def _replace_all(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        content = editor.get("1.0", tk.END)
        count = content.count(query)
        if count:
            new_content = content.replace(query, replacement)
            editor.delete("1.0", tk.END)
            editor.insert("1.0", new_content)
            self.status_var.set(f"{count} 件置換しました")


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

LabelFrameによるセクション分け

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

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import re
import sys
import subprocess
import threading
from datetime import datetime


class App50:
    """マルチタブ簡易IDE"""

    TAB_UNTITLED = "無題"

    PYTHON_KW = (
        r"\b(def|class|import|from|return|if|elif|else|for|while|"
        r"try|except|finally|with|as|pass|break|continue|in|not|and|or|is|"
        r"None|True|False|lambda|yield|async|await|raise|del|global|nonlocal)\b"
    )
    PYTHON_BUILTINS = (
        r"\b(print|len|range|type|str|int|float|list|dict|set|tuple|bool|"
        r"open|sum|max|min|sorted|enumerate|zip|map|filter|super|self|input|"
        r"abs|round|isinstance|hasattr|getattr|setattr|vars|dir|help)\b"
    )
    PYTHON_STR = (
        r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|f"[^"]*"|f\'[^\']*\'|'
        r'"[^"\n]*"|\'[^\'\n]*\')'
    )
    PYTHON_CMT = r"#[^\n]*"
    PYTHON_NUM = r"\b\d+(\.\d+)?\b"
    PYTHON_DECO = r"@\w+"

    SAMPLE_CODE = '''\
# 簡易IDE サンプル — Fibonacci
def fibonacci(n):
    """フィボナッチ数列を返す"""
    a, b = 0, 1
    result = []
    for _ in range(n):
        result.append(a)
        a, b = b, a + b
    return result

if __name__ == "__main__":
    nums = fibonacci(10)
    print("Fibonacci:", nums)
    print("Sum:", sum(nums))
'''

    def __init__(self, root):
        self.root = root
        self.root.title("簡易IDE")
        self.root.geometry("1200x760")
        self.root.configure(bg="#1e1e1e")

        self._tabs = {}      # tab_id -> {"path", "editor", "modified"}
        self._proc = None
        self._file_tree_path = None

        self._build_ui()
        self._new_tab()

    # ── UI構築 ────────────────────────────────────────────────────

    def _build_ui(self):
        # メニューバー
        menubar = tk.Menu(self.root, bg="#252526", fg="#ccc",
                          activebackground="#094771",
                          activeforeground="#fff", bd=0)
        self.root.configure(menu=menubar)

        file_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="ファイル", menu=file_menu)
        file_menu.add_command(label="新規タブ (Ctrl+T)",   command=self._new_tab)
        file_menu.add_command(label="開く (Ctrl+O)",       command=self._open_file)
        file_menu.add_command(label="保存 (Ctrl+S)",       command=self._save_file)
        file_menu.add_command(label="名前を付けて保存",     command=self._save_as)
        file_menu.add_separator()
        file_menu.add_command(label="タブを閉じる (Ctrl+W)", command=self._close_tab)
        file_menu.add_separator()
        file_menu.add_command(label="終了", command=self.root.quit)

        run_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                           activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="実行", menu=run_menu)
        run_menu.add_command(label="▶ 実行 (F5)",   command=self._run_code)
        run_menu.add_command(label="⏹ 停止",        command=self._stop_code)

        edit_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="編集", menu=edit_menu)
        edit_menu.add_command(label="検索/置換 (Ctrl+H)", command=self._show_search)

        # キーバインド
        self.root.bind("<Control-t>", lambda e: self._new_tab())
        self.root.bind("<Control-o>", lambda e: self._open_file())
        self.root.bind("<Control-s>", lambda e: self._save_file())
        self.root.bind("<Control-w>", lambda e: self._close_tab())
        self.root.bind("<F5>",        lambda e: self._run_code())

        # ツールバー
        toolbar = tk.Frame(self.root, bg="#2d2d2d", pady=4)
        toolbar.pack(fill=tk.X)
        for text, cmd in [("📄 新規", self._new_tab),
                           ("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("▶ 実行", self._run_code),
                           ("⏹ 停止", self._stop_code)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="#ccc", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=2,
                      activebackground="#505050", bd=0).pack(
                side=tk.LEFT, padx=2)

        tk.Label(toolbar, text="Python インタープリタ:", bg="#2d2d2d",
                 fg="#888", font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 2))
        self.python_path_var = tk.StringVar(value=sys.executable)
        ttk.Entry(toolbar, textvariable=self.python_path_var,
                  width=30).pack(side=tk.LEFT)

        self.run_status = tk.Label(toolbar, text="", bg="#2d2d2d",
                                    fg="#4fc3f7", font=("Arial", 9))
        self.run_status.pack(side=tk.RIGHT, padx=8)

        # メインエリア: ファイルツリー | タブエディタ
        main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        main.pack(fill=tk.BOTH, expand=True)

        # 左: ファイルツリー
        tree_frame = tk.Frame(main, bg="#252526", width=180)
        main.add(tree_frame, weight=0)
        self._build_file_tree(tree_frame)

        # 右: エディタ + ターミナル
        right = tk.Frame(main, bg="#1e1e1e")
        main.add(right, weight=1)

        editor_paned = ttk.PanedWindow(right, orient=tk.VERTICAL)
        editor_paned.pack(fill=tk.BOTH, expand=True)

        # タブエディタ
        editor_area = tk.Frame(editor_paned, bg="#1e1e1e")
        editor_paned.add(editor_area, weight=3)
        self._build_editor_area(editor_area)

        # ターミナル出力
        term_area = tk.Frame(editor_paned, bg="#0d1117")
        editor_paned.add(term_area, weight=1)
        self._build_terminal(term_area)

        # ステータスバー
        self.status_var = tk.StringVar(value="準備完了")
        sb = tk.Frame(self.root, bg="#007acc", pady=2)
        sb.pack(fill=tk.X, side=tk.BOTTOM)
        self.status_lbl = tk.Label(sb, textvariable=self.status_var,
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8), anchor="w", padx=8)
        self.status_lbl.pack(side=tk.LEFT)
        self.cursor_lbl = tk.Label(sb, text="行 1, 列 1",
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8))
        self.cursor_lbl.pack(side=tk.RIGHT, padx=8)

    def _build_file_tree(self, parent):
        hdr = tk.Frame(parent, bg="#252526")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="📁 エクスプローラー", bg="#252526",
                 fg="#ccc", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=4, pady=4)
        ttk.Button(hdr, text="…",
                   command=self._open_folder).pack(side=tk.RIGHT, padx=2)

        self.file_tree = ttk.Treeview(parent, show="tree", selectmode="browse")
        fsb = ttk.Scrollbar(parent, command=self.file_tree.yview)
        self.file_tree.configure(yscrollcommand=fsb.set)
        fsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.file_tree.pack(fill=tk.BOTH, expand=True)
        self.file_tree.bind("<Double-1>", self._on_tree_double_click)

    def _build_editor_area(self, parent):
        # タブバー
        self.notebook = ttk.Notebook(parent)
        self.notebook.pack(fill=tk.BOTH, expand=True)
        self.notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)

        # 検索バー(非表示状態で配置)
        self.search_bar = tk.Frame(parent, bg="#252526", pady=4)
        tk.Label(self.search_bar, text="検索:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.search_entry = ttk.Entry(self.search_bar, width=20)
        self.search_entry.pack(side=tk.LEFT, padx=2)
        tk.Label(self.search_bar, text="置換:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.replace_entry = ttk.Entry(self.search_bar, width=20)
        self.replace_entry.pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="次へ",
                   command=self._find_next).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="置換",
                   command=self._replace_one).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="全置換",
                   command=self._replace_all).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="✕",
                   command=self._hide_search).pack(side=tk.LEFT, padx=4)
        self._search_visible = False

    def _build_terminal(self, parent):
        hdr = tk.Frame(parent, bg="#1a1a2e")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="ターミナル", bg="#1a1a2e",
                 fg="#4fc3f7", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=6, pady=2)
        ttk.Button(hdr, text="クリア",
                   command=self._clear_terminal).pack(side=tk.RIGHT, padx=4)

        self.terminal = tk.Text(parent, bg="#0d1117", fg="#d4d4d4",
                                 font=("Courier New", 9), relief=tk.FLAT,
                                 state=tk.DISABLED, wrap=tk.WORD)
        tsb = ttk.Scrollbar(parent, command=self.terminal.yview)
        self.terminal.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.terminal.pack(fill=tk.BOTH, expand=True)

        self.terminal.tag_configure("err",  foreground="#f48771")
        self.terminal.tag_configure("info", foreground="#4fc3f7")
        self.terminal.tag_configure("ok",   foreground="#4ec9b0")

    # ── タブ管理 ──────────────────────────────────────────────────

    def _new_tab(self, path=None, content=None):
        frame = tk.Frame(self.notebook, bg="#1e1e1e")

        # 行番号キャンバス + エディタ
        code_area = tk.Frame(frame, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

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

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

        # 初期コンテンツ
        initial = content if content is not None else self.SAMPLE_CODE
        editor.insert("1.0", initial)

        label = os.path.basename(path) if path else self.TAB_UNTITLED
        self.notebook.add(frame, text=f"  {label}  ")
        tab_id = frame

        self._tabs[id(tab_id)] = {
            "path": path,
            "editor": editor,
            "line_canvas": line_canvas,
            "modified": False,
            "frame": frame,
        }

        # イベント
        def on_key(e, tid=id(tab_id)):
            info = self._tabs.get(tid)
            if info and not info["modified"]:
                info["modified"] = True
                idx = self.notebook.index(frame)
                cur_text = self.notebook.tab(idx, "text")
                if not cur_text.startswith("●"):
                    self.notebook.tab(idx, text="● " + cur_text.strip())
            self._update_line_numbers(tid)
            self._highlight(tid)
            self._update_cursor_pos(e.widget)

        editor.bind("<KeyRelease>", on_key)
        editor.bind("<ButtonRelease-1>",
                    lambda e: self._update_cursor_pos(e.widget))

        self.notebook.select(frame)
        self._update_line_numbers(id(tab_id))
        self._highlight(id(tab_id))

    def _current_tab(self):
        try:
            frame = self.notebook.nametowidget(self.notebook.select())
            return id(frame), self._tabs.get(id(frame))
        except Exception:
            return None, None

    def _on_tab_changed(self, event=None):
        _, info = self._current_tab()
        if info:
            path = info["path"] or self.TAB_UNTITLED
            self.status_var.set(path)

    def _close_tab(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["modified"]:
            name = os.path.basename(info["path"]) if info["path"] else self.TAB_UNTITLED
            ans = messagebox.askyesnocancel("保存確認",
                                             f"「{name}」は変更されています。保存しますか?")
            if ans is None:
                return
            if ans:
                self._save_file()
        frame = info["frame"]
        self.notebook.forget(frame)
        del self._tabs[tid]

    # ── ファイル操作 ──────────────────────────────────────────────

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            with open(path, encoding="utf-8") as f:
                content = f.read()
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        self._new_tab(path=path, content=content)

    def _save_file(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["path"] is None:
            self._save_as()
            return
        self._do_save(tid, info, info["path"])

    def _save_as(self):
        tid, info = self._current_tab()
        if info is None:
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".py",
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        self._do_save(tid, info, path)

    def _do_save(self, tid, info, path):
        content = info["editor"].get("1.0", tk.END)
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        info["path"] = path
        info["modified"] = False
        idx = self.notebook.index(info["frame"])
        self.notebook.tab(idx, text=f"  {os.path.basename(path)}  ")
        self.status_var.set(f"保存: {path}")

    # ── ファイルツリー ────────────────────────────────────────────

    def _open_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self._file_tree_path = folder
            self._refresh_file_tree()

    def _refresh_file_tree(self):
        folder = self._file_tree_path
        if not folder:
            return
        self.file_tree.delete(*self.file_tree.get_children())
        root_node = self.file_tree.insert(
            "", tk.END, text=os.path.basename(folder),
            values=[folder], open=True)
        try:
            for name in sorted(os.listdir(folder)):
                full = os.path.join(folder, name)
                if os.path.isfile(full):
                    self.file_tree.insert(root_node, tk.END,
                                           text=name, values=[full])
        except Exception:
            pass

    def _on_tree_double_click(self, event=None):
        sel = self.file_tree.selection()
        if not sel:
            return
        vals = self.file_tree.item(sel[0], "values")
        if not vals:
            return
        path = vals[0]
        if os.path.isfile(path):
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self._new_tab(path=path, content=content)
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    # ── 実行 ──────────────────────────────────────────────────────

    def _run_code(self):
        _, info = self._current_tab()
        if info is None:
            return
        code = info["editor"].get("1.0", tk.END)
        path = info["path"]

        # 保存済みならそのファイルを実行、未保存なら一時保存
        if path and not info["modified"]:
            run_path = path
        else:
            import tempfile
            tmp = tempfile.NamedTemporaryFile(
                suffix=".py", delete=False, mode="w", encoding="utf-8")
            tmp.write(code)
            tmp.close()
            run_path = tmp.name

        self._term_write(f"▶ 実行: {os.path.basename(run_path)}\n", "info")
        self.run_status.config(text="実行中...")

        def target():
            try:
                py = self.python_path_var.get() or sys.executable
                self._proc = subprocess.Popen(
                    [py, run_path],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                    text=True, encoding="utf-8", errors="replace")
                stdout, stderr = self._proc.communicate(timeout=60)
                rc = self._proc.returncode
                if stdout:
                    self.root.after(0, self._term_write, stdout, "")
                if stderr:
                    self.root.after(0, self._term_write, stderr, "err")
                msg = f"✔ 終了 (コード {rc})" if rc == 0 else f"✖ エラー終了 (コード {rc})"
                tag = "ok" if rc == 0 else "err"
                self.root.after(0, self._term_write, msg + "\n", tag)
                self.root.after(0, self.run_status.config, {"text": ""})
            except subprocess.TimeoutExpired:
                self._proc.kill()
                self.root.after(0, self._term_write,
                                "⚠ タイムアウト (60秒)\n", "err")
            except Exception as e:
                self.root.after(0, self._term_write, str(e) + "\n", "err")
            finally:
                self._proc = None

        threading.Thread(target=target, daemon=True).start()

    def _stop_code(self):
        if self._proc:
            try:
                self._proc.kill()
            except Exception:
                pass
            self._term_write("⏹ 強制停止\n", "err")

    # ── ターミナル ────────────────────────────────────────────────

    def _term_write(self, text, tag=""):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.insert(tk.END, text, tag)
        self.terminal.see(tk.END)
        self.terminal.configure(state=tk.DISABLED)

    def _clear_terminal(self):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.delete("1.0", tk.END)
        self.terminal.configure(state=tk.DISABLED)

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

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

    def _highlight(self, tid):
        info = self._tabs.get(tid)
        if not info:
            return
        editor = info["editor"]

        for tag in ("kw", "str_", "cmt", "num", "deco", "bi"):
            editor.tag_remove(tag, "1.0", tk.END)
        editor.tag_configure("kw",   foreground="#569cd6")
        editor.tag_configure("str_", foreground="#ce9178")
        editor.tag_configure("cmt",  foreground="#6a9955")
        editor.tag_configure("num",  foreground="#b5cea8")
        editor.tag_configure("deco", foreground="#dcdcaa")
        editor.tag_configure("bi",   foreground="#4ec9b0")

        content = editor.get("1.0", tk.END)

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

        apply(self.PYTHON_NUM,     "num")
        apply(self.PYTHON_STR,     "str_")
        apply(self.PYTHON_CMT,     "cmt")
        apply(self.PYTHON_DECO,    "deco")
        apply(self.PYTHON_BUILTINS,"bi")
        apply(self.PYTHON_KW,      "kw")

    def _update_cursor_pos(self, widget):
        try:
            pos = widget.index(tk.INSERT)
            line, col = pos.split(".")
            self.cursor_lbl.config(text=f"行 {line}, 列 {int(col)+1}")
        except Exception:
            pass

    # ── 検索・置換 ────────────────────────────────────────────────

    def _show_search(self):
        if not self._search_visible:
            self.search_bar.pack(fill=tk.X, before=self.notebook)
            self._search_visible = True
            self.search_entry.focus_set()

    def _hide_search(self):
        if self._search_visible:
            self.search_bar.pack_forget()
            self._search_visible = False

    def _find_next(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        if not query:
            return
        editor.tag_remove("search_hl", "1.0", tk.END)
        editor.tag_configure("search_hl", background="#4d4000",
                              foreground="#fff")
        start = editor.index(tk.INSERT)
        pos = editor.search(query, start, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.tag_add("search_hl", pos, end_pos)
            editor.mark_set(tk.INSERT, end_pos)
            editor.see(pos)

    def _replace_one(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        pos = editor.search(query, tk.INSERT, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.delete(pos, end_pos)
            editor.insert(pos, replacement)

    def _replace_all(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        content = editor.get("1.0", tk.END)
        count = content.count(query)
        if count:
            new_content = content.replace(query, replacement)
            editor.delete("1.0", tk.END)
            editor.insert("1.0", new_content)
            self.status_var.set(f"{count} 件置換しました")


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

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

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

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import re
import sys
import subprocess
import threading
from datetime import datetime


class App50:
    """マルチタブ簡易IDE"""

    TAB_UNTITLED = "無題"

    PYTHON_KW = (
        r"\b(def|class|import|from|return|if|elif|else|for|while|"
        r"try|except|finally|with|as|pass|break|continue|in|not|and|or|is|"
        r"None|True|False|lambda|yield|async|await|raise|del|global|nonlocal)\b"
    )
    PYTHON_BUILTINS = (
        r"\b(print|len|range|type|str|int|float|list|dict|set|tuple|bool|"
        r"open|sum|max|min|sorted|enumerate|zip|map|filter|super|self|input|"
        r"abs|round|isinstance|hasattr|getattr|setattr|vars|dir|help)\b"
    )
    PYTHON_STR = (
        r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|f"[^"]*"|f\'[^\']*\'|'
        r'"[^"\n]*"|\'[^\'\n]*\')'
    )
    PYTHON_CMT = r"#[^\n]*"
    PYTHON_NUM = r"\b\d+(\.\d+)?\b"
    PYTHON_DECO = r"@\w+"

    SAMPLE_CODE = '''\
# 簡易IDE サンプル — Fibonacci
def fibonacci(n):
    """フィボナッチ数列を返す"""
    a, b = 0, 1
    result = []
    for _ in range(n):
        result.append(a)
        a, b = b, a + b
    return result

if __name__ == "__main__":
    nums = fibonacci(10)
    print("Fibonacci:", nums)
    print("Sum:", sum(nums))
'''

    def __init__(self, root):
        self.root = root
        self.root.title("簡易IDE")
        self.root.geometry("1200x760")
        self.root.configure(bg="#1e1e1e")

        self._tabs = {}      # tab_id -> {"path", "editor", "modified"}
        self._proc = None
        self._file_tree_path = None

        self._build_ui()
        self._new_tab()

    # ── UI構築 ────────────────────────────────────────────────────

    def _build_ui(self):
        # メニューバー
        menubar = tk.Menu(self.root, bg="#252526", fg="#ccc",
                          activebackground="#094771",
                          activeforeground="#fff", bd=0)
        self.root.configure(menu=menubar)

        file_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="ファイル", menu=file_menu)
        file_menu.add_command(label="新規タブ (Ctrl+T)",   command=self._new_tab)
        file_menu.add_command(label="開く (Ctrl+O)",       command=self._open_file)
        file_menu.add_command(label="保存 (Ctrl+S)",       command=self._save_file)
        file_menu.add_command(label="名前を付けて保存",     command=self._save_as)
        file_menu.add_separator()
        file_menu.add_command(label="タブを閉じる (Ctrl+W)", command=self._close_tab)
        file_menu.add_separator()
        file_menu.add_command(label="終了", command=self.root.quit)

        run_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                           activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="実行", menu=run_menu)
        run_menu.add_command(label="▶ 実行 (F5)",   command=self._run_code)
        run_menu.add_command(label="⏹ 停止",        command=self._stop_code)

        edit_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="編集", menu=edit_menu)
        edit_menu.add_command(label="検索/置換 (Ctrl+H)", command=self._show_search)

        # キーバインド
        self.root.bind("<Control-t>", lambda e: self._new_tab())
        self.root.bind("<Control-o>", lambda e: self._open_file())
        self.root.bind("<Control-s>", lambda e: self._save_file())
        self.root.bind("<Control-w>", lambda e: self._close_tab())
        self.root.bind("<F5>",        lambda e: self._run_code())

        # ツールバー
        toolbar = tk.Frame(self.root, bg="#2d2d2d", pady=4)
        toolbar.pack(fill=tk.X)
        for text, cmd in [("📄 新規", self._new_tab),
                           ("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("▶ 実行", self._run_code),
                           ("⏹ 停止", self._stop_code)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="#ccc", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=2,
                      activebackground="#505050", bd=0).pack(
                side=tk.LEFT, padx=2)

        tk.Label(toolbar, text="Python インタープリタ:", bg="#2d2d2d",
                 fg="#888", font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 2))
        self.python_path_var = tk.StringVar(value=sys.executable)
        ttk.Entry(toolbar, textvariable=self.python_path_var,
                  width=30).pack(side=tk.LEFT)

        self.run_status = tk.Label(toolbar, text="", bg="#2d2d2d",
                                    fg="#4fc3f7", font=("Arial", 9))
        self.run_status.pack(side=tk.RIGHT, padx=8)

        # メインエリア: ファイルツリー | タブエディタ
        main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        main.pack(fill=tk.BOTH, expand=True)

        # 左: ファイルツリー
        tree_frame = tk.Frame(main, bg="#252526", width=180)
        main.add(tree_frame, weight=0)
        self._build_file_tree(tree_frame)

        # 右: エディタ + ターミナル
        right = tk.Frame(main, bg="#1e1e1e")
        main.add(right, weight=1)

        editor_paned = ttk.PanedWindow(right, orient=tk.VERTICAL)
        editor_paned.pack(fill=tk.BOTH, expand=True)

        # タブエディタ
        editor_area = tk.Frame(editor_paned, bg="#1e1e1e")
        editor_paned.add(editor_area, weight=3)
        self._build_editor_area(editor_area)

        # ターミナル出力
        term_area = tk.Frame(editor_paned, bg="#0d1117")
        editor_paned.add(term_area, weight=1)
        self._build_terminal(term_area)

        # ステータスバー
        self.status_var = tk.StringVar(value="準備完了")
        sb = tk.Frame(self.root, bg="#007acc", pady=2)
        sb.pack(fill=tk.X, side=tk.BOTTOM)
        self.status_lbl = tk.Label(sb, textvariable=self.status_var,
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8), anchor="w", padx=8)
        self.status_lbl.pack(side=tk.LEFT)
        self.cursor_lbl = tk.Label(sb, text="行 1, 列 1",
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8))
        self.cursor_lbl.pack(side=tk.RIGHT, padx=8)

    def _build_file_tree(self, parent):
        hdr = tk.Frame(parent, bg="#252526")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="📁 エクスプローラー", bg="#252526",
                 fg="#ccc", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=4, pady=4)
        ttk.Button(hdr, text="…",
                   command=self._open_folder).pack(side=tk.RIGHT, padx=2)

        self.file_tree = ttk.Treeview(parent, show="tree", selectmode="browse")
        fsb = ttk.Scrollbar(parent, command=self.file_tree.yview)
        self.file_tree.configure(yscrollcommand=fsb.set)
        fsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.file_tree.pack(fill=tk.BOTH, expand=True)
        self.file_tree.bind("<Double-1>", self._on_tree_double_click)

    def _build_editor_area(self, parent):
        # タブバー
        self.notebook = ttk.Notebook(parent)
        self.notebook.pack(fill=tk.BOTH, expand=True)
        self.notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)

        # 検索バー(非表示状態で配置)
        self.search_bar = tk.Frame(parent, bg="#252526", pady=4)
        tk.Label(self.search_bar, text="検索:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.search_entry = ttk.Entry(self.search_bar, width=20)
        self.search_entry.pack(side=tk.LEFT, padx=2)
        tk.Label(self.search_bar, text="置換:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.replace_entry = ttk.Entry(self.search_bar, width=20)
        self.replace_entry.pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="次へ",
                   command=self._find_next).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="置換",
                   command=self._replace_one).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="全置換",
                   command=self._replace_all).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="✕",
                   command=self._hide_search).pack(side=tk.LEFT, padx=4)
        self._search_visible = False

    def _build_terminal(self, parent):
        hdr = tk.Frame(parent, bg="#1a1a2e")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="ターミナル", bg="#1a1a2e",
                 fg="#4fc3f7", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=6, pady=2)
        ttk.Button(hdr, text="クリア",
                   command=self._clear_terminal).pack(side=tk.RIGHT, padx=4)

        self.terminal = tk.Text(parent, bg="#0d1117", fg="#d4d4d4",
                                 font=("Courier New", 9), relief=tk.FLAT,
                                 state=tk.DISABLED, wrap=tk.WORD)
        tsb = ttk.Scrollbar(parent, command=self.terminal.yview)
        self.terminal.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.terminal.pack(fill=tk.BOTH, expand=True)

        self.terminal.tag_configure("err",  foreground="#f48771")
        self.terminal.tag_configure("info", foreground="#4fc3f7")
        self.terminal.tag_configure("ok",   foreground="#4ec9b0")

    # ── タブ管理 ──────────────────────────────────────────────────

    def _new_tab(self, path=None, content=None):
        frame = tk.Frame(self.notebook, bg="#1e1e1e")

        # 行番号キャンバス + エディタ
        code_area = tk.Frame(frame, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

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

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

        # 初期コンテンツ
        initial = content if content is not None else self.SAMPLE_CODE
        editor.insert("1.0", initial)

        label = os.path.basename(path) if path else self.TAB_UNTITLED
        self.notebook.add(frame, text=f"  {label}  ")
        tab_id = frame

        self._tabs[id(tab_id)] = {
            "path": path,
            "editor": editor,
            "line_canvas": line_canvas,
            "modified": False,
            "frame": frame,
        }

        # イベント
        def on_key(e, tid=id(tab_id)):
            info = self._tabs.get(tid)
            if info and not info["modified"]:
                info["modified"] = True
                idx = self.notebook.index(frame)
                cur_text = self.notebook.tab(idx, "text")
                if not cur_text.startswith("●"):
                    self.notebook.tab(idx, text="● " + cur_text.strip())
            self._update_line_numbers(tid)
            self._highlight(tid)
            self._update_cursor_pos(e.widget)

        editor.bind("<KeyRelease>", on_key)
        editor.bind("<ButtonRelease-1>",
                    lambda e: self._update_cursor_pos(e.widget))

        self.notebook.select(frame)
        self._update_line_numbers(id(tab_id))
        self._highlight(id(tab_id))

    def _current_tab(self):
        try:
            frame = self.notebook.nametowidget(self.notebook.select())
            return id(frame), self._tabs.get(id(frame))
        except Exception:
            return None, None

    def _on_tab_changed(self, event=None):
        _, info = self._current_tab()
        if info:
            path = info["path"] or self.TAB_UNTITLED
            self.status_var.set(path)

    def _close_tab(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["modified"]:
            name = os.path.basename(info["path"]) if info["path"] else self.TAB_UNTITLED
            ans = messagebox.askyesnocancel("保存確認",
                                             f"「{name}」は変更されています。保存しますか?")
            if ans is None:
                return
            if ans:
                self._save_file()
        frame = info["frame"]
        self.notebook.forget(frame)
        del self._tabs[tid]

    # ── ファイル操作 ──────────────────────────────────────────────

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            with open(path, encoding="utf-8") as f:
                content = f.read()
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        self._new_tab(path=path, content=content)

    def _save_file(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["path"] is None:
            self._save_as()
            return
        self._do_save(tid, info, info["path"])

    def _save_as(self):
        tid, info = self._current_tab()
        if info is None:
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".py",
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        self._do_save(tid, info, path)

    def _do_save(self, tid, info, path):
        content = info["editor"].get("1.0", tk.END)
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        info["path"] = path
        info["modified"] = False
        idx = self.notebook.index(info["frame"])
        self.notebook.tab(idx, text=f"  {os.path.basename(path)}  ")
        self.status_var.set(f"保存: {path}")

    # ── ファイルツリー ────────────────────────────────────────────

    def _open_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self._file_tree_path = folder
            self._refresh_file_tree()

    def _refresh_file_tree(self):
        folder = self._file_tree_path
        if not folder:
            return
        self.file_tree.delete(*self.file_tree.get_children())
        root_node = self.file_tree.insert(
            "", tk.END, text=os.path.basename(folder),
            values=[folder], open=True)
        try:
            for name in sorted(os.listdir(folder)):
                full = os.path.join(folder, name)
                if os.path.isfile(full):
                    self.file_tree.insert(root_node, tk.END,
                                           text=name, values=[full])
        except Exception:
            pass

    def _on_tree_double_click(self, event=None):
        sel = self.file_tree.selection()
        if not sel:
            return
        vals = self.file_tree.item(sel[0], "values")
        if not vals:
            return
        path = vals[0]
        if os.path.isfile(path):
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self._new_tab(path=path, content=content)
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    # ── 実行 ──────────────────────────────────────────────────────

    def _run_code(self):
        _, info = self._current_tab()
        if info is None:
            return
        code = info["editor"].get("1.0", tk.END)
        path = info["path"]

        # 保存済みならそのファイルを実行、未保存なら一時保存
        if path and not info["modified"]:
            run_path = path
        else:
            import tempfile
            tmp = tempfile.NamedTemporaryFile(
                suffix=".py", delete=False, mode="w", encoding="utf-8")
            tmp.write(code)
            tmp.close()
            run_path = tmp.name

        self._term_write(f"▶ 実行: {os.path.basename(run_path)}\n", "info")
        self.run_status.config(text="実行中...")

        def target():
            try:
                py = self.python_path_var.get() or sys.executable
                self._proc = subprocess.Popen(
                    [py, run_path],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                    text=True, encoding="utf-8", errors="replace")
                stdout, stderr = self._proc.communicate(timeout=60)
                rc = self._proc.returncode
                if stdout:
                    self.root.after(0, self._term_write, stdout, "")
                if stderr:
                    self.root.after(0, self._term_write, stderr, "err")
                msg = f"✔ 終了 (コード {rc})" if rc == 0 else f"✖ エラー終了 (コード {rc})"
                tag = "ok" if rc == 0 else "err"
                self.root.after(0, self._term_write, msg + "\n", tag)
                self.root.after(0, self.run_status.config, {"text": ""})
            except subprocess.TimeoutExpired:
                self._proc.kill()
                self.root.after(0, self._term_write,
                                "⚠ タイムアウト (60秒)\n", "err")
            except Exception as e:
                self.root.after(0, self._term_write, str(e) + "\n", "err")
            finally:
                self._proc = None

        threading.Thread(target=target, daemon=True).start()

    def _stop_code(self):
        if self._proc:
            try:
                self._proc.kill()
            except Exception:
                pass
            self._term_write("⏹ 強制停止\n", "err")

    # ── ターミナル ────────────────────────────────────────────────

    def _term_write(self, text, tag=""):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.insert(tk.END, text, tag)
        self.terminal.see(tk.END)
        self.terminal.configure(state=tk.DISABLED)

    def _clear_terminal(self):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.delete("1.0", tk.END)
        self.terminal.configure(state=tk.DISABLED)

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

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

    def _highlight(self, tid):
        info = self._tabs.get(tid)
        if not info:
            return
        editor = info["editor"]

        for tag in ("kw", "str_", "cmt", "num", "deco", "bi"):
            editor.tag_remove(tag, "1.0", tk.END)
        editor.tag_configure("kw",   foreground="#569cd6")
        editor.tag_configure("str_", foreground="#ce9178")
        editor.tag_configure("cmt",  foreground="#6a9955")
        editor.tag_configure("num",  foreground="#b5cea8")
        editor.tag_configure("deco", foreground="#dcdcaa")
        editor.tag_configure("bi",   foreground="#4ec9b0")

        content = editor.get("1.0", tk.END)

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

        apply(self.PYTHON_NUM,     "num")
        apply(self.PYTHON_STR,     "str_")
        apply(self.PYTHON_CMT,     "cmt")
        apply(self.PYTHON_DECO,    "deco")
        apply(self.PYTHON_BUILTINS,"bi")
        apply(self.PYTHON_KW,      "kw")

    def _update_cursor_pos(self, widget):
        try:
            pos = widget.index(tk.INSERT)
            line, col = pos.split(".")
            self.cursor_lbl.config(text=f"行 {line}, 列 {int(col)+1}")
        except Exception:
            pass

    # ── 検索・置換 ────────────────────────────────────────────────

    def _show_search(self):
        if not self._search_visible:
            self.search_bar.pack(fill=tk.X, before=self.notebook)
            self._search_visible = True
            self.search_entry.focus_set()

    def _hide_search(self):
        if self._search_visible:
            self.search_bar.pack_forget()
            self._search_visible = False

    def _find_next(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        if not query:
            return
        editor.tag_remove("search_hl", "1.0", tk.END)
        editor.tag_configure("search_hl", background="#4d4000",
                              foreground="#fff")
        start = editor.index(tk.INSERT)
        pos = editor.search(query, start, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.tag_add("search_hl", pos, end_pos)
            editor.mark_set(tk.INSERT, end_pos)
            editor.see(pos)

    def _replace_one(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        pos = editor.search(query, tk.INSERT, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.delete(pos, end_pos)
            editor.insert(pos, replacement)

    def _replace_all(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        content = editor.get("1.0", tk.END)
        count = content.count(query)
        if count:
            new_content = content.replace(query, replacement)
            editor.delete("1.0", tk.END)
            editor.insert("1.0", new_content)
            self.status_var.set(f"{count} 件置換しました")


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

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

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

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import re
import sys
import subprocess
import threading
from datetime import datetime


class App50:
    """マルチタブ簡易IDE"""

    TAB_UNTITLED = "無題"

    PYTHON_KW = (
        r"\b(def|class|import|from|return|if|elif|else|for|while|"
        r"try|except|finally|with|as|pass|break|continue|in|not|and|or|is|"
        r"None|True|False|lambda|yield|async|await|raise|del|global|nonlocal)\b"
    )
    PYTHON_BUILTINS = (
        r"\b(print|len|range|type|str|int|float|list|dict|set|tuple|bool|"
        r"open|sum|max|min|sorted|enumerate|zip|map|filter|super|self|input|"
        r"abs|round|isinstance|hasattr|getattr|setattr|vars|dir|help)\b"
    )
    PYTHON_STR = (
        r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|f"[^"]*"|f\'[^\']*\'|'
        r'"[^"\n]*"|\'[^\'\n]*\')'
    )
    PYTHON_CMT = r"#[^\n]*"
    PYTHON_NUM = r"\b\d+(\.\d+)?\b"
    PYTHON_DECO = r"@\w+"

    SAMPLE_CODE = '''\
# 簡易IDE サンプル — Fibonacci
def fibonacci(n):
    """フィボナッチ数列を返す"""
    a, b = 0, 1
    result = []
    for _ in range(n):
        result.append(a)
        a, b = b, a + b
    return result

if __name__ == "__main__":
    nums = fibonacci(10)
    print("Fibonacci:", nums)
    print("Sum:", sum(nums))
'''

    def __init__(self, root):
        self.root = root
        self.root.title("簡易IDE")
        self.root.geometry("1200x760")
        self.root.configure(bg="#1e1e1e")

        self._tabs = {}      # tab_id -> {"path", "editor", "modified"}
        self._proc = None
        self._file_tree_path = None

        self._build_ui()
        self._new_tab()

    # ── UI構築 ────────────────────────────────────────────────────

    def _build_ui(self):
        # メニューバー
        menubar = tk.Menu(self.root, bg="#252526", fg="#ccc",
                          activebackground="#094771",
                          activeforeground="#fff", bd=0)
        self.root.configure(menu=menubar)

        file_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="ファイル", menu=file_menu)
        file_menu.add_command(label="新規タブ (Ctrl+T)",   command=self._new_tab)
        file_menu.add_command(label="開く (Ctrl+O)",       command=self._open_file)
        file_menu.add_command(label="保存 (Ctrl+S)",       command=self._save_file)
        file_menu.add_command(label="名前を付けて保存",     command=self._save_as)
        file_menu.add_separator()
        file_menu.add_command(label="タブを閉じる (Ctrl+W)", command=self._close_tab)
        file_menu.add_separator()
        file_menu.add_command(label="終了", command=self.root.quit)

        run_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                           activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="実行", menu=run_menu)
        run_menu.add_command(label="▶ 実行 (F5)",   command=self._run_code)
        run_menu.add_command(label="⏹ 停止",        command=self._stop_code)

        edit_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="編集", menu=edit_menu)
        edit_menu.add_command(label="検索/置換 (Ctrl+H)", command=self._show_search)

        # キーバインド
        self.root.bind("<Control-t>", lambda e: self._new_tab())
        self.root.bind("<Control-o>", lambda e: self._open_file())
        self.root.bind("<Control-s>", lambda e: self._save_file())
        self.root.bind("<Control-w>", lambda e: self._close_tab())
        self.root.bind("<F5>",        lambda e: self._run_code())

        # ツールバー
        toolbar = tk.Frame(self.root, bg="#2d2d2d", pady=4)
        toolbar.pack(fill=tk.X)
        for text, cmd in [("📄 新規", self._new_tab),
                           ("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("▶ 実行", self._run_code),
                           ("⏹ 停止", self._stop_code)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="#ccc", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=2,
                      activebackground="#505050", bd=0).pack(
                side=tk.LEFT, padx=2)

        tk.Label(toolbar, text="Python インタープリタ:", bg="#2d2d2d",
                 fg="#888", font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 2))
        self.python_path_var = tk.StringVar(value=sys.executable)
        ttk.Entry(toolbar, textvariable=self.python_path_var,
                  width=30).pack(side=tk.LEFT)

        self.run_status = tk.Label(toolbar, text="", bg="#2d2d2d",
                                    fg="#4fc3f7", font=("Arial", 9))
        self.run_status.pack(side=tk.RIGHT, padx=8)

        # メインエリア: ファイルツリー | タブエディタ
        main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        main.pack(fill=tk.BOTH, expand=True)

        # 左: ファイルツリー
        tree_frame = tk.Frame(main, bg="#252526", width=180)
        main.add(tree_frame, weight=0)
        self._build_file_tree(tree_frame)

        # 右: エディタ + ターミナル
        right = tk.Frame(main, bg="#1e1e1e")
        main.add(right, weight=1)

        editor_paned = ttk.PanedWindow(right, orient=tk.VERTICAL)
        editor_paned.pack(fill=tk.BOTH, expand=True)

        # タブエディタ
        editor_area = tk.Frame(editor_paned, bg="#1e1e1e")
        editor_paned.add(editor_area, weight=3)
        self._build_editor_area(editor_area)

        # ターミナル出力
        term_area = tk.Frame(editor_paned, bg="#0d1117")
        editor_paned.add(term_area, weight=1)
        self._build_terminal(term_area)

        # ステータスバー
        self.status_var = tk.StringVar(value="準備完了")
        sb = tk.Frame(self.root, bg="#007acc", pady=2)
        sb.pack(fill=tk.X, side=tk.BOTTOM)
        self.status_lbl = tk.Label(sb, textvariable=self.status_var,
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8), anchor="w", padx=8)
        self.status_lbl.pack(side=tk.LEFT)
        self.cursor_lbl = tk.Label(sb, text="行 1, 列 1",
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8))
        self.cursor_lbl.pack(side=tk.RIGHT, padx=8)

    def _build_file_tree(self, parent):
        hdr = tk.Frame(parent, bg="#252526")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="📁 エクスプローラー", bg="#252526",
                 fg="#ccc", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=4, pady=4)
        ttk.Button(hdr, text="…",
                   command=self._open_folder).pack(side=tk.RIGHT, padx=2)

        self.file_tree = ttk.Treeview(parent, show="tree", selectmode="browse")
        fsb = ttk.Scrollbar(parent, command=self.file_tree.yview)
        self.file_tree.configure(yscrollcommand=fsb.set)
        fsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.file_tree.pack(fill=tk.BOTH, expand=True)
        self.file_tree.bind("<Double-1>", self._on_tree_double_click)

    def _build_editor_area(self, parent):
        # タブバー
        self.notebook = ttk.Notebook(parent)
        self.notebook.pack(fill=tk.BOTH, expand=True)
        self.notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)

        # 検索バー(非表示状態で配置)
        self.search_bar = tk.Frame(parent, bg="#252526", pady=4)
        tk.Label(self.search_bar, text="検索:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.search_entry = ttk.Entry(self.search_bar, width=20)
        self.search_entry.pack(side=tk.LEFT, padx=2)
        tk.Label(self.search_bar, text="置換:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.replace_entry = ttk.Entry(self.search_bar, width=20)
        self.replace_entry.pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="次へ",
                   command=self._find_next).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="置換",
                   command=self._replace_one).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="全置換",
                   command=self._replace_all).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="✕",
                   command=self._hide_search).pack(side=tk.LEFT, padx=4)
        self._search_visible = False

    def _build_terminal(self, parent):
        hdr = tk.Frame(parent, bg="#1a1a2e")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="ターミナル", bg="#1a1a2e",
                 fg="#4fc3f7", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=6, pady=2)
        ttk.Button(hdr, text="クリア",
                   command=self._clear_terminal).pack(side=tk.RIGHT, padx=4)

        self.terminal = tk.Text(parent, bg="#0d1117", fg="#d4d4d4",
                                 font=("Courier New", 9), relief=tk.FLAT,
                                 state=tk.DISABLED, wrap=tk.WORD)
        tsb = ttk.Scrollbar(parent, command=self.terminal.yview)
        self.terminal.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.terminal.pack(fill=tk.BOTH, expand=True)

        self.terminal.tag_configure("err",  foreground="#f48771")
        self.terminal.tag_configure("info", foreground="#4fc3f7")
        self.terminal.tag_configure("ok",   foreground="#4ec9b0")

    # ── タブ管理 ──────────────────────────────────────────────────

    def _new_tab(self, path=None, content=None):
        frame = tk.Frame(self.notebook, bg="#1e1e1e")

        # 行番号キャンバス + エディタ
        code_area = tk.Frame(frame, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

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

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

        # 初期コンテンツ
        initial = content if content is not None else self.SAMPLE_CODE
        editor.insert("1.0", initial)

        label = os.path.basename(path) if path else self.TAB_UNTITLED
        self.notebook.add(frame, text=f"  {label}  ")
        tab_id = frame

        self._tabs[id(tab_id)] = {
            "path": path,
            "editor": editor,
            "line_canvas": line_canvas,
            "modified": False,
            "frame": frame,
        }

        # イベント
        def on_key(e, tid=id(tab_id)):
            info = self._tabs.get(tid)
            if info and not info["modified"]:
                info["modified"] = True
                idx = self.notebook.index(frame)
                cur_text = self.notebook.tab(idx, "text")
                if not cur_text.startswith("●"):
                    self.notebook.tab(idx, text="● " + cur_text.strip())
            self._update_line_numbers(tid)
            self._highlight(tid)
            self._update_cursor_pos(e.widget)

        editor.bind("<KeyRelease>", on_key)
        editor.bind("<ButtonRelease-1>",
                    lambda e: self._update_cursor_pos(e.widget))

        self.notebook.select(frame)
        self._update_line_numbers(id(tab_id))
        self._highlight(id(tab_id))

    def _current_tab(self):
        try:
            frame = self.notebook.nametowidget(self.notebook.select())
            return id(frame), self._tabs.get(id(frame))
        except Exception:
            return None, None

    def _on_tab_changed(self, event=None):
        _, info = self._current_tab()
        if info:
            path = info["path"] or self.TAB_UNTITLED
            self.status_var.set(path)

    def _close_tab(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["modified"]:
            name = os.path.basename(info["path"]) if info["path"] else self.TAB_UNTITLED
            ans = messagebox.askyesnocancel("保存確認",
                                             f"「{name}」は変更されています。保存しますか?")
            if ans is None:
                return
            if ans:
                self._save_file()
        frame = info["frame"]
        self.notebook.forget(frame)
        del self._tabs[tid]

    # ── ファイル操作 ──────────────────────────────────────────────

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            with open(path, encoding="utf-8") as f:
                content = f.read()
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        self._new_tab(path=path, content=content)

    def _save_file(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["path"] is None:
            self._save_as()
            return
        self._do_save(tid, info, info["path"])

    def _save_as(self):
        tid, info = self._current_tab()
        if info is None:
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".py",
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        self._do_save(tid, info, path)

    def _do_save(self, tid, info, path):
        content = info["editor"].get("1.0", tk.END)
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        info["path"] = path
        info["modified"] = False
        idx = self.notebook.index(info["frame"])
        self.notebook.tab(idx, text=f"  {os.path.basename(path)}  ")
        self.status_var.set(f"保存: {path}")

    # ── ファイルツリー ────────────────────────────────────────────

    def _open_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self._file_tree_path = folder
            self._refresh_file_tree()

    def _refresh_file_tree(self):
        folder = self._file_tree_path
        if not folder:
            return
        self.file_tree.delete(*self.file_tree.get_children())
        root_node = self.file_tree.insert(
            "", tk.END, text=os.path.basename(folder),
            values=[folder], open=True)
        try:
            for name in sorted(os.listdir(folder)):
                full = os.path.join(folder, name)
                if os.path.isfile(full):
                    self.file_tree.insert(root_node, tk.END,
                                           text=name, values=[full])
        except Exception:
            pass

    def _on_tree_double_click(self, event=None):
        sel = self.file_tree.selection()
        if not sel:
            return
        vals = self.file_tree.item(sel[0], "values")
        if not vals:
            return
        path = vals[0]
        if os.path.isfile(path):
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self._new_tab(path=path, content=content)
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    # ── 実行 ──────────────────────────────────────────────────────

    def _run_code(self):
        _, info = self._current_tab()
        if info is None:
            return
        code = info["editor"].get("1.0", tk.END)
        path = info["path"]

        # 保存済みならそのファイルを実行、未保存なら一時保存
        if path and not info["modified"]:
            run_path = path
        else:
            import tempfile
            tmp = tempfile.NamedTemporaryFile(
                suffix=".py", delete=False, mode="w", encoding="utf-8")
            tmp.write(code)
            tmp.close()
            run_path = tmp.name

        self._term_write(f"▶ 実行: {os.path.basename(run_path)}\n", "info")
        self.run_status.config(text="実行中...")

        def target():
            try:
                py = self.python_path_var.get() or sys.executable
                self._proc = subprocess.Popen(
                    [py, run_path],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                    text=True, encoding="utf-8", errors="replace")
                stdout, stderr = self._proc.communicate(timeout=60)
                rc = self._proc.returncode
                if stdout:
                    self.root.after(0, self._term_write, stdout, "")
                if stderr:
                    self.root.after(0, self._term_write, stderr, "err")
                msg = f"✔ 終了 (コード {rc})" if rc == 0 else f"✖ エラー終了 (コード {rc})"
                tag = "ok" if rc == 0 else "err"
                self.root.after(0, self._term_write, msg + "\n", tag)
                self.root.after(0, self.run_status.config, {"text": ""})
            except subprocess.TimeoutExpired:
                self._proc.kill()
                self.root.after(0, self._term_write,
                                "⚠ タイムアウト (60秒)\n", "err")
            except Exception as e:
                self.root.after(0, self._term_write, str(e) + "\n", "err")
            finally:
                self._proc = None

        threading.Thread(target=target, daemon=True).start()

    def _stop_code(self):
        if self._proc:
            try:
                self._proc.kill()
            except Exception:
                pass
            self._term_write("⏹ 強制停止\n", "err")

    # ── ターミナル ────────────────────────────────────────────────

    def _term_write(self, text, tag=""):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.insert(tk.END, text, tag)
        self.terminal.see(tk.END)
        self.terminal.configure(state=tk.DISABLED)

    def _clear_terminal(self):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.delete("1.0", tk.END)
        self.terminal.configure(state=tk.DISABLED)

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

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

    def _highlight(self, tid):
        info = self._tabs.get(tid)
        if not info:
            return
        editor = info["editor"]

        for tag in ("kw", "str_", "cmt", "num", "deco", "bi"):
            editor.tag_remove(tag, "1.0", tk.END)
        editor.tag_configure("kw",   foreground="#569cd6")
        editor.tag_configure("str_", foreground="#ce9178")
        editor.tag_configure("cmt",  foreground="#6a9955")
        editor.tag_configure("num",  foreground="#b5cea8")
        editor.tag_configure("deco", foreground="#dcdcaa")
        editor.tag_configure("bi",   foreground="#4ec9b0")

        content = editor.get("1.0", tk.END)

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

        apply(self.PYTHON_NUM,     "num")
        apply(self.PYTHON_STR,     "str_")
        apply(self.PYTHON_CMT,     "cmt")
        apply(self.PYTHON_DECO,    "deco")
        apply(self.PYTHON_BUILTINS,"bi")
        apply(self.PYTHON_KW,      "kw")

    def _update_cursor_pos(self, widget):
        try:
            pos = widget.index(tk.INSERT)
            line, col = pos.split(".")
            self.cursor_lbl.config(text=f"行 {line}, 列 {int(col)+1}")
        except Exception:
            pass

    # ── 検索・置換 ────────────────────────────────────────────────

    def _show_search(self):
        if not self._search_visible:
            self.search_bar.pack(fill=tk.X, before=self.notebook)
            self._search_visible = True
            self.search_entry.focus_set()

    def _hide_search(self):
        if self._search_visible:
            self.search_bar.pack_forget()
            self._search_visible = False

    def _find_next(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        if not query:
            return
        editor.tag_remove("search_hl", "1.0", tk.END)
        editor.tag_configure("search_hl", background="#4d4000",
                              foreground="#fff")
        start = editor.index(tk.INSERT)
        pos = editor.search(query, start, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.tag_add("search_hl", pos, end_pos)
            editor.mark_set(tk.INSERT, end_pos)
            editor.see(pos)

    def _replace_one(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        pos = editor.search(query, tk.INSERT, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.delete(pos, end_pos)
            editor.insert(pos, replacement)

    def _replace_all(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        content = editor.get("1.0", tk.END)
        count = content.count(query)
        if count:
            new_content = content.replace(query, replacement)
            editor.delete("1.0", tk.END)
            editor.insert("1.0", new_content)
            self.status_var.set(f"{count} 件置換しました")


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

例外処理とmessagebox

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

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import re
import sys
import subprocess
import threading
from datetime import datetime


class App50:
    """マルチタブ簡易IDE"""

    TAB_UNTITLED = "無題"

    PYTHON_KW = (
        r"\b(def|class|import|from|return|if|elif|else|for|while|"
        r"try|except|finally|with|as|pass|break|continue|in|not|and|or|is|"
        r"None|True|False|lambda|yield|async|await|raise|del|global|nonlocal)\b"
    )
    PYTHON_BUILTINS = (
        r"\b(print|len|range|type|str|int|float|list|dict|set|tuple|bool|"
        r"open|sum|max|min|sorted|enumerate|zip|map|filter|super|self|input|"
        r"abs|round|isinstance|hasattr|getattr|setattr|vars|dir|help)\b"
    )
    PYTHON_STR = (
        r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|f"[^"]*"|f\'[^\']*\'|'
        r'"[^"\n]*"|\'[^\'\n]*\')'
    )
    PYTHON_CMT = r"#[^\n]*"
    PYTHON_NUM = r"\b\d+(\.\d+)?\b"
    PYTHON_DECO = r"@\w+"

    SAMPLE_CODE = '''\
# 簡易IDE サンプル — Fibonacci
def fibonacci(n):
    """フィボナッチ数列を返す"""
    a, b = 0, 1
    result = []
    for _ in range(n):
        result.append(a)
        a, b = b, a + b
    return result

if __name__ == "__main__":
    nums = fibonacci(10)
    print("Fibonacci:", nums)
    print("Sum:", sum(nums))
'''

    def __init__(self, root):
        self.root = root
        self.root.title("簡易IDE")
        self.root.geometry("1200x760")
        self.root.configure(bg="#1e1e1e")

        self._tabs = {}      # tab_id -> {"path", "editor", "modified"}
        self._proc = None
        self._file_tree_path = None

        self._build_ui()
        self._new_tab()

    # ── UI構築 ────────────────────────────────────────────────────

    def _build_ui(self):
        # メニューバー
        menubar = tk.Menu(self.root, bg="#252526", fg="#ccc",
                          activebackground="#094771",
                          activeforeground="#fff", bd=0)
        self.root.configure(menu=menubar)

        file_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="ファイル", menu=file_menu)
        file_menu.add_command(label="新規タブ (Ctrl+T)",   command=self._new_tab)
        file_menu.add_command(label="開く (Ctrl+O)",       command=self._open_file)
        file_menu.add_command(label="保存 (Ctrl+S)",       command=self._save_file)
        file_menu.add_command(label="名前を付けて保存",     command=self._save_as)
        file_menu.add_separator()
        file_menu.add_command(label="タブを閉じる (Ctrl+W)", command=self._close_tab)
        file_menu.add_separator()
        file_menu.add_command(label="終了", command=self.root.quit)

        run_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                           activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="実行", menu=run_menu)
        run_menu.add_command(label="▶ 実行 (F5)",   command=self._run_code)
        run_menu.add_command(label="⏹ 停止",        command=self._stop_code)

        edit_menu = tk.Menu(menubar, tearoff=False, bg="#252526", fg="#ccc",
                            activebackground="#094771", activeforeground="#fff")
        menubar.add_cascade(label="編集", menu=edit_menu)
        edit_menu.add_command(label="検索/置換 (Ctrl+H)", command=self._show_search)

        # キーバインド
        self.root.bind("<Control-t>", lambda e: self._new_tab())
        self.root.bind("<Control-o>", lambda e: self._open_file())
        self.root.bind("<Control-s>", lambda e: self._save_file())
        self.root.bind("<Control-w>", lambda e: self._close_tab())
        self.root.bind("<F5>",        lambda e: self._run_code())

        # ツールバー
        toolbar = tk.Frame(self.root, bg="#2d2d2d", pady=4)
        toolbar.pack(fill=tk.X)
        for text, cmd in [("📄 新規", self._new_tab),
                           ("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("▶ 実行", self._run_code),
                           ("⏹ 停止", self._stop_code)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="#ccc", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=2,
                      activebackground="#505050", bd=0).pack(
                side=tk.LEFT, padx=2)

        tk.Label(toolbar, text="Python インタープリタ:", bg="#2d2d2d",
                 fg="#888", font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 2))
        self.python_path_var = tk.StringVar(value=sys.executable)
        ttk.Entry(toolbar, textvariable=self.python_path_var,
                  width=30).pack(side=tk.LEFT)

        self.run_status = tk.Label(toolbar, text="", bg="#2d2d2d",
                                    fg="#4fc3f7", font=("Arial", 9))
        self.run_status.pack(side=tk.RIGHT, padx=8)

        # メインエリア: ファイルツリー | タブエディタ
        main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        main.pack(fill=tk.BOTH, expand=True)

        # 左: ファイルツリー
        tree_frame = tk.Frame(main, bg="#252526", width=180)
        main.add(tree_frame, weight=0)
        self._build_file_tree(tree_frame)

        # 右: エディタ + ターミナル
        right = tk.Frame(main, bg="#1e1e1e")
        main.add(right, weight=1)

        editor_paned = ttk.PanedWindow(right, orient=tk.VERTICAL)
        editor_paned.pack(fill=tk.BOTH, expand=True)

        # タブエディタ
        editor_area = tk.Frame(editor_paned, bg="#1e1e1e")
        editor_paned.add(editor_area, weight=3)
        self._build_editor_area(editor_area)

        # ターミナル出力
        term_area = tk.Frame(editor_paned, bg="#0d1117")
        editor_paned.add(term_area, weight=1)
        self._build_terminal(term_area)

        # ステータスバー
        self.status_var = tk.StringVar(value="準備完了")
        sb = tk.Frame(self.root, bg="#007acc", pady=2)
        sb.pack(fill=tk.X, side=tk.BOTTOM)
        self.status_lbl = tk.Label(sb, textvariable=self.status_var,
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8), anchor="w", padx=8)
        self.status_lbl.pack(side=tk.LEFT)
        self.cursor_lbl = tk.Label(sb, text="行 1, 列 1",
                                    bg="#007acc", fg="#fff",
                                    font=("Arial", 8))
        self.cursor_lbl.pack(side=tk.RIGHT, padx=8)

    def _build_file_tree(self, parent):
        hdr = tk.Frame(parent, bg="#252526")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="📁 エクスプローラー", bg="#252526",
                 fg="#ccc", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=4, pady=4)
        ttk.Button(hdr, text="…",
                   command=self._open_folder).pack(side=tk.RIGHT, padx=2)

        self.file_tree = ttk.Treeview(parent, show="tree", selectmode="browse")
        fsb = ttk.Scrollbar(parent, command=self.file_tree.yview)
        self.file_tree.configure(yscrollcommand=fsb.set)
        fsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.file_tree.pack(fill=tk.BOTH, expand=True)
        self.file_tree.bind("<Double-1>", self._on_tree_double_click)

    def _build_editor_area(self, parent):
        # タブバー
        self.notebook = ttk.Notebook(parent)
        self.notebook.pack(fill=tk.BOTH, expand=True)
        self.notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)

        # 検索バー(非表示状態で配置)
        self.search_bar = tk.Frame(parent, bg="#252526", pady=4)
        tk.Label(self.search_bar, text="検索:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.search_entry = ttk.Entry(self.search_bar, width=20)
        self.search_entry.pack(side=tk.LEFT, padx=2)
        tk.Label(self.search_bar, text="置換:", bg="#252526",
                 fg="#ccc", font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.replace_entry = ttk.Entry(self.search_bar, width=20)
        self.replace_entry.pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="次へ",
                   command=self._find_next).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="置換",
                   command=self._replace_one).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="全置換",
                   command=self._replace_all).pack(side=tk.LEFT, padx=2)
        ttk.Button(self.search_bar, text="✕",
                   command=self._hide_search).pack(side=tk.LEFT, padx=4)
        self._search_visible = False

    def _build_terminal(self, parent):
        hdr = tk.Frame(parent, bg="#1a1a2e")
        hdr.pack(fill=tk.X)
        tk.Label(hdr, text="ターミナル", bg="#1a1a2e",
                 fg="#4fc3f7", font=("Arial", 9, "bold")).pack(
            side=tk.LEFT, padx=6, pady=2)
        ttk.Button(hdr, text="クリア",
                   command=self._clear_terminal).pack(side=tk.RIGHT, padx=4)

        self.terminal = tk.Text(parent, bg="#0d1117", fg="#d4d4d4",
                                 font=("Courier New", 9), relief=tk.FLAT,
                                 state=tk.DISABLED, wrap=tk.WORD)
        tsb = ttk.Scrollbar(parent, command=self.terminal.yview)
        self.terminal.configure(yscrollcommand=tsb.set)
        tsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.terminal.pack(fill=tk.BOTH, expand=True)

        self.terminal.tag_configure("err",  foreground="#f48771")
        self.terminal.tag_configure("info", foreground="#4fc3f7")
        self.terminal.tag_configure("ok",   foreground="#4ec9b0")

    # ── タブ管理 ──────────────────────────────────────────────────

    def _new_tab(self, path=None, content=None):
        frame = tk.Frame(self.notebook, bg="#1e1e1e")

        # 行番号キャンバス + エディタ
        code_area = tk.Frame(frame, bg="#1e1e1e")
        code_area.pack(fill=tk.BOTH, expand=True)

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

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

        # 初期コンテンツ
        initial = content if content is not None else self.SAMPLE_CODE
        editor.insert("1.0", initial)

        label = os.path.basename(path) if path else self.TAB_UNTITLED
        self.notebook.add(frame, text=f"  {label}  ")
        tab_id = frame

        self._tabs[id(tab_id)] = {
            "path": path,
            "editor": editor,
            "line_canvas": line_canvas,
            "modified": False,
            "frame": frame,
        }

        # イベント
        def on_key(e, tid=id(tab_id)):
            info = self._tabs.get(tid)
            if info and not info["modified"]:
                info["modified"] = True
                idx = self.notebook.index(frame)
                cur_text = self.notebook.tab(idx, "text")
                if not cur_text.startswith("●"):
                    self.notebook.tab(idx, text="● " + cur_text.strip())
            self._update_line_numbers(tid)
            self._highlight(tid)
            self._update_cursor_pos(e.widget)

        editor.bind("<KeyRelease>", on_key)
        editor.bind("<ButtonRelease-1>",
                    lambda e: self._update_cursor_pos(e.widget))

        self.notebook.select(frame)
        self._update_line_numbers(id(tab_id))
        self._highlight(id(tab_id))

    def _current_tab(self):
        try:
            frame = self.notebook.nametowidget(self.notebook.select())
            return id(frame), self._tabs.get(id(frame))
        except Exception:
            return None, None

    def _on_tab_changed(self, event=None):
        _, info = self._current_tab()
        if info:
            path = info["path"] or self.TAB_UNTITLED
            self.status_var.set(path)

    def _close_tab(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["modified"]:
            name = os.path.basename(info["path"]) if info["path"] else self.TAB_UNTITLED
            ans = messagebox.askyesnocancel("保存確認",
                                             f"「{name}」は変更されています。保存しますか?")
            if ans is None:
                return
            if ans:
                self._save_file()
        frame = info["frame"]
        self.notebook.forget(frame)
        del self._tabs[tid]

    # ── ファイル操作 ──────────────────────────────────────────────

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            with open(path, encoding="utf-8") as f:
                content = f.read()
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        self._new_tab(path=path, content=content)

    def _save_file(self):
        tid, info = self._current_tab()
        if info is None:
            return
        if info["path"] is None:
            self._save_as()
            return
        self._do_save(tid, info, info["path"])

    def _save_as(self):
        tid, info = self._current_tab()
        if info is None:
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".py",
            filetypes=[("Python", "*.py"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if not path:
            return
        self._do_save(tid, info, path)

    def _do_save(self, tid, info, path):
        content = info["editor"].get("1.0", tk.END)
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            return
        info["path"] = path
        info["modified"] = False
        idx = self.notebook.index(info["frame"])
        self.notebook.tab(idx, text=f"  {os.path.basename(path)}  ")
        self.status_var.set(f"保存: {path}")

    # ── ファイルツリー ────────────────────────────────────────────

    def _open_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self._file_tree_path = folder
            self._refresh_file_tree()

    def _refresh_file_tree(self):
        folder = self._file_tree_path
        if not folder:
            return
        self.file_tree.delete(*self.file_tree.get_children())
        root_node = self.file_tree.insert(
            "", tk.END, text=os.path.basename(folder),
            values=[folder], open=True)
        try:
            for name in sorted(os.listdir(folder)):
                full = os.path.join(folder, name)
                if os.path.isfile(full):
                    self.file_tree.insert(root_node, tk.END,
                                           text=name, values=[full])
        except Exception:
            pass

    def _on_tree_double_click(self, event=None):
        sel = self.file_tree.selection()
        if not sel:
            return
        vals = self.file_tree.item(sel[0], "values")
        if not vals:
            return
        path = vals[0]
        if os.path.isfile(path):
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self._new_tab(path=path, content=content)
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    # ── 実行 ──────────────────────────────────────────────────────

    def _run_code(self):
        _, info = self._current_tab()
        if info is None:
            return
        code = info["editor"].get("1.0", tk.END)
        path = info["path"]

        # 保存済みならそのファイルを実行、未保存なら一時保存
        if path and not info["modified"]:
            run_path = path
        else:
            import tempfile
            tmp = tempfile.NamedTemporaryFile(
                suffix=".py", delete=False, mode="w", encoding="utf-8")
            tmp.write(code)
            tmp.close()
            run_path = tmp.name

        self._term_write(f"▶ 実行: {os.path.basename(run_path)}\n", "info")
        self.run_status.config(text="実行中...")

        def target():
            try:
                py = self.python_path_var.get() or sys.executable
                self._proc = subprocess.Popen(
                    [py, run_path],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                    text=True, encoding="utf-8", errors="replace")
                stdout, stderr = self._proc.communicate(timeout=60)
                rc = self._proc.returncode
                if stdout:
                    self.root.after(0, self._term_write, stdout, "")
                if stderr:
                    self.root.after(0, self._term_write, stderr, "err")
                msg = f"✔ 終了 (コード {rc})" if rc == 0 else f"✖ エラー終了 (コード {rc})"
                tag = "ok" if rc == 0 else "err"
                self.root.after(0, self._term_write, msg + "\n", tag)
                self.root.after(0, self.run_status.config, {"text": ""})
            except subprocess.TimeoutExpired:
                self._proc.kill()
                self.root.after(0, self._term_write,
                                "⚠ タイムアウト (60秒)\n", "err")
            except Exception as e:
                self.root.after(0, self._term_write, str(e) + "\n", "err")
            finally:
                self._proc = None

        threading.Thread(target=target, daemon=True).start()

    def _stop_code(self):
        if self._proc:
            try:
                self._proc.kill()
            except Exception:
                pass
            self._term_write("⏹ 強制停止\n", "err")

    # ── ターミナル ────────────────────────────────────────────────

    def _term_write(self, text, tag=""):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.insert(tk.END, text, tag)
        self.terminal.see(tk.END)
        self.terminal.configure(state=tk.DISABLED)

    def _clear_terminal(self):
        self.terminal.configure(state=tk.NORMAL)
        self.terminal.delete("1.0", tk.END)
        self.terminal.configure(state=tk.DISABLED)

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

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

    def _highlight(self, tid):
        info = self._tabs.get(tid)
        if not info:
            return
        editor = info["editor"]

        for tag in ("kw", "str_", "cmt", "num", "deco", "bi"):
            editor.tag_remove(tag, "1.0", tk.END)
        editor.tag_configure("kw",   foreground="#569cd6")
        editor.tag_configure("str_", foreground="#ce9178")
        editor.tag_configure("cmt",  foreground="#6a9955")
        editor.tag_configure("num",  foreground="#b5cea8")
        editor.tag_configure("deco", foreground="#dcdcaa")
        editor.tag_configure("bi",   foreground="#4ec9b0")

        content = editor.get("1.0", tk.END)

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

        apply(self.PYTHON_NUM,     "num")
        apply(self.PYTHON_STR,     "str_")
        apply(self.PYTHON_CMT,     "cmt")
        apply(self.PYTHON_DECO,    "deco")
        apply(self.PYTHON_BUILTINS,"bi")
        apply(self.PYTHON_KW,      "kw")

    def _update_cursor_pos(self, widget):
        try:
            pos = widget.index(tk.INSERT)
            line, col = pos.split(".")
            self.cursor_lbl.config(text=f"行 {line}, 列 {int(col)+1}")
        except Exception:
            pass

    # ── 検索・置換 ────────────────────────────────────────────────

    def _show_search(self):
        if not self._search_visible:
            self.search_bar.pack(fill=tk.X, before=self.notebook)
            self._search_visible = True
            self.search_entry.focus_set()

    def _hide_search(self):
        if self._search_visible:
            self.search_bar.pack_forget()
            self._search_visible = False

    def _find_next(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        if not query:
            return
        editor.tag_remove("search_hl", "1.0", tk.END)
        editor.tag_configure("search_hl", background="#4d4000",
                              foreground="#fff")
        start = editor.index(tk.INSERT)
        pos = editor.search(query, start, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.tag_add("search_hl", pos, end_pos)
            editor.mark_set(tk.INSERT, end_pos)
            editor.see(pos)

    def _replace_one(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        pos = editor.search(query, tk.INSERT, stopindex=tk.END)
        if not pos:
            pos = editor.search(query, "1.0", stopindex=tk.END)
        if pos:
            end_pos = f"{pos} + {len(query)} chars"
            editor.delete(pos, end_pos)
            editor.insert(pos, replacement)

    def _replace_all(self):
        _, info = self._current_tab()
        if not info:
            return
        editor = info["editor"]
        query = self.search_entry.get()
        replacement = self.replace_entry.get()
        if not query:
            return
        content = editor.get("1.0", tk.END)
        count = content.count(query)
        if count:
            new_content = content.replace(query, replacement)
            editor.delete("1.0", tk.END)
            editor.insert("1.0", new_content)
            self.status_var.set(f"{count} 件置換しました")


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

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

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

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

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

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

    App50クラスを定義し、__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:機能拡張

    マルチタブ簡易IDEに新しい機能を1つ追加してみましょう。どんな機能があると便利か考えてから実装してください。

  2. 課題2:UIの改善

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

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

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

🚀
次に挑戦するアプリ

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