如何运行一个30帧的图像序列完全在Python/Tkinter以内?

How to run a 30 frame image sequence completely within Python/Tkinter?

我在一个文件夹中有 30 张 .gif 图片。我想创建一个图像序列,在出现提示时将这些图像作为电影(24 或 30 fps)播放。我也只想使用 Python 的内部库(没有外部下载)。任何帮助将不胜感激。

我将以警告开始这个答案:此功能不适合初学者,而且对于您正在处理的项目来说几乎肯定没有必要。我会建议您避免此功能蔓延,而只是让您的程序实现其目的(输入正确的密码以打开保险箱)。没有人会介意保险柜门不会动画到打开位置。当然,如果你愿意,可以随意尝试,但我强烈建议先完成基本功能。然后,如果你想添加一个动画作为一种 "part two" 项目,无论你对动画进行多远,你至少都会有一些工作(即使是专业程序员也会这样做,这样项目 X 就可以满足老板的新截止日期,即使不必要的功能 Y 尚未完成)。

其实我最近刚实现了一个动画功能。我真的希望 tkinterpillow 中有一个更好的系统,或者你有什么玩 gif 的,但这是迄今为止我能想到的最好的系统。

我使用了四种方法(在我的 tkinter 应用程序中):

  • 选择包含图像序列的文件夹
  • 选择帧率
  • 开始动画
  • 保持动画运行宁

图像应采用 'xyz_1.gif''xyz_2.gif' 等形式,其中包含一些字符(或无字符)后跟下划线 _ 后跟点 . 和扩展名 gif。数字必须在文件名的最后一个下划线和最后一个点之间。我的程序使用 PILPillow 分支,因为它具有格式兼容性和图像处理功能(我在这个应用程序中主要只使用 resize()),因此您可以忽略将 resize() 转换为 pillow 图像转换为 tkinter 兼容图像。

为了完整起见,我已经包含了与动画相关的所有方法,但您无需担心其中的大部分。感兴趣的部分是 start_animation() 中的 self.animating 标志,以及整个 animate() 方法。 animate() 方法值得解释:

animate() 做的第一件事是将背景图像重新配置为包含图像序列的 list 中的下一张图像。接下来,它检查动画是否打开。如果关闭,它会停在那里。如果它打开,该函数调用 parent/master/root 小部件的 after() 方法,它基本上告诉 tkinter "I don't want this done now, so just wait this many milliseconds and then do it." 所以它调用它,然后等待指定的毫秒数如果可能的话,在用下一个数字调用 animate() 之前,或者如果我们在列表的末尾则为零(如果你只希望它 运行 一次而不是循环,有一个简单的 if 语句只有在 list 中有另一张图片时才会调用 after())。这几乎是递归调用的一个例子,但我们不是从函数中调用函数本身,我们只是在调用 "puts it in the queue," 可以这么说的东西。这样的函数可以无限期地执行而不会达到 Python 的递归限制。

def choose_animation(self):
    """
    Pops a choose so the user can select a folder containing their image sequence.
    """
    sequence_dir = filedialog.askdirectory() # ask for a directory for the image sequence
    if not sequence_dir: # if the user canceled,
        return # we're done
    files = os.listdir(sequence_dir) # get a list of files in the directory
    try:
        # make a list of tkinter images, sorted by the portion of the filenames between the last '_' and the last '.'.
        self.image_sequence = [ImageTk.PhotoImage(Image.open(os.path.join(sequence_dir, filename)).resize(((self.screen_size),(self.screen_size))))
        for filename in sorted(os.listdir(sequence_dir), key=lambda x: int(x.rpartition('_')[2][:-len(x.rpartition('.')[2])-1]))]
        self.start_animation() # no error? start the animation
    except: # error? announce it
        if self.audio:
            messagebox.showerror(title='Error', message='Could not load animation.')
        else:
            self.status_message.config(text='Error: could not load animation.')

def choose_framerate(self):
    """
    Pops a chooser for the framerate.
    """
    framerate_window = Toplevel()
    framerate_window.focus_set()
    framerate_window.title("Framerate selection")
    try: # try to load an img for the window's icon (top left corner of title bar)
        framerate_window.tk.call('wm', 'iconphoto', framerate_window._w, ImageTk.PhotoImage(Image.open("ico.png")))
    except: # if it fails
        pass # leave the user alone
    enter_field = Entry(framerate_window) # an Entry widget
    enter_field.grid(row=0, column=0) # grid it
    enter_field.focus_set() # and focus on it
    def set_to(*args):
        try:
            # use this framerate if it's a good value
            self.framerate = float(enter_field.get()) if 0.01 < float(enter_field.get()) <= 100 else [][0]
            framerate_window.destroy() # and close the window
        except:
            self.framerate = 10 # or just use 10
            framerate_window.destroy() # and close the window
    ok_button = Button(framerate_window, text='OK', command=set_to) # make a Button
    ok_button.grid(row=1, column=0) # grid it
    cancel_button = Button(framerate_window, text='Cancel', command=framerate_window.destroy) # cancel button
    cancel_button.grid(row=2, column=0) # grid it
    framerate_window.bind("<Return>", lambda *x: set_to()) # user can hit Return to accept the framerate
    framerate_window.bind("<Escape>", lambda *x: framerate_window.destroy()) # or Escape to cancel

def start_animation(self):
    """
    Starts the animation.
    """
    if not self.animating: # if animation is off
        try:
            self.animating = True # turn it on
            self.bg_img = self.image_sequence[0] # set self.bg_img to the first frame
            self.set_square_color('light', 'clear') # clear the light squares
            self.set_square_color('dark', 'clear') # clear the dark squares
            self.animate(0) # start the animation at the first frame
        except: # if something failed there,
            if self.audio: # they probably haven't set an animation. use messagebox,
                messagebox.showerror(title='Error', message='Animation not yet set.')
            else: # or a silent status_message update to announce the error.
                self.status_message.config(text='Error: animation not yet set.')
    else: # otherwise
        self.animating = False # turn it off

def animate(self, counter):
    """
    Animates the images in self.image_sequence.
    """
    self.board.itemconfig(self.bg_ref, image=self.image_sequence[counter]) # set the proper image to the passed element
    if self.animating: # if we're animating,
        # do it again with the next one
        self.parent.after(int(1000//self.framerate), lambda: self.animate(counter+1 if (counter+1)<len(self.image_sequence) else 0))