Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Footer for JTable

Tags:

java

swing

jtable

JTable lacks support for showing a footer with aggregated data for each column. Inspired by a suggested solution at Oracle/Suns bug database that looked promising, I started with the approach that the footer is painted by the border of the scrollpane.

Now at first I just want a 'proof of concept' and it seems I'm almost there, each column have a footer, widths are in sync. etc. So it seems to work pretty good except one thing, no text is painted! I expect the dummy values returned from getFooterValueAt painted on each "footer cell" but they're all blank, only the background (just for testing) is painted.

Why isn't any text painted? Is there something wrong with my position/size calculations in paintFooter?

import static java.awt.BorderLayout.CENTER;
import static java.awt.Color.BLUE;
import static java.awt.Color.CYAN;
import static java.awt.Color.GREEN;
import static java.awt.Color.LIGHT_GRAY;
import static java.awt.Color.MAGENTA;
import static java.awt.Color.ORANGE;
import static java.awt.Color.PINK;
import static java.awt.Color.RED;
import static java.awt.Color.WHITE;
import static java.awt.Color.YELLOW;
import static javax.swing.JTable.AUTO_RESIZE_OFF;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;

import javax.swing.CellRendererPane;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;

/**
 * Demo application for JTable with footer.
 * 
 * @author Martin Uhlén
 */
