pythontkinterpyhookpythoncom

Tkinter text entry with pyHook hangs GUI window


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()

Solution

  • 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()