如何通过多个按钮切换Text组件内容?
我想做一个应用程序,用来从FTP服务器读取日志文件。当用户点击一个按钮时,它会连接到FTP,切换目录,并每秒读取一次数据。如果数据有变化(比如出现了新的日志),就把这些数据添加到文本框里。这个功能运行得很好,直到用户多次点击同一个按钮,这时就会出现错误信息“RuntimeError: threads can only be started once”。为了处理这个问题,我在下面的代码中为每个USB按钮添加了一个额外的“显示日志”按钮。同时,每个USB按钮都有自己的文本框。
所以当用户第一次点击USB按钮时,就可以通过“显示日志”按钮在文本框之间切换,但这样就变得有些混乱,主要是因为我在告诉我的函数readdata()哪个文本框要隐藏(forget)和哪个要显示(.pack)时遇到了问题。例如,我想先隐藏文本框1,然后显示文本框2,但我不知道怎么让应用记住当前的上下文。此外,在切换到的文本框中滚动也不工作(只有在“活动”文本框中可以滚动),有时还无法读取数据文件,还有很多其他问题。
有没有更优雅的方法来实现这个功能?我的意思是,能不能只通过点击“USB0”和“USB1”按钮来切换日志,而不需要为每个USB按钮使用额外的“显示日志”按钮?
import tkinter as tk
import pysftp
import time
import threading
root = tk.Tk()
root.geometry("1200x700")
frame = tk.Frame(root)
frame.place(x=15, y=10)
scroll = tk.Scrollbar(frame)
t = tk.Text(frame, width=183, height=45, yscrollcommand=scroll.set)
scroll.config(command=t.yview)
scroll.pack(side='right', fill='y')
def clears():
t.delete(1.0, tk.END)
def readdata(text,dir,logfile):
clears()
t.insert(tk.END, text+"LOADING LOGS\n\n")
cnopts = pysftp.CnOpts()
cnopts.hostkeys = None
sftp = pysftp.Connection('ip', username='user', password='pass', cnopts=cnopts)
sftp.chdir('uart-logs')
sftp.chdir(dir)
active_log = logfile
with sftp.open(active_log, mode="r") as file:
old_text = file.read().decode('ASCII')
t.insert(tk.END, old_text)
t.see('end')
while True:
with sftp.open(active_log, mode="r") as file:
new_text = file.read().decode('ASCII')
if new_text != old_text:
t.insert(tk.END, new_text[len(old_text):])
root.update()
t.see('end')
old_text = new_text
time.sleep(1)
buttons = tk.Frame()
b0 = tk.Button(buttons, text = "ttyUSB0", command =threading.Thread(target=lambda:readdata('USB0: ','USB0',"USB0-active.log")).start)
b1 = tk.Button(buttons, text = "ttyUSB1", command =threading.Thread(target=lambda:readdata('USB1: ','USB1',"USB1-active.log")).start)
clear = tk.Button(buttons, text="Clear", command=lambda:clears())
chkbox = tk.Checkbutton(buttons, text="Auto-scroll")
buttons.pack()
b0.pack(in_=buttons, side=tk.LEFT, padx=10)
b1.pack(in_=buttons, side=tk.LEFT, padx=10)
clear.pack(in_=buttons, side=tk.LEFT, padx=10)
chkbox.pack(in_=buttons, side=tk.RIGHT, padx=40)
frame.pack(fill='both',expand=True)
t.pack(fill='both',expand=True)
tk.mainloop()
1 个回答
2
这个功能很好用,直到用户多次按同一个按钮,这时就会出现错误信息:'RuntimeError: threads can only be started once'(运行时错误:线程只能启动一次)。
解决这个问题很简单,只需将
command=threading.Thread(target=lambda:readdata('USB0: ','USB0',"USB0-active.log")).start
(这段代码是把一个线程的start
方法绑定到按钮上)改成
command=lambda: threading.Thread(target=lambda:readdata('USB0: ','USB0',"USB0-active.log")).start()
这样每次点击按钮时都会创建一个新的线程并启动它,但你还需要确保之前的线程不会同时尝试更新同一个字段。
你可以用类似下面的代码来做到这一点:
current_read_thread = None
current_stop_event = None
def readdata(text, dir, logfile, stop_event):
...
while not stop_event.is_set(): # (not `while True`)
...
def start_thread(text, dir, logfile):
global current_read_thread
global current_stop_event
if current_read_thread:
current_stop_event.set()
# Wait for the other thread to stop
current_read_thread.join()
current_read_thread = None
current_stop_event = threading.Event()
current_read_thread = threading.Thread(target=readdata, args=(text, dir, logfile, current_stop_event))
current_read_thread.start()
# ...
b0 = tk.Button(buttons, text="ttyUSB0", command=lambda: start_thread("USB0: ", "USB0", "USB0-active.log"))