parent previous next question (Smalltalk Textbook 07)

AutoScrollingView

This section covers Scrolling Views. You already used the Scroll function in 'EngiDisplayModel', 'EngiDisplayView', and 'EngiDisplayController' in the previous section. Confirm that the super class of 'EngiDisplayView' is 'AutoScrollingView' (Check Appendix 04). Scrolling functionality is inherited from this class.

Program 7-1 creates a window with a scrolling view.


Program-7-1: (AutoScrollingView, LookPreferences, Rectangle)
---------------------------------------------------------------------
| scrollingView edgeDecorator topWindow |
scrollingView := AutoScrollingView new.
edgeDecorator := LookPreferences edgeDecorator on: scrollingView.
topWindow := EngiTopView
            model: nil
            label: 'ScrollingView'
            minimumSize: 200 @ 200.
topWindow add: edgeDecorator in: (0 @ 0 corner: 1 @ 1).
topWindow open
---------------------------------------------------------------------

Evaluate Program 7-1 and you'll see a window containing a view with a vertical scroll bar. Program 7-2 shows how to add a horizontal scroll bar.


Program-7-2: (AutoScrollingView, LookPreferences, Rectangle; scrollbar)
---------------------------------------------------------------------
| scrollingView edgeDecorator topWindow |
scrollingView := AutoScrollingView new.
edgeDecorator := LookPreferences edgeDecorator on: scrollingView.
edgeDecorator useHorizontalScrollBar.
topWindow := EngiTopView
            model: nil
            label: 'ScrollingView'
            minimumSize: 200 @ 200.
topWindow add: edgeDecorator in: (0 @ 0 corner: 1 @ 1).
topWindow open
---------------------------------------------------------------------

Nothing is displayed in the view by this program. Yet the scrollbars still move. How can this be? Or, in other words, what is it that is getting scrolled? To answer of this question, use a class browser to find the 'View' class which is the super class of 'AutoScrollingView'. Use the 'find method...' menu on the protocol pane to find the 'preferredBounds' method. It is shown in Example 7-1:

Example 7-1
---------------------------------------------------------------------
"View"
preferredBounds
    ^Screen default bounds
---------------------------------------------------------------------

The 'preferredBounds' message calculates the display area. In this case the display area is the same as the screen size. And the default 'displayOn:' message is a noop as shown in Example 7-2.

Example 7-2
---------------------------------------------------------------------
"VisualPart"
displayOn: aGraphicsContext
    "Do nothing."
---------------------------------------------------------------------

Therefore, Program 7-2 displays nothing, but the scroll view is the screen size. That means you can scroll the view as large as the screen size even though the view is empty.

There is an another message to answer display area named 'bounds'. The 'preferredBounds' message is useful because it returns accurate answers. However 'preferredBounds' is slower than 'bounds' and is called by 'bounds'.

Program 7-3 shows how 'bounds' is defined in 'VisualPart' and 'VisualComponent', both super clases of 'View'. You see 'bounds' uses 'preferredBounds', which is explicitly defined so that subclasses must define (overide) it. Example 7-4 just shows the class hierarchy of some relevant classes.

Example 7-4
---------------------------------------------------------------------
Object
    VisualComponent
        VisualPart
            DependentPart
                View
                    AutoScrollingView
---------------------------------------------------------------------

Program-7-3: (VisualComponent; bounds, preferredBounds, container)
---------------------------------------------------------------------
"VisualComponent"
bounds
    ^self preferredBounds

preferredBounds
    ^self subclassResponsibility

"VisualPart"
bounds
    ^container == nil
        ifTrue: [self preferredBounds]
        ifFalse: [container compositionBoundsFor: self]
---------------------------------------------------------------------

Program-7-4: (VisualComponent; bounds, preferredBounds)
---------------------------------------------------------------------
"Sub-classes of VisualComponent"
bounds
    bounds isNil ifTrue: [bounds := self preferredBounds]
    ^bounds
---------------------------------------------------------------------

