Building and delivering a table editor with SWT/JFace

来源:百度文库 编辑:神马文学网 时间:2024/04/29 07:05:58
Copyright © 2003Mirasol 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 tableviews. In this article, we present a fairly extensive example that exercisesthe JFace and SWT classes needed to implement a table with cell editors forcheck-boxes, free text and combo-boxes. We also show how to package and deliverthe classes into a stand-alone (non-Eclipse) Java application.
By Laurent Gauthier, Mirasol Op‘nWorks,lgauthier@opnworks.comJuly 3, 2003
Introduction
Users of business software take many things for granted from modern GUIapplications. Their reference consists of office automation software such asword processing and spreadsheet applications and they rightly expect otherprograms to exhibit similar look and feel. They certainly do not want to hearthat because the application was written using this or that technology, theyshould not expect the same behavior from the application as the one they areused to. They do not understand, do not want to understand and should not evenhave to be confronted with such an issue. The Eclipse team and morespecifically, the folks who worked on the SWT and JFace APIs fully understandthis and have done a tremendous job in providing Java developers with a toolsetthat 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 allowus 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 meansa truly useful application and the GUI design was driven essentially by the needto provide examples of the use of cell editors. The code presented here can be auseful starting point for projects that need the functionality of a table viewwith editable cells.
Installation
The project and code described herein was designed and implemented as a standaloneSWT/JFace application. In other words, it was meant to run outside the Eclipseenvironment 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 installedinto Eclipse. This also shows how one can build an application that is meantto run both as a standalone Java application and as an Eclipse extension. Butthat could be the subject of another article.
The reader can install theexecutable and source code by extracting the contents of theTableViewerExamplePlugin.zipfile 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 -> ShowView -> Other... -> Examples -> TableViewerExample view. You may alsowant to import the Java source code into an Eclipse project. To run the exampleas a standalone Java application inside the Eclipse workbench, simply run the TableViewerExampleclass making sure you specify the following VM runtime variable:
-Djava.library.path=ECLIPSE_HOME\plugins\org.eclipse.swt.win32_2.1.0\os\win32\x86
where ECLIPSE_HOME is your eclipse home installation folder (the above is forEclipse v2.1 on Win32, for other versions and other platforms, the path and filename will be different).
The Task List Editor
A Task List editor window is shown below. The first column is used toindicate whether the task is completed or not. The other columns and buttons areself explanatory. The user can toggle the "completed" cell, edit inplace the "Description" and "% Complete" cells, change the"Owner" cell through the use of a combo box, change the sort order byclicking on a column header and add and delete tasks by clicking on thecorresponding button.

