I have a minimal tkinter program which analyses some data. Some of the datafiles are quite large, so to ensure that the GUI remains responsive I load the data in a new thread.
How can I run analysis on the data once the thread has terminated?
Some example code is below.
import tkinter
from threading import Thread
from time import sleep
result = []
def func(result):
sleep(10)
ans = 1
result.append(ans)
class myApp(tkinter.Tk):
def __init__(self, parent):
tkinter.Tk.__init__(self, parent)
self.grid()
self.myButton = tkinter.Button(self, text="Press me!", command=self.onButtonPress)
self.myButton.grid(column=0, row=0)
def onButtonPress(self):
thread = Thread(target=func, args=(result,))
thread.start()
self.myButton["text"]=result
app = myApp(None)
app.mainloop()
How can I make the button text change only when func returns?
multiprocessing has an isalive() function and so you can use Tkinter's after() to query it periodically if you want to switch over
Related
I have simple code using which I am trying to execute multiple threads where some heavy calculations are done in each thread and appropriately, the progress bar has to be updated. Sadly, when I click the START button, the Tkinter GUI freezes and never recovers. My code is as follows.
from tkinter import *
from tkinter.ttk import Progressbar
from threading import Thread
#########################################################################
def run():
for i in range(1, 5001):
P1['value'] = (i/5001)*100
P1.update_idletasks()
#########################################################################
def thread():
T1 = Thread(target=run)
T1.start()
T1.join()
#########################################################################
root = Tk()
B1 = Button(root, text='START', command=thread)
B1.grid(row=0, column=0)
P1 = Progressbar(root, orient=HORIZONTAL, length=450, mode='determinate')
P1.grid(row=0, column=1)
root.geometry('500x35')
root.mainloop()
#########################################################################
I have an app that starts the GUI and performs "the heavy part" of the code in another thread using QThread. In this thread I emit a SIGNAL that is connected to the GUI Class and that performs addItem on QListWidget.
There are a massive "signaling" from this thread to the GUI and it "freeze".
Is there a way to avoid this? Have I to use another mini GUI in different thread only for QListWidget?
Thanks
EDIT:
This is the thread that execute the heavy logic
class YourThreadName(QThread):
def __init__(self, some variables):
QThread.__init__(self)
def __del__(self):
self.wait()
def run(self):
# Here there is a for cycle that emits a SIGNAL
for ... :
...
self.emit(SIGNAL("needed_variable"), needed_variable)
...
In the GUI Class there are some methods, particularly:
class GUI(QtGui.QMainWindow, GUI.Ui_MainWindow):
def __init__(self, parent=None):
super(GUI, self).__init__(parent)
self.setupUi(self)
def ... (self):
...
def start_main_code(self):
self.new_thread = YourThreadName(some variables)
self.connect(self.new_thread, SIGNAL("finished()"), self.done)
self.connect(self.new_thread, SIGNAL("needed_variable"), self.show_variable)
self.new_thread.start()
def show_variable(self, data):
self.QListWidget_object.addItem(data)
def ... (self):
...
The script below is a Minimal, Complete, and Verifiable Example based on the information currently given in your question and comments. It emits data from a worker thread every 10ms and updates a list-widget in the GUI. On my Linux system (using Python-3.6.3, Qt-4.8.7 and PyQt-4.12.1) it does not block or freeze the GUI. There is obviously some flickering whilst the list-widget is being updated, but I am able to select items, scroll up and down, click the button, etc. And if I increase the sleep to 25ms, I don't even get any flickering.
UPDATE:
The performance can be improved by using setUniformItemSizes and sending the messages in batches. On my system, after a slight initial delay, the list populates with fifty thousand items almost instantly.
import sys
from PyQt4 import QtCore, QtGui
class Worker(QtCore.QThread):
message = QtCore.pyqtSignal(object)
def run(self):
batch = []
for index in range(50000):
if len(batch) < 200:
batch.append(index)
continue
self.message.emit(batch)
batch = []
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.listWidget = QtGui.QListWidget()
self.listWidget.setUniformItemSizes(True)
self.button = QtGui.QPushButton('Start')
self.button.clicked.connect(self.handleButton)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.listWidget)
layout.addWidget(self.button)
self.worker = Worker()
self.worker.message.connect(self.handleMessages)
def handleMessages(self, batch):
for message in batch:
self.listWidget.addItem('Item (%s)' % message)
def handleButton(self):
if not self.worker.isRunning():
self.worker.start()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(600, 50, 200, 400)
window.show()
sys.exit(app.exec_())
I'm realtively new to python and am making a GUI app that does a lot of file i/o and processing. To complete this i would like to get a confirmation box to pop-up when the user commits and actions. From this when clicking 'yes' the app then runs the i/o and displays a progress bar.
From other threads on here I have gotten as far as reading about the requirement to create an addtional thread to take on one of these processes (for example Tkinter: ProgressBar with indeterminate duration and Python Tkinter indeterminate progress bar not running have been very helpful).
However, I'm getting a little lost because I'm not activating the threaded process from the Main() function. So I'm still getting lost in how, and where, I should be creating the progress bar and passing of the i/o process to another thread (reading in a csv file here).
Here is my code and I would be very grateful for any help anyone can give me:
import tkinter as tk
import tkinter.messagebox as messagebox
import csv
import tkinter.ttk as ttk
import threading
class ReadIn(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.initUI()
def initUI(self):
self.parent.title("Read in file and display progress")
self.pack(fill=tk.BOTH, expand=True)
self.TestBtn = tk.Button(self.parent, text="Do Something", command=lambda: self.confirm_pb())
self.TestBtn.pack()
def confirm_pb(self):
result = messagebox.askyesno("Confirm Action", "Are you sure you want to?")
if result:
self.handle_stuff()
def handle_stuff(self):
nf = threading.Thread(target=self.import_csv)
nf.start()
self.Pbar()
nf.join()
def Pbar(self):
self.popup = tk.Tk()
self.popup.title('Loading file')
self.label = tk.Label(self.popup, text="Please wait until the file is created")
self.progressbar = ttk.Progressbar(self.popup, orient=tk.HORIZONTAL, length=200,
mode='indeterminate')
self.progressbar.pack(padx=10, pady=10)
self.label.pack()
self.progressbar.start(50)
def import_csv(self):
print("Opening File")
with open('csv.csv', newline='') as inp_csv:
reader = csv.reader(inp_csv)
for i, row in enumerate(reader):
# write something to check it reading
print("Reading Row " + str(i))
def main():
root = tk.Tk() # create a Tk root window
App = ReadIn(root)
root.geometry('400x300+760+450')
App.mainloop() # starts the mainloop
if __name__ == '__main__':
main()
The statement nf.join() in function handle_stuff() will block tkinter's main loop to show the progress bar window. Try modify handle_stuff() as below:
def handle_stuff(self):
nf = threading.Thread(target=self.import_csv)
nf.start()
self.Pbar()
#nf.join() # don't call join() as it will block tkinter's mainloop()
while nf.is_alive():
self.update() # update the progress bar window
self.popup.destroy()
I create a simple window with tkinter and start it as thread to keep the main program running next to the window. This is a shortened version of it:
import tkinter as tk
import threading
class mainWindow(threading.Thread):
def __init__(self, winWidth=500, winHeight=300):
threading.Thread.__init__(self)
self.winWidth = winWidth
self.winHeight = winHeight
# Save all drawn objects, to move or delete them later
self.bricks = []
self.start() #start thread
def run(self):
# parent object for all windows
self.master = tk.Tk()
self.master.protocol("WM_DELETE_WINDOW", self.callback)
self.show()
def callback(self):
self.master.quit()
# Initialize everything important
def show(self, tileSize=10):
# create main window
self.w = tk.Canvas(
self.master,
width=self.winWidth,
height=self.winHeight,
background="white")
self.w.pack()
# draw brick
color = "gray49"
posX = 200
posY = 100
self.bricks.append(self.w.create_rectangle(posX, posY, posX+20, posY+20, fill=color))
tk.mainloop()
def move_brick(self, x,y):
self.w.move(self.brick, x, y)
mainWindow = mainWindow()
mainWindow.move_brick(100,100)
When I run the shown code the window opens correctly, but when I try to move the rectangle with move_brick(...) I get this error:
AttributeError: 'mainWindow' object has no attribute 'w'
Why can't the object find my Canvas w?
You probably have a race condition, which is common for threaded applications. The main thread is likely calling move_brick before the worker thread has a chance to create the widget.
You can probably see this happening if you add print statements right before and after creating the widget, and in your move_brick function.
Even if you fix this, this code likely won't work because all tkinter code needs to run in a single thread. Creating the GUI in one thread and calling move_brick in another thread is not the right way to use tkinter.
i have tkinter class and some functions in it, (assume all other functions are present to initiate the GUI). what i have done i have started one self.function as a thread from other self.function and in threaded function upon error i want to use tkMessageBox.showerror('Some Error') but this does not work in threaded function and my program got stuck. msgbox is working in other function.
import threading
from Tkinter import *
import Pmw
import tkMessageBox
class tkinter_ui:
def __init__(self, title=''):
... assume all functions are present ...
def login(self, username, password)
if password == "":
tkMessageBox.showerror('Login Error', 'password required') # but on this msg box program become unresponsive why???
def initiateLogin(self)
tkMessageBox.showinfo('Thread', 'Started') #you see this msg box works
self.t = threading.Timer(1, self.login)
self.t.start()
Since I got stuck on the same problem and didn't find a proper, well explained solution, I'd like to share a basic strategy I came out with.
Note that this is not the only nor the best way to do threading with tkinter, but it's quite straightforward and should preserve your workflow if you designed your code without being aware of tkinter's thread-unsafetiness.
Why threads?
First of all, I chose to use threads seeing that blocking actions like os.popen, subprocess.call, time.sleep and the like would "freeze" the GUI until they run (of course this may not be your case since threads are useful by their own for many reasons and sometimes they are just needed).
This is how my code looked like before using threads:
from Tkinter import *
import tkMessageBox
from time import sleep
# Threadless version.
# Buttons will freeze the GUI while running (blocking) commands.
def button1():
sleep(2)
tkMessageBox.showinfo('title', 'button 1')
def button2():
sleep(2)
tkMessageBox.showinfo('title', 'button 2')
root = Tk()
frame = Frame(root)
frame.pack()
Frame(root).pack( side = BOTTOM )
Button(frame, command=button1, text="Button 1").pack( side = LEFT )
Button(frame, command=button2, text="Button 2").pack( side = LEFT )
root.mainloop()
Buggy threaded version
Then I turned the commands called by the buttons into threads. This way, the GUI would not freeze.
I thought it was ok, but on Windows this code leads the interpreter to crash irreparably due to the tkMessageBoxes called from threads other than the one in which the tkinter's root is running:
from Tkinter import *
import tkMessageBox
from time import sleep
import threading
# Buggy threads.
# WARNING: Tkinter commands are run into threads: this is not safe!!!
def button1():
sleep(2)
tkMessageBox.showinfo('title', 'button 1')
def button2():
sleep(2)
tkMessageBox.showinfo('title', 'button 2')
def start_thread(fun, a=(), k={}):
threading.Thread(target=fun, args=a, kwargs=k).start()
root = Tk()
frame = Frame(root)
frame.pack()
Frame(root).pack( side = BOTTOM )
Button(frame, command=lambda: start_thread(button1), text="Button 1").pack( side = LEFT)
Button(frame, command=lambda: start_thread(button2), text="Button 2").pack( side = LEFT )
root.mainloop()
Thread-safe version
When I discovered the thread-unsafetiness of tkinter, I wrote a small function tkloop that would run in the main thread each few milliseconds checking requests and executing requested (tkinter) functions on behalf of the threads that wish to run them.
The two keys here are the widget.after method that "registers a callback function that will be called after a given number of milliseconds" and a Queue to put and get requests.
This way, a thread can just put the tuple (function, args, kwargs) into the queue instead of calling the function, resulting in a unpainful change of the original code.
This is the final, thread-safe version:
from Tkinter import *
import tkMessageBox
from time import sleep
import threading
from Queue import Queue
# Thread-safe version.
# Tkinter functions are put into queue and called by tkloop in the main thread.
q = Queue()
def button1():
sleep(2)
q.put(( tkMessageBox.showinfo, ('title', 'button 1'), {} ))
def button2():
sleep(2)
q.put(( tkMessageBox.showinfo, ('title', 'button 2'), {} ))
def start_thread(fun, a=(), k={}):
threading.Thread(target=fun, args=a, kwargs=k).start()
def tkloop():
try:
while True:
f, a, k = q.get_nowait()
f(*a, **k)
except:
pass
root.after(100, tkloop)
root = Tk()
frame = Frame(root)
frame.pack()
Frame(root).pack( side = BOTTOM )
Button(frame, command=lambda: start_thread(button1), text="Button 1").pack( side = LEFT)
Button(frame, command=lambda: start_thread(button2), text="Button 2").pack( side = LEFT )
tkloop() # tkloop is launched here
root.mainloop()
Edit: two-way communication: if your threads need to get informations from the main (e.g. return values from tkinter functions) you can edit the interface of tkloop adding a queue for the return values. Here's an example based on the code above:
def button1():
q1 = Queue()
sleep(2)
q.put(( tkMessageBox.askokcancel, ('title', 'question'), {}, q1 ))
response = 'user said ' + 'OK' if q1.get() else 'CANCEL'
q.put(( tkMessageBox.showinfo, ('title', response), {}, None ))
# ...
def tkloop():
try:
while True:
f, a, k, qr = q.get_nowait()
r = f(*a, **k)
if qr: qr.put(r)
except:
pass
root.after(100, tkloop)
tkinter is not thread safe -- you can't reliably call any tkinter functions from any thread other than the one in which you initialized tkinter.
If you want your other thread to block until you get response (e,g: you want to ask a question and wait for the answer) you can use this function:
def runInGuiThreadAndReturnValue(self, fun, *args, **kwargs):
def runInGui(fun, ret, args, kwargs):
ret.append(fun( *args, **kwargs))
ret = []
sleeptime = kwargs.pop('sleeptime', 0.5)
self.after(0, runInGui, fun, ret, args, kwargs)
while not ret:
time.sleep(sleeptime)
return ret[0]