中級者向け No.16

音楽プレイヤー

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

🎯 難易度: ★★☆ 📦 ライブラリ: tkinter(標準ライブラリ), pygame 🧩 作るもの: 曲やフォルダを並べて聴く音楽プレイヤー

1. アプリ概要

パソコンに保存した MP3・WAV・OGG・FLAC をファイルやフォルダ単位でリストに並べ、順番に聴くための音楽プレイヤーです。プレイリスト管理・再生・一時停止・音量調整ができ、音を鳴らす部分は pygame の mixer モジュールが受け持ちます。

pygame が入っていないときや音声デバイスを初期化できないときは、起動直後に pip install pygame-ce を案内するダイアログを出す作りです(音声デバイスが原因なら、再インストールでは直りません)。

音楽プレイヤー 実行画面(Windows)
実行画面(Windows)
音楽プレイヤー 実行画面(Linux Mint)
実行画面(Linux Mint)

2. 機能一覧

  • ループで生成するボタン群(それぞれ _add_files()・_add_folder()・_remove_selected()・_clear_playlist() を実行)
  • 「⏮」ボタン(_prev())・「▶」ボタン(_play_pause())・「⏹」ボタン(_stop())・「⏭」ボタン(_next())で操作
  • マウス操作 <Double-1> から _on_double_click() を実行
  • tk.Listbox による一覧表示(スクロールバー付き)
  • filedialog.askdirectory() によるフォルダ選択ダイアログ
  • filedialog.askopenfilenames() によるファイル選択ダイアログ(複数)
  • messagebox.showerror()・messagebox.showwarning() によるダイアログ通知
  • ウィンドウはタイトル「音楽プレイヤー」・サイズ 560x543 で起動

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.11 以上(Ubuntu で 3.14・3.15 の起動を確認) / Windows 11(実機)で起動と一部の操作(音量の変更)を確認・曲の再生は未確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)

