Hotspots: Admin Pages | Turn-in Site |
Current Links: Cases Final Project Summer 2007
Creating Custom Widgets in VisualWorks
by Brandon Carpenter (brandon [dot] carpenter [at] gatech [dot] edu)
What is a Widget?
VisualWorks Smalltalk has a really great GUI creation tool called the painter. To use the painter, you drag icons from the Palette window onto the Canvas you are creating. Voila! A new piece of your GUI is now positioned and ready for use! Each of these pieces that are draggable from the Palette are called Widgets.
Occasionally none of the default Visualworks widgets suit your needs exactly. In these cases creating your own custom Widget is the solution. (Note: Often it makes more sense to extend an existing widget than to create a widget from scratch. However in order to gain a better understanding of Widgets, we will discuss how to build a Widget from it's most basic components.)
VisualWorks Smalltalk GUI internals.
The best way to find your way around VisualWorks' GUI system is to poke around inside one. So lets start by creating a new GUI with the Painter tool. In the main VisualWorks window choose Painter->New Canvas. Now drag some widgets onto the canvas if you'd like, or leave it blank, and have the Painter Tool build your ApplicationModel class by choosing Edit->Install in the GUI Painter Tool window. What you name the class doesn't really matter, but for this example we'll call it widgetExampleGUI. So type WidgetExampleGUI into the textbox after "INSTALL on Class", click ok, and then click ok again in the second dialog which asks you if it should Create the new class. Now that it has created the new class we can click ok again on the first dialog, and we can close the "Unlabeled Canvas" window. This will close the rest of the Painter windows as well. If you wish you can now look at the Class method windowSpec in WidgetExampleGUI to see what the Painter tool has generated.
Now let's inspect our ApplicationModel by going to a workspace, typing "WidgetExampleGUI open." (without the quotes) and pressing the button in the toolbar that looks like a lignting bolt with a magnifying glass (i.e. The "Inspector" tool). This will open two new windows: one is the window you created, the other is an "Inspector" window that lets you view the instance variables and methods of the object. Note that the title bar of the Inspector window says "an UIBuilder". This UIBuilder is the object that takes all of the text in the WindowSpec and converts it into an actual GUi. So we can learn about the heirarchy of a GUI by looking through the contents of the UIBuilder from within the Inspector window. Some items of interest would be the source variable, the namedComponents list, the window, and the composite.
As you're poking around, you may notice that the basic way that the VisualWorks GUI system works is that the UIBuilder takes various "specs", and "wrappers", and converts them into something that shows up on the screen.
Anatomy of a Widget.
A Widget is actually comprised of four seperate classes, each with seperate responsibilities. The class of most note is the Widget's Specification, or Spec, class. This is the class that the UIBuilder uses to actually compose the Widget, and put it into a GUI. The three other classes fulfill the classic "Model View Controller" architecture. We will begin our spelunking into the world of widgets by looking at the Widget Spec class and its applications. Next we will discuss the Widget's Model, View, and Controller classes. Finally an example file is included wich demonstrates each of the four classes in a minimal widget.
The Widget Spec
The Widget Spec is the heart of the Widget. It tells the UIBuilder how to build the Widget! The normal class to extend when creating new Widgets is called WidgetSpec. The full class heirarchy for WidgetSpec is as follows:
Core.Object -> UI.UISpecification -> UI.ComponentSpec -> UI.NamedSpec -> UI.WidgetSpec.
If you like you can delve into the source for all of WidgetSpec's superclasses to understand them more thouroughly. They aren't that complicated. Also, the first pdf reference ("Adding A Widget") explains more of what each superclass provides.
But for the purposes of our example, it is sufficient to know that subclassing WidgetSpec is the easiest way to build your own Widget's Specification. So create a new class, we'll call it ExampleWidgetSpec, and make it be a subclass of UI.WidgetSpec.
If you left the box labeled "Subclass Responsibilities" checked in the new class dialog, you will notice an instance method in the private protocol named defaultModel. If you created the class manually or did not leave the box checked, create the private protocol and defaultModel methods now. Either way you're going to need to fill in the method by having it return a new instance of the Widget's model. For now make it return an empty block (the code is ^). We'll put a real model there once we make one.
The Widget Model
Let's go ahead and make the model while we're at it! For our example we're simply going to make a Widget that displays a colored box that toggles between two colors when you click on it. So the model part of our widget should store the color we're currently displaying. We'll start by making a subclass of UI.Model named WidgetExampleModel. Our model is going to contain a single instance variable named red. In the initialize instance method, intialize red to true. Also create getting and setting methods for red. Our view class will use this boolean to display the square in the color red if the getter returns True, or blue if the getter returns false.
(red = nil) ifTrue:[red := true].
red := aBool
Now go back to our ExampleWidgetSpec class and fill in the defaultModel method to return a new instance of ExampleWidgetModel.
The Widget Controller
The Widget's Controller is used to manipulate the model in accordance to what is going on in the GUI. In other words, it lets you make changes to the model whenever the user does fun things like typing on the keyboard, or clicking the mouse.
The grandaddy (well, maybe just the daddy) Widet Controller class is UI.Controller. So create a subclass of UI.Controller named, you guessed it, ExampleWidgetController. This class is the one that's going to toggle the model's red value back and forth between True and False, whenever the user clicks on it.
The method that we're going to override to capture a left mouse click is the redButtonPressedEvent: method in the events protocol of Control. So add the following method into the events protocol of our ExampleWidgetController class.
(self model red)
ifTrue:[self model red:false]
ifFalse:[self model red:true].
self view update:#red.
^super redButtonPressedEvent: event
The (self view update:#red) line causes the view to redraw itself. Otherwise, the change would be taking place but we wouldn't be able to tell unless we resized the window or caused it to redraw itself through some other action. It doesn't actually matter what symbol you give to the update method, but #red seems appropriate here. Note that it's a really good idea to call the superclass's method so that the original processing still gets done. For more fun events to tap into, check out the UI.Controller class's code in the System Brower.
The Widget View
Finally, let's make the Widget's View, or what appears on the screen. To do this we are going to create a new subclass of the abstract class UI.SimpleView. Keeping our boring naming scheme, let's call it WidgetExampleView.
The interesting method of SimpleView that we're going to be implementing to display our colored square is called displayOn:. So go ahead and create the instance protocol display and add the method displayOn:aGraphicsContext. Whenever this method is called by the UIBuilder, it is given a GraphicsContext object to draw itself on. To see all the things you can do with a GraphicsContext, open it up in the System Browser and look around. Our final code for the displayOn:aGraphicsContext method looks something like this:
(self model red)
ifTrue: [aGraphicsContext paint: (ColorValue red)]
ifFalse: [aGraphicsContext paint: (ColorValue blue)].
(self bounds) displayFilledOn: aGraphicsContext.
Note that (self bounds) returns a Rectangle object representing the bounds that this Widget is allowed to paint in. For more information about Rectangles, view it's source in the System Browser.
Building the Component
Now, similar to what we had to do with the model, we have to somehow let the UIBUilder know to associate this View with our ExampleWidgetSpec. This can be a bit hairy, but stick with me. When the Builder needs to build a Widget it calls the Spec's dispatchTo:with: method. The second parameter is a reference to the builder itself. The first parameter is an object of type UI.UILookPolicy. The Spec is then responsible for calling the correct method in UILookPolicy. Note that there are several subclasses of UILookPolicy, which are used to create widgets that look and feel like native components of the operating system that the program is being run on. However we will be placing our method in UILookPolicy itself so that the View will be the same no matter what platform the program is being run on.
So open up UI.UILookPolicy in the System Browser and look in the instance protocol building. If you want you can look through some of the methods used in building other widgets. Generally they take in the Spec and the Builder, wraps the Spec in some borders and whatnot, and applies the Spec's layout. We're going to create our own method. Here it is:
exampleWidget: spec into: builder
| component model |
model := spec modelInBuilder: builder.
component := ExampleWidgetView model: model.
component controller: ExampleWidgetController new.
self setDispatcherOf: component fromSpec: spec builder: builder.
builder isEditing ifFalse: [component widgetState isVisible: spec initiallyVisible].
builder component: component.
builder wrapWith: (self simpleWrapperFor: spec).
builder applyLayout: spec layout.
builder wrapWith: (self simpleWidgetWrapperOn: builder spec: spec)
You may notice that I obtained this code by copying the actionButton:into: method and making the necessary changes. You can learn a lot about the building process by viewing the other methods in the building protocol of UI.UILookPolicy.
Finally, let's go back to our ExampleWidgetSpec class and fill in the dispatchTo:with: method so that it calls our newly created method in the UILookPolicy object.
policy exampleWidget:self into:builder
Creating the Spec's Edit Pages
We're almost there! From the main VisualWorks window choose Painter->New Canvas, and add any widget to the canvas. Now see those cool tabs in the GUI Painter Tool window (Basic, Details, ...)? It turns out that we have to create the Basic and Details pages for our Widget. We'll just use a blank canvas. So go ahead and delete the widget that you added to the canvas. Then in the GUI Painter Tool window choose Edit->Install. Be sure to install it on our Spec class, ExampleWidgetSpec, with the new selector basicsEditSpec. Then repeat the process, installing in ExampleWidgetSpec with the new selector detailsEditSpec. Of course if you wanted to you could have added Labels, TextFields, and such to the canvas, and installed the TextFields' aspects into the Spec class for use in affecting the model and such!
Using the Widget Spec with the Painter's Palette.
Alright, we're done with our Widget! But, Having a Widget doesn't do us much good until we can use it with the Painter tool! There are a few more things that we need to do before we get there. We need to give our widget an Icon that gets displayed in the Palette. So go to the main VisualWorks window, choose Painter->Image Editor, and draw until you're heart's content. The go to the Image Editor's Image menu and choose Install. For the class to install the image into type the name of our Spec class, ExampleWidgetSpec, and in the "Enter a new selector" textfield type paletteIcon. Submit the changes! We also have to make a version of the Icon for monochrome screens. So go ahead and convert your image to black and white and install it in the same class with the selector paletteMonoIcon. Make sure that you save your Visualworks Image so that you don't have to go back and draw your icon pixel by pixel again.
Also we want our Widget to have a name! So create a class method named componentName and have it return a string that is the title of your Widget. So for our example we will use the code ^'Example Widget!'.
Now we can add the Spec to the Painter's Palette so that you can drag it onto a canvas like other Widgets. In a workspace past the following code, highlight it with your mouse, and click the Run It! button.
(UIPalette activeSpecsList includes: #ExampleWidgetSpec)
ifFalse:[UIPalette activeSpecsList add: #ExampleWidgetSpec].
If you later want to remove the widget use:
UIPalette activeSpecsList remove: #ExampleWidgetSpec ifAbsent:.
According to the first PDF source listed below ("Adding A Widget"), placing the code for adding the Spec to the pallette into the Class method initialize will cause the Spec to be automatically loaded into the Palette whenever the class is Filed-in, but I have not tested this.
Try it out!
Now reopen that WidgetExampleGUI that we fooled around with at the beginning in the UIPainter tool and add our new widget to it! To reopen the canvas go to the main VisualWorks window and choose Painter->Resource Finder. Then find our WidgetExampleGUI class and open the windowSpec resource. Add our widget to the canvas, and be sure to install the edited canvas back into the WidgetExampleGUI class with the selector windowSpec. Then run the WidgetExampleGUI application and try it out! And give yourself a pat on the back. And think, "Man that Brandon dude was awesome for posting this great tutorial to further my education and make my life easier."
RTFS! (Read the source!)
Links to this Page
- Cases last edited on 30 July 2011 at 2:33 am by r59h132.res.gatech.edu
- Index of Individual Cases last edited on 3 May 2011 at 12:46 pm by r52h48.res.gatech.edu