如何在Swing的JTable中合并两个列标题

5

我需要在Swing中制作像这样的GUI,您能帮助我吗?是否有一种方法可以通过列和行来合并列标题?

enter image description here


2
请查看这里:链接 - Salah
在你提问之前,一定要,一定要,非常务必在 Stack Overflow、Google 等搜索引擎中寻找答案。很有可能在世界的某个角落,某段时间,已经有人遇到了和你一样的问题。 - MarsAtomic
1
这是一个非常常见的问题,而且非常难以很好地实现。您可以查看来获取示例... - MadProgrammer
这个问题经常被问到。以前的解决方案都是部分的:固定列或者只有基本的外观,或者只有2行,或者需要商业许可证。现在有一个免费的解决方案,支持L&F,能够移动列和任意数量的行。我会留下一个链接给未来的搜索者这个答案 - Qualtagh
2个回答

6

您想要一个多标题行的JTable。您需要对标题进行分组。

GroupableHeaderExample.java

public class GroupableHeaderExample extends JFrame {

  GroupableHeaderExample() {
    super( "Groupable Header Example" );

    DefaultTableModel dm = new DefaultTableModel();
    dm.setDataVector(new Object[][]{
      {"119","foo","bar","ja","ko","zh"},
      {"911","bar","foo","en","fr","pt"}},
    new Object[]{"SNo.","1","2","Native","2","3"});

    JTable table = new JTable( dm ) {
      protected JTableHeader createDefaultTableHeader() {
          return new GroupableTableHeader(columnModel);
      }
    };


    TableColumnModel cm = table.getColumnModel();
    ColumnGroup g_name = new ColumnGroup("Name");
    g_name.add(cm.getColumn(1));
    g_name.add(cm.getColumn(2));
    ColumnGroup g_lang = new ColumnGroup("Language");
    g_lang.add(cm.getColumn(3));
    ColumnGroup g_other = new ColumnGroup("Others");
    g_other.add(cm.getColumn(4));
    g_other.add(cm.getColumn(5));
    g_lang.add(g_other);

    GroupableTableHeader header = (GroupableTableHeader)table.getTableHeader();
    header.addColumnGroup(g_name);
    header.addColumnGroup(g_lang);
    JScrollPane scroll = new JScrollPane( table );
    getContentPane().add( scroll );
    setSize( 400, 120 );   
  }

  public static void main(String[] args) {
    GroupableHeaderExample frame = new GroupableHeaderExample();
    frame.addWindowListener( new WindowAdapter() {
      public void windowClosing( WindowEvent e ) {
  System.exit(0);
      }
    });
    frame.setVisible(true);
  }
}

ColumnGroup.java

public class ColumnGroup {

    protected TableCellRenderer renderer;

    protected List<TableColumn> columns;
    protected List<ColumnGroup> groups;

    protected String text;
    protected int margin = 0;

    public ColumnGroup(String text) {
        this(text, null);
    }

    public ColumnGroup(String text, TableCellRenderer renderer) {
        this.text = text;
        this.renderer = renderer;
        this.columns = new ArrayList<TableColumn>();
        this.groups = new ArrayList<ColumnGroup>();
    }

    public void add(TableColumn column) {
        columns.add(column);
    }

    public void add(ColumnGroup group) {
        groups.add(group);
    }

    /**
     * @param column
     *            TableColumn
     */
    public List<ColumnGroup> getColumnGroups(TableColumn column) {
        if (!contains(column)) {
            return Collections.emptyList();
        }
        List<ColumnGroup> result = new ArrayList<ColumnGroup>();
        result.add(this);
        if (columns.contains(column)) {
            return result;
        }
        for (ColumnGroup columnGroup : groups) {
            result.addAll(columnGroup.getColumnGroups(column));
        }
        return result;
    }

