|
|||
|
|
EarthWeb sites: |
Day 13Creating User Interfaces with the awtby Laura Lemay
For the past five days you've concentrated on creating applets that do very simple things: display text, play an animation or a sound, or interact with the user. When you get past that point, however, you may want to start creating more complex applets that behave like real applications embedded in a Web page-applets that start to look like real GUI applications with buttons, menus, text fields, and other elements. It's this sort of real work in Java applets and applications for which Java's Abstract Windowing Toolkit, or awt, was designed. You've actually been using the awt all along, as you might have guessed from the classes you've been importing. The Applet class and most of the classes you've been using this week are all integral parts of the awt. The awt provides the following:
Today you'll learn about how to use all these things in your Java applets. Tomorrow you'll learn about creating windows, menus, and dialog boxes, which enable you to pop up separate windows from the browser window. In addition, you can use the awt in standalone applications, so everything you've learned so far this week can still be used. If you find the framework of the Web browser too limiting, you can take your awt background and start writing full-fledged Java applications.
Today, however, you'll continue focusing on applets.
An awt OverviewThe basic idea behind the awt is that a graphical Java program is a set of nested components, starting from the outermost window all the way down to the smallest UI component. Components can include things you can actually see on the screen, such as windows, menu bars, buttons, and text fields, and they can also include containers, which in turn can contain other components. Figure 13.1 shows how a sample page in a Java browser might include several different components, all of which are managed through the awt. This nesting of components within containers within other components creates a hierarchy of components, from the smallest check box inside an applet to the overall window on the screen. The hierarchy of components determines the arrangement of items on the screen and inside other items, the order in which they are painted, and how events are passed from one component to another. These are the major components you can work with in the awt:
The classes inside the java.awt package are written and organized to mirror the abstract structure of containers, components, and individual UI components. Figure 13.2 shows some of the class hierarchy that makes up the main classes in the awt. The root of most of the awt components is the class Component, which provides basic display and event-handling features. The classes Container, Canvas, TextComponent, and many of the other UI components inherit from Component. Inheriting from the Container class are objects that can contain other awt components-the Panel and Window classes, in particular. Note that the java.applet.Applet class, even though it lives in its own package, inherits from Panel, so your applets are an integral part of the hierarchy of components in the awt system. Figure 13.2 : A Partial awt class Hierarchy. A graphical user interface-based application that you write by using the awt can be as complex as you like, with dozens of nested containers and components inside each other. The awt was designed so that each component can play its part in the overall awt system without needing to duplicate or keep track of the behavior of other parts in the system. In addition to the components themselves, the awt also includes a set of layout managers. Layout managers determine how the various components are arranged when they are displayed onscreen, and their various sizes relative to each other. Because Java applets and applications that use the awt can run on different systems with different displays, different fonts, and different resolutions, you cannot just stick a particular component at a particular spot on the window. Layout managers help you create UI layouts that are dynamically arranged and can be displayed anywhere the applet or application might be run. The Basic User Interface ComponentsThe simplest form of awt component is the basic UI component. You can create and add these to your applet without needing to know anything about creating containers or panels-your applet, even before you start painting and drawing and handling events, is already an awt container. Because an applet is a container, you can put other awt components-such as UI components or other containers-into it. In this section, you'll learn about the basic UI components: labels, buttons, check boxes, choice menus, and text fields. In each case, the procedure for creating the component is the same-you first create the component and then add it to the panel that holds it, at which point it is displayed on the screen. To add a component to a panel (such as your applet, for example), use the add() method: public void init() { Button b = new Button("OK"); add(b); } Here the add() method refers to the current applet-in other words, it means "add this element to me." You can also add elements to other containers, as you'll learn later. Note that where the component appears in the panel depends on the layout manager that panel is defined to have. In these examples I've used both flow layouts and grid layouts, depending on which makes the applet look better. You'll learn more about panels and layouts in the next section. Note also that each of these components has an action associated with it-that is, something that component does when it's activated. Actions generally trigger events or other activities in your applet (they are often called callbacks in other window toolkits). In this section, you'll focus on creating the components themselves; you'll learn about adding actions to them later in today's lesson. On to the components! LabelsThe simplest form of UI component is the label, which is, effectively, a text string that you can use to label other UI components. Labels are not editable; they just label other components on the screen. The advantages that a label has over an ordinary text string (that you'd draw using drawString() in the paint() method) are
A label is an uneditable text string that acts as a description for other awt components. To create a label, use one of the following constructors:
You can change the label's font with the setFont() method, either called on the label itself to change the individual label, or on the enclosing component to change all the labels. Here's some simple code to create a few labels in Helvetica Bold (Figure 13.3 shows how this looks onscreen):
Figure 13.3 : Three labels with various alignments.
import java.awt.*; public class LabelTest extends java.applet.Applet { public void init() { setFont(new Font ("Helvetica", Font.BOLD, 14)); setLayout(new GridLayout(3,1)); add(new Label("aligned left", Label.LEFT)); add(new Label("aligned center", Label.CENTER)); add(new Label("aligned right", Label.RIGHT)); } }
When you have a Label object,
you can use methods defined in the Label
class to get and set the values of the text, as shown in Table
13.1.
ButtonsThe second user interface component to explore is the button. Buttons are simple UI components that trigger some action in your interface when they are pressed. For example, a calculator applet might have buttons for each number and operator, or a dialog box might have buttons for OK and Cancel. A button is a UI component that, when "pressed" (selected) with the mouse, triggers some action. To create a button, use one of the following constructors:
Once you have a Button object, you can get the value of the button's label by using the getLabel() method and set the label using the setLabel(String) method. Figure 13.4 shows some simple buttons, created using the following code: Figure 13.4 : Four buttons in Netscape. public class ButtonTest extends java.applet.Applet { public void init() { add(new Button("Rewind")); add(new Button("Play")); add(new Button("Fast Forward")); add(new Button("Stop")); } } Check BoxesCheck boxes are user-interface components that have two states: on and off (or checked and unchecked, selected and unselected, true and false, and so on). Unlike buttons, check boxes usually don't trigger direct actions in a UI, but instead are used to indicate optional features of some other action. Check boxes can be used in two ways:
The latter kind of check boxes are called radio buttons or check box groups, and are described in the next section. Check boxes are UI components that can be selected or deselected (checked or unchecked) to provide options. Nonexclusive check boxes can be checked or unchecked independently of other check boxes. Exclusive check boxes, sometimes called radio buttons, exist in groups; only one in the group can be checked at one time. Nonexclusive check boxes can be created by using the Checkbox class. You can create a check box using one of the following constructors:
Figure 13.5 shows a few simple check boxes (only Underwear is selected) generated using the following code: Figure 13.5 : Five check boxes, one selected. import java.awt.*; public class CheckboxTest extends java.applet.Applet { public void init() { setLayout(new FlowLayout(FlowLayout.LEFT)); add(new Checkbox("Shoes")); add(new Checkbox("Socks")); add(new Checkbox("Pants")); add(new Checkbox("Underwear", null, true)); add(new Checkbox("Shirt")); } }
Table 13.2 lists some of the check box methods.
Radio ButtonsRadio buttons have the same appearance as check boxes, but only one in a series can be selected at a time. To create a series of radio buttons, first create an instance of CheckboxGroup: CheckboxGroup cbg = new CheckboxGroup(); Then create and add the individual check boxes using the constructor with three arguments (the first is the label, the second is the group, and the third is whether that check box is selected). Note that because radio buttons, by definition, have only one in the group selected at a time, the last true to be added will be the one selected by default: add(new Checkbox("Yes", cbg, true); add(new Checkbox("No", cbg, false); Here's a simple example (the results of which are shown in Figure 13.6): Figure 13.6 : Six radio buttons (exclusive check boxes), one selected. import java.awt.*; public class CheckboxGroupTest extends java.applet.Applet { public void init() { setLayout(new FlowLayout(FlowLayout.LEFT)); CheckboxGroup cbg = new CheckboxGroup(); add(new Checkbox("Red", cbg, false)); add(new Checkbox("Blue", cbg, false)); add(new Checkbox("Yellow", cbg, false)); add(new Checkbox("Green", cbg, true)); add(new Checkbox("Orange", cbg, false)); add(new Checkbox("Purple", cbg, false)); } }
All the check box methods shown in Table 13.2 in the previous
section can be used with Finally, the getCurrent() and setCurrent(Checkbox) methods, defined in CheckboxGroup, can be used to get or set the currently selected check box. Choice MenusThe choice menu is a more complex UI component than labels, buttons, or check boxes. Choice menus are pop-up (or pull-down) menus from which you can select an item. The menu then displays that choice on the screen. The function of a choice menu is the same across platforms, but its actual appearance may vary from platform to platform. Note that choice menus can have only one item selected at a time. If you want to be able to choose multiple items from the menu, use a scrolling list instead (you'll learn more about scrolling lists later today, in the section "More UI Components"). Choice menus are pop-up menus of items from which you can choose one item. To create a choice menu, create an instance of the Choice class and then use the addItem() method to add individual items to it in the order in which they should appear. Finally, add the entire choice menu to the panel in the usual way. Here's a simple program that builds a choice menu of fruits; Figure 13.7 shows the result (with the menu pulled down): import java.awt.*; public class ChoiceTest extends java.applet.Applet { public void init() { Choice c = new Choice(); c.addItem("Apples"); c.addItem("Oranges"); c.addItem("Strawberries"); c.addItem("Blueberries"); c.addItem("Bananas"); add(c); } }
Even after your choice menu has been added to a panel, you can
continue to add items to that menu with the addItem()
method. Table 13.3 shows some other methods that may be useful
in working with choice menus.
Text FieldsUnlike the UI components up to this point, which only enable you to select among several options to perform an action, text fields allow you to enter and edit text. Text fields are generally only a single line and do not have scrollbars; text areas, which you'll learn about later today, are better for larger amounts of text. Text fields are different from labels in that they can be edited; labels are good for just displaying text, text fields for getting text input from the user. Text fields provide an area where you can enter and edit a single line of text. To create a text field, use one of the following constructors:
For example, the following line creates a text field 30 characters wide with the string "Enter Your Name" as its initial contents: TextField tf = new TextField("Enter Your Name", 30); add(tf);
You can also create a text field that obscures the characters typed into it-for example, for password fields. To do this, first create the text field itself; then use the setEchoCharacter() method to set the character that is echoed on the screen. Here is an example: TextField tf = new TextField(30); tf.setEchoCharacter('*'); Figure 13.8 shows three text boxes (and labels) that were created using the following code: Figure 13.8 : Three text fields to allow input from the user. add(new Label("Enter your Name")); add(new TextField("your name here", 45)); add(new Label("Enter your phone number")); add(new TextField(12)); add(new Label("Enter your password")); TextField t = new TextField(20); t.setEchoCharacter('*'); add(t); The text in the first field (your name here) was initialized in the code; I typed the text in the remaining two boxes just before taking the snapshot.
Text fields inherit from the class TextComponent
and have a whole suite of methods, both inherited from that class
and defined in their own class, that may be useful to you in your
Java programs. Table 13.4 shows a selection of those methods.
Panels and Layoutawt panels can contain UI components or other panels. The question now is how those components are actually arranged and displayed onscreen. In other windowing systems, UI components are often arranged using hard-coded pixel measurements-put a text field at the position 10,30, for example-the same way you used the graphics operations to paint squares and ovals on the screen. In the awt, your UI design may be displayed on many different window systems on many different screens and with many different kinds of fonts with different font metrics. Therefore, you need a more flexible method of arranging components on the screen so that a layout that looks nice on one platform isn't a jumbled, unusable mess on another. For just this purpose, Java has layout managers, insets, and hints that each component can provide to help dynamically lay out the screen. Note that the nice thing about awt components and user-interface items is that you don't have to paint them-the awt system manages all that for you. If you have graphical components or images, or you want to create animation inside panels, you still have to do that by hand, but for most of the basic components, all you have to do is put them on the screen and Java will handle the rest. Layout Managers: An OverviewThe actual appearance of the awt components on the screen is usually determined by two things: how those components are added to the panel that holds them (either the order or through arguments to add()) and the layout manager that panel is currently using to lay out the screen. The layout manager determines how portions of the screen will be sectioned and how components within that panel will be placed. The layout manager determines how awt components are dynamically arranged on the screen. Each panel on the screen can have its own layout manager. By nesting panels within panels, and using the appropriate layout manager for each one, you can often arrange your UI to group and arrange components in a way that is functionally useful and that looks good on a variety of platforms and windowing systems. You'll learn about nesting panels in a later section. The awt provides five basic layout managers: FlowLayout, GridLayout, BorderLayout, CardLayout, and GridBagLayout. To create a layout manager for a given panel, create an instance of that layout manager and then use the setLayout() method for that panel. This example sets the layout manager of the entire enclosing applet panel: public void init() { setLayout(new FlowLayout()); } Setting the default layout manager, like creating user-interface components, is best done during the applet's initialization, which is why it's included here. After the layout manager is set, you can start adding components to the panel. The order in which components are added or the arguments you use to add those components is often significant, depending on which layout manager is currently active. Read on for information about the specific layout managers and how they present components within the panel to which they apply. The following sections describe the five basic Java awt layout managers. The FlowLayout ClassThe FlowLayout class is the most basic of layouts. Using flow layout, components are added to the panel one at a time, row by row. If a component doesn't fit onto a row, it's wrapped onto the next row. The flow layout also has an alignment, which determines the alignment of each row. By default, each row is centered. Flow layout arranges components from left to right in rows. The rows are aligned left, right, or centered. To create a basic flow layout with a centered alignment, use the following line of code in your panel's initialization (because this is the default pane layout, you don't need to include this line if that is your intent): setLayout(new FlowLayout()); With the layout set, the order in which you add elements to the layout determines their position. The following code creates a simple row of six buttons in a centered flow layout (Figure 13.9 shows the result): Figure 13.9 : Six buttons, arranged using a flow layout manager. import java.awt.*; public class FlowLayoutTest extends java.applet.Applet { public void init() { setLayout(new FlowLayout()); add(new Button("One")); add(new Button("Two")); add(new Button("Three")); add(new Button("Four")); add(new Button("Five")); add(new Button("Six")); } } To create a flow layout with an alignment other than centered, add the FlowLayout.RIGHT or FlowLayout.LEFT class variable as an argument: setLayout(new FlowLayout(FlowLayout.LEFT)); You can also set horizontal and vertical gap values by using flow layouts. The gap is the number of pixels between components in a panel; by default, the horizontal and vertical gap values are three pixels, which can be very close indeed. Horizontal gap spreads out components to the left and to the right; vertical gap spreads them to the top and bottom of each component. Add integer arguments to the flow layout constructor to increase the gap. Figure 13.10 shows the result of adding a gap of 30 points in the horizontal and 10 in the vertical directions, like this: Figure 13.10: Flow layout with a gap of 10 points. setLayout(new FlowLayout(FlowLayout.LEFT, 30, 10)); Grid LayoutsGrid layouts offer more control over the placement of components inside a panel. Using a grid layout, you portion off the display area of the panel into rows and columns. Each component you then add to the panel is placed in a cell of the grid, starting from the top row and progressing through each row from left to right (here's where the order of calls to the add() method are very relevant to how the screen is laid out). To create a grid layout, indicate the number of rows and columns you want the grid to have when you create a new instance of the GridLayout class. Here's a grid layout with three rows and two columns (Figure 13.11 shows the result): Figure 13.11: Six buttons displayed using a grid layout of three rows and two columns. import java.awt.*; public class GridLayoutTest extends java.applet.Applet { public void init() { setLayout(new GridLayout(3,2); add(new Button("One")); add(new Button("Two")); add(new Button("Three")); add(new Button("Four")); add(new Button("Five")); add(new Button("Six")); } } Grid layouts can also have a horizontal and vertical gap between components. To create gaps, add those pixel values: setLayout(new GridLayout(3, 3, 10, 30)); Figure 13.12 shows a grid layout with a 10-pixel horizontal gap and a 30-pixel vertical gap. Figure 13.12: A grid layout with horizontal and vertical gaps. Border LayoutsBorder layouts behave differently from flow and grid layouts. When you add a component to a panel that uses a border layout, you indicate its placement as a geographic direction: north, south, east, west, or center. (See Figure 13.13.) The components around all the edges are laid out with as much size as they need; the component in the center, if any, gets any space left over. Figure 13.13: Where components go in a border layout. To use a border layout, you create it as you do the other layouts; then you add the individual components with a special add() method that has two arguments. The first argument is a string indicating the position of the component within the layout, and the second is the component to add: add("North", new TextField("Title", 50)); You can also use this form of add() for the other layout managers; the string argument will just be ignored if it's not needed. Here's the code to generate the border layout shown in Figure 13.13: import java.awt.*; public class BorderLayoutTest extends java.applet.Applet { public void init() { setLayout(new BorderLayout()); add("North", new Button("One")); add("East", new Button("Two")); add("South", new Button("Three")); add("West", new Button("Four")); add("Center", new Button("Five")); add(new Button("Six")); } } Border layouts can also have horizontal and vertical gaps. Note that the north and south components extend all the way to the edge of the panel, so the gap will result in less vertical space for the east, right, and center components. To add gaps to a border layout, include those pixel values in the constructor as with the other layout managers: setLayout(new BorderLayout(10, 10)); Card LayoutsCard layouts behave much differently from the other layouts. When you add components to one of the other layout managers, all those components appear on the screen at once. Card layouts are used to produce slide shows of components, one at a time. If you've ever used the HyperCard program on the Macintosh, or seen dialog boxes on windows with several different tabbed pages, you've worked with the same basic idea. When you create a card layout, the components you add to the outer panel will be other container components-usually other panels. You can then use different layouts for those individual cards so that each screen has its own look. Cards, in a card layout, are different panels added one at a time and displayed one at a time. If you think of a card file, you'll get the idea; only one card can be displayed at once, but you can switch between cards. When you add each card to the panel, you can give it a name. Then, to flip between the container cards, you can use methods defined in the CardLayout class to move to a named card, move forward or back, or move to the first card or to the last card. Typically you'll have a set of buttons that call these methods to make navigating the card layout easier. Here's a simple snippet of code that creates a card layout containing three cards: setLayout(new CardLayout()); //add the cards Panel one = new Panel() add("first", one); Panel two = new Panel() add("second", two); Panel three = new Panel() add("third", three); // move around show(this, "second"); //go to the card named "second" show(this, "third"); //go to the card named "third" previous(this); //go back to the second card first(this); // got to the first card Grid Bag LayoutsI've saved grid bag layouts for last because although they are the most powerful way of managing awt layout, they are also extremely complicated. Using one of the other four layout managers, it can sometimes be difficult to get the exact layout you want without doing a lot of nesting of panels within panels. Grid bags provide a more general-purpose solution. Like grid layouts, grid bag layouts allow you to arrange your components in a grid-like layout. However, grid bag layouts also allow you to control the span of individual cells in the grid, the proportions between the rows and columns, and the arrangement of components inside cells in the grid. To create a grid bag layout, you actually use two classes: GridBagLayout, which provides the overall layout manager, and GridBagConstraints, which defines the properties of each component in the grid-its placement, dimensions, alignment, and so on. It's the relationship between the grid bag, the constraints, and each component that defines the overall layout. In its most general form, creating a grid bag layout involves the following steps:
Here's some simple code that sets up the layout and then creates constraints for a single button (don't worry about the various values for the constraints; I'll cover these later on in this section): // set up layout GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints constraints = new GridBagConstraints(); setLayout(gridbag); // define constraints for the button Button b = new Button("Save"); constraints.gridx = 0; constraints.gridy = 0; constraints.gridwidth = 1; constraints.gridheight = 1; constraints.weightx = 30; constraints.weighty = 30; constraints.fill = GridBagConstraints.NONE; constraints.anchor = GridBagConstraints.CENTER; // attach constraints to layout, add button gridbag.setConstraints(b, constraints); add(b); By far, the most tedious part of this process is setting up the constraints for each component (as you can see from this example, you have to set all those constraints for every component you want to add to the panel). In addition to the tedium, constraints aren't all that easy to understand; they have many different values, many of which are interrelated, which means that changing one may have strange effects on others. Given the numerous constraints, it helps to have a plan and to deal with each kind of constraint one at a time. There are four steps I like to follow in this process. Let's walk through each of them. Step One: Design the GridThe first place to start in the grid bag layout is on paper. Sketching out your UI design beforehand-before you write even a single line of code-will help enormously in the long run with trying to figure out where everything goes. So put your editor aside for a second, pick up a piece of paper and a pencil, and let's build the grid. Figure 13.14 shows the panel layout we'll be building in this example. Figure 13.15 shows the same layout with a grid imposed on top of it. Your layout will have a grid similar to this one, with rows and columns forming individual cells. Figure 13.14: A grid bag layout. Figure 13.15: The grid bag layout from Figure 13.14, with grid imposed. Keep in mind as you draw your grid that each component must have its own cell. You cannot put more than one component into the same cell. The reverse is not true, however; one component can span multiple cells in the x or y directions (as in the OK button in the bottom row, which spans two columns). Note in Figure 13.15 that the labels and text fields have their own grids and that the button spans two column cells. While you're still working on paper, something that will help you later is to label the cells with their x and y coordinates. These aren't pixel coordinates; rather, they're cell coordinates. The top-left cell is 0,0. The next cell to the right of that in the top row is 1,0. The cell to the right of that is 2,0. Moving to the next row, the leftmost cell is 1,0, the next cell in the row is 1,1, and so on. Label your cells on the paper with these numbers; you'll need them later when we do the code for this example. Figure 13.16 shows the numbers for each of the cells in this example. Figure 13.16: The grid bag layout from Figure 13.14, with cell coordinates. Step Two: Create the Grid in JavaLet's go back to Java and start implementing the layout you've just drawn on paper. Initially we're going to focus exclusively on the layout-getting the grid and the proportions right. For that, it helps to not work with actual UI elements. I like to use buttons as placeholders for the actual elements in the layout until I can get everything set up right, and then change the buttons to the right elements. To cut down on the amount of typing we have to do to set up all those constraints, I'm going to start by defining a helper method that takes several values and sets the constraints for those values. buildConstraints() takes seven arguments: a GridBagConstraints object and six integers representing the GridBagConstraints instance variables gridx, gridy, gridwidth, gridheight, weightx, and weighty. You'll learn what these actually do soon; for now, here's the code to the helper method that we'll use further on in this example: void buildConstraints(GridBagConstraints gbc, int gx, int gy, int gw, int gh, int wx, int wy) { gbc.gridx = gx; gbc.gridy = gy; gbc.gridwidth = gw; gbc.gridheight = gh; gbc.weightx = wx; gbc.weighty = wy; } Now let's move on to the init() method, where all the layout actually occurs. Here's the basic method definition, where we'll define the GridBagLayout to be the initial layout manager and create a constraints object (an instance of GridBagConstraints): public void init() { GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints constraints = new GridBagConstraints(); setLayout(gridbag); constraints.fill = GridBagConstraints.BOTH; } One more small note of explanation: That last line, which sets the value of constraints.fill, will be removed (and explained) later. It's there so that the components will fill the entire cell in which they're contained, which makes it easier to see what's going on. Add it for now and you'll get a clearer idea of what it's for later. Now we'll add the button placeholders to the layout (remember, we're focusing on basic grid organization at the moment, so we'll use buttons as placeholders for the actual UI elements you'll add later). Let's start with a single button so you can get a feel for setting its constraints. This code will go into the init() method just after the setLayout line: // Name label buildConstraints(constraints, 0, 0, 1, 1, 100, 100); Button label1 = new Button("Name:"); gridbag.setConstraints(label1, constraints); add(label1); These four lines set up the constraints for an object, create a new button, attach those constraints to that button, and then add it to the panel. Note that constraints for a component are stored in the GridBagConstraints object, so the component doesn't even have to exist to set up its constraints. Now let's get down to details: Just what are the values for the constraints that we've plugged into the helper method buildConstraints? The first two integer arguments are the gridx and gridy values of the constraints. These are the cell coordinates of the cell that contains this component. Remember how you wrote these down on the paper in step one? With the cells nearly numbered on paper, all you have to do is plug in the right values. Note that if you have a component that spans multiple cells, the cell coordinates are those of the cell in the top-left corner. Here this button is in the top-left corner, so its gridx and gridy (the first two arguments to buildConstraints()) are 0 and 0, respectively. The second two integer arguments are the gridwidth and gridheight. These are not the pixel widths and heights of the cells; rather, they are the number of cells this component spans: gridwidth for the columns and gridheight for the rows. Here this component spans only one cell, so the values for both are 1. The last two integer arguments are for weightx and weighty. These are used to set up the proportions of the rows and columns-that is, how wide or deep they will be. Weights can become very confusing, so for now just set both values to 100. You'll deal with weights in step three. After the constraints have been built, you can attach them to an object using the setConstraints() method. setConstraints90, which is a method defined in GridBagLayout, takes two arguments: the component (here a button) and the constraints for that button. Finally, you can add the button to the panel. After you've set and assigned the constraints to one component, you can reuse that GridBagConstraints object to set up the constraints for the next object. This effectively means duplicating those four lines for each component in the grid, with different values for the buildConstraints() method. To save space, I'm just going to show you the buildConstraints() methods for the last four cells. The second cell we'll add is the one that will hold the text box for the name. The cell coordinates for this one are 1,0 (second column, first row); it too spans only one cell, and the weights (for now) are also both 100: buildConstraints(constraints, 1, 0, 1, 1, 100, 100); The next two components, which will be a label and a text field, are nearly exactly the same as the previous two; the only difference is in their cell coordinates. The password label is at 0,1 (first column, second row), and the password text field is at 1,1 (second column, second row): buildConstraints(constraints, 0, 1, 1, 1, 100, 100); buildConstraints(constraints, 1, 1, 1, 1, 100, 100); And, finally, there is the OK button, which is a component that spans two cells in the bottom row of the panel. Here the cell coordinates are the left and topmost cell where the span starts (0,2). Here, unlike the previous components, we'll set gridwidth and gridheight to be something other than 1 because this cell spans multiple columns. The gridweight is 2 (it spans two cells), and the gridheight is 1 (it spans only one row): buildConstraints(constraints, 0, 2, 2, 1, 100, 100); Got it? Those are the placement constraints for all the components that you'll add to the grid layout. You will also need to assign each component's constraints to the layout manager and then add each component to the panel. Figure 13.17 shows the result so far. Note that you're not concerned about exact proportions here, or about making sure everything lines up. What you should keep track of at this point is making sure the grid is working, that there are the right number of rows and columns, that the spans are correct, and that nothing strange is going on (cells in the wrong place, cells overlapping, that kind of thing). Figure 13.17: Grid bag layout, first pass. Step Three: Determine the ProportionsThe next step is to determine the proportions of the rows and columns in relation to other rows and columns. For example, in this case you'll want the labels (name and password) to take up less space than the text boxes. And you might want the OK button at the bottom to be only half the height of the two text boxes above it. You arrange the proportions of the cells within your layout using the weightx and weighty constraints.
The easiest way to think of weightx
and weighty is that their
values are either percentages of the total width and height of
the panel, or 0 if the weight
or height has been set by some other cell. The values of weightx
and weighty for all your
components, therefore, should sum to 100.
So which cells get values and which cells get 0? Cells that span multiple rows or columns should always be 0 in the direction they span. Beyond that, it's simply a question of picking a cell to have a value, and then all the other cells in that row or columns should be 0. Let's look at the five calls to buildConstraints() we made in the last step: buildConstraints(constraints, 0, 0, 1, 1, 100, 100); //name buildConstraints(constraints, 1, 0, 1, 1, 100, 100); //name text buildConstraints(constraints, 0, 1, 1, 1, 100, 100); //password buildConstraints(constraints, 1, 1, 1, 1, 100, 100); //password text buildConstraints(constraints, 0, 2, 2, 1, 100, 100); //OK button We'll be changing those last two arguments in each call to buildConstraints to be either a value or 0. Let's start with the x direction (the proportions of the columns), which is the second-to-last argument in that list. If you look back to Figure 13.15 (the picture of the panel with the grid imposed), you'll note that the second column is much larger than the first. If you were going to pick theoretical percentages for those columns, you might say that the first is 10 percent and the second is 90 percent (I'm making a guess here; that's all you need to do as well). With those two guesses, let's assign them to cells. We don't want to assign any values to the cell with the OK button because that cell spans both columns, and percentages there wouldn't work. So let's add them to the first two cells, the name label and the name text field: buildConstraints(constraints, 0, 0, 1, 1, 10, 100); //name buildConstraints(constraints, 1, 0, 1, 1, 90, 100); //name text And what about the values of the remaining two cells, the password label and text field? Because the proportions of the columns have already been set up by the name label and field, we don't have to reset them here. We'll give both of these cells and the one for the OK box 0 values: buildConstraints(constraints, 0, 1, 1, 1, 0, 100); //password buildConstraints(constraints, 1, 1, 1, 1, 0, 100); //password text buildConstraints(constraints, 0, 2, 2, 1, 0, 100); //OK button Note here that a 0 value does not mean that the cell has 0 width. These are proportions, not pixel values. A 0 simply means that the proportion has been set somewhere else; all 0 says is "stretch it to fit." Now that the totals of all the weightx constraints are 100, let's move onto the weightys. Here there are three rows; glancing over the grid we drew, it looks like the button has about 20 percent and the text fields have the rest (40 percent each). As with the x values, we only have to set the value of one cell per row (the two labels and the button), with all the other cells having a weightx of 0. Here are the final five calls to buildConstraints() with the weights in place: buildConstraints(constraints, 0, 0, 1, 1, 10, 40); //name buildConstraints(constraints, 1, 0, 1, 1, 90, 0); //name text buildConstraints(constraints, 0, 1, 1, 1, 0, 40); //password buildConstraints(constraints, 1, 1, 1, 1, 0, 0); //password text buildConstraints(constraints, 0, 2, 2, 1, 0, 20); //OK button Figure 13.18 shows the result with the correct proportions. Figure 13.18: Grid bag layout second pass. At this step, the goal here is to try to come up with some basic proportions for how the rows and cells will be spaced on the screen. You can make some basic estimates based on how big you expect the various components to be, but chances are you're going to use a lot of trial and error in this part of the process. Step Four: Add and Arrange the ComponentsWith the layout and the proportions in place, now you can replace the button placeholders with actual labels and text fields. And because you set everything up already, it should all work perfectly, right? Well, almost. Figure 13.19 shows what you get if you use the same constraints as before and replace the buttons with actual components. Figure 13.19: Grid bag layout, almost there. It's close, but it's weird. The text boxes are too tall, and the OK button stretches the width of the cell. What's missing are the constraints that arrange the components inside the cell. There are two of them: fill and anchor. The fill constraint determines, for components that can stretch in either direction (like text boxes and buttons), in which direction to stretch. fill can have one of four values, defined as class variables in the GridBagConstraints class:
By default, the fill constraint for all components is NONE. So why are those text fields and labels filling the cells? If you remember way back to the start of the code for this example, I added this line to the init() method: constraints.fill = GridBagConstraints.BOTH; Now you know what it does. For the final version of this applet, you'll want to remove that line and add fill values for each independent component. The second constraint that affects how a component appears in the cell is anchor. This constraint applies only to components that aren't filling the whole cell, and it tells the awt where inside the cell to place the component. The possible values for the anchor constraint are GridBagConstraints.CENTER, which aligns the component both vertically and horizontally inside the cell, or one of eight direction values: GridBagConstraints.NORTH, GridBagConstraints.NORTHEAST, GridBagConstraints.EAST, GridBagConstraints.SOUTHEAST, GridBagConstraints.SOUTH, GridBagConstraints.SOUTHWEST, GridBagConstraints.WEST, or GridBagConstraints.NORTHWEST. The default value of anchor is GridBagConstraints.CENTER. You set these constraints in the same way you did all the other ones: by changing instance variables in the GridBagConstraints object. Here you can change the definition of buildConstraints() to take two more arguments (they're ints), or you could just set them in the body of the init() method. I prefer the latter way. Be careful with defaults. Keep in mind that because you're reusing the same GridBagConstraints object for each component, there may be some values left over after you're done with one component. On the other hand, if a fill or anchor from one object is the same as the one before it, you don't have to reset that object. For this example, I'm going to make three changes to the fills and anchors of the components:
I'm not going to show you all the code for this here; the full code for the example is at the end of this section. You can see the changes I've made there. Step Five: Futz with ItI added this step to the list because in my own experimentation with grid bag layouts, I found that even by following all the steps, usually the resulting layout wasn't quite right, and I needed to do a considerable amount of tinkering and playing with various values of the constraints in order to get it to come out right (that's what futzing means) There's nothing wrong with that; the goal of the previous three steps was to get things fairly close to their final positions, not to come out with a perfect layout each and every time. The CodeListing 13.1 shows the complete code for the panel layout we've been building up in this section. If you had trouble following the discussion up to this point, you might find it useful to go through this code line by line to make sure you understand the various bits. Listing 13.1. The panel with the final grid bag layout. 1:import java.awt.*; 2: 3:public class GridBagTestFinal extends java.applet.Applet { 4: 5: void buildConstraints(GridBagConstraints gbc, int gx, int gy, 6: int gw, int gh, 7: int wx, int wy) { 8: gbc.gridx = gx; 9: gbc.gridy = gy; 10: gbc.gridwidth = gw; 11: gbc.gridheight = gh; 12: gbc.weightx = wx; 13: gbc.weighty = wy; 14: } 15: 16: public void init() { 17: GridBagLayout gridbag = new GridBagLayout(); 18: GridBagConstraints constraints = new GridBagConstraints(); 19: setLayout(gridbag); 20: 21: // Name label 22: buildConstraints(constraints, 0, 0, 1, 1, 10, 40); 23: constraints.fill = GridBagConstraints.NONE; 24: constraints.anchor = GridBagConstraints.EAST; 25: Label label1 = new Label("Name:", Label.LEFT); 26: gridbag.setConstraints(label1, constraints); 27: add(label1); 28: 29: // Name text field 30: buildConstraints(constraints, 1, 0, 1, 1, 90, 0); 31: constraints.fill = GridBagConstraints.HORIZONTAL; 32: TextField tfname = new TextField(); 33: gridbag.setConstraints(tfname, constraints); 34: add(tfname); 35: 36: // password label 37: buildConstraints(constraints, 0, 1, 1, 1, 0, 40); 38: constraints.fill = GridBagConstraints.NONE; 39: constraints.anchor = GridBagConstraints.EAST; 40: Label label2 = new Label("Password:", Label.LEFT); 41: gridbag.setConstraints(label2, constraints); 42: add(label2); 43: 44: // password text field 45: buildConstraints(constraints, 1, 1, 1, 1, 0, 0); 46: constraints.fill = GridBagConstraints.HORIZONTAL; 47: TextField tfpass = new TextField(); 48: tfpass.setEchoCharacter('*'); 49: gridbag.setConstraints(tfpass, constraints); 50: add(tfpass); 51: 52: // OK Button 53: buildConstraints(constraints, 0, 2, 2, 1, 0, 20); 54: constraints.fill = GridBagConstraints.NONE; 55: constraints.anchor = GridBagConstraints.CENTER; 56: Button okb = new Button("OK"); 57: gridbag.setConstraints(okb, constraints); 58: add(okb); 59: } 60:} ipadx and ipadyBefore finishing up with grid bag layouts (isn't it over yet?), there are a two more constraints that deserve mentioning: ipadx and ipady. These two constraints control the padding-that is, the extra space around an individual component. By default, no components have extra space around them (which is easiest to see in components that fill their cells). ipadx adds space to either side of the component, and ipady adds it above and below. InsetsHorizontal and vertical gap, created when you create a new layout manager (using ipadx and ipady in grid bag layouts), are used to determine the amount of space between components in a panel. Insets, however, are used to determine the amount of space around the panel itself. The Insets class includes values for the top, bottom, left, and right insets, which are then used when the panel itself is drawn. Insets determine the amount of space between the edges of a panel and that panel's components. To include an inset, override the insets() method in your class (your Applet class or other class that serves as a panel). Inside the insets() method, create a new Insets object, where the constructor to the Insets class takes four integer values representing the insets on the top, left, bottom, and right of the panel. The insets() method should then return that Insets object. Here's some code to add insets for a grid layout, 10 to the top and bottom, and 30 to the left and right. (Figure 13.20 shows the inset): public Insets insets() { return new Insets(10, 30, 10, 30); } The arguments to the Insets constructor provide pixel insets for the top, bottom, left, and right edges of the panel, respectively. This particular example provides an inset of 10 pixels on all four sides of the panel. Handling UI Actions and EventsIf you stopped reading today's lesson right now, you could go out and create an applet that had lots of little UI components, nicely laid out on the screen with the proper layout manager, gap, and insets. If you did stop right here, however, your applet would be really dull, because none of your UI components would actually do anything when they were pressed, typed into, or selected. For your UI components to do something when they are activated, you need to hook up the UI's action with an operation. Actions are a form of event, and testing for an action by a UI component involves event management. Everything you learned yesterday about events will come in handy here. UI actions are events that occur when a UI component is activated-pressed, selected, typed into, and so on. To intercept an action event generated by any UI component, you define an action() method in your applet or class: public boolean action(Event evt, Object arg) { ... } The action() method should look similar to the basic mouse and keyboard event methods. Like those methods, it gets passed the event object that represents this event. It also gets an extra object (in this code, the parameter arg), which can be of any class type. What kind of object that second argument to the action method is depends on the UI component that's generating the action. The basic definition is that it's any arbitrary argument-when a component generates an event, it can pass along any extra information that might be useful for you to use in processing that action. All the basic UI components (except for labels, which have no action) have different actions and arguments:
Note that with actions, unlike with ordinary events, you can have many different kinds of objects generating the action event, as opposed to a single movement (a mouse press) generating a single event (such as a mouseDown). To deal with those different UI components and the actions they generate, you have to test for the type of object that sent/created the event in the first place inside the body of your action() method. That object is stored in the event's target instance variable, and you can use the instanceof operator to find out what kind of UI component sent it: public boolean action(Event evt, Object arg) { if (evt.target instanceof TextField) return handleText(evt.target); else if (evt.target instanceof Choice) return handleChoice(arg); ... } Although you can handle UI actions in the body of the action() method, it's much more common simply to define a special method in your action() method and call that method instead. Here, there are two special methods: one to handle the action on the text field (handleText()) and one to handle the action on the choice menu (handleChoice()). Depending on the action you want to handle, you may also want to pass on the argument from the action, the UI component that sent it, or any other information that the event might contain. As with the other event methods, action() returns a boolean value. As with all the event methods, you should return true if action() itself deals with the method, or false if it passes the method on somewhere else (or ignores it). Listing 13.2 shows a simple applet that has five buttons labeled with colors. The action() method tests for a button action and then passes control to a method called changeColor(), which changes the background color of the applet based on which button was pressed (see Figure 13.21 to see the applet in action).
Figure 13.21: The ButtonAction
applet. Listing 13.2. The ButtonActionsTest applet. 1:import java.awt.*; 2: 3:public class ButtonActionsTest extends java.applet.Applet { 4: 5: public void init() { 6: setBackground(Color.white); 7: 8: add(new Button("Red")); 9: add(new Button("Blue")); 10: add(new Button("Green")); 11: add(new Button("White")); 12: add(new Button("Black")); 13: } 14: 15: public boolean action(Event evt, Object arg) { 16: if (evt.target instanceof Button) { 17: changeColor((String)arg); 18: return true; 19: } else return false; 20: } 21: 22: void changeColor(String bname) { 23: if (bname.equals("Red")) setBackground(Color.red); 24: else if (bname.equals("Blue")) setBackground(Color.blue); 25: else if (bname.equals("Green")) setBackground(Color.green); 26: else if (bname.equals("White")) setBackground(Color.white); 27: else setBackground(Color.black); 28: 29: repaint(); 30: } 31:} As with most awt-based applets, this one starts with an init() method that initializes the applet's state and creates and adds components to the layout. The init() method defined in lines 8 through 13 here sets the applet's background color to white and creates five new buttons with color labels. Here we'll use the default layout manager, which is a FlowLayout. The buttons will appear all in a row at the top of the screen. With the buttons in place, the second step is to attach actions to those buttons. The action() method, defined in lines 15 through 20, does this. The first thing to check is to make sure it's a button action that's been generated (line 16) and, if so, to pass the extra argument (cast to a string) to the changeColor() method, which will do all the work to change the color. If the event is indeed a button action, we'll return true to intercept that event. Otherwise, we'll return false and let some other component handle the event. The changeColor() method is where all the work goes on. Here we test for each of the button labels in turn to see which button it was that was pressed and to set the background to the appropriate color. A final repaint at the end does the actual change (setting the background color does not automatically trigger a repaint; you'll have to do it yourself). Nesting Panels and ComponentsAdding UI components to individual panels or applets is fun, but working with the awt begins to turn into lots of fun when you start working with nested panels. By nesting different panels inside your applet, and panels inside those panels, you can create different layouts for different parts of the overall applet area, isolate background and foreground colors and fonts to individual parts of an applet, and manage the design of your UI components individually and in distinct groups. The more complex the layout of your applet, the more likely you're going to want to use nested panels. Nested PanelsPanels, as you've already learned, are components that can be actually displayed onscreen; Panel's superclass Container provides the generic behavior for holding other components inside it. The Applet class, from which your applets all inherit, is a subclass of Panel. To nest other panels inside an applet, you merely create a new panel and add it to the applet, just as you would add any other UI component: setLayout(new GridLayout(1, 2, 10, 10)); Panel panel1 = new Panel(); Panel panel2 = new Panel(); add(panel1); add(panel2); You can then set up an independent layout for those subpanels and add awt components to them (including still more subpanels) by calling the add() method in the appropriate panel: panel1.setLayout(new FlowLayout()); panel1.add(new Button("Up")); panel1.add(new Button("Down")); Although you can do all this in a single class, it's common in graphical applets and applications that make heavy use of subpanels to factor out the layout and behavior of the subpanels into separate classes and to communicate between the panels by using methods. You'll look at an extensive example of this later in today's lesson in the section "A Complete Example: RGB-to-HSB Converter." Events and Nested PanelsWhen you create applets with nested panels, those panels form a hierarchy from the outermost panel (the applet, usually) to the innermost UI component. This hierarchy is important to how each component in the interface interacts with other components; for example, the component hierarchy determines the order in which those components are painted to the screen. More importantly, however, the hierarchy also affects event handling, particularly for user-input events such as mouse and keyboard events. Events are received by the innermost component in the component hierarchy and passed up the chain to the applet's panel (or to the root window in Java applications). Suppose, for example, that you have an applet with a subpanel that can handle mouse events (using the mouseDown() and mouseUp() methods), and that panel contains a button. Clicking the button means that the button receives the event before the panel does; if the button isn't interested in that mouseDown(), the event gets passed to the panel, which can then process it or pass it further up the hierarchy. Remember the discussion about the basic event methods yesterday? You learned that the basic event methods all return boolean values. Those boolean values become important when you're talking about handling events or passing them on. An event-handling method, whether it is the set of basic event methods or the more generic handleEvent(), can do one of three things, given any random event:
More UI ComponentsAfter you master the basic UI components and how to add them to panels, organize their layout, and manage their events, you can add more UI components. In this section, you'll learn about text areas, scrolling lists, scrollbars, and canvases. Note that most of the components in this section do not produce actions, so you can't use the action() method to handle their behavior. Instead, you have to use a generic handleEvent() method to test for specific events that these UI components generate. You'll learn more about this in the next section. Text AreasText areas are like text fields, except they have more functionality for handling large amounts of text. Because text fields are limited in size and don't scroll, they are better for one-line responses and simple data entry; text areas can be any given width and height and have scrollbars by default, so you can deal with larger amounts of text more easily. Text areas are larger, scrollable text-entry components. Whereas text fields only provide one line of text, text areas can hold any amount of editable text. To create a text area, use one of the following constructors:
Figure 13.22 shows a simple text area generated from the following code: import java.awt.*; public class TextAreaTest extends java.applet.Applet { public void init() { String str = "Once upon a midnight dreary, while I pondered, weak and weary,\n" + "Over many a quaint and curious volume of forgotten lore,\n" + "While I nodded, nearly napping, suddenly there came a tapping,\n" + "As of some one gently rapping, rapping at my chamber door.\n" + "\"'Tis some visitor,\" I muttered, \"tapping at my chamber door-\n" + "Only this, and nothing more.\"\n\n"; // more text deleted for space add(new TextArea(str,10,50)); } }
Both text areas and text fields inherit from the TextComponent
class, so a lot of the behavior for text fields (particularly
getting and setting text and selections) is usable on text areas
as well (refer to Table 13.4). Text areas also have a number of
their own methods that you may find useful. Table 13.5 shows a
sampling of those methods.
Scrolling ListsRemember the choice menu, with which you could choose one of several different options? A scrolling list is functionally similar to a choice menu in that it lets you pick several options from a list, but scrolling lists differ in two significant ways:
Scrolling lists provide a menu of items that can be selected or deselected. Unlike choice menus, scrolling lists are not pop-up menus and can be defined to allow multiple selections. To create a scrolling list, create an instance of the List class and then add individual items to that list. The List class has two constructors:
After creating a List object, add items to it using the addItem() method and then add the list itself to the panel that contains it. Here's an example that creates a list five items high that allows multiple selections (the result of this code is shown in Figure 13.23): Figure 13.23: A scrolling list. import java.awt.*; public class ListsTest extends java.applet.Applet { public void init() { List lst = new List(5, true); lst.addItem("Hamlet"); lst.addItem("Claudius"); lst.addItem("Gertrude"); lst.addItem("Polonius"); lst.addItem("Horatio"); lst.addItem("Laertes"); lst.addItem("Ophelia"); add(lst); } } Scrolling lists generate actions when the user double-clicks a list item (single-clicking generates a LIST_SELECT or LIST_DESELECT event ID; you'll learn more about these in the section "More UI Events"). A scrolling list action has the argument of the string of the item that was double-clicked.
Table 13.6 shows some of the methods available to scrolling lists.
See the API documentation for a complete set.
Scrollbars and SlidersText areas and scrolling lists come with their own scrollbars, which are built into those UI components and enable you to manage both the body of the area or the list and its scrollbar as a single unit. You can also create individual scrollbars, or sliders, to manipulate a range of values. Scrollbars are used to select a value between a maximum and a minimum value. To change the current value of that scrollbar, you can use three different parts of the scrollbar (seeFigure 13.24): Figure 13.24: Scrollbar parts.
Choosing any of these visual elements causes a change in the scrollbar's value; you don't have to update anything or handle any events. All you have to do is give the scrollbar a maximum and minimum, and Java will handle the rest. A scrollbar is a visual UI element that allows you to choose a value between some minimum and some maximum. Scrollbars are sometimes called sliders. To create a scrollbar, you can use one of three constructors:
Here's a simple example of a scrollbar that increments a single value (see Figure 13.25). The label to the left of the scrollbar is updated each time the scrollbar's value changes: import java.awt.*; public class SliderTest extends java.applet.Applet { Label l; public void init() { setLayout(new GridLayout(1,2)); l = new Label("0", Label.CENTER); add(l); add(new Scrollbar(Scrollbar.HORIZONTAL,0,0,1,100)); } public Insets insets() { return new Insets(15,15,15,15); } public boolean handleEvent(Event evt) { if (evt.target instanceof Scrollbar) { int v = ((Scrollbar)evt.target).getValue(); l.setText(String.valueOf(v)); repaint(); return true; } else return false; } }
The Scrollbar class provides
several methods for managing the values within scrollbars. (See
Table 13.7.)
CanvasesAlthough you can draw on most awt components such as panels using the graphics methods you learned about on Day 11, "More Animation, Images, and Sound," canvases do little except let you draw on them. They can't contain other components, but they can accept events, and you can create animation and display images on them. If you have a panel that doesn't need to do anything except display images or animation, a canvas would make a lighter-weight surface than a panel would. A canvas is a component that you can draw on. To create a canvas, use the Canvas class and add it to a panel as you would any other component: Canvas can = new Canvas(); add(can); More UI EventsYesterday, you learned about some basic event types that are generated from user input to the mouse or the keyboard. These event types are stored in the Event object as the event ID, and can be tested for in the body of a handleEvent() method by using class variables defined in Event. For many basic events, such as mouseDown() and keyDown(), you can define methods for those events to handle the event directly. You learned a similar mechanism today for UI actions where creating an action() method handled a specific action generated by a UI component. The most general way of managing events, however, continues to be the handleEvent() method. For events relating to scrollbars and scrolling lists, the only way to intercept these events is to override handleEvent().
To intercept a specific event, test for that event's ID. The available
IDs are defined as class variables in the Event
class, so you can test them by name. You learned about some of
the basic events yesterday; Table 13.8 shows additional events
that may be useful to you for the components you've learned about
today (or that you might find useful in general).
Fun with ComponentsThe Component class is the root of all the awt objects: all the UI elements, panels, canvases, even applets. Just about everything you can display, lay out, change the color of, draw to, or interact with using events in the awt is a component. Components have a set of methods that allow you to modify their appearance or change their behavior. You've seen the use of a few of these methods already (setBackground(), setFont, size()), applied specifically to applets. But the methods defined in Component can be used with any component, allowing you to modify the appearance or the behavior of just about any element in your program. You can also create custom components (classes that inherit from Panel or Canvas) to make your own special awt elements or user interface widgets. Table 13.9 summarizes some of the methods you can use with individual components. For more methods, check out the Java API documentation for the class Component. The JDK 1.0.2 documentation is online at http://java.sun.com:80/products/JDK/CurrentRelease/api/
A Complete Example: RGB-to-HSB ConverterLet's take a break here from theory and smaller examples to create a larger example that puts together much of what you've learned so far. The following applet example demonstrates layouts, nesting panels, creating user-interface components, and catching and handling actions, as well as using multiple classes to put together a single applet. In short, it's the most complex applet you've created so far. Figure 13.26 shows the applet you'll be creating in this example. The ColorTest applet enables you to pick colors based on RGB (red, green, and blue) and HSB (hue, saturation, and brightness) values.
Figure 13.26: The ColorTest
applet.
The ColorTest applet has
three main parts: a colored box on the left side and two groups
of text fields on the right. The first group indicates RGB values;
the right, HSB. By changing any of the values in any of the text
boxes, the colored box is updated to the new color, as are the
values in the other group of text boxes.
This applet uses two classes:
Let's work through this step by step, because it's very complicated and can get confusing. All the code for this applet will be shown at the end of this section. Designing and Creating the Applet LayoutThe best way to start creating an applet that uses awt components is to worry about the layout first and then worry about the functionality. When dealing with the layout, you should start with the outermost panel first and work inward. Making a sketch of your UI design can help you figure out how to organize the panels inside your applet or window to best take advantage of layout and space. Paper designs are helpful even when you're not using grid bag layouts, but doubly so when you are (we'll be using a simple grid layout for this applet). Figure 13.27 shows the ColorTest applet with a grid drawn over it so that you can get an idea of how the panels and embedded panels work. Figure 13.27: The ColorTest applet panels and components. Let's start with the outermost panel-the applet itself. This panel has three parts: the color box on the left, the RGB text fields in the middle, and the HSB fields on the right. Because the outermost panel is the applet itself, your ColorTest class will be the applet class and will inherit from Applet. You'll also import the awt classes here (note that because you use so many of them in this program, it's easiest to just import the entire package): import java.awt.*; public class ColorTest extends java.applet.Applet { ... } This applet has three main things to keep track of: the color box and the two subpanels. The two subpanels each refer to different things, but they're essentially the same panel and behave in the same ways. Rather than duplicate a lot of code here in this class, this is a perfect opportunity to create another class just for the subpanels, use instances of that class here in the applet, and communicate between everything using methods. In a bit we'll define that new class, called ColorControls. For now, however, we know we need to keep a handle to all three parts of the applet so you can update them when they change. So let's create three instance variables: one of type Canvas for the color box, and the other two of type ColorControls for the control panels: ColorControls RGBcontrols, HSBcontrols; Canvas swatch; Now we'll move onto the init() method, where all the basic initialization and layout of the applet takes place. There are three steps to initializing this applet:
Step one is the layout. Let's use a grid layout and a gap of 10 points to separate each of the components: setLayout(new GridLayout(1, 3, 5, 15)); Step two is creating the components-the canvas first. You have an instance variable to hold that one. Here we'll create the canvas and initialize its background to black: swatch = new Canvas(); swatch.setBackground(Color.black); You need to create two instances of your as-of-yet nonexistent ColorControls panels here as well, but because we haven't created the class yet we don't know what the constructors to that class will look like. Let's put in some placeholder constructors here; we'll fill in the details later: RGBcontrols = new ColorControls(...) HSBcontrols = new ColorControls(...); Step three is adding all three components to the applet panel: add(swatch); add(RGBcontrols); add(HSBcontrols); While you're working on layout, let's add insets for the applet: 10 points along all the edges: public Insets insets() { return new Insets(10, 10, 10, 10); } Got it so far? At this point you have three instance variables, an init() method with two incomplete constructors, and an insets() method in your ColorTest class. Let's move on now to creating the subpanel layout in the ColorControls class so we can fill in those constructors and finish up the layout. Defining the SubpanelsThe ColorControls class will have behavior for laying out and handling the subpanels that represent the RGB and HSB values for the color. ColorControls doesn't need to be a subclass of Applet because it isn't actually an applet; it's just a panel. Define it to inherit from Panel: import java.awt.* class ColorControls extends Panel { ... }
The ColorControls class will need a number of instance variables so that information from the panel can get back to the applet. The first of these instance variables is a hook back up to the applet class that contains this panel. Because it's the outer applet class that controls the updating of each panel, this panel will need a way to tell the applet that something has changed. And to call a method in that applet, you need a reference to that object. So, instance variable number one is a reference an instance of the class ColorTest: ColorTest applet; If you figure that the applet class is the one that's going to be updating everything, that class if going to be interested in the individual text fields in this subpanel. We'll create instance variables for those text fields: TextField tfield1, tfield2, tfield3; Now let's move on to the constructor for this class. Because this class isn't an applet, we won't use init() to initialize it; instead we'll use a constructor method. Inside the constructor you'll do much of what you did inside init(): create the layout for the subpanel, create the text fields, and add them to the panel. The goal here is to make the ColorControls class generic enough so that you can use it for both the RGB fields and the HSB fields. Those two panels differ in only one respect: the labels for the text. That's three values to get before you can create the object. You can pass those three values in through the constructors in ColorTest. You also need one more: that reference to the enclosing applet, which you can get from the constructor as well. You now have four arguments to the basic constructor for the ColorControls class. Here's the signature for that constructor: ColorControls(ColorTest parent, String l1, String l2, String l3) { } Let's start this constructor by first setting the value of parent to the applet instance variable: applet = parent; Next, create the layout for this panel. You can also use a grid layout for these subpanels, as you did for the applet panel, but this time the grid will have three rows (one for each of the text field and label pairs) and two columns (one for the labels and one for the fields). We'll also define a 10-point gap between the components in the grid: setLayout(new GridLayout(3,2,10,10)); Now we can create and add the components to the panel. First, we'll create the text field objects (initialized to the string "0"), and assign them to the appropriate instance variables: tfield1 = new TextField("0"); tfield2 = new TextField("0"); tfield3 = new TextField("0"); Now we'll add those fields and the appropriate labels to the panel, using the remaining three parameters to the constructor as the text for the labels: add(new Label(l1, Label.RIGHT)); add(tfield1); add(new Label(l2, Label.RIGHT)); add(tfield2); add(new Label(l3, Label.RIGHT)); add(tfield3); That finishes up the constructor for the subpanel class ColorControls. Are we done with the layout? Not quite. We'll also add an inset around the subpanel-only on the top and bottom edges-to tinker the layout. Add the inset here as you did in the ColorTest class, using the insets() method: public Insets insets() { return new Insets(10, 10, 0, 0); } You're almost there. You have 98 percent of the basic structure in place and ready to go, but there's one step left: going back to ColorTest and fixing those placeholder constructors for the subpanel so they match the actual constructors for ColorControls. The constructor for ColorControls that we just created now has four arguments: the ColorTest object and three labels (strings). Remember back to when we created the init() method for ColorTest: We added two placeholders for creating new ColorControls objects; we'll replace those placeholders with the correct versions now. Make sure you add the four arguments that constructor needs to work: the ColorTest object and three strings. To pass the ColorTest object to those constructors, we can use the this keyword: RGBcontrols = new ColorControls(this, "Red", "Green", "Blue"); HSBcontrols = new ColorControls(this, "Hue", "Saturation", "Brightness");
Handling the ActionsWith the layout done, its time to set up event handling and updating between the various components so that when the user interacts with the applet, the applet can respond. The action of this applet occurs when the user changes a value in any of the text fields and presses Enter. By causing an action in a text field, the color changes, the color box updates to the new color, and the values of the fields in the opposite subpanel change to reflect the new color. The ColorTest class is responsible for actually doing the updating because it keeps track of all the subpanels. Because the actual event occurs in the subpanel, however, you'll need to track and intercept those events in that subpanel using the action() method in the ColorControls class: public boolean action(Event evt, Object arg) { if (evt.target instanceof TextField) { applet.update(this); return true; } else return false; } In the action() method, you test to make sure the action was indeed generated by a text field (because there are only text fields available, that's the only action you'll get, but it's a good idea to test for it anyhow). If there was indeed a text field action, we'll call a method to update all the subpanels. That method, which we'll call update(), is defined in the enclosing class, so we'll call it using the object stored in the applet instance variable (and pass along a reference to the panel so that the applet can get at our values). And, finally, we'll return either true or false so that other actions that might occur on this applet can be passed along to enclosing panels or components. Updating the Result
Now comes the hard part: actually doing the updating based on
the new values of whatever text field was changed. For this, you
define the update() method
in the ColorTest class. This
update() method takes a single
argument-the ColorControls
instance that contains the changed value (you get that argument
from the action() method
in the ColorControls object).
The update() method is responsible for updating all the panels in the applet. To know which panel to update, you need to know which panel changed. You can find out by testing to see whether the argument you got passed from the panel is the same as the subpanels you have stored in the RGBcontrols and HSBcontrols instance variables: void update(ColorControls controlPanel) { if (controlPanel == RGBcontrols) { // RGB has changed, update HSB ... } else { // HSB has changed, update RGB ... } } This test is the heart of the update() method. Let's start with that first case-a number has been changed in the RGB text fields. So now, based on those new RGB values, you have to generate a new Color object and update the values on the HSB panel. To reduce some typing, you create a few local variables to hold some basic values. In particular, the values of the text fields are strings whose values you can get to using the getText() method defined in the TextField objects of the ColorControls object. Because most of the time in this method we'll want to deal with those values as integers, we'll get those string values, convert them to integers, and store them in local variables (value1, value2, value3). Here's the code to do this (it looks more complicated than it actually is): int value1 = Integer.parseInt(controlPanel.tfield1.getText()); int value2 = Integer.parseInt(controlPanel.tfield2.getText()); int value3 = Integer.parseInt(controlPanel.tfield3.getText()); While we're here defining local variables, we'll also need one for the new Color object: Color c; OK. Let's assume one of the text fields in the RGB side of the applet has changed and add the code to the if part of the update() method. We'll need to create a new Color object and update the HSB side of the panel. That first part is easy; given the three RGB values, you can create a new Color object using those as arguments to the constructor: c = new Color(value1, value2, value3);
Now we'll convert the RGB values to HSB. There are standard algorithms to convert an RGB-based color to an HSB color, but we don't have to go look them up. The Color class has a class method we can use called RGBtoHSB() that will do the work for us-or, at least, most of it. There are two problems with the RGBtoHSB() method, however:
Neither of these problems is insurmountable; it just means some extra lines of code. Let's start by calling RGBtoHSB() with the new RGB values we have. The return type of that method is an array of floats, so we'll create a local variable (HSB) to store the results of the RBGtoHSB() method. (Note that you'll also need to create and pass in an empty array of floats as the fourth argument to RGBtoHSB()): float[] HSB = Color.RGBtoHSB(value1, value2, value3, (new float[3])); Now we'll convert those floating-point values that range from 0.0 to 1.0 to values that range from 0 and 100 (for the saturation and brightness) and 0 to 360 for the hue by multiplying the appropriate numbers and reassigning the value back to the array: HSB[0] *= 360; HSB[1] *= 100; HSB[2] *= 100; Now we have the numbers we want. The last part of the update is to put those values back into the text fields. Of course, those values are still floating-point numbers, so we'll have to cast them to ints before turning them into strings and storing them: HSBcontrols.tfield1.setText(String.valueOf((int)HSB[0])); HSBcontrols.tfield2.setText(String.valueOf((int)HSB[1])); HSBcontrols.tfield3.setText(String.valueOf((int)HSB[2])); You're halfway there. The next part of the applet is that part that updates the RGB values where a text field on the HSB side has changed. This is the else in the big if-else that defines this method and determines what to update, given a change. It's actually easier to generate values from HSB values than it is to do it the other way around. There's a class method in the Color class, called getHSBColor(), that creates a new Color object from three HSB values, and once you have a Color object you can easily pull the RGB values out of there. The catch, of course, is that getHSBColor takes three floating-point arguments, and the values we have are the integer values I prefer to use. So in the call to getHSBColor, we'll have to cast the integer values from the text fields to floats and divide them by the proper conversion factor. The result of getHSBColor is a Color object, so we can simply assign that object to our c local variable so we can use it again later: c = Color.getHSBColor((float)value1 / 360, (float)value2 / 100, (float)value3 / 100); With the Color object all set, updating the RGB values involves extracting those values from that Color object. The getRed(), getGreen() and getBlue() methods, defined in the Color class, will do just that: RGBcontrols.tfield1.setText(String.valueOf(c.getRed())); RGBcontrols.tfield2.setText(String.valueOf(c.getGreen())); RGBcontrols.tfield3.setText(String.valueOf(c.getBlue())); And finally, regardless of whether the RGB or HSB value has changed, you'll need to update the color box on the left to reflect the new color. Because we have a new Color object stored in the variable c, we can use the setBackground method to change that color. Also note that setBackground doesn't automatically repaint the screen, so you'll want to fire off a repaint() as well: swatch.setBackground(c); swatch.repaint(); That's it! You're done. Compile both the ColorTest and ColorControls classes, create an HTML file to load the ColorTest applet, and check it out. The Complete Source CodeListing 13.3 shows the complete source code for the applet class ColorTest, and Listing 13.4 shows the source for the helper class ColorControls. Often it's easier to figure out what's going on in an applet when it's all in one place and you can follow the method calls and how values are passed back and forth. Start with the init() method in the ColorTest applet and go from there. Listing 13.3. The ColorTest applet. 1:import java.awt.*; 2: 3:public class ColorTest extends java.applet.Applet { 4: ColorControls RGBcontrols, HSBcontrols; 5: Canvas swatch; 6: 7: public void init() { 8: setLayout(new GridLayout(1,3,5,15)); 9: 10: // The color swatch 11: swatch = new Canvas(); 12: swatch.setBackground(Color.black); 13: 14: // the subpanels for the controls 15: RGBcontrols = new ColorControls(this, "Red", "Green", "Blue"); 16: HSBcontrols = new ColorControls(this, "Hue", "Saturation", "Brightness"); 17: 18: //add it all to the layout 19: add(swatch); 20: add(RGBcontrols); 21: add(HSBcontrols); 22: } 23: 24: public Insets insets() { 25: return new Insets(10,10,10,10); 26: } 27: 28: void update(ColorControls controlPanel) { 29: Color c; 30: // get string values from text fields, convert to ints 31: int value1 = Integer.parseInt(controlPanel.tfield1.getText()); 32: int value2 = Integer.parseInt(controlPanel.tfield2.getText()); 33: int value3 = Integer.parseInt(controlPanel.tfield3.getText()); 34: 35: if (controlPanel == RGBcontrols) { // RGB has changed, update HSB 36: c = new Color(value1, value2, value3); 37: 38: // convert RGB values to HSB values 39: float[] HSB = Color.RGBtoHSB(value1, value2, value3, (new float[3])); 40: HSB[0] *= 360; 41: HSB[1] *= 100; 42: HSB[2] *= 100; 43: 44: // reset HSB fields 45: HSBcontrols.tfield1.setText(String.valueOf((int)HSB[0])); 46: HSBcontrols.tfield2.setText(String.valueOf((int)HSB[1])); 47: HSBcontrols.tfield3.setText(String.valueOf((int)HSB[2])); 48: 49: } else { // HSB has changed, update RGB 50: c = Color.getHSBColor((float)value1 / 360, 51: (float)value2 / 100, (float)value3 / 100); 52: 53: // reset RGB fields 54: RGBcontrols.tfield1.setText(String.valueOf(c.getRed())); 55: RGBcontrols.tfield2.setText(String.valueOf(c.getGreen())); 56: RGBcontrols.tfield3.setText(String.valueOf(c.getBlue())); 57: } 58: 59: //update swatch 60: swatch.setBackground(c); 61: swatch.repaint(); 62:} 63:} Listing 13.4. The ColorControls class. 1:import java.awt.*; 2: 3:class ColorControls extends Panel { 4: TextField tfield1, tfield2, tfield3; 5: ColorTest applet; 6: 7: ColorControls(ColorTest parent, 8: String l1, String l2, String l3) { 9: 10: // get hook to outer applet parent 11: applet = parent; 12: 13: //do layouts 14: setLayout(new GridLayout(3,2,10,10)); 15: 16: tfield1 = new TextField("0"); 17: tfield2 = new TextField("0"); 18: tfield3 = new TextField("0"); 19: 20: add(new Label(l1, Label.RIGHT)); 21: add(tfield1); 22: add(new Label(l2, Label.RIGHT)); 23: add(tfield2); 24: add(new Label(l3, Label.RIGHT)); 25: add(tfield3); 26: } 27: 28: public Insets insets() { 29: return new Insets(10,10,0,0); 30: } 31: 32: public boolean action(Event evt, Object arg) { 33: if (evt.target instanceof TextField) { 34: applet.update(this); 35: return true; 36: } else return false; 37: } 38:} Up and Coming in Java 1.1Everything you've learned up to this point is available in the 1.0.2 Java API. Java 1.1, however, will add many more features to the awt, as well as improve performance and robustness across platforms. The goal for the awt is to move beyond the basics that 1.0.2 provided and make the awt more suitable for large-scale application development. Note, also, that the 1.1 API will be backward-compatible with the 1.0.2 features; none of the code you write after reading this chapter will be obsolete in 1.1. Explicit details about the changes to the awt for 1.1 were not available at the time this book was being written. Sun has announced the following teasers, however for new features in 1.1:
For more information about the Java 1.1 changes to the awt, check out the 1.1 preview page at http://java.sun.com/products/JDK/1.1/designspecs/. SummaryThe Java awt, or Abstract Windowing Toolkit, is a package of Java classes and interfaces for creating full-fledged access to a window-based graphical user interface system, with mechanisms for graphics display, event management, text and graphics primitives, user-interface components, and cross-platform layout. Applets are also an integral part of the awt. Today has been a big day; the lesson has brought together everything you've learned up to this point about simple applet management and added a lot more about creating applets, panels, and user-interface components and managing the interactions between all of them. With the information you got today and the few bits you'll learn tomorrow, you can create cross-platform Java applications that do just about anything you want.
Q&A
|
I really dislike working with layout managers; they're either too simplistic or too complicated (grid bag layout). Even with a whole lot of tinkering, I can never get my applets to look like I want them to. All I want to do is define the sizes of my components and put them at an x and y position on the screen. Can I do this? | |
I'm going to tell you how to do this, but not without a lecture. Java applications and the awt were designed such that the same graphical user interface could run equally well on different platforms and with different resolutions, different fonts, different screen sizes, and so on. Relying on pixel coordinates in this case is a really bad idea; variations from one platform to another or even from one Java environment to another on the same platform can mess up your careful layouts such that you can easily have components overlapping or obscuring each other, the edges of your applet cut off, or other layout disasters. Just as an example-I found significant differences in the layout of the same applet running in the JDK's appletviewer and in Netscape, both on Windows 95, side by side. Can you guarantee that your applet will always be run in precisely the same environment as the one in which you designed it? Layout managers, by dynamically placing elements on the screen, get around these problems. This does mean that your applet may end up looking not quite right on any platform-but at least it's usable on any platform. New versions of the awt promise to offer better layout and UI design controls. Still not convinced? Well, then. To make a component a specific size and to place it at a particular position, use a null layout manager and the reshape() method:
setLayout(null); You can find out more about reshape() in the Component class. | |
I was exploring the awt classes, and I saw this subpackage called peer. There are also references to the peer classes sprinkled throughout the API documentation. What do peers do? | |
Peers are responsible for the platform-specific parts of the awt. For example, when you create a Java awt window, you have an instance of the Window class that provides generic window behavior, and then you have an instance of a class implementing WindowPeer that creates the very specific window for that platform-a motif window under X Window, a Macintosh-style window under the Macintosh, or a Windows 95 window under Windows 95. These "peer" classes also handle communication between the window system and the Java window itself. By separating the generic component behavior (the awt classes) from the actual system implementation and appearance (the peer classes), you can focus on providing behavior in your Java application and let the Java implementation deal with the platform-specific details. | |
There's a whole lot of functionality in the awt that you haven't talked about here. Why? | |
Given that even a basic introduction took this long, I figured that if I put in even more detail than I already have, this book would turn into Teach Yourself Java in 21 Days Plus a Few Extra for the awt Stuff. | |
As it is, I've left windows, menus, and dialog boxes until tomorrow, so you'll have to wait for those. But you can find out about a lot of the other features of awt merely by exploring the API documentation. Start with the Applet class and examine the sorts of methods you can call. Then look at Panel, from which Applet inherits-you have all that class's functionality as well. The superclass of Panel is Container, which provides still more interesting detail. Component comes next. Explore the API and see what you can do with it. You might find something interesting. | |
I have a new button class I defined to look different from the standard awt button objects. I'd like to implement callbacks on this button (that is, to execute an arbitrary function when the button is pressed), but I can't figure out how to get Java to execute an arbitrary method. In C++ I'd just have a pointer to a function. In Smalltalk I'd use perform:. How can I do this in Java? | |
You can't; Java doesn't have this facility. This is why normal button actions are executed from the generic action() method rather than using a mechanism for actions attached to the button itself (which would be more object-oriented, easier to extend, and wouldn't require a whole lot of if...elses inside action()). |
Use of this site is subject certain Terms & Conditions. Copyright (c) 1996-1999 EarthWeb, Inc.. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of EarthWeb is prohibited. Please read our privacy policy for details. |