Lesson 11.2 Canvas
Canvas & Component
Now that you know how to paint, it's time to look at where you should paint. In Java, you have six different choices:
- You can paint right on your applet, as we've been doing.
- You can paint on a Panel. You can paint on a Canvas.
- You can paint on a Window. You can paint on a Frame.
- You can paint on a Component or Container.
In this lesson, we'll look at painting on Canvas objects and Component objects.
Inheritance Revisited
Component, Canvas, Window? What are all of those classes?
Before we take a closer look at these classes, let's step back and take a look at their AWT family tree. In Java, the Object class is ultimate superclass for all classes. Although important, in the Abstract Window Toolkit [AWT], the Component class is probably more important. Component is the superclass for most AWT GUI "widgets" such as Label, Button, and TextField, as you can see from the illustration. [click image to enlarge].
If you look closely at the illustration, you might notice that the Canvas class is nowhere to be found, but that's only because the illustration is abbreviated. The Canvas class is a subclass of Component just like Button or Label. The Component class is an abstract class; it has methods, but you cannot create Component objects. For that, you have to use one of Component's concrete subclasses, such as Button or Label. You can also create your own Component subclasses, which you'll learn to do shortly. One of the most important of the Component subclasses is the Container class. A Container is a Component that can "hold" other Components. Like Component, Container is also an abstract class; you must use one of the concrete Container subclasses such as Panel, Frame, and Window, or you must create your own Container subclass.
The Canvas Class
One of the problems with overriding the Label or Button class is that native peers, the Mac buttons or the Windows buttons that actually carry out the work of "being a button", don't act the same on every platform.
The solution is to write your own Button or Label class "from scratch." That way you can be sure that your Buttons will look and work the same on every platform.
Heavyweights & Lightweights
In Java 1.0 there was only one way to do this: by using the Canvas class. The Canvas class provides a simple "bare" component that you can extend to create your own controls. The advantages of using Canvas as the basis for your own controls are:
- Your custom components will work in Java 1.0 as well as Java 1.1 browsers.
- The code is slightly simpler than that required by the alternative methods.
There are disadvantages as well. Each of your Canvas-based components uses a native "window peer". It is because of the use of these native peers that Canvas-based components are called "heavyweight" components.
The disadvantages of using heavyweight components are:
- Your O/S normally has a limited number of window peers, so using them for buttons and labels depletes system resources.
- The heavyweight window peer is opaque, so all of your controls must appear to be rectangular.
In Java 1.1, you can also directly extend the Component class to create "lightweight" components. Lightweight components do not require a native peer, and so are somewhat faster and use fewer resources than heavyweights.
Lightweight components can also contain transparent regions, so you can create round or irregularly shaped components. You'll get your chance to build a lightweight component shortly. Right now, let's look at creating your own classes using the Canvas class as a starting point. We'll start by building a very simple class--XCanvas--and then we'll look at a more realistic project.
Extending Canvas
When you extend the Canvas class, whether to create your own Label class or for anything else, you'll almost always override the getPreferredSize() method. [preferredSize() if you're writing a 1.0 component.
Canvas objects don't have a "default" size, and if you fail to override getPreferredSize() your component won't appear at all when placed in a FlowLayout controlled container. In addition to getPreferredSize(), you'll also need to override the paint() method.
The XCanvas Class
The XCanvas class is the simplest possible extension to the Canvas class. Every XCanvas object is 50 pixels square, and contains a nice X on its face. The epitome of usefulness.
To write XCanvas you follow these steps:
- Create a new class extending Canvas. Override getPreferredSize() returning a new 50x50 Dimension object.
- Override the paint() method, using drawLine() to create your mark.
It doesn't get much simpler than that. Here's the finished XCanvas class:
|
XCanvas.java
|
| import java.awt.*;
public class XCanvas extends Canvas
{
public Dimension getPreferredSize()
{
return new Dimension(50, 50);
} public void paint(Graphics g)
{
int x2 = getSize().width - 1;
int y2 = getSize().height - 1;
g.drawLine( 0, 0, x2, y2);
g.drawLine(x2, 0, 0, y2);
}
}
|
Here's an applet that tests the XCanvas class. Note that you can send any Component message to an XCanvas object: setBackground() works, as does setForeground() and setLocation(). Remember, though, that when placed into a FlowLayout controlled applet, like TestX.java, every Component is resized according to its preferred size, so all three XCanvii [plural of XCanvas] end up 50 pixels wide.
|
TestX.java
|
// Test XCanvas
import java.awt.*;
import java.applet.*;
public class TestX extends Applet
{
public void init()
{
XCanvas red = new XCanvas();
XCanvas grn = new XCanvas();
XCanvas blu = new XCanvas(); red.setBackground(Color.red);
red.setForeground(Color.green); grn.setBackground(Color.green);
grn.setForeground(Color.red); blu.setBackground(Color.blue);
blu.setForeground(Color.yellow);
grn.setSize(100, 100);
add(red);
add(grn);
add(blu);
}
}
|
Now that you've mastered the Canvas "basics", [what few there are], it's time to look at a more extensive example, that involves a little more complex code. This second Canvas example, BorderLabel, works like a regular Label except that it allows you to set one of several different borders around the label.
Designing BorderLabel
A "full-featured" BorderLabel class should be able to do everything that a Label can do, as well as some extra. Since that would make this lesson a little long, we'll simplify our BorderLabel to meet these requirements:
- We'll write only a single constructor, BorderLabel(String). We won't worry about alignment; we'll center everything. We'll have only four border styles: PLAIN, RAISED, SUNKEN, and LINE.
- The default border thickness will be 2 pixels wide.
Attributes
Given these requirements, you can see that we'll need the following attributes:
- Four public static final constants for the border styles. A String attribute to hold the label's text.
- A pair of int attributes to hold the border style and the thickness of the border.
Methods
We'll also need the following methods:
- A single-argument constructor that takes a String. The String will be used to initialize the label's text property. An accessor/mutator pair for the text property. A mutator for the border thickness property, and another for the border style property.
- We'll need to override getPreferredSize() [as previously mentioned] as well as paint().
Here's a "skeleton" class that meets these requirements:
|
The BorderLabel Skeleton
|
| import java.awt.*;
public class BorderLayout extends Canvas
{
// Constants
public static final int PLAIN = 0;
public static final int LINE = 1;
public static final int RAISED = 2;
public static final int SUNKEN = 3;
// Constructor
public BorderLabel(String text) {}
// Accessors & mutators
public String getText() {}
public void setText(String s) {}
public void setBorderStyle(int n) {}
public void setBorderThickness(int n) {}
// Overridden methods
public void paint(Graphics g) {}
public Dimension getPreferredSize() {}
// Attributes
private String text = "";
private int border = PLAIN;
private int thickness = 2;
}
|
The Constructor
Of all the methods, the constructor is the easiest; all it has to do is set the value of the text field like this:
public BorderLabel(String text)
{
this.text = text;
} |
Accessors & Mutators
Each of the accessor and mutator methods is equally simple. The accessor requires only a single line of text, while the mutators each require a second line to apply their changes to the label like this:
// Accessor --------------------------------
public String getText()
{
return text;
}
// Mutators --------------------------------
public void setText(String s)
{
text = s;
repaint();
} public void setBorderStyle(int n)
{
border = n;
repaint();
}
public void setBorderThickness(int n)
{
thickness = n;
repaint();
}
|
getPreferredSize()
The getPreferredSize() method is a little more complex. Here's what it has to accomplish:
- Measure the height and width of the text, given the current font. This requires both a Graphics object, and a FontMetrics object.
- Decide on the overall width and height of the label based upon the thickness of the border and the size of the rendered text. You'll also want to allow a little extra for spacing between the border and the label's text.
The first part of this is pretty straightforward:
public Dimension getPreferredSize()
{
int strWidth = 0;
int strHeight = 0;
Graphics g = getGraphics();
if (g != null)
{
FontMetrics fm = g.getFontMetrics();
strWidth = fm.stringWidth(text);
strHeight = fm.getHeight();
g.dispose();
}
// Calculate new size here
} |
Notice that after calling getGraphics() we check to see if the call succeeded. It's possible that getPreferredSize() will be called when the Graphics context is not yet initialized. After measuring the size of the text, we can calculate a new height and width like this:
- The width will be the width of the text plus the thickness of the border on the left and on the right, plus fourteen to provide a seven-pixel spacing between the border and each end of the text. [When you improve on this course, you may want to add adjustable insets to take care of this].
- The height of the label will be the height of the text plus the thickness of the top and bottom borders plus ten, to provide a five-pixel spacing above and below the text.
Here's the line you need to add to meet those requirements:
return new Dimension(
thickness * 2 + 14 + strWidth,
thickness * 2 + 10 + strHeight); |
The paint() Method
The paint() method is the most complex part of your class. To make it a little simpler, divide it into two parts:
- First draw the border.
- Then draw the text.
To draw the border, you use an if statement or a switch statement like this:
int w = getSize().width;
int h = getSize().height;
switch (border)
{
case RAISED:
case SUNKEN:
g.setColor(getBackground());
for (int i = 0; i < thickness; i++)
g.draw3DRect(0+i, 0+i,
w-1-(i*2), h-1-(i*2),
border==RAISED);
break;
case LINE:
g.setColor(getForeground());
g.fillRect(0, 0, w, h);
g.setColor(getBackground());
g.fillRect(thickness, thickness,
w - (thickness * 2),
h - (thickness * 2));
break;
default:
g.setColor(getBackground());
g.fillRect(0, 0, w, h);
}
|
Once the border has been drawn, just compute the "x,y" position for your text like this:
FontMetrics fm = g.getFontMetrics();
int x = getSize().width / 2 -
fm.stringWidth(text) /2;
int y = getSize().height / 2 +
fm.getHeight() / 2; |
Then you can draw your text using drawString() like this:
g.setColor(getForeground());
g.drawString(text, x, y); |
Try It Out
Here's an applet, TestBL.java, that puts the BorderLabel class through its paces. Here's the completed source code for BorderLabel.java as well.
Extending the Component Class
As mentioned earlier, classes derived directly from the Component class are called lightweight components, because such classes are "peerless": they don't use one of your operating system's native windows objects each time you create a new object.
The advantage of this is that lightweight components require less resources [memory] than heavyweight components. The disadvantage is that such components can be, occasionally, temperamental. You may find yourself forced to write code to carry out actions that were previously performed by the operating system, "behind the scenes". As we did with the Canvas class, we'll look at two examples: the XCanvas class converted to a lightweight--we'll call it the XComponent class--and the BorderLabel class, likewise converted from Canvas to Component.
The XComponent Class
To create the XComponent class simply follow these steps:
- Copy XCanvas.java to XComponent.java. Change the class name. Change extends Canvas to extends Component.
- Recompile.
To create a test-bed for your XComponent, follow these steps:
- Copy TestX.java to TestXLW.java. Replace the class name with TestXLW. Globally change all references from XCanvas to XComponent.
- Recompile.
Here's what the program looks like when you run it:
Whassup!
If you run this in Netscape or IE, you won't see a foreground or background color. Each of the components appears as a black X. Looking at this, you might think that lightweight components don't have a background or foreground property, like heavyweight components. That's not really true. Each of the components you see running here has its own foreground color property set to green, red, and yellow respectively. Each background property is likewise set correctly.
The problem lies, not with the lightweight component, but with the Graphics object used to draw your X. Every Graphics object is intimately connected to a native window peer. Since your lightweight components don't have a peer, they also don't have their own dedicated Graphics object. Each of your lightweight components has to "borrow" a Graphics object from its nearest heavyweight companion. In this case, the nearest heavyweight is the Applet where the three XComponents reside. It might seem that such a deficiency would render lightweight components unusable. After all, if you can't even change their colors... Fortunately, such is not the case. To make your lightweight components as well behaved as their heavyweight cousins, you must remember to initialize the drawing pen and any fonts you use inside your paint() method. Note, though, that when you use the latest version of Java 2, this is done automatically, as you can see if you run this applet in the appletviewer. To retrieve the foreground color you send your component the getForeground() message. Using getBackground() retrieves the background color. Here's a revised set of lightweight XComponents showing their green, blue, and yellow:
Where's the Background?
As you can see, these last XComponents now draw in the correct color, but the background is still "missing in action". To fix this, you have to use fillRect() to color the background during the paint() method, if you want an opaque rectangular background.
Of course, that's one of the advantages of lightweight components; with lightweights you can have an opaque colored background if you choose. With heavyweights, the background must be opaque.
Lightweight Border Labels
Here's one last example. In the BorderLabel class you learned how to extend Canvas to create a label with a border. The BorderLabel class carefully sets its Graphics object before drawing, so we should be able to simply replace extends Canvas with extends Component, and be on our way.
Let's see if it works. Here is LWBorderLabel.java and the TestLWBL.java applet which exercises it. You can see the TestLWBL applet running below.
Something to Talk About
Perhaps you noticed that the TextXLW applet doesn't work exactly like the TestX applet. Why don't you try to fix it?
- Download and compile XComponent.java and TestXLW.java.
- Change the XComponent class, so that the TestXLW applet works just like the TestX applet.
Please continue to the next section of this lesson.
|