Skip to main content

How to create custom popup menus

Posted by kirillcool on March 12, 2005 at 8:55 AM PST

The popup menus in Swing application are rather standard. Put a few JMenuItems (most probably with icons), spice them up with an assorted selection of JSeparators, and your plain boring popup is ready for the outside world to see.

The immediate solution to creating custom menu items is, of course, using some lively look'n'feel scheme. However, you don't have to go that far to bring your popup menu alive. There are two things you can do - use HTML text and create custom background image. The following example illustrates both techniques.

First, here is a sample popup menu:

menu.jpg



The zoomed-in version shows some interesting details on the underlying visual artifacts:

menuBig.jpg

  • The text of the header entry is painted twice (black shadow makes the white text stand out).
  • The text of regular entries is HTML and anti-aliased.
  • The gradient on the header entry and the overlaid squares will be discussed below.

The first and the simpler thing to do - make menu items with HTML text. Simply provide valid HTML construct to the constructor (or setText() function) of JMenuItem:

JMenuItem myItem = new JMenuItem("<html><b>Increase</b> font size</html>");

The more interesting part is providing custom rendering for your menu items. The obvious approach is to extend the JMenuItem class and override the paintComponent(Graphics g) function:

@Override
protected final void paintComponent(Graphics g) {
   Graphics2D graphics = (Graphics2D) g;
   graphics.setColor(Color.red);
   graphics.fillRect(0,0,this.getWidth(), this.getHeight());
   super.paintComponent(graphics);
}

Of course, the fillRect function can be replaced by drawImage for painting your background image before the super implementation is called. However, this approach fails when the corresponding menu item is selected - the background is filled with grey color (or the color specified in UIDefaults / L&F specific setting). In this case, you may completely override the painting function, using HTMLEditorKit, HTMLDocument and HTML.Attribute classes to correctly display both the background image and the HTML text. Of course, special attention has to be paid to icons and accelerator strings. The code given below goes halfway in this direction - it draws only the background image and simple (non-HTML) text.

Let's call our class HeaderMenuItem:

public final class HeaderMenuItem extends JMenuItem 

The constructor is straightforward, making this item disabled:

   
public HeaderMenuItem(String text) {
   super(text);
   this.setEnabled(false);
}

The main functionality lies in the paintComponent overriden function. It sets anti-aliasing hint on the graphics context, paints the background image (with the corresponding dimension) and paints the text with shadow:

@Override
protected final void paintComponent(Graphics g) {
   Graphics2D graphics = (Graphics2D) g;
   graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
   // paint background image
   graphics.drawImage(ImageCreator.getGradientCubesImage(
        this.getWidth(), this.getHeight(),
        ImageCreator.mainMidColor,
        ImageCreator.mainUltraDarkColor,
        (int) (0.7 * this.getWidth()),
(int) (0.9 * this.getWidth())), 0, 0, null);
   graphics.setFont(HEADER_FONT);
   // place the text slightly to the left of the center
   int x = (this.getWidth() - graphics.getFontMetrics().stringWidth(
this.getText())) / 4;
   int y = (int)(graphics.getFontMetrics().getLineMetrics(
this.getText(), graphics).getHeight());

   // paint the text with black shadow
   graphics.setColor(Color.black);
   graphics.drawString(this.getText(), x+1, y+1);
   graphics.setColor(Color.white);
   graphics.drawString(this.getText(), x, y);
}

Where the default font is

private static final Font HEADER_FONT = new Font("Arial", Font.BOLD, 14);

And the ImageCreator class defines color constants

public static final Color mainMidColor = new Color(0, 64, 196);
public static final Color mainUltraDarkColor = new Color(0, 0, 64);

and the getGradientCubesImage function. This function is pretty straightforward:

