Using the View and ScrollableView classes
Creating a user interface component for your application's data structure
is achieved by subclassing and customising the View
class, or its subclass ScrollableView. This section describes how these classes works and what needs to
be done to accomplish various things with them.
Coordinates, scrolling and the extent
Each instance of View or ScrollableView has its own local coordinate system in which
drawing takes place and the locations of mouse events are reported. For a plain View, the
origin of the local coordinate system is always at the top left corner
of the view. For a ScrollableView, the origin changes when the view is scrolled.
In addition to its bounds rectangle, a ScrollableView also as an extent rectangle
defining the limits of scrolling. The local coordinate system is
relative to this rectangle, so that its top left corner is always at
(0, 0) in local coordinates. The size of the extent rectangle is called
offset is the difference in local coordinates between the top left corner
of the view and the top left corner of the extent rectangle. Figure 1 illustrates
the relationship between the view's bounds, the extent rectangle, and the scroll offset.
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 rectangle. So,
in order for scrolling to be possible in a given direction, the extent must
be larger than the view's bounds in that direction.
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
to zero. In that situation, the viewed rectangle will include areas
are outside the extent rectangle. These areas are filled with the background color of the ScrollableView before your drawing method is called.
background color can be set to None to suppress automatic filling of
the background areas. However, whether you are able to draw anything
outside the extent rectangle yourself is platform-dependent, so for maximum portability you should either specify a background color or leave it set to the default.
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 draw method is also passed an update rectangle that bounds the region needing to be drawn.
The initial clipping region of the canvas is set to the update rectangle. 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 update rectangle 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-down events are delivered to a view by calling its mouse_down
method. In response, many applications will want to enter a mode in
which the mouse is tracked and some action performed until a mouse-up
event occurs. The track_mouse method provides a convenient way to do this. The idiom for using it goes like this:
def mouse_down(self, event):
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.
# Do something in response to the mouse click,
for event in self.track_mouse():
# Do something in response
# Do something in response to release of the
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.
Also note that track_mouse only reports mouse-drag and
mouse-up events -- any other kind of events, such as key events,
occurring during the drag will be ignored. If you need to handle such events while dragging, you will
have to implement mouse tracking non-modally using mouse_drag and mouse_up methods on your view.
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
self.notify_views(changed_item = 42)
def model_changed(self, model, changed_item):
At another level, you can send a custom change message and define
a method in the view to handle it:
which = w)
|In the view