Subpages: 1. Labels and buttons overview
2. Custom buttons: part I - Transparent buttons
3. Custom buttons: part II - Polygonal buttons
4. Custom buttons: part III - Tooltip management
5.3 Custom buttons: part II - Polygonal buttons
The approach described in the previous section assumes that all navigational buttons have a rectangular shape. This can be too restrictive for complex active regions needed in the navigation of images such as geographical maps. In this example we will show how to extend the idea of transparent buttons developed in the previous example, to transparent non-rectangular buttons.
The java.awt.Polygon class is extremely helpful for this purpose, especially its following two related methods (see API docs for more info):
Polygon.contains(int x, int y): returns true if a point with the given coordinates is contained inside the Polygon.
Graphics.drawPolygon(Polygon polygon): draws an outline of a Polygon using given Graphics object.
The first method will be used in this example to verify that the mouse cursor is located inside the given polygon. The second will be used to actually draw a polygon representing the bounds of a non-rectangular button.
This seems fairly basic, but there is one significant complication that exists. All Swing components are encapsulated in rectangular bounds and nothing can be done about this. If some component receives a mouse event which occurs in it's rectangular bounds, the overlapped underlying components do not have a chance to receive this event. Figure 5.9 illustrates two overlapping non-rectangular buttons. A part of Button B lying under the rectangle of Button A will never receive mouse events and cannot be clicked.

Figure 5.9 Illustration of two overlapping non-rectangular buttons.
<<file figure5-9.gif>>
To resolve this situation we can skip any mouse event processing in our non-rectangular components. Instead, all mouse events can be directed to the parent container. All buttons can then register themselves as MouseListeners and MouseMotionListeners with that container. In this way, mouse events can be received without regard to overlapping! By doing this all buttons will receive notification about all events without any preliminary filtering. To minimize the resulting impact on the system's performance we need to provide a quick discard of events lying outside the button's basic rectangle.

