0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

シンプルな配信用フォルダ指定スライドショーアプリ

Posted at

はじめに

とある絵師の方の配信を拝見していた際に、ちょうど使い勝手の良いシンプルなスライドショーアプリが欲しいとおっしゃっていたので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()

おわりに

キャッ
キャッ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?