用ai写了一个简单的图片轮播的工具

功能

1.随机播放特定文件夹中的图片

2.左右方向键随机切换图片

特点

1.支持播放webp动图

2.切换图片时不会卡顿

使用说明

1.安装依赖pip install pygame pillow

2.将IMAGE_DIR 改成你的文件夹路径

注意

短时间多次切换图片会卡顿

import os
import random
import threading
import time
from collections import OrderedDict
from concurrent.futures import ThreadPoolExecutor

import pygame
from PIL import Image, ImageSequence

# ------------------------- 配置区 -------------------------
IMAGE_DIR = r"这里输入文件夹路径Enter the folder path here."
SUPPORTED_EXT = {".webp", ".png", ".jpg", ".jpeg", ".bmp", ".gif"}
PREFETCH_POOL_SIZE = 2      # 后台随时保持解码好的候选图数量
DECODE_THREAD_WORKERS = 4   # 解码线程池大小
IMAGE_CACHE_SIZE = 8        # 最近访问图片的 LRU 缓存容量
BG_COLOR = (0, 0, 0)
TARGET_FPS = 60
# -----------------------------------------------------------


class Frame:
    """单帧数据:原始 Surface(未做显示格式转换) + 时长(秒)"""
    __slots__ = ("raw_surf", "converted_surf", "duration")

    def __init__(self, raw_surf, duration):
        self.raw_surf = raw_surf
        self.converted_surf = None
        self.duration = duration

    def get_surface(self):
        # 懒转换:第一次被主线程用到时才 convert_alpha,
        # 避免在后台解码线程里调用 SDL 相关函数
        if self.converted_surf is None:
            self.converted_surf = self.raw_surf.convert_alpha()
        return self.converted_surf


class AnimatedImage:
    """
    表示一张图片(可能是动图)。后台线程逐帧解码并追加到 self.frames,
    first_frame_ready 事件在第 0 帧解码完成后立即置位,主线程据此即可开始播放,
    不需要等整张动图解码完。
    """

    def __init__(self, path):
        self.path = path
        self.frames = []  # List[Frame]
        self.is_static = False
        self.done_decoding = threading.Event()
        self.first_frame_ready = threading.Event()
        self.error = None

    def decode(self):
        try:
            im = Image.open(self.path)
            n_frames = getattr(im, "n_frames", 1)
            self.is_static = n_frames <= 1
            for i, frame in enumerate(ImageSequence.Iterator(im)):
                rgba = frame.convert("RGBA")
                data = rgba.tobytes()
                # frombuffer 是纯内存操作,线程安全,不涉及 SDL 显示子系统
                surf = pygame.image.frombuffer(data, rgba.size, "RGBA").copy()
                duration = frame.info.get("duration", 100) / 1000.0
                if duration <= 0:
                    duration = 0.1
                self.frames.append(Frame(surf, duration))
                if i == 0:
                    self.first_frame_ready.set()
        except Exception as e:  # 单张图片损坏不应影响整个程序
            self.error = e
            self.first_frame_ready.set()
        finally:
            self.done_decoding.set()


class ImageLibrary:
    """管理图片列表、后台解码线程池、LRU 缓存与随机预取候选池"""

    def __init__(self, root_dir):
        self.paths = self._scan(root_dir)
        if not self.paths:
            raise RuntimeError(f"目录中没有找到支持的图片: {root_dir}")
        self.executor = ThreadPoolExecutor(max_workers=DECODE_THREAD_WORKERS)
        self.cache = OrderedDict()   # path -> AnimatedImage (LRU)
        self.prefetch_pool = {}      # path -> AnimatedImage (预取中/已就绪)
        self._lock = threading.Lock()

    @staticmethod
    def _scan(root_dir):
        paths = []
        for dirpath, _, filenames in os.walk(root_dir):
            for fn in filenames:
                ext = os.path.splitext(fn)[1].lower()
                if ext in SUPPORTED_EXT:
                    paths.append(os.path.join(dirpath, fn))
        return paths

    def _start_decode(self, path):
        img = AnimatedImage(path)
        self.executor.submit(img.decode)
        return img

    def _touch_cache(self, path, img):
        with self._lock:
            if path in self.cache:
                self.cache.move_to_end(path)
            else:
                self.cache[path] = img
                while len(self.cache) > IMAGE_CACHE_SIZE:
                    self.cache.popitem(last=False)

    def get_or_start(self, path):
        with self._lock:
            if path in self.cache:
                img = self.cache[path]
                self.cache.move_to_end(path)
                return img
            if path in self.prefetch_pool:
                img = self.prefetch_pool.pop(path)
                self._touch_cache(path, img)
                return img
        img = self._start_decode(path)
        self._touch_cache(path, img)
        return img

    def random_path(self, exclude=None):
        if len(self.paths) == 1:
            return self.paths[0]
        while True:
            p = random.choice(self.paths)
            if p != exclude:
                return p

    def ensure_prefetch(self, exclude_paths, count=PREFETCH_POOL_SIZE):
        """把预取候选池补充到 count 个"""
        with self._lock:
            need = count - len(self.prefetch_pool)
            existing = set(self.prefetch_pool) | set(self.cache) | set(exclude_paths)
        for _ in range(max(0, need)):
            candidate = self.random_path()
            tries = 0
            while candidate in existing and tries < 10:
                candidate = self.random_path()
                tries += 1
            img = self._start_decode(candidate)
            with self._lock:
                self.prefetch_pool[candidate] = img
            existing.add(candidate)

    def pop_prefetched(self, exclude_paths, wait_timeout=0.5):
        """从预取池取一张随机候选;优先取已经就绪的,极端连续按键时短暂兜底等待"""
        with self._lock:
            items = [
                (p, img) for p, img in self.prefetch_pool.items()
                if p not in exclude_paths
            ]
        if not items:
            return None
        ready = [(p, img) for p, img in items if img.first_frame_ready.is_set()]
        if ready:
            p, img = random.choice(ready)
        else:
            p, img = items[0]
            img.first_frame_ready.wait(timeout=wait_timeout)
        with self._lock:
            self.prefetch_pool.pop(p, None)
        self._touch_cache(p, img)
        return p, img


