はじめに
とある絵師の方の配信を拝見していた際に、ちょうど使い勝手の良いシンプルなスライドショーアプリが欲しいとおっしゃっていたのでChatGPT o3-miniに作ってもらい共有する次第で候。クシクシ...
機能
-
画像フォルダの選択
- 設定ウィンドウからフォルダを選択可能
-
サブフォルダの画像を含めるオプション
- チェックボックスでサブフォルダの画像も対象にするか選択可能
-
スライドショーの再生順
- 連番 / ランダム をラジオボタンで切り替え
-
表示間隔の設定
- インターバル(秒単位)を入力してスライドの切り替え速度を変更
-
ウィンドウサイズに応じた画像表示
- 画像は縦または横をウィンドウサイズに合わせ、余白部分には暗くフェードした背景画像を表示
-
ウィンドウサイズ変更対応
- ウィンドウサイズを変更すると、それに合わせて画像をリサイズ
置き場
以下で実行ファイル化したものを公開しています。
プログラム
import os
import glob
import random
import tkinter as tk
import tkinter.filedialog as filedialog
import tkinter.messagebox as messagebox
from PIL import Image, ImageTk, ImageEnhance, ImageOps
class SlideShowApp:
def __init__(self, master, image_folder, interval, shuffle_mode, include_subfolders):
self.master = master
self.master.title("スライドショー")
self.interval = interval # ミリ秒単位
self.image_folder = image_folder
self.shuffle_mode = shuffle_mode # True: ランダム, False: 連番
self.include_subfolders = include_subfolders # サブフォルダ内も含むか
self.canvas = tk.Canvas(master, highlightthickness=0)
self.canvas.pack(fill="both", expand=True)
self.master.bind("<Configure>", self.on_resize)
self.load_images()
if not self.image_paths:
messagebox.showerror("エラー", "指定フォルダに画像がありません")
self.master.destroy()
return
self.current_index = 0
self.bg_imgtk = None
self.fg_imgtk = None
self.current_img = None
self.show_image()
def load_images(self):
"""指定フォルダから画像パスを取得(サブフォルダを含むかは設定に依存)"""
self.image_paths = []
exts = ("*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif")
for ext in exts:
if self.include_subfolders:
# 再帰的に検索(Python3.5以降)
pattern = os.path.join(self.image_folder, '**', ext)
self.image_paths.extend(glob.glob(pattern, recursive=True))
else:
pattern = os.path.join(self.image_folder, ext)
self.image_paths.extend(glob.glob(pattern))
self.image_paths.sort()
if self.shuffle_mode:
random.shuffle(self.image_paths)
def show_image(self):
if not self.image_paths:
return
path = self.image_paths[self.current_index]
try:
img = Image.open(path).convert("RGB")
except Exception as e:
print(f"画像読み込みエラー: {path} ({e})")
self.next_image()
return
self.current_img = img
self.update_images()
self.master.after(self.interval, self.next_image)
def update_images(self):
if self.current_img is None:
return
win_w = self.master.winfo_width()
win_h = self.master.winfo_height()
if win_w < 10 or win_h < 10:
return
# 背景画像:ウィンドウサイズに合わせて切り抜き、暗くフェードさせる
bg_img = ImageOps.fit(self.current_img, (win_w, win_h), method=Image.LANCZOS)
bg_img = ImageEnhance.Brightness(bg_img).enhance(0.3)
self.bg_imgtk = ImageTk.PhotoImage(bg_img)
# 前景画像:アスペクト比を維持してウィンドウに収める
orig_w, orig_h = self.current_img.size
ratio = orig_w / orig_h
win_ratio = win_w / win_h
if ratio > win_ratio:
new_w = win_w
new_h = int(win_w / ratio)
else:
new_h = win_h
new_w = int(win_h * ratio)
fg_img = self.current_img.resize((new_w, new_h), Image.LANCZOS)
self.fg_imgtk = ImageTk.PhotoImage(fg_img)
self.canvas.delete("all")
self.canvas.create_image(win_w // 2, win_h // 2, image=self.bg_imgtk)
self.canvas.create_image(win_w // 2, win_h // 2, image=self.fg_imgtk)
def on_resize(self, event):
try:
if self.current_img:
self.update_images()
except tk.TclError:
pass
def next_image(self):
if not self.image_paths:
return
self.current_index = (self.current_index + 1) % len(self.image_paths)
self.show_image()
def update_config(self, new_folder, new_interval, new_shuffle_mode, new_include_subfolders):
"""設定更新:画像フォルダ、インターバル、再生順、サブフォルダ含有の更新"""
folder_changed = new_folder != self.image_folder
mode_changed = new_shuffle_mode != self.shuffle_mode
include_changed = new_include_subfolders != self.include_subfolders
self.image_folder = new_folder
self.interval = new_interval
self.shuffle_mode = new_shuffle_mode
self.include_subfolders = new_include_subfolders
if folder_changed or mode_changed or include_changed:
self.load_images()
self.current_index = 0
if self.current_img:
self.update_images()
class ConfigWindow:
def __init__(self, root):
self.root = root
self.root.title("スライドショー設定")
# 画像フォルダ
tk.Label(root, text="画像フォルダ:").grid(row=0, column=0, padx=10, pady=10, sticky="e")
self.folder_entry = tk.Entry(root, width=40)
self.folder_entry.grid(row=0, column=1, padx=10, pady=10)
self.browse_button = tk.Button(root, text="参照", command=self.browse_folder)
self.browse_button.grid(row=0, column=2, padx=10, pady=10)
# インターバル
tk.Label(root, text="インターバル (秒):").grid(row=1, column=0, padx=10, pady=10, sticky="e")
self.interval_entry = tk.Entry(root, width=10)
self.interval_entry.grid(row=1, column=1, padx=10, pady=10, sticky="w")
# 再生順ラジオボタン
tk.Label(root, text="再生順:").grid(row=2, column=0, padx=10, pady=10, sticky="e")
self.shuffle_mode = tk.BooleanVar(value=False)
self.radio_seq = tk.Radiobutton(root, text="連番", variable=self.shuffle_mode, value=False)
self.radio_rand = tk.Radiobutton(root, text="ランダム", variable=self.shuffle_mode, value=True)
self.radio_seq.grid(row=2, column=1, padx=5, pady=10, sticky="w")
self.radio_rand.grid(row=2, column=2, padx=5, pady=10, sticky="w")
# サブフォルダを含むかチェックボックス
self.include_subfolders = tk.BooleanVar(value=False)
self.subfolder_check = tk.Checkbutton(root, text="サブフォルダを含む", variable=self.include_subfolders)
self.subfolder_check.grid(row=3, column=1, padx=10, pady=10, sticky="w")
# ボタン:開始・更新設定
self.start_button = tk.Button(root, text="開始", command=self.start_slideshow)
self.start_button.grid(row=4, column=0, padx=10, pady=20)
self.update_button = tk.Button(root, text="更新設定", command=self.update_slideshow)
self.update_button.grid(row=4, column=1, padx=10, pady=20)
self.slideshow_app = None
def browse_folder(self):
folder = filedialog.askdirectory()
if folder:
self.folder_entry.delete(0, tk.END)
self.folder_entry.insert(0, folder)
def start_slideshow(self):
folder = self.folder_entry.get().strip()
if not folder or not os.path.isdir(folder):
messagebox.showerror("エラー", "有効なフォルダを指定してください")
return
try:
interval = int(float(self.interval_entry.get()) * 1000)
except ValueError:
messagebox.showerror("エラー", "インターバルは数値で指定してください")
return
slideshow_window = tk.Toplevel(self.root)
slideshow_window.geometry("800x600")
self.slideshow_app = SlideShowApp(
slideshow_window,
folder,
interval,
self.shuffle_mode.get(),
self.include_subfolders.get()
)
def update_slideshow(self):
if self.slideshow_app and self.slideshow_app.master.winfo_exists():
folder = self.folder_entry.get().strip()
if not folder or not os.path.isdir(folder):
messagebox.showerror("エラー", "有効なフォルダを指定してください")
return
try:
interval = int(float(self.interval_entry.get()) * 1000)
except ValueError:
messagebox.showerror("エラー", "インターバルは数値で指定してください")
return
self.slideshow_app.update_config(
folder,
interval,
self.shuffle_mode.get(),
self.include_subfolders.get()
)
if __name__ == "__main__":
root = tk.Tk()
root.geometry("500x250")
ConfigWindow(root)
root.mainloop()
おわりに
キャッ
キャッ