    private boolean contains(TableColumn column) {
        if (columns.contains(column)) {
            return true;
        }
        for (ColumnGroup group : groups) {
            if (group.contains(column)) {
                return true;
            }
        }
        return false;
    }

    public TableCellRenderer getHeaderRenderer() {
        return renderer;
    }

    public void setHeaderRenderer(TableCellRenderer renderer) {
        this.renderer = renderer;
    }

    public String getHeaderValue() {
        return text;
    }

    public Dimension getSize(JTable table) {
        TableCellRenderer renderer = this.renderer;
        if (renderer == null) {
            renderer = table.getTableHeader().getDefaultRenderer();
        }
        Component comp = renderer.getTableCellRendererComponent(table, getHeaderValue() == null || getHeaderValue().trim().isEmpty() ? " "
                : getHeaderValue(), false, false, -1, -1);
        int height = comp.getPreferredSize().height;
        int width = 0;
        for (ColumnGroup columnGroup : groups) {
            width += columnGroup.getSize(table).width;
        }
        for (TableColumn tableColumn : columns) {
            width += tableColumn.getWidth();
            width += margin;
        }
        return new Dimension(width, height);
    }

    public void setColumnMargin(int margin) {
        this.margin = margin;
        for (ColumnGroup columnGroup : groups) {
            columnGroup.setColumnMargin(margin);
        }
    }

}

GroupableTableHeader.java

@SuppressWarnings("serial")
public class GroupableTableHeader extends JTableHeader {

    @SuppressWarnings("unused")
    private static final String uiClassID = "GroupableTableHeaderUI";

    protected List<ColumnGroup> columnGroups = new ArrayList<ColumnGroup>();

    public GroupableTableHeader(TableColumnModel model) {
        super(model);
        setUI(new GroupableTableHeaderUI());
        setReorderingAllowed(false);
        // setDefaultRenderer(new MultiLineHeaderRenderer());
    }

    @Override
    public void updateUI() {
        setUI(new GroupableTableHeaderUI());
    }

    @Override
    public void setReorderingAllowed(boolean b) {
        super.setReorderingAllowed(false);
    }

    public void addColumnGroup(ColumnGroup g) {
        columnGroups.add(g);
    }

    public List<ColumnGroup> getColumnGroups(TableColumn col) {
        for (ColumnGroup group : columnGroups) {
            List<ColumnGroup> groups = group.getColumnGroups(col);
            if (!groups.isEmpty()) {
                return groups;
            }
        }
        return Collections.emptyList();
    }

    public void setColumnMargin() {
        int columnMargin = getColumnModel().getColumnMargin();
        for (ColumnGroup group : columnGroups) {
            group.setColumnMargin(columnMargin);
        }
    }

}

GroupableTableHeaderUI.java

public class GroupableTableHeaderUI extends BasicTableHeaderUI {

    protected GroupableTableHeader getHeader() {
        return (GroupableTableHeader) header;
    }

    @Override
    public void paint(Graphics g, JComponent c) {
        Rectangle clipBounds = g.getClipBounds();
        if (header.getColumnModel().getColumnCount() == 0) {
            return;
        }
        int column = 0;
        Dimension size = header.getSize();
        Rectangle cellRect = new Rectangle(0, 0, size.width, size.height);
        Map<ColumnGroup, Rectangle> groupSizeMap = new HashMap<ColumnGroup, Rectangle>();

        for (Enumeration<TableColumn> enumeration = header.getColumnModel().getColumns(); enumeration.hasMoreElements();) {
            cellRect.height = size.height;
            cellRect.y = 0;
            TableColumn aColumn = enumeration.nextElement();
            List<ColumnGroup> groups = getHeader().getColumnGroups(aColumn);
            int groupHeight = 0;
            for (ColumnGroup group : groups) {
                Rectangle groupRect = groupSizeMap.get(group);
                if (groupRect == null) {
                    groupRect = new Rectangle(cellRect);
                    Dimension d = group.getSize(header.getTable());
                    groupRect.width = d.width;
                    groupRect.height = d.height;
                    groupSizeMap.put(group, groupRect);
                }
                paintCell(g, groupRect, group);
                groupHeight += groupRect.height;
                cellRect.height = size.height - groupHeight;
                cellRect.y = groupHeight;
            }
            cellRect.width = aColumn.getWidth();
            if (cellRect.intersects(clipBounds)) {
                paintCell(g, cellRect, column);
            }
            cellRect.x += cellRect.width;
            column++;
        }
    }

