具有复杂格式的 JTable

JTable with complex formatting

我需要制作一个 JTable,其中每个“单元格”都有各种单独的数据要显示。我需要为每个项目显示目标值、实际值、预计值以及三者之间的差值,包括占总价值的百分比和美元价值。附加图像是在 Excel 中完成的,但显示了我试图在 JTable 中生成的内容。

为了让事情变得更有趣,除了目标和实际值行之外,所有其他行都是由用户在 运行 时选择的table。因为他们可以通过在我的程序中选择选项来选择要显示的行。我需要能够在逐个子单元格的基础上自定义单元格底纹、字体和文本格式,我还需要在整个 table.[= 中改变边框、行高和列宽。 11=]

我还没有开始编写代码,因为我正在考虑不同的方法。我正在寻找其他人关于采用哪种方法的反馈,或者是否还有其他我应该考虑的选择。

选项 1 - 只需将单个 JTable 与自定义数据模型一起使用,但在其他方面尽可能接近默认的单元格渲染器和单元格编辑器,并使用每个单元格的边框和格式来模仿预期的外观-n-感觉。数据模型将处理 JTable 提供的行、列的转换,以及 item/value 每个单元格实际对应的内容。

选项 2 - 使用 JTable 作为 JTable 中每个单元格的单元格编辑器(table 中的 table)。

选项 3 - 使用各种标签和文本字段编写我自己的自定义单元格渲染器。

选项 4 - 是否有任何预先编写的组件可以提供我正在寻找的那种我应该考虑的灵活性?

对采取最佳方法的想法表示赞赏。

谢谢。

我选择选项 2A - 谢谢,吉尔伯特。下面的代码和附加的图像是 proof-of-concept。格式化目前还很初级,但作为一个演示项目,它可以正常工作。

package tableTest;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.text.DecimalFormat;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumnModel;


/** Listener interface between data and TableDataModel to keep things in sync. */
interface DataNotifier {
    void update();
}

class Grid {

    JFrame frame;
    /** Data array in Row,Col array order */
    MyData[][] myData;
    Double totalValue = 1234567.89d;        // just a hack for demonstration purposes
    
    public Double getTotalValue() {
        return totalValue;
    }
    
    /** Constructor */
    public Grid() {
                
        MyData[][] myData1 = {
                {new MyData(0.060, 0.052, 0.058, "Red", "Square", this), new MyData(0.040, 0.038, 0.040, "Red", "Circle", this), new MyData(0.045, 0.045, 0.045, "Red", "Triangle", this), new MyData(0.080, 0.084, 0.081, "Red", "Octagon", this)},
                {new MyData(0.160, 0.142, 0.172, "Blue", "Square", this), new MyData(0.120, 0.135, 0.123, "Blue", "Circle", this), new MyData(0.000, 0.000, 0.000, "Blue", "Triangle", this), new MyData(0.060, 0.050, 0.052, "Blue", "Octagon", this)},
                {new MyData(0.080, 0.072, 0.079, "Green", "Square", this), new MyData(0.010, 0.023, 0.011, "Green", "Circle", this), new MyData(0.090, 0.120, 0.099, "Green", "Triangle", this), new MyData(0.110, 0.105, 0.110, "Green", "Octagon", this)},
                {new MyData(0.000, 0.000, 0.000, "Pink", "Square", this), new MyData(0.080, 0.065, 0.078, "Pink", "Circle", this), new MyData(0.030, 0.010, 0.028, "Pink", "Triangle", this), new MyData(0.035, 0.060, 0.024, "Pink", "Octagon", this)},
        };
        myData = myData1;
        
        initialize();
    }
    
