Today we look at the Form, and the two basic ways to use it.
Design
We'll be making three UserInterface. We'll start with one we'll name FormWork, and it will have a method that creates a Form object, and adds panes to it, then we'll call that twice giving each a different position on the window.
Then we'll create a UserInterface we'll name AddressUserInterface, which will have a singe set of panes on it. We'll then create a UserInterface named MoreFormWork which will load the AddressUserInterface into two forms.
Here is our starting UserInterface subclass:
Smalltalk defineClass: #FormWork
superclass: #{Widgetry.UserInterface}
indexedType: #none
private: false
instanceVariableNames: ' '
classInstanceVariableNames: ''
imports: 'private Widgetry.*'
category: '(none)'
Dress Making
A Form is in essence a grouping of panes gathered into a container. The purpose of a Form is mostly for reuse. You can create many instances of a Form, and place them in a single user interface, or use a Form in many UserInterfaces.
Our example today is a simple set of address fields. Some Labels and InputFields to allow the entering of a street address, city, state and zip. I always like this example, because this is the basis of a very common pattern in some everyday application development: Ship To and Bill To addresses. With the Form, we'll be able to create a generic address form, and be able to use it for ship to and bill to.
Our first example as implied in the Design section, is a method that answers a Form, with all its parts all laid out. This is what that looks like:
addressForm
| form label inputField |
form := Form new.
label := DisplayLabel string: 'Street'.
label frame extent: 30 @ 25.
form addComponent: label.
inputField := InputField new.
(inputField frame)
inset: 33 @ 0;
extent: 140 @ 25.
form addComponent: inputField.
label := DisplayLabel string: 'City'.
(label frame)
inset: 0 @ 28;
extent: 30 @ 25.
form addComponent: label.
inputField := InputField new.
(inputField frame)
inset: 33 @ 28;
extent: 35 @ 25.
form addComponent: inputField.
label := DisplayLabel string: 'State'.
(label frame)
inset: 70 @ 28;
extent: 25 @ 25.
form addComponent: label.
inputField := InputField new.
(inputField frame)
inset: 97 @ 28;
extent: 25 @ 25.
form addComponent: inputField.
label := DisplayLabel string: 'Zip'.
(label frame)
inset: 124 @ 28;
extent: 15 @ 25.
form addComponent: label.
inputField := InputField new.
(inputField frame)
inset: 142 @ 28;
extent: 31 @ 25.
form addComponent: inputField.
^form
You may notice I'm reusing the same temporary variables over and over here. For this posting, we're concentrating on Forms and how to use them. In the future we'll talk about how to access panes in a Form.... For a hint though, look at #paneAt:.
Now, in our #createInterface method, we'll get that form, give it a frame, and put it in our window. If you look at my layouts, you'll see that the total width is 173. So we'll make our form have a width of 175 because I'm fond of that rounded number, and the total height is 53, and again, I'll round it out to 55.
createInterface
| addressForm |
addressForm := self addressForm.
(addressForm frame)
inset: 10 @ 10;
extent: 175 @ 55.
self addComponent: addressForm
Note that we gave our Form an inset of 10 @ 10... Here is what this looks like when we execute FormWork open:
Yeah, I know, not the most compelling layout or sizes, but you get the picture.
Pig Iron
Now we'll add a second instance of our address form, below the first:
createInterface
| addressForm |
addressForm := self addressForm.
(addressForm frame)
inset: 10 @ 10;
extent: 175 @ 55.
self addComponent: addressForm.
addressForm := self addressForm.
(addressForm frame)
inset: 10 @ 75;
extent: 175 @ 55.
self addComponent: addressForm
And here is what it looks like after I've played with the input fields some:
Kenetic Energy
For our second example, we'll first create a whole new UserInterface we'll name AddressUserInterface, and make instance variables for all those input fields:
Smalltalk defineClass: #AddressUserInterface
superclass: #{Widgetry.UserInterface}
indexedType: #none
private: false
instanceVariableNames: 'street city state zip'
classInstanceVariableNames: ''
imports: 'private Widgetry.*'
category: '(none)'
Now we'll take our #addressForm method from our FormWork class, and make it into a #createInterface method, using instance variables for the InputFields instead of reusing the inputField temporary. Yes, I know this is reuse by copy/paste... so sue me.
Note, we don't use the form in this method, we just add components directly to the user interface itself:
createInterface
| label |
label := DisplayLabel string: 'Street'.
label frame extent: 30 @ 25.
self addComponent: label.
street := InputField new.
(street frame)
inset: 33 @ 0;
extent: 140 @ 25.
self addComponent: street.
label := DisplayLabel string: 'City'.
(label frame)
inset: 0 @ 28;
extent: 30 @ 25.
self addComponent: label.
city := InputField new.
(city frame)
inset: 33 @ 28;
extent: 35 @ 25.
self addComponent: city.
label := DisplayLabel string: 'State'.
(label frame)
inset: 70 @ 28;
extent: 25 @ 25.
self addComponent: label.
state := InputField new.
(state frame)
inset: 97 @ 28;
extent: 25 @ 25.
self addComponent: state.
label := DisplayLabel string: 'Zip'.
(label frame)
inset: 124 @ 28;
extent: 15 @ 25.
self addComponent: label.
zip := InputField new.
(zip frame)
inset: 142 @ 28;
extent: 31 @ 25.
self addComponent: zip.
If we execute AddressUserInterface open, here is what we see:
You'll notice that the widgets are all pushed up to the top and left. That is what we want.
Abracadabra
Now we create a new UserInterface subclass which will use our AddressUserInterface twice:
Smalltalk defineClass: #MoreFormWork
superclass: #{Widgetry.UserInterface}
indexedType: #none
private: false
instanceVariableNames: 'billTo shipTo'
classInstanceVariableNames: ''
imports: 'private Widgetry.*'
category: '(none)'
Here is how we use our prior AddressUserInterface for our forms:
createInterface
billTo := Form new.
billTo addComponentsFromClass: AddressUserInterface.
(billTo frame)
inset: 10 @ 20;
extent: 175 @ 55.
self addComponent: billTo.
shipTo := Form new.
shipTo addComponentsFromClass: AddressUserInterface.
(shipTo frame)
inset: 10 @ 100;
extent: 175 @ 55.
self addComponent: shipTo.
Here is what it looks like when we execute MoreFormWork open:
The method #addComponentsFromClass: takes a UserInterface, and collects all of the panes in it, and adds them to the Form.
You might now be able to imagine how with this facility, you can build up UserInterfaces from other UserInterfaces and so on.
Extra Credit
We'll add a few GroupBoxes:
createInterface
| groupBox |
groupBox := GroupBox string: 'Bill To:'.
(groupBox frame)
inset: 5 @ 5;
extent: 185 @ 75.
self addComponent: groupBox.
groupBox := GroupBox string: 'Ship To:'.
(groupBox frame)
inset: 5 @ 85;
extent: 185 @ 75.
self addComponent: groupBox.
billTo := Form new.
billTo addComponentsFromClass: AddressUserInterface.
(billTo frame)
inset: 10 @ 20;
extent: 175 @ 55.
self addComponent: billTo.
shipTo := Form new.
shipTo addComponentsFromClass: AddressUserInterface.
(shipTo frame)
inset: 10 @ 100;
extent: 175 @ 55.
self addComponent: shipTo.
This will make it ready for next time when we have fun with TabControls...
Isn't that pretty?
And So It GoesSames