Reputation: 6532
Scenario: The user selects a tabular file for analysis, which is then loaded as a preview in the GUI using JTable. I want the user to annotate the columns prior to the analysis, but I cannot replace the column headers because that would be confusing.
My existing solution works but is very crude, as you can see in the screenshot below the positioning of the comboboxes isn't particularly well, and this gets pretty confusing when the number of columns increases to 20-30 or more.
Currently the tabbedPane has three children, the top panel that includes the label and the buttons, the middle panel that includes the comboBoxes and the table, and bottom panel that has the analysis button.
private void dataPreview(final String[][] data, String[] headers, final JTabbedPane comp) {
// Take care of column headers
if (headers.length == 0) {
headers = new String[data[1].length];
for (int i = 0; i < headers.length; i++)
headers[i] = "C" + i;
}
// Column annotations
final Dataset.ANNOT_TYPE[] annots = new Dataset.ANNOT_TYPE[headers.length];
final JComboBox<?>[] combos = new JComboBox[annots.length];
// the upper part of the panel
final PreviewPanel descPanel = new PreviewPanel(frame);
final ParamPanel paramPanel = new ParamPanel();
final JPanel upperContainer = new JPanel(new BorderLayout());
paramPanel.setVisible(false);
descPanel.setParamButtonAction(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
boolean b = paramPanel.isVisible();
paramPanel.setVisible(!b);
}
});
upperContainer.add(descPanel, BorderLayout.NORTH);
upperContainer.add(paramPanel, BorderLayout.SOUTH);
// Define table model
DataPreviewTableModel model = new DataPreviewTableModel(data, headers);
final JTable table = new JTable(model);
table.getColumnModel().getColumn(0).setPreferredWidth(25);
table.setTableHeader(new JTableHeader(table.getColumnModel()){
//Implement table header tool tips.
private static final long serialVersionUID = -7015589028339208609L;
public String getToolTipText(MouseEvent e) {
java.awt.Point p = e.getPoint();
int index = columnModel.getColumnIndexAtX(p.x);
return table.getColumnName(index);
}
});
for(int i=0; i<headers.length; i++)
table.getColumnModel().getColumn(i).setMinWidth(60);
table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
// create the combo boxes for column annotation
final JPanel comboPanel = new JPanel();
comboPanel.setBorder(new EmptyBorder(3, 0, 3, 0));
comboPanel.add(new JLabel("Columns:"));
for (int i = 0; i < combos.length; i++) {
final JComboBox<?> box = new JComboBox<Object>(Dataset.ANNOT_TYPE.values());
final int colIndex = i;
box.setMinimumSize(new Dimension(60, box.getMinimumSize().height));
box.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
int colType = box.getSelectedIndex();
table.getColumnModel().getColumn(colIndex+1)
.setCellRenderer(new CellColorRenderer(colType));
table.repaint();
}
});
comboPanel.add(box);
combos[i] = box;
}
final JPanel middlePanel = new JPanel(new BorderLayout());
middlePanel.add(comboPanel, BorderLayout.NORTH);
middlePanel.add(new JScrollPane(table), BorderLayout.CENTER);
JPanel lowerPanel = new JPanel(new BorderLayout());
final JButton analyzeButton = new JButton("Analyze Dataset!");
lowerPanel.add(analyzeButton, BorderLayout.LINE_END);
final JPanel container = new JPanel(new BorderLayout());
container.add(upperContainer, BorderLayout.NORTH);
container.add(new JScrollPane(middlePanel), BorderLayout.CENTER);
container.add(lowerPanel, BorderLayout.SOUTH);
comp.addTab("Preview", container);
Questions:
I looked at [JTableHeader.getHeaderRect()][2]
as advised here but I am not sure how I can place the combos according to the x,y coordinates of the header rectangles, seeing as they are in different panels.
Upvotes: 1
Views: 616
Reputation: 324118
I created a simple layout based on the TableColumnModel
of the JTable
. You just add your component to the panel and specify the column the component should be positioned above (or below if you add the panel below the table):
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ChangeEvent;
import javax.swing.table.TableColumnModel;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.table.TableColumn;
import javax.swing.*;
/**
*/
public class TableColumnModelLayout
implements LayoutManager2, java.io.Serializable, TableColumnModelListener
{
// Track a constraint added to a component
private HashMap<Component, Integer> constraints = new HashMap<Component, Integer>();
private TableColumnModel model;
private JPanel container;
/**
* Convenience constructor to provide for "stacking" of components. Each
* component will be stacked above the previous component and sized to
* fill the space of the parent container.
*/
public TableColumnModelLayout(TableColumnModel model, JPanel container)
{
this.model = model;
this.container = container;
model.addColumnModelListener( this );
}
/**
* Gets the constraints for the specified component.
*
* @param component the component to be queried
* @return the constraint for the specified component, or null
* if component is null or is not present in this layout
*/
public Integer getConstraints(Component component)
{
return (Integer)constraints.get(component);
}
/**
* Adds the specified component with the specified name to the layout.
* @param name the name of the component
* @param comp the component to be added
*/
public void addLayoutComponent(String name, Component comp) {}
/*
* Keep track of any specified constraint for the component.
*/
public void addLayoutComponent(Component component, Object constraint)
{
if (constraint == null)
{
constraints.remove(component);
}
else if (constraint instanceof Integer)
{
Integer column = (Integer)constraint;
if (column >= 0 && column < model.getColumnCount())
{
constraints.put(component, (Integer)constraint);
}
else
{
String message = "Invalid column specified: " + column;
throw new IllegalArgumentException( message );
}
}
else
{
String message = "Constraint parameter must be of type Integer";
throw new IllegalArgumentException( message );
}
}
/**
* Removes the specified component from the layout.
*
* @param comp the component to be removed
*/
public void removeLayoutComponent(Component component)
{
constraints.remove( component );
}
/**
* Determine the minimum size on the Container
*
* @param target the container in which to do the layout
* @return the minimum dimensions needed to lay out the
* subcomponents of the specified container
*/
public Dimension minimumLayoutSize(Container parent)
{
return preferredLayoutSize(parent);
}
/**
* Determine the preferred size on the Container
*
* @param parent the container in which to do the layout
* @return the preferred dimensions to lay out the
* subcomponents of the specified container
*/
public Dimension preferredLayoutSize(Container parent)
{
synchronized (parent.getTreeLock())
{
int width = 0;
for (int i = 0; i < model.getColumnCount(); i++)
{
TableColumn tc = model.getColumn(i);
width += tc.getWidth();
}
int height = 0;
for (Component component: parent.getComponents())
{
height = Math.max(height, component.getPreferredSize().height);
}
Insets insets = parent.getInsets();
width += insets.left + insets.right;
height += insets.top + insets.bottom;
return new Dimension(width, height);
}
}
/**
* Lays out the specified container using this layout.
* <p>
*
* @param target the container in which to do the layout
*/
public void layoutContainer(Container parent)
{
synchronized (parent.getTreeLock())
{
Insets insets = parent.getInsets();
int offset = insets.left;
int[] offsets = new int[model.getColumnCount()];
for (int i = 0; i < model.getColumnCount(); i++)
{
offsets[i] = offset;
TableColumn tc = model.getColumn(i);
offset += tc.getWidth();
}
for (Component component: parent.getComponents())
{
Dimension preferred = component.getPreferredSize();
Integer column = constraints.get(component);
int width = model.getColumn(column).getWidth();
component.setBounds(offsets[column], insets.top, width, preferred.height);
}
}}
/**
* There is no maximum.
*/
public Dimension maximumLayoutSize(Container target)
{
return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
/**
* Returns the alignment along the x axis. Use center alignment.
*/
public float getLayoutAlignmentX(Container parent)
{
return 0.0f;
}
/**
* Returns the alignment along the y axis. Use center alignment.
*/
public float getLayoutAlignmentY(Container parent)
{
return 0.5f;
}
/**
* Invalidates the layout, indicating that if the layout manager
* has cached information it should be discarded.
*/
public void invalidateLayout(Container target)
{
// remove constraints here?
}
/**
* Returns the string representation of this column layout's values.
* @return a string representation of this grid layout
*/
public String toString()
{
return getClass().getName();
}
// Implement TableColumnModelListener
public void columnMarginChanged(ChangeEvent e)
{
/*
for (int i = 0; i < tcm.getColumnCount(); i++)
{
TableColumn tc = tcm.getColumn(i);
Component c = header.getComponent(i);
rl.addLayoutComponent(c, new Float(tc.getWidth()));
}
header.revalidate();
*/
container.revalidate();
}
public void columnAdded(TableColumnModelEvent e) {}
public void columnMoved(TableColumnModelEvent e) {}
public void columnRemoved(TableColumnModelEvent e) {}
public void columnSelectionChanged(ListSelectionEvent e) {}
private static void createAndShowGUI()
{
JTable table = new JTable(5, 5);
JScrollPane scrollPane1 = new JScrollPane( table );
table.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
JPanel header = new JPanel();
TableColumnModelLayout layout = new TableColumnModelLayout(table.getColumnModel(), header);
header.setLayout( layout );
header.add(new JLabel("Column 0"), new Integer(0));
JLabel label2 = new JLabel("Column 2");
label2.setHorizontalAlignment(JLabel.RIGHT);
header.add(label2, new Integer(2));
header.add(new JLabel("Column 4"), new Integer(4));
JScrollPane scrollPane2 = new JScrollPane( header );
scrollPane2.setHorizontalScrollBarPolicy( JScrollPane.HORIZONTAL_SCROLLBAR_NEVER );
scrollPane2.getHorizontalScrollBar().setModel( scrollPane1.getHorizontalScrollBar().getModel() );
JFrame frame = new JFrame("Table Column Model Layout");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(scrollPane2, BorderLayout.PAGE_START);
frame.add(scrollPane1, BorderLayout.CENTER);
frame.setLocationByPlatform( true );
frame.pack();
frame.setVisible( true );
}
public static void main(String[] args)
{
EventQueue.invokeLater( () -> createAndShowGUI() );
/*
EventQueue.invokeLater(new Runnable()
{
public void run()
{
createAndShowGUI();
}
});
*/
}
}
The component will grow/shrink as the TableColumn
is resized.
The component is displayed at the left edge of the column. You may want to modify that to center the component relative to the column?
Upvotes: 2
Reputation: 205785
It may help to note that comboPanel
is a JPanel
having the default FlowLayout
, which centers components based on the preferred sizes of the comboboxes. As a result, they "clump" together in the middle. Some alternatives:
Specify a GridLayout
having an extra column and use an empty component for the check column. The initial tableau will be aligned, although subsequent changes to the column widths will change that.
JPanel comboPanel = new JPanel(new GridLayout(0, annots.length + 1));
Add a combobox to each relevant header using the approach shown here, being mindful of the caveats adduced here.
Upvotes: 2