PyGUI - Using the View class

# 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.

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 that are outside the extent. It is platform-dependent whether drawing done in those areas will appear on the screen. For maximum portability, you should clear the whole viewed rectangle before starting to draw.

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 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 tracking

Mouse-down events are delivered to a view by calling its `mouse_down` method. In response, may 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):`
> # 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.
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 deferred until the tracking loop is finished. 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.

## 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, model, 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, model, which):` ...