Зависание при использовании tkinter + pyhook. Два цикла событий и многопоточность

Я пишу инструмент в Python 2.7, регистрирующий количество раз, когда пользователь нажимал клавиатуру или кнопку мыши. Количество кликов будет отображаться в маленьком черном поле в левом верхнем углу экрана. Программа регистрирует щелчки, даже когда другое приложение является активным.

Он отлично работает, за исключением случаев, когда я перемещаю мышь над окном. Затем мышь останавливается на несколько секунд, после чего программа снова работает. Если я затем наведу курсор мыши на поле во второй раз, мышь снова зависнет, но на этот раз программа вылетает.

Я попытался закомментировать pumpMessages(), а затем программа работает. Проблема очень похожа на этот вопрос pyhook + tkinter = crash, но решения там не дано.

Другие ответы показали, что при использовании wx и pyhook в python 2.6 существует ошибка с файлами dll. Я не знаю, уместно ли это здесь.

Я думаю, что это как-то связано с двумя параллельными циклами событий. Я читал, что tkinter не является поточно-ориентированным, но я не вижу, как заставить эту программу работать в одном потоке, так как мне нужно запустить как pumpmessages(), так и mainlooop().

Подводя итог: почему моя программа зависает при наведении мыши?

import pythoncom, pyHook, time, ctypes, sys
from Tkinter import *
from threading import Thread

print 'Welcome to APMtool. To exit the program press delete'

## Creating input hooks

#the function called when a MouseAllButtonsUp event is called
def OnMouseUpEvent(event): 
    global clicks
    clicks+=1
    updateCounter()
    return True

#the function called when a KeyUp event is called
def OnKeyUpEvent(event): 
    global clicks
    clicks+=1
    updateCounter()
    if (event.KeyID == 46):
        killProgram()
    return True


hm = pyHook.HookManager()# create a hook manager

# watch for mouseUp and keyUp events
hm.SubscribeMouseAllButtonsUp(OnMouseUpEvent)
hm.SubscribeKeyUp(OnKeyUpEvent)

clicks = 0

hm.HookMouse()# set the hook
hm.HookKeyboard()

## Creating the window
root = Tk()
label = Label(root,text='something',background='black',foreground='grey')
label.pack(pady=0) #no space around the label
root.wm_attributes("-topmost", 1) #alway the top window
root.overrideredirect(1) #removes the 'Windows 7' box around the label

## starting a new thread to run pumMessages() and mainloop() simultaniusly
def startRootThread():
    root.mainloop()

def updateCounter():
    label.configure(text=clicks)

def killProgram():
    ctypes.windll.user32.PostQuitMessage(0) # stops pumpMessages
    root.destroy() #stops the root widget
    rootThread.join()
    print 'rootThread stopped'



rootThread = Thread(target=startRootThread)
rootThread.start()

pythoncom.PumpMessages() #pump messages is a infinite loop waiting for events

print 'PumpMessages stopped'

3 ответа

Я решил эту проблему с многопроцессорностью:

  1. основной процесс обрабатывает графический интерфейс (MainThread) и поток, который принимает сообщения от второго процесса

  2. дочерний процесс перехватывает все события мыши / клавиатуры и передает их основному процессу (через объект Queue)

Из информации о том, что Tkinter должен работать в основном потоке, а не вызываться вне этого потока, я нашел решение:

Моя проблема заключалась в том, что оба PumpMessages а также mainLoop нужно запустить в главном потоке. Чтобы одновременно получать входные данные и показывать метку Tkinter с количеством нажатий, мне нужно переключаться между pumpMessages и кратко работает mainLoop обновить дисплей.

Делать mainLoop() Выйти сам я использовал:

after(100,root.quit()) #root is the name of the Tk()
mainLoop()

так через 100 миллисекунд root называет это quit метод и выходит из своего основного цикла

Чтобы вырваться из pumpMessages, я сначала нашел указатель на основной поток:

mainThreadId = win32api.GetCurrentThreadId()

Затем я использовал новый поток, который отправляет WM_QUIT к основной теме (примечание PostQuitMessage(0) работает только если он вызывается в основном потоке):

win32api.PostThreadMessage(mainThreadId, win32con.WM_QUIT, 0, 0)

Тогда можно было создать цикл while, который изменялся между pumpMessages а также mainLoop, обновляя labeltext между ними. После того, как два цикла событий больше не работают одновременно, у меня не было проблем:

def startTimerThread():
    while True:
        win32api.PostThreadMessage(mainThreadId, win32con.WM_QUIT, 0, 0)
        time.sleep(1)

mainThreadId = win32api.GetCurrentThreadId()
timerThread = Thread(target=startTimerThread)
timerThread.start()

while programRunning:
    label.configure(text=clicks)
    root.after(100,root.quit)
    root.mainloop()
    pythoncom.PumpMessages()

Спасибо Брайану Оукли за информацию о Tkinter и Boaz Yaniv за предоставление информации, необходимой для остановки pumpMessages() из подпотока

Tkinter не предназначен для запуска из какого-либо потока, кроме основного. Это может помочь поместить GUI в основной поток и вызвать вызов PumpMessages в отдельной теме. Хотя вы должны быть осторожны и не вызывать никаких функций Tkinter из другого потока (кроме, возможно, event_generate).

Другие вопросы по тегам