Creating Custom Components

In this chapter and the previous one, we’ve worked with different user interface objects. We’ve used Swing’s impressive repertoire of components as building blocks and extended their functionality, but we haven’t actually created any new components. In this section, we create an entirely new component from scratch, a dial.

Until now, our examples have been fairly self-contained; they generally know everything about what to do and don’t rely on additional parts to do processing. Our menu example created a DinnerFrame class that had a menu of dinner options, but it included all the processing needed to handle the user’s selections. If we wanted to process the selections differently, we’d have to modify the class. A true component separates the detection of user input from the handling of those choices. It lets the user take some action and then informs other interested parties by emitting events.

Generating Events

Because we want our new classes to be components, they should communicate the way components communicate: by generating event objects and sending those events to listeners. So far, we’ve written a lot of code that listened for events but haven’t seen an example that generated its own custom events.

Generating events sounds like it might be difficult, but it isn’t. You can either create new kinds of events by subclassing java.util.EventObject, or use one of the standard event types. In either case, you just need to allow registration of listeners for your events and provide a means to deliver events to those listeners. Swing’s JComponent class provides a protected member variable called listenerList, which you can use to keep track of event listeners. It’s an instance of EventListenerList; basically it acts like the maître d’ at a restaurant, keeping track of all event listeners, sorted by type.

Often, you won’t need to worry about creating a custom event type. JComponent has methods that support firing of generic PropertyChangeEvents whenever one of a component’s properties changes. The example we’ll look at next uses this infrastructure to fire PropertyChangeEvents whenever a value changes.

A Dial Component

The standard Swing classes don’t have a component that’s similar to an old-fashioned dial—for example, the volume control on your radio. (The JSlider fills this role, of course.) In this section, we implement a Dial class. The dial has a value that can be adjusted by clicking and dragging to “twist” the dial (see Figure 18-11). As the value of the dial changes, DialEvents are fired off by the component. The dial can be used just like any other Java component. We even have a custom DialListener interface that matches the DialEvent class.

The Dial component

Figure 18-11. The Dial component

Here’s the Dial code:

    //file: Dial.java
    import java.awt.*;
    import java.awt.event.*;
    import java.util.*;
    import javax.swing.*;

    public class Dial extends JComponent {
      int minValue, nvalue, maxValue, radius;

      public Dial() { this(0, 100, 0); }

      public Dial(int minValue, int maxValue, int value) {
        setMinimum( minValue );
        setMaximum( maxValue );
        setValue( value );
        setForeground( Color.lightGray );

        addMouseListener(new MouseAdapter() {
          public void mousePressed(MouseEvent e) { spin(e); }
        });
        addMouseMotionListener(new MouseMotionAdapter() {
          public void mouseDragged(MouseEvent e) { spin(e); }
        });
      }

      protected void spin( MouseEvent e ) {
        int y = e.getY();
        int x = e.getX();
        double th = Math.atan((1.0 * y - radius) / (x - radius));
        int value=(int)(th / (2 * Math.PI) * (maxValue - minValue));
        if (x < radius)
          setValue( value + (maxValue-minValue) / 2 + minValue);
        else if (y < radius)
          setValue( value + maxValue );
        else
          setValue( value + minValue);
      }

      public void paintComponent(Graphics g) {
        Graphics2D g2 = (Graphics2D)g;
        int tick = 10;
        radius = Math.min( getSize().width,getSize().height )/2 - tick;
        g2.setPaint( getForeground().darker() );
        g2.drawLine( radius * 2 + tick / 2, radius,
           radius * 2 + tick, radius);
        g2.setStroke( new BasicStroke(2) );
        draw3DCircle( g2, 0, 0, radius, true );
        int knobRadius = radius / 7;
        double th = nvalue * (2 * Math.PI) / (maxValue - minValue);
        int x = (int)(Math.cos(th) * (radius - knobRadius * 3)),
        y = (int)(Math.sin(th) * (radius - knobRadius * 3));
        g2.setStroke(new BasicStroke(1));
        draw3DCircle(g2, x + radius - knobRadius,
           y + radius - knobRadius, knobRadius, false );
      }

      private void draw3DCircle(
          Graphics g, int x, int y, int radius, boolean raised)
      {
        Color foreground = getForeground();
        Color light = foreground.brighter();
        Color dark = foreground.darker();
        g.setColor(foreground);
        g.fillOval(x, y, radius * 2, radius * 2);
        g.setColor(raised ? light : dark);
        g.drawArc(x, y, radius * 2, radius * 2, 45, 180);
        g.setColor(raised ? dark : light);
        g.drawArc(x, y, radius * 2, radius * 2, 225, 180);
      }

      public Dimension getPreferredSize() {
        return new Dimension(100, 100);
      }

      public void setValue( int value ) {
        this.nvalue = value - minValue;
        repaint();
        fireEvent();
      }
      public int getValue()  { return nvalue+minValue; }
      public void setMinimum(int minValue)  { this.minValue = minValue; }
      public int getMinimum()  { return minValue; }
      public void setMaximum(int maxValue)  { this.maxValue = maxValue; }
      public int getMaximum()  { return maxValue; }

      public void addDialListener(DialListener listener) {
        listenerList.add( DialListener.class, listener );
      }
      public void removeDialListener(DialListener listener) {
        listenerList.remove( DialListener.class, listener );
      }

      void fireEvent() {
        Object[] listeners = listenerList.getListenerList();
        for ( int i = 0; i < listeners.length; i += 2 )
          if ( listeners[i] == DialListener.class )
            ((DialListener)listeners[i + 1]).dialAdjusted(
              new DialEvent(this, getValue()) );
      }

      public static void main(String[] args) {
        JFrame frame = new JFrame("Dial v1.0");
        final JLabel statusLabel = new JLabel("Welcome to Dial v1.0");
        final Dial dial = new Dial();
        frame.add(dial, BorderLayout.CENTER);
        frame.add(statusLabel, BorderLayout.SOUTH);

        dial.addDialListener(new DialListener() {
          public void dialAdjusted(DialEvent e) {
            statusLabel.setText("Value is " + e.getValue());
          }
        });

        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setSize( 150, 150 );
        frame.setVisible( true );
      }
    }

