Copyright © 2003 Mirasol Op'nWorks inc.
 Eclipse Corner Article

 

Building and delivering a table editor with SWT/JFace

Summary
The JFace API provides several classes that can be used to build editable table views. In this article, we present a fairly extensive example that exercises the JFace and SWT classes needed to implement a table with cell editors for check-boxes, free text and combo-boxes. We also show how to package and deliver the classes into a stand-alone (non-Eclipse) Java application.

By Laurent Gauthier, Mirasol Op'nWorks, lgauthier@opnworks.com
July 3, 2003


Introduction

Users of business software take many things for granted from modern GUI applications. Their reference consists of office automation software such as word processing and spreadsheet applications and they rightly expect other programs to exhibit similar look and feel. They certainly do not want to hear that because the application was written using this or that technology, they should not expect the same behavior from the application as the one they are used to. They do not understand, do not want to understand and should not even have to be confronted with such an issue. The Eclipse team and more specifically, the folks who worked on the SWT and JFace APIs fully understand this and have done a tremendous job in providing Java developers with a toolset that can be used to quickly build robust native applications. In this article, we examine a small part of the SWT/JFace API, namely the constructs that allow us to program a table with editable cells and sortable columns.

The example we use for this article is a task list editor. It is by no means a truly useful application and the GUI design was driven essentially by the need to provide examples of the use of cell editors. The code presented here can be a useful starting point for projects that need the functionality of a table view with editable cells.

Installation

The project and code described herein was designed and implemented as a standalone SWT/JFace application. In other words, it was meant to run outside the Eclipse environment per say although it has dependencies on some core Eclipse APIs. I have wrapped the main class in a plug-in so that it can be conveniently installed into Eclipse. This also shows how one can build an application that is meant to run both as a standalone Java application and as an Eclipse extension. But that could be the subject of another article.

The reader can install the executable and source code by extracting the contents of the TableViewerExamplePlugin.zip file into the plugins directory of an Eclipse 2.1 installation and start (or restart) Eclipse.

Running the example

To run the example, simply open the sample view through the following menu item: Window -> Show View -> Other... -> Examples -> TableViewerExample view. You may also want to import the Java source code into an Eclipse project. To run the example as a standalone Java application inside the Eclipse workbench, simply run the TableViewerExample class.

The Task List Editor

A Task List editor window is shown below. The first column is used to indicate whether the task is completed or not. The other columns and buttons are self explanatory. The user can toggle the "completed" cell, edit in place the "Description" and "% Complete" cells, change the "Owner" cell through the use of a combo box, change the sort order by clicking on a column header and add and delete tasks by clicking on the corresponding button.

The ExampleTask and ExampleTaskList classes

The example uses two simple classes that play the role of the model in our MVC design. The ExampleTask is a simple business object representing a task with getters and setters for the following properties:

     private boolean completed   = false;
     private String description  = "";
     private String owner        = "?";
     private int percentComplete = 0;

The ExampleTaskList is used to hold, as the name implies, a collection of ExampleTask instances. It also knows about the list of possible task owners.

Building the TableViewerExample class

Here we get into the substance of our subject matter: how do we go about building and implementing the view and behavior described above? We do this with a combination of SWT and JFace constructs. SWT provides an interface to the native platform widgetry while JFace provides a high-level abstraction to build rich GUIs. In other words, we could implement the example by using only SWT objects but that would represent much more work and more complicated code.

So lets start by examining our TableViewerExample class. It inherits from Object and the main() method simply creates a new instance, calls the open() method on the window and runs until the user tells the window to close itself.

     /**
      * Main method to launch the window 
      */
     public static void main(String[] args) {
     Shell shell = new Shell();
        shell.setText("Task List - TableViewer Example");

        // Set layout for shell
        GridLayout layout = new GridLayout();
        shell.setLayout(layout);
		
        // Create a composite to hold the children
     Composite composite = new Composite(shell, SWT.NONE);
     TableViewerExample tableViewerExample = new TableViewerExample(composite);

        // Open the shell and run until a close event is detected
        shell.open();
        tableViewerExample.run(shell);
     }

We first create and configure a SWT Shell object. Then, we create a Composite to hold the widgets (table and buttons) and instantiate our class passing the composite to the constructor. We could have done things differently but passing the composite to the constructor makes it easy to wrap our class in an Eclipse view for example.

