Creating an Experimental
GUI
By Patrick Phelan
If you like to experiment with GUIs, Swing's lightweight classes
are are just the ticket. Because Because Swing's lightweight classes
don't have peers, your class extensions won't fight OS-specific
implementations of the same classes. Also, Swing's pluggable look-and-feel
architecture makes it easy for you make changes in other people's
implementations. Also, Swing's pluggable look-and-feel architecture
makes it easy for you make changes in other people's code.
The
Double-Headed Scroll Bar
Consider the double-headed scroll bar shown on the left. It has
all the same components of a normal scroll bar, plus two more: a
pair of extra arrows that can make it easier to position the mouse.
This is a perfect example of a customization for a PLAF, because
only the look has changed.
The code that creates the double-headed scroll bar does not worry
about mouse clicks or events. It only makes two more up and down
buttons, positions them, and calculates the thumb for the reduced
space. Where there originally was a reference to an incrbutton,
there is now an incrbutton2:
incrButton.setBounds
(itemX, incrButtonY, itemW, incrButtonH);
decrButton2.setBounds
(itemX, incrButtonY-decrButtonH, itemW, decrButtonH);
incrButton2.setBounds
(itemX, decrButtonY+decrButtonH, itemW, incrButtonH);
The important part is shrinking the track to fit in the extra buttons:
/*
Update the trackRect field.
*/
int itrackY = decrButtonY + decrButtonH + incrButtonH;
int itrackH = incrButtonY + decrButtonH - itrackY;
trackRect.setBounds(itemX, itrackY, itemW, itrackH);\
/* If the thumb isn't going to fit, zero it's bounds. Otherwise
* make sure it fits between the buttons. Note that setting the
* thumbs bounds will cause a repaint.
*/
if(thumbH >= (int)trackH) {
setThumbBounds(0, 0, 0, 0);
} else {
if ((thumbY + thumbH) > incrButtonY) {
thumbY = incrButtonY - incrButtonH - thumbH;
}
if (thumbY < (decrButtonY + decrButtonH)) {
thumbY = decrButtonY + decrButtonH+ decrButtonH + 1;
}
setThumbBounds(itemX, thumbY, itemW, thumbH);
}
The biggest benefit in all this is that the change is completely
transparent to the programmer. In the past, I accomplished the same
thing using a modified Lightscrollbar,
which acted the same as awt.Scrollbar
but was not interchangeable. A plugged swing.JScrollBar
is always a JScrollBar, no matter what it actually ends up doing.
This brings us to what I consider a minor problem with with PLAF
customizations. If you wanted to make a particular alteration available
to a number of different PLAFs, you would probably customize the
original BasicUI element. Then, when you wanted to customize the
actual UI element, you would have to rewrite the element. After
all,
Metal2ScrollBarUI extends BasicDoubledScrollBarUI
means that
Metal2ScrollBarUI
cannot extend
MetalScrollBarUI
It also means that the number of such customizations should be
kept to a minimum, because you can only make one change to each
UI element -- and even that requires writing your own initClassDefaults.
So you would would wind up with a Metal2ScrollBarUI
and a MetalRolloverCheckBoxUI,
and so on.
Downloading the Code
The complete code for my double-headed scroll bar is available
in a zip file. Just click the download button:
As a final touch, you might want to add this snippet to start of
your code:
try {
javax.swing.LookAndFeel m2 = new Metal2LookAndFeel();
javax.swing.UIManager.setLookAndFeel(m2);
} catch (Exception exc) {
System.err.println("Error loading L&F: " + exc);
}
Popup Buttons and Popup Menus
Now suppose you have discovered some other useful customization
such as, a NiftyCheckBoxUI.
In this case, you must replace your MetalRolloverCheckBoxUI.
An alternative technique might be to install another CheckBoxUI,
but the only way I can think of to do that would be to install one
on top of another, by somehow copying (Class.defineClass)
the old class to a new name, and compiling the new class to extend
the old class by the new name, hoping all the while that they won't
fight.
To illustrate, consider the Mailpuccino
program described in another article in this section. When you run
it, it displays this window:
Note the big icons on the left. They are unusual, yet obvious.
You can't miss seeing them, and the absence of a menu, along with
the rollover effect, forces you to try clicking them eventually.
The only gripe I have is that there is nothing to signify that some
of the buttons are popup menus and other dialogs, while others are
functions.
In my own experiments, I have resolved this problem with something
I call a pPopup button:
My very own menu-ing system includes the concept of a "popper,"
a component signifying that a menu lives there. Admittedly, I do
not know how to add an image to the popper, so the green blob image
in my popper is the button that pops up the menu. Here's a screen
shot of the menu from my
Web site:
The pPopup Menu
In a series of ads for tape drive that that IBM once ran, the company
pointed out that because the drive's normal parking position was
halfway through the tape, the distance for any point on the tape
could be no more than half the length of the tape.
Well, I can do that one better. Here's a pPopup menu on which no
item can be more than one quarter the distance away from the center
position of the mouse:
Alternately, you can use some of the four available slots to store
the same things all the time.
To illustrate, consider the URL address box at the top of Internet
Explorer window. When you click on a line of text, the contextual
menu lets you type in a URL, but removes everything else.
If you have used the address box and the back button on the IE
browser millions of times, you get used to its rhythm of click,
slide, click. Suddenly removing it or putting something else there
would not be a nice thing to do. But the addition a pPopup menu
could make life easier with no loss of IE's familiar rhythm. The
new rhythm at the bottom right would always be back, forward, and
so on, while also allowing for easy access to the text/image/link/applet/files
context menus.
I can't say that the quad-style menu is the best way to do a menu.
I can say that it solves some problems. It may be more confusing,
it may often include huge amounts of empty space, and it currently
has problems with submenus, but at least I made an effort.
Coding the pPopup Menu
The code for pPopup is almost boring. It makes a GridBag location
10,100 the center. (The ability to define your own center is an
often neglected feature of GridBag. Everyone assumes that it has
to start at 0. To add the upper half, it counts from 100 down. To
add the lower half, it adds from 101 up. The left side uses an X
of 0.)
In the pPopup menu, the maximum width of the MenuItems on the left
and the maximum height for the top are used to offset the x
and y in show().
This leaves the cursor at the top left of the bottom right set of
MenuItems. pPopup
also won't go off the edge of the screen. It checks the width and
height against the screen bounds. If there are no left or top MenuItems,
then the pPopup looks just like a normal popup menu.
public void addnw(Vector nwv){ //Northwest
if(nwv == null) return;
setPopmode(QUAD);
Enumeration en = nwv.elements();
try {
while(true) {
JMenuItem menuItem = (JMenuItem)en.nextElement();
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridx = 10; //Put it on the right
gbc.gridy = nw++; //And on the next level down
add(menuItem, gbc);
nwem = Math.max(menuItem.getPreferredSize().width, nwem);
nwm += menuItem.getPreferredSize().height;
}
} catch(NoSuchElementException e) {}
}
And so on for the other three directions.
public void show(Component invoker, int x, int y) {
setInvoker(invoker);
try {
Frame newFrame = getFrame(invoker);
if (newFrame != super.frame) {
// Use the invoker's frame so that events
// are propogated properly
if (newFrame!=null) {
super.frame = newFrame;
if(popup != null) {
setVisible(false);
}
}
}
} catch(Exception e) {
}
Point invokerOrigin = invoker.getLocationOnScreen();
//Move the show point so that the cursor is at 0,0 on the compass.
//I like the cursor to be just inset from the edge
if(mode != SINGLE){
invokerOrigin.x -= Math.max(sem, nwem) + 10;
}
if(mode == QUAD){
invokerOrigin.y -= Math.max(nwm, nem)+ 5;
}
setLocation(invokerOrigin.x + x,
invokerOrigin.y + y);
setVisible(true);
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
if(invokerOrigin.x + x < 10){
invokerOrigin.x = 10;
}
if(invokerOrigin.y + y < 10){
invokerOrigin.y = 10;
}
if(invokerOrigin.x + x + getWidth() > screenSize.width){
invokerOrigin.x = screenSize.width - getWidth() - x;
}
if(invokerOrigin.y + y + getHeight() > screenSize.height){
invokerOrigin.y = screenSize.height - getHeight() - y;
}
}
Downloading the Code
To view or download the complete pPopup code, click the download
button:
You don't have to use pPopup as a traditional popup. Here's a variation:
Vector v1 = new Vector();
v1.addElement(new JMenuItem("A"));
v1.addElement(new JMenuItem("B"));
v1.addElement(new JMenuItem("C"));
v1.addElement(new JMenuItem("D"));
pPopup pp;
pp = new pPopup(v1, null, null, null);
pp.addActionListener(this);
_____
Patrick Phelan works as a programmer at SmartChoice Technologies
in Hoboken, NJ, and has a really neat Web
page.
|