Using the View class

Creating a user interface component for your application's data structure is achieved by subclassing and customising the View class. This section describes how the View class works and what needs to be done to accomplish various things with it.

Coordinates, scrolling and the extent

Each instance of View has its own local coordinate system in which drawing takes place and the locations of mouse events are reported. The origin of the local coordinate system is initially at the top left corner of the view, but this changes when the view is scrolled.

Scrolling is controlled by the view's extent, which is a rectangle in local coordinates representing the limits of scrolling. The scroll offset is the difference in local coordinates between the top left corner of the view and the top left corner of the extent. Figure 1 illustrates the relationship between the view's bounds, the extent, and the scroll offset. Note that the origin of the local coordinate system is not necessarily at the top left of the extent, but the scroll offset is always measured from the top left corner of the extent.

The part of the local coordinate system that is visible in the view is called the viewed rectangle. The scroll offset is constrained, as far as possible, so that the viewed rectangle lies within the extent. So, in order for scrolling to be possible in a given direction, the extent must be larger than the view's bounds.

If the extent is smaller than the bounds in a given direction, there is no room for movement and the scroll offset in that direction will be clamped to zero. In that situation, the viewed rectangle will include areas which are outside the extent. However, there's nothing stopping you from drawing in those areas; drawing is clipped to the bounds, but not the extent. So if you don't care about scrolling, you can leave the extent unspecified and ignore it.


Figure 1
Bounds, extent, viewed rect and scroll offset

Drawing and invalidating

Whenever some part of the view needs to be drawn, the draw method is called with a Canvas object as parameter. The canvas object encapsulates a drawing state and provides drawing methods.

The initial clipping region of the canvas is set to the part of the view that needs to be drawn. In the simplest case, the draw method can just erase and redraw everything, and the clipping will ensure that only the parts that actually need drawing are affected. A more intelligent draw method can make tests against the clipping region and be more selective about what to draw.

There are two ways that calls to draw can be triggered. One is when part of a window becomes uncovered on the screen. The other is by calling the view's invalidate method, which marks the whole viewed rectangle as needing to be drawn, or invalidate_rect, which marks a specified rectangle.

Note that the canvas passed to the draw method is only valid for the duration of the call, and should not be retained beyond it. To draw into the view at other times, it is necessary to call the with_canvas method, passing it a function that accepts a canvas as parameter. However, this should be avoided if possible. It is almost always easier and more efficient to simply invalidate the affected region and wait for the draw method to be called.

Mouse tracking

Mouse-down events are delivered to a view by calling its mouse_down method. Mouse-drag and mouse-up events are not delivered automatically, however. To receive them, you need to write a mouse tracking loop using the track_mouse method. The idiom for mouse tracking goes like this:
def mouse_down(self, event):
    # Do something in response to the mouse click, and then...
    for event in self.track_mouse():
        # Do something in response to dragging
    # Do something in response to release of the mouse
The track_mouse method returns an iterator which yields a series of mouse events. All of these events will be mouse-drag events, except for the final one, which will be a mouse-up event. Thus, when the above loop is finished, event will be bound to a mouse-up event representing the location where the mouse was released.

Note that the body of the loop will be executed for the final mouse-up event as well as for the mouse-drag events. Usually it doesn't do any harm to treat them both the same way, but if it matters, you'll need to test the kind of the event in the loop.

Model observation

Since one of the primary uses of a view is to display a model, some convenience features are provided to support using it in the role of a model observer. For the frequent case where the view observes a single model object, there is a model property. Assigning to this property has the side effect of connecting the view to the model.

If the view needs to respond to changes in more than one model object, you can use the add_model and remove_model methods to attach and detach models, and the models property to retrieve a list of currently attached models.

An alternative way of connecting and disconnecting views and models is to use the add_view and remove_view methods of the model. It doesn't matter whether you connect the view to the model or the model to the view; the end result is the same.

A default model_changed method is provided which simply invalidates the whole view, causing it to be completely redrawn. If redrawing your view is fairly quick, you won't need to do anything else to respond to model changes -- just call the model's notify_views method and the view will update itself.

If you need to be more selective about what you redraw, you'll have to pass some information about what part of the model has changed. There are a couple of levels at which you can customise the process. At one level, you can pass some parameters along with the model_changed message:

In the model
...
self.notify_views(changed_item = 42)
...


In the view

def model_changed(self, changed_item):
    ...


At another level, you can send a custom change message and define a method in the view to handle it:

In the model
...
self.notify_views('wibble_twisted',
  which = w)
...


In the view

def wibble_twisted(self, which):
    ...