JavaFX: Reintroduce Swing JTable
Join the DZone community and get the full member experience.
Join For FreeIn this blog entry yours truly will explain how to reintroduce the Swing JTable to a JavaFX application. We will mull over why it this valuable component was removed and extol over its virtue. I will show some of my Gmail Client code.
Why JTable Was Removed?
The JTable was removed from the JavaFX, because the FX GUI team are looking to rewrite UI using it with the Project SceneGraph API. The scenegraph is based on modular structure tree layout of graphical object, whereas the components in Swing and AWT components in Java were built from Java 2D and thus are capable of only rendering immediate-mode graphics.
What is is the advantage of a graph of objects? Well it makes it easy to consider each graphic as a node. Each node can combined together in to group of node objects. For each individual node one apply an affine transformation, where one can translate a node by a distance, rotate a node about angle and shear a node across to a parallelogram. You can apply the same affine transformations to a group of node objects. Realise, then, what you can achieve when you have a group of group of nodes. Scenegraph has all of this and also adds light shading, painting effects and filters and shape clipping on each node (SGNode ).
Another way to think of nodes, is to look at the field of computer aided design (CAD). When automative engineers design car they obviously use graphic elements to represent each part of car. A car has an axle, three or more wheels, an engine and usually a steering wheel. Each component of car can be render in a transformation, colour and details that it requires. However you can then rotate the car completely and see all its of constituent move or change as a whole.
Obviously the JTable, then, will have to be redeveloped for Project Scenegraph
What Are The Positives About JTable?
There are plenty of good points to venerable JTable, which has survived the turmoil of Java UI design. We have lived with the JTable, since probably 1998, when it was revealed in a com.sun.java.swing.JTable name. Even back then, it was a really stunning to be able to drag a table column from one end of the view to another column. It was amazing engineering.
It implements a rather good FLYWEIGHT design pattern (see TableCellRenderer, DefaultTableCellRenderer and TableCellEditor).
It can handle a vast amount of data thrown at it.
The data model resides in a TableModel, which is completely divorced the component UI.
It follows the Model View Controller like every other Swing component.
Adding Swing JTable
Sometimes you just cannot live without these advantages. JTable is great if you want to display rows and columns from a database table. If you have already Swing knowledge then you know already how to use and program it. So why not use it?
You can however get the JTable back inside your JavaFX programs. It is reasonably straight forward to reuse any Swing component inside the JavaFX Preview SDK and Subversion release now. You can wrap any JComponent inside a javafx.ext.swing.ComponentView. Therefore you can add a JTable to a scenegraph based JavaFX user interface program easily. Under the hood, the magic is handled by a SGComponent (from Project SceneGraph).
What I will quickly show in this example is my own version of JTable, based alot on studying the code in the Assortis Demo. The Assortis Demo is part of the Subversion openjfx-compiler project. Here is my XenonTable.
/*
* XenonTable.fx
*
* Created on 22-Jul-2008, 00:31:28
*/
package com.xenonsoft.gmailclient.ui;
/**
* @author Peter Pilgrim
*/
import javafx.ext.swing.Component;
import javax.swing.JTable;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.table.TableModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.System;
import java.lang.Exception;
public class XenonTable extends Component {
private attribute table: JTable;
public attribute selectedRows: Integer[];
public attribute selectedColumns: Integer[];
public attribute onRowLeadSelection: function( row: Integer, column: Integer ): Void;
public attribute onColumnLeadSelection: function( row: Integer, column: Integer ): Void;
public attribute rowSelectionAllowed: Boolean = true on replace {
if ( table != null ) {
table.setRowSelectionAllowed( rowSelectionAllowed );
}
};
public attribute columnSelectionAllowed: Boolean = false on replace {
if ( table != null ) {
table.setColumnSelectionAllowed( columnSelectionAllowed );
}
};
public attribute tableDataModel: XenonTableModel on replace {
if ( tableDataModel!= null ){
table.setModel(tableDataModel);
}
};
override function createJComponent(): JComponent {
table = new JTable();
table.setFillsViewportHeight(true);
if ( tableDataModel != null ) {
table.setModel(tableDataModel);
}
var rowListener = ListSelectionListener {
override function valueChanged(e:ListSelectionEvent): Void {
if (e.getValueIsAdjusting()) {
return;
}
System.out.print("ROW SELECTION EVENT ");
debugSelection();
// Compiler bug convert int[] to Integer[]?
var sRows:Integer[] = table.getSelectedRows();
selectedRows = sRows;
fireOnRowLeadSelection( e );
}
};
table.getSelectionModel().addListSelectionListener(rowListener);
var columnListener = ListSelectionListener {
override function valueChanged(e:ListSelectionEvent): Void {
if (e.getValueIsAdjusting()) {
return;
}
System.out.print("COLUMN SELECTION EVENT ");
debugSelection();
// Compiler bug convert int[] to Integer[]?
var sColumns = table.getSelectedColumns();
selectedColumns = sColumns;
fireOnColumnLeadSelection( e );
}
};
table.getColumnModel().getSelectionModel().addListSelectionListener(columnListener);
var scrollPane = new JScrollPane(table);
return scrollPane;
// return table;
}
protected function fireOnRowLeadSelection( e: ListSelectionEvent ): Void {
var row:Integer = table.getSelectionModel().getLeadSelectionIndex();
var column:Integer = table.getColumnModel().getSelectionModel().getLeadSelectionIndex();
if (this.onRowLeadSelection != null ) {
this.onRowLeadSelection( row, column );
}
}
protected function fireOnColumnLeadSelection( e: ListSelectionEvent ): Void {
var row:Integer = table.getSelectionModel().getLeadSelectionIndex();
var column:Integer = table.getColumnModel().getSelectionModel().getLeadSelectionIndex();
if (this.onColumnLeadSelection != null ) {
this.onColumnLeadSelection( row, column );
}
}
protected function debugSelection(): Void {
System.out.print("Lead: {%d table.getSelectionModel().getLeadSelectionIndex()} ");
System.out.println("{%d table.getColumnModel().getSelectionModel().getLeadSelectionIndex()} ");
System.out.print("Selected Rows: ");
var selectedRows: Integer[] = table.getSelectedRows();
for ( r in selectedRows ) {
System.out.print("{%d r} ")
}
System.out.println();
System.out.print("Selected Columns: ");
var selectedColumns: Integer[] = table.getSelectedColumns();
for ( c in selectedColumns ) {
System.out.print("{%d c} ")
}
System.out.println();
}
}
Although it looks complicated the above code is really straight forward. In order to create the component in JavaFX, one subclasses from a special node called Component and then one implements the function createJComponent(). The createJComponent creates a JTable and return its wrapped in a JScrollPane.
In order to make the FX table more useful, one needs to listen to certain events, when the user clicks on the table. This is what the ListSelectionListener subclasses do. JavaFX makes it easy to subclass any existing Java interface or class. In the recent FX compiler you need to use override function keywords to override superclass methods and attributes. Let us view the table model class
The Table Data Model
The table model is a subclass of the AbstractTableModel from the Java Swing library.
/*
* XenonTableModel.fx
*
* Created on 22-Jul-2008, 01:32:27
*/
package com.xenonsoft.gmailclient.ui;
import javax.swing.JTable;
import javax.swing.table.TableModel;
import javax.swing.table.AbstractTableModel;
import java.lang.Class;
import java.lang.Object;
import java.lang.System;
/**
* @author Peter Pilgrim
*/
/*
* JTable uses this method to determine the default renderer/
* editor for each cell. If we didn't implement this method,
* then the last column would contain text ("true"/"false"),
* rather than a check box.
*/
var xlass: Class = null;
public class XenonTableModel extends AbstractTableModel {
public attribute DEBUG: Boolean = false;
public attribute columnNames: String[];
public attribute data: String[];
public function getColumnCount(): Integer {
return sizeof columnNames;
}
public function getRowCount(): Integer {
return ((sizeof data) / (sizeof columnNames)) as Integer;
}
override function getColumnName(col:Integer): String {
return columnNames[col as Integer];
}
public function getValueAt(row:Integer, col:Integer): Object {
if (DEBUG) {
System.out.println("getValue({row},{col}) = {data[getIndex(row,col)]}")
}
return (data[getIndex(row,col)]);
}
protected function getIndex( row:Integer, col:Integer): Integer {
var colSize:Integer = sizeof columnNames;
return ( row * colSize + col) as Integer;
}
override function getColumnClass(c:Integer):Class {
if ( xlass == null ) {
xlass = Class.forName("java.lang.String");
}
return xlass; // getValueAt(0, c).getClass();
}
/*
* Don't need to implement this method unless your table's
* editable.
*/
override function isCellEditable(row:Integer, col:Integer): Boolean {
return false;
}
/*
* Don't need to implement this method unless your table's
* data can change.
*/
override function setValueAt(value:Object, row:Integer, col:Integer): Void {
if (DEBUG) {
System.out.println("Setting value at {row},{col} to {value} (an instance of {value.getClass()})");
}
data[getIndex(row,col)] = value.toString();
fireTableCellUpdated(row, col);
if (DEBUG) {
System.out.println("New value of data:");
printDebugData();
}
}
public function printDebugData(): Void {
var numRows: Integer = getRowCount() as Integer;
var numCols: Integer = getColumnCount() as Integer;
for (i in [0..numRows-1]) {
System.out.print(" row {i}:");
for (j in [0..numCols-1]) {
System.out.print(" {data[getIndex(i,j)]}");
}
System.out.println();
}
System.out.println("--------------------------");
}
}
The quick reader will be aware that I am using the indexing function to map the row and column coordinate to a linear array. See my other article : JavaFX A Workaround For Sequences of Sequences.
The reader should be aware that the static keyword is being removed from compiled JavaFX. Note how the script level variable xlass is used in the above XenonTableModel. This is now the correct way to declare a global module variable that replaces static. Actually the xlass variable captures the java.lang.String.class value. This table model, then, only handles Strings for now. This is ok, because JavaFX has great string substitution that permits conversion of an object to String.
In the setValue() method you still have to call the super class fireTableCellUpdated() method as normal.
Listeners
As I showed above, the list selection listeners perform the client of the XenonTable to actually use the table.The listeners are protected and they simply update the sequences in the table component, namely rowSelected, columnsSelected. They also execute the user defined onLeadRowSelection and onLeadColumnSelection functions, if there are used. The logic handled by JavaFX triggers.
The Demo Class
Now that is enough talk, here is the example program.
/*
* XenonTableDemo.fx
*
* Created on 22-Jul-2008, 02:35:07
*/
package com.xenonsoft.gmailclient;
/**
* @author Peter
*/
import javafx.ext.swing.*;
import com.xenonsoft.gmailclient.ui.XenonTable;
import com.xenonsoft.gmailclient.ui.XenonTableModel;
import java.lang.System;
class PersonModel {
public attribute firstName: String;
public attribute lastName: String;
public attribute activity: String;
public attribute years: Integer;
public attribute vegetarian: Boolean;
}
var persons:PersonModel[] = [
PersonModel{firstName:"Mary", lastName:"Campione",
activity:"Snowboarding", years: 5, vegetarian: false},
PersonModel{firstName:"Alison", lastName:"Huml",
activity:"Rowing", years:3, vegetarian: true},
PersonModel{firstName:"Kathy", lastName:"Walrath",
activity:"Knitting", years:2, vegetarian:false},
PersonModel{firstName:"Sharon", lastName:"Zakhour",
activity:"Speed reading", years:20, vegetarian:true},
PersonModel{firstName:"Philip", lastName:"Milne",
activity:"Pool", years:10, vegetarian:false},
];
SwingFrame {
width: 400;
height: 400;
visible: true;
title: "XenonTableDemo"
content:
XenonTable {
onRowLeadSelection: function ( row:Integer, column: Integer): Void {
System.out.println("*YAHOO* handler {row} {column}");
}
onColumnLeadSelection: function ( row:Integer, column: Integer): Void {
System.out.println("*YIPPEE* handler {row} {column}");
}
columnSelectionAllowed: false
rowSelectionAllowed: true
tableDataModel:
XenonTableModel {
DEBUG: true;
columnNames: [
"First Name",
"Last Name",
"Activity",
"# of Years",
"Vegetarian?"
]
data: bind for ( person in persons ) {
[
person.firstName,
person.lastName,
person.activity,
"{person.years}",
"{person.vegetarian}"
]
}
}
}
}
The Way To Go From Here
This article showed how to reapply to the JTable to JavaFX, if you are in desperate need for it. The code does not show to handle individual table cell rendering or how to bind more of functionality from the JTable. However, it is perfectly acceptable to show hundreds of rows of text information from a database table or a wicked and wild extensive JSON query.
One way to extend the table would be to add the ability to render the background, foreground and colours for each individual table cell. It is a relative simple exercise for the reader to add a cell renderer to XenonTable. (So this extension, if you require it, is on you)
First, the cell renderer in FX.
public class XenonTableCellRenderer extends DefaultTableCellRenderer {
public attribute xenonTable: XenonTable;
//...
override function getTableCellRendererComponent(
table: JTable, value: Object,
isSelected: Boolean, hasFocus: Boolean,
row:Integer, column:Integer): Component
{
var text:String = value as String;
var tableModel:XenonTableModel = xenonTable.tableDataModel;
var cell: XenonTableCell = tableModel.getTableCell(row,column);
if ( cell.font != null ) {
this.setFont( cell.font.getAWTFont());
}
if ( cell.foreground != null ) {
//...
}
if ( cell.background != null ) {
//....
}
this.setText( cell.text );
return this;
}
}
You need register the FX cell renderer with the JTable directly or each TableColumn.
Second code snippet, you need to start using a cell object to group together a bunch of attributes, namely: text, colours and font.
public class XenonTableModel extends AbstractTableModel {
public attribute DEBUG: Boolean = false;
public attribute columnNames: String[];
public attribute data: XenonTableCell[] on replace {
for ( cell in data ) {
cell.tableModel = this;
}
};
/* .... as before */
}
The third snippet of code. This is the partial code for the table cell FX object. The trigger need to be filled in:
public class XenonTableCell {
public attribute foreground: Color = null on replace oldValue {
if ( foreground != oldValue ) {
fireUpdate();
}
};
public attribute background: Color = null on replace oldValue {
/* ditto */
};
public attribute font: Font = null on replace oldValue {
/* ditto */
};
public attribute text: String = null on replace oldValue {
/* ditto */
};
// Private implementation
protected attribute row: Integer;
protected attribute col: Integer;
protected attribute tableModel: XenonTableModel = null;
protected function fireUpdate() {
if ( tableModel != null ) {
tableModel.fireTableCellUpdated(row, col);
}
}
}
Each cell takes a reference to the table model so if any of the values of attribute changed we can update the table UI by firing the appropriate event. You also have to figure how to record the row and column with each table cell before you can fire the event.
Finally you can write something like this main program:
import javafx.scene.paint.*;
import javafx.scene.text.*;
// ...
SwingFrame {
title: "XenonTableDemo"
/* ... as before .. */
content:
XenonTable {
// ...
tableDataModel:
XenonTableModel {
DEBUG: false;
// ...
data: bind for ( person in persons ) {
[
XenonTableCell {
font: Font {
name: "Arial",
style: FontStyle.BOLD
}
text: bind person.firstName
},
XenonTableCell {
foreground: Color.BLUE
text: bind person.lastName
},
XenonTableCell {
text: bind person.activity
},
XenonTableCell {
text: bind "{person.years}"
},
XenonTableCell {
text: bind "{person.vegetarian}"
}
]
}
}
}
}
Have fun! Now you can display those database rows again.
Opinions expressed by DZone contributors are their own.
Comments