Handling mouse click events in PsychoPy
Problem statement
Most computer based experiments require a participant to press a button. This can easily be done using the event.waitKeys() or event.getKeys() functions. The PsychoPy event submodule allows you to get information about the mouse position and which buttons have been pressed. In this post I will show how you can use that information to react to 'mouse hover' and 'mouse click' events.
Let's get started
The basic setup of our code will create two circles. I have added these two circles to a list. This is because we are going to repeat a lot of code for each of these circles. By placing the circles in a list, I can simply iterate over that list and write the code that needs to be performed only once.
For now, the main loop of the program only displays the circles. On each screen flip it also checks if a key has been pressed, which will cause the program to exit the loop and close the window.
from psychopy import visual, event
# Create PsychoPy components
win = visual.Window(units = 'pix')
circle_1 = visual.Circle(win, radius = 50, pos = [-100, 0])
circle_2 = visual.Circle(win, radius = 50, pos = [ 100, 0])
circles = [circle_1, circle_2]
# Main loop
while True:
# Draw the circles
for circle in circles:
circle.draw()
win.flip()
# Check for key presses
keys = event.getKeys()
if len(keys) > 0:
break
win.close()
Figure 1. Two circles
Implementing mouse hover events
Detecting a hover event is actually not that difficult, so we'll start with that. A mouse hover event can be defined as the event where the position of the mouse is within the boundary of an object that we are interested in (in this case, a circle). We could calculate this manually by calling the mouse.getPos() function and then doing a little bit of math to figure out if this position falls within the circumference of each of the circles. But we can also do it in a much simpler way by calling the .contains() function of the circle that we want to check. We simply pass the mouse as an argument to this function, and PsychoPy will do all the calculations and return True if the mouse falls within the circle.
The code below can be added at the begining of the main loop. It iterates over the circles list and uses the contains function to check if the mouse is currently over the shape. If that is the case, the fillColor of the circle is set to red. Of course, if the mouse is currently not over a circle we need to set the fillColor to gray again.
# Detect mouse hover
for circle in circles:
if circle.contains(mouse):
circle.fillColor = "red"
else:
circle.fillColor = "gray"
Figure 1: Mouse hover in left circle
Implementing mouse click events
This part is a little bit more involved. First of all, how do we define a click event? You could say that this is just the event where a mouse button is pressed at the same moment that it is over a shape. But it is a little bit more subtle than that. Just try to hold the mouse down when the cursor is over some button and keep it down. Most likely you will notice that nothing happens until you release the mouse button. So a mouse click is defined more precisely as the event where the mouse button is first pressed down and subsequently released.
In PsychoPy, we can capture the state of the mouse buttons by using the mouse.getPressed() function. This will return an array in which each element represents a mouse button. The value of each element can be 0 (button is not pressed) or 1 (button is pressed). In this example I will always use the left mouse button, so I will use mouse.getPressed()[0] to capture that button.
To detect a mouse click, we first define a state variable mouseIsDown before the start of the main loop and set it to False. Within the main loop, we now continuously check the state of the left mouse button. If the user holds down the mouse button, the state variable mouseIsDown is set to True. When the user now releases the mouse button, PsychoPy will see that the button state is 0, but we can combine that with our state variable to check if the mouse was released:
# Check if the mouse is pressed down
if mouse.getPressed()[0] == 1 and mouseIsDown == False:
mouseIsDown = True
# Check if the mouse is released
if mouse.getPressed()[0] == 0 and mouseIsDown:
mouseIsDown = False
print("Mouse clicked")
Detecting mouse clicks in the circles
First, we will define a circle being clicked as a sequence of the following events:
- The user holds the mouse down in a circle
- The user releases the mouse in the same circle
This means we will need another state variable, mouseDownCircleIndex (initialized to -1), to keep track in which (if any) circle the mouse button is held down. This variable is defined before the main loop together with a third variable: mouseClickedInCircle (initialized to False). The code within the if-statements can now be extended as follows:
# Check if the mouse is pressed down in a circle
if mouse.getPressed()[0] == 1 and mouseIsDown == False:
mouseIsDown = True
mouseDownCircleIndex = -1
for i, circle in enumerate(circles):
if circle.contains(mouse):
mouseDownCircleIndex = i
# Check if the mouse is released in the same circle
if mouse.getPressed()[0] == 0 and mouseIsDown:
mouseIsDown = False
for i, circle in enumerate(circles):
if circle.contains(mouse) and i == mouseDownCircleIndex:
mouseClickedInCircle = True
When the left mouse button is detected as being pressed, we check if the mouse is currently in one of the circles and set the mouseDownCircleIndex variable accordingly. When the mouse is released, we check if the mouse is still over the same circle. If that's the case, we say that the circle has been pressed and set the mouseClickedInCircle variable to True. We can now use a third if-statement to act on this event. Using the mouseClickedInCircle variable allows us to separate the code for detecting the event from the code that needs to be executed when the event is detected:
# Process circle mouse click
if mouseClickedInCircle:
print("Mouse clicked in circle: %d"%mouseDownCircleIndex)
mouseClickedInCircle = False
A complete version of the code can be downloaded here.