posdef
posdef

Reputation: 6532

Positioning components according to the column coordinates of a JTable

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 enter image description here 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:

  1. Is this a reasonable setup, if so, how can I position my combo-boxes better?
  2. If the existing design is sub-optimal, how can I improve it without having to redo the whole thing?

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

Answers (2)

camickr
camickr

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

trashgod
trashgod

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

Related Questions