Hooking up your own events#
If you’re coming from a background of scripting or working with python in an
interactive console, thinking in terms of the “event loop” can feel a bit
strange at times. Often we write code in a very procedural way: “do this …
then do that, etc…”. With napari and other GUI programs however, usually you
hook up a bunch of conditions to callback functions (e.g. “If this event
happens, then call this function”) and then start the loop and hope you hooked
everything up correctly! Indeed, much of the napari
source code is
dedicated to creating and handling events: search the codebase for
.emit(
and
.connect(
to find examples of creating and handling internal events, respectively.
If you would like to set up a custom event listener then you need to hook into the napari event loop. We offer a couple of convenience decorators to easily connect functions to key and mouse events.
Listening for keypress events#
One option is to use keybindings, that will listen for keypresses and then call
some callback whenever pressed, with the viewer instance passed as an argument
to that function. As a basic example, to add a random image to the viewer every
time the i
key is pressed, and delete the last layer when the k
key is
pressed:
import numpy as np
import napari
viewer = napari.Viewer()
@viewer.bind_key('i')
def add_layer(viewer):
viewer.add_image(np.random.random((512, 512)))
@viewer.bind_key('k')
def delete_layer(viewer):
try:
viewer.layers.pop(0)
except IndexError:
pass
napari.run()
See also this custom key bindings example.
Listening for mouse events#
You can also listen for and react to mouse events, like a click or drag event, as shown here where we update the image with random data every time it is clicked.
import numpy as np
import napari
viewer = napari.Viewer()
layer = viewer.add_image(np.random.random((512, 512)))
@layer.mouse_drag_callbacks.append
def update_layer(layer, event):
layer.data = np.random.random((512, 512))
napari.run()
As of this writing MouseProvider
s have 4 list of callbacks that can be registered:
mouse_move_callbacks
mouse_wheel_callbacks
mouse_drag_callbacks
mouse_double_click_callbacks
Please look at the documentation of MouseProvider
for a more in depth
discussion of when each callback is triggered. In particular single click can be
registered with mouse_drag_callbacks
, and mouse_double_click_callbacks
is
triggered in addition to mouse mouse_drag_callbacks
.
See also the custom mouse functions and mouse drag callback examples.
Connection functions to native napari events#
If you want something to happen following some event that happens within
napari, the trick becomes knowing which native signals any given napari object
provides for you to “connect” to. Until we have centralized documentation for
all of the events offered by napari objects, the best way to find these is to
browse the source code. Take for instance, the base
Layer
class: you’ll find in the __init__
method a
self.events
section that looks like this:
self.events = EmitterGroup(
...
data=Event,
name=Event,
...
)
That tells you that all layers are capable of emitting events called data
, and
name
(among many others) that will (presumably) be emitted when that property
changes. To provide your own response to that change, you can hook up a callback
function that accepts the event object:
def print_layer_name(event):
print(f"{event.source.name} changed its data!")
layer.events.data.connect(print_layer_name)
Long-running, blocking functions#
An important detail here is that the napari event loop is running in a single thread. This works just fine if the handling of each event is very short, as is usually the case with moving sliders, and pressing buttons. However, if one of the events in the queue takes a long time to process, then every other event must wait!
Take this example in napari:
viewer = napari.Viewer()
# everything is fine so far... but if we trigger a long computation
image = np.random.rand(512, 1024, 1024).mean(0)
viewer.add_image(image)
# the entire interface freezes!
Here we have a long computation (np.random.rand(512, 1024, 1024).mean(0)
) that
“blocks” the main thread, meaning no button press, key press, or any other
event can be processed until it’s done. In this scenario, it’s best to put
your long-running function into another thread or process. napari
provides a
convenience for that, described in Multithreading in napari.