Adding widgets to the composite is done in the addChildControls() method. The more interesting part of this method is in the following code fragment:

     // Create the table 
  createTable(composite);
	
     // Create and setup the TableViewer
  createTableViewer();
  tableViewer.setContentProvider(new ExampleContentProvider());
     tableViewer.setLabelProvider(new ExampleLabelProvider());
     // The input for the table viewer is the instance of ExampleTaskList
     tableViewer.setInput(taskList);

We first create a org.eclipse.swt.widgets.Table and then create a org.eclipse.jface.viewers.TableViewer on that table. We then set the content provider, label provider and input for the TableViewer. We will come back to these later. Let's first look at the createTable() method. It starts by instantiating a Table object and sets its parent, style and layout attributes. It then creates each of the four columns in turn. As an example, the second column is created with the following fragment:

     // 2nd column with task Description
  column = new TableColumn(table, SWT.LEFT, 1);
     column.setText("Description");
     column.setWidth(400);
     // Add listener to column so tasks are sorted by description when clicked 
  column.addSelectionListener(new SelectionAdapter() {
       	
          public void widgetSelected(SelectionEvent e) {
               tableViewer.setSorter(
                   new ExampleTaskSorter(ExampleTaskSorter.DESCRIPTION));
               }
          });

Here, we create a TableColumn object for the Table, set some of its attributes and add a selection listener that sets the sorter object for the TableViewer object when the column header is clicked. We will look at column sorting later.

The rest of the method is concerned with setting up the other columns using the same approach. The creation of the TableViewer is also of particular interest to us. The method is outlined below (less important code was omitted):

     /**
      * Create the TableViewer 
      */
     private void createTableViewer() {
       tableViewer = new TableViewer(table);    
          tableViewer.setUseHashlookup(true);
       tableViewer.setColumnProperties(columnNames);
     
          // Create the cell editors
       CellEditor[] editors = new CellEditor[columnNames.length];

          // Column 1 : Completed (Checkbox)
       editors[0] = new CheckboxCellEditor(table);

          // Column 2 : Description (Free text)
       TextCellEditor textEditor = new TextCellEditor(table);
          ((Text) textEditor.getControl()).setTextLimit(60);
          editors[1] = textEditor;

          // Column 3 : Owner (Combo Box) 
       editors[2] = new ComboBoxCellEditor(table, taskList.getOwners(),
                                              SWT.READ_ONLY);

          // Column 4 : Percent complete (Text with digits only)
          textEditor = new TextCellEditor(table);
          ((Text) textEditor.getControl()).addVerifyListener(
		
           new VerifyListener() {
                  public void verifyText(VerifyEvent e) {
                      e.doit = "0123456789".indexOf(e.text) >= 0;  
                  }
              });
          editors[3] = textEditor;

          // Assign the cell editors to the viewer 
       tableViewer.setCellEditors(editors);
          // Set the cell modifier for the viewer
          tableViewer.setCellModifier(new ExampleCellModifier(this));
          // Set the default sorter for the viewer 
       tableViewer.setSorter(
             new ExampleTaskSorter(ExampleTaskSorter.DESCRIPTION));
     }


We first construct the TableViewer object, passing along our Table instance since a TableViewer always operates on a table. We then set the column properties that will be used in callbacks to recognize the column on which we will want to operate. We initialize an array of CellEditor objects and we build editors for each column. For the checkbox editor, we do not need to do anything special. For column 2, we set the text length limit of the free text editor while for column 3, we specify the items used in the combo box editor. We obtain these from our ExampleTaskList object. In the case of column 4, we add a validator (a VerifyListener) to makes sure we accept only digits.

We finish setting up our TableViewer by setting it's cellEditors and cellModifier properties and by specifying the default sorter. The ExampleCellModifier is described in detail below.

Specifying Content and Label Providers

