parent previous next question (Smalltalk Textbook 24)

EngiGauge

We have studied how to use pluggable MVC and now we will learn how to create pluggable MVC. We will make a gauge which visualize numerical data. File in Appendix 13.

Our gauge requires two structures: one is MVC and the other is a mapping of a numeric value to between 0 and 1. Both are relatively abstract structures because they are mostly concerned with relations between objects and don't have that much data themselves.

To begin, let us decide that our MVC classes will be named 'EngiGaugeModel' 'EngiGaugeView' 'EngiGaugeController'.

Now let us consider instance variables. 'EngiGaugeModel' stores a 'gaugeValue'. 'EngiGaugeView' needs to know how to extract and set the value of the gauge model, so we sill store those message names in 'getSelector' and 'putSelector'. It also needs instance variables to display things like 'minValue', 'maxValue', 'divideValue' (divisions of a scale) and 'roundValue' of the 'gaugeValue'.

'EngiGaugeController' only needs to handle the mouse rather than menus, so 'Controller' (rather than 'ControllerWithMenu') may be a suitable super class for it. You have to consider (1) referencing and setting the gauge value, (2) mapping the gauge value to a visual display through the view. Then you need to think about (1) how the view reacts to a request to display, (2) how mouse coordinates will map to the gaugue value, (3) message communication for consistency in the MVC parts.

When the gauge view opens, the view receives a request to display the gauge, it draws a frame, a scale and draws the gauge value. To draw the current value, the view uses its model's 'getSelector' instance variable to communicate with the the gauge model and retrieve the value to display.

When you move the mouse to change the gauge value, the controller sends mouse coordinates to the view. The view transforms theses coordinates into the model value based on display area, max. value, min. value of the gauge and sends it to the model ( uses the 'putSelector' instance variable ). The model receives the message, changes it's value and broadcasts that a change has occurred. The view receives the model's broadcast, and updates itself to reflect the new value.

Now notice something subtle (or perhaps not so subtle and rather confusing...) When the mouse moves, you might expect the view to redraw itself immediately to make the bar-graph (for example) end exactly at the new mouse coordinates, but this is not what happens. The view simply maps mouse coordinates into a gauge value and sends a change request to the model. So when does the view finally redraw itself to reflect the changed value? The view only gets redrawn as a result of the model broadcasting the fact that it has changed (through the 'self changed:' message.) The view then receives this change notification as an 'update:' message, asks the model for the (newly changed) current value, and draws the appropriate representation (bar graph, or whatever). Now at first this seems too 'round about,' but you will see over time that dividing responsibilities up like this results in good MVC design.

To recap the message flow:
 1. Model is created and contains some default value
 2. View is created and hooked to model
 3. View is asked to draw itself
 4. View requests current value from model
 5. View draws itself (bar graph or whatever)

    now we start mouse interaction:

 6. mouse clicks somewhere in the view
 7. Controller sends mouse point to View
 8. View translates mouse coordinates into gauge value
 9. View send message to Model with a new gauge value
10. Model broadcasts it has changed ('self changed:')
11. View receives broadcast (via 'update:')
12. View askes Model for current (newly changed) value
13. View draws itself as in step 5

Steps 6 - 13 now repeat until the view is closed.

If the gauge window size is changed, the gauge view needs to redraw itself taking into account the new window dimensions and thus remapping the gauge values to the view size. Normalizing graphical information (i.e. in our example this means mapping a range of gauge values to a 0 - 1.0 scale) makes this easy to manage. You should avoid absolute coordinates for graphical information. Relative coordinates and normalizing are standard in graphics operations.

The view should position numeric labels alongside the representation of the gauge value. To avoid recalculating this often, we will cache the labels and their relative positions in an instance variable named 'scalesLayout'. This is another case where we do not hard-code the label offsets, but rather normalize their offsets in terms of 0 to 1.0. This provides us with flexibility and we just do scalar multiplication by the window size whenever the window changes.

Appendix 13 is implemented according to these considerations. It has three classes whose inheritance is :

-------------------------------------------------------------------
Object()
. . Model('dependents')
. . . . EngiVariable('value')
. . . . . . EngiGaugeModel()

Object()
. . VisualComponent()
. . . . VisualPart('container')
. . . . . . DependentPart('model')
. . . . . . . . View('controller')
. . . . . . . . . . EngiGaugeView('getSelector' 'putSelector'
                                  'minValue' 'maxValue'
                                  'divideValue' 'roundValue'
                                  'scalesLayout')

Object()
. . Controller('model' 'view' 'sensor')
. . . . EngiGaugeController ()
-------------------------------------------------------------------

Program 24-1 taken from 'EngiGaugeModel class> examples> example1'


Program-24-1: (EngiGaugeModel, EngiGaugeView; 
on:get:put:valuesMinMaxDivideRound:, noMenuBar)
-------------------------------------------------------------------
| gaugeModel windowCreation |
gaugeModel := EngiGaugeModel new.
windowCreation :=
        [| gaugeView edgeDecorator topWindow |
        gaugeView := EngiGaugeView
                        on: gaugeModel
                        get: #value
                        put: #value:
                        valuesMinMaxDivideRound: #(-100 100 10 1 ).
        edgeDecorator := LookPreferences edgeDecorator on: gaugeView.
        edgeDecorator noMenuBar.
        edgeDecorator noVerticalScrollBar.
        edgeDecorator noHorizontalScrollBar.
        topWindow := EngiTopView
                                model: nil
                                label: 'Gauge'
                                minimumSize: 100 @ 180.
        topWindow add: edgeDecorator in: (0 @ 0 corner: 1 @ 1).
        topWindow].
3 timesRepeat: [windowCreation value open].
gaugeModel inspect
-------------------------------------------------------------------

The gauge program uses pluggable MVC, so it works for multiple views and controllers on a single model. Program 24-1 creates one gauge model, but three views. Change one of the window sizes and use the inspector to explore the scalesLayout instance variable. Also, don't forget that the mouse is not the only method you have to change the gauge value. For example, evaluate this expression in the EngiGaugeModel inspector to see all three gauges slowly (or quickly if you have a fast machine!) change.


Program-24-2: (Interval; to:, do:)
-------------------------------------------
-100 to: 100 do: [:n | self value: n]
-------------------------------------------

How do the views know the model is changing? Since this expression sends the 'value:' message to the model, explore 'EngiGaugeModel> accessing> value:'

If you understand the gauge program, you are on your way to using MVC and graphics in Smalltalk.


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