I want to make app to read log files from FTP server - when user clicks button, it connects to FTP, changes directory and reads data every 1sec. If data changed (new logs appeared), then it adds it to Text widget. It works cool until user pressed same button more than once - then here's error 'RuntimeError: threads can only be started once'. So in different version of this code below, I added additional button "Show Logs" for each USB button. Also, each USB button had it's own Text widget. So when user pressed USB button first, then he's able to switch between Text widgets by "Show Logs" buttons - but it got messy, mostly because I had issue with telling my func readdata() which Text widget forget (hide) and which render (.pack again) - for example I wanted to pack_forget Text widget1 and .pack Text widget2 afterwards, but I don't know how to make app remember active contexts. Also scrolling was not working in "switched-to" Text widgets (worked only in "active" Texts), it sometimes couldn't read data file and many other issues. Is here more elegant way to do so? I mean, just clicking buttons "USB0" and "USB1" to switch between logs, without using additional "Show Logs" buttons for each USB button?
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()
That's fixed simply by changing
(which is binding a single thread's
startmethod to the button) towhich will create a new thread and start it every time you click it, but you'll also need to be able to stop the previous thread from trying to update the field at the same time.
You can do that with something like