中級者向け No.16

音楽プレイヤー

プレイリスト管理・再生・一時停止・音量調整ができる音楽プレイヤー。pygameのmixerモジュールを活用します。

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

1. アプリ概要

プレイリスト管理・再生・一時停止・音量調整ができる音楽プレイヤー。pygameのmixerモジュールを活用します。

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

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

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

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

2. 機能一覧

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

3. 事前準備・環境

ℹ️
動作確認環境

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

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

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

4. 完全なソースコード

💡
コードのコピー方法

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

app16.py
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import threading

try:
    import pygame
    pygame.mixer.init()
    PYGAME_AVAILABLE = True
except ImportError:
    PYGAME_AVAILABLE = False


class App16:
    """音楽プレイヤー"""

    SUPPORTED_EXT = (".mp3", ".wav", ".ogg", ".flac")

    def __init__(self, root):
        self.root = root
        self.root.title("音楽プレイヤー")
        self.root.geometry("560x540")
        self.root.configure(bg="#1a1a2e")
        self.playlist = []
        self.current_idx = -1
        self.playing = False
        self.paused = False
        self._build_ui()

        if not PYGAME_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "pygame が必要です。\n"
                "pip install pygame でインストールしてください。")
        else:
            self._start_monitor()

        self.root.protocol("WM_DELETE_WINDOW", self._on_close)

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#16213e", pady=10)
        header.pack(fill=tk.X)
        tk.Label(header, text="🎵 音楽プレイヤー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#16213e", fg="#e2b96f").pack(side=tk.LEFT, padx=12)

        # 現在の曲
        info_frame = tk.Frame(self.root, bg="#0f3460", pady=12)
        info_frame.pack(fill=tk.X, padx=8, pady=6)
        self.title_label = tk.Label(info_frame, text="♪ 曲が選択されていません",
                                    font=("Noto Sans JP", 13, "bold"),
                                    bg="#0f3460", fg="#e2b96f",
                                    wraplength=500)
        self.title_label.pack()
        self.artist_label = tk.Label(info_frame, text="",
                                     font=("Arial", 10),
                                     bg="#0f3460", fg="#8b949e")
        self.artist_label.pack()

        # シークバー
        seek_frame = tk.Frame(self.root, bg="#1a1a2e")
        seek_frame.pack(fill=tk.X, padx=16, pady=4)
        self.pos_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.pos_label.pack(side=tk.LEFT)
        self.seek_var = tk.DoubleVar(value=0)
        self.seek_bar = ttk.Scale(seek_frame, variable=self.seek_var,
                                   from_=0, to=100, orient=tk.HORIZONTAL)
        self.seek_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=6)
        self.dur_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.dur_label.pack(side=tk.LEFT)

        # コントロール
        ctrl = tk.Frame(self.root, bg="#1a1a2e")
        ctrl.pack(pady=8)
        btn_style = {"bg": "#0f3460", "fg": "#e2b96f", "relief": tk.FLAT,
                     "font": ("Segoe UI Emoji", 18), "padx": 12, "pady": 4,
                     "activebackground": "#16213e", "activeforeground": "#fff",
                     "bd": 0}
        tk.Button(ctrl, text="⏮", command=self._prev, **btn_style).pack(side=tk.LEFT, padx=4)
        self.play_btn = tk.Button(ctrl, text="▶", command=self._play_pause, **btn_style)
        self.play_btn.pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏹", command=self._stop, **btn_style).pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏭", command=self._next, **btn_style).pack(side=tk.LEFT, padx=4)

        # 音量
        vol_frame = tk.Frame(self.root, bg="#1a1a2e")
        vol_frame.pack(fill=tk.X, padx=16, pady=4)
        tk.Label(vol_frame, text="🔊", bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT)
        self.vol_var = tk.IntVar(value=70)
        vol_scale = ttk.Scale(vol_frame, variable=self.vol_var,
                               from_=0, to=100, orient=tk.HORIZONTAL,
                               length=150,
                               command=lambda v: self._set_volume())
        vol_scale.pack(side=tk.LEFT, padx=6)
        tk.Label(vol_frame, text="シャッフル:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.shuffle_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(vol_frame, variable=self.shuffle_var).pack(side=tk.LEFT)
        tk.Label(vol_frame, text="リピート:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(12, 4))
        self.repeat_var = tk.StringVar(value="off")
        for val, lbl in [("off", "オフ"), ("one", "1曲"), ("all", "全曲")]:
            ttk.Radiobutton(vol_frame, text=lbl, variable=self.repeat_var,
                            value=val).pack(side=tk.LEFT, padx=2)

        # プレイリスト
        pl_frame = ttk.LabelFrame(self.root, text="プレイリスト", padding=4)
        pl_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        toolbar = tk.Frame(pl_frame, bg=pl_frame.cget("background"))
        toolbar.pack(fill=tk.X)
        for text, cmd in [("➕ ファイル追加", self._add_files),
                           ("📁 フォルダ追加", self._add_folder),
                           ("🗑️ 削除", self._remove_selected),
                           ("📋 クリア", self._clear_playlist)]:
            ttk.Button(toolbar, text=text, command=cmd).pack(side=tk.LEFT, padx=2, pady=2)
        self.pl_listbox = tk.Listbox(pl_frame, bg="#161b22", fg="#c9d1d9",
                                      selectbackground="#0f3460",
                                      font=("Arial", 10), relief=tk.FLAT)
        sb = ttk.Scrollbar(pl_frame, command=self.pl_listbox.yview)
        self.pl_listbox.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.pl_listbox.pack(fill=tk.BOTH, expand=True)
        self.pl_listbox.bind("<Double-1>", self._on_double_click)

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#16213e", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _add_files(self):
        paths = filedialog.askopenfilenames(
            filetypes=[("音楽ファイル", "*.mp3 *.wav *.ogg *.flac"),
                       ("すべて", "*.*")])
        for p in paths:
            self._add_to_playlist(p)

    def _add_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            for f in sorted(os.listdir(folder)):
                if f.lower().endswith(self.SUPPORTED_EXT):
                    self._add_to_playlist(os.path.join(folder, f))

    def _add_to_playlist(self, path):
        if path not in self.playlist:
            self.playlist.append(path)
            self.pl_listbox.insert(tk.END, os.path.basename(path))
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _remove_selected(self):
        sel = self.pl_listbox.curselection()
        for idx in reversed(sel):
            self.playlist.pop(idx)
            self.pl_listbox.delete(idx)
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _clear_playlist(self):
        self._stop()
        self.playlist.clear()
        self.pl_listbox.delete(0, tk.END)
        self.current_idx = -1

    def _on_double_click(self, event):
        sel = self.pl_listbox.curselection()
        if sel:
            self.current_idx = sel[0]
            self._load_and_play()

    def _load_and_play(self):
        if not PYGAME_AVAILABLE or not self.playlist:
            return
        if 0 <= self.current_idx < len(self.playlist):
            path = self.playlist[self.current_idx]
            try:
                pygame.mixer.music.load(path)
                pygame.mixer.music.set_volume(self.vol_var.get() / 100)
                pygame.mixer.music.play()
                self.playing = True
                self.paused = False
                self.play_btn.config(text="⏸")
                name = os.path.basename(path)
                self.title_label.config(text=name)
                self.pl_listbox.selection_clear(0, tk.END)
                self.pl_listbox.selection_set(self.current_idx)
                self.pl_listbox.see(self.current_idx)
                self.status_var.set(f"再生中: {name}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _play_pause(self):
        if not PYGAME_AVAILABLE:
            return
        if not self.playlist:
            return
        if self.current_idx < 0:
            self.current_idx = 0
        if not self.playing:
            self._load_and_play()
        elif self.paused:
            pygame.mixer.music.unpause()
            self.paused = False
            self.play_btn.config(text="⏸")
        else:
            pygame.mixer.music.pause()
            self.paused = True
            self.play_btn.config(text="▶")

    def _stop(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.stop()
        self.playing = False
        self.paused = False
        self.play_btn.config(text="▶")
        self.title_label.config(text="♪ 曲が選択されていません")

    def _prev(self):
        if not self.playlist:
            return
        self.current_idx = (self.current_idx - 1) % len(self.playlist)
        self._load_and_play()

    def _next(self):
        if not self.playlist:
            return
        repeat = self.repeat_var.get()
        if repeat == "one":
            pass
        elif self.shuffle_var.get():
            import random
            self.current_idx = random.randint(0, len(self.playlist) - 1)
        else:
            self.current_idx = (self.current_idx + 1) % len(self.playlist)
        self._load_and_play()

    def _set_volume(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.set_volume(self.vol_var.get() / 100)

    def _start_monitor(self):
        def monitor():
            while True:
                try:
                    if (self.playing and not self.paused
                            and not pygame.mixer.music.get_busy()):
                        self.playing = False
                        repeat = self.repeat_var.get()
                        if repeat == "one":
                            self.root.after(0, self._load_and_play)
                        elif repeat == "all" or self.current_idx < len(self.playlist) - 1:
                            self.root.after(0, self._next)
                except Exception:
                    pass
                import time
                time.sleep(0.5)

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

    def _on_close(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.quit()
        self.root.destroy()


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

5. コード解説

音楽プレイヤーのコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。

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

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

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

try:
    import pygame
    pygame.mixer.init()
    PYGAME_AVAILABLE = True
except ImportError:
    PYGAME_AVAILABLE = False


class App16:
    """音楽プレイヤー"""

    SUPPORTED_EXT = (".mp3", ".wav", ".ogg", ".flac")

    def __init__(self, root):
        self.root = root
        self.root.title("音楽プレイヤー")
        self.root.geometry("560x540")
        self.root.configure(bg="#1a1a2e")
        self.playlist = []
        self.current_idx = -1
        self.playing = False
        self.paused = False
        self._build_ui()

        if not PYGAME_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "pygame が必要です。\n"
                "pip install pygame でインストールしてください。")
        else:
            self._start_monitor()

        self.root.protocol("WM_DELETE_WINDOW", self._on_close)

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#16213e", pady=10)
        header.pack(fill=tk.X)
        tk.Label(header, text="🎵 音楽プレイヤー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#16213e", fg="#e2b96f").pack(side=tk.LEFT, padx=12)

        # 現在の曲
        info_frame = tk.Frame(self.root, bg="#0f3460", pady=12)
        info_frame.pack(fill=tk.X, padx=8, pady=6)
        self.title_label = tk.Label(info_frame, text="♪ 曲が選択されていません",
                                    font=("Noto Sans JP", 13, "bold"),
                                    bg="#0f3460", fg="#e2b96f",
                                    wraplength=500)
        self.title_label.pack()
        self.artist_label = tk.Label(info_frame, text="",
                                     font=("Arial", 10),
                                     bg="#0f3460", fg="#8b949e")
        self.artist_label.pack()

        # シークバー
        seek_frame = tk.Frame(self.root, bg="#1a1a2e")
        seek_frame.pack(fill=tk.X, padx=16, pady=4)
        self.pos_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.pos_label.pack(side=tk.LEFT)
        self.seek_var = tk.DoubleVar(value=0)
        self.seek_bar = ttk.Scale(seek_frame, variable=self.seek_var,
                                   from_=0, to=100, orient=tk.HORIZONTAL)
        self.seek_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=6)
        self.dur_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.dur_label.pack(side=tk.LEFT)

        # コントロール
        ctrl = tk.Frame(self.root, bg="#1a1a2e")
        ctrl.pack(pady=8)
        btn_style = {"bg": "#0f3460", "fg": "#e2b96f", "relief": tk.FLAT,
                     "font": ("Segoe UI Emoji", 18), "padx": 12, "pady": 4,
                     "activebackground": "#16213e", "activeforeground": "#fff",
                     "bd": 0}
        tk.Button(ctrl, text="⏮", command=self._prev, **btn_style).pack(side=tk.LEFT, padx=4)
        self.play_btn = tk.Button(ctrl, text="▶", command=self._play_pause, **btn_style)
        self.play_btn.pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏹", command=self._stop, **btn_style).pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏭", command=self._next, **btn_style).pack(side=tk.LEFT, padx=4)

        # 音量
        vol_frame = tk.Frame(self.root, bg="#1a1a2e")
        vol_frame.pack(fill=tk.X, padx=16, pady=4)
        tk.Label(vol_frame, text="🔊", bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT)
        self.vol_var = tk.IntVar(value=70)
        vol_scale = ttk.Scale(vol_frame, variable=self.vol_var,
                               from_=0, to=100, orient=tk.HORIZONTAL,
                               length=150,
                               command=lambda v: self._set_volume())
        vol_scale.pack(side=tk.LEFT, padx=6)
        tk.Label(vol_frame, text="シャッフル:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.shuffle_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(vol_frame, variable=self.shuffle_var).pack(side=tk.LEFT)
        tk.Label(vol_frame, text="リピート:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(12, 4))
        self.repeat_var = tk.StringVar(value="off")
        for val, lbl in [("off", "オフ"), ("one", "1曲"), ("all", "全曲")]:
            ttk.Radiobutton(vol_frame, text=lbl, variable=self.repeat_var,
                            value=val).pack(side=tk.LEFT, padx=2)

        # プレイリスト
        pl_frame = ttk.LabelFrame(self.root, text="プレイリスト", padding=4)
        pl_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        toolbar = tk.Frame(pl_frame, bg=pl_frame.cget("background"))
        toolbar.pack(fill=tk.X)
        for text, cmd in [("➕ ファイル追加", self._add_files),
                           ("📁 フォルダ追加", self._add_folder),
                           ("🗑️ 削除", self._remove_selected),
                           ("📋 クリア", self._clear_playlist)]:
            ttk.Button(toolbar, text=text, command=cmd).pack(side=tk.LEFT, padx=2, pady=2)
        self.pl_listbox = tk.Listbox(pl_frame, bg="#161b22", fg="#c9d1d9",
                                      selectbackground="#0f3460",
                                      font=("Arial", 10), relief=tk.FLAT)
        sb = ttk.Scrollbar(pl_frame, command=self.pl_listbox.yview)
        self.pl_listbox.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.pl_listbox.pack(fill=tk.BOTH, expand=True)
        self.pl_listbox.bind("<Double-1>", self._on_double_click)

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#16213e", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _add_files(self):
        paths = filedialog.askopenfilenames(
            filetypes=[("音楽ファイル", "*.mp3 *.wav *.ogg *.flac"),
                       ("すべて", "*.*")])
        for p in paths:
            self._add_to_playlist(p)

    def _add_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            for f in sorted(os.listdir(folder)):
                if f.lower().endswith(self.SUPPORTED_EXT):
                    self._add_to_playlist(os.path.join(folder, f))

    def _add_to_playlist(self, path):
        if path not in self.playlist:
            self.playlist.append(path)
            self.pl_listbox.insert(tk.END, os.path.basename(path))
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _remove_selected(self):
        sel = self.pl_listbox.curselection()
        for idx in reversed(sel):
            self.playlist.pop(idx)
            self.pl_listbox.delete(idx)
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _clear_playlist(self):
        self._stop()
        self.playlist.clear()
        self.pl_listbox.delete(0, tk.END)
        self.current_idx = -1

    def _on_double_click(self, event):
        sel = self.pl_listbox.curselection()
        if sel:
            self.current_idx = sel[0]
            self._load_and_play()

    def _load_and_play(self):
        if not PYGAME_AVAILABLE or not self.playlist:
            return
        if 0 <= self.current_idx < len(self.playlist):
            path = self.playlist[self.current_idx]
            try:
                pygame.mixer.music.load(path)
                pygame.mixer.music.set_volume(self.vol_var.get() / 100)
                pygame.mixer.music.play()
                self.playing = True
                self.paused = False
                self.play_btn.config(text="⏸")
                name = os.path.basename(path)
                self.title_label.config(text=name)
                self.pl_listbox.selection_clear(0, tk.END)
                self.pl_listbox.selection_set(self.current_idx)
                self.pl_listbox.see(self.current_idx)
                self.status_var.set(f"再生中: {name}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _play_pause(self):
        if not PYGAME_AVAILABLE:
            return
        if not self.playlist:
            return
        if self.current_idx < 0:
            self.current_idx = 0
        if not self.playing:
            self._load_and_play()
        elif self.paused:
            pygame.mixer.music.unpause()
            self.paused = False
            self.play_btn.config(text="⏸")
        else:
            pygame.mixer.music.pause()
            self.paused = True
            self.play_btn.config(text="▶")

    def _stop(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.stop()
        self.playing = False
        self.paused = False
        self.play_btn.config(text="▶")
        self.title_label.config(text="♪ 曲が選択されていません")

    def _prev(self):
        if not self.playlist:
            return
        self.current_idx = (self.current_idx - 1) % len(self.playlist)
        self._load_and_play()

    def _next(self):
        if not self.playlist:
            return
        repeat = self.repeat_var.get()
        if repeat == "one":
            pass
        elif self.shuffle_var.get():
            import random
            self.current_idx = random.randint(0, len(self.playlist) - 1)
        else:
            self.current_idx = (self.current_idx + 1) % len(self.playlist)
        self._load_and_play()

    def _set_volume(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.set_volume(self.vol_var.get() / 100)

    def _start_monitor(self):
        def monitor():
            while True:
                try:
                    if (self.playing and not self.paused
                            and not pygame.mixer.music.get_busy()):
                        self.playing = False
                        repeat = self.repeat_var.get()
                        if repeat == "one":
                            self.root.after(0, self._load_and_play)
                        elif repeat == "all" or self.current_idx < len(self.playlist) - 1:
                            self.root.after(0, self._next)
                except Exception:
                    pass
                import time
                time.sleep(0.5)

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

    def _on_close(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.quit()
        self.root.destroy()


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

LabelFrameによるセクション分け

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

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

try:
    import pygame
    pygame.mixer.init()
    PYGAME_AVAILABLE = True
except ImportError:
    PYGAME_AVAILABLE = False


class App16:
    """音楽プレイヤー"""

    SUPPORTED_EXT = (".mp3", ".wav", ".ogg", ".flac")

    def __init__(self, root):
        self.root = root
        self.root.title("音楽プレイヤー")
        self.root.geometry("560x540")
        self.root.configure(bg="#1a1a2e")
        self.playlist = []
        self.current_idx = -1
        self.playing = False
        self.paused = False
        self._build_ui()

        if not PYGAME_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "pygame が必要です。\n"
                "pip install pygame でインストールしてください。")
        else:
            self._start_monitor()

        self.root.protocol("WM_DELETE_WINDOW", self._on_close)

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#16213e", pady=10)
        header.pack(fill=tk.X)
        tk.Label(header, text="🎵 音楽プレイヤー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#16213e", fg="#e2b96f").pack(side=tk.LEFT, padx=12)

        # 現在の曲
        info_frame = tk.Frame(self.root, bg="#0f3460", pady=12)
        info_frame.pack(fill=tk.X, padx=8, pady=6)
        self.title_label = tk.Label(info_frame, text="♪ 曲が選択されていません",
                                    font=("Noto Sans JP", 13, "bold"),
                                    bg="#0f3460", fg="#e2b96f",
                                    wraplength=500)
        self.title_label.pack()
        self.artist_label = tk.Label(info_frame, text="",
                                     font=("Arial", 10),
                                     bg="#0f3460", fg="#8b949e")
        self.artist_label.pack()

        # シークバー
        seek_frame = tk.Frame(self.root, bg="#1a1a2e")
        seek_frame.pack(fill=tk.X, padx=16, pady=4)
        self.pos_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.pos_label.pack(side=tk.LEFT)
        self.seek_var = tk.DoubleVar(value=0)
        self.seek_bar = ttk.Scale(seek_frame, variable=self.seek_var,
                                   from_=0, to=100, orient=tk.HORIZONTAL)
        self.seek_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=6)
        self.dur_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.dur_label.pack(side=tk.LEFT)

        # コントロール
        ctrl = tk.Frame(self.root, bg="#1a1a2e")
        ctrl.pack(pady=8)
        btn_style = {"bg": "#0f3460", "fg": "#e2b96f", "relief": tk.FLAT,
                     "font": ("Segoe UI Emoji", 18), "padx": 12, "pady": 4,
                     "activebackground": "#16213e", "activeforeground": "#fff",
                     "bd": 0}
        tk.Button(ctrl, text="⏮", command=self._prev, **btn_style).pack(side=tk.LEFT, padx=4)
        self.play_btn = tk.Button(ctrl, text="▶", command=self._play_pause, **btn_style)
        self.play_btn.pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏹", command=self._stop, **btn_style).pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏭", command=self._next, **btn_style).pack(side=tk.LEFT, padx=4)

        # 音量
        vol_frame = tk.Frame(self.root, bg="#1a1a2e")
        vol_frame.pack(fill=tk.X, padx=16, pady=4)
        tk.Label(vol_frame, text="🔊", bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT)
        self.vol_var = tk.IntVar(value=70)
        vol_scale = ttk.Scale(vol_frame, variable=self.vol_var,
                               from_=0, to=100, orient=tk.HORIZONTAL,
                               length=150,
                               command=lambda v: self._set_volume())
        vol_scale.pack(side=tk.LEFT, padx=6)
        tk.Label(vol_frame, text="シャッフル:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.shuffle_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(vol_frame, variable=self.shuffle_var).pack(side=tk.LEFT)
        tk.Label(vol_frame, text="リピート:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(12, 4))
        self.repeat_var = tk.StringVar(value="off")
        for val, lbl in [("off", "オフ"), ("one", "1曲"), ("all", "全曲")]:
            ttk.Radiobutton(vol_frame, text=lbl, variable=self.repeat_var,
                            value=val).pack(side=tk.LEFT, padx=2)

        # プレイリスト
        pl_frame = ttk.LabelFrame(self.root, text="プレイリスト", padding=4)
        pl_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        toolbar = tk.Frame(pl_frame, bg=pl_frame.cget("background"))
        toolbar.pack(fill=tk.X)
        for text, cmd in [("➕ ファイル追加", self._add_files),
                           ("📁 フォルダ追加", self._add_folder),
                           ("🗑️ 削除", self._remove_selected),
                           ("📋 クリア", self._clear_playlist)]:
            ttk.Button(toolbar, text=text, command=cmd).pack(side=tk.LEFT, padx=2, pady=2)
        self.pl_listbox = tk.Listbox(pl_frame, bg="#161b22", fg="#c9d1d9",
                                      selectbackground="#0f3460",
                                      font=("Arial", 10), relief=tk.FLAT)
        sb = ttk.Scrollbar(pl_frame, command=self.pl_listbox.yview)
        self.pl_listbox.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.pl_listbox.pack(fill=tk.BOTH, expand=True)
        self.pl_listbox.bind("<Double-1>", self._on_double_click)

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#16213e", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _add_files(self):
        paths = filedialog.askopenfilenames(
            filetypes=[("音楽ファイル", "*.mp3 *.wav *.ogg *.flac"),
                       ("すべて", "*.*")])
        for p in paths:
            self._add_to_playlist(p)

    def _add_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            for f in sorted(os.listdir(folder)):
                if f.lower().endswith(self.SUPPORTED_EXT):
                    self._add_to_playlist(os.path.join(folder, f))

    def _add_to_playlist(self, path):
        if path not in self.playlist:
            self.playlist.append(path)
            self.pl_listbox.insert(tk.END, os.path.basename(path))
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _remove_selected(self):
        sel = self.pl_listbox.curselection()
        for idx in reversed(sel):
            self.playlist.pop(idx)
            self.pl_listbox.delete(idx)
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _clear_playlist(self):
        self._stop()
        self.playlist.clear()
        self.pl_listbox.delete(0, tk.END)
        self.current_idx = -1

    def _on_double_click(self, event):
        sel = self.pl_listbox.curselection()
        if sel:
            self.current_idx = sel[0]
            self._load_and_play()

    def _load_and_play(self):
        if not PYGAME_AVAILABLE or not self.playlist:
            return
        if 0 <= self.current_idx < len(self.playlist):
            path = self.playlist[self.current_idx]
            try:
                pygame.mixer.music.load(path)
                pygame.mixer.music.set_volume(self.vol_var.get() / 100)
                pygame.mixer.music.play()
                self.playing = True
                self.paused = False
                self.play_btn.config(text="⏸")
                name = os.path.basename(path)
                self.title_label.config(text=name)
                self.pl_listbox.selection_clear(0, tk.END)
                self.pl_listbox.selection_set(self.current_idx)
                self.pl_listbox.see(self.current_idx)
                self.status_var.set(f"再生中: {name}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _play_pause(self):
        if not PYGAME_AVAILABLE:
            return
        if not self.playlist:
            return
        if self.current_idx < 0:
            self.current_idx = 0
        if not self.playing:
            self._load_and_play()
        elif self.paused:
            pygame.mixer.music.unpause()
            self.paused = False
            self.play_btn.config(text="⏸")
        else:
            pygame.mixer.music.pause()
            self.paused = True
            self.play_btn.config(text="▶")

    def _stop(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.stop()
        self.playing = False
        self.paused = False
        self.play_btn.config(text="▶")
        self.title_label.config(text="♪ 曲が選択されていません")

    def _prev(self):
        if not self.playlist:
            return
        self.current_idx = (self.current_idx - 1) % len(self.playlist)
        self._load_and_play()

    def _next(self):
        if not self.playlist:
            return
        repeat = self.repeat_var.get()
        if repeat == "one":
            pass
        elif self.shuffle_var.get():
            import random
            self.current_idx = random.randint(0, len(self.playlist) - 1)
        else:
            self.current_idx = (self.current_idx + 1) % len(self.playlist)
        self._load_and_play()

    def _set_volume(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.set_volume(self.vol_var.get() / 100)

    def _start_monitor(self):
        def monitor():
            while True:
                try:
                    if (self.playing and not self.paused
                            and not pygame.mixer.music.get_busy()):
                        self.playing = False
                        repeat = self.repeat_var.get()
                        if repeat == "one":
                            self.root.after(0, self._load_and_play)
                        elif repeat == "all" or self.current_idx < len(self.playlist) - 1:
                            self.root.after(0, self._next)
                except Exception:
                    pass
                import time
                time.sleep(0.5)

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

    def _on_close(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.quit()
        self.root.destroy()


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

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

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

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

try:
    import pygame
    pygame.mixer.init()
    PYGAME_AVAILABLE = True
except ImportError:
    PYGAME_AVAILABLE = False


class App16:
    """音楽プレイヤー"""

    SUPPORTED_EXT = (".mp3", ".wav", ".ogg", ".flac")

    def __init__(self, root):
        self.root = root
        self.root.title("音楽プレイヤー")
        self.root.geometry("560x540")
        self.root.configure(bg="#1a1a2e")
        self.playlist = []
        self.current_idx = -1
        self.playing = False
        self.paused = False
        self._build_ui()

        if not PYGAME_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "pygame が必要です。\n"
                "pip install pygame でインストールしてください。")
        else:
            self._start_monitor()

        self.root.protocol("WM_DELETE_WINDOW", self._on_close)

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#16213e", pady=10)
        header.pack(fill=tk.X)
        tk.Label(header, text="🎵 音楽プレイヤー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#16213e", fg="#e2b96f").pack(side=tk.LEFT, padx=12)

        # 現在の曲
        info_frame = tk.Frame(self.root, bg="#0f3460", pady=12)
        info_frame.pack(fill=tk.X, padx=8, pady=6)
        self.title_label = tk.Label(info_frame, text="♪ 曲が選択されていません",
                                    font=("Noto Sans JP", 13, "bold"),
                                    bg="#0f3460", fg="#e2b96f",
                                    wraplength=500)
        self.title_label.pack()
        self.artist_label = tk.Label(info_frame, text="",
                                     font=("Arial", 10),
                                     bg="#0f3460", fg="#8b949e")
        self.artist_label.pack()

        # シークバー
        seek_frame = tk.Frame(self.root, bg="#1a1a2e")
        seek_frame.pack(fill=tk.X, padx=16, pady=4)
        self.pos_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.pos_label.pack(side=tk.LEFT)
        self.seek_var = tk.DoubleVar(value=0)
        self.seek_bar = ttk.Scale(seek_frame, variable=self.seek_var,
                                   from_=0, to=100, orient=tk.HORIZONTAL)
        self.seek_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=6)
        self.dur_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.dur_label.pack(side=tk.LEFT)

        # コントロール
        ctrl = tk.Frame(self.root, bg="#1a1a2e")
        ctrl.pack(pady=8)
        btn_style = {"bg": "#0f3460", "fg": "#e2b96f", "relief": tk.FLAT,
                     "font": ("Segoe UI Emoji", 18), "padx": 12, "pady": 4,
                     "activebackground": "#16213e", "activeforeground": "#fff",
                     "bd": 0}
        tk.Button(ctrl, text="⏮", command=self._prev, **btn_style).pack(side=tk.LEFT, padx=4)
        self.play_btn = tk.Button(ctrl, text="▶", command=self._play_pause, **btn_style)
        self.play_btn.pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏹", command=self._stop, **btn_style).pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏭", command=self._next, **btn_style).pack(side=tk.LEFT, padx=4)

        # 音量
        vol_frame = tk.Frame(self.root, bg="#1a1a2e")
        vol_frame.pack(fill=tk.X, padx=16, pady=4)
        tk.Label(vol_frame, text="🔊", bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT)
        self.vol_var = tk.IntVar(value=70)
        vol_scale = ttk.Scale(vol_frame, variable=self.vol_var,
                               from_=0, to=100, orient=tk.HORIZONTAL,
                               length=150,
                               command=lambda v: self._set_volume())
        vol_scale.pack(side=tk.LEFT, padx=6)
        tk.Label(vol_frame, text="シャッフル:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.shuffle_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(vol_frame, variable=self.shuffle_var).pack(side=tk.LEFT)
        tk.Label(vol_frame, text="リピート:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(12, 4))
        self.repeat_var = tk.StringVar(value="off")
        for val, lbl in [("off", "オフ"), ("one", "1曲"), ("all", "全曲")]:
            ttk.Radiobutton(vol_frame, text=lbl, variable=self.repeat_var,
                            value=val).pack(side=tk.LEFT, padx=2)

        # プレイリスト
        pl_frame = ttk.LabelFrame(self.root, text="プレイリスト", padding=4)
        pl_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        toolbar = tk.Frame(pl_frame, bg=pl_frame.cget("background"))
        toolbar.pack(fill=tk.X)
        for text, cmd in [("➕ ファイル追加", self._add_files),
                           ("📁 フォルダ追加", self._add_folder),
                           ("🗑️ 削除", self._remove_selected),
                           ("📋 クリア", self._clear_playlist)]:
            ttk.Button(toolbar, text=text, command=cmd).pack(side=tk.LEFT, padx=2, pady=2)
        self.pl_listbox = tk.Listbox(pl_frame, bg="#161b22", fg="#c9d1d9",
                                      selectbackground="#0f3460",
                                      font=("Arial", 10), relief=tk.FLAT)
        sb = ttk.Scrollbar(pl_frame, command=self.pl_listbox.yview)
        self.pl_listbox.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.pl_listbox.pack(fill=tk.BOTH, expand=True)
        self.pl_listbox.bind("<Double-1>", self._on_double_click)

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#16213e", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _add_files(self):
        paths = filedialog.askopenfilenames(
            filetypes=[("音楽ファイル", "*.mp3 *.wav *.ogg *.flac"),
                       ("すべて", "*.*")])
        for p in paths:
            self._add_to_playlist(p)

    def _add_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            for f in sorted(os.listdir(folder)):
                if f.lower().endswith(self.SUPPORTED_EXT):
                    self._add_to_playlist(os.path.join(folder, f))

    def _add_to_playlist(self, path):
        if path not in self.playlist:
            self.playlist.append(path)
            self.pl_listbox.insert(tk.END, os.path.basename(path))
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _remove_selected(self):
        sel = self.pl_listbox.curselection()
        for idx in reversed(sel):
            self.playlist.pop(idx)
            self.pl_listbox.delete(idx)
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _clear_playlist(self):
        self._stop()
        self.playlist.clear()
        self.pl_listbox.delete(0, tk.END)
        self.current_idx = -1

    def _on_double_click(self, event):
        sel = self.pl_listbox.curselection()
        if sel:
            self.current_idx = sel[0]
            self._load_and_play()

    def _load_and_play(self):
        if not PYGAME_AVAILABLE or not self.playlist:
            return
        if 0 <= self.current_idx < len(self.playlist):
            path = self.playlist[self.current_idx]
            try:
                pygame.mixer.music.load(path)
                pygame.mixer.music.set_volume(self.vol_var.get() / 100)
                pygame.mixer.music.play()
                self.playing = True
                self.paused = False
                self.play_btn.config(text="⏸")
                name = os.path.basename(path)
                self.title_label.config(text=name)
                self.pl_listbox.selection_clear(0, tk.END)
                self.pl_listbox.selection_set(self.current_idx)
                self.pl_listbox.see(self.current_idx)
                self.status_var.set(f"再生中: {name}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _play_pause(self):
        if not PYGAME_AVAILABLE:
            return
        if not self.playlist:
            return
        if self.current_idx < 0:
            self.current_idx = 0
        if not self.playing:
            self._load_and_play()
        elif self.paused:
            pygame.mixer.music.unpause()
            self.paused = False
            self.play_btn.config(text="⏸")
        else:
            pygame.mixer.music.pause()
            self.paused = True
            self.play_btn.config(text="▶")

    def _stop(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.stop()
        self.playing = False
        self.paused = False
        self.play_btn.config(text="▶")
        self.title_label.config(text="♪ 曲が選択されていません")

    def _prev(self):
        if not self.playlist:
            return
        self.current_idx = (self.current_idx - 1) % len(self.playlist)
        self._load_and_play()

    def _next(self):
        if not self.playlist:
            return
        repeat = self.repeat_var.get()
        if repeat == "one":
            pass
        elif self.shuffle_var.get():
            import random
            self.current_idx = random.randint(0, len(self.playlist) - 1)
        else:
            self.current_idx = (self.current_idx + 1) % len(self.playlist)
        self._load_and_play()

    def _set_volume(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.set_volume(self.vol_var.get() / 100)

    def _start_monitor(self):
        def monitor():
            while True:
                try:
                    if (self.playing and not self.paused
                            and not pygame.mixer.music.get_busy()):
                        self.playing = False
                        repeat = self.repeat_var.get()
                        if repeat == "one":
                            self.root.after(0, self._load_and_play)
                        elif repeat == "all" or self.current_idx < len(self.playlist) - 1:
                            self.root.after(0, self._next)
                except Exception:
                    pass
                import time
                time.sleep(0.5)

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

    def _on_close(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.quit()
        self.root.destroy()


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

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

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

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

try:
    import pygame
    pygame.mixer.init()
    PYGAME_AVAILABLE = True
except ImportError:
    PYGAME_AVAILABLE = False


class App16:
    """音楽プレイヤー"""

    SUPPORTED_EXT = (".mp3", ".wav", ".ogg", ".flac")

    def __init__(self, root):
        self.root = root
        self.root.title("音楽プレイヤー")
        self.root.geometry("560x540")
        self.root.configure(bg="#1a1a2e")
        self.playlist = []
        self.current_idx = -1
        self.playing = False
        self.paused = False
        self._build_ui()

        if not PYGAME_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "pygame が必要です。\n"
                "pip install pygame でインストールしてください。")
        else:
            self._start_monitor()

        self.root.protocol("WM_DELETE_WINDOW", self._on_close)

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#16213e", pady=10)
        header.pack(fill=tk.X)
        tk.Label(header, text="🎵 音楽プレイヤー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#16213e", fg="#e2b96f").pack(side=tk.LEFT, padx=12)

        # 現在の曲
        info_frame = tk.Frame(self.root, bg="#0f3460", pady=12)
        info_frame.pack(fill=tk.X, padx=8, pady=6)
        self.title_label = tk.Label(info_frame, text="♪ 曲が選択されていません",
                                    font=("Noto Sans JP", 13, "bold"),
                                    bg="#0f3460", fg="#e2b96f",
                                    wraplength=500)
        self.title_label.pack()
        self.artist_label = tk.Label(info_frame, text="",
                                     font=("Arial", 10),
                                     bg="#0f3460", fg="#8b949e")
        self.artist_label.pack()

        # シークバー
        seek_frame = tk.Frame(self.root, bg="#1a1a2e")
        seek_frame.pack(fill=tk.X, padx=16, pady=4)
        self.pos_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.pos_label.pack(side=tk.LEFT)
        self.seek_var = tk.DoubleVar(value=0)
        self.seek_bar = ttk.Scale(seek_frame, variable=self.seek_var,
                                   from_=0, to=100, orient=tk.HORIZONTAL)
        self.seek_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=6)
        self.dur_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.dur_label.pack(side=tk.LEFT)

        # コントロール
        ctrl = tk.Frame(self.root, bg="#1a1a2e")
        ctrl.pack(pady=8)
        btn_style = {"bg": "#0f3460", "fg": "#e2b96f", "relief": tk.FLAT,
                     "font": ("Segoe UI Emoji", 18), "padx": 12, "pady": 4,
                     "activebackground": "#16213e", "activeforeground": "#fff",
                     "bd": 0}
        tk.Button(ctrl, text="⏮", command=self._prev, **btn_style).pack(side=tk.LEFT, padx=4)
        self.play_btn = tk.Button(ctrl, text="▶", command=self._play_pause, **btn_style)
        self.play_btn.pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏹", command=self._stop, **btn_style).pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏭", command=self._next, **btn_style).pack(side=tk.LEFT, padx=4)

        # 音量
        vol_frame = tk.Frame(self.root, bg="#1a1a2e")
        vol_frame.pack(fill=tk.X, padx=16, pady=4)
        tk.Label(vol_frame, text="🔊", bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT)
        self.vol_var = tk.IntVar(value=70)
        vol_scale = ttk.Scale(vol_frame, variable=self.vol_var,
                               from_=0, to=100, orient=tk.HORIZONTAL,
                               length=150,
                               command=lambda v: self._set_volume())
        vol_scale.pack(side=tk.LEFT, padx=6)
        tk.Label(vol_frame, text="シャッフル:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.shuffle_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(vol_frame, variable=self.shuffle_var).pack(side=tk.LEFT)
        tk.Label(vol_frame, text="リピート:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(12, 4))
        self.repeat_var = tk.StringVar(value="off")
        for val, lbl in [("off", "オフ"), ("one", "1曲"), ("all", "全曲")]:
            ttk.Radiobutton(vol_frame, text=lbl, variable=self.repeat_var,
                            value=val).pack(side=tk.LEFT, padx=2)

        # プレイリスト
        pl_frame = ttk.LabelFrame(self.root, text="プレイリスト", padding=4)
        pl_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        toolbar = tk.Frame(pl_frame, bg=pl_frame.cget("background"))
        toolbar.pack(fill=tk.X)
        for text, cmd in [("➕ ファイル追加", self._add_files),
                           ("📁 フォルダ追加", self._add_folder),
                           ("🗑️ 削除", self._remove_selected),
                           ("📋 クリア", self._clear_playlist)]:
            ttk.Button(toolbar, text=text, command=cmd).pack(side=tk.LEFT, padx=2, pady=2)
        self.pl_listbox = tk.Listbox(pl_frame, bg="#161b22", fg="#c9d1d9",
                                      selectbackground="#0f3460",
                                      font=("Arial", 10), relief=tk.FLAT)
        sb = ttk.Scrollbar(pl_frame, command=self.pl_listbox.yview)
        self.pl_listbox.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.pl_listbox.pack(fill=tk.BOTH, expand=True)
        self.pl_listbox.bind("<Double-1>", self._on_double_click)

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#16213e", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _add_files(self):
        paths = filedialog.askopenfilenames(
            filetypes=[("音楽ファイル", "*.mp3 *.wav *.ogg *.flac"),
                       ("すべて", "*.*")])
        for p in paths:
            self._add_to_playlist(p)

    def _add_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            for f in sorted(os.listdir(folder)):
                if f.lower().endswith(self.SUPPORTED_EXT):
                    self._add_to_playlist(os.path.join(folder, f))

    def _add_to_playlist(self, path):
        if path not in self.playlist:
            self.playlist.append(path)
            self.pl_listbox.insert(tk.END, os.path.basename(path))
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _remove_selected(self):
        sel = self.pl_listbox.curselection()
        for idx in reversed(sel):
            self.playlist.pop(idx)
            self.pl_listbox.delete(idx)
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _clear_playlist(self):
        self._stop()
        self.playlist.clear()
        self.pl_listbox.delete(0, tk.END)
        self.current_idx = -1

    def _on_double_click(self, event):
        sel = self.pl_listbox.curselection()
        if sel:
            self.current_idx = sel[0]
            self._load_and_play()

    def _load_and_play(self):
        if not PYGAME_AVAILABLE or not self.playlist:
            return
        if 0 <= self.current_idx < len(self.playlist):
            path = self.playlist[self.current_idx]
            try:
                pygame.mixer.music.load(path)
                pygame.mixer.music.set_volume(self.vol_var.get() / 100)
                pygame.mixer.music.play()
                self.playing = True
                self.paused = False
                self.play_btn.config(text="⏸")
                name = os.path.basename(path)
                self.title_label.config(text=name)
                self.pl_listbox.selection_clear(0, tk.END)
                self.pl_listbox.selection_set(self.current_idx)
                self.pl_listbox.see(self.current_idx)
                self.status_var.set(f"再生中: {name}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _play_pause(self):
        if not PYGAME_AVAILABLE:
            return
        if not self.playlist:
            return
        if self.current_idx < 0:
            self.current_idx = 0
        if not self.playing:
            self._load_and_play()
        elif self.paused:
            pygame.mixer.music.unpause()
            self.paused = False
            self.play_btn.config(text="⏸")
        else:
            pygame.mixer.music.pause()
            self.paused = True
            self.play_btn.config(text="▶")

    def _stop(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.stop()
        self.playing = False
        self.paused = False
        self.play_btn.config(text="▶")
        self.title_label.config(text="♪ 曲が選択されていません")

    def _prev(self):
        if not self.playlist:
            return
        self.current_idx = (self.current_idx - 1) % len(self.playlist)
        self._load_and_play()

    def _next(self):
        if not self.playlist:
            return
        repeat = self.repeat_var.get()
        if repeat == "one":
            pass
        elif self.shuffle_var.get():
            import random
            self.current_idx = random.randint(0, len(self.playlist) - 1)
        else:
            self.current_idx = (self.current_idx + 1) % len(self.playlist)
        self._load_and_play()

    def _set_volume(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.set_volume(self.vol_var.get() / 100)

    def _start_monitor(self):
        def monitor():
            while True:
                try:
                    if (self.playing and not self.paused
                            and not pygame.mixer.music.get_busy()):
                        self.playing = False
                        repeat = self.repeat_var.get()
                        if repeat == "one":
                            self.root.after(0, self._load_and_play)
                        elif repeat == "all" or self.current_idx < len(self.playlist) - 1:
                            self.root.after(0, self._next)
                except Exception:
                    pass
                import time
                time.sleep(0.5)

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

    def _on_close(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.quit()
        self.root.destroy()


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

例外処理とmessagebox

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

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

try:
    import pygame
    pygame.mixer.init()
    PYGAME_AVAILABLE = True
except ImportError:
    PYGAME_AVAILABLE = False


class App16:
    """音楽プレイヤー"""

    SUPPORTED_EXT = (".mp3", ".wav", ".ogg", ".flac")

    def __init__(self, root):
        self.root = root
        self.root.title("音楽プレイヤー")
        self.root.geometry("560x540")
        self.root.configure(bg="#1a1a2e")
        self.playlist = []
        self.current_idx = -1
        self.playing = False
        self.paused = False
        self._build_ui()

        if not PYGAME_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "pygame が必要です。\n"
                "pip install pygame でインストールしてください。")
        else:
            self._start_monitor()

        self.root.protocol("WM_DELETE_WINDOW", self._on_close)

    def _build_ui(self):
        # ヘッダー
        header = tk.Frame(self.root, bg="#16213e", pady=10)
        header.pack(fill=tk.X)
        tk.Label(header, text="🎵 音楽プレイヤー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#16213e", fg="#e2b96f").pack(side=tk.LEFT, padx=12)

        # 現在の曲
        info_frame = tk.Frame(self.root, bg="#0f3460", pady=12)
        info_frame.pack(fill=tk.X, padx=8, pady=6)
        self.title_label = tk.Label(info_frame, text="♪ 曲が選択されていません",
                                    font=("Noto Sans JP", 13, "bold"),
                                    bg="#0f3460", fg="#e2b96f",
                                    wraplength=500)
        self.title_label.pack()
        self.artist_label = tk.Label(info_frame, text="",
                                     font=("Arial", 10),
                                     bg="#0f3460", fg="#8b949e")
        self.artist_label.pack()

        # シークバー
        seek_frame = tk.Frame(self.root, bg="#1a1a2e")
        seek_frame.pack(fill=tk.X, padx=16, pady=4)
        self.pos_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.pos_label.pack(side=tk.LEFT)
        self.seek_var = tk.DoubleVar(value=0)
        self.seek_bar = ttk.Scale(seek_frame, variable=self.seek_var,
                                   from_=0, to=100, orient=tk.HORIZONTAL)
        self.seek_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=6)
        self.dur_label = tk.Label(seek_frame, text="0:00",
                                  bg="#1a1a2e", fg="#8b949e",
                                  font=("Arial", 9), width=5)
        self.dur_label.pack(side=tk.LEFT)

        # コントロール
        ctrl = tk.Frame(self.root, bg="#1a1a2e")
        ctrl.pack(pady=8)
        btn_style = {"bg": "#0f3460", "fg": "#e2b96f", "relief": tk.FLAT,
                     "font": ("Segoe UI Emoji", 18), "padx": 12, "pady": 4,
                     "activebackground": "#16213e", "activeforeground": "#fff",
                     "bd": 0}
        tk.Button(ctrl, text="⏮", command=self._prev, **btn_style).pack(side=tk.LEFT, padx=4)
        self.play_btn = tk.Button(ctrl, text="▶", command=self._play_pause, **btn_style)
        self.play_btn.pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏹", command=self._stop, **btn_style).pack(side=tk.LEFT, padx=4)
        tk.Button(ctrl, text="⏭", command=self._next, **btn_style).pack(side=tk.LEFT, padx=4)

        # 音量
        vol_frame = tk.Frame(self.root, bg="#1a1a2e")
        vol_frame.pack(fill=tk.X, padx=16, pady=4)
        tk.Label(vol_frame, text="🔊", bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT)
        self.vol_var = tk.IntVar(value=70)
        vol_scale = ttk.Scale(vol_frame, variable=self.vol_var,
                               from_=0, to=100, orient=tk.HORIZONTAL,
                               length=150,
                               command=lambda v: self._set_volume())
        vol_scale.pack(side=tk.LEFT, padx=6)
        tk.Label(vol_frame, text="シャッフル:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.shuffle_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(vol_frame, variable=self.shuffle_var).pack(side=tk.LEFT)
        tk.Label(vol_frame, text="リピート:",
                 bg="#1a1a2e", fg="#8b949e").pack(side=tk.LEFT, padx=(12, 4))
        self.repeat_var = tk.StringVar(value="off")
        for val, lbl in [("off", "オフ"), ("one", "1曲"), ("all", "全曲")]:
            ttk.Radiobutton(vol_frame, text=lbl, variable=self.repeat_var,
                            value=val).pack(side=tk.LEFT, padx=2)

        # プレイリスト
        pl_frame = ttk.LabelFrame(self.root, text="プレイリスト", padding=4)
        pl_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        toolbar = tk.Frame(pl_frame, bg=pl_frame.cget("background"))
        toolbar.pack(fill=tk.X)
        for text, cmd in [("➕ ファイル追加", self._add_files),
                           ("📁 フォルダ追加", self._add_folder),
                           ("🗑️ 削除", self._remove_selected),
                           ("📋 クリア", self._clear_playlist)]:
            ttk.Button(toolbar, text=text, command=cmd).pack(side=tk.LEFT, padx=2, pady=2)
        self.pl_listbox = tk.Listbox(pl_frame, bg="#161b22", fg="#c9d1d9",
                                      selectbackground="#0f3460",
                                      font=("Arial", 10), relief=tk.FLAT)
        sb = ttk.Scrollbar(pl_frame, command=self.pl_listbox.yview)
        self.pl_listbox.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.pl_listbox.pack(fill=tk.BOTH, expand=True)
        self.pl_listbox.bind("<Double-1>", self._on_double_click)

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#16213e", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _add_files(self):
        paths = filedialog.askopenfilenames(
            filetypes=[("音楽ファイル", "*.mp3 *.wav *.ogg *.flac"),
                       ("すべて", "*.*")])
        for p in paths:
            self._add_to_playlist(p)

    def _add_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            for f in sorted(os.listdir(folder)):
                if f.lower().endswith(self.SUPPORTED_EXT):
                    self._add_to_playlist(os.path.join(folder, f))

    def _add_to_playlist(self, path):
        if path not in self.playlist:
            self.playlist.append(path)
            self.pl_listbox.insert(tk.END, os.path.basename(path))
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _remove_selected(self):
        sel = self.pl_listbox.curselection()
        for idx in reversed(sel):
            self.playlist.pop(idx)
            self.pl_listbox.delete(idx)
        self.status_var.set(f"{len(self.playlist)} 曲")

    def _clear_playlist(self):
        self._stop()
        self.playlist.clear()
        self.pl_listbox.delete(0, tk.END)
        self.current_idx = -1

    def _on_double_click(self, event):
        sel = self.pl_listbox.curselection()
        if sel:
            self.current_idx = sel[0]
            self._load_and_play()

    def _load_and_play(self):
        if not PYGAME_AVAILABLE or not self.playlist:
            return
        if 0 <= self.current_idx < len(self.playlist):
            path = self.playlist[self.current_idx]
            try:
                pygame.mixer.music.load(path)
                pygame.mixer.music.set_volume(self.vol_var.get() / 100)
                pygame.mixer.music.play()
                self.playing = True
                self.paused = False
                self.play_btn.config(text="⏸")
                name = os.path.basename(path)
                self.title_label.config(text=name)
                self.pl_listbox.selection_clear(0, tk.END)
                self.pl_listbox.selection_set(self.current_idx)
                self.pl_listbox.see(self.current_idx)
                self.status_var.set(f"再生中: {name}")
            except Exception as e:
                messagebox.showerror("エラー", str(e))

    def _play_pause(self):
        if not PYGAME_AVAILABLE:
            return
        if not self.playlist:
            return
        if self.current_idx < 0:
            self.current_idx = 0
        if not self.playing:
            self._load_and_play()
        elif self.paused:
            pygame.mixer.music.unpause()
            self.paused = False
            self.play_btn.config(text="⏸")
        else:
            pygame.mixer.music.pause()
            self.paused = True
            self.play_btn.config(text="▶")

    def _stop(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.stop()
        self.playing = False
        self.paused = False
        self.play_btn.config(text="▶")
        self.title_label.config(text="♪ 曲が選択されていません")

    def _prev(self):
        if not self.playlist:
            return
        self.current_idx = (self.current_idx - 1) % len(self.playlist)
        self._load_and_play()

    def _next(self):
        if not self.playlist:
            return
        repeat = self.repeat_var.get()
        if repeat == "one":
            pass
        elif self.shuffle_var.get():
            import random
            self.current_idx = random.randint(0, len(self.playlist) - 1)
        else:
            self.current_idx = (self.current_idx + 1) % len(self.playlist)
        self._load_and_play()

    def _set_volume(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.music.set_volume(self.vol_var.get() / 100)

    def _start_monitor(self):
        def monitor():
            while True:
                try:
                    if (self.playing and not self.paused
                            and not pygame.mixer.music.get_busy()):
                        self.playing = False
                        repeat = self.repeat_var.get()
                        if repeat == "one":
                            self.root.after(0, self._load_and_play)
                        elif repeat == "all" or self.current_idx < len(self.playlist) - 1:
                            self.root.after(0, self._next)
                except Exception:
                    pass
                import time
                time.sleep(0.5)

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

    def _on_close(self):
        if PYGAME_AVAILABLE:
            pygame.mixer.quit()
        self.root.destroy()


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

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

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

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

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

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

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

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

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

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

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

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

    _calculate()メソッドに計算・処理ロジックを実装します。

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

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

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

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

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

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。

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

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

💡 データのエクスポート機能

計算結果をCSV・TXTファイルに保存するエクスポート機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。

💡 入力履歴機能

以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。

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

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

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

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

❌ ウィンドウのサイズが変更できない

原因:resizable(False, False)が設定されています。

解決法:resizable(True, True)に変更してください。

9. 練習問題

アプリの理解を深めるための練習問題です。難易度順に挑戦してみてください。

  1. 課題1:機能拡張

    音楽プレイヤーに新しい機能を1つ追加してみましょう。どんな機能があると便利か考えてから実装してください。

  2. 課題2:UIの改善

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

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

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

🚀
次に挑戦するアプリ

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