How to run multiple threads in tkinter - python

Let's say if we write something like this:
import threading
Button(root, command=threading.Thread(target=func1).start)
Now if we click the button once then it will be fine but we try to click the button again then a error comes "Thread can only be executed once".
So, how to avoid this

Edited Answer::
As you clarified it in comments, you can redefine the Button every time it is clicked allowing it to accept multiple clicks and thus creating multiple threads as required.
You could do that inside the target func1() or callback function for threading.Thread object.
A working example example would be like this:
import tkinter as tk
import threading
def func1():
theButton.configure(command=threading.Thread(target=func1).start)
print('Do everything else here')
root = tk.Tk()
theButton = tk.Button(root, text='Start', command=threading.Thread(target=func1).start)
theButton.pack()
root.mainloop()
Edit: Thanks to CoolCloud for suggesting a better way to configure the Button inside callback func1().

It is because you create one instance of threading.Thread() and pass its start method to command option of the button. You should create new instance whenever the button is clicked by using lambda:
import tkinter as tk
import threading
root = tk.Tk()
def func1():
print('Hello')
tk.Button(root, text='Go', command=lambda: threading.Thread(target=func1).start()).pack()
root.mainloop()

Related

Print the output of imported functions in Tkinter widget with Python

My GUI app has two files: gui.py that contains all the Tkinter objects and controller.py contains the logic. The logic is one main function def automation(): that nests several other functions. The app is very simple it's only one button that calls automation().
I would like to add the print statements and errors that appear in the terminal in the GUI widget so that the user can see what's going on. I can't find a way to do that for imported modules.
gui.py
import tkinter as tk
from tkinter import *
from controller import automation
root = tk.Tk()
frame_button = tk.Frame(root)
button = Button(frame_button, text="Ship", command=lambda:automation())
lower_frame = tk.Frame(root)
terminal = tk.Label(lower_frame)
frame_button.place()
button.place()
lower_frame.place()
terminal.place()
controller.py
def automation():
def folder_cleaner():
print('Folders cleaned')
def dispatch():
print('Dispatch done')
def ship():
print('Shipment successful')
def process():
folder_cleaner()
dispatch()
ship()
process()
This is very simplified but each function has many different kinds of outputs. How can I redirect all of them to gui.py and inside the terminal widget?
To show the output in the UI, I've added an output function in gui.py. So the function can be used in controller.py, I've added a parameter outputFunc when calling automation. This can then be used instead of print to display the strings in the UI.
gui.py
import tkinter as tk
from tkinter import *
from controller import automation
def output(text):
terminal.insert("end", text + "\n")
root = tk.Tk()
frame_button = tk.Frame(root)
button = Button(frame_button, text="Ship", command=lambda:automation(output))
lower_frame = tk.Frame(root)
#Changed to text widget as it is more ideal for this purpose
terminal = tk.Text(lower_frame)
#Changed place to pack so widgets actually display
frame_button.pack()
button.pack()
lower_frame.pack()
terminal.pack()
controller.py
def automation(outputFunc):
def folder_cleaner():
outputFunc('Folders cleaned')
def dispatch():
outputFunc('Dispatch done')
def ship():
outputFunc('Shipment successful')
def process():
folder_cleaner()
dispatch()
ship()
process()
Here is one approach to Your issue (this works especially well if Your automation runs in a loop or basically is not as fast, it will make sure to update when any change happens):
from tkinter import Tk, Button, Label, Frame
from _thread import start_new_thread
from time import sleep
values = {'random_values': []}
def automation():
for i in range(10):
values['random_values'].append(i)
sleep(3)
def watch_values():
compare = None
while True:
value = str(values['random_values'])
if compare != value:
label.config(text=values['random_values'])
compare = value
root = Tk()
Button(root, text='start', command=lambda: start_new_thread(automation, ())).pack()
label = Label(root)
label.pack()
start_new_thread(watch_values, ())
root.mainloop()
So basically first it starts a thread that watches the values dictionary (it is important for it to interact with tkinter only when there are changes tho because otherwise it will cause issues with tkinter GUI so that is why there is compare).
Whenever there is a change in that particular variable it sets the text of the label as that variable (variable being values['random_values']).
Then there is the automation function. I suggest that You put everything it returns in a dictionary that will be watched by the other thread. You also need to thread Your automation function so that it can work in parallel. (And You can obviously import that function from another file, just add it to the threads, also it may seem weird but those empty tuples there are necessary since that is an argument (in those tuples it is also possible to put the function argument if You have those but in this case there are none))
And You only need one watcher function, just add more compares and checks to that loop and so on (mentioned in case You wanted to create such a function for every key in the dictionary which is completely unnecessary)

