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):
...
|