如何通过多个按钮切换Text组件内容?

0 投票
1 回答
45 浏览
提问于 2025-04-14 16:35

我想做一个应用程序,用来从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"))

撰写回答