napari models and events#
This document explains the links between the three main components of napari: Python models, Qt classes and vispy classes, with code examples. This knowledge is not necessary to use napari and is more aimed at developers interested in understanding the inner workings of napari. This document assumes you’re familiar with basic usage of napari.
The three main components:
Python models describing components in the napari application - these are able to operate without the GUI interface and do not have any dependencies on user interface classes
this code lives in
napari/components
(utility objects) andnapari/layers
(objects that contain data)
Qt classes that handle the interactive GUI aspect of the napari viewer
the private Qt code lives in
napari/_qt
and the smaller public Qt interface code lives innapari/qt
vispy classes that handle rendering
the code for this is private and lives in
napari/_vispy
The separation of the Python models from viewer GUI code allows:
analysis plugins to be developed without worrying about the GUI aspect
napari to have the option to move away from the rendering backend currently used
tests to be easily run headlessly
the Python models to be run headlessly (see Running napari headlessly for more)
Python models and events#
Commonly, Python models in napari are classes that store information about their state as an attribute and are the “source of ground truth”. When these attributes are changed an “event” needs to be emitted such that relevant observers of the model (such as other classes) can take the appropriate action.
One way this is achieved in napari is via getters and setters. Let’s take
for example the Dims
class with a selected few attributes:
from napari.utils.events import EmitterGroup
class Dims:
"""Dimensions object modeling slicing and displaying.
Parameters
----------
ndisplay : int
Number of displayed dimensions.
...
"""
def __init__(self, ndisplay):
self._ndisplay = ndisplay
# an `EmitterGroup` manages a set of `EventEmitters`
# we add one emitter for each attribute we'd like to track
self.events = EmitterGroup(source=self, ndisplay=None)
# for each attribute, we create a `@property` getter/setter
# so that we can emit the appropriate event when that attribute
# is changed using the syntax: ``Dim.attribute = new_value``
@property
def ndisplay(self):
"""Number of displayed dimensions."""
return self._ndisplay
@ndisplay.setter
def ndisplay(self, value):
self._ndisplay = value
# emit the ndisplay "changed" event
self.events.ndisplay(value=value)
Another object can then “listen” for changes in our Dims
model and register
a callback function with the event emitter of the attribute they would like
to watch:
# create an instance of the model
dims = Dims(ndisplay=2)
# define some callback that should respond to changes in the model
# if the function takes a single parameter it will receive the event
# as first value.
def _update_display(event):
"""
Updates display for all sliders.
"""
# the code updating the display code is not relevant for this
# example thus has been omitted.
# we can get the source object of the event
assert event.source == dims
# ... and query any attributes
ndisplay = event.source.ndisplay
# ... or directly get the new value for this specific event:
assert ndisplay == event.value
print(f"Update number of dimensions displayed to {ndisplay}")
# register our callback with the model
dims.events.ndisplay.connect(_update_display)
# now, everytime dims.ndisplay is changed, _update_display is called
dim.ndisplay = 3
This method is very customizable but requires a lot of boilerplate. The
generic base model EventedModel
was added to reduce this and
“standardize” this change/emit pattern. The EventedModel
provides the
following features:
type validation and coercion on class instantiation and attribute assignment
event emission after successful attribute assignment
Using EventedModel
would reduce the above Dim
class code to:
class Dim(EventedModel):
"""Dimensions object modeling slicing and displaying.
Parameters
----------
ndisplay : int
Number of displayed dimensions.
...
"""
ndisplay: float
This Dim
class will automatically emit an event when one of its attributes
changes. Other classes interested in the Dim
class can register a callback
function that will be executed when an attribute changes.
class DimsDependentClass():
"""A class that needs to 'do something' when Dims attributes change.
Parameters
----------
dims : napari.components.dims.Dims
Dims object.
...
Attributes
----------
dims : napari.components.dims.Dims
Dimensions object modeling slicing and displaying.
...
"""
def __init__(self, dims: Dims):
self.dims = dims
self.dims.events.ndisplay.connect(self._update_display)
Currently most of the models in napari/components/
are EventedModels
but
not the layer models although there is intention to convert these to
EventedModels
in the future.
Qt classes#
Qt classes are responsible for all napari’s user interface elements. There is
generally one to one mapping between Python models and Qt models in napari, for
example Python model Dims
and Qt model QtDims
.
The Qt class can register callbacks such that when an attribute of the
corresponding Python model changes, the appropriate actions are taken.
The Qt classes are also instantiated with a reference to
the Python model, which gets updated directly when a field is changed via the
GUI.
For example, below is a code snippet showing the QtDims
class instantiating
with a reference to the Python class Dims
and registering the callback
_update_display
:
class QtDims(QWidget):
"""Qt view for the napari Dims model.
Parameters
----------
dims : napari.components.dims.Dims
Dims object to be passed to Qt object.
...
Attributes
----------
dims : napari.components.dims.Dims
Dimensions object modeling slicing and displaying.
...
"""
def __init__(self, dims: Dims):
self.dims = dims
self.dims.events.ndisplay.connect(self._update_display)
Vispy classes#
Vispy classes are responsible for drawing the canvas contents, thus need to be informed of any changes to Python models. They achieve this by connecting callbacks to Python model events just like Qt models.
For example, below is a code snippet showing the VispyCamera
class connecting
the function _on_ndisplay_change
:
class VispyCamera:
"""Vipsy camera for both 2D and 3D rendering.
"""
def __init__(self, dims: Dims):
self._dims = dims
...
self._dims.events.ndisplay.connect(
self._on_ndisplay_change, position='first'
)