public class TableFooterBorderDemo extends JFrame
{   
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                new TableFooterBorderDemo().setVisible(true);
            }
        });
    }

    private TableFooterBorderDemo()
    {
        setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        setLayout(new BorderLayout());

        JTable table = createTable();
        JScrollPane scroll = new JScrollPane(table);
        add(scroll, CENTER);
        TableFooter.install(scroll, table);
        pack();
        positionAtMiddleOfScreen();
    }

    private void positionAtMiddleOfScreen()
    {
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        setLocation((int) ((screenSize.getWidth() / 2) - (getWidth() / 2)), 
                    (int) ((screenSize.getHeight() / 2) - (getHeight() / 2)));
    }

    private JTable createTable()
    {
        Object[][] data = new Object[][]
        {
            { "A", "*", "*", "*", "*", "*" },
            { "*", "B", "*", "*", "*", "*" }, 
            { "*", "*", "C", "*", "*", "*" },
            { "*", "*", "*", "D", "*", "*" }, 
            { "*", "*", "*", "*", "E", "*" },
            { "*", "*", "*", "*", "*", "F" } 
        };
        Object[] columns = new Object[] { "A", "B", "C", "D", "E", "F" };
        DefaultTableModel model = new DefaultTableModel(data, columns);
        JTable table = new JTable(model);
        table.setAutoResizeMode(AUTO_RESIZE_OFF);
        return table;
    }

    /**
     * A Border for JScrollPane that paints a footer for JTable.
     */
    private static class TableFooter implements Border
    {
        private static final Color[] COLORS = {RED, GREEN, BLUE, YELLOW, PINK, CYAN, LIGHT_GRAY, MAGENTA, ORANGE, WHITE};

        private final JScrollPane scroll;
        private final JTable table;
        private final CellRendererPane cellRendererPane;

        TableFooter(JScrollPane scroll, JTable table)
        {
            this.scroll = scroll;
            this.table = table;
            cellRendererPane = new CellRendererPane();
        }

        public static TableFooter install(JScrollPane scroll, JTable table)
        {
            verify(scroll, table);
            TableFooter footer = new TableFooter(scroll, table);

            RepaintListener repainter = new RepaintListener(scroll);
            scroll.getViewport().addChangeListener(repainter);
            scroll.getHorizontalScrollBar().addAdjustmentListener(repainter);
            table.getColumnModel().addColumnModelListener(repainter);
            scroll.setViewportBorder(footer);
            return footer;
        }

        private static void verify(JScrollPane scroll, JTable table)
        {
            if (scroll.getViewport().getView() != table)
            {
                throw new IllegalArgumentException("Given table must be inside given scroll pane");
            }
        }

        /**
         * @see javax.swing.border.Border#paintBorder(java.awt.Component, java.awt.Graphics, int, int, int, int)
         */
        @Override
        public void paintBorder(Component c, Graphics g, int x, int y, int width, int height)
        {
            System.out.println("x: " + x + ", y: " + y + ", width: " + width + ", height: " + height);
            System.out.println("viewRect: " + scroll.getViewport().getViewRect());
            paintFooter(g, x, y, width, height);
        }

        private void paintFooter(Graphics g, int x, int y, int width, int height)
        {
            Color oldColor = null;
            Component cellRendererComponent = null;
            int columnWidths = x - scroll.getViewport().getViewRect().x;
            for (int column = 0; column < table.getColumnCount(); column++)
            {
                TableCellRenderer cellRenderer = table.getCellRenderer(0, column);
                cellRendererComponent = cellRenderer.getTableCellRendererComponent(table, getFooterValueAt(column), false, false, 0, column);
                if (oldColor == null)
                {
                    oldColor = cellRendererComponent.getBackground();
                }
                int columnWidth = table.getColumnModel().getColumn(column).getWidth();
                cellRendererComponent.setBackground(COLORS[column % COLORS.length]);
                cellRendererPane.paintComponent(g, cellRendererComponent, scroll, columnWidths, y, columnWidth, height);
                columnWidths += columnWidth;
            }
            if (cellRendererComponent != null)
            {
                cellRendererComponent.setBackground(oldColor);
            }
        }

        private Object getFooterValueAt(int viewColumn)
        {
            return "Column " + viewColumn;
        }

        @Override
        public Insets getBorderInsets(Component c)
        {
            return new Insets(0, 0, table.getRowHeight(), 0);
        }

        @Override
        public boolean isBorderOpaque()
        {
            return true;
        }
    }

    /**
     * Repaints JScrollPane when needed.
     */
    private static class RepaintListener implements ChangeListener, AdjustmentListener, TableColumnModelListener
    {
        private final JScrollPane scroll;

        RepaintListener(JScrollPane scroll)
        {
            this.scroll = scroll;
        }

        @Override
        public void columnAdded(TableColumnModelEvent e)
        {
            repaint();
        }

        @Override
        public void columnRemoved(TableColumnModelEvent e)
        {
            repaint();
        }

        @Override
        public void columnMoved(TableColumnModelEvent e)
        {
            repaint();
        }

        @Override
        public void columnMarginChanged(ChangeEvent e)
        {
            repaint();
        }

        @Override
        public void columnSelectionChanged(ListSelectionEvent e)
        {
            repaint();
        }

        @Override
        public void adjustmentValueChanged(AdjustmentEvent e)
        {
            repaint();
        }

        @Override
        public void stateChanged(ChangeEvent e)
        {
            repaint();
        }

        private void repaint()
        {
            scroll.repaint();
        }   
    }
}
like image 968
Uhlen Avatar asked Jan 18 '23 14:01

Uhlen


2 Answers

The problem is the location of the rendererPane: what happens in your code is that the label is located at x/y, that is near the upper borderline having the complete height of the viewport rect, then the text is painted somewhere in the middle of the viewport, and later overpainted by the viewport. The see that effect, use a zero row tableModel and make the viewport not-opaque.

