マルチタブ簡易IDE
タブ付きエディタ・コード実行・出力パネル・ファイルツリーを統合した簡易IDE。集大成プロジェクトです。
1. アプリ概要
タブ付きエディタ・コード実行・出力パネル・ファイルツリーを統合した簡易IDE。集大成プロジェクトです。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★★ です。
このアプリは「ユーティリティ」カテゴリです。小回りの効くユーティリティは Python の真骨頂で、汎用的なユーザー操作を組み合わせる発想が他のアプリにもそのまま流用できます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。
完成形を見てから細部の解説に進む流れが効果的です。実行→気になる箇所を解説で確認→自分の手で改造、というサイクルで定着が早まります。
カスタマイズ章では具体的な拡張アイデアを示しています。一つ実装するごとに動作確認することで、変更が予期せぬ副作用を起こさないかも体感できます。
2. 機能一覧
- ループで生成するボタン群(それぞれ
_new_tab()・_open_file()・_save_file()・_run_code()・_stop_code()を実行) - 「…」ボタン(
_open_folder())・「次へ」ボタン(_find_next())・「置換」ボタン(_replace_one())・「全置換」ボタン(_replace_all())・「✕」ボタン(_hide_search())・「クリア」ボタン(_clear_terminal())で操作 - キー
<Control-t>から_new_tab()を実行 - キー
<Control-o>から_open_file()を実行 - キー
<Control-s>から_save_file()を実行 - キー
<Control-w>から_close_tab()を実行 - キー
<F5>から_run_code()を実行 - マウス操作
<Double-1>から_on_tree_double_click()を実行 - イベント
<<NotebookTabChanged>>から_on_tab_changed()を実行 - キー
<KeyRelease>から 処理 を実行 - マウス操作
<ButtonRelease-1>から_update_cursor_pos()を実行 ttk.Treeviewによる表形式の一覧表示filedialog.askdirectory()によるファイル選択ダイアログfiledialog.askopenfilename()によるファイル選択ダイアログfiledialog.asksaveasfilename()によるファイル選択ダイアログmessagebox.askyesnocancel()・messagebox.showerror()によるダイアログ通知- ウィンドウはタイトル「簡易IDE」・サイズ 1200x905 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は標準ライブラリの範囲で動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox, 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("1200x905")
self.root.minsize(1200, 905)
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 クラスにアプリの全機能をまとめています(メソッド28個・全619行)。__init__ ではタイトル「簡易IDE」とウィンドウサイズ 1200x905 を設定します。あわせて状態を保持する変数(self._proc・self._file_tree_path)を初期化し、そのあと _build_ui() → _new_tab() の順に呼び出して初期状態を作ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Menu×4・tk.Frame×11・tk.Button(ループで生成)・tk.Label×8・ttk.Entry×3・ttk.PanedWindow×2・ttk.Button×6・ttk.Treeview・ttk.Scrollbar×4・ttk.Notebook・tk.Text×2・tk.Canvas で構成しています。PanedWindow を使っているので、ペインの境界をマウスでドラッグして幅を変えられます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「…」ボタン(_open_folder())・「次へ」ボタン(_find_next())・「置換」ボタン(_replace_one())・「全置換」ボタン(_replace_all())・「✕」ボタン(_hide_search())・「クリア」ボタン(_clear_terminal())を command= で接続しています。ボタンはラベルと処理をペアで並べたデータをループで回して生成し、それぞれ _new_tab()・_open_file()・_save_file()・_run_code()・_stop_code() を command= に渡しています。ボタンを増やしたいときはこのペアを1行足すだけで済みます。また bind("<Control-t>", ...) から _new_tab()、bind("<Control-o>", ...) から _open_file()、bind("<Control-s>", ...) から _save_file()、bind("<Control-w>", ...) から _close_tab()、bind("<F5>", ...) から _run_code()、bind("<Double-1>", ...) から _on_tree_double_click()、bind("<<NotebookTabChanged>>", ...) から _on_tab_changed()、bind("<KeyRelease>", ...) から 処理、bind("<ButtonRelease-1>", ...) から _update_cursor_pos() を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
クラス定数によるデータ定義
クラスの先頭でデータを定数として定義しています:TAB_UNTITLED = '無題'、PYTHON_NUM = '\\b\\d+(\\.\\d+)?\\b'。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception・subprocess.TimeoutExpired を捕捉しています。あわせて messagebox.askyesnocancel()・messagebox.showerror() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app50.py と保存します。使うのは
datetime・os・re・subprocess・sys・tempfile・threading・tkinterだけなので、追加インストールは不要です。 -
2クラスの骨格を作る
App50クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("簡易IDE")・geometry("1200x905")・configure(bg="#1e1e1e")・minsize(1200, 905)をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Menu×4・tk.Frame×11・tk.Button(ループで生成)・tk.Label×8・ttk.Entry×3・ttk.PanedWindow×2・ttk.Button×6・ttk.Treeview・ttk.Scrollbar×4・ttk.Notebook・tk.Text×2・tk.Canvasを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_open_folder()・_find_next()・_replace_one()・_replace_all()・_hide_search()・_clear_terminal())につなぎます。ループ生成のボタンは、ラベルと処理(_new_tab()・_open_file()・_save_file()・_run_code()・_stop_code())をペアで並べたデータをループで回し、command=に渡して接続します。bind("<Control-t>", ...)・bind("<Control-o>", ...)・bind("<Control-s>", ...)・bind("<Control-w>", ...)・bind("<F5>", ...)・bind("<Double-1>", ...)・bind("<<NotebookTabChanged>>", ...)・bind("<KeyRelease>", ...)・bind("<ButtonRelease-1>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_term_write()(5行)・_new_tab()(60行)・_run_code()(48行)など を実装します。 -
7動作確認する
python app50.pyで起動し、各ボタンが反応すること・キー操作(<Control-t>・<Control-o>・<Control-s>・<Control-w>・<F5>・<KeyRelease>)が効くこと・マウス操作(クリック・ホイールなど)が効くことを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 クラス定数を書き換えて動きを変える
データはクラス定数(TAB_UNTITLED・PYTHON_KW)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。
💡 入力履歴機能
以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。
8. よくある問題と解決法
❌ <Control-t> キーを押しても反応しない
原因:bind("<Control-t>", ...) はウィジェット単位で登録されるため、ウィンドウ(またはバインド先)にキーボードフォーカスが無いとイベントが届きません。
解決法:一度ウィンドウ内をクリックしてフォーカスを与えてから、キーを押してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("1200x905") の固定サイズで起動するためです。
解決法:geometry() と minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(1200, 905) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。「▶ 実行」は、いま開いているタブの中身を subprocess.Popen([py, run_path], ...)(440-443行)で Python として実行します。実行するのは、自分で中身を説明できるコードだけにしてください。ネットで拾ったコードや配布元の分からない .py を開いて実行しないでください。60秒でタイムアウトしますが(444行)、その前にファイルを消したり通信したりする処理は止められません。保存していないタブの中身は、一時フォルダに .py として書き出されます(427-432行)。パスワードや API キーを書いたまま「▶ 実行」を押さないでください。課題1・課題2に追加のライブラリは要りません。
-
課題1:実行するたびに一時ファイルが増えていく
保存していないタブで「▶ 実行」を押すと、中身が一時フォルダの
.pyに書き出されます。この一時ファイルを消す処理はどこにもありません。押した回数だけ、コードが平文で残り続けます。期待結果:起動直後のサンプルコードのまま「▶ 実行」を押す。ターミナルに
▶ 実行: tmpで始まるファイル名が出て、Fibonacci: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]・Sum: 88・✔ 終了 (コード 0)と続く。同じ操作を3回くり返すと、ファイル名は毎回違う。python -c "import tempfile; print(tempfile.gettempdir())"が表示するフォルダを開くと、tmpで始まる.pyが3つ残っている(他のアプリの一時ファイルも入っているので、更新時刻の新しいものを見てください)。直したあとは、実行が終わった時点で残らない。合格条件
- 保存済みのファイルを開いて実行したときは、そのファイルを消さない
- 保存していないコードを実行したあと、一時ファイルが残らない
- 実行中に「⏹ 停止」を押したときも残らない
ヒント①(どこを触るか): 触るのは2か所です。
_run_code()の427-432行(一時ファイルを作る側)と、その下のfinally:(460-461行)。
ヒント②(使うもの): 424-425行の分岐で一時ファイルを作ったかどうかを変数に覚えておき、finally:の中でos.remove(run_path)を呼びます。osは3行目で読み込み済みです。消せないことがあるのでtry:とexcept OSError:で囲みます。
つまずきやすい点:run_pathはtarget()の外で決まる変数なので、そのまま参照できます(432行)。消すのはcommunicate()(444行)が戻ってから=実行が終わったあとにしてください。「⏹ 停止」(465-471行)でkill()したときもcommunicate()は戻るので、finally:は通ります。delete=Falseを渡している以上、自分で消さないかぎり一時ファイルは残ったままです(428-429行)。 -
課題2:エクスプローラーにフォルダが1つも出てこない
左のツリーは、選んだフォルダの直下にあるファイルだけを並べます。中にあるフォルダは飛ばされるため、1階層ぶんしか見えません。
期待結果:ヘッダーの
…を押して、中にサブフォルダがあるフォルダを選ぶ。ツリーにはいちばん上のフォルダ名の行と、その直下のファイルだけが並び、サブフォルダの行は出ない。直したあとは、サブフォルダも行として並び、開くとその中のファイルが見える。ファイルをダブルクリックすると、今までどおり新しいタブで開く。合格条件
- いちばん上のフォルダ名の行と、その直下のファイルの並びは今までどおり
- 直下のサブフォルダが行として並ぶ
- ファイルのダブルクリックでタブが開く動きは変わらない
ヒント①(どこを触るか): 触るのは2か所です。
_refresh_file_tree()の390-394行のループと、フォルダの中身を入れるメソッドを1つ追加。
ヒント②(使うもの): いまのループはos.path.isfile(full)が真のときだけinsert()しています(392-394行)。os.path.isdir(full)のときもinsert()し、その戻り値(親ノードの ID)を渡して同じ処理を呼べば入れ子になる形です。フォルダを先に並べたいときは、sorted()(390行)の結果をos.path.isdirで2つに分けてから回します。
つまずきやすい点:values=[full]を付け忘れると、_on_tree_double_click()(398-412行)がvals空でreturnします。フォルダの行をダブルクリックしても、406行のos.path.isfile(path)が偽なので何も起きません(案内も出ません)。深いフォルダを一度に全部たどると、開いた瞬間に固まることがあります。中を読むのは<<TreeviewOpen>>で開かれたときだけにする方法もあるので、大きいフォルダを扱うなら検討してください。読み取り権限が無いフォルダではos.listdir()がPermissionErrorを出すため、395行と同じ形で受け止めてください。 -
課題3:新しいボタンを追加する
既存のボタンと同じ書き方で新しいボタンを1つ足し、押されたときに動くメソッドを自作してみましょう。ボタンの作成と
command=の接続がセットで身に付きます。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。