Updated for release version: 1.0.0-alpha
Hello GUI (or, Working with Graphic User Interfaces)
This tutorial will guide you through the basics of creating your own Interface Panel and associated Dialog Box.
Swing
Since the modelGUI GUI is constructed entirely upon Sun's Swing library, which in turn is built upon its Abstract Window Toolkit (AWT), your first task is to acquaint yourself with this library. Although we will be running you through all the steps of creating GUI here, it is crucial that you understand how Swing works, if you want to start getting creative. Luckily, Sun has provided a large number of tutorials to ease you into Swing. You don't need to try them all now, but it's good to know they exist when you start running into questions down the line. Like Swing, ModelGUI is based upon a Model-View-Controller (MVC) framework.
Interface Panel
An interface panel is the basic building block for a GUI component in modelGUI. InterfacePanel is an abstract class which extends the Swing component JPanel, and implements a number of Java interfaces which are shown in its declaration:
public abstract class InterfacePanel extends JPanel implements InterfaceObject, DisplayListener, AttributeObject, AttributeListener, ComponentListener, ShapeListener, IconObject, PopupMenuObject, MouseListener, CategoryObject
It would be a big task to go through all of these interfaces right now, so we won't. It will suffice to say that InterfacePanel is the basic container for building a GUI component in modelGUI. Perhaps the best way to go about this task is to have a look an existing panel; we can start a new class and build it up from the examplar as we go. For this purpose, InterfaceFilePanel is a good examplar. Its class declaration looks like:
public class InterfaceFilePanel extends InterfacePanel implements ActionListener, InterfaceIOPanel
So let's start a new java class called HelloGUI.java, and put it in the package mgui.tutorials. The first thing we want is a declaration similar to InterfaceFilePanel. We don't need to worry about implementing the InterfaceIOPanel interface, but the ActionListener interface is quite useful. It is the Swing interface that is used to respond to GUI events such as button presses, as we'll see soon. ActionListener specifies a single method, which is the actionPerformed method; so we should also include a default implementation of this method. Our initial HelloGUI.java class should look like this then:
package mgui.tutorials; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import mgui.interfaces.InterfacePanel; public class HelloGUI extends InterfacePanel implements ActionListener{ public void actionPerformed(ActionEvent e){} }
Layouts
Before we go any further, we should give some thought to what we actually want this new interface panel to do. A related question is how should we organize its appearance? Since this is a simple tutorial, let's think of a simple task: we want to allow our user to make the shape model visible or invisible (in which case we should have named it "HelloGoodbyeGUI", but we won't). One way to do this is to provide two buttons, one which says "Hello" and one which says "Goodbye". Another way is to provide a check box labelled "Show World", which turns the visibility off and on depending upon whether it's checked. We'll provide both of these options, because it allows me to segway into an introduction of Layouts.
A layout (or Layout Manager) is a Swing concept which allows the programmer to specify how GUI components are visually organized within a container. Swing provides a number of layout managers by default (see here). You are quite free to use any layout you want for your interface panel, or design your own. For convenience, however, mgui provides two simple layout managers that most core panels utilize. The first is called LineLayout, which allows you to specify a line height and gap size, and then simply add components as separate lines. The second is called CategoryLayout, which behaves the same as LineLayout, but adds the ability to organize the layout into collapsible "categories". For HelloGUI we'll use the latter, making separate categories for our two approaches.
As we can see from InterfaceFilePanel, the first component added to the panel is an instance of CategoryTitle. This is an extension of JButton which is specifically for use with CategoryLayout. Let's say we want two categories, one called "Buttons" and the other called "Checkbox". We add these as shown in the updated HelloGUI file:
package mgui.tutorials; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JCheckBox; import mgui.interfaces.InterfacePanel; import mgui.interfaces.layouts.CategoryTitle; public class HelloGUI extends InterfacePanel implements ActionListener{ //declare Swing components CategoryTitle lblButtons = new CategoryTitle("BUTTONS"); JButton cmdButtonsHello = new JButton("Hello"); JButton cmdButtonsGoodbye = new JButton("Goodbye"); CategoryTitle lblCheckbox = new CategoryTitle("CHECK BOX"); JCheckBox chkCheckboxShowWorld = new JCheckBox(" Show world"); //required by the ActionListener interface public void actionPerformed(ActionEvent e){} }
The constructor for CategoryLayout looks like:
CategoryLayout layout = new CategoryLayout(20, 5, 200, 10);
This specifies the line height, the gap size, the (preferred) line width, and the category spacing, respectively. The layout manager uses "layout constraints" to determine where components should go. For CategoryLayout, this is done through an instance of CategoryLayoutConstraints. Setting constraints for a CategoryTitle is easy, just use an empty constructor:
CategoryLayoutConstraints c = new CategoryLayoutConstraints(); add(lblButtons, c);
Categories are ordered in the same way they are added. For components within categories, the constructor looks like:
c = new CategoryLayoutConstraints("BUTTONS", 1, 1, 0.05, 0.9, 1);
This tells the manager to put the component in the "BUTTONS" category, starting and ending at line 1, starting at 0.05 of the line width, and stretching 0.9 of the width. The last argument should always be 1 for our purposes.
Initializing
Next, we need to specify a constructor and initialize the panel. It is important to note here that you should always specify at least an empty constructor for use with dynamic loading. The initialization method does a number of things. Firstly, it should call _init(), which is a generic initialization method in the InterfacePanel superclass. Second, it specifies the layout manager. Third, it sets the "action commands" for the relevant components and sets this panel as a listener for events on those components. When a button is pressed or a check box is changed, this will trigger a call to the actionPerformed we have already declared. Thus, we can put code in this method that responds to these user actions. Finally, it lays out all the components using layout constraints. Adding all of this to our class file we have:
package mgui.tutorials; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JCheckBox; import mgui.interfaces.InterfaceDisplayPanel; import mgui.interfaces.InterfacePanel; import mgui.interfaces.layouts.CategoryLayout; import mgui.interfaces.layouts.CategoryLayoutConstraints; import mgui.interfaces.layouts.CategoryTitle; public class HelloGUI extends InterfacePanel implements ActionListener{ //declare Swing components CategoryTitle lblButtons = new CategoryTitle("BUTTONS"); JButton cmdButtonsHello = new JButton("Hello"); JButton cmdButtonsGoodbye = new JButton("Goodbye"); CategoryTitle lblCheckbox = new CategoryTitle("CHECK BOX"); JCheckBox chkCheckboxShowWorld = new JCheckBox(" Show world"); //empty constructor, required for dynamic loading public HelloGUI (){} public HelloGUI (InterfaceDisplayPanel panel){ setDisplayPanel(panel); init(); } protected void init(){ //call to superclass initialization _init(); //set layout manager setLayout(new CategoryLayout(20, 5, 200, 10)); //set action commands cmdButtonsHello.setActionCommand("Buttons Hello"); cmdButtonsGoodbye.setActionCommand("Buttons Goodbye"); chkCheckboxShowWorld.setActionCommand("Check Show World"); //add this panel as an action listener cmdButtonsHello.addActionListener(this); cmdButtonsGoodbye.addActionListener(this); chkCheckboxShowWorld.addActionListener(this); //lay out components CategoryLayoutConstraints c = new CategoryLayoutConstraints(); lblButtons.setParentObj(this); //sets this category's parent to this panel add(lblButtons, c); c = new CategoryLayoutConstraints("BUTTONS", 1, 1, 0.05, 0.9, 1); add(cmdButtonsHello, c); c = new CategoryLayoutConstraints("BUTTONS", 2, 2, 0.05, 0.9, 1); add(cmdButtonsGoodbye, c); c = new CategoryLayoutConstraints(); lblCheckbox.setParentObj(this); add(lblCheckbox, c); c = new CategoryLayoutConstraints("CHECK BOX", 1, 1, 0.05, 0.9, 1); add(chkCheckboxShowWorld, c); } //required by the ActionListener interface public void actionPerformed(ActionEvent e){} }
Registering Your Panel
Before we go any further, it's a good point to discuss how your panel can actually be included in the ModelGUI instance. This can be done by registering it through the mgui init file, which is loaded at runtime. The default init file is located in the $MODELGUI_HOME/init directory and is called mgui.default.init. The init file, in brief, tells mgui how the GUI should be set up, amongst other things (see the main page for details). To register our panel, we want to add an entry to the init file below the line #load interface panel classes, like so:
#load interface panel classes
loadInterfacePanelClass "Hello GUI" mgui.tutorials.HelloGUI
As a further hint you will want to add a toString function to your panel, which is what will show up in the combo box [as a side note, this will eventually change so that the registered name is what shows up]:
public String toString(){ return "Hello GUI"; }
Now that you've done this, you can run mgui and find your panel in the combo box. This is quite useful for inspecting the layout and debugging the behaviours you are about to implement…
Handling Action Events
Handling events is the whole point of an interface panel. It is the "controller" part of the MVC design, which takes user actions and alters the data model accordingly. As stated above, our desired behaviour is to turn the visibility of the shape model off and on. To do this, we first need access to the shape model, which is obtained through the display panel. All of this is accomplished in the actionPerformed method:
public void actionPerformed(ActionEvent e){ //first determine what action the event represents: if (e.getActionCommand().startsWith("Button")){ //the event is a button press, but which one? boolean set_visible = e.getActionCommand().endsWith("Hello"); Shape3DSet model_set = displayPanel.getShapeSet(); //if visibility will change, set it if (model_set.getVisibility() != set_visible) model_set.setVisibility(set_visible); return; } if (e.getActionCommand().equals("Check Show World")){ //this is a check box event boolean set_visible = chkCheckboxShowWorld.isSelected(); Shape3DSet model_set = displayPanel.getShapeSet(); //if visibility will change, set it if (model_set.getVisibility() != set_visible) model_set.setVisibility(set_visible); return; } }
Our first task is to determine the nature of the event. This can be done using the "Action Command", which we've already specified in the init method. Using simple if clauses, we can determine the type of event and act accordingly. The code above simply determines whether visibility should be turned off or on, checks with the shape set to see what its current visibility is, and changes it only if it the desired state is different from the current one.
And that's it, you've created a fully functional interface panel. Try to load some data (see Getting Started) and see if it works!
package mgui.tutorials; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JCheckBox; import mgui.interfaces.InterfaceDisplayPanel; import mgui.interfaces.InterfacePanel; import mgui.interfaces.layouts.CategoryLayout; import mgui.interfaces.layouts.CategoryLayoutConstraints; import mgui.interfaces.layouts.CategoryTitle; import mgui.interfaces.shapes.ShapeSet3DInt; public class HelloGUI extends InterfacePanel implements ActionListener{ //declare Swing components CategoryTitle lblButtons = new CategoryTitle("BUTTONS"); JButton cmdButtonsHello = new JButton("Hello"); JButton cmdButtonsGoodbye = new JButton("Goodbye"); CategoryTitle lblCheckbox = new CategoryTitle("CHECK BOX"); JCheckBox chkCheckboxShowWorld = new JCheckBox(" Show world"); //empty constructor, required for dynamic loading public HelloGUI (){} public HelloGUI (InterfaceDisplayPanel panel){ setDisplayPanel(panel); init(); } protected void init(){ //call to superclass initialization _init(); //set layout manager setLayout(new CategoryLayout(20, 5, 200, 10)); //set action commands cmdButtonsHello.setActionCommand("Buttons Hello"); cmdButtonsGoodbye.setActionCommand("Buttons Goodbye"); chkCheckboxShowWorld.setActionCommand("Check Show World"); //add this panel as an action listener cmdButtonsHello.addActionListener(this); cmdButtonsGoodbye.addActionListener(this); chkCheckboxShowWorld.addActionListener(this); //lay out components CategoryLayoutConstraints c = new CategoryLayoutConstraints(); lblButtons.setParentObj(this); //sets this category's parent to this set add(lblButtons, c); c = new CategoryLayoutConstraints("BUTTONS", 1, 1, 0.05, 0.9, 1); add(cmdButtonsHello, c); c = new CategoryLayoutConstraints("BUTTONS", 2, 2, 0.05, 0.9, 1); add(cmdButtonsGoodbye, c); c = new CategoryLayoutConstraints(); lblCheckbox.setParentObj(this); add(lblCheckbox, c); c = new CategoryLayoutConstraints("CHECK BOX", 1, 1, 0.05, 0.9, 1); add(chkCheckboxShowWorld, c); } //required by the ActionListener interface public void actionPerformed(ActionEvent e){ //first determine what action the event represents: if (e.getActionCommand().startsWith("Button")){ //the event is a button press, but which one? boolean set_visible = e.getActionCommand().endsWith("Hello"); ShapeSet3DInt model_set = displayPanel.getShapeSet(); //if visibility will change, set it if (model_set.getVisibility() != set_visible) model_set.setVisibility(set_visible); return; } if (e.getActionCommand().equals("Check Show World")){ //this is a check box event boolean set_visible = chkCheckboxShowWorld.isSelected(); ShapeSet3DInt model_set = displayPanel.getShapeSet(); //if visibility will change, set it if (model_set.getVisibility() != set_visible) model_set.setVisibility(set_visible); return; } } //string which shows up in the combo box public String toString(){ return "Hello GUI"; } }
Interface Dialog Box
While many GUIs work well within the interface panel framework, there are some cases where dialog boxes are preferable; for example, when you need to have the user specify a number of options for some task. modelGUI has implemented a number of abstract classes to help in the construction of dialog boxes, including InterfaceDialogBox, InterfaceOptionsDialogBox, and InterfaceOptionsTabbedDialogBox. In addition, the abstract InterfaceIODialogBox is designed for use with I/O operations. All of these classes are descendants of Swing's JDialogBox.
To get an idea of how dialog boxes can be constructed, let's think of a silly reason for implementing a dialog box with our HelloGUI panel. Instead of only turning the visibility for a shape set off and on, for instance, let's allow the user to pick the shape as well as the boolean attribute that our panel toggles. This will involve a number of more complicated tasks such as getting lists of shapes and attributes to populate a combo box, but these are also useful things to learn for the prospective GUI builder. So, for our dialog box, we have to do the following:
- define an "options class"
- design the dialog box
- add an "Options" button to our panel
- add an ActionEvent handler to show when the options button is pressed
Options Classes
The first task requires that you know what an "options class" is. In general, it is a class that implements the InterfaceOptions interface, which specifies no methods but acts instead as a "marker interface" for any class which is used to specify a set of parameters. What we need is a class which implements InterfaceOptions and has fields for the options we are interested in, which are:
- the shape whose attributes the user wants to toggle
- the attribute s/he wants to toggle
That's pretty simple, and so is our options class:
package mgui.tutorials; import mgui.interfaces.InterfaceDisplayPanel; import mgui.interfaces.InterfaceOptions; import mgui.interfaces.shapes.Shape3DInt; public class HelloGuiOptions implements InterfaceOptions { public Shape3DInt shape; public String attribute; public InterfaceDisplayPanel display_panel; public HelloGuiOptions(Shape3DInt shape, String attribute, InterfaceDisplayPanel display_panel){ this.shape = shape; this.attribute = attribute; this.display_panel = display_panel; } }
One thing we've added that is not actually an option is the display panel reference. The options class is quite convenient for use a bridge between the interface panel and the dialog box; since we need to access the display panel to get the information we need about shapes and attributes, we put this in the options class too.
Dialog Box Design
From here we can get started on the dialog box itself. Since InterfaceOptionsDialogBox is designed for used with InterfaceOptions, this is the class we want to subclass. First we want a similar constructor that sets the parent frame and the options class and then initiates the dialog box:
package mgui.tutorials; import java.util.ArrayList; import javax.swing.JComboBox; import javax.swing.JFrame; import javax.swing.JLabel; import mgui.interfaces.InterfaceOptionsDialogBox; import mgui.interfaces.attributes.Attribute; import mgui.interfaces.layouts.LineLayout; import mgui.interfaces.layouts.LineLayoutConstraints; import mgui.interfaces.shapes.Shape3DInt; public class HelloGuiDialogBox extends InterfaceOptionsDialogBox { //declare Swing components JLabel lblShape3D = new JLabel("Target shape:"); JComboBox cmbShape3D = new JComboBox(); JLabel lblAttribute = new JLabel("Attribute:"); JComboBox cmbAttribute = new JComboBox(); //constructor public HelloGuiDialogBox(JFrame frame, HelloGuiOptions options){ super(frame, options); _init(); } //initialize dialog box void _init(){ setButtonType(BT_OK_CANCEL); super.init(); //specify layout manager LineLayout lineLayout = new LineLayout(20, 5, 0); this.setMainLayout(lineLayout); this.setDialogSize(450,300); this.setTitle("HelloGUI Options"); //lay out components LineLayoutConstraints c = new LineLayoutConstraints(1, 1, 0.05, 0.35, 1); mainPanel.add(lblShape3D, c); c = new LineLayoutConstraints(1, 1, 0.4, 0.45, 1); mainPanel.add(cmbShape3D, c); c = new LineLayoutConstraints(2, 2, 0.05, 0.35, 1); mainPanel.add(lblAttribute, c); c = new LineLayoutConstraints(2, 2, 0.4, 0.45, 1); mainPanel.add(cmbAttribute, c); //fill combo boxes fillShapes(); fillAttributes(); } void fillShapes(){ cmbShape3D.removeAllItems(); HelloGuiOptions _options = (HelloGuiOptions)options; ArrayList<Shape3DInt> shapes = _options.display_panel.getShapeSet().getMembers(); for (Shape3DInt shape : shapes) cmbShape3D.addItem(shape); if (_options.shape != null) cmbShape3D.setSelectedItem(_options.shape); } void fillAttributes(){ cmbAttribute.removeAllItems(); Shape3DInt shape = (Shape3DInt)cmbShape3D.getSelectedItem(); if (shape == null) return; ArrayList<Attribute> attributes = shape.getAttributes().getAttributes(); for (Attribute attribute : attributes) if (attribute.getValue() instanceof Boolean || attribute.getValue() instanceof arBoolean) cmbAttribute.addItem(attribute.getName()); HelloGuiOptions _options = (HelloGuiOptions)options; if (_options.shape != null && _options.shape.equals(cmbShape3D.getSelectedItem()) && _options.attribute != null) cmbAttribute.setSelectedItem(_options.attribute); } }
Okay, so.. here we've done a number of things. Firstly, as in the interface panel example, we declare our Swing components at the beginning: two combo boxes and their labels, which allow us to list shapes and their attributes. In the constructor, we make a call to the super classes to get all the Swing initialization done. Next we do our own initialization. As in our interface panel, we specify a layout manager, but here we are using LineLayout, since we have no need for categories. We next set the dialog size and its title, and lay out the components as before. Finally, we call the two methods which populate the combo boxes. The fillShapes method retrieves a list of all the base shape set's members, and fills the combo box with these. The fillAttributes method determines the shape currently selected in the shape combo box and if one is selected, fills the attributes combo box with its boolean attributes. Both methods also update the combo boxes with the current values in the options class, so that the current values are shown when the user first opens the dialog box.
Action Listeners
Now we have to do a few more things before this is ready:
- add an action listener to the shape combo box, so that the attributes combo box can be updated whenever it changes
- add another action listener to respond to a "OK" button event
InterfaceDialogBox already implements the ActionListener interface, so we don't have to worry about that, but can simply go ahead and override the actionPerformed method. We also need to set up our combo box with an action command:
import java.awt.event.ActionEvent; ... void _init(){ setButtonType(BT_OK_CANCEL); super.init(); //set up cmbShape3D for event handling cmbShape3D.setActionCommand("Shape Changed"); cmbShape3D.addActionListener(this); ... } ... public void actionPerformed(ActionEvent e){ //respond to a change in the shape combo box if (e.getActionCommand().equals("Shape Changed")){ fillAttributes(); return; } //respond to an "OK" button press if (e.getActionCommand().equals(DLG_CMD_OK)){ //set option values HelloGuiOptions _options = (HelloGuiOptions)options; _options.shape = (Shape3DInt)cmbShape3D.getSelectedItem(); _options.attribute = (String)cmbAttribute.getSelectedItem(); this.setVisible(false); return; } //handles a "Cancel" button press super.actionPerformed(e); } }