The ExampleTask and ExampleTaskList classes
The example uses two simple classes that play the role of the model in ourMVC design. The ExampleTask is a simple business objectrepresenting 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, acollection of ExampleTask instances. It also knows about the listof possible task owners.
Building the TableViewerExample class
Here we get into the substance of our subject matter: how do we go aboutbuilding and implementing the view and behavior described above? We do this witha combination of SWT and JFace constructs. SWT provides an interface to thenative platform widgetry while JFace provides a high-level abstraction to buildrich GUIs. In other words, we could implement the example by using only SWTobjects but that would represent much more work and more complicated code.
So lets start by examining our TableViewerExample class. Itinherits from Object and the main() method simplycreates a new instance, calls the open() method on the window andruns 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 shellGridLayout layout = new GridLayout();shell.setLayout(layout);// Create a composite to hold the childrenComposite composite = new Composite(shell, SWT.NONE);TableViewerExample tableViewerExample = new TableViewerExample(composite);// Open the shell and run until a close event is detectedshell.open();tableViewerExample.run(shell);}
We firstcreate and configure a SWT Shell object. Then,we create a Composite to hold the widgets (table and buttons) andinstantiate our class passing the composite to the constructor. We could havedone things differently but passing the composite to the constructor makes iteasy 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 tablecreateTable(composite);// Create and setup the TableViewercreateTableViewer();tableViewer.setContentProvider(new ExampleContentProvider());tableViewer.setLabelProvider(new ExampleLabelProvider());// The input for the table viewer is the instance of ExampleTaskListtableViewer.setInput(taskList);
We firstcreate a org.eclipse.swt.widgets.Table and thencreate a org.eclipse.jface.viewers.TableViewer on that table. Wethenset 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 itsparent, style and layout attributes. It then creates each of the four columns inturn. As an example, the second column is created with the following fragment:
// 2nd column with task Descriptioncolumn = new TableColumn(table, SWT.LEFT, 1);column.setText("Description");column.setWidth(400);// Add listener to column so tasks are sorted by description when clickedcolumn.addSelectionListener(new SelectionAdapter() {public void widgetSelected(SelectionEvent e) {tableViewer.setSorter(new ExampleTaskSorter(ExampleTaskSorter.DESCRIPTION));}});
Here, wecreate a TableColumn object for the Table, set some ofits attributes andadd a selection listener that sets the sorter object for the TableViewerobject 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 usingthe same approach. The creation of the TableViewer is also ofparticular interest to us. The method is outlined below (less important code wasomitted):
/*** Create the TableViewer*/private void createTableViewer() {tableViewer = new TableViewer(table);tableViewer.setUseHashlookup(true);tableViewer.setColumnProperties(columnNames);// Create the cell editorsCellEditor[] 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 viewertableViewer.setCellEditors(editors);// Set the cell modifier for the viewertableViewer.setCellModifier(new ExampleCellModifier(this));// Set the default sorter for the viewertableViewer.setSorter(new ExampleTaskSorter(ExampleTaskSorter.DESCRIPTION));}
Wefirstconstruct the TableViewer object, passing along our Tableinstance since a TableViewer always operates on a table. We thenset the column properties that will be used in callbacks to recognize the columnon which we will want to operate. Weinitialize an array of CellEditor objects and webuild editors for each column. For the checkbox editor, we do not need to doanything special. For column 2, we set the text length limit of the free texteditor while for column 3, we specify the items used in the combo box editor.We obtain these from our ExampleTaskList object. In the case ofcolumn 4, weadd a validator (a VerifyListener) to makes sure we accept onlydigits.
We finish setting up our TableViewer bysetting it‘s cellEditors and cellModifier properties and byspecifying the default sorter. The ExampleCellModifier is describedin detail below.
Specifying Content and Label Providers
Coming back to the TableViewerExample.open() method, we now havea TableViewer instance for which we can set the content and labelproviders. The content provider is implemented as an inner class of the TableViewerExampleclass. This allows us to implement both the IStructuredContentProviderand the ITaskListViewer without "polluting", so to speak,our model with JFace dependencies. This class does several things. It handlestheinputChanged() anddispose() calls by registering (or deregistering) itself as a changelistener for the model. Itrelays the getElements() request to our model (a TaskListobject) and packages the result into an array of Object instances.Finally, it implements theaddTask(),removeTask() andupdateTasks() callbacks specified by ITaskListViewer. Thegeneral principle here is that one should not assume that the model"feeds" a single view. Hence, views should register and deregistertheir interest in model changes (in our case, task additions, removals andchanges).
/*** 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 ExampleContentProviderimplements 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 Objectspublic 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 ExampleLabelProviderclass). An org.eclipse.jface.viewers.ITableLabelProvider mustimplement two methods:getColumnText() andgetColumnImage(). As the names imply, these methods must return the labeltext and image for a given column of a given element and are implemented usingthe following code:
public String getColumnText(Object element, int columnIndex) {String result = "";ExampleTask task = (ExampleTask) element;switch (columnIndex) {case 0: // COMPLETED_COLUMNbreak;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 elementneeds 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 inplace the mechanics to load and manage instances of org.eclipse.jface.resource.ImageDescriptorand org.eclipse.jface.resource.ImageRegistry. The details of thiscan be assessed by looking at the source code for the class. In SWT, platformresources such as images and fonts must be disposed of when the applicationexits. However, an ImageRegistry owns all of the image objectsregistered with it and automatically disposes of them when the SWT Display isdisposed 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. Whatwe need to add is the componentry to support cell editing. For this, weimplement an org.eclipse.jface.viewers.ICellModifier. The latermust define the canModify(), getValue() and modify()methods. The first one returns a boolean given an element (a tablerow) and a column name. The second method returns a cell value (given an elementand a column name). Part of its implementation is shown below:
public Object getValue(Object element, String property) {// Find the index of the columnint columnIndex = tableViewer.getColumnNames().indexOf(property);Object result = null;ExampleTask task = (ExampleTask) element;switch (columnIndex) {case 0 : // COMPLETED_COLUMNresult = new Boolean(task.isCompleted());break;case 1 : // DESCRIPTION_COLUMNresult = task.getDescription();break;case 2 : // OWNER_COLUMNString 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 ExampleTaskobject 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. Incase 2, we must return the index for the combo box. Hence, we match the stringValueto one of the choices and return an Integer representing thecurrent 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_COLUMNtask.setCompleted(((Boolean) value).booleanValue());break;case 1 : // DESCRIPTION_COLUMNtask.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 booleanand use the resulting value as an argument to the ExampleTask.setCompleted()method. We implement an equivalent behavior for each column. Thefinal line asks the TableViewerExample for its model (an ExampleTaskList)and notifies the model that a given task was changed. The model in turn notifiesthis 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 tasksusing 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" buttonButton 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 viewpublic void widgetSelected(SelectionEvent e) {taskList.addTask();}});
In the above code fragment, welayout the button andspecify a selection listener that invokes the model‘s addTask()method. This later method notifies all its active views that a task was addedusing 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. Throughrelatively simple implementations of ICellModifier, ITableLabelProviderand IStructuredContentProvider, we have a sophisticated and robustGUI supporting in-place edition of table cells and a viewer that will alwaysreflect the current state of the model.
Supporting column-wise sorting
As hinted above, adding column-wise sorting is almost trivial. To achievethis, we extend the org.eclipse.jface.viewers.ViewerSorter classand override the compare() public method with one that knows how tocompare tasks based on its preset criteria. We leave it up to the reader to lookup the class implementation. Specifying a sorter for our table viewer is doneusing the following pattern:
tableViewer.setSorter(new ExampleTaskSorter(ExampleTaskSorter.PERCENT_COMPLETE))
We construct a sorter using one of its exposed criteria and use the result asan argument to the TableViewer.setSorter() method. On selection ofa column header, we tell the TableViewer to use a different sorterand 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 ofit in applications? One answer is, obviously, within an Eclipse plugin. But morethan that, as others have described, SWT and JFace can be used to buildapplications that live outside the Eclipse framework. This is actually verysimple: 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, youwill need the swt-win32-2133.dll file which contains the "glue"allowing Java objects to communicate with Windows native widgets and APIs. Onother platforms, the Java to native code will be found in a different file soyou would need to somehow make sure your Java VM can find it.
On Windows, to launchthe 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 alsoachieve a clickable icon result on Windows and on other platforms by using aJava aware packager/installer application, some of which are free to use evenfor commercial purposes. With such a packaging, users will never know they areusing Java technology! Not that there is anything to be shy about, it is justthat most users really don‘t care and why should they?
Wrap-up
With the popularity of the web, it seems people and programmers have beenshying away from building GUI applications. In addition, Java programmers andsoftware development managers are often convinced that Java does not cut it onthe client and that it cannot be used to build "native" applications.You can convince them of the contrary by rapidly prototyping and packaging anative application using the SWT and JFace APIs.