2026年10月3日に、起動時の例外処理を修正しました(内容は下の注記)。修正後のコードを同じ日に、Windows 11(実機・Python 3.12.10)で pygame 2.6.1 を入れた状態で起動し、音量を変えられることを確認しました(曲の読み込み・再生は確認していません)。Linux Mint 22.3(仮想マシン・Python 3.12.3)では起動と画面表示を確認しています。操作は確認用のスクリプトから行ったもので、全機能の網羅テストではありません。macOS は必要なライブラリ(pip install)を入れれば動作する想定ですが、未検証です。

  • Python 3.11 以上(Ubuntu で 3.14・3.15 の起動を確認)
  • OS: Windows 11(実機で起動と一部の操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
⚠️
Python 3.14・3.15 の Windows では、pygame が通常の手順では入りません(2026年10月3日時点)

pygame の最新版 2.6.1 は、Windows 用の wheel(ビルド済みのパッケージ)が Python 3.13 用までで、3.14・3.15 用はありません(2026年10月3日に PyPI の配布ファイル一覧で確認)。この場合 pip install pygame はソースからのビルドを試み、SDL 系のライブラリの準備が必要になるため、通常の手順では入りません。これは Windows の Python 3.14 で実際に pip install を試した結果ではなく、配布ファイルの有無からの判断です。対処は次のどちらかです。

  • pygame-ce を使う:pygame-ce 2.5.8 は Python 3.10〜3.15 に対応し、3.15 用の Windows 用 wheel もあります。pygame を入れていた場合は、先に下の1行目でアンインストールします。コードは import pygame のまま変えずに使えます。
  • Python 3.13 の仮想環境を使う:x64/x86 の Windows なら、3.13 用の pygame の wheel があります(ARM 版の Windows 用はありません)。当サイトで確認したのは wheel があることまでで、3.13 の仮想環境への導入と動作は未検証です。
pip uninstall pygame
pip install pygame-ce

当サイトが pygame-ce でこのアプリを確認したのは、Linux(Ubuntu・Python 3.15.0rc2)で import pygame と起動までです。音の再生は確認していません。

💡
起動時の例外処理を except Exception に変えました(2026年10月3日)

コード冒頭の except ImportError: を except Exception: に変えました。音声デバイスが無い環境では pygame.mixer.init() が pygame.error を出し、except ImportError では拾えずに起動時に落ちていたためです。Windows 11(Python 3.12.10・pygame 2.6.1)では、存在しない音声ドライバを指定して、音声デバイスを使えない状態を作りました。この状態で pygame.mixer.init() は pygame.error を出し、修正後のコードは読み込み時に例外を出さず「pygame なし」の扱いになりました(2026年10月3日)。確認したのは画面を開かない読み込みまでで、この状態での起動画面は Windows では見ていません。修正後は、pygame が入っていないときと同じ扱い(警告を出し、再生はしない)で起動する作りです。警告の文面は、2026年10月14日に pygame-ce のインストールを促し、pygame が入っている環境では先に pip uninstall pygame をするよう書き添えたものに変えました(Python 3.14・3.15 の Windows では pygame の wheel が無いため・上の注記)。

4. 完全なソースコード

💡
コードのコピー方法

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

📦 必要なライブラリをインストール(初回のみ)
pip install pygame
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 Exception:  # ImportError に加え pygame.error(音声デバイスなし)も
    PYGAME_AVAILABLE = False


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

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

    def __init__(self, root):
        self.root = root
        self.root.title("音楽プレイヤー")
        self.root.geometry("560x543")
        self.root.minsize(560, 543)
        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-ce でインストールしてください。\n"
                "(pygame が入っている場合は先に pip uninstall 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=ttk.Style().lookup("TLabelframe", "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 クラスにアプリの全機能をまとめています(メソッド16個・全273行)。__init__ ではタイトル「音楽プレイヤー」とウィンドウサイズ 560x543 を設定します。あわせて状態を保持する変数(self.playing・self.paused)を初期化し、最後に _build_ui() で画面を組み立てます。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

ウィジェット構成

画面は tk.Frame×6・tk.Label×9・ttk.Scale×2・tk.Button×4・ttk.Checkbutton・ttk.Radiobutton(ループで生成)・ttk.LabelFrame・ttk.Style・ttk.Button(ループで生成)・tk.Listbox・ttk.Scrollbar で構成しています。見出し付きの枠(LabelFrame)は「プレイリスト」のラベルでエリアを区切っています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

イベント処理とボタンの接続

「⏮」ボタン(_prev())・「▶」ボタン(_play_pause())・「⏹」ボタン(_stop())・「⏭」ボタン(_next())を command= で接続しています。ボタンはラベルと処理をペアで並べたデータをループで回して生成し、それぞれ _add_files()・_add_folder()・_remove_selected()・_clear_playlist() を command= に渡しています。ボタンを増やしたいときはこのペアを1行足すだけで済みます。また bind("<Double-1>", ...) から _on_double_click() を呼べるようにイベントも登録しています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

クラス定数によるデータ定義

クラスの先頭でデータを定数として定義しています:SUPPORTED_EXT = ('.mp3', '.wav', '.ogg', '.flac')。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

例外処理

try-except で Exception を捕捉しています。あわせて messagebox.showerror()・messagebox.showwarning() のダイアログで、ユーザーへの通知・確認を行います。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

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

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

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

    新しいファイルを作成して app16.py と保存します。外部ライブラリ pygame を使います。先に pip install pygame を実行してください。Python 3.14・3.15 の Windows では、上の注記のとおり pip install pygame-ce を使ってください。

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

    App16 クラスを定義し、__init__ と、末尾の root = tk.Tk() ~ mainloop() の最小構成を書いて、まず空のウィンドウが出ることを確認します。

  3. 3
    ウィンドウを設定する

    title("音楽プレイヤー")・geometry("560x543")・configure(bg="#1a1a2e")・minsize(560, 543) をコンストラクタで設定します。

  4. 4
    画面部品を並べる

    _build_ui() の中で、tk.Frame×6・tk.Label×9・ttk.Scale×2・tk.Button×4・ttk.Checkbutton・ttk.Radiobutton(ループで生成)・ttk.LabelFrame・ttk.Style・ttk.Button(ループで生成)・tk.Listbox・ttk.Scrollbar を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。

  5. 5
    イベントを接続する

    「2. 機能一覧」で挙げた各ボタンを command= で対応するメソッド(_prev()・_play_pause()・_stop()・_next())につなぎます。ループ生成のボタンは、ラベルと処理(_add_files()・_add_folder()・_remove_selected()・_clear_playlist())をペアで並べたデータをループで回し、command= に渡して接続します。bind("<Double-1>", ...) の登録も忘れずに。

  6. 6
    中心になるメソッドを実装する

    アプリの本体である _load_and_play()(20行)・_next()(12行)・_play_pause()(17行)など を実装します。

  7. 7
    動作確認する

    python app16.py で起動し、各ボタンが反応すること・マウス操作(クリック・ホイールなど)が効くことを確認します。

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

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

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

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

💡 一覧の内容をファイルに書き出す

tk.Listbox に溜めた内容は、アプリを閉じると消えます(このコードに保存処理はありません)。一覧を1行ずつテキストファイルへ書き出す保存ボタンを追加しましょう。保存先の選択には、既に import されている filedialog が使えます。

💡 入力履歴機能

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

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

❌ 文字のフォントが崩れる・見た目が違う

原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。

解決法:ヘッダーと再生中の曲名は Noto Sans JP、再生時間・プレイリスト・ステータス行は Arial、⏮ ▶ ⏹ ⏭ の操作ボタンは Windows 用の絵文字フォント Segoe UI Emoji を指定しています。置き換わるとボタンの記号の形や大きさが見本と変わることがあります。font は見た目の指定にしか使われていないので、ボタンや音量スライダーから呼ばれる再生・停止・曲送り・音量の処理はフォントと無関係です。記号を整えるなら、btn_style の "font" を手元の絵文字フォント名に書き換えるか、その項目を消してください。

❌ ウィンドウの大きさが画面に合わない

原因:geometry("560x543") の固定サイズで起動するためです。

解決法:geometry() と minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(560, 543) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。

9. 練習問題

アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は全アプリ共通の定型課題です)。

  1. 課題1:曲を消しても再生位置がずれないようにする

    「🗑️ 削除」は選んだ曲をリストから取り除きますが、再生位置を表す self.current_idx はそのままです。再生中の曲より前を消すと、番号が1つずつ後ろへずれます。削除に合わせて位置を直してください。

    期待結果:3曲入れて2曲目を再生し、1曲目を消してから ⏭ を押すと3曲目に進む(いまは同じ曲が頭から鳴り直す)。

    合格条件

    • 再生中の曲より後ろを消したときは、⏭ の動きが変わらない
    • 再生中の曲そのものを消したときの動きを決めて、そのとおりになる
    • プレイリストが1曲だけのときに再生中の曲を消しても、⏭ でエラーにならない

    ヒント①(どこを触るか): 触るのは _remove_selected() の1か所です。ここは self.playlist.pop(idx) と self.pl_listbox.delete(idx) を呼ぶだけで、self.current_idx に触れていません。_clear_playlist() は -1 に戻しているので、そのままで構いません。
    ヒント②(使うもの): 削除は for idx in reversed(sel) のループですが、tk.Listbox は既定で1件しか選べないため、いまは必ず1回だけ回ります。idx が self.current_idx より小さければ1つ減らす、同じなら再生を止める、という2つの分岐を pop() の直後に足すと素直です。停止には既存の self._stop() が使えます。
    つまずきやすい点: _stop() は再生を止めるだけで、プレイリストからは何も消えません。消した曲がいま鳴っている曲だったかどうかは、self.playlist から取り除く前に判定してください。取り除いたあとでは番号と中身の対応が変わっています。

  2. 課題2:再生中の経過時間を表示する

    シークバーの左右にある 0:00 は作られたきりで、一度も書き換えられていません。再生中の経過時間を左側の pos_label に出してください。シークバー本体(seek_bar)は今回そのままで構いません。

    期待結果:曲を再生すると左の 0:00 が1秒ごとに増え、⏹ を押すと 0:00 に戻る。

    合格条件

    • 一時停止(⏸)の間は数字が止まる
    • 曲を切り替えると、新しい曲の経過時間として数え直される
    • pygame が入っていない環境でも、起動時にエラーが出ない

    ヒント①(どこを触るか): 触るのは3か所です。更新用のメソッドを1つ足し、__init__() の self._build_ui() の次の行から呼び始めます。0:00 に戻す処理は _stop() に1行足すだけで十分です。
    ヒント②(使うもの): pygame.mixer.music.get_pos() が再生開始からの経過ミリ秒を返します。1000 で割った秒数を f"{秒 // 60}:{秒 % 60:02d}" に整えると、いまの表示と同じ書式になります。繰り返しは self.root.after(500, ...) でメインスレッドに回してください。
    つまずきやすい点: 再生していないとき get_pos() は負の値を返すことがあります。self.playing と self.paused を見て、再生中だけ更新するのが安全です。監視用のスレッド(_start_monitor())から直接ラベルを触るのは避けます。既存のコードも self.root.after(0, ...) を通しています。

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

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

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のアプリにも挑戦してみましょう。

🐛
エラーが出て動かないときは

写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。

📖
次のレベルへ進む参考書

このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。