    /** Initialize console */
    private void initialize() {
        
        final int ROW_HEIGHT = 24;
        
        frame = new JFrame("Grid Window");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setSize(800, 800);
        frame.setLayout(new BorderLayout());
        frame.setLocationRelativeTo(null);
        
        Font defaultFont = javax.swing.UIManager.getDefaults().getFont("Label.font");
        
        // Add a control panel - allows us to initiate a change in the data.
        JPanel controlPanel = new JPanel();
        controlPanel.setLayout(new BoxLayout(controlPanel, BoxLayout.X_AXIS));
        JButton action1 = new JButton("Increase Total Value");
        action1.addActionListener(this::action1ButtonPressed);
        controlPanel.add(action1);
        
        frame.add(controlPanel, BorderLayout.NORTH);
        
        JPanel tablePanel = new JPanel();
        tablePanel.setLayout(new GridLayout(4, 4));
        
        for (int r = 0; r < 4; r++) {
            for (int c = 0; c < 4; c++) {
                
                // cellPanel represents one of our "cells" in tabelPanel - it encloses the table, header and super-header
                JPanel cellPanel = new JPanel();
                cellPanel.setLayout(new BorderLayout());
                cellPanel.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, Color.black));
                
                // Create the super-header that is the title for each cell's data
                JLabel cellLabel = new JLabel(myData[r][c].getColor() + " " + myData[r][c].getShape());
                cellLabel.setFont(defaultFont.deriveFont(Font.BOLD));
                cellLabel.setHorizontalAlignment(SwingConstants.CENTER);
                cellLabel.setPreferredSize(new Dimension(102, ROW_HEIGHT));
                cellLabel.setMinimumSize(new Dimension(102, ROW_HEIGHT));
                // Add to cellPanel
                cellPanel.add(cellLabel, BorderLayout.NORTH);
                
                // Create the table
                MyTableModel cellDataModel = new MyTableModel(myData[r][c], this);
                myData[r][c].setOurDataModel(cellDataModel);            // Gives myData a link to the TableModel for data change notifications
                MyCellRenderer cellRender = new MyCellRenderer();
                JTable cellTable = new JTable(cellDataModel);
                cellTable.setDefaultRenderer(Object.class, cellRender);
                cellTable.getTableHeader().setMinimumSize(new Dimension(102, ROW_HEIGHT));
                cellTable.setRowHeight(ROW_HEIGHT);
                cellTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
                cellTable.setMinimumSize(new Dimension(102, ROW_HEIGHT));
                TableColumnModel tcm = cellTable.getColumnModel();
                tcm.getColumn(0).setPreferredWidth(120);
                tcm.getColumn(1).setPreferredWidth(80);
                tcm.getColumn(2).setPreferredWidth(80);
                tcm.getColumn(0).setMinWidth(120);
                tcm.getColumn(1).setMinWidth(80);
                tcm.getColumn(2).setMinWidth(80);
                cellTable.setFillsViewportHeight(true);
                
                // Since we're not putting the table into a scroll pane, we need to explicitly add the header
                // or it won't show. We'll use another panel for this.
                JPanel dataPanel = new JPanel();
                dataPanel.setLayout(new BorderLayout());
                // Add the header
                dataPanel.add(cellTable.getTableHeader(), BorderLayout.NORTH);
                // Add the table
                dataPanel.add(cellTable, BorderLayout.CENTER);
                
                // Add dataPanel to the cell panel
                cellPanel.add(dataPanel, BorderLayout.CENTER);
                
                // Add cellPanel to the tablePlanel
                tablePanel.add(cellPanel);
                
            }
        }
        
        // We now have a panel with our home-made grid - add it to a scroll pane
        JScrollPane spTable = new JScrollPane(tablePanel);
        spTable.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
        spTable.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        
        frame.add(spTable, BorderLayout.CENTER);
        
    }

    public void show() {
        frame.setVisible(true);
    }
    
    /** Pressing the button just increases TotalValue by 50% - just for demo reasons. */
    private void action1ButtonPressed(ActionEvent ae) {
        totalValue *= 1.5;
        System.out.println("New value = " + Double.toString(totalValue));
        updateData();
    }
    
    /** Initiate a data update
     * <p>
     * This tells each data object that the data has changed, which in turn tells each
     * TableModel which then tells the corresponding table.
     */
    private void updateData() {
        for (int r = 0; r < 4; r++) {
            for (int c = 0; c < 4; c++) {
                myData[r][c].update();
            }
        }
    }
    
    /** Custom cell renderer */
    private class MyCellRenderer extends DefaultTableCellRenderer {
        
        DecimalFormat currencyFormatter = new DecimalFormat("$#,###.00");
        DecimalFormat percentFormatter = new DecimalFormat("0.0%");

        protected MyCellRenderer() {
            super();
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
                int row, int column) {
            
            // First, adjust the value
            String newValue;
            if (column == 0) {
                newValue = (String) value;
            } else if (column == 1) {
                newValue = currencyFormatter.format(value);
            } else {
                newValue = percentFormatter.format(value);
            }
            
            // Call super
            JComponent c = (JComponent) super.getTableCellRendererComponent(table, newValue, isSelected, hasFocus, row, column);
            JLabel cl = (JLabel) c;
            
            // Adjust formatting
            switch (column) {
            case 0:
                // row header
                cl.setBackground(Color.LIGHT_GRAY);
                cl.setHorizontalAlignment(SwingConstants.LEFT);
                break;
            case 1:
                // value column
                cl.setBackground(Color.white);
                cl.setHorizontalAlignment(SwingConstants.RIGHT);
                break;
            case 2:
                cl.setBackground(Color.white);
                cl.setHorizontalAlignment(SwingConstants.CENTER);
                break;
            default:
                // Something we don't expect
                throw new IllegalArgumentException("column unexpected.");
            }
                        
            return cl;
        }
        
    }
    
    
    private class MyTableModel extends DefaultTableModel implements DataNotifier {
        
        /** Reference to the source data */
        MyData thisData;
        /** Reference to the console app - implemented for future use */
        Grid parent;
        private static final String[] rowNames = {"Target", "Actual", "Actual - Target", "Projected", "Projected - Target", "Projected - Actual"};
        private static final String[] columnNames = {"", "Value", "%"};

        /** Constructor */
        protected MyTableModel(MyData dataItem, Grid parent) {
            super();
            thisData = dataItem;
            this.parent = parent;
        }
        
        /** TableModel's end of the listenr interface */
        public void update() {
            fireTableDataChanged();
        }
        
        @Override
        public int getRowCount() {
            return rowNames.length;
        }

        @Override
        public int getColumnCount() {
            return columnNames.length;
        }

        @Override
        public String getColumnName(int column) {
            return columnNames[column];
        }
        
        @Override
        public Class<?> getColumnClass(int col) {
            return String.class;
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }

        @Override
        public Object getValueAt(int row, int column) {
            Double ret;
            Double mult;
            
            // Evaluate the column
            switch (column) {
            case 0:
                // row header
                return rowNames[row];
            case 1:
                // value column
                mult = parent.getTotalValue();
                break;
            case 2:
                mult = 1d;
                break;
            default:
                // Something we don't expect
                throw new IllegalArgumentException("column greater than 2");
            }
            
            // Evaluate the row
            switch (row) {
            case 0:
                // Target
                ret = thisData.getTargetPct() * mult;
                break;
            case 1:
                // Actual
                ret = thisData.getActualPct() * mult;
                break;
            case 2:
                // Actual - Target
                ret = (thisData.getActualPct() - thisData.getTargetPct()) * mult;
                break;
            case 3:
                // Projected
                ret = thisData.getPlanPct() * mult;
                break;
            case 4:
                // Projected - Target
                ret = (thisData.getPlanPct() - thisData.getTargetPct()) * mult;
                break;
            case 5:
                // Projected - Actual
                ret = (thisData.getPlanPct() - thisData.getActualPct()) * mult;
                break;
            default:
                throw new IllegalArgumentException("row greater than our size");    
            }
            
            return ret;
        }

        @Override
        public void setValueAt(Object aValue, int row, int column) {
            super.setValueAt(aValue, row, column);
        }
    }
    
    /** Custom data object for our demo */
    private class MyData {
        
        Double pTarget;
        Double pActual;
        Double pPlan;
        String color;
        String shape;
        Grid parent;
        /** Data's end of the listener interface */
        DataNotifier ourDataModel;
        
        
        protected String getColor() {
            return color;
        }
        protected void setColor(String color) {
            this.color = color;
            update();
        }
        protected String getShape() {
            return shape;
        }
        protected void setShape(String shape) {
            this.shape = shape;
            update();
        }
        protected Double getTargetPct() {
            return pTarget;
        }
        protected void setTargetPct(Double pTarget) {
            this.pTarget = pTarget;
            update();
        }
        protected Double getActualPct() {
            return pActual;
        }
        protected void setActualPct(Double pActual) {
            this.pActual = pActual;
            update();
        }
        protected Double getPlanPct() {
            return pPlan;
        }
        protected void setPlanPct(Double pPlan) {
            this.pPlan = pPlan;
            update();
        }
        protected DataNotifier getOurDataModel() {
            return ourDataModel;
        }
        protected void setOurDataModel(DataNotifier ourDataModel) {
            this.ourDataModel = ourDataModel;
        }
        
        protected MyData(Double pTarget, Double pActual, Double pPlan, String color, String shape, Grid parent) {
            super();
            this.pTarget = pTarget;
            this.pActual = pActual;
            this.pPlan = pPlan;
            this.color = color;
            this.shape = shape;
            this.parent = parent;
        }
        
        /** Fire off an udpate to the listener */
        protected void update() {
            if (ourDataModel != null) ourDataModel.update();
        }
    }
}   // END OF GRID




public class TableTest {
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                Grid runwindow = new Grid();
                runwindow.show();
            }
        });
    }
}

还有一张图片

Screen shot

谢谢。