Thanks in advance,
Bruce
> Is there a way to have a the column header span more than one column in a
> JTable? Something like this:
Not really. First of all, the header is used to resize and move columns.
That would effectively not any when the header didn't hold a cell for
each column.
Having said that,
a) nothing prevents you from styling multiple header
cells so that they look as one (TableColumn.setHeaderRenderer, JTable-
Header.setDefaultRenderer), except that text placement will be tricky.
b) you can also put the data for two "columns" in a single column in
the JTable. Then reordering and resizing still works for each column
(which then appears to contain of multiple "columns".)
c) hacks including replacing the TableHeaderUI. But it is probably
impossible to write a TableHeaderUI (even if one were in the position
where that was an option) that doesn't have one cell per column, with-
out losing the well-definedness of some methods in JTableHeader and
doing strange things to Accessibility.
d) don't use JTableHeader, but something different. This is probably
not completely easy, but you don't run the risk of incompatibilities
as with c).
> ------------------------------------------
> | Header | Header |
> ------------------------------------------
For ASCII drawings, use an monospaced font.
Christian
--
Half-wracked prejudice leaped forth, / "Rip down all hate," I screamed,
Lies that life is black and white / Spoke from my skull I dreamed
Romantic facts of musketeers / Foundationed deep, somehow
Bob Dylan, My Back Pages
> Bruce T <btr...@contactsystems.com> wrote:
>> Is there a way to have a the column header span more than one column in a
>> JTable? Something like this:
[...]
> d) don't use JTableHeader, but something different. This is probably
> not completely easy, but you don't run the risk of incompatibilities
> as with c).
package de.chka.swing.experimental;
import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Enumeration;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.IOException;
/**
Example of a table header that allows cells to span multiple columns.
Copyright (C) 2001 Christian Kaufhold <ch-ka...@gmx.de>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/** Just a default renderer. To work with TableHeader, the corresponding
TableHeader must be read from the JTable client property (see code),
since JTable.getTableHeader() cannot be used.
*/
class DefaultHeaderRenderer
extends DefaultTableCellRenderer
{
{
setBorder(UIManager.getBorder("TableHeader.cellBorder"));
}
public void updateUI()
{
super.updateUI();
setHorizontalAlignment(CENTER);
setBorder(UIManager.getBorder("TableHeader.cellBorder"));
}
public Component getTableCellRendererComponent
(JTable table, Object value, boolean selected, boolean focused,
int row, int column)
{
TableHeader header;
if (table != null && (header = (TableHeader)table.getClientProperty(TableHeader.KEY)) != null)
{
setForeground(header.getForeground());
setBackground(header.getBackground());
setFont(header.getFont());
setComponentOrientation(header.getComponentOrientation());
setEnabled(header.isEnabled());
}
else
{
setForeground(UIManager.getColor("TableHeader.foreground"));
setBackground(UIManager.getColor("TableHeader.background"));
setFont(UIManager.getFont("TableHeader.font"));
setComponentOrientation(ComponentOrientation.UNKNOWN);
setEnabled(true);
}
setText(value != null ? value.toString() : "");
return this;
}
}
/** An alternative TableHeader that allows header cells to span multiple
columns. This is just a bare-bones prototype, things to do are:
- Optimize paint code to paint only concerned columns
- Area conversion (columnAtPoint, headerRect or similar)
- getToolTipText(MouseEvent)
- resizing of columns (which of the columns should be resized?)
- moving of columns (which cannot be done in real time since JTable
doesn't know that it has to paint multiple dragged columns.)
- how to serialize if the renderer isn't serializable?
- Accessibility
Note: JTable aggressively installs its JTableHeader in the column header
view of JScrollPane, even if it is set to 'null'. To avoid confusion,
override its configureEnclosingScrollPane() method to do nothing, for
example.
Typically, a TableHeader is set up like this:
TableColumnModel columns = ...;
// columns contains some XTableColumns that have spans set.
JTable table = new FixedJTable(data, columns); // see above which fix
table.setTableHeader(null);
TableHeader header = new TableHeader(table);
JScrollPane pane = new JScrollPane(table);
pane.setColumnHeaderView(header);
*/
public class TableHeader
extends JComponent
{
/** TableColumn that also has a headerSpan property. */
public static class XTableColumn
extends TableColumn
{
private int headerSpan;
public XTableColumn()
{
headerSpan = 1;
}
/** number of columns that the header cell spans. */
public final int headerSpan()
{
return headerSpan;
}
/** set 'headerSpan'. requires newHeaderSpan > 0. During the
manipulation of TableColumnModels, there may be times where
a column spans more columns than there are at the right of it.
This state is only allowed during such modifications. Once they
are finished, all spans must not be too large.
XTableColumns that are hidden by the spans of their predecessors
are ignored (in the TableHeader, of course not by the JTable).
Due to the broken notification way of TableColumnModel/Table-
Column there is no way to notify the header that it must
repaint().
If firePropertyChange were not private, we could send the
following fake event
firePropertyChange("width", getWidth(), getWidth());
which would handle that (only width and preferred width
changes can reach the header).
The moral is: don't change spans later, or if, repaint the
header manually.
*/
public void setHeaderSpan(int newHeaderSpan)
{
headerSpan = newHeaderSpan;
}
}
private static TableCellRenderer staticDefaultRenderer
= new DefaultHeaderRenderer();
/** Under this key, the table header is stored in the JTable, so that
the renderer can access it. See demo renderer above.
*/
public static Object KEY = TableHeader.class;
private JTable table;
private transient TableColumnModel columns;
private TableCellRenderer defaultRenderer
= staticDefaultRenderer;
private transient Listener listener;
public TableHeader(JTable table)
{
this.table = table;
table.putClientProperty(KEY, this);
this.columns = table.getColumnModel();
this.listener = createListener();
table.addPropertyChangeListener(listener);
columns.addColumnModelListener(listener);
add(new CellRendererPane());
updateUI();
}
public void updateUI()
{
LookAndFeel.installColorsAndFont
(this, "TableHeader.background", "TableHeader.foreground",
"TableHeader.font");
LookAndFeel.installBorder(this, "TableHeader.border");
if (defaultRenderer instanceof JComponent)
((JComponent)defaultRenderer).updateUI();
revalidate(); repaint();
}
public final JTable table()
{
return table;
}
public void setTable(JTable t)
{
JTable oldTable = table;
TableColumnModel oldColumns = columns;
table.putClientProperty(KEY, null);
table.removePropertyChangeListener(listener);
columns.removeColumnModelListener(listener);
table = t;
table.putClientProperty(KEY, this);
columns = t.getColumnModel();
table.addPropertyChangeListener(listener);
columns.addColumnModelListener(listener);
revalidate(); repaint();
firePropertyChange("table", oldTable, table);
firePropertyChange("columns", oldColumns, columns);
}
public final TableColumnModel columns()
{
return columns;
}
/** For serialization, the TableCellRenderer is needed to be serializable.
*/
public void setDefaultRenderer(TableCellRenderer r)
{
TableCellRenderer oldRenderer = defaultRenderer;
defaultRenderer = r;
revalidate(); repaint();
firePropertyChange("defaultRenderer", oldRenderer, defaultRenderer);
}
public final TableCellRenderer defaultRenderer()
{
return defaultRenderer;
}
private TableCellRenderer renderer(TableColumn c)
{
TableCellRenderer result = c.getHeaderRenderer();
if (result != null)
return result;
return defaultRenderer;
}
private Component component(TableCellRenderer r, TableColumn c, int column)
{
return r.getTableCellRendererComponent
(table, c.getHeaderValue(), false, false, -1, column);
}
private Dimension size(long innerWidth)
{
Insets i = getInsets();
return new Dimension((int)Math.min(innerWidth + i.left + i.bottom, Integer.MAX_VALUE), innerHeight() + i.top + i.bottom);
}
/** Alas, this cannot be cached. */
private int innerHeight()
{
int result = 0;
int count = columns.getColumnCount();
for (int j = 0; j < count; )
{
TableColumn c = columns.getColumn(j);
int span;
if (c instanceof XTableColumn)
span = ((XTableColumn)c).headerSpan();
else
span = 1;
Component d = component(renderer(c), c, j);
result = Math.max(result, d.getPreferredSize().height);
j += span;
}
return result;
}
public Dimension getMinimumSize()
{
if (isMinimumSizeSet())
return super.getMinimumSize();
return size(minWidth(columns));
}
public Dimension getPreferredSize()
{
if (isPreferredSizeSet())
return super.getPreferredSize();
return size(preferredWidth(columns));
}
public Dimension getMaximumSize()
{
if (isMaximumSizeSet())
return super.getMaximumSize();
return size(maxWidth(columns));
}
public void paintComponent(Graphics g)
{
Insets i = getInsets();
Rectangle clip = g.getClipBounds();
CellRendererPane pane = (CellRendererPane)getComponent(0);
Rectangle r = new Rectangle();
r.x = i.left;
r.y = i.top;
r.height = getHeight() - i.top - i.bottom;
if (r.height <= 0)
return;
int count = columns.getColumnCount();
for (int j = 0; j < count; )
{
TableColumn c = columns.getColumn(j);
r.width = c.getWidth();
int span;
if (c instanceof XTableColumn)
{
span = ((XTableColumn)c).headerSpan();
if (j + span > count)
{
System.err.println("column: "+j+" span: "+span+" > "+count);
System.err.println("This state of TableColumnModel is forbidden!");
span = count - j;
}
for (int k = 1; k < span; k++)
r.width += columns.getColumn(j + k).getWidth();
}
else
{
span = 1;
}
Component d = component(renderer(c), c, j);
pane.paintComponent(g, d, this,
r.x, r.y, r.width, r.height, true);
r.x += r.width;
j += span;
}
pane.removeAll();
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
in.defaultReadObject();
listener = createListener();
table.addPropertyChangeListener(listener);
columns = table.getColumnModel();
columns.addColumnModelListener(listener);
}
protected Object clone()
throws CloneNotSupportedException
{
throw new CloneNotSupportedException();
}
private Listener createListener()
{
return new Listener();
}
private class Listener
implements TableColumnModelListener, PropertyChangeListener
{
public void propertyChange(PropertyChangeEvent e)
{
if (e.getPropertyName().equals("columnModel"))
{
TableColumnModel oldColumns = columns;
columns.removeColumnModelListener(this);
columns = table.getColumnModel();
columns.addColumnModelListener(this);
revalidate(); repaint();
firePropertyChange("columns", oldColumns, columns);
}
}
public void columnAdded(TableColumnModelEvent e)
{
revalidate(); repaint();
}
public void columnRemoved(TableColumnModelEvent e)
{
revalidate(); repaint();
}
public void columnSelectionChanged(ListSelectionEvent e)
{
}
public void columnMoved(TableColumnModelEvent e)
{
repaint();
}
public void columnMarginChanged(ChangeEvent e)
{
revalidate(); repaint();
}
}
/* Utility methods. Copied here from TableColumnModels. */
public static long minWidth(TableColumnModel columns)
{
long result = 0;
for (Enumeration e = columns.getColumns(); e.hasMoreElements();)
result += ((TableColumn)e.nextElement()).getMinWidth();
return result;
}
public static long preferredWidth(TableColumnModel columns)
{
long result = 0;
for (Enumeration e = columns.getColumns(); e.hasMoreElements();)
result += ((TableColumn)e.nextElement()).getPreferredWidth();
return result;
}
public static long maxWidth(TableColumnModel columns)
{
long result = 0;
for (Enumeration e = columns.getColumns(); e.hasMoreElements();)
result += ((TableColumn)e.nextElement()).getMaxWidth();
return result;
}
}
class TableHeaderExample
{
public static void main(String[] args)
{
DefaultTableModel data = new DefaultTableModel(10, 0);
data.addColumn("ABC");
data.addColumn("DEF");
data.addColumn("GHI");
data.addColumn("JKL");
data.addColumn("MNO");
data.addColumn("PQR");
TableColumnModel columns = new DefaultTableColumnModel();
TableHeader.XTableColumn abc = new TableHeader.XTableColumn();
abc.setHeaderValue("ABC");
abc.setHeaderSpan(2);
TableColumn ghi = new TableColumn(2);
ghi.setHeaderValue("GHI");
TableHeader.XTableColumn jkl = new TableHeader.XTableColumn();
jkl.setHeaderValue("JKL");
jkl.setHeaderSpan(3);
jkl.setModelIndex(3);
columns.addColumn(abc);
columns.addColumn(new TableColumn(1));
columns.addColumn(ghi);
columns.addColumn(jkl);
columns.addColumn(new TableColumn(4));
columns.addColumn(new TableColumn(5));
JTable table = new JTable(data, columns)
{
protected void configureEnclosingScrollPane()
{
}
};
table.setTableHeader(null);
TableHeader header = new TableHeader(table);
header.setForeground(Color.blue);
header.setFont(header.getFont().deriveFont(18.0f));
JScrollPane pane = new JScrollPane(table);
pane.setColumnHeaderView(header);
JFrame f = new JFrame();
f.setContentPane(pane);
f.pack();
f.setVisible(true);
}
}
Christian
--
... for he was wise enough to know that nothing ever happened on this
globe, for good, at which some people did not have their fill of
laughter in the outset ...
A Christmas Carol
Christian Kaufhold wrote:
> Christian Kaufhold <use...@chka.de> wrote:
> > Bruce T <btr...@contactsystems.com> wrote:
> >> Is there a way to have a the column header span more than one column in a
> >> JTable?
> Something like this:
> package de.chka.swing.experimental;
[...]
Christian,
I implemented your MultiSpanHeader, it looks very special :)
(I added some colors and my CellRenderer, too). Regarding:
> protected void configureEnclosingScrollPane()
> {
> }
> };
>
> table.setTableHeader(null);
(I have read your comment;)
It does not make a difference for me when I comment the line
table.setTableHeader(null);
out. Can you explain it a bit more detailed?
Linda
> It does not make a difference for me when I comment the line
> table.setTableHeader(null);
I could claim that I thought about it, but I actually first had the
code without the fix for JTable, and thought JTable would be smart
enough not to install the non-existant header in the scroll pane,
but it does. With the fix, of course, it never installs anything
at all.
Still setting the header to 'null' shows that the Accessible-
implementation of JTable has a bug since it assumes the header is
not null, but that is explicitly allowed; but, more importantly,
doesn't introduce an additional component in the hierarchy (and also
in the Accessible hierarchy) that doesn't reflect what's actually
shown. It is not really needed for everyday work (still a Component
is so heavyweight that it may be preferable to reduce their number).
Christian
http://www2.gol.com/users/tame/
go to the swing examples link.
-Mary
Christian Kaufhold wrote:
> I could claim that I thought about it, but I actually first had the
> code without the fix for JTable, and thought JTable would be smart
> enough not to install the non-existant header in the scroll pane,
> but it does. With the fix, of course, it never installs anything
> at all.
> Still setting the header to 'null' shows that the Accessible-
> implementation of JTable has a bug since it assumes the header is
> not null, but that is explicitly allowed; but, more importantly,
> doesn't introduce an additional component in the hierarchy (and also
> in the Accessible hierarchy) that doesn't reflect what's actually
> shown. It is not really needed for everyday work (still a Component
> is so heavyweight that it may be preferable to reduce their number).
Your explication makes it more clear to me, thank you.
Linda