Repeat function call while button is pressed PyQt5 - python-3.x

I started with some code like this which connects a function to a GUI button.
def on_click():
call_other_funct()
time.sleep(1)
button = QPushButton('Do the thing', self)
button.pressed.connect(on_click)
The problem is I need to repeatedly call on_click() every second for the duration of the mouse being held down on the button. I've searched quite a bit but haven't found a solution using PyQt.
I've been trying to fix this using a timer interval
def on_release():
self.timer.stop()
def on_click():
self.timer.start(1000)
self.timer.timeout.connect(on_click())
print('click')
button.pressed.connect(on_click)
button.released.connect(on_release)
This sort of works, but for some reason there seem to be and exponential number of on_click() calls happening. (on the first call, "click" prints once, then twice, then 4 times, then 8 etc). Is there a way to make this work correctly so each call only calls itself again once?
Or is there an all together better way to be doing this?

I suspect that the "exponential growth" comes from the fact that in the event handler on_click, you create a connection between the timer and the event handler itself. So I would expect something like this to happen:
on_click is executed once and the timer is connected once to on_click
after a second, the timer runs out and triggers on_click. During execution of on_click, the timer is connected to on_click again.
after a second, the timer runs out and triggers on_click twice (due to 2 connections). Which in turn then generate 2 more connections.
etc.
What you should do is connect your timer to another function which actually does the thing you want to execute every second while the mouse button is down.
def on_release():
self.timer.stop()
def on_press():
self.timer.start(1000)
def every_second_while_pressed():
print('click')
button.pressed.connect(on_press)
button.released.connect(on_release)
self.timer.timeout.connect(every_second_while_pressed)

Related

Control close event pyglet

I have a program that has multiple windows in pyglet, and I want to make one window unclosable, rather to set its visibility to false. Is there a way I can access the event handle for close, to make it instead of closing the window, possibly return a bool or a string like "should close."?
Found the answer to my own question, first define a custom event loop with window_event_loop = pyglet.app.EventLoop(), then create a handler for .event for the event loop
#window_event_loop.event
def on_window_close(window):
# Extra code here
return pyglet.event.EVENT_HANDLED

PyQt5 UI stuck at long operation

I'm creating a game with some AI that may take some time. The problem is even if I call relevant methods to update the UI before running the AI function, the UI is not visually updated.
Some example code looks like this
def onClickBoard(self, e):
x, y = toBoardGrid(e.x(), e.y())
self.game.move(x, y)
self.update_board()
print("before AI")
# This line takes a few seconds
ai_move = self.ai.get_best_move(self.game)
print("after AI")
self.game.move(ai_move[0], ai_move[1])
self.update_board()
Where self.update_board is a method that updates a QWidget and it's very fast. This onClickBoard method is assigned to the widget's mouseReleaseEvent.
self.board.mouseReleaseEvent = self.onClickBoard
When running the game, I can see before AI printed to the terminal but the visual window doesn't change. I see the window updates only once, after the AI commits its move.
Is there a way to make the board update once before the slow function call and another once after it?
Yes, you can force Qt to process all pending events, and thus update the GUI, with the QApplication::processEvents() method. Add the following line just before the slow function call:
QtWidgets.QApplication.instance().processEvents()

do something after a period of gui user inactivity tkinter

What is the general method for 'doing something' after a period of user inactivity in tkinter? In my case the 'do something' will be to go to the start screen (tk.frame) that is already instantiated.
The simplest solution I can think of looks something like this:
start a timer
set a binding on any key press or any button click to reset the timer
if the timer goes off, do something
Create a function to call when the user is inactive:
def user_is_inactive():
<your code here>
Create a function to reset the timer.
We want to be able to call it from an event or directly, so the event argument needs to be optional:
timer = None
def reset_timer(event=None):
global timer
# cancel the previous event
if timer is not None:
root.after_cancel(timer)
# create new timer
timer = root.after(10000, user_is_inactive)
Set up bindings to reset the timer
Using bind_all means that every widget can potentially handle these events:
root.bind_all('<Any-KeyPress>', reset_timer)
root.bind_all('<Any-ButtonPress>', reset_timer)
Start the timer
A good time to do this is right before calling mainloop.
reset_timer()
root.mainloop()
I will admit that I have no experience in this, but maybe a sort of watchdog timer could work? A timer would count up to your desired time, but anytime an element is activated it would reset the counter. This concept is used in micro-controllers a lot but I'm not sure how you would apply it to python.

