Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

How Can I Extend the Swing JCombobox?


Jan01: Java Q&A

Jason is the principal of CyberNostics Pty Ltd. He can be contacted at jasonw@ cybernostics.com.au.


The lightweight controls that come with Sun's Java Foundation Classes Swing framework provide a rich set of interfaces for extending the way they look or behave. In this article, I will extend the JComboBox control using listener interfaces and a custom data model to make it a little smarter and less prone to error.

I'm in the final stages of writing a personal golf-statistics database application in Java. The software lets golfers record details of their exploits to glean something useful from the data. They can enter records for each round at a given golf course, allowing trends to be spotted, and hopefully, handicaps to be lowered.

You enter your score in a tabular view based on the JTable. To save typing, I used JComboBoxes to present the list of courses/players in the system within cells, which contain the name of the course played or the player involved.

The Personal Golf Assistant (the name of the application) uses two types of comboboxes in its main data-entry window: a read-only JComboBox for selecting the relevant golf course, and an editable one for selecting players. Figure 1 shows a JComboBox used to select the player for a given score. This JComboBox will be the focus of my discussion.

I wanted to improve the behavior of the JComboBox to minimize the amount of typing required and to reduce entry errors. This led me to compose the following list of requirements for extended JComboBox functionality:

  • The list items shall be sorted alphabetically.
  • Any text entered in the edit box should trigger a search through the list, selecting the first matching item. For example, with a list comprised of "Jason, Jeremy, John," users should only have to type "je" before Jeremy is selected.

  • The current selection should be updated with every new character entered into the listbox.

  • If any text is automatically inserted into the edit box, it should be selected in such a way that subsequent keystrokes will replace it with the typed text.

  • When the Enter key (or equivalent) is pressed with a text entry not in the list, some action should be taken to either add the item or reject it.

I decided to partition these requirements into logical groups, around which I based my class design. I arrived at the following three elements to fulfill the requirements I had set down: sorting the JComboBox items, autocompletion of typed text, and managing list item creation. I'll deal with each of these separately as I build the ComboBoxAutoCompleteMgr utility class. This class ties these design elements together for ease of use.

Sorting JComboBox Items

The first goal in achieving the new auto-complete behavior is to ensure that the list items of the JComboBox are sorted, so that autocomplete behavior won't result in random jumps around the list. In Windows, you can click a checkbox in a resource editor to indicate that the items should be sorted. There is no equivalent property in the JComboBox, but it does have an extensive framework to enable extending its behavior.

If you were working with JDK 1.0 and its AWT, such a change would have required you to subclass the component, and place the new behavior within the relevant event-response function. This is not the case with Swing components, due to their Model/View/Controller (MVC) architecture. Instead, you can delegate the responsibility for various aspects of the new behavior to objects of your choosing.

MVC is a design pattern for component management, not unlike the Document/ View architecture in Microsoft's MFC library, which splits the management of application document data from MDI views that render that data. MVC takes this concept one step further by applying a similar pattern to all of its components. The components in Swing have all been designed to separate the underlying data (which Sun refers to as the "model") from the object that both renders it and accepts input to modify it (called the "view/ controller" or "delegate").

It is possible to use Swing components without ever knowing or caring what a model is, because each control can use a default data model that can handle basic component operations such as adding or removing data items. After calling one of the default constructors for a component, an instance of the default data model of that control is created. You can then call the appropriate get/set/add functions to manipulate the control's data items, which are handled by this default model. However, if your needs extend beyond a control's default behavior, the MVC pattern allows the tailoring of almost all aspects of component performance. This is particularly useful with JTree and JTable controls, where the data might need to be sourced from a database or cached from a remote object using RMI and custom data models. In my case, I needed to create a new JComboBox data model to ensure that the JComboBox's items are sorted.

Listing One shows the definition of the SortedComboBoxModel class, derived from the DefaultComboBoxModel class. I chose to subclass the DefaultComboBoxModel because it already handles all the functionality related to listener management and data storage. All I needed to do was bolt on the requisite sorting behavior.

The only challenge with subclassing the default model was that the list item Vector in the base class was not directly accessible because it has "package protected" or "friendly" access (for anyone interested in Java 2 Certification nomenclature or if you are a pedant). You are therefore confined to using the public and protected functionality of the DefaultComboBoxModel. This doesn't prove to be too much of a problem with judicious use of the existing API.

