JSlider Appearance Improvements

by John Zukowsi

The JSlider component is a popular component for selecting a value from a numerical range. While some people might think of a JScrollBar for numerical input, that really serves the purpose of scrolling around a viewport, not data input. By default, the input range of a JSlider is 0 to 100, with an initial value of 50. You can change any of the three defaults, the direction of the scrollbar, and whether tick marks should appear and what to show next to them. You'll look at all of these possible features.

First off is the basic JSlider, just creating it and adding it to a screen, preserving all the defaults. Nothing fancy here, but builds the basics for what to build on for the remaining examples. The SliderSample program actually has four sliders; two horizontal and two vertical.

import javax.swing.\*;
import java.awt.\*;

public class SliderSample {
  public static void main(final String args[]) {
    Runnable runner = new Runnable() {
      public void run() {
        JFrame frame = new JFrame("Sample Sliders");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JSlider js1 = new JSlider();
        JSlider js2 = new JSlider();
        js2.setInverted(true);
        JSlider js3 = new JSlider(JSlider.VERTICAL);
        js3.setPaintLabels(true);
        JSlider js4 = new JSlider(JSlider.VERTICAL);
        js4.setInverted(true);
        frame.add(js1, BorderLayout.NORTH);
        frame.add(js2, BorderLayout.SOUTH);
        frame.add(js3, BorderLayout.WEST);
        frame.add(js4, BorderLayout.EAST);
        frame.setSize(300, 200);
        frame.setVisible(true);
      }
    };
    EventQueue.invokeLater(runner);
  }
}

Compiling and running the program shows a screen with four sliders on it. While you cannot tell from the screen, the ones in the EAST and SOUTH regions of the BorderLayout are actually inverted. So, for the horizontal sliders, 0 is on the left for the top one and 100 is on the left for the inverted bottom one. For the vertical sliders, 0 is on the bottom for the left one and on the top for the inverted right one.

By default, there is no indicator on the slider for what its value is. Instead, you would just ask the slider its value at some later time, or attach a listener to the control to be told the value upon movement. Ignoring the import lines, here is a listener that reports the value when the user stops dragging the slider knob. The getValueIsAdjusting() method reports true while the user still has the knob selected with the mouse.

 public class SliderChangeListener implements ChangeListener {
    public void stateChanged(ChangeEvent changeEvent) { 
      Object source = changeEvent.getSource();
      JSlider theJSlider = (JSlider)source;
      if (!theJSlider.getValueIsAdjusting()) { 
        System.out.println ("Slider changed: " + theJSlider.getValue());
      } 
    } 
  } 

Not showing a value on the slider makes it somewhat useless, though you can certainly work with it where the exact value doesn't matter and the user can see results based upon relative position. To help in making the slider more useful, you can show tick marks with labels. There are two classes of tick marks, major and minor ones. Major and minor are just classifications for the length and positioning of the lines drawn for the tick mark, where major tick marks are longer than minor ones. To enable tick marks, you have to call setPaintTicks(true), and, say how far apart the tick marks should appear. By default, the spacing is zero, resulting in no tick marks drawn, even when paint ticks is true. Adding the appropriate lines to the earlier program will show tick marks on the top and left sliders, with major ticks every 10 and minor ones at the midpoint between them.

  
  js1.setPaintTicks(true);
  js1.setMajorTickSpacing(10);
  js1.setMinorTickSpacing(5);

  js3.setPaintTicks(true);
  js3.setMajorTickSpacing(10);
  js3.setMinorTickSpacing(5); 

Showing tick marks certainly helps, but showing labels makes things most useful. Adding setPaintLabels(true) calls will show labels at the major tick marks. By default, the labels are their respective cardinal numbers, so with a major tick mark at value 0, the associated label is string "0," and so on for "10,", "20,", all the way to "100."

Instead of showing the cardinal numbers, you can provide a label table: setLabelTable(Dictionary labels). In the table, you would map integer values to different components to show as the label. In generic-speak, that would be Dictionary<Integer, JComponent>, though the method isn't explicitly defined to require the key-value pair to be such. Typically, the mapping is Integer object to JLabel. As the Swing component set predates the Collections framework, the method takes a Dictionary, not a Map, so remember to use a Hashtable for the label table. Here's one label table setup that shows text roughly every eleven positions. At the ends are colored diamond icons, instead of text, to help show that that are labels not text strings that can go at each position.

   Hashtable<Integer, JComponent> table =
      new Hashtable<Integer, JComponent>();
    table.put(new Integer(0), new JLabel(
      new DiamondIcon(Color.RED)));
    table.put(new Integer(11), new JLabel("Eleven"));
    table.put(new Integer(22), new JLabel("Twenty-Two"));
    table.put(new Integer(33), new JLabel("Thirty-Three"));
    table.put(new Integer(44), new JLabel("Fourty-Four"));
    table.put(new Integer(55), new JLabel("Fifty-Five"));
    table.put(new Integer(66), new JLabel("Sixty-Six"));
    table.put(new Integer(77), new JLabel("Seventy-Seven"));
    table.put(new Integer(88), new JLabel("Eighty-Eight"));
    table.put(new Integer(100), new JLabel(
      new DiamondIcon(Color.BLACK)));
     js4.setLabelTable(table);
     js4.setPaintLabels(true); 

The definition for the DiamondIcon will be shown at end with the full class definition.

Notice that the slider in the eastern area of the screen was used for the label table. Had the southern slider been used instead, the label text would overlap. Keep that in mind when you work with slider labels.

