Зависание при использовании 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 ответа
Я решил эту проблему с многопроцессорностью:
основной процесс обрабатывает графический интерфейс (MainThread) и поток, который принимает сообщения от второго процесса
дочерний процесс перехватывает все события мыши / клавиатуры и передает их основному процессу (через объект 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
).