As Listing One shows, the SortedComboBoxModel constructors all mimic the constructors available for the DefaultComboBoxModel base class. I did this to make it as easy as possible to drop this class into a context where a DefaultComboBoxModel was already being used. The sorting behavior is implemented in two places:

  • The constructor, to ensure that the list is sorted initially.
  • The add() function, to ensure the list remains sorted as new items are added to the model.

With this in mind I went to the JDK documentation in search of a sort routine.

JDK 1.2 Sorting and The Collections Framework

Before Java 1.2, sorting was left to the whim of individual programmers. There were no stock algorithms incorporated in the class library (à la STL), nor any convention for establishing how to order individual objects within collection objects. These features have been included with the JDK 1.2 class library:

  • A new Collections framework (java.util.Col- lections): A more consistent set of collection classes.
  • Common algorithms: Functions such as sorting and searching have been collected into a set of static functions in the java.util.Arrays class.

  • Object ordering: Two interfaces, java.util.Comparable and java.util.Comparator, for custom ordering of items in sorted lists by specifying object-comparison functions either within the class of the objects being compared or in another class used as a comparator.

I decided to make the assumption that the objects in the sorted list would implement Comparable. If this is not possible for you, then you'll have to extend the SortedComboBoxModel class to use a custom object implementing the Comparator interface. To perform the actual sorting, I had hoped to use one of the static binary search functions in the java.util.Arrays class, but none of them conformed to the pattern of usage in my class. (They all take Collection objects of various descriptions, none, however, matching a DefaultComboBoxModel.)

Instead, I ended up venturing into the source code for the java.util.Arrays class, and I adapted its binary search algorithm for use in my overridden add function.

The method I used to enforce a sorted order in the new class was to override the add() function. The DefaultComboBoxModel add() function simply appends the new object to the end of the list. My overridden version performs a binary search to always insert the object in the correct position in the list (otherwise known as an "insert sort").

Listing Two shows how to use the SortedComboBoxModel to create a sorted JComboBox. After creating the model, you add some random String items. String implements Comparable, which is used to determine the ordering. If you forget to implement Comparable in your items to be sorted, your application throws a ClassCastException for every compare. Having populated the model, I'll now use it to create and show a JComboBox with its items sorted.

Implementing the Autocomplete Functionality

Now that you have a JComboBox with sorted items, the next step is to add the ability to hop through the list items as new text is typed in its text editor component.

For example, assume a Player record called "Jeremy" exists in the list, but I want to add a new record for Jerome. If I type "J," I want the control to suggest "eremy," but in a way that means I'm not wrestling with it if I don't want to select Jeremy. (I don't think I've ever met anyone who had the autocorrect facility enabled in wordprocessors for this very reason.) The way to achieve this effect is to select any automatically inserted text so that any subsequent keystrokes will replace the suggestion. So when "Jer" is entered in the edit box, you want "emy" automatically suggested as a completion, but you want it selected, so it will be replaced if the user goes on to type "o," "m," and "e;" see Figure 2.

Here are the steps to do this whenever a new character is entered in the editable text field of the JComboBox:

1. Search the list for an item that matches the text typed so far.

2. If the list item is longer, then append the remainder text in the text field.

3. Select the item in the control's listbox.

