I have a Tkinter GUI application that I need to enter text in. I cannot assume that the application will have focus, so I implemented pyHook, keylogger-style.
When the GUI window does not have focus, text entry works just fine and the StringVar updates correctly. When the GUI window does have focus and I try to enter text, the whole thing crashes.
i.e., if I click on the console window or anything else after launching the program, text entry works. If I try entering text immediately (the GUI starts with focus), or I refocus the window at any point and enter text, it crashes.
What's going on?
Below is a minimal complete verifiable example to demonstrate what I mean:
from Tkinter import *
import threading
import time
try:
import pythoncom, pyHook
except ImportError:
print 'The pythoncom or pyHook modules are not installed.'
# main gui box
class TestingGUI:
def __init__(self, root):
self.root = root
self.root.title('TestingGUI')
self.search = StringVar()
self.searchbox = Label(root, textvariable=self.search)
self.searchbox.grid()
def ButtonPress(self, scancode, ascii):
self.search.set(ascii)
root = Tk()
TestingGUI = TestingGUI(root)
def keypressed(event):
key = chr(event.Ascii)
threading.Thread(target=TestingGUI.ButtonPress, args=(event.ScanCode,key)).start()
return True
def startlogger():
obj = pyHook.HookManager()
obj.KeyDown = keypressed
obj.HookKeyboard()
pythoncom.PumpMessages()
# need this to run at the same time
logger = threading.Thread(target=startlogger)
# quits on main program exit
logger.daemon = True
logger.start()
# main gui loop
root.mainloop()
I modified the source code given in the question (and the other one) so that the pyHook
related callback function sends keyboard event related data to a
queue. The way the GUI object is notified about the event may look
needlessly complicated. Trying to call root.event_generate in
keypressed seemed to hang. Also the set method of
threading.Event seemed to cause trouble when called in
keypressed.
The context where keypressed is called, is probably behind the
trouble.
from Tkinter import *
import threading
import pythoncom, pyHook
from multiprocessing import Pipe
import Queue
import functools
class TestingGUI:
def __init__(self, root, queue, quitfun):
self.root = root
self.root.title('TestingGUI')
self.queue = queue
self.quitfun = quitfun
self.button = Button(root, text="Withdraw", command=self.hide)
self.button.grid()
self.search = StringVar()
self.searchbox = Label(root, textvariable=self.search)
self.searchbox.grid()
self.root.bind('<<pyHookKeyDown>>', self.on_pyhook)
self.root.protocol("WM_DELETE_WINDOW", self.on_quit)
self.hiding = False
def hide(self):
if not self.hiding:
print 'hiding'
self.root.withdraw()
# instead of time.sleep + self.root.deiconify()
self.root.after(2000, self.unhide)
self.hiding = True
def unhide(self):
self.root.deiconify()
self.hiding = False
def on_quit(self):
self.quitfun()
self.root.destroy()
def on_pyhook(self, event):
if not queue.empty():
scancode, ascii = queue.get()
print scancode, ascii
if scancode == 82:
self.hide()
self.search.set(ascii)
root = Tk()
pread, pwrite = Pipe(duplex=False)
queue = Queue.Queue()
def quitfun():
pwrite.send('quit')
TestingGUI = TestingGUI(root, queue, quitfun)
def hook_loop(root, pipe):
while 1:
msg = pipe.recv()
if type(msg) is str and msg == 'quit':
print 'exiting hook_loop'
break
root.event_generate('<<pyHookKeyDown>>', when='tail')
# functools.partial puts arguments in this order
def keypressed(pipe, queue, event):
queue.put((event.ScanCode, chr(event.Ascii)))
pipe.send(1)
return True
t = threading.Thread(target=hook_loop, args=(root, pread))
t.start()
hm = pyHook.HookManager()
hm.HookKeyboard()
hm.KeyDown = functools.partial(keypressed, pwrite, queue)
try:
root.mainloop()
except KeyboardInterrupt:
quit_event.set()
Related
I have started a small project and am currently stuck on a problem.
I have written a small GUI in Tkinter which opens another thread when the button is pressed in order to read the ID of an RFID chip. This is then returned to the GUI via a queue object and displayed there by the label. This works so far.
Now I would like to implement that the thread is terminated when no RFID chip has been detected for 10 seconds or the thread still exists after this time. I have tried to interrupt this with an event as follows but without success.
Does anyone have an idea how this could work?
Thanks already.
#!/usr/bin/env python3
import tkinter as tk
import queue
import threading
import time
import RPi.GPIO as GPIO
from mfrc522 import SimpleMFRC522
class GUI:
def __init__(self, master):
self.master = master
self.test_button = tk.Button(self.master, command=self.tb_click)
self.test_button.configure(text="Start", background="Grey", padx=50)
self.test_button.pack(side='top')
self.test_label = tk.Label(self.master, text="test")
self.test_label.pack(side='bottom')
def tb_click(self):
self.stop = threading.Event()
self.queue = queue.Queue( )
thread = Rfid(self.queue, self.stop)
thread.start()
self.master.after(100, self.process_queue)
self.master.after(10000, self.terminate)
def process_queue(self):
try:
msg = self.queue.get(0)
#print(str(msg))
self.test_label.configure(text=str(msg))
except queue.Empty:
self.master.after(100, self.process_queue)
def terminate(self):
if thread.is_alive():
self.stop.set()
class Rfid(threading.Thread):
def __init__(self, queue, stop):
threading.Thread.__init__(self)
self.queue = queue
self.stop = stop
def run(self):
while True:
if self.stop.isSet():
print("abgeschalten")
break
reader = SimpleMFRC522()
id, text = reader.read()
print(str(id))
self.queue.put(str(text))
GPIO.cleanup()
root = tk.Tk()
root.title("Test")
main_ui = GUI(root)
root.mainloop()
I'm writing a Python programme which listens for RFID input and only runs if a valid token is presented. The programme also has a GUI which I'm wanting to build using TkInter.
Both parts of the puzzle work fine in isolation, however as it stands I seem to be able to choose one or the other - but not both! I can draw my TkInter window fine, however if I call the function to start listening for the RFID input then whilst that bit runs OK and works... there's no GUI.
Code is below. You can see my debugging efforts so far with my printouts to the terminal...
#!/usr/bin/env python3
import sys
import MySQLdb
if sys.version_info[0] == 2:
from Tkinter import *
import Tkinter as ttk
else:
from tkinter import *
import tkinter as ttk
class Fullscreen_Window:
def __init__(self):
self.tk = Tk()
self.frame = Frame(self.tk)
self.frame.pack()
ttk.Button(self.tk, text="hello world").pack()
self.tk.attributes('-zoomed', True)
self.state = False
self.tk.bind("<F11>", self.toggle_fullscreen)
self.tk.bind("<Escape>", self.end_fullscreen)
print("init running")
self.listen_rfid() # Commenting this out makes the GUI appear, uncommenting means no GUI :(
def toggle_fullscreen(self, event=None):
self.state = not self.state # Just toggling the boolean
self.tk.attributes("-fullscreen", self.state)
print("Toggling")
return "break"
def end_fullscreen(self, event=None):
self.state = False
self.tk.attributes("-fullscreen", False)
return "break"
def listen_rfid(self):
print("Main loop running")
dbHost = 'localhost'
dbName = 'python'
dbUser = 'python'
dbPass = 'PASSWORD'
dbConnection = MySQLdb.connect(host=dbHost, user=dbUser, passwd=dbPass, db=dbName)
cur = dbConnection.cursor(MySQLdb.cursors.DictCursor)
with open('/dev/stdin', 'r') as tty:
while True:
RFID_input = tty.readline().rstrip()
cur.execute("SELECT * FROM access_list WHERE rfid_code = '%s'" % (RFID_input))
if cur.rowcount != 1:
print("ACCESS DENIED")
else:
user_info = cur.fetchone()
print("Welcome %s!!" % (user_info['name']))
tty.close()
listen_rfid()
if __name__ == '__main__':
w = Fullscreen_Window()
w.tk.mainloop()
I'm sure it's something really simple but as I'm a Python/TkInter n00b it's beaten me and I'm all done Googling. Any help gratefully received :)
Tkinter (and all GUIs) has an infinite loop called the mainloop that keeps the GUI active and responsive. When you make another infinite loop (while True) you block Tkinter's mainloop; and the GUI fails. You need to either put your loop in a separate thread or use Tkinter's mainloop to do your work. Since you are using a blocking readline, the thread is the best way to go. As a guess, replace your call with this:
from threading import Thread
t = Thread(target=self.listen_rfid)
t.daemon = True # this line tells the thread to quit if the GUI (master thread) quits.
t.start()
Edit: BTW, your imports are very bad. "ttk" is a subset of tkinter, not an alias, the alias "tk" is usually used for tkinter, and wildcard imports are bad and should be avoided. This is how your tkinter imports should look:
try:
# python 2
import Tkinter as tk
import ttk
except ImportError:
# python 3
import tkinter as tk
from tkinter import ttk
And then you use the appropriate prefix:
self.tk = tk.Tk()
self.frame = tk.Frame(self.tk)
You should run listen_rfid using after. The problem is that listen_rfid as you have written it will run forever meaning that mainloop never starts. If you do this:
#!/usr/bin/env python3
import sys
import select
import MySQLdb
if sys.version_info[0] == 2:
from Tkinter import *
import Tkinter as ttk
else:
from tkinter import *
import tkinter as ttk
class Fullscreen_Window:
def __init__(self):
self.tk = Tk()
self.frame = Frame(self.tk)
self.frame.pack()
ttk.Button(self.tk, text="hello world").pack()
self.tk.attributes('-zoomed', True)
self.state = False
self.tk.bind("<F11>", self.toggle_fullscreen)
self.tk.bind("<Escape>", self.end_fullscreen)
print("init running")
# Schedule self.listen_rfid to run after the mainloop starts
self.tk.after(0, self.listen_rfid)
def toggle_fullscreen(self, event=None):
self.state = not self.state # Just toggling the boolean
self.tk.attributes("-fullscreen", self.state)
print("Toggling")
return "break"
def end_fullscreen(self, event=None):
self.state = False
self.tk.attributes("-fullscreen", False)
return "break"
def listen_rfid(self):
print("Main loop running")
dbHost = 'localhost'
dbName = 'python'
dbUser = 'python'
dbPass = 'PASSWORD'
dbConnection = MySQLdb.connect(host=dbHost, user=dbUser, passwd=dbPass, db=dbName)
cur = dbConnection.cursor(MySQLdb.cursors.DictCursor)
# readline is blocking so check that there is input
# before attempting to read it.
r, w, x = select.select([sys.stdin], [], [], 0)
if r:
# There is available input, so read a line.
RFID_input = sys.stdin.readline().rstrip()
cur.execute("SELECT * FROM access_list WHERE rfid_code = '%s'" % (RFID_input))
if cur.rowcount != 1:
print("ACCESS DENIED")
else:
user_info = cur.fetchone()
print("Welcome %s!!" % (user_info['name']))
# keep running every 500 milliseconds for as long as
# the mainloop is active.
self.tk.after(500, self.listen_rfid)
if __name__ == '__main__':
w = Fullscreen_Window()
w.tk.mainloop()
it will check every half second whether there is some input on the command line and process it.
I am trying to display a count variable from a background task in the main task which is my tkinter GUI. Why? I want to display that the long time taking background task is performing and later use this count variable to visualize it with a progress bar.
My problem is, that even when using a Queue, I am not able to display the count variable. Maybe I've got problems in understanding python and its behaviour with objects and/or threads.
import threading
import time
import Queue
import Tkinter as Tk
import Tkconstants as TkConst
from ScrolledText import ScrolledText
from tkFont import Font
import loop_simulation as loop
def on_after_elapsed():
while True:
try:
v = dataQ.get(timeout=0.1)
except:
break
scrText.insert(TkConst.END, str(v)) # "value=%d\n" % v
scrText.see(TkConst.END)
scrText.update()
top.after(100, on_after_elapsed)
def thread_proc1():
x = -1
dataQ.put(x)
x = loop.loop_simulation().start_counting()
# th_proc = threading.Thread(target=x.start_counting())
# th_proc.start()
for i in range(5):
for j in range(20):
dataQ.put(x.get_i())
time.sleep(0.1)
# x += 1
time.sleep(0.5)
dataQ.put(x.get_i())
top = Tk.Tk()
dataQ = Queue.Queue(maxsize=0)
f = Font(family='Courier New', size=12)
scrText = ScrolledText(master=top, height=20, width=120, font=f)
scrText.pack(fill=TkConst.BOTH, side=TkConst.LEFT, padx=15, pady=15, expand=True)
th = threading.Thread(target=thread_proc1)
th.start()
top.after(100, on_after_elapsed)
top.mainloop()
th.join()
In thread_proc1() I want to get the value of the counter of background task. This is the background task:
import time
class loop_simulation:
def __init__(self):
self.j = 0
# self.start_counting()
def start_counting(self):
for i in range(0, 1000000):
self.j = i
time.sleep(0.5)
def get_i(self):
return str(self.j)
The reason the count variable isn't being displayed is due to the
x = loop.loop_simulation().start_counting()
statement in thread_proc1(). This creates a loop_simulation instance and calls its start_counting() method. However, other than already inserting a -1 into the dataQ, thread_proc1() doesn't do anything else until start_counting() returns, which won't be for a long time (500K seconds).
Meanwhile, the rest of your script is running and displaying only that initial -1 that was put in.
Also note that if start_counting() ever did return, its value of None is going to be assigned to x which later code attempts to use with: x.get_i().
Below is reworking of your code that fixes these issues and also follows the PEP 8 - Style Guide for Python Code more closely. To avoid the main problem of calling start_counting(), I changed your loop_simulation class into a subclass of threading.Thread and renamed it LoopSimulation, and create an instance of it in thread_proc1, so there are now two background threads in addition to the main one handling the tkinter-based GUI.
import loop_simulation as loop
from ScrolledText import ScrolledText
import threading
import Tkinter as Tk
import Tkconstants as TkConst
from tkFont import Font
import time
import Queue
def on_after_elapsed():
# removes all data currently in the queue and displays it in the text box
while True:
try:
v = dataQ.get_nowait()
scrText.insert(TkConst.END, str(v)+'\n')
scrText.see(TkConst.END)
except Queue.Empty:
top.after(100, on_after_elapsed)
break
def thread_proc1():
dataQ.put(-1)
ls = loop.LoopSimulation() # slow background task Thread
ls.start_counting()
while ls.is_alive(): # background task still running?
for i in range(5):
for j in range(20):
dataQ.put(ls.get_i())
time.sleep(0.1)
time.sleep(0.5)
dataQ.put('background task finished')
top = Tk.Tk()
dataQ = Queue.Queue(maxsize=0)
font = Font(family='Courier New', size=12)
scrText = ScrolledText(top, height=20, width=120, font=font)
scrText.pack(fill=TkConst.BOTH, side=TkConst.LEFT, padx=15, pady=15,
expand=TkConst.YES)
th = threading.Thread(target=thread_proc1)
th.daemon = True # OK for main to exit even if thread is still running
th.start()
top.after(100, on_after_elapsed)
top.mainloop()
loop_simulation.py module:
import threading
import time
class LoopSimulation(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True # OK for main to exit even if instance still running
self.lock = threading.Lock()
self.j = 0
start_counting = threading.Thread.start # an alias for starting thread
def run(self):
for i in range(1000000):
with self.lock:
self.j = i
time.sleep(0.5)
def get_i(self):
with self.lock:
return self.j
I want some basics on the problem of making some sort of "Stop" button that in my case terminates the series of beeps:
from tkinter import *
import winsound
from random import randint
class App(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.widgets()
def widgets(self):
self.beep = Button(self, text = "Beep", command = play_beep)
self.beep.pack()
self.stop = Button(self, text = "Stop", command = stop_beep)
self.stop.pack()
go_on = True
def play_beep():
count = 10
while go_on == True and count != 0:
winsound.Beep(randint(100, 2500), 200)
count -= 1
def stop_beep():
go_on = False
root = Tk()
app = App(root)
root.mainloop()
When I press the "Beep" button it gets stuck as well as all the GUI until the beeps end. Could anyone tell me how to fix it?
I don't use TKinter, but I believe your button press is not creating a separate thread or process. The reason why your button gets stuck is because your play_beep loop is blocking your GUI execution loop. So we use threading. The thread executes at the same time as your GUI, so you can basically do two things at once (listen for GUI events and play beep noises).
import threading
class App(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.is_playing = False
self.pack()
self.widgets()
def widgets(self):
self.beep = Button(self, text = "Beep", command = self.play_beep)
self.beep.pack()
self.stop = Button(self, text = "Stop", command = self.stop_beep)
self.stop.pack()
def play_beep(self):
self.is_running = True
self.beep_th = threading.Thread(target=self.run)
self.beep_th.start()
def run(self):
count = 10
while self.is_running == True and count != 0:
winsound.Beep(randint(100, 2500), 200)
count -= 1
def stop_beep(self):
try:
self.is_running = False
self.beep_th.join(0)
self.beep_th = None
except (AttributeError, RuntimeError): # beep thread could be None
pass
def closeEvent(self, event): # This is a pyside method look for a TKinter equivalent.
"""When you close the App clean up the thread and close the thread properly."""
self.stop_beep()
super().closeEvent(event)
First off, your question has nothing to do about threads or processes. Tkinter is single-threaded.
If you want to run some function periodically in a tkinter program, you must give the event loop a chance to process events. The typical solution is to do it like this:
def play_beep(count=10):
if go_on and count != 0:
winsound.Beep(randint(100, 2500), 200)
root.after(1000, play_beep, count=1)
This will cause the beep to play every second (1000ms) for ten iterations. In between each call, the event loop will have a chance to process other events.
Now, if the code you are running takes a long time, you're going to have to run that code in a separate thread or process. I know nothing about winsound.Beep so I don't know if that's necessary or not.
Second, to be able to interrupt it, you need to make go_on global, otherwise you're simply setting a local variable that never gets used.
def stop_beek():
global go_on
go_on = False
I'm planning to write a small GUI around a numerical simulation which is why I'm now playing around with Tkinter. Simulations should be launched from GUI in seperate processes. To play around a bit I defined a function random_process that generates pairs of randn numbers (this should later be a real simulation process). As that function is meant to be launched in a seperate process, two mp.Event objects and one mp.Pipe object are passed as parameters.
The main application can use one event to request accumulated data from the process, another event is used as a "Poison Pill" to kill the "simulation" process. A pipe is then used to pass the data.
In the main application, I use Tkinter's after-function to regularly check if new data has arrived and then plot it. Starting and stopping the "simulation process" is done by buttons in the main app, the same goes for requesting data from it.
At least that was the idea, in practice the program doesn't play nice. When I click on the "go!" button that is meant to launch the simulation process, a second Tkinter window appears, identical to the main one. I don't have the slightest clue why that happens. The communication with the process doesn't work neither, no data seems to be send. When googling for a solution, I found a working example of a Tkinter program launching processes and talking to them, but I didn't find out what makes it not work in my case. Has anybody got a clue?
BTW, the OS is Windows-7.
Cheers,
Jan
import matplotlib
matplotlib.use('TkAgg')
import time
import multiprocessing as mp
import Tkinter as Tk
import numpy.random as npr
import matplotlib.figure
import matplotlib.backends.backend_tkagg as tkagg
def random_process(delay, data_request, data_in, poison):
while not poison.is_set():
time.sleep(delay)
print("Generating pair of random numbers...")
x,y = npr.randn(), npr.randn()
try:
random_process.l.append((x,y))
except:
random_process.l = [(x,y)]
if data_request.is_set():
data_request.clear()
try:
ll = len(random_process.l)
if ll > 0:
print("Sending %d pairs to main program.." % ll)
data_in.send(random_process.l)
random_process.l = []
except:
print("data requested, but none there.")
# when poison event is set, clear it:
poison.clear()
class GuiInterfaceApp:
def __init__(self, parent):
self.myParent = parent
self.previewplot_container = Tk.Frame(self.myParent)
self.f = matplotlib.figure.Figure()
self.ax = self.f.add_subplot(111)
self.preview_canvas = tkagg.FigureCanvasTkAgg(self.f, master=self.previewplot_container)
self.preview_canvas.show()
self.button_container = Tk.Frame(self.myParent)
self.hellobutton = Tk.Button(self.button_container, text="hello!")
self.hellobutton.config(command = self.printhello)
self.startbutton = Tk.Button(self.button_container, text="go!")
self.startbutton.config(command=self.run_simulation)
self.plotbutton = Tk.Button(self.button_container, text="show!")
self.plotbutton.config(command=self.request_data)
self.stopbutton = Tk.Button(self.button_container, text="stop.")
self.stopbutton.config(command=self.stop_simulation)
self.quitbutton = Tk.Button(self.button_container, text="get me outta here!")
self.quitbutton.config(command=self.quit_program)
self.previewplot_container.pack(side = Tk.TOP)
self.preview_canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
self.button_container.pack(side = Tk.BOTTOM)
self.hellobutton.pack(side = Tk.LEFT)
self.startbutton.pack(side = Tk.LEFT)
self.plotbutton.pack(side = Tk.LEFT)
self.stopbutton.pack(side = Tk.LEFT)
self.quitbutton.pack(side = Tk.LEFT)
self.simulation_running = False
self.datarequest = mp.Event()
self.DataIn, self.DataOut = mp.Pipe()
self.PoisonEvent = mp.Event()
self.p = mp.Process(target = random_process, args=(1.0, self.datarequest, self.DataIn, self.PoisonEvent))
self.l = [] # list of received pairs to plot
self.mytask_time = 100 # delay in ms between calls to self.mytask
def printhello(self):
print("hello!")
def run_simulation(self):
print("startbutton pressed.")
if not self.simulation_running:
print("starting simulation...")
self.p.start()
self.simulation_running = True # attention: no error checking
def stop_simulation(self):
print("stop button pressed.")
if self.simulation_running:
print("Sending poison pill to simulation process..")
self.PoisonEvent.set()
self.simulation_running = False
# todo: wait a short amount of time and check if simu stopped.
def request_data(self):
print("plotbutton pressed.")
if self.simulation_running:
print("requesting data from simulation process")
self.datarequest.set()
def update_plot(self):
print("update_plot called.")
if len(self.l) > 0:
print("there is data to plot.")
while len(self.l) > 0:
x,y = self.l.pop()
print("plotting point (%.2f, %.2f)" % (x,y))
self.ax.plot([x], [y], '.', color='blue')
print("drawing the hole thing..")
self.ax.draw()
else:
print("nothing to draw")
def quit_program(self):
print("quitbutton pressed.")
if self.simulation_running:
print("sending poison pill to simulation process..")
self.PoisonEvent.set()
print("quitting mainloop..")
self.myParent.quit()
print("destroying root window..")
self.myParent.destroy()
def receive_data(self):
if self.DataOut.poll():
print("receiving data..")
data = self.DataOut.recv()
self.l.append(data)
self.update_plot()
def my_tasks(self):
self.receive_data()
self.myParent.after(self.mytask_time, self.my_tasks)
return
root = Tk.Tk()
myGuiInterfaceApp = GuiInterfaceApp(root)
root.after(100, myGuiInterfaceApp.my_tasks)
root.mainloop()
Try hiding your main logic behind a test for whether the code is being run or imported.
if __name__ == "__main__":
root = Tk.Tk()
...