How do I add a keyboard shortcut to a function in tkinter

I did some research in Tkinter and found the root.bind('<Control-Key-whatever key', function).
I wanted to add this to an application I was making.
I made a button and I want it to perform a function when I click a certain key combination.
Here is my code:
from tkinter import *
root = Tk()
root.geometry("600x600")
def printFunction():
print("Hello World")
root.bind('<Control-Key-v>', printFunction)
button = Button(root, text="click here", command=printFunction)
button.pack()
root.mainloop()
So when I click the button the function should perform, and when I click Ctrl+v the function should perform. The button works fine, but the key combination does not work. How do I fix this?
It should be something like
root.bind('<Control-v>', printFunction)
But keep in mind, this will again throw another error, because you have to pass event as a parameter to the function.
def printFunction(event=None):
print("Hello World")
Why event=None? This is because your button is also using the same function as a command but without any arguments passed to it on declaration. So to nullify it, this is a workaround.
Alternatively, your could also pass something like *args instead of event:
def printFunction(*args):
print("Hello World")
Hope you understood better.
Cheers
You can use
from tkinter import *
root = Tk()
root.geometry("600x600")
def printFunction(event):
print("Hello World")
button = Button(root, text="click here", command=lambda:printFunction(None))
root.bind('<Control-v>', printFunction)
button.pack()
root.mainloop()
Argument event is needed for the concerned function
Event name should be converted to <Control-v>
Don't forget to add lambda just before the function name call from
the button in order to call without issue by any means.

Combining Tkinter mainloop with another event listener

I am trying to build a program that listens for certain key combinations and then shows information to the user in a Tkinter window. To do this, I'm using a keylogger like so (simplified for this example):
from pyHook import HookManager
from pythoncom import PumpMessages
import Tkinter as tk
def on_keyboard_event(event):
label.config(text=event.Key)
root.update()
return True
hm = HookManager()
hm.KeyDown = on_keyboard_event
hm.HookKeyboard()
root = tk.Tk()
label = tk.Label(root, text='Hello world')
label.pack()
PumpMessages()
As expected, the window pops up and shows the user what key they pressed. However, I would like to integrate functionality to show other messages by interacting with the Tkinter window, such as by pressing a button. However, it seems I need Tkinter's mainloop to do this, which I can't figure out how to run alongside PumpMessages(), since it also halts the code similar to mainloop().
I tried running root.mainloop() in a root.after(), and I tried recreating root.mainloop like so:
def mainloop():
root.update()
root.after(50, mainloop)
and then running it right before PumpMessages, but neither of these solutions worked. It also doesn't seem like you can run PumpMessages or root.mainloop in a thread, though I could just not be doing it right. If this is not possible with Tkinter, is there an alternate Python GUI I could use that would make it possible?
You don't need to create a function to use mainloop() so just simply place the mainloop() at the bottom of your code. If you want a delay on it, use root.after(milliseconds, function)
Also, remember to put mainloop() before PumpMessages()
e.g.
def mainloopfunction():
mainloop()
root.after(5000, mainloopfunction)
Hope I could help!

tkinter.Button retains depressed appearance after exiting event handling