4. Select the new text so any new keystrokes will erase it. Only added text should trigger a search. (If the Delete key is pressed, you shouldn't keep resuggesting text that has just been deleted.)

There are several stages in the event-handling process where our code could be inserted to perform this task. My first impulse was to create a Listener to respond to keyPressed events from the JTextField used to capture the edited text of the JComboBox. I then realized that a more appropriate object to Listen to would be the Model of the JTextField. After searching in vain for a JTextFieldModel or JTextComponentModel, I found that the class was in fact called the Document class. The Document class provides functions for both updating the contents of the text Document and listening for Document update events. To respond to these changes in a Document class, you need to register a DocumentListener object. The DocumentListener interface specifies functions for responding to events where data is removed from, added to, or changed within the text Document object.

Listing Three shows the definition for the ComboBoxAutoCompleteMgr class, which registers an anonymous instance of the DocumentListener interface for the JComboBox specified as an argument to that constructor. The only response function with a nonnull body is the insertUpdate() function. This function calls FindAutoCompleteText() to update the selected item to match the contents of the Document. The function is not called directly, but via a call to SwingUtilities.invokeLater(). I had initially called FindAutoCompleteText directly, but found the test application (available electronically; see "Resource Center," page 5) threw a ConcurrentModificationException. This is thrown to prevent any illegal updates to an Object, such as when you try to modify a Document object in response function to a Document update. I felt this was reasonable in a world where finite stack space is the overarching reality.

As an alternative, I decided to move the autocomplete function call to a separate thread, so its execution would be blocked until the event-response function had completed. The FindAutoCompleteText function implements the main autoselection. A search that starts with the text contained in the edit control is made for the item. If a match is found, then any remaining text is appended to the text in the combobox's JTextField. When the edit text matches exactly, no further action is required. If new text has been inserted, a reference to the Caret object for the edit text is obtained from the JComboBox object ManagedCB. This object is then used to select text (from the end of the text to the current position for sensible behavior when a new character is typed).

Processing Items Not From the List

The final issue to be resolved is what to do once a user has typed text that doesn't match any existing item. Should a new item be added? Is any validation of the item required?

As there is no way to hard code this behavior, I decided to create an interface that lets this behavior be delegated to specialized objects that will know the answers to the aforementioned questions.

The ItemCreator handles new item creation when its createItemFor() function is called. The event that will result in this function being called is an ActionEvent sent from the JTextField. This occurs when users press the Enter key (or equivalent for their hardware).

To register interest in this event, you can use an anonymous Listener class with an actionPerformed() function that calls createItemFor() on a previously registered ItemCreator instance. Figure 3 shows the sequence of events for item creation. In the CBTest.java example, I created a simple anonymous instance of an ItemCreator, but you could make the processing as involved and complex as you like.

For example, your version of the item creator could perform a SQL query or call functions on a Remote object to determine if the item should be added. In the example, I simply display a JOptionPane dialog to confirm the creation of the new object. Typical use of the ComboBoxAutoCompleteMgr class is available electronically. It ties the three aspects of list sorting, item selection, and new item creation into a few lines of code.

Conclusion

The components, component listeners, and data models that comprise the Swing framework may at first appear overcomplicated. However, once you get over the initial learning curve, you can reap great rewards when it comes to flexibility in extending a wide range of behaviors. It also encourages better design by breaking up a problem into several manageable tasks, rather than trying to tackle it within one monolithic class. If only my golf swing was as easy to improve...

DDJ

Listing One

package cybernostics.framework.dialogs;
import javax.swing.DefaultComboBoxModel;
import java.util.Arrays;
import java.util.Vector;
import java.util.Iterator;
public class SortedComboBoxModel extends DefaultComboBoxModel
{
    public SortedComboBoxModel()
    {
        super();
    }
    public SortedComboBoxModel(final Object items[])
    {
        super();
        int i,c;
        for ( i=0,c=items.length;i<c;i++ )
        {    addElement(items[i]);
        }
    }
    public SortedComboBoxModel(Vector v)
    {
        for(Iterator it = v.iterator(); it.hasNext();)
        { addElement( it.next() );
        }
    }
    // Binary search before insert - assumes sorted array
    public void addElement(Object anObject)
    {
        // Similar algorithm to Arrays.BinarySearch()
        int low = 0;
        int high = getSize()-1;
        int iPlaceToInsert = -1;
        
        while (low <= high)
        {
            Object midVal = getElementAt(mid);
            int cmp = ((Comparable)anObject).compareTo(midVal);
            if (cmp < 0)
            {   high = mid - 1;
            }
            else
            {   if (cmp > 0)
                {   low = mid + 1;
                }
                else  // equal
                {   iPlaceToInsert = mid;
                    break;
                }
            }
        }
        if(iPlaceToInsert == -1)
        {   iPlaceToInsert = low;
        }
        if(iPlaceToInsert >= getSize())
        {   super.addElement(anObject); // just add to end
        }
        else
        {   insertElementAt(anObject,iPlaceToInsert);
        }
    }
}

Back to Article

Listing Two

import javax.swing.*;
import javax.swing.event.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import cybernostics.framework.dialogs.*;
public class Example1  
{
    public static void main( String[] args )
    {
        // Create new test app frame
        JFrame f = new JFrame("SortedComboBoxModel Test");
        f.setSize(400,300);
        f.addWindowListener(new WindowAdapter()
        {   public void windowClosing(WindowEvent e)
            {   System.exit(0);
            } });
        // Create a combobox (with a sorted model) and add some items //
        JComboBox jcb = new JComboBox( new SortedComboBoxModel() );
        jcb.addItem("Jerome");
        jcb.addItem("Zelda");
        jcb.addItem("Abram");
        jcb.addItem("Keith");
        jcb.addItem("Jeremy");
        jcb.addItem("Abraham");

        JPanel jp = new JPanel();     // create a frame
        f.getContentPane().add(jp);   // add the panel to it
        jp.add(jcb);                  // add the combobox to the panel
        f.pack();                     // adjust size using layouts
        f.setVisible(true);           // Now show the frame
    }
} 

Back to Article

Listing Three

package cybernostics.framework.dialogs;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Arrays;
//_________________________________________________________
public class ComboBoxAutoCompleteMgr implements ActionListener
//_________________________________________________________
{
    //_________________________________________________________
    // 'package protected' access members
    //_________________________________________________________

     /** Combobox controlled by this class */
     JComboBox ManagedCB;
     ItemCreator creator;
    /** @param jcb   ComboBox to manage. This constructor ensures that the
    *                   combobox is editable. */
    //________________________________________________________________________
    public ComboBoxAutoCompleteMgr(JComboBox jcb)
    //________________________________________________________________________
    {
        ManagedCB = jcb;
        ManagedCB.setEditable(true);
        // Get a pointer to the combobox's editor component. We're assuming it
        // is a JTextField.
        JTextField jtf = (JTextField)ManagedCB.getEditor().getEditorComponent();
        jtf.addActionListener(this);
        // Register a listener to repond to changes in the combobox's editor
        // We're only interested in additions
       jtf.getDocument().addDocumentListener( new DocumentListener()
        {
            public void changedUpdate(DocumentEvent ev){}   // Nil respose
            public void removeUpdate(DocumentEvent ev){}    // Nil respose
            public void insertUpdate(DocumentEvent ev)
            {
                // Kick off the following thread after this response thread has 		    //finished
                // Otherwise we get an exception trying to update the list model
                // within a response function.
                SwingUtilities.invokeLater(new Runnable()
                {
                    public void run()
                    {    FindAutoCompleteText(((JTextComponent)ManagedCB.getEditor().getEditorComponent()).getText());
                    }
                });
            }

        });
    }
    //________________________________________________________________________
    public void setItemCreator( ItemCreator ic )
    //________________________________________________________________________
    {
        creator = ic;
    }
    //________________________________________________________________________
    public void actionPerformed(ActionEvent ae)
    //________________________________________________________________________
    {   JTextField jtf = (JTextField)ManagedCB.getEditor().getEditorComponent();
        String s = jtf.getText();
        jtf.getCaret().setDot(jtf.getText().length());
        for( int i = 0; i < ManagedCB.getItemCount(); i++ )
        {   Object o = ManagedCB.getItemAt( i );
            String sTemp = o.toString();
            if(sTemp.equals(s))
            {   return;
            }
        }
        System.out.println(ae);     
        if( creator != null )
        {   creator.createItemFor( s, (DefaultComboBoxModel)ManagedCB.getModel() );
        }
    }
   //________________________________________________________________________
    public void FindAutoCompleteText(String s)
    //________________________________________________________________________
    {   for( int i = 0; i < ManagedCB.getItemCount(); i++ )
        {   Object o = ManagedCB.getItemAt( i );
            String sTemp = o.toString();
            // Don't do anything if the text exactly matches...
            if(sTemp.equals(s))
            {   return;
            }
            if( sTemp.startsWith(s) )
            {
                ManagedCB.setSelectedIndex(i);
                JTextComponent jtc = (JTextComponent)ManagedCB.getEditor().getEditorComponent();
                // Insert the suggested text
                jtc.setText(sTemp);
                // Select the inserted text from the end to the current
                // edit position.
                Caret c = jtc.getCaret();
                c.setDot(sTemp.length());
                c.moveDot(s.length());
                break;
            }
        }
    }
    public static ComboBoxAutoCompleteMgr createAutoSelectComboBox()
    {
        JComboBox jcb = new JComboBox(new SortedComboBoxModel());
        ComboBoxAutoCompleteMgr acm = new ComboBoxAutoCompleteMgr(jcb);
        return acm;
    }
}

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.