Hide/Show columns of a JTable
The ability to dynamically hide and re-show columns of a JTable
is often asked for.
Letting the user decide which columns of a table she wants to see at any given moment
is not just good style but can improve readablity.
Unfortunately there is no built-in support for invisible columns in the swing table component.
But then, luckily there is some functionality to built upon.
Jumpstart: If you're in a hurry grab the code: Let's Do It
Inner workings of JTable et. al.
Each JTable
uses an instance of TableColumnModel
to keep track of all columns.
The interface TableColumnModel
specifies the protocol used to add, move, remove columns and query
for TableColumn
objects.
These TableColumn
objects hold data such as the column index in the table model it resembles, the
column's width and its cell renderer and editor.
The DefaultTableColumnModel
, ahm, well, is just that: Whenever you don't supply an column model
to the JTable
constructor, an instance of this class is created and populated with one column for
each column in your table model.
It's important to separate your idea of a column in the TableColumnModel
from a column in
the TableModel
.
There can be multiple TableColumn
objects for each column in the TableModel
(perhaps
showing the same value in different currencies).
Column indices in both JTable
and TableColumnModel
refer to the visible columns and
indices in TableModel
refer to actual index in the data.
Off course it is this indirection that allows for columns to be reordered. JTable
offers you two
methods to convert between these indices:
int convertColumnIndexToModel(int viewColumnIndex)
int convertColumnIndexToView(int modelColumnIndex)
Will return -1 if this column is not visible else it will return the viewIndex of the first (leftmost) column
with this model index.
The Simple Solution
The simplest way to hide a column and re-show it later is off course to remove it and add it again later.
The following methods from JTable
just delegate to the same methods defined in TableColumnModel
void addColumn(TableColumn aColumn)
void removeColumn(TableColumn column)
This is how it was done in any previous examples I have found.
One shortcoming is that columns are always added
to the right. That's not what I expect if I hide a column in my sophisticated table and immediatly make it visible again.
Columns should keep their order even when invisible. moveColumn() to the rescue:
void moveColumn(int columnIndex, int newIndex)
The problem then is to determine the newIndex
. It just won't work to use the last visible position,
because other columns might have been reordered and/or made invisible, so changing the overall column indices.
Another nuisance is the need to store the invisible TableColumn
objects someplace.
As soon as you called removeColumn(aColumn)
noone else is holding a reference to aColumn anymore.
If you want to make the same column visible again you must either throw it into some collection or construct it
from scratch. The former alternative is much more practical because it saves you from remembering the columns properties.
Improved Solution
What does it mean after all, that columns retain their positions when made invisible and visible again?
As long as you don't reorder columns it simply means their relative order stays the same no matter in which order
you hide/show any columns.
In order to see what should happen if you reorder columns while other columns are invisible, imagine these columns
in your model:
A B (C) D E
The parantheses indicate that C has just been made invisible.
If the user drags column A to a new position between D and E it is quite clear that new order is:
B (C) D A E
The only decision to make is what should happen if a column is dragged at a position where an invisible column rests:
Now drag E and drop it between B and D. We could either decide to always position to the left or to the right of any
invisible columns or to always exclude or include the invisible columns in the drag distance.
I choose the last alternative, so the new order is:
B (C) E D A
There is an easy implementation for this behaviour:
Put all columns (both visible and invisible) in a vector. If any column is moved then remove it from the old position
and insert it into the new position. The vector will adjust positions of the remaining elements accordingly.
In fact this is exactly what the DefaultTableColumnModel
does for the visible columns.
The vector of all columns also serves the purpose of keeping a reference to invisible columns, so that they can be
retrieved later on and they aren't garbage collected.
So we go ahead and subclass the default column model to get an extended version that can handle invisible columns.
Implementation
public class XTableColumnModel extends DefaultTableColumnModel {
protected Vector allTableColumns = new Vector();
public void setColumnVisible(TableColumn column, boolean visible);
public boolean isColumnVisible(TableColumn aColumn);
Let's throw in a method to query for the (first) column that shows a certain column index from the table model. We'll
need this if we want to retrieve an invisible column and pass it to setColumnVisible()
:
public TableColumn getColumnByModelIndex(int modelColumnIndex)
And then add a convenience method to show all invisible columns again:
public void setAllColumnsVisible();
For JTable
and other table column model listeners to work as before it is important that the column model
behaves as if invisible columns have been removed from the model. So the columns, count and index in each of these
functions still refer to the visible columns only:
getColumnCount()
, getColumns()
, getColumnIndex()
, getColumn()
You can see in the javadoc that I added methods with the same name that let you
specifiy wether you want to have invisible columns take into account.
To keep the vector of all columns up to date, these methods have to be overridden:
void addColumn(TableColumn aColumn); | Just also add the column to the allTableColumns vector.
|
void removeColumn(TableColumn column); | Remove the column from the allTableColumns vector.
|
void moveColumn(int columnIndex, int newIndex); | Find the corresponding column objects in the allTableColumns vector.#
and move them accordingly.
|
All these let the superclass do the real work.
Now take a closer look at the most important method:
public void setColumnVisible(TableColumn column, boolean visible);
If the column
is made invisible the call is simply rerouted to super.removeColumn(column);
,
while leaving allTableColumns
as is.
To make the column
visible again, we find its new position, then call super.addColumn(column)
and super.moveColumn(tableColumns.size() - 1 /* rightmost position */, newPosition);
The newPosition
is calculated by counting all visible column in allTableColumns
up to
column
.
For details check out the source code.
There is one gotcha with this design: If you currently have invisible columns and change your table model the
JTable
will recreate columns, but will fail to remove any invisible columns.
Alternative Implementations
Off course there are alternatives to this implementation. Here are those that come to my mind:
- Add all functionality to a
JTable
subclass. This would be easier to use, because there's no need
to bother about setting the column model and accessing it later. However it is not generally safe to overload
JTable.addcolumn()
, JTable.removecolumn()
and JTable.moveColumn()
to do the job
of adjusting allTableColumns
.
Columns can be manipulated directly at the column model, bypassing the JTable
methods.
So you would have to adjust the listener methods
JTable.columnAdded()
, JTable.columnRemoved()
and JTable.columnMoved()
instead.
Then problems arise because if the table itself makes a column (in)visible these listener are called also, but must act
differently. This is a clear indication that the code belonged to JTableColumnModel
in the first place.
- Subclass both
JTable
and TableColumnModel
. This would maybe be cleaner, but hey: How far
do you wanna carry this. While we're at it we can subclass TableColumnModelListener
also to add events
for column visibility changes...
- Leave alone both
TableColumnModel
and JTable
and implement a distinct class that is
responsible of hiding columns and keeping track of all hidden columns.
This has some appeal to me, but the same difficulty as in the first alternative arises: You have to listen to
column table model events and have to distinguish a real "column removed event" from a "column made invisible event".
- Others? Maybe I'm thinking to complicated and there is a real simple solution?
Let's Do It - Put It to Use
Take this code, and do with it whatever you like, but
- Don't blame it on me if it does not work for you or causes you any troubles.
Off course I'll be glad to hear of any defects or possible improvements.
- Do not claim that you have written it.
Using the extended column model requires two simple steps:
- Make
XTableColumnModel
your table column model
If you have used your own column model before you know how to replace it with XTableColumnModel.
However it is more probable that you relied on JTable
creating a default table column model for you.
Here is what you do to replace it:
Create your table as usual: | JTable yourTable = new JTable(...);
|
Create and remember a XTableColumnModel : | yourColumnModel = new XTableColumnModel();
|
Make your table use the new column model: | yourTable.setColumnModel(yourColumnModel);
|
Create columns: | yourTable.createDefaultColumnsFromModel();
|
- Get hold of the
TableColumn you want to show or hide and call setColumnVisible(), e.g. to hide the
very first column:
TableColumn column = columnModel.getColumnByModelIndex(0);
yourColumnModel.setColumnVisible(column, false);
Any comments and suggestions are welcome:
mail@StephenKelvin.de