0

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()
heidi
  • 655
  • 6
  • 20
  • 1
    1. If you want to respond to users, use comments; it is fortunate that I came back and looked. 2. If you want to avoid this happening, spend more time writing a descriptive title rather than *"oddness"*, which helps nobody. That said, I apologise for confusing the two. – jonrsharpe Jun 19 '16 at 16:55
  • 1
    1. I wasn't aware that this notified you, seeing as you hadn't commented or otherwise interacted with my post. 2. Reading only the second paragraph would have shown that they are different questions, or were you basing your duplicate purely based on the title? I'm using a vague term like oddness because I don't know what's going on. – heidi Jun 19 '16 at 16:59
  • Threads and tkinter don't mix well, see for example http://stackoverflow.com/a/10556698/5781248 – J.J. Hakala Jun 19 '16 at 19:29
  • I know there are existing issues, however, you'll notice that `mainloop()` *is* running in the main thread. Additionally, this whole project works just fine in linux when I use X's record context. This is definitely a pyHook-related problem. – heidi Jun 19 '16 at 22:42

1 Answers1

2

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()
J.J. Hakala
  • 6,136
  • 6
  • 27
  • 61
  • Thank you! This worked, somehow. I was doing the exact same thing as in the OP on linux and it worked perfectly, so I assumed it would work on windows too. One more thing: I notice you didn't use PumpMessages - I thought that was necessary? – heidi Jun 21 '16 at 13:12
  • 1
    @heidi tkinter mainloop calls [PeekMessage](https://msdn.microsoft.com/en-us/library/windows/desktop/ms644943(v=vs.85).aspx) at some point which is probably enough to trigger the hooks installed by pyHook, as in [answer](http://stackoverflow.com/a/7460728) – J.J. Hakala Jun 22 '16 at 20:00