Текстовая запись Tkinter с pyHook зависает в окне графического интерфейса

У меня есть приложение с графическим интерфейсом Tkinter, в которое мне нужно ввести текст. Я не могу предположить, что приложение будет иметь фокус, поэтому я реализовал pyHook в стиле кейлоггера.

Когда окно GUI не имеет фокуса, ввод текста работает нормально, а переменная StringVar обновляется корректно. Когда окно графического интерфейса пользователя действительно имеет фокус, и я пытаюсь ввести текст, все происходит сбой.

т. е. если я нажму на окно консоли или что-нибудь еще после запуска программы, ввод текста сработает. Если я попытаюсь ввести текст немедленно (графический интерфейс запускается с фокусом) или перефокусирую окно в любой точке и введу текст, произойдет сбой.

Что происходит?

Ниже приведен минимальный полный проверяемый пример, чтобы продемонстрировать, что я имею в виду:

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

person heidi    schedule 19.06.2016    source источник
comment
1. Если вы хотите ответить пользователям, используйте комментарии; повезло, что я вернулся и посмотрел. 2. Если вы хотите избежать этого, потратьте больше времени на написание описательного заголовка, а не на странность, что никому не поможет. Тем не менее, я извиняюсь за то, что перепутал два.   -  person jonrsharpe    schedule 19.06.2016
comment
1. Я не знал, что это уведомило вас, поскольку вы не прокомментировали или иным образом не взаимодействовали с моим сообщением. 2. Чтение только второго абзаца показало бы, что это разные вопросы, или вы основывали свой дубликат исключительно на заголовке? Я использую такой расплывчатый термин, как странность, потому что не знаю, что происходит.   -  person heidi    schedule 19.06.2016
comment
Потоки и tkinter плохо сочетаются, см., например, stackoverflow.com/a/10556698/5781248   -  person J.J. Hakala    schedule 19.06.2016
comment
Я знаю, что существуют проблемы, однако вы заметите, что mainloop() работает в основном потоке. Кроме того, весь этот проект отлично работает в Linux, когда я использую контекст записи X. Это определенно проблема, связанная с pyHook.   -  person heidi    schedule 20.06.2016


Ответы (1)


Я изменил исходный код, указанный в вопросе (и другом), чтобы функция обратного вызова, связанная с pyHook, отправляла данные, связанные с событиями клавиатуры, в очередь. То, как объект GUI уведомляется о событии, может показаться излишне сложным. Попытка позвонить root.event_generate в keypressed зависла. Также казалось, что метод set из threading.Event вызывает проблемы при вызове в keypressed.

Контекст, в котором вызывается keypressed, вероятно, стоит за проблемой.

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()
person J.J. Hakala    schedule 20.06.2016
comment
Спасибо! Это как-то сработало. Я делал то же самое, что и в OP на Linux, и это работало отлично, поэтому я предположил, что это будет работать и на Windows. Еще один момент: я заметил, что вы не использовали PumpMessages — я думал, что это необходимо? - person heidi; 21.06.2016
comment
@heidi tkinter mainloop вызывает PeekMessage в какой-то момент, которого, вероятно, достаточно, чтобы активировать хуки, установленные pyHook, как в answer - person J.J. Hakala; 22.06.2016