One last thing you can do to affect the display. You can hide the track that the knob slides on. Just call the setPaintTrack() method, with a setting of false. Here, the bottom slider has its track hidden.

There isn't that much more you can do to customize the look of the JSlider, but as you've seen there is certainly quite a bit you can do. From adding tick marks and labels to hiding the track, you've seen the most significant features. If you want to get really fancy, consider creating a custom UI for the component and show it as a dial instead of a line. Sounds like an idea for a future tip.

Here's the final and complete source used to generate the samples. You will need to work backwards to remove code sections to return back to the first screen shot.

import javax.swing.\*;
import javax.swing.event.\*;
import java.awt.\*;
import java.util.Hashtable;

public class SliderSample {
  public static void main(final String args[]) {
    Runnable runner = new Runnable() {
      public void run() {
        JFrame frame = new JFrame("Sample Sliders");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ChangeListener listener = new SliderChangeListener();
        JSlider js1 = new JSlider();
        js1.setPaintLabels(true);
        js1.setPaintTicks(true);
        js1.setMajorTickSpacing(10);
        js1.setMinorTickSpacing(5);
        js1.addChangeListener(listener);
        JSlider js2 = new JSlider();
        js2.setInverted(true);
	js2.setPaintTrack(false);
        js2.addChangeListener(listener);
        JSlider js3 = new JSlider(JSlider.VERTICAL);
        js3.setPaintLabels(true);
        js3.setPaintTicks(true);
        js3.setMajorTickSpacing(10);
        js3.setMinorTickSpacing(5);
        js3.addChangeListener(listener);
        JSlider js4 = new JSlider(JSlider.VERTICAL);
        js4.setInverted(true);
        Hashtable<Integer, JComponent> table =
          new Hashtable<Integer, JComponent>();
        table.put(new Integer(0), new JLabel(
          new DiamondIcon(Color.RED)));
        table.put(new Integer(11), new JLabel("Eleven"));
        table.put(new Integer(22), new JLabel("Twenty-Two"));
        table.put(new Integer(33), new JLabel("Thirty-Three"));
        table.put(new Integer(44), new JLabel("Fourty-Four"));
        table.put(new Integer(55), new JLabel("Fifty-Five"));
        table.put(new Integer(66), new JLabel("Sixty-Six"));
        table.put(new Integer(77), new JLabel("Seventy-Seven"));
        table.put(new Integer(88), new JLabel("Eighty-Eight"));
        table.put(new Integer(100), new JLabel(
          new DiamondIcon(Color.BLACK)));
        js4.setLabelTable(table);
        js4.setPaintLabels(true);
        js4.addChangeListener(listener);
        frame.add(js1, BorderLayout.NORTH);
        frame.add(js2, BorderLayout.SOUTH);
        frame.add(js3, BorderLayout.WEST);
        frame.add(js4, BorderLayout.EAST);
        frame.setSize(400, 300);
        frame.setVisible(true);
      }
    };
    EventQueue.invokeLater(runner);
  }
  public static class SliderChangeListener implements ChangeListener {
    public void stateChanged(ChangeEvent changeEvent) { 
      Object source = changeEvent.getSource();
      JSlider theJSlider = (JSlider)source;
      if (!theJSlider.getValueIsAdjusting()) { 
        System.out.println ("Slider changed: " + theJSlider.getValue());
      } 
    } 
  }
  public static class DiamondIcon implements Icon {
    private Color color;
    private boolean selected;
    private int width;
    private int height;
    private Polygon poly;
    private static final int DEFAULT_WIDTH = 10;
    private static final int DEFAULT_HEIGHT = 10;

    public DiamondIcon(Color color) {
      this(color, true, DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }

    public DiamondIcon(Color color, boolean selected) {
      this(color, selected, DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }

    public DiamondIcon(Color color, boolean selected,
        int width, int height) {
      this.color = color;
      this.selected = selected;
      this.width = width;
      this.height = height;
      initPolygon();
    }

    private void initPolygon() {
      poly = new Polygon();
      int halfWidth = width / 2;
      int halfHeight = height / 2;
      poly.addPoint(0, halfHeight);
      poly.addPoint(halfWidth, 0);
      poly.addPoint(width, halfHeight);
      poly.addPoint(halfWidth, height);
    }

    public int getIconHeight() {
      return height;
    }

    public int getIconWidth() {
      return width;
    }

    public void paintIcon(Component c, Graphics g, int x, int y) {
      g.setColor(color);
      g.translate(x, y);
      if (selected) {
        g.fillPolygon(poly);
      } else {
        g.drawPolygon(poly);
      }
      g.translate(-x, -y);
    }
  }
}

Comments:

You can adjust the behaviour of JSliders too. One thing that always bothered me with JSliders is how once setSnapToTicks(true) is passed to a JSlider, it only snaps once the drag is complete, while the mouse button is held down the slider "thumb" will freely move instead of snapping to ticks. I wrote an article a few months back about how this can be fixed for any Look and Feel without having to change application code, save for one line of initialisation. See here:

http://www.javaspecialists.eu/archive/Issue148.html

Posted by M Kneebone on April 04, 2008 at 08:55 AM PDT #

Good point. I've often used the JSlider as well to supplement and extend various other controls:
http://www.brunata.dk/ptd/3/backup/analyse.gif

Posted by Casper on April 06, 2008 at 05:56 PM PDT #

Very useful indeed! Thanks for your efforts.

Posted by S.K.Dogra on April 06, 2008 at 11:23 PM PDT #

Post a Comment:
Comments are closed for this entry.
About

John O'Conner

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today