Trying this again...I have a Python programmed GUI in which the pressed Button retains a depressed look after the event handler exits. The event handler made use of a messagebox. Normally, this does not happen. Here is an example that recreates the problem:
import tkinter as tk
from tkinter import messagebox
# post a message
def post_message(event):
messagebox.showinfo("Sample Messgebox", "close this and look at button")
root = tk.Tk()
b = tk.Button(root, text="Press Me")
b.bind("<Button-1>", func=post_message)
b.pack()
root.mainloop()
When you use the blind with the event Button-1, you aren't using the main event of the button. You can active the main event of the button with the argument command.
import tkinter as tk
from tkinter import messagebox
def post_message():
messagebox.showinfo("Sample Messgebox", "close this and look at button")
root = tk.Tk()
b = tk.Button(root, text="Press Me", command=post_message)
b.pack()
root.mainloop()
While I'm not sure why your code isn't working properly, since I'm fairly new to Py, I managed to rewrite it to work with minimal changes.
Solution 1
import tkinter as tk
from tkinter import messagebox
# post a message
def post_message():
messagebox.showinfo("Sample Messgebox", "close this and look at button")
root = tk.Tk()
b = tk.Button(root, text="Press Me", command=post_message)
b.pack()
root.mainloop()
What I changed:
no more bind() as this caused problem, instead the function is called by adding command= option while declaring Button object,
also notice that command option doesn't provide function called with event parameter, so this had to be removed or else errors would occur.
Another workaround, this time it works with bind() just fine!
Solution 2
import tkinter as tk
from tkinter import messagebox
# post a message
def post_message(event):
root.after(0, lambda: messagebox.showinfo\
("Sample Messgebox", "close this and look at button"))
root = tk.Tk()
b = tk.Button(root, text="Press Me")
b.bind("<Button-1>", post_message)
b.pack()
root.mainloop()
I used master.after(time_in_ms, callback_func) to tell the program that it should run a given func after the given time, here 0ms so ASAP.
Why is that lambda inside after? Lambda is a dynamic, not-named function. After takes a reference to the function you want called, so you can't directly give it parameters.
To do so, like in this example, set up a lambda that will be refrenced.
When it finally gets called, that lambda func will then call the actual function you wanted to call giving it the parameters it needs.
If you don't know yet how lambdas work, I know you're confused right now, so read more on them here, they're super-useful: Lambdas explained
For great source of info on tkinter, please visit effbot.org Events and Bindings

Intercept Tkinter "Exit" command?

I'm writing a client-server program in Python with Tkinter. I need the server to keep track of the connected clients. For this, I would like to have the client send an automated message to the server after the exit button(the standard "X" in the corner) is clicked. How can I know when the user is exiting the program?
You want to use the wm_protocol method of the toplevel window. Specifically, you are interested in the WM_DELETE_WINDOW protocol. If you use that method, it allows you to register a callback which is called when the window is being destroyed.
Usage:
root.protocol("WM_DELETE_WINDOW", app.on_delete)
You can use python atexit module.
For example:
import atexit
def doSomethingOnExit():
pass
atexit.register(doSomethingOnExit)
In my case, the following code didn't work:
root.protocol("WM_DELETE_WINDOW", app.on_delete) # doesn't work
However, it worked using this form:
root.wm_protocol ("WM_DELETE_WINDOW", app.on_delete) # does work
FWIW: It is also possible to assign a widget-specific behavior.
If you want an action to occur when a specific widget is destroyed, you may consider overriding the destroy() method. See the following example:
class MyButton(Tkinter.Button):
def destroy(self):
print "Yo!"
Tkinter.Button.destroy(self)
root = Tkinter.Tk()
f = Tkinter.Frame(root)
b1 = MyButton(f, text="Do nothing")
b1.pack()
f.pack()
b2 = Tkinter.Button(root, text="f.destroy", command=f.destroy)
b2.pack()
root.mainloop()
When the button 'b2' is pressed, the frame 'f' is destroyed, with the child 'b1' and "Yo!" is printed.
I posted the same answer on this topic.
Don't forget to use a lambda like this:
class App:
def run(self):
self.root.protocol("WM_DELETE_WINDOW", lambda: self.quit())
def quit(self):
self.root.destroy()

Categories