class Viewer:
    def __init__(self, library):
        pygame.init()
        pygame.display.set_caption("随机图片播放器")
        info = pygame.display.Info()
        self.window_size = (info.current_w, info.current_h)
        self.screen = pygame.display.set_mode(self.window_size, pygame.RESIZABLE)

        self.library = library
        self.current_path = self.library.random_path()
        self.current_img = self.library.get_or_start(self.current_path)
        # 初始第一张图需要短暂等待首帧解码完成(通常几毫秒到几十毫秒)
        self.current_img.first_frame_ready.wait(timeout=3.0)

        self.frame_index = 0
        self.frame_timer = 0.0
        self._scaled_cache = None
        self._scaled_cache_key = None

        self.clock = pygame.time.Clock()
        self.running = True

        # 提前把预取池填满,为第一次按键切换做准备
        self.library.ensure_prefetch(exclude_paths={self.current_path})

    def switch_to_random(self):
        result = self.library.pop_prefetched(exclude_paths={self.current_path})
        if result is None:
            path = self.library.random_path(exclude=self.current_path)
            img = self.library.get_or_start(path)
        else:
            path, img = result
        self.current_path = path
        self.current_img = img
        self.frame_index = 0
        self.frame_timer = 0.0
        self._scaled_cache = None
        self._scaled_cache_key = None
        # 补齐预取池,为下一次切换做准备
        self.library.ensure_prefetch(exclude_paths={self.current_path})

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.VIDEORESIZE:
                new_size = (event.w, event.h)
                if new_size != self.window_size:
                    self.window_size = new_size
                    self.screen = pygame.display.set_mode(self.window_size, pygame.RESIZABLE)
                    self._scaled_cache = None
            elif event.type == pygame.KEYDOWN:
                if event.key in (pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN):
                    self.switch_to_random()
                elif event.key == pygame.K_ESCAPE:
                    self.running = False

    def _current_frame(self):
        img = self.current_img
        if img.error and not img.frames:
            # 损坏文件:自动跳过,换下一张,避免卡死在黑屏
            self.switch_to_random()
            img = self.current_img
        if not img.frames:
            return None
        if self.frame_index >= len(img.frames):
            if img.done_decoding.is_set():
                self.frame_index = 0
                self.frame_timer = 0.0
            else:
                self.frame_index = len(img.frames) - 1
        return img.frames[self.frame_index]

    def update_animation(self, dt):
        img = self.current_img
        if img.is_static or not img.frames:
            return
        self.frame_timer += dt
        while True:
            idx = min(self.frame_index, len(img.frames) - 1)
            duration = img.frames[idx].duration
            if self.frame_timer < duration:
                break
            self.frame_timer -= duration
            nxt = self.frame_index + 1
            if nxt >= len(img.frames):
                if img.done_decoding.is_set():
                    nxt = 0
                else:
                    break  # 解码还没追上播放进度,先停在当前帧
            self.frame_index = nxt

    def _get_scaled(self, frame):
        surf = frame.get_surface()
        key = (id(surf), self.window_size)
        if self._scaled_cache_key == key:
            return self._scaled_cache
        w, h = surf.get_size()
        win_w, win_h = self.window_size
        if w == 0 or h == 0 or win_w <= 0 or win_h <= 0:
            return surf
        scale = min(win_w / w, win_h / h)
        new_size = (max(1, int(w * scale)), max(1, int(h * scale)))
        scaled = surf if new_size == (w, h) else pygame.transform.smoothscale(surf, new_size)
        self._scaled_cache = scaled
        self._scaled_cache_key = key
        return scaled

    def draw(self):
        self.screen.fill(BG_COLOR)
        frame = self._current_frame()
        if frame is not None:
            scaled = self._get_scaled(frame)
            win_w, win_h = self.window_size
            x = (win_w - scaled.get_width()) // 2
            y = (win_h - scaled.get_height()) // 2
            self.screen.blit(scaled, (x, y))
        pygame.display.flip()

    def run(self):
        while self.running:
            dt = self.clock.tick(TARGET_FPS) / 1000.0
            self.handle_events()
            self.update_animation(dt)
            self.draw()
        pygame.quit()


def main():
    library = ImageLibrary(IMAGE_DIR)
    viewer = Viewer(library)
    viewer.run()


if __name__ == "__main__":
    main()