    private void paintCell(Graphics g, Rectangle cellRect, int columnIndex) {
        TableColumn aColumn = header.getColumnModel().getColumn(columnIndex);
        TableCellRenderer renderer = aColumn.getHeaderRenderer();
        if (renderer == null) {
            renderer = getHeader().getDefaultRenderer();
        }
        Component c = renderer.getTableCellRendererComponent(header.getTable(), aColumn.getHeaderValue(), false, false,
                -1, columnIndex);

        c.setBackground(UIManager.getColor("control"));

        rendererPane.paintComponent(g, c, header, cellRect.x, cellRect.y, cellRect.width, cellRect.height, true);
    }

    private void paintCell(Graphics g, Rectangle cellRect, ColumnGroup cGroup) {
        TableCellRenderer renderer = cGroup.getHeaderRenderer();
        if (renderer == null) {
            renderer = getHeader().getDefaultRenderer();
        }

        Component component = renderer.getTableCellRendererComponent(header.getTable(), cGroup.getHeaderValue(), false,
                false, -1, -1);
        rendererPane
                .paintComponent(g, component, header, cellRect.x, cellRect.y, cellRect.width, cellRect.height, true);
    }

    private int getHeaderHeight() {
        int headerHeight = 0;
        TableColumnModel columnModel = header.getColumnModel();
        for (int column = 0; column < columnModel.getColumnCount(); column++) {
            TableColumn aColumn = columnModel.getColumn(column);
            TableCellRenderer renderer = aColumn.getHeaderRenderer();
            if (renderer == null) {
                renderer = getHeader().getDefaultRenderer();
            }

            Component comp = renderer.getTableCellRendererComponent(header.getTable(), aColumn.getHeaderValue(), false,
                    false, -1, column);
            int cHeight = comp.getPreferredSize().height;
            List<ColumnGroup> groups = getHeader().getColumnGroups(aColumn);
            for (ColumnGroup group : groups) {
                cHeight += group.getSize(header.getTable()).height;
            }
            headerHeight = Math.max(headerHeight, cHeight);
        }
        return headerHeight;
    }

    @Override
    public Dimension getPreferredSize(JComponent c) {
        int width = 0;
        for (Enumeration<TableColumn> enumeration = header.getColumnModel().getColumns(); enumeration.hasMoreElements();) {
            TableColumn aColumn = enumeration.nextElement();
            width += aColumn.getPreferredWidth();
        }
        return createHeaderSize(width);
    }

    private Dimension createHeaderSize(int width) {
        TableColumnModel columnModel = header.getColumnModel();
        width += columnModel.getColumnMargin() * columnModel.getColumnCount();
        if (width > Integer.MAX_VALUE) {
            width = Integer.MAX_VALUE;
        }
        return new Dimension(width, getHeaderHeight());
    }

}

您可以在分组(Group)头部示例中找到此内容。

1
已经被连接3次,这会破坏常规JTable的默认行排序和列拖动支持,更不用说行标题的默认外观。您也可以查看“初始线程”和“JFrame#setDefaultCloseOperation” - 我知道我在挑剔,为此我道歉,但事实就是如此... - MadProgrammer
请问您如何添加带有按钮的列组? - Ebin James
这个解决方案来自1998年,对于非常大的表格也非常缓慢。 - Asu

1

这是完美工作的代码:

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);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接