Here’s DialEvent, a simple subclass of java.util.EventObject:

    //file: DialEvent.java
    import java.awt.*;

    public class DialEvent extends java.util.EventObject {
        int value;

        DialEvent( Dial source, int value ) {
            super( source );
            this.value = value;
        }

        public int getValue() {
            return value;
        }
    }

Finally, here’s the code for DialListener:

    //file: DialListener.java
    public interface DialListener extends java.util.EventListener {
        void dialAdjusted( DialEvent e );
    }

Let’s start from the top of the Dial class. We’ll focus on the structure and leave you to figure out the trigonometry on your own.

Dial’s main() method demonstrates how to use the dial to build a user interface. It creates a Dial and adds it to a JFrame. Then main() registers a dial listener on the dial. Whenever a DialEvent is received, the value of the dial is examined and displayed in a JLabel at the bottom of the frame window.

The constructor for the Dial class stores the dial’s minimum, maximum, and current values; a default constructor provides a minimum of 0, a maximum of 100, and a current value of 0. The constructor sets the foreground color of the dial and registers listeners for mouse events. If the mouse is pressed or dragged, Dial’s spin() method is called to update the dial’s value. spin() performs some basic trigonometry to figure out what the new value of the dial should be.

paintComponent() and draw3DCircle() do a lot of trigonometry to figure out how to display the dial. draw3DCircle() is a private helper method that draws a circle that appears either raised or depressed; we use this to make the dial look three-dimensional.

The next group of methods provides ways to retrieve or change the dial’s current setting and the minimum and maximum values. The important thing to notice here is the pattern of get and set methods for all of the important values used by the Dial. We will talk more about this in Chapter 22. Also, notice that the setValue() method does two important things: it repaints the component to reflect the new value and fires the DialEvent signifying the change.

The final group of methods in the Dial class provides the plumbing necessary for our event firing. addDialListener() and removeDialListener() take care of maintaining the listener list. Using the listenerList member variable we inherited from JComponent makes this an easy task. The fireEvent() method retrieves the registered listeners for this component. It sends a DialEvent to any registered DialListeners.

Model and View Separation

The Dial example is overly simplified. All Swing components, as we’ve discussed, keep their data model and view separate. In the Dial component, we’ve combined these elements in a single class, which limits its reusability. To have Dial implement the MVC paradigm, we would have developed a dial data model and something called a UI-delegate that handled displaying the component and responding to user events. For a full treatment of this subject, see the JogShuttle example in O’Reilly’s Java Swing.

In Chapter 19, we’ll take what we know about components and containers and put them together using layout managers to create complex GUIs.

Get Learning Java, 4th Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.