Python 在mainloop旁边运行依赖于Tkinter的代码而不冻结GUI

Python 在mainloop旁边运行依赖于Tkinter的代码而不冻结GUI,python,multithreading,image,user-interface,tkinter,Python,Multithreading,Image,User Interface,Tkinter,我正在编写一个简单的图像查看器,它可以让用户快速浏览上万张图像,每次大约100张。这些图像是磁盘上的文件 为了让查看器正常工作,它必须在用户当前图像之前不断预加载图像(否则查看器将无法正常工作) 我用来在Tkinter标签的网格中显示图像的基本方法如下(这已经过测试并且有效): 我需要ImageTk.PhotoImage实例在标签上显示图像。我实现了两种不同的方法,每种方法都有一个相关的问题 第一种方法: 启动一个单独的线程预加载图像: def load_ahead(): for fn

我正在编写一个简单的图像查看器,它可以让用户快速浏览上万张图像,每次大约100张。这些图像是磁盘上的文件

为了让查看器正常工作,它必须在用户当前图像之前不断预加载图像(否则查看器将无法正常工作)

我用来在Tkinter标签的网格中显示图像的基本方法如下(这已经过测试并且有效):

我需要ImageTk.PhotoImage实例在标签上显示图像。我实现了两种不同的方法,每种方法都有一个相关的问题

第一种方法: 启动一个单独的线程预加载图像:

def load_ahead():
    for fn in images:
        cache[fn] = load_image()
threading.Thread(target=load_ahead).start()
top.mainloop()
这在我的Linux机器上运行得很好。然而,在另一台机器上(恰好运行Windows,并使用pyinstaller编译),似乎发生了死锁。打印“Before Photoimage”,然后程序冻结,这表明加载程序线程在创建ImageTk.Photoimage对象时卡住了。在主线程(Tkinter mainloop的)内创建ImageTk.PhotoImage对象会发生什么?创建照片图像在计算上是昂贵的,还是与实际从磁盘加载图像相比可以忽略不计

第二种方法: 为了规避从Tkiner的主循环线程中创建PhotoImage对象的可能要求,我求助于Tk.after:

def load_some_images():
    #load only 10 images. function must return quickly to prevent freezing GUI
    for i in xrange(10): 
        fn = get_next_image()
        cache[fn] = load_image(fn)
    top.after_idle(load_some_images)

top.after_idle(load_some_images)
这样做的问题是,由于产生额外的开销(即图像加载过程必须分解为非常小的块,因为它与GUI竞争),它会在调用期间定期冻结GUI,并且似乎会消耗执行期间发生的任何键盘事件

第三种方法 是否有一种方法可以检测挂起的用户事件?我怎样才能完成这样的事情

def load_some_images():
    while True:
        try: top.pending_gui_events.get_nowait()
        except: break
        #user is still idle! continuing caching of images
        fn = get_next_image()
        cache[fn] = load_image(fn)
    top.after_idle(load_some_images)

top.after(5,load_some_images)
编辑:我尝试使用top.tk.call('after','info')检查挂起的键盘事件。这并不总是可靠的,接口仍然迟钝/无响应


提前感谢您的建议

我建议您创建一个
加载一张图像
函数,而不是
加载一些图像
函数。它不太可能干扰事件循环

此外,根据经验,在空闲后通过
调用的函数不应在空闲后使用
重新调度。原因是空闲后的
将阻塞,直到空闲事件队列耗尽。如果在处理队列的过程中不断向队列中添加内容,它永远不会完全耗尽。这可能就是为什么您的GUI在使用第二种方法时偶尔会挂起的原因


在(5,…)
之后尝试
,而不是在空闲(…)之后尝试
。如果您的系统可以在不到5毫秒的时间内创建一个图像,那么您可以在大约半秒钟内处理100个图像,这可能足够快,可以提供一个非常简洁的界面。您可以调整延迟,看看它如何影响应用程序的整体感觉。

谢谢。我试过几种不同的方法。此方法引入的大量开销导致图像加载速度至少慢5倍。尽管如此,GUI仍然相当缓慢,忽略键盘事件并随机冻结数秒。即使我只加载一个映像,当加载一个较大的映像时,或者如果磁盘读取文件的速度较慢,用户也会不可避免地感受到这1-2秒或所需的时间。我认为这几乎排除了将映像的IO密集型加载放在主循环中的可能性。我尝试了第三种方法(使用self.tk.call('after','info')检查挂起的用户事件),这种方法可能更好一些,但仍然很慢且不可靠(事件并不总是很快被检测到)。使UI响应且不容易随机挂起的唯一方法似乎是在单独的线程上加载图像。@ealfonso:
在_info
只告诉您在
之后安排的事件后,它对任何其他类型的事件一无所知。@ealfonso:图像有多大,您的计算机有多旧?在2013 macbook pro上,我可以在大约0.5秒内直接加载100张1300x600.gif图像。
def load_some_images():
    while True:
        try: top.pending_gui_events.get_nowait()
        except: break
        #user is still idle! continuing caching of images
        fn = get_next_image()
        cache[fn] = load_image(fn)
    top.after_idle(load_some_images)

top.after(5,load_some_images)