中級者向け No.36

Markdownエディタ(プレビュー付き)

左にMarkdown入力・右にHTMLプレビューを表示するMarkdownエディタ。markdownライブラリとHTMLウィジェットを学びます。

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

1. アプリ概要

左にMarkdown入力・右にHTMLプレビューを表示するMarkdownエディタ。markdownライブラリとHTMLウィジェットを学びます。

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

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

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

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

2. 機能一覧

  • Markdownエディタ(プレビュー付き)のメイン機能
  • 直感的なGUIインターフェース
  • 入力値のバリデーション
  • エラーハンドリング
  • 結果の見やすい表示
  • キーボードショートカット対応

3. 事前準備・環境

ℹ️
動作確認環境

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

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

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

4. 完全なソースコード

💡
コードのコピー方法

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

📦 必要なライブラリをインストール(初回のみ)
pip install markdown tkinterweb
app36.py
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import re

try:
    import markdown
    MD_AVAILABLE = True
except ImportError:
    MD_AVAILABLE = False

try:
    import tkinterweb
    TKWEB_AVAILABLE = True
except ImportError:
    TKWEB_AVAILABLE = False


class App36:
    """Markdownエディタ(プレビュー付き)"""

    SAMPLES = {
        "基本サンプル": """\
# Markdownエディタ

## 見出し

### 小見出し

通常のテキスト。**太字** や *斜体* が使えます。

## リスト

- 項目1
- 項目2
  - ネスト項目
- 項目3

1. 番号付きリスト
2. 2番目
3. 3番目

## コード

インラインコード: `print("Hello")`

```python
def hello(name):
    return f"Hello, {name}!"

print(hello("World"))
```

## リンクと画像

[Python公式](https://www.python.org)

## テーブル

| 名前 | 年齢 | 職業 |
|------|------|------|
| 田中 | 25   | エンジニア |
| 鈴木 | 30   | デザイナー |

## 引用

> これは引用です。
> 複数行も可能。

---

水平線の上下に区切り線が表示されます。
""",
        "README テンプレート": """\
# プロジェクト名

## 概要

プロジェクトの概要を記述してください。

## インストール

```bash
pip install プロジェクト名
```

## 使い方

```python
import プロジェクト名

# 使用例
```

## 機能

- 機能1
- 機能2
- 機能3

## ライセンス

MIT License
""",
    }

    CSS = """\
<style>
body { font-family: 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto;
       padding: 20px; line-height: 1.6; color: #333; }
h1,h2,h3,h4 { color: #0d47a1; }
h1 { border-bottom: 2px solid #0d47a1; padding-bottom: 8px; }
h2 { border-bottom: 1px solid #ddd; padding-bottom: 4px; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 4px;
       font-family: Consolas, monospace; font-size: 0.9em; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 16px;
      border-radius: 8px; overflow-x: auto; }
pre code { background: none; color: inherit; }
blockquote { border-left: 4px solid #0d47a1; margin: 0;
             padding-left: 16px; color: #555; }
table { border-collapse: collapse; width: 100%; }
th { background: #0d47a1; color: white; padding: 8px 12px; }
td { border: 1px solid #ddd; padding: 8px 12px; }
tr:nth-child(even) { background: #f9f9f9; }
a { color: #1565c0; }
hr { border: none; border-top: 2px solid #ddd; }
</style>
"""

    def __init__(self, root):
        self.root = root
        self.root.title("Markdownエディタ(プレビュー付き)")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")
        self._current_file = None
        self._modified = False
        self._build_ui()
        self._load_sample("基本サンプル")

    def _build_ui(self):
        # ツールバー
        toolbar = tk.Frame(self.root, bg="#252526", pady=4)
        toolbar.pack(fill=tk.X)
        tk.Label(toolbar, text="📝 Markdownエディタ",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # サンプル
        tk.Label(toolbar, text="サンプル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 4))
        self.sample_var = tk.StringVar()
        sample_cb = ttk.Combobox(toolbar, textvariable=self.sample_var,
                                  values=list(self.SAMPLES.keys()),
                                  state="readonly", width=18)
        sample_cb.pack(side=tk.LEFT)
        sample_cb.bind("<<ComboboxSelected>>",
                       lambda e: self._load_sample(self.sample_var.get()))

        # ボタン
        for text, cmd in [("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("💾 別名保存", self._save_as_file)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="white", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=3,
                      activebackground="#505050", bd=0).pack(side=tk.LEFT, padx=4)

        # ビューモード
        tk.Label(toolbar, text="表示:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.RIGHT, padx=4)
        self.view_mode = tk.StringVar(value="split")
        for val, lbl in [("edit", "編集"), ("split", "分割"), ("preview", "プレビュー")]:
            ttk.Radiobutton(toolbar, text=lbl, variable=self.view_mode,
                            value=val,
                            command=self._change_view_mode
                            ).pack(side=tk.RIGHT, padx=2)

        if not MD_AVAILABLE:
            tk.Label(self.root,
                     text="⚠ markdown が未インストール (pip install markdown)。"
                          "簡易テキストプレビューになります。",
                     bg="#fff3cd", fg="#856404", font=("Arial", 9),
                     anchor="w", padx=8).pack(fill=tk.X)

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

        # 左: エディタ
        self.edit_frame = tk.Frame(self.main_paned, bg="#1e1e1e")
        self.main_paned.add(self.edit_frame, weight=1)

        # 行番号
        num_frame = tk.Frame(self.edit_frame, bg="#1e1e1e")
        num_frame.pack(fill=tk.BOTH, expand=True)
        self.line_nums = tk.Text(num_frame, width=4, bg="#252526",
                                  fg="#858585", font=("Courier New", 12),
                                  state=tk.DISABLED, relief=tk.FLAT,
                                  padx=4, pady=4, takefocus=0)
        self.line_nums.pack(side=tk.LEFT, fill=tk.Y)
        self.editor = tk.Text(num_frame, bg="#1e1e1e", fg="#d4d4d4",
                               font=("Courier New", 12), insertbackground="#aeafad",
                               relief=tk.FLAT, undo=True, padx=8, pady=4,
                               selectbackground="#264f78",
                               wrap=tk.WORD)
        ed_sb = ttk.Scrollbar(num_frame, command=self._scroll_editor)
        self.editor.configure(yscrollcommand=ed_sb.set)
        ed_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.editor.pack(fill=tk.BOTH, expand=True)
        self.editor.bind("<KeyRelease>", self._on_key)
        self._setup_editor_tags()

        # 右: プレビュー
        self.preview_frame = tk.Frame(self.main_paned, bg="#ffffff")
        self.main_paned.add(self.preview_frame, weight=1)

        if TKWEB_AVAILABLE:
            self.html_viewer = tkinterweb.HtmlFrame(
                self.preview_frame, messages_enabled=False)
            self.html_viewer.pack(fill=tk.BOTH, expand=True)
        else:
            # フォールバック: Text widget
            self.preview_text = tk.Text(
                self.preview_frame, bg="#ffffff", fg="#333",
                font=("Arial", 12), relief=tk.FLAT, wrap=tk.WORD,
                state=tk.DISABLED, padx=12, pady=12)
            pr_sb = ttk.Scrollbar(self.preview_frame,
                                   command=self.preview_text.yview)
            self.preview_text.configure(yscrollcommand=pr_sb.set)
            pr_sb.pack(side=tk.RIGHT, fill=tk.Y)
            self.preview_text.pack(fill=tk.BOTH, expand=True)
            self._setup_preview_tags()

        # ステータス
        self.status_var = tk.StringVar(value="準備完了")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#252526", fg="#858585", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _setup_editor_tags(self):
        # Markdown構文色付け
        self.editor.tag_configure("heading", foreground="#4fc3f7",
                                   font=("Courier New", 13, "bold"))
        self.editor.tag_configure("bold", foreground="#ffffff",
                                   font=("Courier New", 12, "bold"))
        self.editor.tag_configure("italic", foreground="#ce9178",
                                   font=("Courier New", 12, "italic"))
        self.editor.tag_configure("code", foreground="#4ec9b0",
                                   background="#2d2d30")
        self.editor.tag_configure("link", foreground="#569cd6")
        self.editor.tag_configure("list_item", foreground="#dcdcaa")
        self.editor.tag_configure("quote", foreground="#6a9955")
        self.editor.tag_configure("hr", foreground="#555555")

    def _setup_preview_tags(self):
        self.preview_text.tag_configure("h1", font=("Arial", 20, "bold"),
                                         foreground="#0d47a1",
                                         spacing3=8)
        self.preview_text.tag_configure("h2", font=("Arial", 16, "bold"),
                                         foreground="#1565c0",
                                         spacing3=6)
        self.preview_text.tag_configure("h3", font=("Arial", 14, "bold"),
                                         foreground="#1976d2")
        self.preview_text.tag_configure("code_inline",
                                         font=("Courier New", 11),
                                         background="#f4f4f4")
        self.preview_text.tag_configure("code_block",
                                         font=("Courier New", 11),
                                         background="#1e1e1e",
                                         foreground="#d4d4d4",
                                         spacing1=4, spacing3=4)
        self.preview_text.tag_configure("bold_text",
                                         font=("Arial", 12, "bold"))
        self.preview_text.tag_configure("italic_text",
                                         font=("Arial", 12, "italic"))
        self.preview_text.tag_configure("quote_text",
                                         foreground="#555555",
                                         lmargin1=20, lmargin2=20)
        self.preview_text.tag_configure("bullet",
                                         foreground="#0d47a1")

    def _scroll_editor(self, *args):
        self.editor.yview(*args)
        self.line_nums.yview(*args)

    def _on_key(self, event=None):
        self._update_line_nums()
        self._highlight_md()
        self._update_preview()
        self._modified = True
        lines = int(self.editor.index(tk.END).split(".")[0]) - 1
        chars = len(self.editor.get("1.0", tk.END)) - 1
        self.status_var.set(f"行: {lines}  文字: {chars}")

    def _update_line_nums(self):
        content = self.editor.get("1.0", tk.END)
        lines = content.count("\n")
        self.line_nums.config(state=tk.NORMAL)
        self.line_nums.delete("1.0", tk.END)
        self.line_nums.insert("1.0", "\n".join(str(i) for i in range(1, lines + 1)))
        self.line_nums.config(state=tk.DISABLED)

    def _highlight_md(self):
        content = self.editor.get("1.0", tk.END)
        for tag in ["heading", "bold", "italic", "code", "link",
                    "list_item", "quote", "hr"]:
            self.editor.tag_remove(tag, "1.0", tk.END)
        patterns = [
            ("heading",   r"^#{1,6} .+$"),
            ("code",      r"`[^`\n]+`"),
            ("bold",      r"\*\*[^*\n]+\*\*"),
            ("italic",    r"\*[^*\n]+\*"),
            ("link",      r"\[.*?\]\(.*?\)"),
            ("list_item", r"^[\s]*[-*+] .+$"),
            ("list_item", r"^\d+\. .+$"),
            ("quote",     r"^> .+$"),
            ("hr",        r"^[-*_]{3,}$"),
        ]
        for tag, pattern in patterns:
            for m in re.finditer(pattern, content, re.MULTILINE):
                start = f"1.0+{m.start()}c"
                end = f"1.0+{m.end()}c"
                self.editor.tag_add(tag, start, end)

    def _update_preview(self):
        content = self.editor.get("1.0", tk.END)
        if TKWEB_AVAILABLE:
            if MD_AVAILABLE:
                html = markdown.markdown(
                    content,
                    extensions=["tables", "fenced_code", "codehilite", "nl2br"])
            else:
                html = self._simple_md(content)
            full_html = f"<html><head>{self.CSS}</head><body>{html}</body></html>"
            self.html_viewer.load_html(full_html)
        else:
            # フォールバック
            self.preview_text.config(state=tk.NORMAL)
            self.preview_text.delete("1.0", tk.END)
            self._render_preview_text(content)
            self.preview_text.config(state=tk.DISABLED)

    def _simple_md(self, text):
        """markdownライブラリなしの簡易変換"""
        lines = text.split("\n")
        html_lines = []
        in_code = False
        for line in lines:
            if line.startswith("```"):
                if in_code:
                    html_lines.append("</pre>")
                    in_code = False
                else:
                    html_lines.append("<pre><code>")
                    in_code = True
            elif in_code:
                html_lines.append(line)
            elif re.match(r"^######", line):
                html_lines.append(f"<h6>{line[7:]}</h6>")
            elif re.match(r"^#####", line):
                html_lines.append(f"<h5>{line[6:]}</h5>")
            elif re.match(r"^####", line):
                html_lines.append(f"<h4>{line[5:]}</h4>")
            elif re.match(r"^###", line):
                html_lines.append(f"<h3>{line[4:]}</h3>")
            elif re.match(r"^##", line):
                html_lines.append(f"<h2>{line[3:]}</h2>")
            elif re.match(r"^#", line):
                html_lines.append(f"<h1>{line[2:]}</h1>")
            elif re.match(r"^[-*+] ", line):
                html_lines.append(f"<li>{line[2:]}</li>")
            elif re.match(r"^> ", line):
                html_lines.append(f"<blockquote>{line[2:]}</blockquote>")
            elif re.match(r"^[-*_]{3,}$", line):
                html_lines.append("<hr>")
            else:
                l = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", line)
                l = re.sub(r"\*(.+?)\*", r"<em>\1</em>", l)
                l = re.sub(r"`(.+?)`", r"<code>\1</code>", l)
                l = re.sub(r"\[(.+?)\]\((.+?)\)", r'<a href="\2">\1</a>', l)
                html_lines.append(f"<p>{l}</p>" if l else "")
        return "\n".join(html_lines)

    def _render_preview_text(self, content):
        """tkinterweb未使用時のフォールバックプレビュー"""
        for line in content.split("\n"):
            if re.match(r"^# ", line):
                self.preview_text.insert(tk.END, line[2:] + "\n", "h1")
            elif re.match(r"^## ", line):
                self.preview_text.insert(tk.END, line[3:] + "\n", "h2")
            elif re.match(r"^### ", line):
                self.preview_text.insert(tk.END, line[4:] + "\n", "h3")
            elif re.match(r"^[-*+] ", line):
                self.preview_text.insert(tk.END, "• " + line[2:] + "\n", "bullet")
            elif re.match(r"^> ", line):
                self.preview_text.insert(tk.END, "  " + line[2:] + "\n", "quote_text")
            else:
                self.preview_text.insert(tk.END, line + "\n")

    def _change_view_mode(self):
        mode = self.view_mode.get()
        # ペインを作り直す
        for item in self.main_paned.panes():
            self.main_paned.forget(item)
        if mode == "edit":
            self.main_paned.add(self.edit_frame, weight=1)
        elif mode == "preview":
            self.main_paned.add(self.preview_frame, weight=1)
        else:
            self.main_paned.add(self.edit_frame, weight=1)
            self.main_paned.add(self.preview_frame, weight=1)

    def _load_sample(self, name):
        content = self.SAMPLES.get(name, "")
        self.editor.delete("1.0", tk.END)
        self.editor.insert("1.0", content)
        self._on_key()

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Markdown", "*.md *.markdown *.txt"),
                       ("すべて", "*.*")])
        if path:
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self.editor.delete("1.0", tk.END)
                self.editor.insert("1.0", content)
                self._current_file = path
                self._modified = False
                self._on_key()
                self.root.title(f"Markdownエディタ — {os.path.basename(path)}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _save_file(self):
        if self._current_file:
            self._write_file(self._current_file)
        else:
            self._save_as_file()

    def _save_as_file(self):
        path = filedialog.asksaveasfilename(
            defaultextension=".md",
            filetypes=[("Markdown", "*.md"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if path:
            self._write_file(path)
            self._current_file = path
            self.root.title(f"Markdownエディタ — {os.path.basename(path)}")

    def _write_file(self, path):
        try:
            content = self.editor.get("1.0", tk.END)
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
            self._modified = False
            self.status_var.set(f"保存しました: {path}")
        except Exception as e:
            messagebox.showerror("エラー", str(e))


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

5. コード解説

Markdownエディタ(プレビュー付き)のコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。

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

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

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

try:
    import markdown
    MD_AVAILABLE = True
except ImportError:
    MD_AVAILABLE = False

try:
    import tkinterweb
    TKWEB_AVAILABLE = True
except ImportError:
    TKWEB_AVAILABLE = False


class App36:
    """Markdownエディタ(プレビュー付き)"""

    SAMPLES = {
        "基本サンプル": """\
# Markdownエディタ

## 見出し

### 小見出し

通常のテキスト。**太字** や *斜体* が使えます。

## リスト

- 項目1
- 項目2
  - ネスト項目
- 項目3

1. 番号付きリスト
2. 2番目
3. 3番目

## コード

インラインコード: `print("Hello")`

```python
def hello(name):
    return f"Hello, {name}!"

print(hello("World"))
```

## リンクと画像

[Python公式](https://www.python.org)

## テーブル

| 名前 | 年齢 | 職業 |
|------|------|------|
| 田中 | 25   | エンジニア |
| 鈴木 | 30   | デザイナー |

## 引用

> これは引用です。
> 複数行も可能。

---

水平線の上下に区切り線が表示されます。
""",
        "README テンプレート": """\
# プロジェクト名

## 概要

プロジェクトの概要を記述してください。

## インストール

```bash
pip install プロジェクト名
```

## 使い方

```python
import プロジェクト名

# 使用例
```

## 機能

- 機能1
- 機能2
- 機能3

## ライセンス

MIT License
""",
    }

    CSS = """\
<style>
body { font-family: 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto;
       padding: 20px; line-height: 1.6; color: #333; }
h1,h2,h3,h4 { color: #0d47a1; }
h1 { border-bottom: 2px solid #0d47a1; padding-bottom: 8px; }
h2 { border-bottom: 1px solid #ddd; padding-bottom: 4px; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 4px;
       font-family: Consolas, monospace; font-size: 0.9em; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 16px;
      border-radius: 8px; overflow-x: auto; }
pre code { background: none; color: inherit; }
blockquote { border-left: 4px solid #0d47a1; margin: 0;
             padding-left: 16px; color: #555; }
table { border-collapse: collapse; width: 100%; }
th { background: #0d47a1; color: white; padding: 8px 12px; }
td { border: 1px solid #ddd; padding: 8px 12px; }
tr:nth-child(even) { background: #f9f9f9; }
a { color: #1565c0; }
hr { border: none; border-top: 2px solid #ddd; }
</style>
"""

    def __init__(self, root):
        self.root = root
        self.root.title("Markdownエディタ(プレビュー付き)")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")
        self._current_file = None
        self._modified = False
        self._build_ui()
        self._load_sample("基本サンプル")

    def _build_ui(self):
        # ツールバー
        toolbar = tk.Frame(self.root, bg="#252526", pady=4)
        toolbar.pack(fill=tk.X)
        tk.Label(toolbar, text="📝 Markdownエディタ",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # サンプル
        tk.Label(toolbar, text="サンプル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 4))
        self.sample_var = tk.StringVar()
        sample_cb = ttk.Combobox(toolbar, textvariable=self.sample_var,
                                  values=list(self.SAMPLES.keys()),
                                  state="readonly", width=18)
        sample_cb.pack(side=tk.LEFT)
        sample_cb.bind("<<ComboboxSelected>>",
                       lambda e: self._load_sample(self.sample_var.get()))

        # ボタン
        for text, cmd in [("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("💾 別名保存", self._save_as_file)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="white", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=3,
                      activebackground="#505050", bd=0).pack(side=tk.LEFT, padx=4)

        # ビューモード
        tk.Label(toolbar, text="表示:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.RIGHT, padx=4)
        self.view_mode = tk.StringVar(value="split")
        for val, lbl in [("edit", "編集"), ("split", "分割"), ("preview", "プレビュー")]:
            ttk.Radiobutton(toolbar, text=lbl, variable=self.view_mode,
                            value=val,
                            command=self._change_view_mode
                            ).pack(side=tk.RIGHT, padx=2)

        if not MD_AVAILABLE:
            tk.Label(self.root,
                     text="⚠ markdown が未インストール (pip install markdown)。"
                          "簡易テキストプレビューになります。",
                     bg="#fff3cd", fg="#856404", font=("Arial", 9),
                     anchor="w", padx=8).pack(fill=tk.X)

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

        # 左: エディタ
        self.edit_frame = tk.Frame(self.main_paned, bg="#1e1e1e")
        self.main_paned.add(self.edit_frame, weight=1)

        # 行番号
        num_frame = tk.Frame(self.edit_frame, bg="#1e1e1e")
        num_frame.pack(fill=tk.BOTH, expand=True)
        self.line_nums = tk.Text(num_frame, width=4, bg="#252526",
                                  fg="#858585", font=("Courier New", 12),
                                  state=tk.DISABLED, relief=tk.FLAT,
                                  padx=4, pady=4, takefocus=0)
        self.line_nums.pack(side=tk.LEFT, fill=tk.Y)
        self.editor = tk.Text(num_frame, bg="#1e1e1e", fg="#d4d4d4",
                               font=("Courier New", 12), insertbackground="#aeafad",
                               relief=tk.FLAT, undo=True, padx=8, pady=4,
                               selectbackground="#264f78",
                               wrap=tk.WORD)
        ed_sb = ttk.Scrollbar(num_frame, command=self._scroll_editor)
        self.editor.configure(yscrollcommand=ed_sb.set)
        ed_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.editor.pack(fill=tk.BOTH, expand=True)
        self.editor.bind("<KeyRelease>", self._on_key)
        self._setup_editor_tags()

        # 右: プレビュー
        self.preview_frame = tk.Frame(self.main_paned, bg="#ffffff")
        self.main_paned.add(self.preview_frame, weight=1)

        if TKWEB_AVAILABLE:
            self.html_viewer = tkinterweb.HtmlFrame(
                self.preview_frame, messages_enabled=False)
            self.html_viewer.pack(fill=tk.BOTH, expand=True)
        else:
            # フォールバック: Text widget
            self.preview_text = tk.Text(
                self.preview_frame, bg="#ffffff", fg="#333",
                font=("Arial", 12), relief=tk.FLAT, wrap=tk.WORD,
                state=tk.DISABLED, padx=12, pady=12)
            pr_sb = ttk.Scrollbar(self.preview_frame,
                                   command=self.preview_text.yview)
            self.preview_text.configure(yscrollcommand=pr_sb.set)
            pr_sb.pack(side=tk.RIGHT, fill=tk.Y)
            self.preview_text.pack(fill=tk.BOTH, expand=True)
            self._setup_preview_tags()

        # ステータス
        self.status_var = tk.StringVar(value="準備完了")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#252526", fg="#858585", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _setup_editor_tags(self):
        # Markdown構文色付け
        self.editor.tag_configure("heading", foreground="#4fc3f7",
                                   font=("Courier New", 13, "bold"))
        self.editor.tag_configure("bold", foreground="#ffffff",
                                   font=("Courier New", 12, "bold"))
        self.editor.tag_configure("italic", foreground="#ce9178",
                                   font=("Courier New", 12, "italic"))
        self.editor.tag_configure("code", foreground="#4ec9b0",
                                   background="#2d2d30")
        self.editor.tag_configure("link", foreground="#569cd6")
        self.editor.tag_configure("list_item", foreground="#dcdcaa")
        self.editor.tag_configure("quote", foreground="#6a9955")
        self.editor.tag_configure("hr", foreground="#555555")

    def _setup_preview_tags(self):
        self.preview_text.tag_configure("h1", font=("Arial", 20, "bold"),
                                         foreground="#0d47a1",
                                         spacing3=8)
        self.preview_text.tag_configure("h2", font=("Arial", 16, "bold"),
                                         foreground="#1565c0",
                                         spacing3=6)
        self.preview_text.tag_configure("h3", font=("Arial", 14, "bold"),
                                         foreground="#1976d2")
        self.preview_text.tag_configure("code_inline",
                                         font=("Courier New", 11),
                                         background="#f4f4f4")
        self.preview_text.tag_configure("code_block",
                                         font=("Courier New", 11),
                                         background="#1e1e1e",
                                         foreground="#d4d4d4",
                                         spacing1=4, spacing3=4)
        self.preview_text.tag_configure("bold_text",
                                         font=("Arial", 12, "bold"))
        self.preview_text.tag_configure("italic_text",
                                         font=("Arial", 12, "italic"))
        self.preview_text.tag_configure("quote_text",
                                         foreground="#555555",
                                         lmargin1=20, lmargin2=20)
        self.preview_text.tag_configure("bullet",
                                         foreground="#0d47a1")

    def _scroll_editor(self, *args):
        self.editor.yview(*args)
        self.line_nums.yview(*args)

    def _on_key(self, event=None):
        self._update_line_nums()
        self._highlight_md()
        self._update_preview()
        self._modified = True
        lines = int(self.editor.index(tk.END).split(".")[0]) - 1
        chars = len(self.editor.get("1.0", tk.END)) - 1
        self.status_var.set(f"行: {lines}  文字: {chars}")

    def _update_line_nums(self):
        content = self.editor.get("1.0", tk.END)
        lines = content.count("\n")
        self.line_nums.config(state=tk.NORMAL)
        self.line_nums.delete("1.0", tk.END)
        self.line_nums.insert("1.0", "\n".join(str(i) for i in range(1, lines + 1)))
        self.line_nums.config(state=tk.DISABLED)

    def _highlight_md(self):
        content = self.editor.get("1.0", tk.END)
        for tag in ["heading", "bold", "italic", "code", "link",
                    "list_item", "quote", "hr"]:
            self.editor.tag_remove(tag, "1.0", tk.END)
        patterns = [
            ("heading",   r"^#{1,6} .+$"),
            ("code",      r"`[^`\n]+`"),
            ("bold",      r"\*\*[^*\n]+\*\*"),
            ("italic",    r"\*[^*\n]+\*"),
            ("link",      r"\[.*?\]\(.*?\)"),
            ("list_item", r"^[\s]*[-*+] .+$"),
            ("list_item", r"^\d+\. .+$"),
            ("quote",     r"^> .+$"),
            ("hr",        r"^[-*_]{3,}$"),
        ]
        for tag, pattern in patterns:
            for m in re.finditer(pattern, content, re.MULTILINE):
                start = f"1.0+{m.start()}c"
                end = f"1.0+{m.end()}c"
                self.editor.tag_add(tag, start, end)

    def _update_preview(self):
        content = self.editor.get("1.0", tk.END)
        if TKWEB_AVAILABLE:
            if MD_AVAILABLE:
                html = markdown.markdown(
                    content,
                    extensions=["tables", "fenced_code", "codehilite", "nl2br"])
            else:
                html = self._simple_md(content)
            full_html = f"<html><head>{self.CSS}</head><body>{html}</body></html>"
            self.html_viewer.load_html(full_html)
        else:
            # フォールバック
            self.preview_text.config(state=tk.NORMAL)
            self.preview_text.delete("1.0", tk.END)
            self._render_preview_text(content)
            self.preview_text.config(state=tk.DISABLED)

    def _simple_md(self, text):
        """markdownライブラリなしの簡易変換"""
        lines = text.split("\n")
        html_lines = []
        in_code = False
        for line in lines:
            if line.startswith("```"):
                if in_code:
                    html_lines.append("</pre>")
                    in_code = False
                else:
                    html_lines.append("<pre><code>")
                    in_code = True
            elif in_code:
                html_lines.append(line)
            elif re.match(r"^######", line):
                html_lines.append(f"<h6>{line[7:]}</h6>")
            elif re.match(r"^#####", line):
                html_lines.append(f"<h5>{line[6:]}</h5>")
            elif re.match(r"^####", line):
                html_lines.append(f"<h4>{line[5:]}</h4>")
            elif re.match(r"^###", line):
                html_lines.append(f"<h3>{line[4:]}</h3>")
            elif re.match(r"^##", line):
                html_lines.append(f"<h2>{line[3:]}</h2>")
            elif re.match(r"^#", line):
                html_lines.append(f"<h1>{line[2:]}</h1>")
            elif re.match(r"^[-*+] ", line):
                html_lines.append(f"<li>{line[2:]}</li>")
            elif re.match(r"^> ", line):
                html_lines.append(f"<blockquote>{line[2:]}</blockquote>")
            elif re.match(r"^[-*_]{3,}$", line):
                html_lines.append("<hr>")
            else:
                l = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", line)
                l = re.sub(r"\*(.+?)\*", r"<em>\1</em>", l)
                l = re.sub(r"`(.+?)`", r"<code>\1</code>", l)
                l = re.sub(r"\[(.+?)\]\((.+?)\)", r'<a href="\2">\1</a>', l)
                html_lines.append(f"<p>{l}</p>" if l else "")
        return "\n".join(html_lines)

    def _render_preview_text(self, content):
        """tkinterweb未使用時のフォールバックプレビュー"""
        for line in content.split("\n"):
            if re.match(r"^# ", line):
                self.preview_text.insert(tk.END, line[2:] + "\n", "h1")
            elif re.match(r"^## ", line):
                self.preview_text.insert(tk.END, line[3:] + "\n", "h2")
            elif re.match(r"^### ", line):
                self.preview_text.insert(tk.END, line[4:] + "\n", "h3")
            elif re.match(r"^[-*+] ", line):
                self.preview_text.insert(tk.END, "• " + line[2:] + "\n", "bullet")
            elif re.match(r"^> ", line):
                self.preview_text.insert(tk.END, "  " + line[2:] + "\n", "quote_text")
            else:
                self.preview_text.insert(tk.END, line + "\n")

    def _change_view_mode(self):
        mode = self.view_mode.get()
        # ペインを作り直す
        for item in self.main_paned.panes():
            self.main_paned.forget(item)
        if mode == "edit":
            self.main_paned.add(self.edit_frame, weight=1)
        elif mode == "preview":
            self.main_paned.add(self.preview_frame, weight=1)
        else:
            self.main_paned.add(self.edit_frame, weight=1)
            self.main_paned.add(self.preview_frame, weight=1)

    def _load_sample(self, name):
        content = self.SAMPLES.get(name, "")
        self.editor.delete("1.0", tk.END)
        self.editor.insert("1.0", content)
        self._on_key()

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Markdown", "*.md *.markdown *.txt"),
                       ("すべて", "*.*")])
        if path:
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self.editor.delete("1.0", tk.END)
                self.editor.insert("1.0", content)
                self._current_file = path
                self._modified = False
                self._on_key()
                self.root.title(f"Markdownエディタ — {os.path.basename(path)}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _save_file(self):
        if self._current_file:
            self._write_file(self._current_file)
        else:
            self._save_as_file()

    def _save_as_file(self):
        path = filedialog.asksaveasfilename(
            defaultextension=".md",
            filetypes=[("Markdown", "*.md"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if path:
            self._write_file(path)
            self._current_file = path
            self.root.title(f"Markdownエディタ — {os.path.basename(path)}")

    def _write_file(self, path):
        try:
            content = self.editor.get("1.0", tk.END)
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
            self._modified = False
            self.status_var.set(f"保存しました: {path}")
        except Exception as e:
            messagebox.showerror("エラー", str(e))


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

LabelFrameによるセクション分け

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

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

try:
    import markdown
    MD_AVAILABLE = True
except ImportError:
    MD_AVAILABLE = False

try:
    import tkinterweb
    TKWEB_AVAILABLE = True
except ImportError:
    TKWEB_AVAILABLE = False


class App36:
    """Markdownエディタ(プレビュー付き)"""

    SAMPLES = {
        "基本サンプル": """\
# Markdownエディタ

## 見出し

### 小見出し

通常のテキスト。**太字** や *斜体* が使えます。

## リスト

- 項目1
- 項目2
  - ネスト項目
- 項目3

1. 番号付きリスト
2. 2番目
3. 3番目

## コード

インラインコード: `print("Hello")`

```python
def hello(name):
    return f"Hello, {name}!"

print(hello("World"))
```

## リンクと画像

[Python公式](https://www.python.org)

## テーブル

| 名前 | 年齢 | 職業 |
|------|------|------|
| 田中 | 25   | エンジニア |
| 鈴木 | 30   | デザイナー |

## 引用

> これは引用です。
> 複数行も可能。

---

水平線の上下に区切り線が表示されます。
""",
        "README テンプレート": """\
# プロジェクト名

## 概要

プロジェクトの概要を記述してください。

## インストール

```bash
pip install プロジェクト名
```

## 使い方

```python
import プロジェクト名

# 使用例
```

## 機能

- 機能1
- 機能2
- 機能3

## ライセンス

MIT License
""",
    }

    CSS = """\
<style>
body { font-family: 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto;
       padding: 20px; line-height: 1.6; color: #333; }
h1,h2,h3,h4 { color: #0d47a1; }
h1 { border-bottom: 2px solid #0d47a1; padding-bottom: 8px; }
h2 { border-bottom: 1px solid #ddd; padding-bottom: 4px; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 4px;
       font-family: Consolas, monospace; font-size: 0.9em; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 16px;
      border-radius: 8px; overflow-x: auto; }
pre code { background: none; color: inherit; }
blockquote { border-left: 4px solid #0d47a1; margin: 0;
             padding-left: 16px; color: #555; }
table { border-collapse: collapse; width: 100%; }
th { background: #0d47a1; color: white; padding: 8px 12px; }
td { border: 1px solid #ddd; padding: 8px 12px; }
tr:nth-child(even) { background: #f9f9f9; }
a { color: #1565c0; }
hr { border: none; border-top: 2px solid #ddd; }
</style>
"""

    def __init__(self, root):
        self.root = root
        self.root.title("Markdownエディタ(プレビュー付き)")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")
        self._current_file = None
        self._modified = False
        self._build_ui()
        self._load_sample("基本サンプル")

    def _build_ui(self):
        # ツールバー
        toolbar = tk.Frame(self.root, bg="#252526", pady=4)
        toolbar.pack(fill=tk.X)
        tk.Label(toolbar, text="📝 Markdownエディタ",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # サンプル
        tk.Label(toolbar, text="サンプル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 4))
        self.sample_var = tk.StringVar()
        sample_cb = ttk.Combobox(toolbar, textvariable=self.sample_var,
                                  values=list(self.SAMPLES.keys()),
                                  state="readonly", width=18)
        sample_cb.pack(side=tk.LEFT)
        sample_cb.bind("<<ComboboxSelected>>",
                       lambda e: self._load_sample(self.sample_var.get()))

        # ボタン
        for text, cmd in [("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("💾 別名保存", self._save_as_file)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="white", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=3,
                      activebackground="#505050", bd=0).pack(side=tk.LEFT, padx=4)

        # ビューモード
        tk.Label(toolbar, text="表示:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.RIGHT, padx=4)
        self.view_mode = tk.StringVar(value="split")
        for val, lbl in [("edit", "編集"), ("split", "分割"), ("preview", "プレビュー")]:
            ttk.Radiobutton(toolbar, text=lbl, variable=self.view_mode,
                            value=val,
                            command=self._change_view_mode
                            ).pack(side=tk.RIGHT, padx=2)

        if not MD_AVAILABLE:
            tk.Label(self.root,
                     text="⚠ markdown が未インストール (pip install markdown)。"
                          "簡易テキストプレビューになります。",
                     bg="#fff3cd", fg="#856404", font=("Arial", 9),
                     anchor="w", padx=8).pack(fill=tk.X)

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

        # 左: エディタ
        self.edit_frame = tk.Frame(self.main_paned, bg="#1e1e1e")
        self.main_paned.add(self.edit_frame, weight=1)

        # 行番号
        num_frame = tk.Frame(self.edit_frame, bg="#1e1e1e")
        num_frame.pack(fill=tk.BOTH, expand=True)
        self.line_nums = tk.Text(num_frame, width=4, bg="#252526",
                                  fg="#858585", font=("Courier New", 12),
                                  state=tk.DISABLED, relief=tk.FLAT,
                                  padx=4, pady=4, takefocus=0)
        self.line_nums.pack(side=tk.LEFT, fill=tk.Y)
        self.editor = tk.Text(num_frame, bg="#1e1e1e", fg="#d4d4d4",
                               font=("Courier New", 12), insertbackground="#aeafad",
                               relief=tk.FLAT, undo=True, padx=8, pady=4,
                               selectbackground="#264f78",
                               wrap=tk.WORD)
        ed_sb = ttk.Scrollbar(num_frame, command=self._scroll_editor)
        self.editor.configure(yscrollcommand=ed_sb.set)
        ed_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.editor.pack(fill=tk.BOTH, expand=True)
        self.editor.bind("<KeyRelease>", self._on_key)
        self._setup_editor_tags()

        # 右: プレビュー
        self.preview_frame = tk.Frame(self.main_paned, bg="#ffffff")
        self.main_paned.add(self.preview_frame, weight=1)

        if TKWEB_AVAILABLE:
            self.html_viewer = tkinterweb.HtmlFrame(
                self.preview_frame, messages_enabled=False)
            self.html_viewer.pack(fill=tk.BOTH, expand=True)
        else:
            # フォールバック: Text widget
            self.preview_text = tk.Text(
                self.preview_frame, bg="#ffffff", fg="#333",
                font=("Arial", 12), relief=tk.FLAT, wrap=tk.WORD,
                state=tk.DISABLED, padx=12, pady=12)
            pr_sb = ttk.Scrollbar(self.preview_frame,
                                   command=self.preview_text.yview)
            self.preview_text.configure(yscrollcommand=pr_sb.set)
            pr_sb.pack(side=tk.RIGHT, fill=tk.Y)
            self.preview_text.pack(fill=tk.BOTH, expand=True)
            self._setup_preview_tags()

        # ステータス
        self.status_var = tk.StringVar(value="準備完了")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#252526", fg="#858585", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _setup_editor_tags(self):
        # Markdown構文色付け
        self.editor.tag_configure("heading", foreground="#4fc3f7",
                                   font=("Courier New", 13, "bold"))
        self.editor.tag_configure("bold", foreground="#ffffff",
                                   font=("Courier New", 12, "bold"))
        self.editor.tag_configure("italic", foreground="#ce9178",
                                   font=("Courier New", 12, "italic"))
        self.editor.tag_configure("code", foreground="#4ec9b0",
                                   background="#2d2d30")
        self.editor.tag_configure("link", foreground="#569cd6")
        self.editor.tag_configure("list_item", foreground="#dcdcaa")
        self.editor.tag_configure("quote", foreground="#6a9955")
        self.editor.tag_configure("hr", foreground="#555555")

    def _setup_preview_tags(self):
        self.preview_text.tag_configure("h1", font=("Arial", 20, "bold"),
                                         foreground="#0d47a1",
                                         spacing3=8)
        self.preview_text.tag_configure("h2", font=("Arial", 16, "bold"),
                                         foreground="#1565c0",
                                         spacing3=6)
        self.preview_text.tag_configure("h3", font=("Arial", 14, "bold"),
                                         foreground="#1976d2")
        self.preview_text.tag_configure("code_inline",
                                         font=("Courier New", 11),
                                         background="#f4f4f4")
        self.preview_text.tag_configure("code_block",
                                         font=("Courier New", 11),
                                         background="#1e1e1e",
                                         foreground="#d4d4d4",
                                         spacing1=4, spacing3=4)
        self.preview_text.tag_configure("bold_text",
                                         font=("Arial", 12, "bold"))
        self.preview_text.tag_configure("italic_text",
                                         font=("Arial", 12, "italic"))
        self.preview_text.tag_configure("quote_text",
                                         foreground="#555555",
                                         lmargin1=20, lmargin2=20)
        self.preview_text.tag_configure("bullet",
                                         foreground="#0d47a1")

    def _scroll_editor(self, *args):
        self.editor.yview(*args)
        self.line_nums.yview(*args)

    def _on_key(self, event=None):
        self._update_line_nums()
        self._highlight_md()
        self._update_preview()
        self._modified = True
        lines = int(self.editor.index(tk.END).split(".")[0]) - 1
        chars = len(self.editor.get("1.0", tk.END)) - 1
        self.status_var.set(f"行: {lines}  文字: {chars}")

    def _update_line_nums(self):
        content = self.editor.get("1.0", tk.END)
        lines = content.count("\n")
        self.line_nums.config(state=tk.NORMAL)
        self.line_nums.delete("1.0", tk.END)
        self.line_nums.insert("1.0", "\n".join(str(i) for i in range(1, lines + 1)))
        self.line_nums.config(state=tk.DISABLED)

    def _highlight_md(self):
        content = self.editor.get("1.0", tk.END)
        for tag in ["heading", "bold", "italic", "code", "link",
                    "list_item", "quote", "hr"]:
            self.editor.tag_remove(tag, "1.0", tk.END)
        patterns = [
            ("heading",   r"^#{1,6} .+$"),
            ("code",      r"`[^`\n]+`"),
            ("bold",      r"\*\*[^*\n]+\*\*"),
            ("italic",    r"\*[^*\n]+\*"),
            ("link",      r"\[.*?\]\(.*?\)"),
            ("list_item", r"^[\s]*[-*+] .+$"),
            ("list_item", r"^\d+\. .+$"),
            ("quote",     r"^> .+$"),
            ("hr",        r"^[-*_]{3,}$"),
        ]
        for tag, pattern in patterns:
            for m in re.finditer(pattern, content, re.MULTILINE):
                start = f"1.0+{m.start()}c"
                end = f"1.0+{m.end()}c"
                self.editor.tag_add(tag, start, end)

    def _update_preview(self):
        content = self.editor.get("1.0", tk.END)
        if TKWEB_AVAILABLE:
            if MD_AVAILABLE:
                html = markdown.markdown(
                    content,
                    extensions=["tables", "fenced_code", "codehilite", "nl2br"])
            else:
                html = self._simple_md(content)
            full_html = f"<html><head>{self.CSS}</head><body>{html}</body></html>"
            self.html_viewer.load_html(full_html)
        else:
            # フォールバック
            self.preview_text.config(state=tk.NORMAL)
            self.preview_text.delete("1.0", tk.END)
            self._render_preview_text(content)
            self.preview_text.config(state=tk.DISABLED)

    def _simple_md(self, text):
        """markdownライブラリなしの簡易変換"""
        lines = text.split("\n")
        html_lines = []
        in_code = False
        for line in lines:
            if line.startswith("```"):
                if in_code:
                    html_lines.append("</pre>")
                    in_code = False
                else:
                    html_lines.append("<pre><code>")
                    in_code = True
            elif in_code:
                html_lines.append(line)
            elif re.match(r"^######", line):
                html_lines.append(f"<h6>{line[7:]}</h6>")
            elif re.match(r"^#####", line):
                html_lines.append(f"<h5>{line[6:]}</h5>")
            elif re.match(r"^####", line):
                html_lines.append(f"<h4>{line[5:]}</h4>")
            elif re.match(r"^###", line):
                html_lines.append(f"<h3>{line[4:]}</h3>")
            elif re.match(r"^##", line):
                html_lines.append(f"<h2>{line[3:]}</h2>")
            elif re.match(r"^#", line):
                html_lines.append(f"<h1>{line[2:]}</h1>")
            elif re.match(r"^[-*+] ", line):
                html_lines.append(f"<li>{line[2:]}</li>")
            elif re.match(r"^> ", line):
                html_lines.append(f"<blockquote>{line[2:]}</blockquote>")
            elif re.match(r"^[-*_]{3,}$", line):
                html_lines.append("<hr>")
            else:
                l = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", line)
                l = re.sub(r"\*(.+?)\*", r"<em>\1</em>", l)
                l = re.sub(r"`(.+?)`", r"<code>\1</code>", l)
                l = re.sub(r"\[(.+?)\]\((.+?)\)", r'<a href="\2">\1</a>', l)
                html_lines.append(f"<p>{l}</p>" if l else "")
        return "\n".join(html_lines)

    def _render_preview_text(self, content):
        """tkinterweb未使用時のフォールバックプレビュー"""
        for line in content.split("\n"):
            if re.match(r"^# ", line):
                self.preview_text.insert(tk.END, line[2:] + "\n", "h1")
            elif re.match(r"^## ", line):
                self.preview_text.insert(tk.END, line[3:] + "\n", "h2")
            elif re.match(r"^### ", line):
                self.preview_text.insert(tk.END, line[4:] + "\n", "h3")
            elif re.match(r"^[-*+] ", line):
                self.preview_text.insert(tk.END, "• " + line[2:] + "\n", "bullet")
            elif re.match(r"^> ", line):
                self.preview_text.insert(tk.END, "  " + line[2:] + "\n", "quote_text")
            else:
                self.preview_text.insert(tk.END, line + "\n")

    def _change_view_mode(self):
        mode = self.view_mode.get()
        # ペインを作り直す
        for item in self.main_paned.panes():
            self.main_paned.forget(item)
        if mode == "edit":
            self.main_paned.add(self.edit_frame, weight=1)
        elif mode == "preview":
            self.main_paned.add(self.preview_frame, weight=1)
        else:
            self.main_paned.add(self.edit_frame, weight=1)
            self.main_paned.add(self.preview_frame, weight=1)

    def _load_sample(self, name):
        content = self.SAMPLES.get(name, "")
        self.editor.delete("1.0", tk.END)
        self.editor.insert("1.0", content)
        self._on_key()

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Markdown", "*.md *.markdown *.txt"),
                       ("すべて", "*.*")])
        if path:
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self.editor.delete("1.0", tk.END)
                self.editor.insert("1.0", content)
                self._current_file = path
                self._modified = False
                self._on_key()
                self.root.title(f"Markdownエディタ — {os.path.basename(path)}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _save_file(self):
        if self._current_file:
            self._write_file(self._current_file)
        else:
            self._save_as_file()

    def _save_as_file(self):
        path = filedialog.asksaveasfilename(
            defaultextension=".md",
            filetypes=[("Markdown", "*.md"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if path:
            self._write_file(path)
            self._current_file = path
            self.root.title(f"Markdownエディタ — {os.path.basename(path)}")

    def _write_file(self, path):
        try:
            content = self.editor.get("1.0", tk.END)
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
            self._modified = False
            self.status_var.set(f"保存しました: {path}")
        except Exception as e:
            messagebox.showerror("エラー", str(e))


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

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

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

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

try:
    import markdown
    MD_AVAILABLE = True
except ImportError:
    MD_AVAILABLE = False

try:
    import tkinterweb
    TKWEB_AVAILABLE = True
except ImportError:
    TKWEB_AVAILABLE = False


class App36:
    """Markdownエディタ(プレビュー付き)"""

    SAMPLES = {
        "基本サンプル": """\
# Markdownエディタ

## 見出し

### 小見出し

通常のテキスト。**太字** や *斜体* が使えます。

## リスト

- 項目1
- 項目2
  - ネスト項目
- 項目3

1. 番号付きリスト
2. 2番目
3. 3番目

## コード

インラインコード: `print("Hello")`

```python
def hello(name):
    return f"Hello, {name}!"

print(hello("World"))
```

## リンクと画像

[Python公式](https://www.python.org)

## テーブル

| 名前 | 年齢 | 職業 |
|------|------|------|
| 田中 | 25   | エンジニア |
| 鈴木 | 30   | デザイナー |

## 引用

> これは引用です。
> 複数行も可能。

---

水平線の上下に区切り線が表示されます。
""",
        "README テンプレート": """\
# プロジェクト名

## 概要

プロジェクトの概要を記述してください。

## インストール

```bash
pip install プロジェクト名
```

## 使い方

```python
import プロジェクト名

# 使用例
```

## 機能

- 機能1
- 機能2
- 機能3

## ライセンス

MIT License
""",
    }

    CSS = """\
<style>
body { font-family: 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto;
       padding: 20px; line-height: 1.6; color: #333; }
h1,h2,h3,h4 { color: #0d47a1; }
h1 { border-bottom: 2px solid #0d47a1; padding-bottom: 8px; }
h2 { border-bottom: 1px solid #ddd; padding-bottom: 4px; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 4px;
       font-family: Consolas, monospace; font-size: 0.9em; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 16px;
      border-radius: 8px; overflow-x: auto; }
pre code { background: none; color: inherit; }
blockquote { border-left: 4px solid #0d47a1; margin: 0;
             padding-left: 16px; color: #555; }
table { border-collapse: collapse; width: 100%; }
th { background: #0d47a1; color: white; padding: 8px 12px; }
td { border: 1px solid #ddd; padding: 8px 12px; }
tr:nth-child(even) { background: #f9f9f9; }
a { color: #1565c0; }
hr { border: none; border-top: 2px solid #ddd; }
</style>
"""

    def __init__(self, root):
        self.root = root
        self.root.title("Markdownエディタ(プレビュー付き)")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")
        self._current_file = None
        self._modified = False
        self._build_ui()
        self._load_sample("基本サンプル")

    def _build_ui(self):
        # ツールバー
        toolbar = tk.Frame(self.root, bg="#252526", pady=4)
        toolbar.pack(fill=tk.X)
        tk.Label(toolbar, text="📝 Markdownエディタ",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # サンプル
        tk.Label(toolbar, text="サンプル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 4))
        self.sample_var = tk.StringVar()
        sample_cb = ttk.Combobox(toolbar, textvariable=self.sample_var,
                                  values=list(self.SAMPLES.keys()),
                                  state="readonly", width=18)
        sample_cb.pack(side=tk.LEFT)
        sample_cb.bind("<<ComboboxSelected>>",
                       lambda e: self._load_sample(self.sample_var.get()))

        # ボタン
        for text, cmd in [("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("💾 別名保存", self._save_as_file)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="white", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=3,
                      activebackground="#505050", bd=0).pack(side=tk.LEFT, padx=4)

        # ビューモード
        tk.Label(toolbar, text="表示:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.RIGHT, padx=4)
        self.view_mode = tk.StringVar(value="split")
        for val, lbl in [("edit", "編集"), ("split", "分割"), ("preview", "プレビュー")]:
            ttk.Radiobutton(toolbar, text=lbl, variable=self.view_mode,
                            value=val,
                            command=self._change_view_mode
                            ).pack(side=tk.RIGHT, padx=2)

        if not MD_AVAILABLE:
            tk.Label(self.root,
                     text="⚠ markdown が未インストール (pip install markdown)。"
                          "簡易テキストプレビューになります。",
                     bg="#fff3cd", fg="#856404", font=("Arial", 9),
                     anchor="w", padx=8).pack(fill=tk.X)

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

        # 左: エディタ
        self.edit_frame = tk.Frame(self.main_paned, bg="#1e1e1e")
        self.main_paned.add(self.edit_frame, weight=1)

        # 行番号
        num_frame = tk.Frame(self.edit_frame, bg="#1e1e1e")
        num_frame.pack(fill=tk.BOTH, expand=True)
        self.line_nums = tk.Text(num_frame, width=4, bg="#252526",
                                  fg="#858585", font=("Courier New", 12),
                                  state=tk.DISABLED, relief=tk.FLAT,
                                  padx=4, pady=4, takefocus=0)
        self.line_nums.pack(side=tk.LEFT, fill=tk.Y)
        self.editor = tk.Text(num_frame, bg="#1e1e1e", fg="#d4d4d4",
                               font=("Courier New", 12), insertbackground="#aeafad",
                               relief=tk.FLAT, undo=True, padx=8, pady=4,
                               selectbackground="#264f78",
                               wrap=tk.WORD)
        ed_sb = ttk.Scrollbar(num_frame, command=self._scroll_editor)
        self.editor.configure(yscrollcommand=ed_sb.set)
        ed_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.editor.pack(fill=tk.BOTH, expand=True)
        self.editor.bind("<KeyRelease>", self._on_key)
        self._setup_editor_tags()

        # 右: プレビュー
        self.preview_frame = tk.Frame(self.main_paned, bg="#ffffff")
        self.main_paned.add(self.preview_frame, weight=1)

        if TKWEB_AVAILABLE:
            self.html_viewer = tkinterweb.HtmlFrame(
                self.preview_frame, messages_enabled=False)
            self.html_viewer.pack(fill=tk.BOTH, expand=True)
        else:
            # フォールバック: Text widget
            self.preview_text = tk.Text(
                self.preview_frame, bg="#ffffff", fg="#333",
                font=("Arial", 12), relief=tk.FLAT, wrap=tk.WORD,
                state=tk.DISABLED, padx=12, pady=12)
            pr_sb = ttk.Scrollbar(self.preview_frame,
                                   command=self.preview_text.yview)
            self.preview_text.configure(yscrollcommand=pr_sb.set)
            pr_sb.pack(side=tk.RIGHT, fill=tk.Y)
            self.preview_text.pack(fill=tk.BOTH, expand=True)
            self._setup_preview_tags()

        # ステータス
        self.status_var = tk.StringVar(value="準備完了")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#252526", fg="#858585", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _setup_editor_tags(self):
        # Markdown構文色付け
        self.editor.tag_configure("heading", foreground="#4fc3f7",
                                   font=("Courier New", 13, "bold"))
        self.editor.tag_configure("bold", foreground="#ffffff",
                                   font=("Courier New", 12, "bold"))
        self.editor.tag_configure("italic", foreground="#ce9178",
                                   font=("Courier New", 12, "italic"))
        self.editor.tag_configure("code", foreground="#4ec9b0",
                                   background="#2d2d30")
        self.editor.tag_configure("link", foreground="#569cd6")
        self.editor.tag_configure("list_item", foreground="#dcdcaa")
        self.editor.tag_configure("quote", foreground="#6a9955")
        self.editor.tag_configure("hr", foreground="#555555")

    def _setup_preview_tags(self):
        self.preview_text.tag_configure("h1", font=("Arial", 20, "bold"),
                                         foreground="#0d47a1",
                                         spacing3=8)
        self.preview_text.tag_configure("h2", font=("Arial", 16, "bold"),
                                         foreground="#1565c0",
                                         spacing3=6)
        self.preview_text.tag_configure("h3", font=("Arial", 14, "bold"),
                                         foreground="#1976d2")
        self.preview_text.tag_configure("code_inline",
                                         font=("Courier New", 11),
                                         background="#f4f4f4")
        self.preview_text.tag_configure("code_block",
                                         font=("Courier New", 11),
                                         background="#1e1e1e",
                                         foreground="#d4d4d4",
                                         spacing1=4, spacing3=4)
        self.preview_text.tag_configure("bold_text",
                                         font=("Arial", 12, "bold"))
        self.preview_text.tag_configure("italic_text",
                                         font=("Arial", 12, "italic"))
        self.preview_text.tag_configure("quote_text",
                                         foreground="#555555",
                                         lmargin1=20, lmargin2=20)
        self.preview_text.tag_configure("bullet",
                                         foreground="#0d47a1")

    def _scroll_editor(self, *args):
        self.editor.yview(*args)
        self.line_nums.yview(*args)

    def _on_key(self, event=None):
        self._update_line_nums()
        self._highlight_md()
        self._update_preview()
        self._modified = True
        lines = int(self.editor.index(tk.END).split(".")[0]) - 1
        chars = len(self.editor.get("1.0", tk.END)) - 1
        self.status_var.set(f"行: {lines}  文字: {chars}")

    def _update_line_nums(self):
        content = self.editor.get("1.0", tk.END)
        lines = content.count("\n")
        self.line_nums.config(state=tk.NORMAL)
        self.line_nums.delete("1.0", tk.END)
        self.line_nums.insert("1.0", "\n".join(str(i) for i in range(1, lines + 1)))
        self.line_nums.config(state=tk.DISABLED)

    def _highlight_md(self):
        content = self.editor.get("1.0", tk.END)
        for tag in ["heading", "bold", "italic", "code", "link",
                    "list_item", "quote", "hr"]:
            self.editor.tag_remove(tag, "1.0", tk.END)
        patterns = [
            ("heading",   r"^#{1,6} .+$"),
            ("code",      r"`[^`\n]+`"),
            ("bold",      r"\*\*[^*\n]+\*\*"),
            ("italic",    r"\*[^*\n]+\*"),
            ("link",      r"\[.*?\]\(.*?\)"),
            ("list_item", r"^[\s]*[-*+] .+$"),
            ("list_item", r"^\d+\. .+$"),
            ("quote",     r"^> .+$"),
            ("hr",        r"^[-*_]{3,}$"),
        ]
        for tag, pattern in patterns:
            for m in re.finditer(pattern, content, re.MULTILINE):
                start = f"1.0+{m.start()}c"
                end = f"1.0+{m.end()}c"
                self.editor.tag_add(tag, start, end)

    def _update_preview(self):
        content = self.editor.get("1.0", tk.END)
        if TKWEB_AVAILABLE:
            if MD_AVAILABLE:
                html = markdown.markdown(
                    content,
                    extensions=["tables", "fenced_code", "codehilite", "nl2br"])
            else:
                html = self._simple_md(content)
            full_html = f"<html><head>{self.CSS}</head><body>{html}</body></html>"
            self.html_viewer.load_html(full_html)
        else:
            # フォールバック
            self.preview_text.config(state=tk.NORMAL)
            self.preview_text.delete("1.0", tk.END)
            self._render_preview_text(content)
            self.preview_text.config(state=tk.DISABLED)

    def _simple_md(self, text):
        """markdownライブラリなしの簡易変換"""
        lines = text.split("\n")
        html_lines = []
        in_code = False
        for line in lines:
            if line.startswith("```"):
                if in_code:
                    html_lines.append("</pre>")
                    in_code = False
                else:
                    html_lines.append("<pre><code>")
                    in_code = True
            elif in_code:
                html_lines.append(line)
            elif re.match(r"^######", line):
                html_lines.append(f"<h6>{line[7:]}</h6>")
            elif re.match(r"^#####", line):
                html_lines.append(f"<h5>{line[6:]}</h5>")
            elif re.match(r"^####", line):
                html_lines.append(f"<h4>{line[5:]}</h4>")
            elif re.match(r"^###", line):
                html_lines.append(f"<h3>{line[4:]}</h3>")
            elif re.match(r"^##", line):
                html_lines.append(f"<h2>{line[3:]}</h2>")
            elif re.match(r"^#", line):
                html_lines.append(f"<h1>{line[2:]}</h1>")
            elif re.match(r"^[-*+] ", line):
                html_lines.append(f"<li>{line[2:]}</li>")
            elif re.match(r"^> ", line):
                html_lines.append(f"<blockquote>{line[2:]}</blockquote>")
            elif re.match(r"^[-*_]{3,}$", line):
                html_lines.append("<hr>")
            else:
                l = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", line)
                l = re.sub(r"\*(.+?)\*", r"<em>\1</em>", l)
                l = re.sub(r"`(.+?)`", r"<code>\1</code>", l)
                l = re.sub(r"\[(.+?)\]\((.+?)\)", r'<a href="\2">\1</a>', l)
                html_lines.append(f"<p>{l}</p>" if l else "")
        return "\n".join(html_lines)

    def _render_preview_text(self, content):
        """tkinterweb未使用時のフォールバックプレビュー"""
        for line in content.split("\n"):
            if re.match(r"^# ", line):
                self.preview_text.insert(tk.END, line[2:] + "\n", "h1")
            elif re.match(r"^## ", line):
                self.preview_text.insert(tk.END, line[3:] + "\n", "h2")
            elif re.match(r"^### ", line):
                self.preview_text.insert(tk.END, line[4:] + "\n", "h3")
            elif re.match(r"^[-*+] ", line):
                self.preview_text.insert(tk.END, "• " + line[2:] + "\n", "bullet")
            elif re.match(r"^> ", line):
                self.preview_text.insert(tk.END, "  " + line[2:] + "\n", "quote_text")
            else:
                self.preview_text.insert(tk.END, line + "\n")

    def _change_view_mode(self):
        mode = self.view_mode.get()
        # ペインを作り直す
        for item in self.main_paned.panes():
            self.main_paned.forget(item)
        if mode == "edit":
            self.main_paned.add(self.edit_frame, weight=1)
        elif mode == "preview":
            self.main_paned.add(self.preview_frame, weight=1)
        else:
            self.main_paned.add(self.edit_frame, weight=1)
            self.main_paned.add(self.preview_frame, weight=1)

    def _load_sample(self, name):
        content = self.SAMPLES.get(name, "")
        self.editor.delete("1.0", tk.END)
        self.editor.insert("1.0", content)
        self._on_key()

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Markdown", "*.md *.markdown *.txt"),
                       ("すべて", "*.*")])
        if path:
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self.editor.delete("1.0", tk.END)
                self.editor.insert("1.0", content)
                self._current_file = path
                self._modified = False
                self._on_key()
                self.root.title(f"Markdownエディタ — {os.path.basename(path)}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _save_file(self):
        if self._current_file:
            self._write_file(self._current_file)
        else:
            self._save_as_file()

    def _save_as_file(self):
        path = filedialog.asksaveasfilename(
            defaultextension=".md",
            filetypes=[("Markdown", "*.md"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if path:
            self._write_file(path)
            self._current_file = path
            self.root.title(f"Markdownエディタ — {os.path.basename(path)}")

    def _write_file(self, path):
        try:
            content = self.editor.get("1.0", tk.END)
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
            self._modified = False
            self.status_var.set(f"保存しました: {path}")
        except Exception as e:
            messagebox.showerror("エラー", str(e))


if __name__ == "__main__":
    root = tk.Tk()
    app = App36(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

try:
    import markdown
    MD_AVAILABLE = True
except ImportError:
    MD_AVAILABLE = False

try:
    import tkinterweb
    TKWEB_AVAILABLE = True
except ImportError:
    TKWEB_AVAILABLE = False


class App36:
    """Markdownエディタ(プレビュー付き)"""

    SAMPLES = {
        "基本サンプル": """\
# Markdownエディタ

## 見出し

### 小見出し

通常のテキスト。**太字** や *斜体* が使えます。

## リスト

- 項目1
- 項目2
  - ネスト項目
- 項目3

1. 番号付きリスト
2. 2番目
3. 3番目

## コード

インラインコード: `print("Hello")`

```python
def hello(name):
    return f"Hello, {name}!"

print(hello("World"))
```

## リンクと画像

[Python公式](https://www.python.org)

## テーブル

| 名前 | 年齢 | 職業 |
|------|------|------|
| 田中 | 25   | エンジニア |
| 鈴木 | 30   | デザイナー |

## 引用

> これは引用です。
> 複数行も可能。

---

水平線の上下に区切り線が表示されます。
""",
        "README テンプレート": """\
# プロジェクト名

## 概要

プロジェクトの概要を記述してください。

## インストール

```bash
pip install プロジェクト名
```

## 使い方

```python
import プロジェクト名

# 使用例
```

## 機能

- 機能1
- 機能2
- 機能3

## ライセンス

MIT License
""",
    }

    CSS = """\
<style>
body { font-family: 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto;
       padding: 20px; line-height: 1.6; color: #333; }
h1,h2,h3,h4 { color: #0d47a1; }
h1 { border-bottom: 2px solid #0d47a1; padding-bottom: 8px; }
h2 { border-bottom: 1px solid #ddd; padding-bottom: 4px; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 4px;
       font-family: Consolas, monospace; font-size: 0.9em; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 16px;
      border-radius: 8px; overflow-x: auto; }
pre code { background: none; color: inherit; }
blockquote { border-left: 4px solid #0d47a1; margin: 0;
             padding-left: 16px; color: #555; }
table { border-collapse: collapse; width: 100%; }
th { background: #0d47a1; color: white; padding: 8px 12px; }
td { border: 1px solid #ddd; padding: 8px 12px; }
tr:nth-child(even) { background: #f9f9f9; }
a { color: #1565c0; }
hr { border: none; border-top: 2px solid #ddd; }
</style>
"""

    def __init__(self, root):
        self.root = root
        self.root.title("Markdownエディタ(プレビュー付き)")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")
        self._current_file = None
        self._modified = False
        self._build_ui()
        self._load_sample("基本サンプル")

    def _build_ui(self):
        # ツールバー
        toolbar = tk.Frame(self.root, bg="#252526", pady=4)
        toolbar.pack(fill=tk.X)
        tk.Label(toolbar, text="📝 Markdownエディタ",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # サンプル
        tk.Label(toolbar, text="サンプル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 4))
        self.sample_var = tk.StringVar()
        sample_cb = ttk.Combobox(toolbar, textvariable=self.sample_var,
                                  values=list(self.SAMPLES.keys()),
                                  state="readonly", width=18)
        sample_cb.pack(side=tk.LEFT)
        sample_cb.bind("<<ComboboxSelected>>",
                       lambda e: self._load_sample(self.sample_var.get()))

        # ボタン
        for text, cmd in [("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("💾 別名保存", self._save_as_file)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="white", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=3,
                      activebackground="#505050", bd=0).pack(side=tk.LEFT, padx=4)

        # ビューモード
        tk.Label(toolbar, text="表示:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.RIGHT, padx=4)
        self.view_mode = tk.StringVar(value="split")
        for val, lbl in [("edit", "編集"), ("split", "分割"), ("preview", "プレビュー")]:
            ttk.Radiobutton(toolbar, text=lbl, variable=self.view_mode,
                            value=val,
                            command=self._change_view_mode
                            ).pack(side=tk.RIGHT, padx=2)

        if not MD_AVAILABLE:
            tk.Label(self.root,
                     text="⚠ markdown が未インストール (pip install markdown)。"
                          "簡易テキストプレビューになります。",
                     bg="#fff3cd", fg="#856404", font=("Arial", 9),
                     anchor="w", padx=8).pack(fill=tk.X)

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

        # 左: エディタ
        self.edit_frame = tk.Frame(self.main_paned, bg="#1e1e1e")
        self.main_paned.add(self.edit_frame, weight=1)

        # 行番号
        num_frame = tk.Frame(self.edit_frame, bg="#1e1e1e")
        num_frame.pack(fill=tk.BOTH, expand=True)
        self.line_nums = tk.Text(num_frame, width=4, bg="#252526",
                                  fg="#858585", font=("Courier New", 12),
                                  state=tk.DISABLED, relief=tk.FLAT,
                                  padx=4, pady=4, takefocus=0)
        self.line_nums.pack(side=tk.LEFT, fill=tk.Y)
        self.editor = tk.Text(num_frame, bg="#1e1e1e", fg="#d4d4d4",
                               font=("Courier New", 12), insertbackground="#aeafad",
                               relief=tk.FLAT, undo=True, padx=8, pady=4,
                               selectbackground="#264f78",
                               wrap=tk.WORD)
        ed_sb = ttk.Scrollbar(num_frame, command=self._scroll_editor)
        self.editor.configure(yscrollcommand=ed_sb.set)
        ed_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.editor.pack(fill=tk.BOTH, expand=True)
        self.editor.bind("<KeyRelease>", self._on_key)
        self._setup_editor_tags()

        # 右: プレビュー
        self.preview_frame = tk.Frame(self.main_paned, bg="#ffffff")
        self.main_paned.add(self.preview_frame, weight=1)

        if TKWEB_AVAILABLE:
            self.html_viewer = tkinterweb.HtmlFrame(
                self.preview_frame, messages_enabled=False)
            self.html_viewer.pack(fill=tk.BOTH, expand=True)
        else:
            # フォールバック: Text widget
            self.preview_text = tk.Text(
                self.preview_frame, bg="#ffffff", fg="#333",
                font=("Arial", 12), relief=tk.FLAT, wrap=tk.WORD,
                state=tk.DISABLED, padx=12, pady=12)
            pr_sb = ttk.Scrollbar(self.preview_frame,
                                   command=self.preview_text.yview)
            self.preview_text.configure(yscrollcommand=pr_sb.set)
            pr_sb.pack(side=tk.RIGHT, fill=tk.Y)
            self.preview_text.pack(fill=tk.BOTH, expand=True)
            self._setup_preview_tags()

        # ステータス
        self.status_var = tk.StringVar(value="準備完了")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#252526", fg="#858585", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _setup_editor_tags(self):
        # Markdown構文色付け
        self.editor.tag_configure("heading", foreground="#4fc3f7",
                                   font=("Courier New", 13, "bold"))
        self.editor.tag_configure("bold", foreground="#ffffff",
                                   font=("Courier New", 12, "bold"))
        self.editor.tag_configure("italic", foreground="#ce9178",
                                   font=("Courier New", 12, "italic"))
        self.editor.tag_configure("code", foreground="#4ec9b0",
                                   background="#2d2d30")
        self.editor.tag_configure("link", foreground="#569cd6")
        self.editor.tag_configure("list_item", foreground="#dcdcaa")
        self.editor.tag_configure("quote", foreground="#6a9955")
        self.editor.tag_configure("hr", foreground="#555555")

    def _setup_preview_tags(self):
        self.preview_text.tag_configure("h1", font=("Arial", 20, "bold"),
                                         foreground="#0d47a1",
                                         spacing3=8)
        self.preview_text.tag_configure("h2", font=("Arial", 16, "bold"),
                                         foreground="#1565c0",
                                         spacing3=6)
        self.preview_text.tag_configure("h3", font=("Arial", 14, "bold"),
                                         foreground="#1976d2")
        self.preview_text.tag_configure("code_inline",
                                         font=("Courier New", 11),
                                         background="#f4f4f4")
        self.preview_text.tag_configure("code_block",
                                         font=("Courier New", 11),
                                         background="#1e1e1e",
                                         foreground="#d4d4d4",
                                         spacing1=4, spacing3=4)
        self.preview_text.tag_configure("bold_text",
                                         font=("Arial", 12, "bold"))
        self.preview_text.tag_configure("italic_text",
                                         font=("Arial", 12, "italic"))
        self.preview_text.tag_configure("quote_text",
                                         foreground="#555555",
                                         lmargin1=20, lmargin2=20)
        self.preview_text.tag_configure("bullet",
                                         foreground="#0d47a1")

    def _scroll_editor(self, *args):
        self.editor.yview(*args)
        self.line_nums.yview(*args)

    def _on_key(self, event=None):
        self._update_line_nums()
        self._highlight_md()
        self._update_preview()
        self._modified = True
        lines = int(self.editor.index(tk.END).split(".")[0]) - 1
        chars = len(self.editor.get("1.0", tk.END)) - 1
        self.status_var.set(f"行: {lines}  文字: {chars}")

    def _update_line_nums(self):
        content = self.editor.get("1.0", tk.END)
        lines = content.count("\n")
        self.line_nums.config(state=tk.NORMAL)
        self.line_nums.delete("1.0", tk.END)
        self.line_nums.insert("1.0", "\n".join(str(i) for i in range(1, lines + 1)))
        self.line_nums.config(state=tk.DISABLED)

    def _highlight_md(self):
        content = self.editor.get("1.0", tk.END)
        for tag in ["heading", "bold", "italic", "code", "link",
                    "list_item", "quote", "hr"]:
            self.editor.tag_remove(tag, "1.0", tk.END)
        patterns = [
            ("heading",   r"^#{1,6} .+$"),
            ("code",      r"`[^`\n]+`"),
            ("bold",      r"\*\*[^*\n]+\*\*"),
            ("italic",    r"\*[^*\n]+\*"),
            ("link",      r"\[.*?\]\(.*?\)"),
            ("list_item", r"^[\s]*[-*+] .+$"),
            ("list_item", r"^\d+\. .+$"),
            ("quote",     r"^> .+$"),
            ("hr",        r"^[-*_]{3,}$"),
        ]
        for tag, pattern in patterns:
            for m in re.finditer(pattern, content, re.MULTILINE):
                start = f"1.0+{m.start()}c"
                end = f"1.0+{m.end()}c"
                self.editor.tag_add(tag, start, end)

    def _update_preview(self):
        content = self.editor.get("1.0", tk.END)
        if TKWEB_AVAILABLE:
            if MD_AVAILABLE:
                html = markdown.markdown(
                    content,
                    extensions=["tables", "fenced_code", "codehilite", "nl2br"])
            else:
                html = self._simple_md(content)
            full_html = f"<html><head>{self.CSS}</head><body>{html}</body></html>"
            self.html_viewer.load_html(full_html)
        else:
            # フォールバック
            self.preview_text.config(state=tk.NORMAL)
            self.preview_text.delete("1.0", tk.END)
            self._render_preview_text(content)
            self.preview_text.config(state=tk.DISABLED)

    def _simple_md(self, text):
        """markdownライブラリなしの簡易変換"""
        lines = text.split("\n")
        html_lines = []
        in_code = False
        for line in lines:
            if line.startswith("```"):
                if in_code:
                    html_lines.append("</pre>")
                    in_code = False
                else:
                    html_lines.append("<pre><code>")
                    in_code = True
            elif in_code:
                html_lines.append(line)
            elif re.match(r"^######", line):
                html_lines.append(f"<h6>{line[7:]}</h6>")
            elif re.match(r"^#####", line):
                html_lines.append(f"<h5>{line[6:]}</h5>")
            elif re.match(r"^####", line):
                html_lines.append(f"<h4>{line[5:]}</h4>")
            elif re.match(r"^###", line):
                html_lines.append(f"<h3>{line[4:]}</h3>")
            elif re.match(r"^##", line):
                html_lines.append(f"<h2>{line[3:]}</h2>")
            elif re.match(r"^#", line):
                html_lines.append(f"<h1>{line[2:]}</h1>")
            elif re.match(r"^[-*+] ", line):
                html_lines.append(f"<li>{line[2:]}</li>")
            elif re.match(r"^> ", line):
                html_lines.append(f"<blockquote>{line[2:]}</blockquote>")
            elif re.match(r"^[-*_]{3,}$", line):
                html_lines.append("<hr>")
            else:
                l = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", line)
                l = re.sub(r"\*(.+?)\*", r"<em>\1</em>", l)
                l = re.sub(r"`(.+?)`", r"<code>\1</code>", l)
                l = re.sub(r"\[(.+?)\]\((.+?)\)", r'<a href="\2">\1</a>', l)
                html_lines.append(f"<p>{l}</p>" if l else "")
        return "\n".join(html_lines)

    def _render_preview_text(self, content):
        """tkinterweb未使用時のフォールバックプレビュー"""
        for line in content.split("\n"):
            if re.match(r"^# ", line):
                self.preview_text.insert(tk.END, line[2:] + "\n", "h1")
            elif re.match(r"^## ", line):
                self.preview_text.insert(tk.END, line[3:] + "\n", "h2")
            elif re.match(r"^### ", line):
                self.preview_text.insert(tk.END, line[4:] + "\n", "h3")
            elif re.match(r"^[-*+] ", line):
                self.preview_text.insert(tk.END, "• " + line[2:] + "\n", "bullet")
            elif re.match(r"^> ", line):
                self.preview_text.insert(tk.END, "  " + line[2:] + "\n", "quote_text")
            else:
                self.preview_text.insert(tk.END, line + "\n")

    def _change_view_mode(self):
        mode = self.view_mode.get()
        # ペインを作り直す
        for item in self.main_paned.panes():
            self.main_paned.forget(item)
        if mode == "edit":
            self.main_paned.add(self.edit_frame, weight=1)
        elif mode == "preview":
            self.main_paned.add(self.preview_frame, weight=1)
        else:
            self.main_paned.add(self.edit_frame, weight=1)
            self.main_paned.add(self.preview_frame, weight=1)

    def _load_sample(self, name):
        content = self.SAMPLES.get(name, "")
        self.editor.delete("1.0", tk.END)
        self.editor.insert("1.0", content)
        self._on_key()

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Markdown", "*.md *.markdown *.txt"),
                       ("すべて", "*.*")])
        if path:
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self.editor.delete("1.0", tk.END)
                self.editor.insert("1.0", content)
                self._current_file = path
                self._modified = False
                self._on_key()
                self.root.title(f"Markdownエディタ — {os.path.basename(path)}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _save_file(self):
        if self._current_file:
            self._write_file(self._current_file)
        else:
            self._save_as_file()

    def _save_as_file(self):
        path = filedialog.asksaveasfilename(
            defaultextension=".md",
            filetypes=[("Markdown", "*.md"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if path:
            self._write_file(path)
            self._current_file = path
            self.root.title(f"Markdownエディタ — {os.path.basename(path)}")

    def _write_file(self, path):
        try:
            content = self.editor.get("1.0", tk.END)
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
            self._modified = False
            self.status_var.set(f"保存しました: {path}")
        except Exception as e:
            messagebox.showerror("エラー", str(e))


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

例外処理とmessagebox

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

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

try:
    import markdown
    MD_AVAILABLE = True
except ImportError:
    MD_AVAILABLE = False

try:
    import tkinterweb
    TKWEB_AVAILABLE = True
except ImportError:
    TKWEB_AVAILABLE = False


class App36:
    """Markdownエディタ(プレビュー付き)"""

    SAMPLES = {
        "基本サンプル": """\
# Markdownエディタ

## 見出し

### 小見出し

通常のテキスト。**太字** や *斜体* が使えます。

## リスト

- 項目1
- 項目2
  - ネスト項目
- 項目3

1. 番号付きリスト
2. 2番目
3. 3番目

## コード

インラインコード: `print("Hello")`

```python
def hello(name):
    return f"Hello, {name}!"

print(hello("World"))
```

## リンクと画像

[Python公式](https://www.python.org)

## テーブル

| 名前 | 年齢 | 職業 |
|------|------|------|
| 田中 | 25   | エンジニア |
| 鈴木 | 30   | デザイナー |

## 引用

> これは引用です。
> 複数行も可能。

---

水平線の上下に区切り線が表示されます。
""",
        "README テンプレート": """\
# プロジェクト名

## 概要

プロジェクトの概要を記述してください。

## インストール

```bash
pip install プロジェクト名
```

## 使い方

```python
import プロジェクト名

# 使用例
```

## 機能

- 機能1
- 機能2
- 機能3

## ライセンス

MIT License
""",
    }

    CSS = """\
<style>
body { font-family: 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto;
       padding: 20px; line-height: 1.6; color: #333; }
h1,h2,h3,h4 { color: #0d47a1; }
h1 { border-bottom: 2px solid #0d47a1; padding-bottom: 8px; }
h2 { border-bottom: 1px solid #ddd; padding-bottom: 4px; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 4px;
       font-family: Consolas, monospace; font-size: 0.9em; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 16px;
      border-radius: 8px; overflow-x: auto; }
pre code { background: none; color: inherit; }
blockquote { border-left: 4px solid #0d47a1; margin: 0;
             padding-left: 16px; color: #555; }
table { border-collapse: collapse; width: 100%; }
th { background: #0d47a1; color: white; padding: 8px 12px; }
td { border: 1px solid #ddd; padding: 8px 12px; }
tr:nth-child(even) { background: #f9f9f9; }
a { color: #1565c0; }
hr { border: none; border-top: 2px solid #ddd; }
</style>
"""

    def __init__(self, root):
        self.root = root
        self.root.title("Markdownエディタ(プレビュー付き)")
        self.root.geometry("1100x700")
        self.root.configure(bg="#1e1e1e")
        self._current_file = None
        self._modified = False
        self._build_ui()
        self._load_sample("基本サンプル")

    def _build_ui(self):
        # ツールバー
        toolbar = tk.Frame(self.root, bg="#252526", pady=4)
        toolbar.pack(fill=tk.X)
        tk.Label(toolbar, text="📝 Markdownエディタ",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        # サンプル
        tk.Label(toolbar, text="サンプル:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(16, 4))
        self.sample_var = tk.StringVar()
        sample_cb = ttk.Combobox(toolbar, textvariable=self.sample_var,
                                  values=list(self.SAMPLES.keys()),
                                  state="readonly", width=18)
        sample_cb.pack(side=tk.LEFT)
        sample_cb.bind("<<ComboboxSelected>>",
                       lambda e: self._load_sample(self.sample_var.get()))

        # ボタン
        for text, cmd in [("📂 開く", self._open_file),
                           ("💾 保存", self._save_file),
                           ("💾 別名保存", self._save_as_file)]:
            tk.Button(toolbar, text=text, command=cmd,
                      bg="#3c3c3c", fg="white", relief=tk.FLAT,
                      font=("Arial", 9), padx=8, pady=3,
                      activebackground="#505050", bd=0).pack(side=tk.LEFT, padx=4)

        # ビューモード
        tk.Label(toolbar, text="表示:", bg="#252526", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.RIGHT, padx=4)
        self.view_mode = tk.StringVar(value="split")
        for val, lbl in [("edit", "編集"), ("split", "分割"), ("preview", "プレビュー")]:
            ttk.Radiobutton(toolbar, text=lbl, variable=self.view_mode,
                            value=val,
                            command=self._change_view_mode
                            ).pack(side=tk.RIGHT, padx=2)

        if not MD_AVAILABLE:
            tk.Label(self.root,
                     text="⚠ markdown が未インストール (pip install markdown)。"
                          "簡易テキストプレビューになります。",
                     bg="#fff3cd", fg="#856404", font=("Arial", 9),
                     anchor="w", padx=8).pack(fill=tk.X)

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

        # 左: エディタ
        self.edit_frame = tk.Frame(self.main_paned, bg="#1e1e1e")
        self.main_paned.add(self.edit_frame, weight=1)

        # 行番号
        num_frame = tk.Frame(self.edit_frame, bg="#1e1e1e")
        num_frame.pack(fill=tk.BOTH, expand=True)
        self.line_nums = tk.Text(num_frame, width=4, bg="#252526",
                                  fg="#858585", font=("Courier New", 12),
                                  state=tk.DISABLED, relief=tk.FLAT,
                                  padx=4, pady=4, takefocus=0)
        self.line_nums.pack(side=tk.LEFT, fill=tk.Y)
        self.editor = tk.Text(num_frame, bg="#1e1e1e", fg="#d4d4d4",
                               font=("Courier New", 12), insertbackground="#aeafad",
                               relief=tk.FLAT, undo=True, padx=8, pady=4,
                               selectbackground="#264f78",
                               wrap=tk.WORD)
        ed_sb = ttk.Scrollbar(num_frame, command=self._scroll_editor)
        self.editor.configure(yscrollcommand=ed_sb.set)
        ed_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.editor.pack(fill=tk.BOTH, expand=True)
        self.editor.bind("<KeyRelease>", self._on_key)
        self._setup_editor_tags()

        # 右: プレビュー
        self.preview_frame = tk.Frame(self.main_paned, bg="#ffffff")
        self.main_paned.add(self.preview_frame, weight=1)

        if TKWEB_AVAILABLE:
            self.html_viewer = tkinterweb.HtmlFrame(
                self.preview_frame, messages_enabled=False)
            self.html_viewer.pack(fill=tk.BOTH, expand=True)
        else:
            # フォールバック: Text widget
            self.preview_text = tk.Text(
                self.preview_frame, bg="#ffffff", fg="#333",
                font=("Arial", 12), relief=tk.FLAT, wrap=tk.WORD,
                state=tk.DISABLED, padx=12, pady=12)
            pr_sb = ttk.Scrollbar(self.preview_frame,
                                   command=self.preview_text.yview)
            self.preview_text.configure(yscrollcommand=pr_sb.set)
            pr_sb.pack(side=tk.RIGHT, fill=tk.Y)
            self.preview_text.pack(fill=tk.BOTH, expand=True)
            self._setup_preview_tags()

        # ステータス
        self.status_var = tk.StringVar(value="準備完了")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#252526", fg="#858585", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _setup_editor_tags(self):
        # Markdown構文色付け
        self.editor.tag_configure("heading", foreground="#4fc3f7",
                                   font=("Courier New", 13, "bold"))
        self.editor.tag_configure("bold", foreground="#ffffff",
                                   font=("Courier New", 12, "bold"))
        self.editor.tag_configure("italic", foreground="#ce9178",
                                   font=("Courier New", 12, "italic"))
        self.editor.tag_configure("code", foreground="#4ec9b0",
                                   background="#2d2d30")
        self.editor.tag_configure("link", foreground="#569cd6")
        self.editor.tag_configure("list_item", foreground="#dcdcaa")
        self.editor.tag_configure("quote", foreground="#6a9955")
        self.editor.tag_configure("hr", foreground="#555555")

    def _setup_preview_tags(self):
        self.preview_text.tag_configure("h1", font=("Arial", 20, "bold"),
                                         foreground="#0d47a1",
                                         spacing3=8)
        self.preview_text.tag_configure("h2", font=("Arial", 16, "bold"),
                                         foreground="#1565c0",
                                         spacing3=6)
        self.preview_text.tag_configure("h3", font=("Arial", 14, "bold"),
                                         foreground="#1976d2")
        self.preview_text.tag_configure("code_inline",
                                         font=("Courier New", 11),
                                         background="#f4f4f4")
        self.preview_text.tag_configure("code_block",
                                         font=("Courier New", 11),
                                         background="#1e1e1e",
                                         foreground="#d4d4d4",
                                         spacing1=4, spacing3=4)
        self.preview_text.tag_configure("bold_text",
                                         font=("Arial", 12, "bold"))
        self.preview_text.tag_configure("italic_text",
                                         font=("Arial", 12, "italic"))
        self.preview_text.tag_configure("quote_text",
                                         foreground="#555555",
                                         lmargin1=20, lmargin2=20)
        self.preview_text.tag_configure("bullet",
                                         foreground="#0d47a1")

    def _scroll_editor(self, *args):
        self.editor.yview(*args)
        self.line_nums.yview(*args)

    def _on_key(self, event=None):
        self._update_line_nums()
        self._highlight_md()
        self._update_preview()
        self._modified = True
        lines = int(self.editor.index(tk.END).split(".")[0]) - 1
        chars = len(self.editor.get("1.0", tk.END)) - 1
        self.status_var.set(f"行: {lines}  文字: {chars}")

    def _update_line_nums(self):
        content = self.editor.get("1.0", tk.END)
        lines = content.count("\n")
        self.line_nums.config(state=tk.NORMAL)
        self.line_nums.delete("1.0", tk.END)
        self.line_nums.insert("1.0", "\n".join(str(i) for i in range(1, lines + 1)))
        self.line_nums.config(state=tk.DISABLED)

    def _highlight_md(self):
        content = self.editor.get("1.0", tk.END)
        for tag in ["heading", "bold", "italic", "code", "link",
                    "list_item", "quote", "hr"]:
            self.editor.tag_remove(tag, "1.0", tk.END)
        patterns = [
            ("heading",   r"^#{1,6} .+$"),
            ("code",      r"`[^`\n]+`"),
            ("bold",      r"\*\*[^*\n]+\*\*"),
            ("italic",    r"\*[^*\n]+\*"),
            ("link",      r"\[.*?\]\(.*?\)"),
            ("list_item", r"^[\s]*[-*+] .+$"),
            ("list_item", r"^\d+\. .+$"),
            ("quote",     r"^> .+$"),
            ("hr",        r"^[-*_]{3,}$"),
        ]
        for tag, pattern in patterns:
            for m in re.finditer(pattern, content, re.MULTILINE):
                start = f"1.0+{m.start()}c"
                end = f"1.0+{m.end()}c"
                self.editor.tag_add(tag, start, end)

    def _update_preview(self):
        content = self.editor.get("1.0", tk.END)
        if TKWEB_AVAILABLE:
            if MD_AVAILABLE:
                html = markdown.markdown(
                    content,
                    extensions=["tables", "fenced_code", "codehilite", "nl2br"])
            else:
                html = self._simple_md(content)
            full_html = f"<html><head>{self.CSS}</head><body>{html}</body></html>"
            self.html_viewer.load_html(full_html)
        else:
            # フォールバック
            self.preview_text.config(state=tk.NORMAL)
            self.preview_text.delete("1.0", tk.END)
            self._render_preview_text(content)
            self.preview_text.config(state=tk.DISABLED)

    def _simple_md(self, text):
        """markdownライブラリなしの簡易変換"""
        lines = text.split("\n")
        html_lines = []
        in_code = False
        for line in lines:
            if line.startswith("```"):
                if in_code:
                    html_lines.append("</pre>")
                    in_code = False
                else:
                    html_lines.append("<pre><code>")
                    in_code = True
            elif in_code:
                html_lines.append(line)
            elif re.match(r"^######", line):
                html_lines.append(f"<h6>{line[7:]}</h6>")
            elif re.match(r"^#####", line):
                html_lines.append(f"<h5>{line[6:]}</h5>")
            elif re.match(r"^####", line):
                html_lines.append(f"<h4>{line[5:]}</h4>")
            elif re.match(r"^###", line):
                html_lines.append(f"<h3>{line[4:]}</h3>")
            elif re.match(r"^##", line):
                html_lines.append(f"<h2>{line[3:]}</h2>")
            elif re.match(r"^#", line):
                html_lines.append(f"<h1>{line[2:]}</h1>")
            elif re.match(r"^[-*+] ", line):
                html_lines.append(f"<li>{line[2:]}</li>")
            elif re.match(r"^> ", line):
                html_lines.append(f"<blockquote>{line[2:]}</blockquote>")
            elif re.match(r"^[-*_]{3,}$", line):
                html_lines.append("<hr>")
            else:
                l = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", line)
                l = re.sub(r"\*(.+?)\*", r"<em>\1</em>", l)
                l = re.sub(r"`(.+?)`", r"<code>\1</code>", l)
                l = re.sub(r"\[(.+?)\]\((.+?)\)", r'<a href="\2">\1</a>', l)
                html_lines.append(f"<p>{l}</p>" if l else "")
        return "\n".join(html_lines)

    def _render_preview_text(self, content):
        """tkinterweb未使用時のフォールバックプレビュー"""
        for line in content.split("\n"):
            if re.match(r"^# ", line):
                self.preview_text.insert(tk.END, line[2:] + "\n", "h1")
            elif re.match(r"^## ", line):
                self.preview_text.insert(tk.END, line[3:] + "\n", "h2")
            elif re.match(r"^### ", line):
                self.preview_text.insert(tk.END, line[4:] + "\n", "h3")
            elif re.match(r"^[-*+] ", line):
                self.preview_text.insert(tk.END, "• " + line[2:] + "\n", "bullet")
            elif re.match(r"^> ", line):
                self.preview_text.insert(tk.END, "  " + line[2:] + "\n", "quote_text")
            else:
                self.preview_text.insert(tk.END, line + "\n")

    def _change_view_mode(self):
        mode = self.view_mode.get()
        # ペインを作り直す
        for item in self.main_paned.panes():
            self.main_paned.forget(item)
        if mode == "edit":
            self.main_paned.add(self.edit_frame, weight=1)
        elif mode == "preview":
            self.main_paned.add(self.preview_frame, weight=1)
        else:
            self.main_paned.add(self.edit_frame, weight=1)
            self.main_paned.add(self.preview_frame, weight=1)

    def _load_sample(self, name):
        content = self.SAMPLES.get(name, "")
        self.editor.delete("1.0", tk.END)
        self.editor.insert("1.0", content)
        self._on_key()

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Markdown", "*.md *.markdown *.txt"),
                       ("すべて", "*.*")])
        if path:
            try:
                with open(path, encoding="utf-8") as f:
                    content = f.read()
                self.editor.delete("1.0", tk.END)
                self.editor.insert("1.0", content)
                self._current_file = path
                self._modified = False
                self._on_key()
                self.root.title(f"Markdownエディタ — {os.path.basename(path)}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _save_file(self):
        if self._current_file:
            self._write_file(self._current_file)
        else:
            self._save_as_file()

    def _save_as_file(self):
        path = filedialog.asksaveasfilename(
            defaultextension=".md",
            filetypes=[("Markdown", "*.md"), ("テキスト", "*.txt"),
                       ("すべて", "*.*")])
        if path:
            self._write_file(path)
            self._current_file = path
            self.root.title(f"Markdownエディタ — {os.path.basename(path)}")

    def _write_file(self, path):
        try:
            content = self.editor.get("1.0", tk.END)
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
            self._modified = False
            self.status_var.set(f"保存しました: {path}")
        except Exception as e:
            messagebox.showerror("エラー", str(e))


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

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

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

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

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

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

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

    Markdownエディタ(プレビュー付き)に新しい機能を1つ追加してみましょう。どんな機能があると便利か考えてから実装してください。

  2. 課題2:UIの改善

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

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

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

🚀
次に挑戦するアプリ

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