Wizards are useful for any task consisting of several steps, which should be done in a certain order. In general, I’m not a big fan of wizards. They seems like a return to the old days where applications forced you to fill in fields in a certain order. But there are some uses for them, so I decided to come up with a simple wizard framework for Suneido.
I’ll use a simple example – a wizard to create a database table. Here is the code (using the wizard framework):
CreateDatabaseTableWizard
Wizard << 1 { Title: 'Create a Database Table' << 2 Pages: ( (Vert Title: "Table Name" << 3 (Field name: table) (Static "e.g. my_table")) (Vert Title: 'Fields' (Editor name: fields) (Static "e.g. name, phone, email")) (Vert Title: "Keys & Indexes" (Pair (Static Key) (Field name: key)) (Pair '' (Static "e.g. name")) Skip (Pair (Static Index) (Field name: index1)) (Pair (Static Index) (Field name: index2)) (Pair (Static Index) (Field name: index3)) (Pair '' (Static "e.g. phone"))) ) Finish() << 4 { data = .Data.Get() schema = "create " $ data.table $ "\n" $ << 5 "(" $ data.fields $ ")\n" $ "key(" $ data.key $ ")\n" for (i = 1; i <= 3; ++i) if data["index" $ i] isnt '' schema $= "index (" $ data["index" $ i] $ ")\n" try << 6 { Database(schema) return true } catch (err) { Alert(err, .Title) return false } } }
- We start by deriving (inheriting) from the Wizard class.
- The Title goes on the wizards title bar.
- Next, we supply a list of pages for the wizard. Each one is a user interface control specification. Each one also specifies a Title for the page. To keep the example self contained, I used explicit Pair’s with Static’s for prompts. In practice, Field_ definitions would probably be simpler.
- Finally we define a Finish method that will be called when the user clicks on Finish.
- We build a database create request from the user’s input. Rather than repeating the code to add data.index1, data.index2, data.index3, we use a loop with data[“index” $ i]
- Since we’re not doing any validation of the user input, we’d better catch any errors when we try to execute the database create. If there is an error, we simply use an Alert to show it to the user. If the command succeeds, we return true so the wizard will close, otherwise we return false so the wizard will stay open.
Note: This is just a simple example – it could obviously be improved.
We can run the wizard with simply:
CreateDatabaseTableWizard()
And here is what the result looks like:
(The pages don’t show up stacked like this – this is multiple screenshots so you can see the pages.)
The Framework Code
The code for Wizard is quite straightforward. FlipControl takes care of handling the pages. Here is the code:
Wizard
Controller { Title: Wizard Xmin: 400 Xstretch: 1 Ymin: 300 Ystretch: 1 CallClass(hwnd = 0) << 1 { return Dialog(hwnd, this, border: 0, style: WS.SYSMENU | WS.SIZEBOX, exStyle: WS_EX.DLGMODALFRAME) } New() { .data = .Vert.Pages.GetChildren()[0] << 2 .flip = .data.GetChild() buttons = .Vert.Buttons.GetChildren[0]() .back = buttons._Back .next = buttons.Next_ .finish = buttons.Finish .pagetitle = .Vert.Header.Title .enableButtons() << 3 } Controls() { .pages = .Pages .npages = .pages.Size(list:) return Object('Vert', Object('Border', Object('Title', .pages[0].Title, xstretch: 1), name: 'Header'), << 4 'EtchedLine', Object('Border', Object('Record', << 5 .pages.Copy().Add('Flip' at: 0)), name: 'Pages', ystretch: 1), 'EtchedLine', #(Border (Horz Fill (Button '< Back' xmin: 75) (Button 'Next >' xmin: 75) Skip (Button Finish xmin: 75) Skip (Button Cancel xmin: 75)) name: Buttons) ) } On__Back() { .move(-1) } On_Next_() { .move(+1) } move(inc) << 6 { cur = .flip.GetCurrent() .flip.SetCurrent(cur += inc) .enableButtons() << 3 .pagetitle.Set(.pages[cur].Title) } enableButtons() << 3 { cur = .flip.GetCurrent() .back.SetEnabled(cur isnt 0) .next.SetEnabled(cur isnt .npages - 1) .finish.SetEnabled(cur is .npages - 1) } On_Finish() << 7 { if .Finish() is true .Window.Result(true) } Finish() // should be redefined by derived class { return true } On_Cancel() { .Window.Result(false) } Get_Data() << 8 { return .data } }
- Defining a CallClass lets us simply “call” our wizards to run them. We turn off the default dialog border so that we can make the EtchedLines extend all the way to the window edge.
- As usual, in the New we set up references to the controls we’re interested in. This makes the code shorter, and also, if we change the control layout, we only have to update the control “paths” in one place.
- The enableButtons method enables and disables the Back, Next, and Finish buttons depending on which page we’re on.
- Because we have two Border controls we need to give them unique names so that we can refer to their contents.
- We wrap a RecordControl around the pages so that we can access the data from all the controls. Note: This means control names need to be unique over all the pages.
- The move method is used by Back and Next to switch pages. We get the current page, flip to the next/previous page, call enableButtons, and then set the new page title. Notice the extra underscores ( _ ) on On__Next and On_Back_. This is because the “<” and “>” characters are stripped out and the spaces are converted to underscores.
- On_Finish simply calls the Finish method. If it returns true, the dialog is closed with a result of true. (Whereas On_Cancel closes the dialog with a result of false.) A default Finish method is provided, but derived wizards are expected to define their own Finish method.
- The Get_Data method allows wizards to access the RecordControl’s data. Giving the method a name starting with “Get_” allows us to access the record control as simply .Data (as in the initial example).
That’s all there is to it. Try it out and let me know what you think.
Download the code including a required fix to FlipControl from: wizlib.zip
Unzip the file, load the library from the command line with:
suneido -load wizlib
and then Use wizlib.
Possible Improvements
- Make Finish the default button on the last page, Next on the other pages.
- Allow controlling the page order. For example, to skip over pages that are not relevant based on your choices.
- Allow each page to determine whether it is “complete” and then disable the Finish button unless all pages are “complete”.
- Support validation of the data entered. Add a status bar to display validation messages. Disable Finish if any page is invalid. Possibly disable Next if the page is not valid.
- Allow adding an image to the title area.
References
Creating JFace Wizards– http://www.eclipse.org articles