Tkinter,线程,几个小部件定期更新



我正在尝试使用tkinter做一个应用程序,在输入框中输入内容,然后单击";负载";按钮

按钮函数从输入框中读取填充字符串。加载后,它从互联网上检索一些信息(正在屏蔽),然后用这些信息更新一些标签。

显然,当点击加载按钮时,由于请求阻塞了程序的流程,应用程序会冻结一微秒。一旦检索到信息并更新了标签,其他一些标签就需要不断地从互联网上检索数据。为了做到这一点,我让他们这样做:

注意:打印语句是为了测试而做的,所以我可以在控制台上看到它们

def update_price_label(self):
# TODO fix .after() duplicate
print("Updating stuff")
price = self.get_formatted_price(self.stuff) # this is another function being called, passing an argument of the stuff that has been loaded by the load button, this function returns price
self.PriceValue.configure(text=price) # updates the price label with the price obtained from the function above

self.PriceValue.after(1000, self.update_price_label) # sets a scheduler to run this function to update the price label each second

上面有一个函数,它在点击"时被调用;负载";对于需要一直更新的标签,此函数调用另一个接收参数的函数,然后返回价格。然后更新标签,然后使用priceValue标签控件的after()方法以无休止的循环对其进行调度。这样,价格就会一直更新

def get_formatted__price(self, stuff):
price = RETRIEVE PRICE # this is not a real function, but this is the request being done to the server to get the price
return f"{price:.12f} # sets decimal numbers

此函数由update_price_label()调用,接收一个参数并返回价格

正如你所看到的,我已经将标签更新功能与实际从服务器检索信息的实际功能进行了区分。因此,第一个函数负责调用另一个函数来检索信息,更新标签,然后使用每个标签小部件的after()方法重新调度自己。

像这样的5个功能需要更新应用程序上的几个标签,连接到不同的来源并保持信息的最新。所有这些都使用after()进行调度,并以相同的间隔(1秒)运行。

显然,由于不使用任何线程,应用程序在请求信息时会冻结很多信息,因为它们在本质上是阻塞的。

因此,我需要实现线程或任何形式的并发。我找不到任何关于这方面的好教程,或者至少符合我对一款定期从来源获取信息的应用程序的需求。

我仍然掌握着线程和并发的概念,也许还有其他方式,比如异步或其他并发方法,我还不知道,可能更适合。但线程似乎是Tkinter最常用的线程。

我认为这些请求函数中的每一个都需要一个线程。像这样:

get_formatted_price_thread = Thread(target=get_formatted_price, args=(stuff), daemon=True) # calling a thread with an argument on the function and setting daemon to true, so it closes when I close the app

因此,我尝试在其中一个线程上创建线程作为示例,并发现了一些限制,例如:

无法直接获取get_formatted_price()函数的返回值。因此,另一种方法可以是让线程中的函数更改标签值。或者将整个标签更新函数包装在一个线程中。但当我到处读的时候,Tkinter并不是线程安全的。这意味着更新tkinter窗口小部件可能在某些操作系统上运行良好,但在其他操作系统中则不然。

除此之外,我似乎很难理解如何将这个应用程序的结构转变为与线程或任何类型的并发都能很好地工作的结构。因为我需要在检索到新信息后尽快更新所有标签。

我还担心线程是由操作系统控制的。因为它决定了线程何时启动,以及在获取数据时如何影响我的应用程序性能。

我也检查了队列和队列库,但我不确定这是否对我有帮助,因为每个价格更新都会由线程放入队列,并由标签小部件检索。但是,当队列获得队列的第一个元素时,信息可能已经过时。

所以我的问题是,我需要在这里改变什么才能实现我所需要的。如果线程是我需要继续的,或者我可能需要尝试另一种方法。

任何能满足我需要的应用程序源代码示例都将不胜感激。毕竟,检索信息并使小部件与这些信息保持最新应该是一个非常常见的用例。

我想到的另一种方法是创建一个数据结构,比如Python字典或对象。每个服务器获取函数都将作为守护进程在一个无休止循环中的线程中运行,并写入字典。然后,标签小部件更新功能,由于它们是调度的,将读取字典上的数据并相应地更新标签。但我认为这种方法可能会很混乱,并且可能会延迟更新标签和字典上的信息,除非设置较小的after()调度程序计时器。或者所有的解决方案默认情况下都是混乱的

谢谢。

我会通过创建一个数据结构,创建一个可以根据数据结构中的当前值更新显示的函数,然后绑定到调用该函数的事件来解决这个问题。然后,创建一个线程来更新此数据结构,并在数据更改时发出事件。

这里有一个人为的例子,它每秒调用一次web服务,并用时间和时区信息更新一个简单的数据结构。每当数据发生变化时,它就会发出一个<<Tick>>事件,触发显示器的更新。

我不是编写线程tkinter代码的专家,我的理解是,除非在极少数情况下,在创建小部件的线程之外的线程中运行任何tkinter码都是不安全的。一个例外是,额外的线程生成事件是安全的,因为事件本身是在主GUI线程中处理的。我猜调用winfo_exists函数也是安全的,因为它不修改任何内部数据结构。

这个例子在10秒后杀死自己,这样就不会对服务器造成太长时间的冲击。

import requests
import tkinter as tk
from tkinter.font import Font
from threading import Thread
import time

class ThreadedClock(tk.Frame):
data = {"time": "", "tz": ""}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.time_label = tk.Label(self, width=12, font=Font(size=32))
self.tz_label = tk.Label(self, text="GMT")
self.time_label.pack(side="top", fill="x")
self.tz_label.pack(side="top", fill="x")
# call the refresh function on every <<Tick>> event
self.bind("<<Tick>>", self.refresh)
# start a thread to update the data and generate <<Tick>> events
self.thread = Thread(target=self.get_data, daemon=True)
self.running = True
self.thread.start()
def get_data(self):
while self.winfo_exists():
now = time.time()
response = requests.get(
"https://timeapi.io/api/Time/current/zone?timeZone=GMT"
)
t = response.json()
timestr = f"{t['hour']:02}:{t['minute']:02}:{t['seconds']:02}"
self.data = {"time": timestr, "tz": t["timeZone"]}
self.event_generate("<<Tick>>")
delta = time.time() - now
time.sleep(delta)
def refresh(self, event=None):
self.time_label.configure(text=self.data["time"])
self.tz_label.configure(text=f"timezone: {self.data['tz']}")

if __name__ == "__main__":
root = tk.Tk()
root.after(10000, root.destroy)
clock = ThreadedClock(root)
clock.pack(fill="both", expand=True)
root.mainloop()

最新更新