フォトギャラリー
フォルダ内の画像をサムネイル一覧表示・スライドショー再生できるフォトギャラリー。Pillowとスクロール可能なCanvasを学びます。
1. アプリ概要
フォルダ内の画像をサムネイル一覧表示・スライドショー再生できるフォトギャラリー。Pillowとスクロール可能なCanvasを学びます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ)・Pillow で、難易度は ★★☆ です。
このアプリは「メディア処理」カテゴリです。画像・音声・動画を扱う領域は Python の強力なライブラリ群が活きる分野で、メディア処理の作法は他の用途にも応用が効きます。tkinter(標準ライブラリ)・Pillow を活かして実装するこの構造は、他のアプリにも応用が効きます。
コピー&実行で動作確認 → コード解説で構造理解 → カスタマイズで応用、の 3 ステップで進めると、短時間で本質を押さえられます。
アプリを完成させた後は、自分の使い方に合わせて改造するのが学びを定着させる近道です。カスタマイズ章のアイデアを足がかりに、独自機能を一つ追加してみてください。
2. 機能一覧
- 「📂 フォルダを開く」ボタン(
_open_folder())・「▶ スライドショー」ボタン(_start_slideshow())・「⏹ 停止」ボタン(_stop_slideshow())・「🔄」ボタン(_sort_images())・「◀ 前」ボタン(_navigate())・「次 ▶」ボタン(_navigate())で操作 <Configure>イベント(ウィンドウ側からの通知)で_on_canvas_resize()を自動実行- マウス操作
<MouseWheel>からlambda e: self.thumb_canvas.yview_scroll(-1 if e.delta > 0 else 1, 'units')を実行 <Configure>イベント(ウィンドウ側からの通知)でlambda e: self.thumb_canvas.configure(scrollregion=self.thumb_canvas.bbox('all'))を自動実行- キー
<Left>から_navigate()を実行 - キー
<Right>から_navigate()を実行 - マウス操作
<Button-1>からlambda e: self.preview_canvas.focus_set()を実行 - マウス操作
<Button-1>からlambda e, c=cell: on_click(cell)を実行 - マウス操作
<Button-1>からlambda e, c=cell: on_click(cell)を実行 - マウス操作
<Button-1>からlambda e, c=cell: on_click(cell)を実行 filedialog.askdirectory()によるファイル選択ダイアログmessagebox.showerror()・messagebox.showinfo()によるダイアログ通知- ウィンドウはタイトル「フォトギャラリー」・サイズ 1000x680 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は必要なライブラリ(pip install)を入れれば動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
インストールが必要なライブラリ
pip install pillow
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import threading
import time
try:
from PIL import Image, ImageTk
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
class App47:
"""フォトギャラリー"""
SUPPORTED_EXT = {".jpg", ".jpeg", ".png", ".bmp", ".gif",
".webp", ".tiff", ".tif"}
THUMB_SIZE = (120, 90)
def __init__(self, root):
self.root = root
self.root.title("フォトギャラリー")
self.root.geometry("1000x680")
self.root.configure(bg="#1e1e1e")
self._images = [] # {"path", "name", "thumb_tk"}
self._thumb_cache = {}
self._slideshow_running = False
self._slideshow_idx = 0
self._slideshow_id = None
self._current_path = None
self._build_ui()
def _build_ui(self):
# ヘッダー
header = tk.Frame(self.root, bg="#252526", pady=6)
header.pack(fill=tk.X)
tk.Label(header, text="📷 フォトギャラリー",
font=("Noto Sans JP", 12, "bold"),
bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
ttk.Button(header, text="📂 フォルダを開く",
command=self._open_folder).pack(side=tk.LEFT, padx=4)
# スライドショー
ttk.Button(header, text="▶ スライドショー",
command=self._start_slideshow).pack(side=tk.LEFT, padx=4)
ttk.Button(header, text="⏹ 停止",
command=self._stop_slideshow).pack(side=tk.LEFT, padx=4)
tk.Label(header, text="間隔(秒):", bg="#252526", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
self.slide_interval_var = tk.DoubleVar(value=3.0)
ttk.Spinbox(header, from_=0.5, to=30, increment=0.5,
textvariable=self.slide_interval_var,
width=5).pack(side=tk.LEFT)
# ソート
tk.Label(header, text="並び:", bg="#252526", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 2))
self.sort_var = tk.StringVar(value="名前順")
ttk.Combobox(header, textvariable=self.sort_var,
values=["名前順", "更新日順", "サイズ順"],
state="readonly", width=10).pack(side=tk.LEFT)
ttk.Button(header, text="🔄",
command=self._sort_images).pack(side=tk.LEFT, padx=2)
if not PIL_AVAILABLE:
tk.Label(self.root,
text="⚠ Pillow が未インストールです (pip install Pillow)。",
bg="#fff3cd", fg="#856404", font=("Arial", 9),
anchor="w", padx=8).pack(fill=tk.X)
# メインエリア: 左(サムネイル) / 右(プレビュー)
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# 左: サムネイルグリッド
left = tk.Frame(paned, bg="#1e1e1e")
paned.add(left, weight=2)
self._build_thumbnail_panel(left)
# 右: プレビュー
right = tk.Frame(paned, bg="#0d1117")
paned.add(right, weight=3)
self._build_preview_panel(right)
# ステータスバー
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 _build_thumbnail_panel(self, parent):
# 検索
search_f = tk.Frame(parent, bg="#252526", pady=4)
search_f.pack(fill=tk.X)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *a: self._filter_thumbnails())
ttk.Entry(search_f, textvariable=self.search_var,
width=20).pack(side=tk.LEFT, padx=4, fill=tk.X, expand=True)
tk.Label(search_f, text="🔍", bg="#252526", fg="#ccc").pack(side=tk.LEFT)
# スクロール可能なキャンバス
thumb_outer = tk.Frame(parent, bg="#1e1e1e")
thumb_outer.pack(fill=tk.BOTH, expand=True)
self.thumb_canvas = tk.Canvas(thumb_outer, bg="#1e1e1e",
highlightthickness=0)
sb = ttk.Scrollbar(thumb_outer, command=self.thumb_canvas.yview)
self.thumb_canvas.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.thumb_canvas.pack(fill=tk.BOTH, expand=True)
self.thumb_canvas.bind("<Configure>", self._on_canvas_resize)
self.thumb_canvas.bind("<MouseWheel>",
lambda e: self.thumb_canvas.yview_scroll(
-1 if e.delta > 0 else 1, "units"))
self.thumb_frame = tk.Frame(self.thumb_canvas, bg="#1e1e1e")
self.thumb_canvas.create_window((0, 0), window=self.thumb_frame,
anchor="nw", tags="inner")
self.thumb_frame.bind("<Configure>",
lambda e: self.thumb_canvas.configure(
scrollregion=self.thumb_canvas.bbox("all")))
def _build_preview_panel(self, parent):
# 画像情報
self.info_var = tk.StringVar(value="")
tk.Label(parent, textvariable=self.info_var, bg="#0d1117",
fg="#8b949e", font=("Arial", 9)).pack(anchor="w", padx=6, pady=2)
# プレビューキャンバス
self.preview_canvas = tk.Canvas(parent, bg="black",
highlightthickness=0)
self.preview_canvas.pack(fill=tk.BOTH, expand=True)
self.preview_img_ref = None
# クリックでナビゲーション
self.preview_canvas.bind("<Left>",
lambda e: self._navigate(-1))
self.preview_canvas.bind("<Right>",
lambda e: self._navigate(1))
self.preview_canvas.bind("<Button-1>",
lambda e: self.preview_canvas.focus_set())
self.preview_canvas.configure(takefocus=True)
# ナビゲーションボタン
nav_f = tk.Frame(parent, bg="#252526", pady=4)
nav_f.pack(fill=tk.X)
tk.Button(nav_f, text="◀ 前",
command=lambda: self._navigate(-1),
bg="#3c3c3c", fg="white", relief=tk.FLAT,
font=("Arial", 10), padx=12, pady=3,
activebackground="#505050", bd=0).pack(side=tk.LEFT, padx=8)
tk.Button(nav_f, text="次 ▶",
command=lambda: self._navigate(1),
bg="#3c3c3c", fg="white", relief=tk.FLAT,
font=("Arial", 10), padx=12, pady=3,
activebackground="#505050", bd=0).pack(side=tk.LEFT, padx=4)
self.nav_label = tk.Label(nav_f, text="",
bg="#252526", fg="#ccc", font=("Arial", 9))
self.nav_label.pack(side=tk.LEFT, padx=8)
# ズーム
tk.Label(nav_f, text="ズーム:", bg="#252526", fg="#ccc",
font=("Arial", 9)).pack(side=tk.RIGHT, padx=4)
self.zoom_var = tk.DoubleVar(value=1.0)
ttk.Scale(nav_f, from_=0.1, to=5.0, variable=self.zoom_var,
orient=tk.HORIZONTAL, length=100,
command=lambda v: self._show_preview(self._current_path)
).pack(side=tk.RIGHT, padx=4)
# ── フォルダ読み込み ──────────────────────────────────────────
def _open_folder(self):
folder = filedialog.askdirectory()
if folder:
self._load_folder(folder)
def _load_folder(self, folder):
if not PIL_AVAILABLE:
messagebox.showerror("エラー", "Pillow が必要です:\npip install Pillow")
return
self.status_var.set("読み込み中...")
self._images.clear()
self._thumb_cache.clear()
threading.Thread(target=self._do_load_folder,
args=(folder,), daemon=True).start()
def _do_load_folder(self, folder):
files = []
for fn in os.listdir(folder):
ext = os.path.splitext(fn)[1].lower()
if ext in self.SUPPORTED_EXT:
files.append(os.path.join(folder, fn))
files.sort(key=lambda p: p.lower())
for path in files:
self._images.append({"path": path, "name": os.path.basename(path)})
self.root.after(0, self._on_images_loaded, folder)
def _on_images_loaded(self, folder):
self.status_var.set(f"{len(self._images)} 枚の画像")
self.root.title(f"フォトギャラリー — {folder}")
self._render_thumbnails(self._images)
if self._images:
self._current_path = self._images[0]["path"]
self._show_preview(self._current_path)
def _render_thumbnails(self, images):
for widget in self.thumb_frame.winfo_children():
widget.destroy()
for idx, img_info in enumerate(images):
self._create_thumb(idx, img_info)
def _create_thumb(self, idx, img_info):
path = img_info["path"]
cell = tk.Frame(self.thumb_frame, bg="#252526",
relief=tk.FLAT, bd=2)
cell.grid(row=idx // 3, column=idx % 3, padx=3, pady=3)
# サムネイル読み込みは遅延
thumb_label = tk.Label(cell, bg="#252526", fg="#888",
text="...", width=14, height=6)
thumb_label.pack()
name_label = tk.Label(cell, text=img_info["name"][:16],
bg="#252526", fg="#ccc",
font=("Arial", 7))
name_label.pack()
# クリックでプレビュー
def on_click(p=path, c=cell):
for w in self.thumb_frame.winfo_children():
w.config(bg="#252526", relief=tk.FLAT)
for ww in w.winfo_children():
ww.config(bg="#252526")
c.config(bg="#0288d1", relief=tk.SUNKEN)
self._current_path = p
self._show_preview(p)
cell.bind("<Button-1>", lambda e, c=cell: on_click(cell))
thumb_label.bind("<Button-1>", lambda e, c=cell: on_click(cell))
name_label.bind("<Button-1>", lambda e, c=cell: on_click(cell))
# サムネイル生成を別スレッドで
threading.Thread(target=self._load_thumb,
args=(path, thumb_label), daemon=True).start()
def _load_thumb(self, path, label):
if path in self._thumb_cache:
self.root.after(0, label.config,
{"image": self._thumb_cache[path], "text": ""})
return
try:
img = Image.open(path)
img.thumbnail(self.THUMB_SIZE, Image.LANCZOS)
tk_img = ImageTk.PhotoImage(img)
self._thumb_cache[path] = tk_img
self.root.after(0, label.config, {"image": tk_img, "text": ""})
except Exception:
pass
def _on_canvas_resize(self, event):
self.thumb_canvas.itemconfig("inner", width=event.width)
# ── プレビュー ────────────────────────────────────────────────
def _show_preview(self, path):
if not path or not PIL_AVAILABLE:
return
try:
img = Image.open(path)
img.load()
w_orig, h_orig = img.size
zoom = self.zoom_var.get()
cw = max(1, self.preview_canvas.winfo_width())
ch = max(1, self.preview_canvas.winfo_height())
# フィット表示
if zoom == 1.0:
scale = min(cw / w_orig, ch / h_orig)
new_w = max(1, int(w_orig * scale))
new_h = max(1, int(h_orig * scale))
else:
new_w = max(1, int(w_orig * zoom))
new_h = max(1, int(h_orig * zoom))
resized = img.resize((new_w, new_h), Image.LANCZOS)
self.preview_img_ref = ImageTk.PhotoImage(resized)
self.preview_canvas.delete("all")
self.preview_canvas.create_image(
cw // 2, ch // 2, image=self.preview_img_ref, anchor="center")
# 情報表示
stat = os.stat(path)
size_kb = stat.st_size // 1024
self.info_var.set(
f"{os.path.basename(path)} | {w_orig}×{h_orig} | {size_kb}KB")
# ナビゲーション
idx = next((i for i, im in enumerate(self._images)
if im["path"] == path), -1)
total = len(self._images)
if idx >= 0:
self.nav_label.config(text=f"{idx+1} / {total}")
except Exception as e:
self.preview_canvas.delete("all")
self.preview_canvas.create_text(
300, 200, text=f"画像を開けませんでした\n{e}",
fill="#888", font=("Arial", 11))
def _navigate(self, delta):
if not self._images:
return
idx = next((i for i, im in enumerate(self._images)
if im["path"] == self._current_path), 0)
new_idx = (idx + delta) % len(self._images)
self._current_path = self._images[new_idx]["path"]
self._show_preview(self._current_path)
def _filter_thumbnails(self):
query = self.search_var.get().strip().lower()
if query:
filtered = [im for im in self._images
if query in im["name"].lower()]
else:
filtered = self._images
self._render_thumbnails(filtered)
def _sort_images(self):
mode = self.sort_var.get()
if mode == "名前順":
self._images.sort(key=lambda im: im["name"].lower())
elif mode == "更新日順":
self._images.sort(key=lambda im: os.path.getmtime(im["path"]),
reverse=True)
elif mode == "サイズ順":
self._images.sort(key=lambda im: os.path.getsize(im["path"]),
reverse=True)
self._render_thumbnails(self._images)
# ── スライドショー ────────────────────────────────────────────
def _start_slideshow(self):
if not self._images:
messagebox.showinfo("情報", "画像を読み込んでください")
return
self._slideshow_running = True
self._slideshow_idx = next(
(i for i, im in enumerate(self._images)
if im["path"] == self._current_path), 0)
self._slideshow_tick()
def _slideshow_tick(self):
if not self._slideshow_running or not self._images:
return
self._slideshow_idx = (self._slideshow_idx + 1) % len(self._images)
self._current_path = self._images[self._slideshow_idx]["path"]
self._show_preview(self._current_path)
interval_ms = int(self.slide_interval_var.get() * 1000)
self._slideshow_id = self.root.after(interval_ms, self._slideshow_tick)
def _stop_slideshow(self):
self._slideshow_running = False
if self._slideshow_id:
self.root.after_cancel(self._slideshow_id)
self._slideshow_id = None
if __name__ == "__main__":
root = tk.Tk()
app = App47(root)
root.mainloop()
5. コード解説
フォトギャラリーのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App47 クラスにアプリの全機能をまとめています(メソッド19個・全368行)。__init__ ではタイトル「フォトギャラリー」とウィンドウサイズ 1000x680 を設定します。あわせて状態を保持する変数(self._slideshow_running・self._slideshow_idx・self._slideshow_id・self._current_path)を初期化し、最後に _build_ui() で画面を組み立てます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×8・tk.Label×11・ttk.Button×4・ttk.Spinbox・ttk.Combobox・ttk.PanedWindow・ttk.Entry・tk.Canvas×2・ttk.Scrollbar・tk.Button×2・ttk.Scale で構成しています。PanedWindow を使っているので、ペインの境界をマウスでドラッグして幅を変えられます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「📂 フォルダを開く」ボタン(_open_folder())・「▶ スライドショー」ボタン(_start_slideshow())・「⏹ 停止」ボタン(_stop_slideshow())・「🔄」ボタン(_sort_images())・「◀ 前」ボタン(_navigate())・「次 ▶」ボタン(_navigate())を command= で接続しています。また bind("<Configure>", ...) から _on_canvas_resize()、bind("<MouseWheel>", ...) から lambda e: self.thumb_canvas.yview_scroll(-1 if e.delta > 0 else 1, 'units')、bind("<Configure>", ...) から lambda e: self.thumb_canvas.configure(scrollregion=self.thumb_canvas.bbox('all'))、bind("<Left>", ...) から _navigate()、bind("<Right>", ...) から _navigate()、bind("<Button-1>", ...) から lambda e: self.preview_canvas.focus_set()、bind("<Button-1>", ...) から lambda e, c=cell: on_click(cell)、bind("<Button-1>", ...) から lambda e, c=cell: on_click(cell)、bind("<Button-1>", ...) から lambda e, c=cell: on_click(cell) を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
クラス定数によるデータ定義
クラスの先頭でデータを定数として定義しています:SUPPORTED_EXT = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp', '.tiff', '.tif'}、THUMB_SIZE = (120, 90)。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception・ImportError を捕捉しています。あわせて messagebox.showerror()・messagebox.showinfo() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app47.py と保存します。外部ライブラリ
PILを使います。先にpip install Pillowを実行してください。(import 名と pip のパッケージ名が異なる点に注意:PILはPillowでインストールします) -
2クラスの骨格を作る
App47クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("フォトギャラリー")・geometry("1000x680")・configure(bg="#1e1e1e")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×8・tk.Label×11・ttk.Button×4・ttk.Spinbox・ttk.Combobox・ttk.PanedWindow・ttk.Entry・tk.Canvas×2・ttk.Scrollbar・tk.Button×2・ttk.Scaleを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_open_folder()・_start_slideshow()・_stop_slideshow()・_sort_images()・_navigate()・_navigate())につなぎます。bind("<Configure>", ...)・bind("<MouseWheel>", ...)・bind("<Configure>", ...)・bind("<Left>", ...)・bind("<Right>", ...)・bind("<Button-1>", ...)・bind("<Button-1>", ...)・bind("<Button-1>", ...)・bind("<Button-1>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_on_images_loaded()(7行)・_slideshow_tick()(8行)・_sort_images()(11行)など を実装します。 -
7動作確認する
python app47.pyで起動し、各ボタンが反応すること・キー操作(<Left>・<Right>)が効くこと・マウス操作(クリック・ホイールなど)が効くことを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 クラス定数を書き換えて動きを変える
データはクラス定数(SUPPORTED_EXT・THUMB_SIZE)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。
💡 結果をファイルに保存する
このコードに保存処理はないため、表示中の数値や結果はアプリを閉じると消えます。テキストファイルへ書き出して、次回起動時に読み込む機能を追加してみましょう。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ <Left> キーを押しても反応しない
原因:bind("<Left>", ...) はウィジェット単位で登録されるため、ウィンドウ(またはバインド先)にキーボードフォーカスが無いとイベントが届きません。
解決法:一度ウィンドウ内をクリックしてフォーカスを与えてから、キーを押してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("1000x680") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。「📂 フォルダを開く」で選んだフォルダの画像は、ファイル名・大きさ・ファイルサイズまで画面に出ます(292-293行)。人の顔や住所が写った写真、仕事で預かった画像は開かないでください。このアプリに保存や削除の処理は無いので、開いた画像を書き換えることはありません。それでも練習は、コピーしたフォルダで行うほうが安心です。画像の読み込みに Pillow を使うので、先に pip install Pillow が必要です(7-11行)。入っていないと、上に警告の帯が出てフォルダを開けません(67-71行・179-181行)。
-
課題1:「▶ スライドショー」を2回押すと、切り替えが速くなる
スライドショーは
after()で次の1枚を予約します。すでに動いているかを確かめないので、押すたびに予約が1本ずつ増えていきます。期待結果:画像が3枚以上あるフォルダを開き、「間隔(秒)」を
3.0のままにして「▶ スライドショー」を1回押す。3秒ごとに写真が変わる。もう一度「▶ スライドショー」を押すと、押した瞬間に1枚進み、そのあとは3秒のあいだに2枚進むようになる(間隔はそろわない)。直したあとは、何回押しても3秒ごとのまま。合格条件
- 1回だけ押したときの動きは今までどおり
- 2回以上押しても、切り替えの間隔は「間隔(秒)」の値のまま
- 「⏹ 停止」のあと、もう一度「▶ スライドショー」で再開できる
ヒント①(どこを触るか): 触るのは1か所です。
_start_slideshow()のself._slideshow_running = True(343行)の手前。
ヒント②(使うもの): すでに動いているなら、そのままreturnするかself._stop_slideshow()を呼んでから始めます。動いているかどうかはself._slideshow_running(28行)で分かり、予約の取り消しは_stop_slideshow()(358-362行)のafter_cancel()が引き受けます。
つまずきやすい点:self._slideshow_idが覚えているのは最後に予約した1本だけです(356行)。残った1本も、_slideshow_runningがFalseなら次のティックで止まります(350-351行)。_slideshow_tick()は1枚進めてから予約する作りなので、押した瞬間に表示が変わります(352-356行)。間隔の値は355行でミリ秒に直され、0.5なら500ミリ秒です。 -
課題2:検索で絞っても「次 ▶」が絞り込みの外へ進む
🔍 に文字を入れると、サムネイルはその名前を含む画像だけになります。ところが前後の移動と枚数の表示が見ているのは、絞り込む前の全部です。画面と操作が食い違います。
期待結果:画像が10枚ほど入ったフォルダを開き、🔍 に文字を1つ入れてサムネイルを2〜3枚に絞る。左のサムネイルは絞り込まれているのに、ボタンの横の枚数は
1 / 10のように全部の枚数のまま(枚数は開いたフォルダによって変わります)。「次 ▶」を押すと、絞り込みに入っていない画像が出る。直したあとは、絞り込んだ枚数(1 / 3のような表示)の中だけを行き来する。合格条件
- 🔍 が空のときは、今までどおり全部の画像を行き来できる
- 🔍 で絞っているときは、絞り込まれた画像だけを行き来する
N / MのMが、いま並んでいるサムネイルの枚数と合う
ヒント①(どこを触るか): 触るのは3か所です。
_filter_thumbnails()(316-323行)、_navigate()(307-314行)、_show_preview()の296-300行。
ヒント②(使うもの):_filter_thumbnails()が作ったfilteredをself._filteredのような属性に残し、_navigate()と_show_preview()はそちらを見るようにします。並べ替え(325-335行)と読み込み直後(199-205行)でも、同じ属性をself._imagesの内容でそろえてください。
つまずきやすい点: 🔍 が空のとき、322行のfilteredはself._imagesと同じリストそのものです。list(self._images)のように写しを取らないと、片方を並べ替えたつもりでもう片方まで変わります。_sort_images()と_on_images_loaded()は絞り込みを見ずに全部を描き直すので、🔍 に文字を入れたまま「🔄」を押すと、サムネイルだけ全部に戻ります。スライドショー(349-356行)もself._imagesを見ているため、そろえるかどうかを先に決めておいてください。 -
課題3:保存機能の追加
入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。