Coming back to the TableViewerExample.open() method, we now have a TableViewer instance for which we can set the content and label providers. The content provider is implemented as an inner class of the TableViewerExample class. This allows us to implement both the IStructuredContentProvider and the ITaskListViewer without "polluting", so to speak, our model with JFace dependencies. This class does several things. It handles the inputChanged() and dispose() calls by registering (or deregistering) itself as a change listener for the model. It relays the getElements() request to our model (a TaskList object) and packages the result into an array of Object instances. Finally, it implements the addTask(), removeTask() and updateTasks() callbacks specified by ITaskListViewer. The general principle here is that one should not assume that the model "feeds" a single view. Hence, views should register and deregister their interest in model changes (in our case, task additions, removals and changes).

    /**    
     * InnerClass that acts as a proxy for the ExampleTaskList 
     * providing content for the Table. It implements the ITaskListViewer 
     * interface since it must register changeListeners with the 
     * ExampleTaskList 
     */
    class ExampleContentProvider
	       implements IStructuredContentProvider, ITaskListViewer {
     public void inputChanged(Viewer v, Object oldInput, Object newInput) {
            if (newInput != null)
                ((ExampleTaskList) newInput).addChangeListener(this);
            if (oldInput != null)
                ((ExampleTaskList) oldInput).removeChangeListener(this);
        }

     public void dispose() {
            taskList.removeChangeListener(this);
        }

        // Return the tasks as an array of Objects
     public Object[] getElements(Object parent) {
            return taskList.getTasks().toArray();
        }

        /* (non-Javadoc)
         * @see ITaskListViewer#addTask(ExampleTask)
         */
        public void addTask(ExampleTask task) {
            tableViewer.add(task);
        }

        /* (non-Javadoc)
         * @see ITaskListViewer#removeTask(ExampleTask)
         */
        public void removeTask(ExampleTask task) {
            tableViewer.remove(task);			
        }

        /* (non-Javadoc)
         * @see ITaskListViewer#updateTask(ExampleTask)
         */
        public void updateTask(ExampleTask task) {
            tableViewer.update(task, null);	
        }
    }

For the label provider, we chose to use a top-level class (the ExampleLabelProvider class). An org.eclipse.jface.viewers.ITableLabelProvider must implement two methods: getColumnText() and getColumnImage(). As the names imply, these methods must return the label text and image for a given column of a given element and are implemented using the following code:

 public String getColumnText(Object element, int columnIndex) {
        String result = "";
        ExampleTask task = (ExampleTask) element;
        switch (columnIndex) {
            case 0:  // COMPLETED_COLUMN
                break;
            case 1 :
                result = task.getDescription();
                break;
            case 2 :
                result = task.getOwner();
                break;
            ...code omitted... 	
            }
        return result;
    }	

 public Image getColumnImage(Object element, int columnIndex) {
        return (columnIndex == 0) ?   // COMPLETED_COLUMN?
            getImage(((ExampleTask) element).isCompleted()) : 
            null;
    }

These methods are called by the TableViewer whenever a table element needs to be refreshed. Our ITableLabelProvider extends org.eclipse.jface.viewers.LabelProvider. This way, we benefit from the behavior defined in the superclass. Internally, our class manages an image registry (for the checkboxes) so we had to put in place the mechanics to load and manage instances of org.eclipse.jface.resource.ImageDescriptor and org.eclipse.jface.resource.ImageRegistry. The details of this can be assessed by looking at the source code for the class. In SWT, platform resources such as images and fonts must be disposed of when the application exits. However, an ImageRegistry owns all of the image objects registered with it and automatically disposes of them when the SWT Display is disposed so we do not need to explicitly do so.

Supporting cell editors with an ICellModifier

We now have in place all the parts to display our task list in a table. What we need to add is the componentry to support cell editing. For this, we implement an org.eclipse.jface.viewers.ICellModifier. The later must define the canModify(), getValue() and modify() methods. The first one returns a boolean given an element (a table row) and a column name. The second method returns a cell value (given an element and a column name). Part of its implementation is shown below:

	public Object getValue(Object element, String property) {

        // Find the index of the column
     int columnIndex = tableViewer.getColumnNames().indexOf(property);

        Object result = null;
     ExampleTask task = (ExampleTask) element;

     switch (columnIndex) {
            case 0 : // COMPLETED_COLUMN 
                result = new Boolean(task.isCompleted());
                break;
            case 1 : // DESCRIPTION_COLUMN 
                result = task.getDescription();
                break;
         case 2 : // OWNER_COLUMN 
                String stringValue = task.getOwner();
                String[] choices = tableViewer.getChoices(property);
                int i = choices.length - 1;
                while (!stringValue.equals(choices[i]) && i > 0)
                    --i;
                result = new Integer(i);
                break;
            ...code omitted...
        }
        return result;	
    }

First, we obtain the column index for the property. Second, we cast the contents of element into a ExampleTask object so we can directly address its interface. Finally, we switch to the proper block of code based on the column index. Cases 0 and 1 are self explanatory. In case 2, we must return the index for the combo box. Hence, we match the stringValue to one of the choices and return an Integer representing the current selection.