public static BufferedImage getGradientCubesImage(int width, 
   int height, Color leftColor, Color rightColor,
   int transitionStart, int transitionEnd) {

First it fills the image with background. All pixels to the left of the transitionStart column are painted with leftColor, all pixels to the right of the transitionEnd column are painted with rightColor, and the pixels in the transition area are filled with the gradient:

BufferedImage image = new BufferedImage(width, height,
   BufferedImage.TYPE_INT_ARGB);

Graphics2D graphics = (Graphics2D) image.getGraphics();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
   RenderingHints.VALUE_ANTIALIAS_ON);

GradientPaint gradient = new GradientPaint(transitionStart, 0,
   leftColor, transitionEnd, 0, rightColor);
graphics.setPaint(gradient);
graphics.fillRect(transitionStart, 0,
   transitionEnd - transitionStart, height);

graphics.setColor(leftColor);
graphics.fillRect(0, 0, transitionStart, height);

graphics.setColor(rightColor);
graphics.fillRect(transitionEnd, 0, width - transitionEnd, height);

Now, the transition area is covered with squares of the predefined size. We compute how many rows and columns of squares we have to paint in order to completely cover the transition area:

public static final int CUBE_DIMENSION = 5;

The computation itself:

int cubeCountY = height / ImageCreator.CUBE_DIMENSION;
int cubeCountX = 1 + (transitionEnd - transitionStart)
  / ImageCreator.CUBE_DIMENSION;
int cubeStartY = (height % ImageCreator.CUBE_DIMENSION) / 2;
int cubeStartX = transitionStart
   - (ImageCreator.CUBE_DIMENSION -
   ((transitionEnd - transitionStart) % ImageCreator.CUBE_DIMENSION));

Now, we iterate over cube rows and columns. A random decision is made - if random number is less than 0.5, no cube is drawn at that location:

for (int col = 0; col < cubeCountX; col++) {
   for (int row = 0; row < cubeCountY; row++) {
     // decide if we should put a cube
     if (Math.random() < 0.5)
        continue;

Now, a semi-random color is chosen. The base color is interpolated according to the current X position in the transition area. This color is then perturbed using a random number:

// Make semi-random choice of color. It should lie
// close to the interpolated color, but still appear
// random
double coef = 1.0 - (((double) col / (double) cubeCountX) +
  0.9 * (Math.random() - 0.5));
coef = Math.max(0.0, coef);
coef = Math.min(1.0, coef);

The interpolated color is computed:

// Compute RGB components
int r = (int) (coef * leftColor.getRed() + (1.0 - coef)
   * rightColor.getRed());
int g = (int) (coef * leftColor.getGreen() + (1.0 - coef)
   * rightColor.getGreen());
int b = (int) (coef * leftColor.getBlue() + (1.0 - coef)
   * rightColor.getBlue());

And the corresponding square is filled:

// fill cube
graphics.setColor(new Color(r, g, b));
graphics.fillRect(cubeStartX + col * ImageCreator.CUBE_DIMENSION,
   cubeStartY + row * ImageCreator.CUBE_DIMENSION,
   ImageCreator.CUBE_DIMENSION,
   ImageCreator.CUBE_DIMENSION);

The last thing - the border of the square is painted with slightly brighter color (10% closer to the pure white):

// draw cube's border in slightly brighter color
graphics.setColor(new Color(
   255 - (int) (0.95 * (255 - r)),
   255 - (int) (0.9 * (255 - g)),
   255 - (int) (0.9 * (255 - b))));
graphics.drawRect(cubeStartX + col * ImageCreator.CUBE_DIMENSION,
   cubeStartY + row * ImageCreator.CUBE_DIMENSION,
   ImageCreator.CUBE_DIMENSION,
   ImageCreator.CUBE_DIMENSION);

The last thing you can do with your regular menu item - draw it in anti-aliased text. All you have to do is extend the JMenuItem class and override the paintComponent function:

@Override
protected final void paintComponent(Graphics g) {
   Graphics2D graphics = (Graphics2D) g;
   Object oldHint = graphics.getRenderingHint(
      RenderingHints.KEY_TEXT_ANTIALIASING);
   graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
      RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
   super.paintComponent(graphics);
   graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
      oldHint);
}

Don't forget to restore the state of the graphics context.

Related Topics >>