As you've seen throughout these tutorials, windows provide the major interface between you and Smalltalk/V. For example, you use the Class Hierarchy Browser window to enter programs into the system, and the Debugger window to view and correct that code when you inevitably make mistakes. In previous chapters, you used standard, supplied windows for the tutorial examples. In this chapter, you'll find out how to make your own windows.
As always, the examples for this tutorial are stored in a disk file. To retrieve these examples, simply use the File menu Open item to access the contents of the file chapter.10.
You will also again be making modifications to the Smalltalk/V environment in this tutorial. Be sure to save the image when you exit the environment; if you want to repeat any section of this or any tutorial, it will already be there for you.
This tutorial builds on several of the previous tutorial examples in Chapters 6, 7, and 9. If you have not done the tutorials in those chapters and saved the image, you should evaluate the following expression to install the needed classes:
(File pathName: 'tutorial\ class10.st') fileIn; close
The later part of this tutorial extends the animal and animal habitat classes with several new methods. Evaluate the following expression to file in all of the new methods:
(File pathName: 'tutorial\ animal10.st') fileIn; close
We will explain the new methods as they are used below.
A Prompter is a special kind of window--called a dialog window in graphic interface terms--which lets you ask a question and wait for a single response. For example, evaluate the following expression with Show It:
Prompter prompt: 'Do you know Smalltalk/V?' default: 'Yes, I''ve done a tutorial'.
A dialog pops up with the prompt: argument displayed as uneditable text, and the default: argument highlighted in a standard text pane. The user can either accept the response, or edit it. When you press the Enter key, or click the OK button, the prompter accepts your answer and displays it as the result of Show It. You can dismiss the prompter unanswered by pressing the Cancel button.
A unique characteristic of a prompter is that as long as it is displayed, you can activate other windows, but not the one which was immediately active when the dialog popped up. You must either accept or cancel the answer (which closes the prompter window) before you can activate the last active window. This provides a means of insuring that you can get critical responses from users before allowing them to continue activity in a window.
While a prompter dialog is, in a limited sense, a type of window--and it does play a critical role in implementing the host graphic interface--full scale applications implemented strictly through dialogs would be frustratingly limited in meeting the full potential of the host environment.
Fortunately, other windows in Smalltalk/V are not as restrictive as prompters. They are more under user, rather than program, control. Such windows can be quite simple, such as a Workspace window, or quite complex, such as a Class Hierarchy Browser.
The base image of Smalltalk/V supplies class definitions for a few simple, "generic" windows which you will find useful for incorporating into your own applications. In addition, you will find classes which implement the complex windows which make up the Smalltalk/V development environment. This chapter and the next two chapters will show you how to create your own complex windows to provide the interface to the applications you develop.
Let's start with a window with only a single text pane. Evaluate the following expression:
LearnWindow := TextWindow windowLabeled: 'Learning Status' frame: ( (Display boundingBox leftTop rightAndDown: 100 @ 100) extentFromLeftTop: 400 @ 200)
Respond Yes to the dialog to define LearnWindow as a global variable.
TextWindow is one of the Window classes whose instance variables, when paired with a TextPane, provide text editing capabilities. Sending the windowLabeled:frame: message to it creates a window occupying a rectangular area with extent '400 @ 200' on the screen, with the specified title centered in its title bar. Workspace windows are instances of TextWindow.
Although this new window automatically knows how perform interactive text editing by way of a user's keyboard and mouse input, you can write text from your active Transcript or Workspace window into the text pane of this new window by evaluating the following expression:
LearnWindow nextPutAll: 'I have learned everything about Smalltalk.'; cr.
LearnWindow is used here like a Stream, which you saw in Chapter 7. As you recall, the message nextPutAll: adds its String argument to the end of the receiver contents. The message cr then adds a line feed.
Click in the new window to select it, which activates the window and lets you edit the text in the window's TextPane.
To retrieve the contents of this window from another window, activate a different window and evaluate the expression with Show It:
LearnWindow contents
To close the window, select Close in the System menu of LearnWindow. Or, if you are in another window, simply evaluate the expression:
LearnWindow close
Although quite simplistic, you have manually instigated a common activity for windows. That is, in addition to the events which affect a window by direct user interaction--such as typing or mouse clicks--windows receive, and react to, messages sent by other windows, dialogs and menu item selections as your application runs.
The window you just created automatically inherits a window's standard text editing capabilities. In many cases, however, you'll want to customize a window to suit your application's design.
In Chapter 7, you developed an animal habitat with a script to command animals. In this section, we will build a custom window for editing and playing animal habitat scripts. You will build the window in stages adding features as you progress through this tutorial.
An important design style which you will see in even this rather simple example is that an application is best structured as a collection of cooperating classes. Generally, all but your most trivial applications will involve at least a closely related application class and its application window class.
Application classes, in this case AnimalHabitat and the related Animal classes, know about the abstract aspects of your application. Abstract here does not mean elusive or difficult to understand. Rather, we mean application classes describe the structure and behavior of the things of which your application is about, not what these things look like in a window or how these representations behave in that window. The related application window class--AnimalHabitat Window in this example--is created to capture the structure and behavior of a window which presents an interactive, visual representation of the objects modeled by the application classes.
The Animal classes model, or represent, animals with various attributes. These animals behave in ways appropriate to their species. The physical reality of a dog barking is captured in a Dog method which outputs a character string, "Bow Wow!" Not very realistic, but we get the idea. The application window design then needs a means to indicate that a dog in the habitat is barking. So we will include a TextPane in the window design.
AnimalHabitat is a class which implements an abstract model of a community of animals. The habitat's script describes a scenario of action among the animals which inhabit the habitat. The play method puts the script into action. To make a script play, we can envision a window design which includes a Habitat menu with a Play menu item. Selecting the Play menu item sends a message to the window's associated habitat asking it to execute its play method.
As you see, the application window class is intimately related to its application classes, but it is distinct from them. Keeping a clean separation between the classes of objects modeled in your application and the window class or classes which provide interaction with those objects will help you keep organized and productive in implementing increasingly complex applications.
The new window you create is for editing and playing scripts about how a group of animals behave together. Since the class AnimalHabitat contains the methods for playing scripts, it is reasonable to call this new window class AnimalHabitatWindow. When we want a habitat to "come to life," the habitat will ask the AnimalHabitatWindow class to create an instance of its type of window through which the habitat can visibly interact with us. In this way, an AnimalHabitat instance, the application class, is associated with an instance of AnimalHabitatWindows, its application window.
To inherit a collection of useful variables and methods which implement the basic structure and behavior of application windows, create this new class as a subclass of ViewManager by selecting and evaluating the following:
ViewManager subclass: #AnimalHabitatWindow instanceVariableNames: 'habitat replyStream inputPane' classVariableNames: ' ' poolDictionaries: ' '
The first instance variable, habitat, stores an instance of AnimalHabitat to which the window is associated. In this manner, you could create multiple AnimalHabitatWindow instances, have them all open on screen at once, and have them each associated with unique or shared instances of AnimalHabitat.
By convention, the message usually used to open a window is either open or open0n:. Since the window needs to know with which instance of AnimalHabitat it is associated, implement a method for the open0n: message. The following expression files in the first version of the open0n: method for the AnimalHabitatWindow class:
(File pathName: 'tutorial\windowl.st') fileIn; close
This method opens a window that behaves in the same way as a workspace or Transcript window:
openOn: anAnimalHabitat "Create a single pane window with the script of anAnimalHabitat as its initial contents." habitat := anAnimalHabitat. self label: 'Habitat'. self addSubpane: TextPane new. self openWindow
The first action upon opening an AnimalHabitatWindow instance is to associate the window with its AnimalHabitat instance by assigning the anAnimalHabitat argument to the habitat instance variable. This first line of open0n: performs the function of letting the window know and remember its state. This assignment does not directly affect the form and behavior of the window. The next three lines of code do that.
A ViewManager instance is the "skeleton" of a window, implementing the window frame, title, min/max icons, menu bar and System menu elements. To make the window capable of doing anything useful, additional messages must be sent to the window during its creation.
The openOn: method uses the special self variable to refer to the new window instance, the receiver of the openOn: message. The window is told to set its label to 'Habitat' and to add a new TextPane instance as a subpane of the window.
The last line then sends the openWindow message to the window, which is implemented in the window's superclass, ViewManager, and results in the window being created and appearing on your screen. (Behind the scenes, Smalltalk/V has communicated with the host Window Manager, telling it exactly what the look and behavior of the required application window should be. The Window Manager complies and creates the window which is then put under control of the Smalltalk/V environment.)
To set up the global variable Habitat and open the window, evaluate:
Snoopy := Dog new name: 'Snoopy'. Polly := Parrot new name: 'Polly'. Habitat := AnimalHabitat new add: Snoopy, add: Polly. Habitat script: 'Snoopy be quiet'. AnimalHabitatWindow new openOn: Habitat
A blank window with the label "Habitat" appears.
Congratulations, you have created your first customized window! However, this window cannot do much, except some interactive text editing. It does not even display the initial script known to the habitat, the argument anAnimalHabitat associated with the window. This script is not displayed because we have not defined a mechanism for the habitat to communicate with the window.
Before proceeding, close the window by double-clicking the System menu icon or selecting Close from the System menu.
Your next version of the openOn: method adds a means of communication between the habitat and the window. Evaluate the following expression to file in a more elaborate openOn: method for the AnimalHabitatWindow class:
(File pathName: 'tutorial\window2.st') fileIn; close
Here is the new code:
openOn: anAnimalHabitat "Create a single pane window with the script of anAnimalHabitat as its initial contents." habitat := anAnimalHabitat. self label: 'Habitat'. self addSubpane: (TextPane new owner: self; when: #getContents perform: #input:). self openWindow
In this method, we've sent two additional messages to the newly created TextPane:
owner: self tells the text pane the identity of the controlling window, in this case the instance of AnimalHabitatWindow referred to by self. This ownership relation means the text pane can send and receive messages to and from the application window.
when: #getContents perform: #input: tells the text pane that when it needs to get its contents, the text pane is to send the input: message to its owner (in this case, the AnimalHabitatWindow instance). A text pane needs to get its contents for display when its owning window is initially opened or when its contents are changed.
Among the administrative functions of the application window object is the responsibility to provide contents of the subpanes contained in the window. That's why we implemented the message input: in the class AnimalHabitatWindow to initialize the contents of the text pane:
input: aPane "Initialize inputPane with the string stored in the script of the owning window's associated habitat." aPane contents: habitat scriptString
Method names sent as the perform: argument of a when:perform: message always implement a binary message where the argument of the method performed is the subpane instance which is affected by the when: event.
We can now open the new window with the following expression:
AnimalHabitatWindow new openOn: Habitat
The initial script now appears when the window is opened.
The universal System, File, Edit and Smalltalk menus let you perform general activities in the Habitat window. To begin incorporating application-specific features into the Habitat window, you can add a new menu to the window's menu bar. Smalltalk/V handles this most efficiently by specifying a when:perform: message in the window initialization method which reacts to the getMenu event. Here's all you do...
The following expression files in the next version of the openOn: method together with the new inputMenu: and its associated methods:
(File pathName: 'tutorial\window3.st') fileIn; close
The openOn: message for AnimalHabitat now looks like this:
openOn: anAnimalHabitat "Create a single pane window with the script of anAnimalHabitat as its initial contents." habitat := anAnimalHabitat. self label: 'Habitat'. self addSubpane: (inputPane := TextPane new owner: self; when: #getContents perform: #input: ; when: #getMenu perform: #inputMenu:). self openWindow
To include a new menu in the window, you simply add one line to the openOn: method. The when: #getMenu perform: #inputMenu: message causes the window to execute the inputMenu: method, again with its argument being the subpane associated with the menu.
Here is the inputMenu: method which returns the new menu for the text pane:
inputMenu:aPane "Set the menu for the aPane text pane." aPane setMenu: ( ( Menu labels: '~Play Selection\Play ~All' withCrs lines: Array new selectors: #(playSelection playAll) ) title: '~Habitat'; owner: self; yourself)
The title: selector supplies a name to install in the window's menu bar. If you do not supply a title, Smalltalk/V will make one up. This allows the menu to be added to the menu bar and reminds you that you will probably want to supply a more descriptive title.
To make this menu work, we implement the following two new methods in AnimalHabitatWindow:
playSelection "Accept selected string as the script and play it to animals." CursorManager execute change. habitat script: inputPane selectedString. habitat play. CursorManager normal change playAll "Accept the entire content of the pane as the script and play it to animals." CursorManager execute change. habitat script: inputPane contents. habitat play. CursorManager normal change
The playSelection method plays only the selected text in the input pane, while playAll plays the entire text in the pane. Notice that we change the cursor shape to execute (shown as an hour glass) while playing the script, and then change it back to normal when finished.
Next we add a browse method to AnimalHabitat so a habitat will have the means to open an AnimalHabitatWindow on itself for user interaction:
browse "Open a window on the receiver." browser := AnimalHabitatWindow new openOn: self
The habitat's browser instance variable is assigned the new window instance. So the habitat knows the window associated with it and the window knows the habitat it is associated with by way of the window's habitat variable.
The application class and its associated window class are now adequately defined to work together in presenting an interface for interacting with a habitat. To try it, first teach the animals commands in the same way as you did in Chapter 7. The final statement asks the habitat to open a window on itself:
Snoopy learn: 'barking' action: [Snoopy talk]; learn: 'quietly' action: [Snoopy beQuiet; talk]; learn: 'is upset' action: [Snoopy beNoisy; talk]. Polly learn: 'to be pleasant' action: [Polly vocabulary: 'Have a nice day'; talk]; learn: '* nasty' action: [Polly vocabulary: 'Why are you bothering me'; talk]. Habitat script: ' Snoopy is upset about the way that Polly is behaving. It is as if whenever anyone asks Polly to talk, Polly will be nasty. Maybe if instead of Snoopy barking at Polly when he wants Polly to talk, Snoopy quietly asks Polly to be pleasant for a change, things would go better. Now maybe Snoopy barking quietly will not make Polly nasty.'. Habitat browse.
After the window is displayed, select Play All from the Habitat menu. You'll then see the dialogue in the Transcript window. Next, try selecting only a portion of the script. Then select Play Selection from the menu bar and watch the dialogue.
Multi-pane windows, such as the Class Hierarchy Browser or the Debugger, group related functions into several panes within a single window. This gives the user a clear picture of the application, since panes within one window do not overlay each other. Multi-pane windows are also ideal when panes within the window interact with one another to a high degree. For example, when you select a class in the Class Hierarchy Browser's class list pane, the contents of three other panes (the variable and method list panes and the bottom text pane) are automatically updated.
We'll now create a window which groups the animal pattern matching you saw in Chapter 7 with the animation from Chapter 9. In this window, we add two more panes to the previous example:
Figure 10.1
Animation Window
The input pane is the same as the one in the previous example, where you can edit the script to be played to the animals. The reply pane contains the dialogue from animals, so that we don't have to use the Transcript window any more. The animation pane is the stage where animals can perform in response to commands in the script.
The following expression files in a new openOn: method which opens the above multi-paned window and initWindowSize which establishes its default size:
(File pathName: 'window4.st') fileIn; close
Here is the new code:
openOn: anAnimalHabitat "Create a single pane window with the script of anAnimalHabitat as its initial contents." habitat := anAnimalHabitat. self label: 'K E N N E L'. self addSubpane: (replyStream := TextPane new owner: self; when: #getContents perform: #reply: ; framingRatio: (Rectangle leftTopUnit extentFromLeftTop: 2/3 @ (1/4))). self addSubpane: (AnimationPane new owner: self; when: #getContents perform: #pictures: ; framingRatio: ((Rectangle leftBottomUnit extentFromLeftBottom: 2/3 @ (3/4))). self addSubpane: (inputPane := TextPane new owner: self; when: #getMenu perform. #inputMenu: ; when: #getContents perform: #input: ; framingRatio: (Rectangle leftTopUnit extentFromLeftTop: 1/3 @ 1)). self openWindow
We call this window "KENNEL" because it will contain only dogs. (Your Smalltalk/V diskettes include the FreeDrawing utility, with which you can draw your own pictures of different animals and try them out.) The window consists of three panes: a text pane associated with the reply: method, an animation pane associated with pictures:, and another text pane associated with the input: method.
We set the instance variable, inputPane, to point to the input pane, so that later we can use it to reference the pane's contents. We also set another instance variable, replyStream, to point to the reply pane, so that we can later use it like a stream and output things to it.
Since the size of each subpane depends on the size of the whole window, the message framingRatio: defines the position and size of each pane relative to its window. The coordinates of the rectangle argument to framingRatio: are a fraction of the width or height of the window. For example, if the window rectangle is:
100 @ 100 extent: 300 @ 200
then a framing ratio of:
Rectangle leftTopUnit rightAndDown: (2/3 @ 0) ) extentFromLeftTop: 1/3 @ 1
evaluates to proportional rectangle:
(2/3 @ 0 bottomRight : 1 @ 1)
which yields the rectangle:
(100 @ 100) + ((300 @ 200) * (2/3 @ 0)) extent: (300 @ 200) * (1/3 @ 1)
which is equivalent to:
300 @ 100 extent: 100 @ 200
Since we want the window to open with a particular size, we provide the method initWindowSize which is used when the window is first opened:
initWindowSize "Answer the initial window extent" ^(Display boundingBox insetBy: 16 @ 16) extent
If you do not supply an initWindowSize method, the system uses a standard default size.
Several other new methods need to be added and existing methods need to be modified before we can try out the new window. The following code files in the changes and additions:
(File pathName: 'tutorial\window5.st') fileIn; close
The following two methods initialize the two additional panes:
reply: replyPane "Initialize reply pane with an empty String." replyStream contents: String new pictures: animationPane "Initialize animation pane objects." animationPane contents: ( habitat animals collect: [:animal | animal picture] )
Now, we'll slightly modify the previous example's playSelection and playAll methods by adding the message changed:, which asks the pane identified by its Symbol argument to reinitialize the pane contents:
playSelection "Accept selected string as the script and play it to animals." self changed: #reply:. CursorManager execute change. habitat script: inputPane selectedString. habitat play. CursorManager normal change playAll "Accept the entire content of the pane as the script and play it to animals." self changed: #reply:. CursorManager execute change. habitat script: inputPane contents. habitat play. CursorManager normal change
Next, we'll change the answer: method to write to the reply pane instead of the Transcript window:
answer: aString "Output aString to the reply Pane." replyStream nextPutAll: aString; cr
Next, we add a new close method rather than relying on the close method inherited from ViewManager. The message ^super close kills the window only after first notifying the window's associated habitat that the window is closing:
close "Window is to be closed, disconnect the habitat instance from the window before killing the window." habitat closeBrowser. ^super close
In the AnimalHabitat class, closeBrowser simply sets the browser variable to nil so the habitat won't think there is still a window around through which to interact with the user.
At the beginning of this tutorial we filed in changes and additions to the Animal methods. We present them in this section.
Since we are going to animate the animal objects, we need to provide methods for initializing and accessing an animal's color and picture attributes. We are storing the color as an integer representing the desired color and the picture as a series of images of the animal in motion that can be used by the Animation class described in Chapter 9. Here are these methods:
color "Answer the receiver's color." ^color name: aString picture: images color: aColor "Initialize the receiver's name, pictures, and color." name := aString. color := aColor. picture := AnimatedObject frames: images. picture color: aColor picture "Answer the receiver's AnimatedObject." ^picture
We need to provide behavior that lets the animals move about the graph pane. Since we are only using dogs for our example, we present the new methods for class Dog:
run "Run for distance." | distance | distance := habitat script peek asInteger. self answer: 'I am running ', distance printString, ' feet'. picture speed: topSpeed; go: distance // 10 turn "Turn direction with anAngle." | anAngle | anAngle := habitat script peek asInteger. self answer: 'I am turning ', anAngle printString, ' degrees'. picture turn: anAngle walk "Walk for distance." | distance | distance := habitat script peek asInteger. self answer: 'I am walking ', distance printString , 'feet'. picture speed: topSpeed // 2; go: distance //10
In addition there are methods implementing the following new messages: bounce, topSpeed, direction, and home. The implementations are very similar to the ones given above. You can use the Class Hierarchy Browser to view them.
Evaluate the following to install the dog bitmaps if you did not save the image after doing Chapter 9:
"Load the dog bitmaps from disk files." | dogPictures | TutorialPictures := Dictionary new. dogPictures := Array new: 4. 1 to: dogPicture size do: [ :i | dogPictures at: i put: ( Bitmap fromFile: 'tutorial\dog', i printString, '.bmp') ]. TutorialPictures at: 'dogs' put: dogPictures
Now that the methods are defined, we can initialize the animals:
| dogImages | dogImages := TutorialPictures at: 'dogs'. Kennel := AnimalHabitat new. Kennel add: (Snoopy := Dog new name: 'Snoopy' picture: dogImages color: ClrBlack). Kennel add: (Lassie := Dog new name: 'Lassie' picture: dogImages color: ClrBrown). Kennel add: (Wow := Dog new name: 'Wow' picture: dogImages color: ClrRed).
The above expressions add three dogs to the Kennel. Each dog has the same array of pictures but different colors. (Chapter 9 explains animation in detail.)
Next we teach each dog the commands that define their behavior. Since all three dogs learn the same set of commands, we show only one dog's learning here:
Snoopy learn: 'bark a little' action: [Snoopy beQuiet]. Snoopy learn: 'bark a lot' action: [Snoopy beNoisy]. Snoopy learn: 'talk action: [Snoopy talk]. Snoopy learn: 'home' action: [Snoopy home]. Snoopy learn: 'top speed' action: [Snoopy topSpeed]. Snoopy learn: 'run' action: [Snoopy run]. Snoopy learn: 'run inside kennel' action: [Snoopy bounce]. Snoopy learn: 'walk' action: [Snoopy walk]. Snoopy learn: 'direction' action: [Snoopy direction]. Snoopy learn: 'turn' action: [Snoopy turn].
Finally, evaluate the following expression to open the new window with a starting script:
Kennel script: 'Snoopy home Lassie home Wow home Snoopy top speed 40 Lassie top speed 30 Wow top speed 20 Snoopy bark a little Lassie bark a little Wow bark a lot Snoopy direction 45 Snoopy walk 200 Snoopy talk Lassie direction 90 Lassie walk 150 Lassie talk Lassie turn 225 Lassie run 150 Lassie talk Wow direction 0 Wow walk 300 Wow talk Wow turn 135 Wow run 250 Wow talk Snoopy run inside kennel 100 feet'. Kennel browse
Select Play All from the Habitat menu to play the entire script, You see the dogs' dialogue in the reply pane, and their performance in the animation pane. The final window is shown in the following picture.
Figure 10.2
Kennel
Try composing your own scripts in the input pane, and then select either all or a portion of it to play.
By the end of this tutorial, you are familiar with:
You've seen a complex interactive application which provides many of Smalltalk's most important building blocks. In particular, you have seen how it is typical for a Smalltalk application to be structured as a combination of application classes and their associated application window classes.
As always, if you want to review any of the topics covered in this tutorial, either repeat the corresponding section of the tutorial or refer to the detailed description in Part 3.
If you are going to exit Smalltalk/V before proceeding on to the next tutorial, be sure to save the image.