The modify() method is somewhat symmetrical to the previous one. It is where the actual modification of the ExampleTask takes place.

    public void modify(Object element, String property, Object value) {	

        // Find the index of the column 
        ...code omitted...

        switch (columnIndex) {
         case 0 : // COMPLETED_COLUMN 
                task.setCompleted(((Boolean) value).booleanValue());
				break;
            case 1 : // DESCRIPTION_COLUMN 
                task.setDescription(((String) value).trim());
                break;
            ...code omitted...
            }
     tableViewerExample.getTaskList().taskChanged(task);
    }

As an example, for the "completed" column, we convert the passed value into a boolean and use the resulting value as an argument to the ExampleTask.setCompleted() method. We implement an equivalent behavior for each column. The final line asks the TableViewerExample for its model (an ExampleTaskList) and notifies the model that a given task was changed. The model in turn notifies this change to each of its registered views.

Adding and Deleting Table Items

Editing existing tasks is useful but we also want to add and remove tasks using the GUI. The following code attaches the proper behavior to the "Add" button (very similar code is used for the "Delete" button):

    // Create and configure the "Add" button
 Button add = new Button(parent, SWT.PUSH | SWT.CENTER);
    add.setText("Add");
		
    GridData gridData = new GridData (GridData.HORIZONTAL_ALIGN_BEGINNING);
    gridData.widthHint = 80;
    add.setLayoutData(gridData);
 add.addSelectionListener(new SelectionAdapter() {
       	
        // Add a task to the ExampleTaskList and refresh the view
	    public void widgetSelected(SelectionEvent e) {
            taskList.addTask();
        }
    });

In the above code fragment, we layout the button and specify a selection listener that invokes the model's addTask() method. This later method notifies all its active views that a task was added using the following implementation:

    public void addTask() {
        ExampleTask task = new ExampleTask("New task");
        tasks.add(tasks.size(), task);
        Iterator iterator = changeListeners.iterator();
        while (iterator.hasNext())
            ((ITaskListViewer) iterator.next()).addTask(task);
    }

This pretty much closes the loop for cell and table editing. Through relatively simple implementations of ICellModifier, ITableLabelProvider and IStructuredContentProvider, we have a sophisticated and robust GUI supporting in-place edition of table cells and a viewer that will always reflect the current state of the model.

Supporting column-wise sorting

As hinted above, adding column-wise sorting is almost trivial. To achieve this, we extend the org.eclipse.jface.viewers.ViewerSorter class and override the compare() public method with one that knows how to compare tasks based on its preset criteria. We leave it up to the reader to look up the class implementation. Specifying a sorter for our table viewer is done using the following pattern:

    tableViewer.setSorter(
       new ExampleTaskSorter(ExampleTaskSorter.PERCENT_COMPLETE))

We construct a sorter using one of its exposed criteria and use the result as an argument to the TableViewer.setSorter() method. On selection of a column header, we tell the TableViewer to use a different sorter and table lines get resorted "automagically"!

Packaging the table viewer as a stand-alone application

This table viewer/editor is all very nice but how and where can I make use of it in applications? One answer is, obviously, within an Eclipse plugin. But more than that, as others have described, SWT and JFace can be used to build applications that live outside the Eclipse framework. This is actually very simple: One simply has to put the following files in the same folder: TableViewerExample.jar (contains the application class and image files), swt.jar, jface.jar, runtime.jar and boot.jar. In addition, in a Win32 environment, you will need the swt-win32-2133.dll file which contains the "glue" allowing Java objects to communicate with Windows native widgets and APIs. On other platforms, the Java to native code will be found in a different file so you would need to somehow make sure your Java VM can find it.

On Windows, to launch the TableViewerExample application, you need to execute the following command:

java -classpath TableViewerExample.jar;swt.jar;jface.jar;runtime.jar;boot.jar 
  -Djava.library.path=. com.opnworks.tableviewer.example.TableViewerExample

You would typically but such a command inside a .bat file. You can also achieve a clickable icon result on Windows and on other platforms by using a Java aware packager/installer application, some of which are free to use even for commercial purposes. With such a packaging, users will never know they are using Java technology! Not that there is anything to be shy about, it is just that most users really don't care and why should they?

Wrap-up

With the popularity of the web, it seems people and programmers have been shying away from building GUI applications. In addition, Java programmers and software development managers are often convinced that Java does not cut it on the client and that it cannot be used to build "native" applications. You can convince them of the contrary by rapidly prototyping and packaging a native application using the SWT and JFace APIs.