Instead you have to position it at the lower bottom of the border.. A very dirty (did nothing to get it pixel-correct, on the run into the weekend :) snippet

        g.setColor(Color.RED);
        int scrollBottom = scroll.getInsets().bottom;
        int lowerBorderTop = height - scrollBottom;
        g.drawLine(x, height - scrollBottom - 1, width - 10,  height - scrollBottom - 1);
        g.drawRect(x + 1,  height - scrollBottom, width - 10, 10);
        Color oldColor = null;
        Component cellRendererComponent = null;
        int columnWidths = x ;//- scroll.getViewport().getViewRect().x;
        for (int column = 0; column < table.getColumnCount(); column++)
        {
            TableCellRenderer cellRenderer = table.getCellRenderer(0, column);
            cellRendererComponent = cellRenderer.getTableCellRendererComponent(table, 
                    getFooterValueAt(column), false, false, 0, column);
            if (oldColor == null)
            {
                oldColor = cellRendererComponent.getBackground();
            }
            int columnWidth = table.getColumnModel().getColumn(column).getWidth();
            cellRendererComponent.setForeground(Color.BLACK);
            cellRendererComponent.setBackground(COLORS[column % COLORS.length]);
            cellRendererPane.paintComponent(g, cellRendererComponent, scroll, columnWidths, 
                  lowerBorderTop , columnWidth, table.getRowHeight(), false);
            columnWidths += columnWidth;
        }
        if (cellRendererComponent != null)
        {
            cellRendererComponent.setBackground(oldColor);
        }

the additional line and rectangle is just to see where we are. BTW, nice approach :-)

like image 57
kleopatra Avatar answered Jan 20 '23 03:01

kleopatra


  • place JComponent/JTextComponents to JVievport (advantage take MouseEvents)

  • place JComponent/JTextComponents to Glasspane (advantage take MouseEvents)

  • place JComponent/JTextComponents to JPanel, then this JPanel to the JScrollPane, by implements ComponentListener you can resize JPanel to the JViewport's Dimension only is required resize smaller JPanel upto the JViewport's Dimension

  • in all case required add own Borders to JScrollPane and with same for "TableFooter"

  • is possible wrote I don't know how to call it ????

simpliest way should be workaround for two JTables or example by @camickr

enter image description here

import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class TableFilterRow extends JFrame implements TableColumnModelListener {
    private static final long serialVersionUID = 1L;

    private JTable table;
    private JPanel filterRow;

    public TableFilterRow() {
        table = new JTable(3, 5);
        table.setPreferredScrollableViewportSize(table.getPreferredSize());
        JScrollPane scrollPane = new JScrollPane(table);
        getContentPane().add(scrollPane);
        table.getColumnModel().addColumnModelListener(this);
        //  Panel for text fields
        filterRow = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
        for (int i = 0; i < table.getColumnCount(); i++) {
            filterRow.add(new JTextField(" Sum at - " + i));
        }
        columnMarginChanged(new ChangeEvent(table.getColumnModel()));
        getContentPane().add(filterRow, BorderLayout.SOUTH);
    }

    //  Implement TableColumnModelListener methods
    //  (Note: instead of implementing a listener you should be able to
    //  override the columnMarginChanged and columMoved methods of JTable)
    @Override
    public void columnMarginChanged(ChangeEvent e) {
        TableColumnModel tcm = table.getColumnModel();
        int columns = tcm.getColumnCount();

        for (int i = 0; i < columns; i++) {
            JTextField textField = (JTextField) filterRow.getComponent(i);
            Dimension d = textField.getPreferredSize();
            d.width = tcm.getColumn(i).getWidth();
            textField.setPreferredSize(d);
        }

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                filterRow.revalidate();
            }
        });
    }

    @Override
    public void columnMoved(TableColumnModelEvent e) {
        Component moved = filterRow.getComponent(e.getFromIndex());
        filterRow.remove(e.getFromIndex());
        filterRow.add(moved, e.getToIndex());
        filterRow.validate();
    }

    @Override
    public void columnAdded(TableColumnModelEvent e) {
    }

    @Override
    public void columnRemoved(TableColumnModelEvent e) {
    }

    @Override
    public void columnSelectionChanged(ListSelectionEvent e) {
    }

    public static void main(String[] args) {
        JFrame frame = new TableFilterRow();
        frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}
like image 23
mKorbel Avatar answered Jan 20 '23 02:01

mKorbel