Figure 5.10 Polygonal buttons in an applet.
<<file figure5-10.gif>>
The Code: ButtonApplet2.java
see \Chapter5\5
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
public class ButtonApplet2 extends JApplet
{
public ButtonApplet2() {}
public synchronized void init() {
// Unchanged code from section 5.2
int index = 1;
while(true) {
String paramSize = getParameter("button"+index);
String paramName = getParameter("name"+index);
String paramUrl = getParameter("url"+index);
if (paramSize==null || paramName==null || paramUrl==null)
break;
Polygon p = new Polygon();
try {
StringTokenizer tokenizer = new StringTokenizer(
paramSize, ",");
while (tokenizer.hasMoreTokens()) {
String str = tokenizer.nextToken().trim();
int x = Integer.parseInt(str);
str = tokenizer.nextToken().trim();
int y = Integer.parseInt(str);
p.addPoint(x, y);
}
}
catch (Exception ex) { break; }
PolygonButton btn = new PolygonButton(this, p,
paramName, paramUrl);
bigLabel.add(btn);
index++;
}
getContentPane().setLayout(null);
getContentPane().add(bigLabel);
bigLabel.setBounds(0, 0, bigImage.getIconWidth(),
bigImage.getIconHeight());
}
public String getAppletInfo() {
return "Sample applet with PolygonButtons";
}
public String[][] getParameterInfo() {
String pinfo[][] = {
{"image", "string", "base image file name"},
{"buttonX","x1,y1, x2,y2, ...", "button's bounds"},
{"nameX", "string", "tooltip text"},
{"urlX", "url", "link URL"} };
return pinfo;
}
}
class PolygonButton extends JComponent
implements MouseListener, MouseMotionListener
{
static public Color ACTIVE_COLOR = Color.red;
static public Color INACTIVE_COLOR = Color. darkGray;
protected JApplet m_parent;
protected String m_text;
protected String m_sUrl;
protected URL m_url;
protected Polygon m_polygon;
protected Rectangle m_rc;
protected boolean m_active;
protected static PolygonButton m_currentButton;
public PolygonButton(JApplet parent, Polygon p,
String text, String sUrl)
{
m_parent = parent;
m_polygon = p;
setText(text);
m_sUrl = sUrl;
try {
m_url = new URL(sUrl);
}
catch(Exception ex) { m_url = null; }
setOpaque(false);
m_parent.addMouseListener(this);
m_parent.addMouseMotionListener(this);
m_rc = new Rectangle(m_polygon.getBounds()); // Bug alert!
m_rc.grow(1, 1);
setBounds(m_rc);
m_polygon.translate(-m_rc.x, -m_rc.y);
}
public void setText(String text) { m_text = text; }
public String getText() { return m_text; }
public void mouseMoved(MouseEvent e) {
if (!m_rc.contains(e.getX(), e.getY()) || e.isConsumed()) {
if (m_active)
setState(false);
return; // quickly return, if outside our rectangle
}
int x = e.getX() - m_rc.x;
int y = e.getY() - m_rc.y;
boolean active = m_polygon.contains(x, y);
if (m_active != active)
setState(active);
if (m_active)
e.consume();
}
public void mouseDragged(MouseEvent e) {}
protected void setState(boolean active) {
m_active = active;
repaint();
if (m_active) {
if (m_currentButton != null)
m_currentButton.setState(false);
m_currentButton = this;
m_parent.setCursor(Cursor.getPredefinedCursor(
Cursor.HAND_CURSOR));
m_parent.showStatus(m_sUrl);
}
else {
m_currentButton = null;
m_parent.setCursor(Cursor.getPredefinedCursor(
Cursor.DEFAULT_CURSOR));
m_parent.showStatus("");
}
}
public void mouseClicked(MouseEvent e) {
if (m_active && m_url != null && !e.isConsumed()) {
AppletContext context = m_parent.getAppletContext();
if (context != null)
context.showDocument(m_url);
e.consume();
}
}
public void mousePressed(MouseEvent e) {}
public void mouseReleased(MouseEvent e) {}
public void mouseExited(MouseEvent e) { mouseMoved(e); }
public void mouseEntered(MouseEvent e) { mouseMoved(e); }
public void paint(Graphics g) {
g.setColor(m_active ? ACTIVE_COLOR : INACTIVE_COLOR);
g.drawPolygon(m_polygon);
}
}
Understanding the Code
Class ButtonApplet2
This class is a slightly modified version of the ButtonApplet class in the previous section to accommodate polygonal button sizes rather than rectangles (the parser has been modified to read in an arbitrary amount of points). Now it creates a Polygon instance and parses a data string, which is assumed to contain pairs of comma-separated coordinates, adding each coordinate to the Polygon using the the addPoint() method. The resulting Polygon instance is used to create a new PolygonButton component.
Class PolygonButton
This class serves as a replacement for the NavigateButton class in the previous example. Note that it extends JComponent directly. This is necessary to disassociate any mouse handling inherent in buttons (which is actually built into the button UI delegates). Remember, we want to handle mouse events ourselves, but we want them each to be sent from within the parent's bounds to each PolygonButton, not from each PolygonButton to the parent.
Note: This is the opposite way of working with mouse listeners that we are used to. The idea may take a few moments to sink in because directing events from child to parent is so much more common, we generally don't think of things the other way around.
So, to be notified of mouse events from the parent, we'll need to implement the MouseListener and MouseMotionListener interfaces.
Four new instance variables are declared:
Polygon m_polygon: the polygonal region representing this button's bounds.
Rectangle m_rc: this button's bounding rectangle as seen in the coordinate space of the parent.
boolean m_active: flag indicating that this button is active.
PolygonButton m_currentButton: a static reference to the instance of this class which is currently active.
The constructor of the PolygonButton class takes four parameters: a reference to the parent applet, the Polygon instance representing this component's bounds, tooltip text, and a String representation of a URL. It assigns all instance variables and instantiates a URL using the associate String parameter (similar to what we saw in the last example). Note that this component adds itself to the parent applet as a MouseListener and MouseMotionListener:
m_parent.addMouseListener(this);
m_parent.addMouseMotionListener(this);
The bounding rectangle m_rc is computed with the Polygon.getBounds() method. Note that this method does not create a new instance of the Rectangle class, but returns a reference to the an internal Polygon instance variable which is subject to change. This is not safe, so we must explicitly create a new Rectangle instance from the supplied reference. This Rectangle's bounds are expanded (using its grow() method) to take into account border width. Finally the Rectangle m_rc is set as the button's bounding region, and the Polygon is translated into the component's local coordinates by shifting it's origin using its translate() method.
The mouseMoved() method is invoked when mouse events occur in the parent container. First we quickly check whether the event lies inside our bounding rectangle and is not yet consumed by another component. If this is true, we continue processing this event. Otherwise our method returns. Before we return, however, we first check whether this button is still active for some reason (this can happen if the mouse cursor moves too fast out of this button's bound, and the given component did not receive a MOUSE_EXITED MouseEvent to deactivate itself). If this is the case, we deactivate it and then exit.
Next we translate the coordinates of the event, manually, into our button's local system (remember that this is an event from the parent container) and check whether this point lies within our polygon. This gives us a boolean result which should indicate whether this component is currently active or inactive. If our button's current activation state (m_active) is not equal to this value, we call the setState() method to change it so that it is. Finally, if this component is active we consume the given MouseEvent to avoid activation of two components simultaneously.
The setState() method is called, as described above, to set a new activation state of this component. It takes a boolean value as parameter and stores it in the m_active instance variable. Then it repaints the component to reflect a change in state, if any:
1. If the m_active flag is set to true, this method checks the class reference to the currently active button stored in the m_currentButton static variable. In the case where this reference still points to some other component (again, it potentially can happen if the mouse cursor moves too quickly out of a components rectangular bounds) we force that component to be inactive. Then we store a this reference into the m_currentButton static variable, letting all other button's know that this button is now the currently active one. We then change the mouse cursor to the hand cursor (as in the previous example) and display our URL in the browser's status bar.
2. If the m_active flag is set to false this method sets the m_currentButton static variable to null, changes mouse cursor to the default cursor, and clears the browser's status bar.
The mouseClicked() method checks whether this component is active (this implies that the mouse cursor is located within our polygon, and not just within the bounding rectangle), the URL is resolved, and the mouse event is not consumed. If all three checks are verifiable, this method redirects the browser to the component's associated URL and consumes the mouse event to avoid processing by any other components.
The rest of methods, implemented due to the MouseListener and MouseMotionListener interfaces, receive empty bodies, except for the mouseExited() and mouseEntered() methods. Both of these methods send all their traffic to the mouseMoved() method to notify the component that the cursor has left or has enetered the container.
The paintComponent() method simply draws the component's Polygon with in gray if inactive, and in red if active.
Note: We've purposefully avoided including tooltip text for these non-rectangular buttons. The reason is that the underlying Swing ToolTipManager essentially relies on the rectangular shape of the components it manages. Somehow, invoking the Swing tooltip API destroys our model of processing mouse events. In order to allow tooltips we have to develop our own version of a tooltip manager--this is the subject of the next example.
Running the Code
To run it in the web browser we have constructed the following HTML file (see Java Plug-in and Java Plug-in HTML converter references in the previous example):
<html>
<head>
<title></title>
</head>
<body>
<OBJECT classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93"
WIDTH = 400 HEIGHT = 380 codebase="http://java.sun.com/products/plugin/1.2/jinstall-12-win32.cab#Version=1,2,0,0">
<PARAM NAME = "CODE" VALUE = "ButtonApplet2.class" >
<PARAM NAME = "type"
VALUE ="application/x-java-applet;version=1.2">
<param name="image" value="bay_area.gif">
<param name="button1"
value="112,122, 159,131, 184,177, 284,148, 288,248, 158,250, 100,152">
<param name="name1" value="Alameda County">
<param name="url1"
value="http://dir.yahoo.com/Regional/U_S__States/
California/Counties_and_Regions/Alameda_County/">
<param name="button2"
value="84,136, 107,177, 76,182, 52,181, 51,150">
<param name="name2" value="San Francisco County">
<param name="url2"
value="http://dir.yahoo.com/Regional/U_S__States/
California/Counties_and_Regions/San_Francisco_County/">
<param name="button3"
value="156,250, 129,267, 142,318, 235,374, 361,376, 360,347, 311,324, 291,250">
<param name="name3" value="Santa Clara County">
<param name="url3"
value="http://dir.yahoo.com/Regional/U_S__States/
California/Counties_and_Regions/Santa_Clara_County/">
<param name="button4"
value="54,187, 111,180, 150,246, 130,265, 143,318, 99,346, 63,314">
<param name="name4" value="San Mateo County">
<param name="url4"
value="http://dir.yahoo.com/Regional/U_S__States/
California/Counties_and_Regions/San_Mateo_County/">
<param name="button5"
value="91,71, 225,79, 275,62, 282,147, 185,174, 160,129, 95,116, 79,97">
<param name="name5" value="Contra Costa County">
<param name="url5"
value="http://dir.yahoo.com/Regional/U_S__States/
California/Counties_and_Regions/Contra_Costa_County/">
<COMMENT>
<EMBED type="application/x-java-applet;version=1.2" CODE = "ButtonApplet2.class"
WIDTH = "400" HEIGHT = "380"
codebase="./"
image="bay_area.gif"
button1="112,122, 159,131, 184,177, 284,148, 288,248, 158,250, 100,152"
name1="Alameda County"
url1="http://dir.yahoo.com/Regional/U_S__States/California/Counties_and_Regions/Alameda_County/"
button2="84,136, 107,177, 76,182, 52,181, 51,150"
name2="San Francisco County"
url2="http://dir.yahoo.com/Regional/U_S__States/California/Counties_and_Regions/San_Francisco_County/"
button3="156,250, 129,267, 142,318, 235,374, 361,376, 360,347, 311,324, 291,250"
name3="Santa Clara County"
url3="http://dir.yahoo.com/Regional/U_S__States/California/Counties_and_Regions/Santa_Clara_County/"
button4="54,187, 111,180, 150,246, 130,265, 143,318, 99,346, 63,314"
name4="San Mateo County"
url4="http://dir.yahoo.com/Regional/U_S__States/California/Counties_and_Regions/San_Mateo_County/"
button5="91,71, 225,79, 275,62, 282,147, 185,174, 160,129, 95,116, 79,97"
name5="Contra Costa County"
url5="http://dir.yahoo.com/Regional/U_S__States/California/Counties_and_Regions/Contra_Costa_County/"
pluginspage="http://java.sun.com/products/plugin/1.2/plugin-install.html">
<NOEMBED></COMMENT>
alt="Your browser understands the <APPLET> tag but isn't running the applet, for some reason."
Your browser is completely ignoring the <APPLET> tag!
</NOEMBED>
</EMBED>
</OBJECT>
</p>
<p> </p>
</body>
</html>
Figure 5.10 shows the ButtonApplet2 example running in Netscape 4.05 with the Java Plug-in. Our HTML file has been constructed to display an active map of the San Francisco bay area. Five non-rectangular buttons correspond to this area's five counties. Note how the non-rectangular buttons react when the mouse cursor moves in and out of their boundaries. Verify that they behave correctly even if a part of a given button lies under the bounding rectangle of another button (a good place to check is the sharp border between Alameda and Contra Costa counties). Click over the button and note the navigation to one of the Yahoo sites containing information about the selected county.
It is clear that tooltip displays would help to dispel any confusion as to which county is which. The next example shows how to implement this feature.



RSS feed Java FAQ News