Tkinter traffic-jamming

I have an expensive function that is called via a Tkinter callback:
def func: # called whenever there is a mouse press in the screen.
print("Busy? " + str(X.busy)) # X.busy is my own varaible and is initialized False.
X.busy = True
do_calculations() # do_calculations contains several tk.Canvas().update() calls
X.busy = False
When I click too quickly, the func()'s appear to pile up because the print gives "Busy? True", indicating that the function hasen't finished yet and we are starting it on another thread.
However, print(threading.current_thread()) always gives <_MainThread(MainThread, started 123...)>, the 123... is always the same each print for a given program run. How can the same thread be multiple threads?
It looks to me like you're running into recursive message processing. In particular, tk.Canvas().update() will process any pending messages, including extra button clicks. Further, it will do this on the same thread (at least on Windows).
So your thread ID is constant, but your stack trace will have multiple nested calls to func.

Freeze when using tkinter + pyhook. Two event loops and multithreading

I am writing a tool in python 2.7 registering the amount of times the user pressed a keyboard or mouse button. The amount of clicks will be displayed in a small black box in the top left of the screen. The program registers clicks even when another application is the active one.
It works fine except when I move the mouse over the box. The mouse then freezes for a few seconds after which the program works again. If I then move the mouse over the box a second time, the mouse freezes again, but this time the program crashes.
I have tried commenting out pumpMessages() and then the program works. The problem looks a lot like this question pyhook+tkinter=crash, but no solution was given there.
Other answers has shown that there is a bug with the dll files when using wx and pyhook together in python 2.6. I don't know if that is relevant here.
My own thoughts is that it might have something to do with the two event loops running parallel. I have read that tkinter isn't thread safe, but I can't see how I can make this program run in a single thread since I need to have both pumpmessages() and mainlooop() running.
To sum it up: Why does my program freeze on mouse over?
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'
I've solved this problem with multiprocessing:
the main process handles the GUI (MainThread) and a thread that consumes messages from the second process
a child process hooks all mouse/keyboard events and pushes them to the main process (via a Queue object)
From the information that Tkinter needs to run in the main thread and not be called outside this thred, I found a solution:
My problem was that both PumpMessages and mainLoop needed to run in the main thread. In order to both receive inputs and show a Tkinter label with the amount of clicks I need to switch between running pumpMessages and briefly running mainLoop to update the display.
To make mainLoop() quit itself I used:
after(100,root.quit()) #root is the name of the Tk()
mainLoop()
so after 100 milliseconds root calls it's quit method and breaks out of its own main loop
To break out of pumpMessages I first found the pointer to the main thread:
mainThreadId = win32api.GetCurrentThreadId()
I then used a new thread that sends the WM_QUIT to the main thread (note PostQuitMessage(0) only works if it is called in the main thread):
win32api.PostThreadMessage(mainThreadId, win32con.WM_QUIT, 0, 0)
It was then possible to create a while loop which changed between pumpMessages and mainLoop, updating the labeltext in between. After the two event loops aren't running simultaneously anymore, I have had no problems:
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()
Thank you to Bryan Oakley for information about Tkinter and Boaz Yaniv for providing the information needed to stop pumpMessages() from a subthread
Tkinter isn't designed to be run from any thread other than the main one. It might help to put the GUI in the main thread and put the call to PumpMessages in a separate thread. Though you have to be careful and not call any Tkinter functions from the other thread (except perhaps event_generate).

Resources