If the value of the instance variable 'bounds' is nil, Program 7-4 calculates it by using 'preferredBounds' and then returns it. You see the instance variable 'bounds' works as cache-memory for the display area. Because 'preferredBounds' is costly, it is worthwhile to cache the results. I recommend you use 'preferredBounds' inside of your methods and use 'bounds' externally.

Let us return to the scroll function. Evaulate Program 7-5 to define 'EngiDummyView'. You must cut and paste Program 7-5 into a File List buffer, then select the text and 'file it in'). You used the 'EngiDummyView' before and if you did not delete the class, you have to delete it before loading the new one.


Program-7-5: (AutoScrollingView, EngiDummyView; displayObject, 
displayObject:, preferredBounds, displayOn:, example1)
---------------------------------------------------------------------
AutoScrollingView subclass: #EngiDummyView
    instanceVariableNames: 'displayObject '
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Engi-Dummy'!


!EngiDummyView methodsFor: 'accessing'!

displayObject
    ^displayObject!

displayObject: aVisualComponent 
    displayObject := aVisualComponent! !

!EngiDummyView methodsFor: 'bounds accessing'!

preferredBounds
    displayObject isNil ifTrue: [^Screen default bounds].
    ^displayObject bounds! !

!EngiDummyView methodsFor: 'displaying'!

displayOn: graphicsContext 
    displayObject isNil ifFalse: [displayObject displayOn: graphicsContext]! !
"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!

EngiDummyView class
    instanceVariableNames: ''!


!EngiDummyView class methodsFor: 'examples'!

example1
    "EngiDummyView example1."

    | scrollingView edgeDecorator topWindow |
    scrollingView := EngiDummyView new.
    scrollingView displayObject: Image fromUser.
    edgeDecorator := LookPreferences edgeDecorator on: scrollingView.
    edgeDecorator useHorizontalScrollBar.
    topWindow := EngiTopView
                model: nil
                label: 'ScrollingView'
                minimumSize: 100 @ 100.
    topWindow add: edgeDecorator in: (0 @ 0 corner: 1 @ 1).
    topWindow open! !
---------------------------------------------------------------------

After loading the program, evaluate the following expression.

---------------------------------------------------------------------
EngiDummyView example1
---------------------------------------------------------------------

The cursor will change to a cross-hair and you will need to select a rectangle on the screen. Choose a fairly large rectangle because the rectangle you choose will be shown in the scrollable view.

An instance variable named 'displayObject' is defined in 'EngiDummyView' (a subclass of 'AutoScrollingView'). All you have to do is to define an access method for the 'displayObject' and redefine methods for 'preferredBounds' and 'displayOn:'.

If 'displayObject' is defined (not nil) 'preferredBounds' returns the display area of 'displayObject'. Otherwise 'preferredBounds' returns the size of the default screen.

'displayOn:' delegates a message to the 'displayObject' if it is not nil 'displayObject' . Otherwise 'displayObject' does nothing.

So all you have to do is make a subclass of the 'AutoScrollingView' and then redefine 'preferredBounds' and 'displayOn:' methods to create a new view which displays an object.

This 'EngiDummyView' is a temporary class. Delete it when you're done experimenting with it.

Now, let us review how to redefine 'preferredBounds' and 'displayOn:' in the 'EngiDisplayView' which you studied in the previous section.


Program-7-6: (EngiDisplayView; preferredBounds, displayOn:, respondsTo:)
---------------------------------------------------------------------
"EngiDisplayView"
preferredBounds
    (model notNil and: [model respondsTo: #bounds])
        ifTrue: [^model bounds]
        ifFalse: [^Point zero corner: Point zero]

"EngiDisplayView"
displayOn: graphicsContext 
    (model notNil and: [model respondsTo: #displayOn:])
        ifTrue: [model displayOn: graphicsContext]
---------------------------------------------------------------------

If the model of the view is not nil and it can understand the 'bounds' message or 'displayOn:' message, 'EngiDisplayView' delegates the message to the model. Otherwise 'EngiDisplayView' does nothing.


parent previous next question
Copyright (C) 1994-1996 by Atsushi Aoki
Translated by Kaoru Rin Hayashi & Brent N. Reeves