Java Forum / General / October 2005
resize JTable
mmellinger66@gmail.com - 26 Sep 2005 23:27 GMT I'm trying to dynamically resize the number of rows in my JTable object. Basically, I'm reading data from a database table and I'm allowing people to filter to return a subset of the data. I could set the size to the approximate maximum, but when the user filters, there may only be 5 results out of 1000 returned. The scrollbar will be too small. In addition, when I try to print() with the 1.5 JTable print, it will send too many pages to the printer. I see where I can create a data model, etc. but I have half a dozen little screens where I want to provide this functionality. Seems like a lot of work...
-Mike
zero - 27 Sep 2005 00:20 GMT > I'm trying to dynamically resize the number of rows in my JTable > object. Basically, I'm reading data from a database table and I'm [quoted text clipped - 7 lines] > > -Mike Changing the number of rows shouldn't be any problem at all. You just change the underlaying TableModel, and notify the JTable component of this (I believe that's a fireTableModelChanged). And remember this is OO, so you only have to have one TableModel for all your screens, and dynamically fill in the data. Keep the GUI and the model separate.
Beyond that I'm afraid there one big truth you'll have to face: JTables are very powerful, but if you want anything but the most basic functionality, it'll be a lot of work.
Roedy Green - 27 Sep 2005 01:34 GMT >I'm trying to dynamically resize the number of rows in my JTable >object. Basically, I'm reading data from a database table and I'm [quoted text clipped - 5 lines] >data model, etc. but I have half a dozen little screens where I want to >provide this functionality. Seems like a lot of work... I have read your request several times and I am still not sure what you problem is.
Why not change your table and only tell swing about it with your event firings only when you know how many rows you got? You could keep a copy of the old model around until the new one is complete, then swap it in as a whole.
 Signature Canadian Mind Products, Roedy Green. http://mindprod.com Again taking new Java programming contracts.
mmellinger66@gmail.com - 27 Sep 2005 03:11 GMT I don't have a model. I figure out how many rows(max) of data that I will need then I create the JTable with the appropriate number of rows then change them as I load in the new dataset. I want to filter the data based on 1 or 2 criteria to get a subset to reduce the results display in the table from 200, for example, to 5 then allow someone to print the table. It's all pretty simple except that when I go to print, I get all those blank rows because I initialized the JTable with a size of 250. It almost works without adding the model. I've got half a dozen screens that I want like this.
Anyway, I guess I need to build a model and maintain it myself. I've done a lot of Tcl/Tk and I'm new to Java so I guess I'm used to doing this the "easy" way. The results are impressive so far, so I'll get over the little bit of extra it. Java has come a long way...
-Mike
Dag Sunde - 27 Sep 2005 06:45 GMT >I don't have a model. I figure out how many rows(max) of data that I > will need then I create the JTable with the appropriate number of rows [quoted text clipped - 10 lines] > this the "easy" way. The results are impressive so far, so I'll get > over the little bit of extra it. Java has come a long way... As Zero said in an earlier post, There is almost nothing you can't do with JTable, but for anything than the most basic you'll need a data- model.
Put some work into the design of your model and rely on the events the JTable listens to, and complex tables becomes a lot easier.
You do all and any work on the data in the datamodel itself, and just tell the table when you're done.
 Signature Dag.
Roedy Green - 27 Sep 2005 07:23 GMT >but for anything than the most basic you'll need a data- >model. Even when you don't have a DataModel, I gather what happens is JTable constructs a simple one for you.
Some day I will have to look and see what JTable does. It feels like just a skeleton to hang your own code on.
 Signature Canadian Mind Products, Roedy Green. http://mindprod.com Again taking new Java programming contracts.
Dag Sunde - 27 Sep 2005 10:30 GMT >>but for anything than the most basic you'll need a data- >>model. [quoted text clipped - 4 lines] > Some day I will have to look and see what JTable does. It feels like > just a skeleton to hang your own code on. I read somwhere the other day that JTable at first came without the constructor that let you define the data on instantiation of the JTable.
But people where complaining about how difficult it was to create a datamodel just to show some simple tabular data, so they they added a couple of convenience constructors that did the work behind the sceenes.
Later on the designers at Sun regretted this painfully, because of the confusion and misunderstandings it have created.
 Signature Dag.
steve - 01 Oct 2005 23:10 GMT >>> but for anything than the most basic you'll need a data- >>> model. [quoted text clipped - 14 lines] > Later on the designers at Sun regretted this painfully, because of the > confusion and misunderstandings it have created. yes they messed it up badly. the other thing they messed up was not being able to easily sort the data , from a user point of view. you have to resort to all sorts of tricks , ESP. if the user can add & delete lines from the table. ( whilst in sort mode)
Roedy Green - 02 Oct 2005 03:21 GMT >yes they messed it up badly. the other thing they messed up was not being >able to easily sort the data , from a user point of view. >you have to resort to all sorts of tricks , ESP. if the user can add & delete >lines from the table. ( whilst in sort mode) here is the song and dance I had to go through to keep a table ordered with new elements arriving with random sort keys to insert them in the right spot or overwrite an existing element.
package com.mindprod.table;
import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Comparator; import java.util.HashMap;
import javax.swing.JTable; import javax.swing.table.JTableHeader; import javax.swing.table.TableColumnModel;
/** * TableOrderer, Keeps a table in order sorted on several columns. * Also ensures no dups inserted. * Also ensures table does not get too big. * Does not implement OrderedTableModel but requires a TableModel that does. * It is not aware of how long the various rows are. * It just keeps track of rows. * * @author Roedy Green * @version 1.1 * @since 2002 Sep 10 */ public class TableOrderer {
/** * Column sorted in ascending order. */ public static final int ASCENDING = 1;
/** * Column sorted in descending order */ public static final int DESCENDING = -1;
/** * Column not sorted. */ public static final int UNSORTED = 0;
/** * constant to specify pruning tables at row 0, * Usually used with ascending sorts. Scrolling text moves up. * Chops off biggest values. */ public final static int CHOP_AT_TOP = 0;
/** * constant to specify pruning tables at the last row. * Usually used with descending sorts, or unsorted * tables. Scrolling text moves down. chops off smallest values. */ public final static int CHOP_AT_BOTTOM = 1;
/** * whether ith key to sort on is ascending or * descending. True for ascending. */ private boolean[] ascDesc;
/** *comparator to order this table, null forunsorted. */ private Comparator comparator;
/** * Column we want to keep free of duplicates, 0-based. */ private int deDupCol;
/** * List of keys we don't want to duplicate * and where they currently are in the list. */ private HashMap deDupMap = null;
/** * true if the deduping code is turned on. */ private boolean deDupEngaged;
/** * Maximum number of rows we will allow the table to grow too. * We let the table temporarily get one bigger than this. */ private int maxRows = 1000;
/** * OrderedTableModel that we are keeping in * order and deduped. */ private OrderedTableModel model;
/** * most recently changed row. If rows are added, this does not slide. * It is the absolute position of the data when it was recently changed, * not where it slid to now. */ private int mostRecentlyChangedRow = -1;
/** * CHOP_AT_TOP CHOP_AT_BOTTOM */ private int whereToChop = CHOP_AT_TOP;
/** * zero based columns to sort on, major first. * null if no sort keys. Usually array of one element holding the 0-based column to sort on. */ private int[] sortCols0Based;
/** * true if the sorting code is turned on. */ private boolean sortEngaged;
/** * Track how long (in looks) the dup list has remained stable. * If it sits stable it is faster to use the HashMap, * if not, linear search for detecting dups. */ private int stableFor = 0;
/** * Public constructor. * Should do a setComparator, setMaxRows, setWhereToChop, setDeDupCol as well. * * @param model OrderedTableModel to be kept deduped/sorted. */ public TableOrderer ( OrderedTableModel model ) { this.model = model; }
/** * replace a row cyclically within an single table. * Does no sort or dedup. * @param aux auxiliary data about the row, * usually dangerlevel and age. * possibly null. * * @param rowData row of Object data to add. */ public void acceptCyclicRow( Object[] rowData, Object aux ) { int row = mostRecentlyChangedRow+1; if ( row >= maxRows ) { row = 0; } if ( row < model.getRowCount() ) { // write over top of an existing row. model.replaceRow( rowData, aux, row ); setMostRecentlyChangedRow( row ); model.fireTableRowsUpdated( row, row ); } else { // it is a brand new row, add it to the end, will not overflow. model.addRow( rowData, aux, row ); setMostRecentlyChangedRow( row ); model.fireTableRowsInserted( row, row ); } }
/** * Accept a new row, add/delete/replace existing row if duplicate. * * @param rowData row of Object data to add. * @param aux auxiliary data about the row, * usually dangerlevel and age. * possibly null. * @param specialInstructions * ServerDataItemResponse.ADD, ADD_NO_DEDUP or DELETE. */ public void acceptRow( Object[] rowData, Object aux, int specialInstructions ) { switch ( specialInstructions ) { case ServerDataItemResponse.ADD: this.addRow( rowData, aux); break;
case ServerDataItemResponse.ADD_NO_DEDUP: this.addRowInOrder ( rowData, aux ); break;
case ServerDataItemResponse.DELETE: this.deleteMatchingRow ( rowData ); break;
default: Log.println( Log.BUG,"invalid specialInstructions."); } }
/** * Add a new row, replace existing row if duplicate. * * @param rowData row of Object data to add. * @param aux auxiliary data about the row, * usually dangerlevel and age. * possibly null. */ public void addRow( Object[] rowData, Object aux ) { if ( deDupEngaged ) { int row = findDup( rowData[ deDupCol ] ); if ( row >= 0 ) { // was a duplicate, replace existing // This should be the normal case. this.replaceRow ( rowData, aux, row ); } else { /* not a duplicate */ this.addRowInOrder ( rowData, aux ); /* deDupMap is no longer valid, we don't know where that row will go or how it will shift other rows. */ invalidateDeDupMap(); } } else { /* no deduping */ this.addRowInOrder ( rowData, aux ); } }
/** * add a new row wherever it belongs in sort order. * * @param rowData row of Object data to add. * @param aux auxiliary data about the row, * usually dangerlevel and age. * possibly null. */ private void addRowInOrder( Object[] rowData, Object aux) { if ( sortEngaged ) { /* row where found dup */ int where = model.binarySearch( rowData, comparator ); /* row where to insert new row */ int insertAt = where; if ( where < 0 ) { /* we did not find it there already, where contains (-(insertion point) - 1) */ insertAt = -where - 1; } else { /* found key, we have the index of one of the dups. */ switch ( whereToChop ) { case CHOP_AT_BOTTOM:
// We chop at bottom, (desc order) so we want to add before all dups. // Remember that descending Comparator has reverse sign result. for ( int row=where-1; row>=0; row-- ) { if ( comparator.compare( rowData, model.getQuickRowData(row)) > 0 ) { insertAt = row+1; break; } } if ( insertAt == where ) { // did not find anything smaller insertAt = 0; } break;
case CHOP_AT_TOP:
// We chop at top, (asc order) so we want to add after all dups. // CHOP_AT_TOP int numRows = model.getRowCount(); for ( int row=where+1; row<numRows; row++ ) { if ( comparator.compare( model.getQuickRowData(row), rowData) > 0 ) { insertAt = row; break; } } if ( insertAt == where ) { // did not find anything bigger insertAt = numRows; } break;
default: break; } // end switch } // end else // We now have the insertion point. /* add either at insertion point or or after dup */ if ( insertAt >= maxRows && whereToChop == CHOP_AT_BOTTOM ) { // off the bottom end. No point in inserting at all, will just get chopped. insertAt = -1; }
if ( insertAt == 0 && model.getRowCount() >= maxRows && whereToChop == CHOP_AT_TOP ) { // off the top end. table is full. Adding a row at the top will just get chopped. // don't bother. insertAt = -1; }
if ( insertAt >= 0 ) {
model.addRow( rowData, aux, insertAt ); setMostRecentlyChangedRow( insertAt ); model.fireTableRowsInserted( insertAt, insertAt ); /* inserted record may push existing one off either end */ chopTable( 0 ); } // otherwise we discarded the row as off either end. // leave existing mostRecentlyChangedRow } else { // not sorting // make room for new row chopTable( 1 ); switch ( whereToChop ) { case CHOP_AT_BOTTOM: // not sorting, just add at the top, model.addRow( rowData, aux, 0 ); setMostRecentlyChangedRow ( 0 ); model.fireTableRowsInserted( 0, 0 ); break;
case CHOP_AT_TOP: // not sorting, just add at the bottom int size = model.getRowCount(); model.addRow( rowData, aux, size ); setMostRecentlyChangedRow( size ); model.fireTableRowsInserted( size, size ); break; } } invalidateDeDupMap(); /* added row, disturbs map */ }
/** * Allow user to select a new sort column. * Works by adding a MouseListener to the column headers. * Mouse listener fields requests for sorting columns. * Allows click, shift-click on column to select column and ascending/descending. * * @param table the visible JTable part of the table. */ public void allowUserToSelectSortColumn( JTable table ) { // make available to anonymous inner class final JTable tableView = table; tableView.setColumnSelectionAllowed( false ); JTableHeader th = tableView.getTableHeader(); th.addMouseListener( new MouseAdapter() { /** * Handler for mouse selection of sort column * and order. Shift-click means descending. * * @param event Details of event */ public void mouseClicked( MouseEvent event ) { TableColumnModel columnModel = tableView.getColumnModel(); int viewColumn = columnModel.getColumnIndexAtX( event.getX() ); int column = tableView.convertColumnIndexToModel( viewColumn ); if ( event.getClickCount() == 1 && column != -1 ) { int shiftPressed = event.getModifiers() & InputEvent.SHIFT_MASK; boolean ascending = ( shiftPressed == 0 ); // if we are ascending, we chop at the top, if descending at the bottom. TableOrderer.this.setWhereToChop( ascending ? TableOrderer.CHOP_AT_TOP : TableOrderer.CHOP_AT_BOTTOM); // uses 1-based column numbers, -ve to request descending TableOrderer.this.sortByColumns( new int[] { ascending ? column +1 : -(column+1)} ); } } } ); } // end allowUserToSelectSortColumn
/** * Are two rows exactly equal? (except aux). * * @param rowa First row to compare. Defines number of columns. * @param rowb Second row To Compare * * @return true if all fields exactly match. */ private static boolean areRowsExactlyEqual ( Object[] rowa, Object[] rowb ) { int cols = rowa.length; // rowb may have an aux appended, one extra ignored column. for ( int i=0; i<cols; i++ ) { if ( ( (Comparable)rowa[i] ).compareTo( rowb[i] ) != 0 ) { return false; } } return true; }
/** * chop table back down to size if it has grown * too big. We do this whether or not deDuping or sorting. * Chop from bottom or top as controlled by whereToChop. * ChopTable maintains mostRecentlyChangedRow. * * @param room How many free slots are needed to be free, in rows. */ private void chopTable( int room ) { // remove excess rows, working backwards. More efficient. // In practice should never have to chop more than one row, // so it does not may to save up the fireTableRows events. int highestRowToDelete = model.getRowCount()-1; int highestRowToKeep = maxRows-1-room; for ( int i=highestRowToDelete; i>highestRowToKeep; i-- ) { switch ( whereToChop ) { case CHOP_AT_BOTTOM: model.removeRow ( i ); model.fireTableRowsDeleted(i, i); // don't setMostRecentlyChangedRow yet break;
case CHOP_AT_TOP: model.removeRow ( 0 ); model.fireTableRowsDeleted(0, 0); // don't setMostRecentlyChangedRow yet break;
default: throw new IllegalArgumentException ("TableOrderer.chopTable expects only CHOP_AT_BOTTOM or CHOP_AT_TOP"); } /* deDupMap is no longer valid, since chopped key is no longer in list. */ /* only called if we chop a row */ invalidateDeDupMap(); } // end for }
/** * find a duplicate via a dedup mechanism. Needs match on only one column. * presumes deDupEngaged. * * @param item Item to check for duplicate. The value of the * cell to match in the deDup column. * @return row where dup found, or -1 if not a dup. */ private int findDup ( Object item ) { int row; /* check to see if we have seen this key before */ /* pick faster algorithm, depending on recent stability. */ if ( stableFor > 50 ) { row = findDupViaMap( item ); } else { /* Everthing is changing so fast, no point in using deDupMap, since it goes out of date and has to be rebuilt. */ row = findDupViaModel( item ); } stableFor++; /* note one more cycle of stability. */ return row; }
/** * Find duplicate of this row using HashMap lookup. * This technique means we don't need to sort on dedup column. * * @param item Item to check for duplicate. The value of the * cell to match in the deDup column. * @return row where dup found, or -1 if not a dup. */ private int findDupViaMap( Object item ) { if ( deDupMap == null ) { rebuildDeDupMap(); } Integer row = (Integer)deDupMap.get( item ); return row == null ? -1 : row.intValue(); }
/** * Find duplicate of this row using linear search of Model * * @param item Item to check for duplicate. The value of the * cell to match in the deDup column. * @return row where dup found, or -1 if not a dup. */ private int findDupViaModel( Object item ) { int numRows = model.getRowCount(); for ( int row=0; row<numRows; row++ ) { if ( item.equals( model.getValueAt( row, deDupCol ) ) ) { return row; } } return -1; }
/** * Delete a matching row, all fields must match exactly except aux. * If more than one row matches, only first match is deleted. * If no match in found, that is OK. * * @param rowData matching row of Object data to delete. */ public void deleteMatchingRow( Object[] rowData ) { // try for a fast lookup via the dedup mechanism. int row = -1; if ( deDupEngaged ) { row = findExactMatchViaMap( rowData ); } if ( row < 0 ) { // dedup mechanism failed, try linear search from the top row = findExactMatchViaModel( rowData, 0 ); }
if ( row >= 0 ) { model.removeRow( row ); /* most recently changed row won't be there, use row where it was. */ setMostRecentlyChangedRow( row ); // if there is a second match, we leave it. model.fireTableRowsDeleted( row, row ); } // otherwise could not find a match. Don't complain. } // end deleteMatchingRow
/** * Find exact duplicate of this row using dedup mechanism. * all fields must match. Presumes deDupEngaged, * * @param rowData row of Object data to match * @return row where match found, or -1 if not found */ private int findExactMatchViaMap( Object[] rowData ) { int row = findDup( rowData[ deDupCol ] ); if ( row >= 0 ) { if ( areRowsExactlyEqual( rowData, model.getQuickRowData( row ) ) ) { return row; } // try linear search of succeeding entries return findExactMatchViaModel ( rowData, row+1 ); } return -1; }
/** * Find exact duplicate of this row using linear search of Model. * All fields must match. * * @param rowData row of Object data to match * @param startRow row where to start looking. Does not search * above this row. * @return row where match found, or -1 if not found */ private int findExactMatchViaModel( Object[] rowData, int startRow ) { // do a linear search starting at the given row. No wrap around. int numRows = model.getRowCount(); for ( int row=startRow; row<numRows; row++ ) { if ( areRowsExactlyEqual ( rowData, model.getQuickRowData( row ) ) ) { return row; } } return -1; }
/** * Mark deDupMap as no longer accurate. * The mapping of dups to row numbers has * been disturbed. The table needs to be * rebuilt. */ private void invalidateDeDupMap() { deDupMap = null; stableFor = 0; }
/** * Was this row recently changed? * If so it will likely get inverse highlighting. * * @param row 0-based row in question. * @return true if this was the most recently painted row. */ public boolean isRecentlyChanged ( int row ) { return row == mostRecentlyChangedRow; }
/** * Which row was most recently changed? * * @return 0-based row most recently changed. * -1 if none. */ public int getMostRecentlyChangedRow () { return mostRecentlyChangedRow; }
/** * How is a given column sorted. Used by header painting routines. * * @param column zero-based column number * @return 1, 0, -1 ASCENDING UNSORTED DESCENDING */ public int howColumnSorted ( int column ) { if ( sortCols0Based == null || sortCols0Based.length == 0 ) return UNSORTED; for ( int i=0; i<sortCols0Based.length; i++ ) { if ( column == sortCols0Based[i] ) return ascDesc[i] ? ASCENDING : DESCENDING; } return UNSORTED; }
/** * rebuild the list of dups and which row * they are associated with. * This is likely to be a bottleneck. * Becomes invalid as soon as any row moves or changes dedup key. * Only useful when display has a fixed set of dedup keys with changing data. */ private void rebuildDeDupMap () { if ( deDupEngaged ) { int numRows = model.getRowCount(); deDupMap = new HashMap( numRows * 2 ); for ( int i=0; i<numRows; i++ ) { deDupMap.put( model.getValueAt(i, deDupCol), new Integer(i)); } } }
/** * replace a row, doing sort adjust. * * @param rowData row of Object data to replace the existing row with. * @param aux auxiliary data about the row, * usually dangerlevel and age. * possibly null. * @param row row number to replace */ public void replaceRow( Object[] rowData, Object aux, int row ) { if ( sortEngaged ) { model.replaceRow( rowData, aux, row );
// need to determine if this disturbed the existing ordering. if ( ( row > 0 && model.compareRows( row, row-1, comparator ) < 0 ) ||( row < model.getRowCount()-1 && model.compareRows( row+1, row, comparator ) < 0 ) ) { // row is now out of place. // mostRecentlyChangedRow will soon be set in addRowInOrder model.removeRow( row ); model.fireTableRowsDeleted( row, row ); this.addRowInOrder( rowData, aux ); /* deDupMap is no longer valid, we don't know where that row will go or how it will shift other rows. */ invalidateDeDupMap(); } else { // did not disturb order setMostRecentlyChangedRow( row ); model.fireTableRowsUpdated( row, row ); } } else { // no sort model.replaceRow( rowData, aux, row ); setMostRecentlyChangedRow( row ); model.fireTableRowsUpdated( row, row ); } } // end replaceRow
/** which column should be keep free of duplicates? * * @param deDupCol1based to keep free of duplicates. 1-based. * 0 means no deduping */ public void setDeDupColumn( int deDupCol1based ) { int deDupCol = deDupCol1based - 1; if ( this.deDupCol != deDupCol ) { this.invalidateDeDupMap(); } this.deDupCol = deDupCol; this.deDupEngaged = ( deDupCol >= 0 ); }
/** * How many rows of data should be allow to accumulate before pruning it back. * Does not clear the model of data. * @param maxRows max size we are to allow the table to get. If it gets * bigger we chop off the last row. * 0 means leave the value as it was. */ public void setMaxRows( int maxRows ) { if ( maxRows != 0 ) { this.maxRows = maxRows; chopTable( 0 ); } }
/** * Track the MostRecentlyChangedRow and * previouslyChangedRow. Used to refresh display * of previously changed row. * * @param mostRecentlyChangedRow * New row that was most recently changed. * -1 if none. */ private void setMostRecentlyChangedRow ( int mostRecentlyChangedRow ) { int prev = this.mostRecentlyChangedRow; this.mostRecentlyChangedRow = mostRecentlyChangedRow; if ( 0 <= prev && prev != mostRecentlyChangedRow && prev < model.getRowCount() ) { /* Repaint the previous, since likely won't need highlight any more. It does not matter if data moved. We want the slot where it was. */ model.fireTableRowsUpdated( prev, prev ); } /* don't need to fire new mostRecentlyChangedRow, since handled separately. There are too many variants of the fireTableChange */ }
/** * Where should data be pruned when the table gets too big? * @param whereToChop CHOP_AT_TOP if should chop bottom element when table gets too big, * CHOP_AT_BOTTOM chop the top element. */ public void setWhereToChop( int whereToChop ) { this.whereToChop = whereToChop; chopTable( 0 ); }
/** * reSort the entire table, using the current Comparator. */ private void sort() { if ( sortEngaged ) { if ( model.getRowCount() > 0 ) { // will not be able to track where most recently changed went. setMostRecentlyChangedRow( -1 ); model.sort( comparator ); model.fireTableDataChanged();
} /* deDupMap is no longer valid, who knows how sort shuffled. */ invalidateDeDupMap(); } } // end sort
/** * Sort on a set of columns * * @param sortCols1Based * array of column-1 based columns so sort on, * major key first. * Negative column number means desceding sort. */ public void sortByColumns( int[] sortCols1Based ) { final int size = sortCols1Based.length; if ( size == 0 ) { this.sortCols0Based = null; this.ascDesc = null; this.sortEngaged = false; this.comparator = null; } else { // convert to more convenient // zero-based format, with separate ascending/descending boolean. this.sortCols0Based = new int[ size ]; this.ascDesc = new boolean[ size ]; for ( int i=0; i<size; i++ ) { if ( sortCols1Based[i] == 0 ) { throw new IllegalArgumentException("columns to sort cannot be 0 in TableOrderer.sortByColumns"); } sortCols0Based[ i ] = Math.abs( sortCols1Based[ i ] ) -1; ascDesc[ i ] = sortCols1Based[ i ] > 0 /* true=ascending */; } this.comparator = new MultiColumnComparator( sortCols0Based, ascDesc ); this.sortEngaged = true; sort(); } // make sure headers get repainted model.fireTableStructureChanged(); }
} // end TableOrderer
 Signature Canadian Mind Products, Roedy Green. http://mindprod.com Again taking new Java programming contracts.
steve - 02 Oct 2005 11:45 GMT >> yes they messed it up badly. the other thing they messed up was not being >> able to easily sort the data , from a user point of view. [quoted text clipped - 5 lines] > with new elements arriving with random sort keys to insert them in the > right spot or overwrite an existing element. <polite snip>
yep thats about the limit of it, no wonder people shy away from tables. how about deleting elements, and sorting on dates , as a date?
the other thing i hate , and had to write from scratch, was a security implementer, so that different users only have access to certain buttons , menu items lists etc, but could share common screens. That's still a work in progress!!
Steve
Dag Sunde - 02 Oct 2005 18:22 GMT >>yes they messed it up badly. the other thing they messed up was not being >>able to easily sort the data , from a user point of view. [quoted text clipped - 5 lines] > with new elements arriving with random sort keys to insert them in the > right spot or overwrite an existing element. http://java.sun.com/docs/books/tutorial/uiswing/components/table.html#sorting
Have you seen thisone? One tablemodel for data feeds the second. Second tablemodel feeds Table, and implements sorting, filtering, etc.
--------- TableModel ---------- TableModel --------
| | <-------------- | | <------------- | | | Model | | Sorter | | View | | | --------------> | | -------------> | | --------- TblModelListen ---------| TblModelListen --------
 Signature Dag.
steve - 02 Oct 2005 23:09 GMT >>> yes they messed it up badly. the other thing they messed up was not being >>> able to easily sort the data , from a user point of view. [quoted text clipped - 17 lines] >>> --------------> | | -------------> | | > --------- TblModelListen ---------| TblModelListen -------- yep it's bugged. but it could be good, as it does not actually sort the table , but keeps an ordered list of row indexes. However it can get real messy , if you start adding & removing rows.
steve
Roedy Green - 03 Oct 2005 00:57 GMT >--------- TableModel ---------- TableModel -------- >| | <-------------- | | <------------- | | >| Model | | Sorter | | View | >| | --------------> | | -------------> | | >--------- TblModelListen ---------| TblModelListen -------- My first reaction -- that's crazy, all the data duplicated, but it is not as bad as it looks. There in only one set of objects, just the references are duplicated.
I am puzzled though how having two copies makes things any easier. You still have to figure out where to insert/delete. It is not as though you just tack the new elts on the end and call sort every time.
 Signature Canadian Mind Products, Roedy Green. http://mindprod.com Again taking new Java programming contracts.
steve - 03 Oct 2005 10:40 GMT >> --------- TableModel ---------- TableModel -------- >>>> <-------------- | | <------------- | | [quoted text clipped - 9 lines] > still have to figure out where to insert/delete. It is not as though > you just tack the new elts on the end and call sort every time. yes , it's not clean, he just posted part of the diagram from the sun website
the data is a single copy, then a "list is maintained", and the rows translated.
so basically you can go to your table model, ask for row 1 , the sorter then goes to the list & knows that 'sorted' row 1 is actually 'un-sorted' row 6 , so actually returns row 6.
like i said it can get messy for adding/removing rows., and it is a buggy implementation, that is not error trapped correctly. I think sun never meant it to be anything more than a very basic sort routine.
no to mention it often crashes, when the table contains no data. and the sorter is broken.
ESP. for dates.
24/11/2005 is smaller than 27/06/1999
and we all 'know' why :-)
steve
Rogan Dawes - 03 Oct 2005 11:43 GMT >>My first reaction -- that's crazy, all the data duplicated, but it is >>not as bad as it looks. There in only one set of objects, just the [quoted text clipped - 3 lines] >>still have to figure out where to insert/delete. It is not as though >>you just tack the new elts on the end and call sort every time. The trick is that you DO just add the elements into your original table model, whereever you want to. The Sorter listens for new rows being added, and depending on how many are being added, either works out where to insert the single new row, or else reperforms the sort.
If you provide the user with a way of selecting a row in the table, and saying "delete" or "insert", the sorter provides a "int modelIndex(int viewIndex)" method that translates the visible row to the underlying tablemodel's row, so you can then call "originaltableModel.removeRow(row)" or whatever . . .
> yes , it's not clean, he just posted part of the diagram from the sun > website I dunno, I think the concept is pretty clean, personally.
> the data is a single copy, then a "list is maintained", and the rows > translated. [quoted text clipped - 5 lines] > like i said it can get messy for adding/removing rows., and it is a buggy > implementation, that is not error trapped correctly. I may have had to fix a few things, but it is working perfectly for me now. Get it at <http://cvs.sourceforge.net/viewcvs.py/owasp/webscarab/src/org/owasp/webscarab/ut il/swing/TableSorter.java?rev=1.1&view=markup>
Although the version that I have checked in there is still 1.1, so I doubt that I have made any changes to it.
> I think sun never meant it to be anything more than a very basic sort > routine. Maybe we are talking about different versions. The one that I have (from sun) is quite comprehensive, and allows you to sort on multiple columns, ascending or descending, etc.
> no to mention it often crashes, when the table contains no data. > and the sorter is broken. [quoted text clipped - 6 lines] > > and we all 'know' why :-) Yes, because you are presenting a String version of your date in the table, rather than a Date object and using a renderer . . . The version I link to uses Comparable to determine how to sort items.
> steve Rogan
Dag Sunde - 03 Oct 2005 13:11 GMT >>>My first reaction -- that's crazy, all the data duplicated, but it is >>>not as bad as it looks. There in only one set of objects, just the [quoted text clipped - 57 lines] > table, rather than a Date object and using a renderer . . . The version I > link to uses Comparable to determine how to sort items. Thank you, Rogan...
I was starting to get worried about this, and thought I had recommended something that was seriously flawed.
I use it a lot myself, and have never had any problems with it.
I never take any special considerations when inserting or deleting rows, and my dates are dates in my model, so...
 Signature Dag.
steve - 03 Oct 2005 22:22 GMT >>>> My first reaction -- that's crazy, all the data duplicated, but it is >>>> not as bad as it looks. There in only one set of objects, just the [quoted text clipped - 68 lines] > I never take any special considerations when inserting or deleting > rows, and my dates are dates in my model, so... for inserts & deletes , you need to track them , and it is a problem if the data is from a backend database.
for sorting.
MM9492 MM9461 MM80753 MM5596
the sorting seems to "place" compare and ignores the length. that's what i mean by 'broken'. if you are sorting lists of part numbers, then it is not the intended result.
steve
Roedy Green - 04 Oct 2005 09:14 GMT >MM9492 >MM9461 [quoted text clipped - 4 lines] >mean by 'broken'. if you are sorting lists of part numbers, then it is not >the intended result. The sort just follows whatever natural order Comparable you have defined, right?
How would you like those items to sort? Perhaps I could show you how to write a Comparable
 Signature Canadian Mind Products, Roedy Green. http://mindprod.com Again taking new Java programming contracts.
steve - 04 Oct 2005 22:34 GMT >> MM9492 >> MM9461 [quoted text clipped - 10 lines] > How would you like those items to sort? Perhaps I could show you how > to write a Comparable yes if you are volunteering ;-)
the reason my dates are broken, is more complex. As all the data comes from the database, the dates are actually time-stamps, which have the "microsecond part", to prevent longwinded conversions, ( i read objects from the database not types, so that if i change my table structure , my code does not break) for every date in every row , i just converted to string, since for updates there is no class conversion for the SQL for dates!!. when i get time i will clean it up.
well normally i would prefer the letters to sort as letters & the "numbers" as numbers"
MM123 MM9492 MM5596 MM80753 MM1234556
but we do not always use a 2 letter prefix, and some of our part numbers (rarely) might be
12ASASA8545-878
i suspect the best way is to compare length then alpha, that should fix it.
but if possible i would like it to tie into the sun "broken" sorter.
this is how i am currently sorting.
String[] colheads2 = { TableColMaintainance.HIDDENCOL, TableColMaintainance.HIDDENCOL1, "Models" ,"Flags","Born"}; // hide index cols,&define visible cols private QueryTableModel modelqtbl = new QueryTableModel(colheads2, query.NULLSUBQUERY); // pass in the col names needed, and an empty subquery to get the table to display in the IDE TableSorter sorter = new TableSorter(modelqtbl); private JTable modelsList = new JTable(sorter); //tie in the sorter.
and the sorter is this:
package utils;
import java.awt.*; import java.awt.event.*;
import java.util.*; import java.util.List;
import javax.swing.*; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.*;
/** * TableSorter is a decorator for TableModels; adding sorting * functionality to a supplied TableModel. TableSorter does * not store or copy the data in its TableModel; instead it maintains * a map from the row indexes of the view to the row indexes of the * model. As requests are made of the sorter (like getValueAt(row, col)) * they are passed to the underlying model after the row numbers * have been translated via the internal mapping array. This way, * the TableSorter appears to hold another copy of the table * with the rows in a different order. * <p/> * TableSorter registers itself as a listener to the underlying model, * just as the JTable itself would. Events recieved from the model * are examined, sometimes manipulated (typically widened), and then * passed on to the TableSorter's listeners (typically the JTable). * If a change to the model has invalidated the order of TableSorter's * rows, a note of this is made and the sorter will resort the * rows the next time a value is requested. * <p/> * When the tableHeader property is set, either by using the * setTableHeader() method or the two argument constructor, the * table header may be used as a complete UI for TableSorter. * The default renderer of the tableHeader is decorated with a renderer * that indicates the sorting status of each column. In addition, * a mouse listener is installed with the following behavior: * <ul> * <li> * Mouse-click: Clears the sorting status of all other columns * and advances the sorting status of that column through three * values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to * NOT_SORTED again). * <li> * SHIFT-mouse-click: Clears the sorting status of all other columns * and cycles the sorting status of the column through the same * three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}. * <li> * CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except * that the changes to the column do not cancel the statuses of columns * that are already sorting - giving a way to initiate a compound * sort. * </ul> * <p/> * This is a long overdue rewrite of a class of the same name that * first appeared in the swing table demos in 1997. * * @author Philip Milne * @author Brendon McLean * @author Dan van Enckevort * @author Parwinder Sekhon * @version 2.0 02/27/04 */ public class TableSorter extends AbstractTableModel { public static final int DESCENDING = -1; public static final int NOT_SORTED = 0; public static final int ASCENDING = 1; private static Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED); public static final Comparator COMPARABLE_COMAPRATOR = new Comparator() { public int compare(Object o1, Object o2) { return ((Comparable) o1).compareTo(o2); } };
public static final Comparator LEXICAL_COMPARATOR = new Comparator() { public int compare(Object o1, Object o2) { return o1.toString().compareTo(o2.toString()); } }; private boolean TableEmpty=false; protected TableModel tableModel; private Row[] viewToModel; private int[] modelToView; private JTableHeader tableHeader; private MouseListener mouseListener; private TableModelListener tableModelListener; private Map columnComparators = new HashMap(); private List sortingColumns = new ArrayList();
public TableSorter() { this.mouseListener = new MouseHandler(); this.tableModelListener = new TableModelHandler(); }
public TableSorter(TableModel tableModel) { this(); setTableModel(tableModel); }
public TableSorter(TableModel tableModel, JTableHeader tableHeader) { this(); setTableHeader(tableHeader); setTableModel(tableModel); }
private void clearSortingState() { viewToModel = null; modelToView = null; }
public TableModel getTableModel() { return tableModel; }
public void setTableModel(TableModel tableModel) { if (this.tableModel != null) { this.tableModel.removeTableModelListener(tableModelListener); }
this.tableModel = tableModel;
if (this.tableModel != null) { this.tableModel.addTableModelListener(tableModelListener); }
clearSortingState(); fireTableStructureChanged(); }
public JTableHeader getTableHeader() { return tableHeader; }
public void setTableHeader(JTableHeader tableHeader) { if (this.tableHeader != null) { this.tableHeader.removeMouseListener(mouseListener);
TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer();
if (defaultRenderer instanceof SortableHeaderRenderer) { this.tableHeader.setDefaultRenderer( ((SortableHeaderRenderer) defaultRenderer).tableCellRenderer); } }
this.tableHeader = tableHeader;
if (this.tableHeader != null) { this.tableHeader.addMouseListener(mouseListener); this.tableHeader.setDefaultRenderer( new SortableHeaderRenderer( this.tableHeader.getDefaultRenderer())); } }
public boolean isSorting() { return sortingColumns.size() != 0; }
private Directive getDirective(int column) { for (int i = 0; i < sortingColumns.size(); i++) { Directive directive = (Directive) sortingColumns.get(i);
if (directive.column == column) { return directive; } }
return EMPTY_DIRECTIVE; }
public int getSortingStatus(int column) { return getDirective(column).direction; }
private void sortingStatusChanged() { clearSortingState(); fireTableDataChanged();
if (tableHeader != null) { tableHeader.repaint(); } }
public void setSortingStatus(int column, int status) { Directive directive = getDirective(column);
if (directive != EMPTY_DIRECTIVE) { sortingColumns.remove(directive); }
if (status != NOT_SORTED) { sortingColumns.add(new Directive(column, status)); }
sortingStatusChanged(); }
protected Icon getHeaderRendererIcon(int column, int ArrowSize) { Directive directive = getDirective(column);
if (directive == EMPTY_DIRECTIVE) { return null; }
return new Arrow( directive.direction == DESCENDING, ArrowSize, sortingColumns.indexOf(directive)); }
private void cancelSorting() { sortingColumns.clear(); sortingStatusChanged(); }
public void setColumnComparator(Class type, Comparator comparator) { if (comparator == null) { columnComparators.remove(type); } else { columnComparators.put(type, comparator); } }
protected Comparator getComparator(int column) { Class columnType = tableModel.getColumnClass(column); Comparator comparator = (Comparator) columnComparators.get(columnType);
if (comparator != null) { return comparator; }
if (Comparable.class.isAssignableFrom(columnType)) { return COMPARABLE_COMAPRATOR; }
return LEXICAL_COMPARATOR; }
private Row[] getViewToModel() { if (viewToModel == null) { int tableModelRowCount = tableModel.getRowCount(); viewToModel = new Row[tableModelRowCount];
for (int row = 0; row < tableModelRowCount; row++) { viewToModel[row] = new Row(row); } //if(tableModelRowCount==0){TableEmpty=true;}else{TableEmpty=false;} if (isSorting()) { Arrays.sort(viewToModel); } }
return viewToModel; }
public int modelIndex(int viewIndex) { Row[] temprow= getViewToModel(); // because we may need to initialize it!! //if(TableEmpty==true){return 0;}else{ // return temprow [viewIndex].modelIndex; // } return getViewToModel()[viewIndex].modelIndex; }
private int[] getModelToView() { if (modelToView == null) { int n = getViewToModel().length; modelToView = new int[n];
for (int i = 0; i < n; i++) { modelToView[modelIndex(i)] = i; } }
return modelToView; }
// TableModel interface methods public int getRowCount() { return (tableModel == null) ? 0 : tableModel.getRowCount(); }
public int getColumnCount() { return (tableModel == null) ? 0 : tableModel.getColumnCount(); }
public String getColumnName(int column) { return tableModel.getColumnName(column); }
public Class getColumnClass(int column) { return tableModel.getColumnClass(column); }
public boolean isCellEditable(int row, int column) { return tableModel.isCellEditable(modelIndex(row), column); }
public Object getValueAt(int row, int column) { return tableModel.getValueAt(modelIndex(row), column); }
public void setValueAt(Object aValue, int row, int column) { tableModel.setValueAt(aValue, modelIndex(row), column); }
// Helper classes private class Row implements Comparable { private int modelIndex;
public Row(int index) { this.modelIndex = index; }
public int compareTo(Object o) { int row1 = modelIndex; int row2 = ((Row) o).modelIndex;
for (Iterator it = sortingColumns.iterator(); it.hasNext();) { Directive directive = (Directive) it.next(); int column = directive.column; Object o1 = tableModel.getValueAt(row1, column); Object o2 = tableModel.getValueAt(row2, column); //added by me // Treat empty strings like nulls if (o1 instanceof String && (((String) o1).length() == 0)) { o1 = null; }
if (o2 instanceof String && (((String) o2).length() == 0)) { o2 = null; } //end of added by me int comparison = 0;
// Define null less than everything, except null. if ((o1 == null) && (o2 == null)) { comparison = 0; } else if (o1 == null) { comparison = -1; } else if (o2 == null) { comparison = 1; } else //if (o1 instanceof Comparable && o2 instanceof Comparable) { comparison = getComparator(column).compare(o1, o2); } //else{comparison =o1.toString().compareTo(o2.toString()); //} if (comparison != 0) { return (directive.direction == DESCENDING) ? (-comparison) : comparison; } }
return 0; } } /* // Sort nulls so they appear last, regardless // of sort order if ((o1 == null) && (o2 == null)) { return 0; } else if (o1 == null) { return 1; } else if (o2 == null) { return -1; } else if (o1 instanceof Comparable) { if (ascending) { return ((Comparable) o1).compareTo(o2); } else { return ((Comparable) o2).compareTo(o1); } } else { if (ascending) { return o1.toString().compareTo(o2.toString()); } else { return o2.toString().compareTo(o1.toString()); } } */ private class TableModelHandler implements TableModelListener { public void tableChanged(TableModelEvent e) { // If we're not sorting by anything, just pass the event along. if (!isSorting()) { clearSortingState(); fireTableChanged(e);
return; }
//if the event is null , then eat it!! //there seems to be some sort of bug, making the event null!! if (e == null) { fireTableChanged(e);
return; }
// If the table structure has changed, cancel the sorting; the // sorting columns may have been either moved or deleted from // the model. if (e.getFirstRow() == TableModelEvent.HEADER_ROW) { cancelSorting(); fireTableChanged(e);
return; }
// We can map a cell event through to the view without widening // when the following conditions apply: // // a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and, // b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and, // c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and, // d) a reverse lookup will not trigger a sort (modelToView != null) // // Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS. // // The last check, for (modelToView != null) is to see if modelToView // is already allocated. If we don't do this check; sorting can become // a performance bottleneck for applications where cells // change rapidly in different parts of the table. If cells // change alternately in the sorting column and then outside of // it this class can end up re-sorting on alternate cell updates - // which can be a performance problem for large tables. The last // clause avoids this problem. int column = e.getColumn();
if ( (modelToView != null) && (e.getFirstRow() == e.getLastRow()) && (column != TableModelEvent.ALL_COLUMNS) && (getSortingStatus(column) == NOT_SORTED)) { int viewIndex = getModelToView()[e.getFirstRow()]; fireTableChanged( new TableModelEvent( TableSorter.this, viewIndex, viewIndex, column, e.getType()));
return; }
// Something has happened to the data that may have invalidated the row order. clearSortingState(); fireTableDataChanged();
return; } }
private class MouseHandler extends MouseAdapter { public void mouseClicked(MouseEvent e) { JTableHeader h = (JTableHeader) e.getSource(); TableColumnModel columnModel = h.getColumnModel(); int viewColumn = columnModel.getColumnIndexAtX(e.getX()); int column = columnModel.getColumn(viewColumn).getModelIndex();
if (column != -1) { int status = getSortingStatus(column);
if (!e.isControlDown() && !e.isMetaDown()) { cancelSorting(); }
// Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed. status = status + (e.isShiftDown() ? (-1) : 1); status = ((status + 4) % 3) - 1; // signed mod, returning {-1, 0, 1} setSortingStatus(column, status); } } }
private static class Arrow implements Icon { private boolean descending; private int size; private int priority;
public Arrow(boolean descending, int size, int priority) { this.descending = descending; this.size = size; this.priority = priority; }
public void paintIcon(Component c, Graphics g, int x, int y) { Color color = (c == null) ? Color.BLUE : c.getBackground();
//color = Color.RED; // In a compound sort, make each succesive triangle 20% // smaller than the previous one. //this is not such a good idea if there is a lot of cols to sort!! int dx = (int) (size / 2 * Math.pow(0.8, priority)); int dy = descending ? dx : (-dx);
// Align icon (roughly) with font baseline. y = y + ((5 * size) / 6) + (descending ? (-dy) : 0);
int shift = descending ? 1 : (-1); g.translate(x, y);
// Right diagonal. g.setColor(color.darker()); g.drawLine(dx / 2, dy, 0, 0); g.drawLine(dx / 2, dy + shift, 0, shift);
// Left diagonal. g.setColor(color.brighter()); g.drawLine(dx / 2, dy, dx, 0); g.drawLine(dx / 2, dy + shift, dx, shift);
// Horizontal line. if (descending) { g.setColor(color.darker().darker()); } else { g.setColor(color.brighter().brighter()); }
g.drawLine(dx, 0, 0, 0);
g.setColor(color); g.translate(-x, -y); }
public int getIconWidth() { return size; }
public int getIconHeight() { return size; } }
private class SortableHeaderRenderer implements TableCellRenderer { private TableCellRenderer tableCellRenderer;
public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) { this.tableCellRenderer = tableCellRenderer; }
public Component getTableCellRendererComponent( JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component c = tableCellRenderer.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column);
if (c instanceof JLabel) { JLabel l = (JLabel) c; l.setHorizontalTextPosition(JLabel.LEFT);
int modelColumn = table.convertColumnIndexToModel(column); //this sets the size of the arrow l.setIcon( getHeaderRendererIcon(modelColumn, l.getFont().getSize()+4)); }
return c; } }
private static class Directive { private int column; private int direction;
public Directive(int column, int direction) { this.column = column; this.direction = direction; } } }
Roedy Green - 05 Oct 2005 02:49 GMT >MM123 >MM9492 [quoted text clipped - 8 lines] > >i suspect the best way is to compare length then alpha, that should fix it. Here is the code. I have only tested it with the test harness. Before you trust it, it should be given examples to exercise all the code.
The big thing wrong with it is on every key compare it goes through a great song and dance to parse the key into fragments. Ideally that should be done once, not once per compare.
You could have Compare cache the values or your could precompute them on a separate pass.
However, for a sort of only a few thousand you won't notice and big slowdown.
It is a big ugly the kludge it uses to flush the accumulations for the last key fragment. I'd like to see a tidier way to handle that. I wanted to avoid repeating code just to handle the last frag.
// Comparator for Catalog Numbers. Sorts alpha and numeric parts separately. // Sorts numeric parts numerically.
import java.lang.StringBuilder; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator;
class CatalogOrder implements Comparator<String> { /** * Compare two Strings. Callback for sort, as a catalog number. * effectively returns b-a; * @return +1, b>a, 0 a=b, -1 b<a, case insenstive */ public final int compare( String a, String b ) { // for speed could do tidyKey ahead of time, instead of calling once per compare.
Object[] aa = tidyKey( a );
Object[] bb = tidyKey( b );
int min = Math.min (aa.length, bb.length ); long diff;
for ( int i=0; i<min; i++ ) { if ( aa[i] instanceof String && bb[i] instanceof String ) { diff = ((String)aa[i]).compareTo( (String)bb[i] ); } else if ( aa[i] instanceof Long && bb[i] instanceof Long ) { diff = ((Long)aa[i]).longValue() - ((Long)bb[i]).longValue(); } else { // Mismatched. Not a very consistent catalog number system. // Treat numbers as less than letters. return aa[i] instanceof Long ? -1 : 1; } if ( diff != 0 ) { // can't just return raw diff, since it is a long return diff < 0 ? -1 : 1; } } // end for // we fell out the bottom of the loop with detecting a difference. // the longer key then comes later. if ( aa.length < bb.length ) return -1; if ( aa.length > bb.length ) return 1; // were absolutely identical return 0;
} // end compare
/** * Used create an aux sortable multifield key from a catalog number. * Converts an alphanumeric key to an array of mixed Strings and Longs * breaking catalog number into alpha and numeric fields */ public static Object[] tidyKey ( String catalog ) {
// chars in this catalog number int catalogLength = catalog.length();
// 0-terminated array to hold the catalog String // 0 lets us go round the loop one extra time to flush accumulation. char[] catalogChars = new char[ catalogLength + 1];
// copy catalog to char[]. Terminal 0 already in place. for ( int i=0; i<catalogLength; i++ ) { catalogChars[i] = catalog.charAt( i ); }
// Where we accumulate the fragments of the the key, either String or Long ArrayList<Object> keys = new ArrayList<Object>( 5 );
// keep track if we are processing numbers or letters. boolean inNumeric = false;
// where we accumulate the next chunk of key StringBuilder accum = new StringBuilder( catalogLength );
// loop once for each char, plus one more time to flush the append buffer. for ( int i=0; i<catalogLength+1; i++ ) { char c = catalogChars[i]; boolean isNumeric = '0'<= c && c <= '9'; if ( isNumeric != inNumeric || c == 0 ) { // we need to finish off the previous accumulation if ( inNumeric ) { // finish off previous numeric field String numeric = accum.toString(); if ( numeric.length() > 0 ) { keys.add ( new Long( numeric ) ); } inNumeric = false; } else { // finish off previous alpha field String alpha = accum.toString(); if ( alpha.length() > 0 ) { keys.add ( alpha ); } inNumeric = true; } // clear for reuse accum.setLength( 0 ); }
// tack onto key fragment we have accumulated already, // or to newly cleared accum accum.append ( c ); } // end for
return keys.toArray( new Object[ keys.size() ] ); } // end tidyKey
/** * test harness * * @param args not used */ public static void main ( String[] args ) {
// test tidyKey Object[] key = tidyKey( "12ASASA8545-878" ); for ( Object keyFrag : key ) { System.out.println ( keyFrag.getClass().getName() + " : " + keyFrag.toString() ); }
// try sorting some catalog numbers
String[] cats = { "MM123", "MM9492", "MM80753", "MM5596", "MM1234556", "12ASASA8545-878A", "12ASASA8545-878", "12ASASA8545-876", }; Arrays.sort( cats, new CatalogOrder() ); for ( String cat : cats ) { System.out.println( cat ); } } // end main }
 Signature Canadian Mind Products, Roedy Green. http://mindprod.com Again taking new Java programming contracts.
steve - 06 Oct 2005 22:15 GMT >> MM123 >> MM9492 [quoted text clipped - 198 lines] > } // end main > } thanks roedy, I'm going to take a look at this over the weekend, whilst I tidy up my 'date' implementation in the JTable.
Roedy Green - 03 Oct 2005 18:40 GMT >The trick is that you DO just add the elements into your original table >model, whereever you want to. The Sorter listens for new rows being >added, and depending on how many are being added, either works out where >to insert the single new row, or else reperforms the sort. But why have two copies of the data? Why not just work on the master copy as I did?
 Signature Canadian Mind Products, Roedy Green. http://mindprod.com Again taking new Java programming contracts.
Roedy Green - 03 Oct 2005 18:43 GMT >>The trick is that you DO just add the elements into your original table >>model, whereever you want to. The Sorter listens for new rows being [quoted text clipped - 3 lines] >But why have two copies of the data? Why not just work on the master >copy as I did? Perhaps the thinking was that it was expensive the move the actual data around, rather than an index to the data. That is COBOL thinking. Moving the rows around just means moving a reference to the row. You don't move the data for the row. Perhaps in some pathological model it is expensive to move the rows.
 Signature Canadian Mind Products, Roedy Green. http://mindprod.com Again taking new Java programming contracts.
jonck - 05 Oct 2005 00:50 GMT > But why have two copies of the data? Why not just work on the master > copy as I did? For me the advantage of this technique did not really become clear until instead of a Sorter, I needed a Filter (not an "official" class, but one I brewed up myself, it basically works like the Sorter except it stores a subset of all the indices of the master model). In other words, I wanted only certain rows of my table to be displayed based on certain criteria entered by the user. If I would have done this by actually deleting rows from my master table, records would have been lost and every time the user wanted to delete his previous query and enter a new one, I would have had to go and retrieve the original data from the database/XML file/whatever. Another advantage of this technique is that I could now layer Filters and Sorters on top of each other, therefore giving the user the capability of doing searches within resultsets of previous searches, sorting in between, etc... (so one could, for example, do a search like: give me everyone that lives in Paris, now everyone who's name starts with a B, now sort on zipcode) and if a user wanted to back-track a step, it was a simple case of deleting the last model in the chain.
This type of functionality really showed me the advantage of delegating models.
Kind regards, Jonck
|
|