Search code examples
javaswingnullpointerexceptiontablecellrenderer

JTable with nonuniform Object Array, return of Custom TableCellRenderer implementation


I have Custom Components extended from JPanel. The name are: PanelButton and PanelSlider

QUESTION 1:

Is it valid (or secure) to create a xxxTableModel(...) with nonuniform Matrix?

String[] hdrsObjects = {"PanelButton Class", "PanelSlider Class"};
Object[][] objectMatrix = new Object[3][2];
objectMatrix[0][0] = new PanelButtonData(...);
objectMatrix[1][0] = new PanelButtonData(...);
objectMatrix[2][0] = new PanelButtonData(...);

// objectMatrix[0][1] = /*Non Assigned*/
objectMatrix[1][1] = new PanelSliderData(0, 20, 40);
objectMatrix[2][1] = new PanelSliderData(30, 40, 60);

JTable Mytable = new JTable(new MyTableModel(objectMatrix, hdrsObjects)) {...}

That's equivalent to 3 Rows with different lengths:

jtblGeneral.setModel(new DefaultTableModel(
  new Object [][] { {"Cell Row:0,Col:0"}, {"Cell Row:1,Col:0", "Cell Row:1,Col:1"}, {"Cell Row:2,Col:0", "Cell Row:2,Col:1"}
  },
  new String [] {
    "Title 1", "Title 2"
  }
));

Now I'm implementing my own TableCellRenderer

class MyTableCellRenderer implements TableCellRenderer {

  private final PanelSlider ps = new PanelSlider(new PanelSliderData(0, 25, 50));
  private final PanelButton pb = new PanelButton(new PanelButtonData(false));

  @Override public Component getTableCellRendererComponent(
      JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {

    if (value instanceof PanelButtonData) {
      pb.setData((PanelButtonData) value);
      return pb;
    }

    if (value instanceof PanelSliderData) {
      ps.setData((PanelSliderData) value);
      return ps;
    }

  //if (value != null)
  //  return (Component) value;
  //return this;

  //return null;

  //    return table.getDefaultRenderer(String.class).getTableCellRendererComponent(
  //        table, value, isSelected, hasFocus, row, column);

    return new JLabel();
  }
}

QUESTION 2:

If Answer is YES for before Question. When the value is null and not defined (like cell in row:0, col:1) in my custom JPanel Classes, what Type Object I must return?

  • return null; When I return null (I will have problem with Nimbus and GTK Look And Feels) UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel"); or UIManager.setLookAndFeel("com.sun.java.swing.plaf.gtk.GTKLookAndFeel");

Here the Exception java.lang.NullPointerException:

  Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
    at javax.swing.plaf.synth.SynthTableUI.paintCell(SynthTableUI.java:684)
    at javax.swing.plaf.synth.SynthTableUI.paintCells(SynthTableUI.java:580)
    at javax.swing.plaf.synth.SynthTableUI.paint(SynthTableUI.java:364)
    at javax.swing.plaf.synth.SynthTableUI.update(SynthTableUI.java:275)
    at javax.swing.JComponent.paintComponent(JComponent.java:780)
    at javax.swing.JComponent.paint(JComponent.java:1056)
  • return new JLabel();
  • return new Component();

QUESTION 3:

But, When the value Is not null and I don't know the Class type, How handle the return?

  • if (value != null) return (Component) value;
  • return table.getDefaultRenderer(String.class).getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
  • extend my MyTableCellRenderer class of Component and return this;

ALL CODE

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

public class TableButtonSlider extends JFrame {

  public TableButtonSlider()  {
    setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    setSize(600, 300);
    setVisible(true);
    setLocationRelativeTo(null);
  }
  public static void setLAF(Container container, String laf) {
    try {
      UIManager.setLookAndFeel(laf);
    } catch (ClassNotFoundException | InstantiationException
        | IllegalAccessException | UnsupportedLookAndFeelException e) {
    }
    SwingUtilities.updateComponentTreeUI(container);
  }
  static final JFrame frame = new JFrame();
  public JComponent makeUI() {

    String[] hdrsObjects = {"PanelButton Class", "PanelSlider Class"};
    Object[][] objectMatrix = new Object[3][2];
    objectMatrix[0][0] = new PanelButtonData(true);
    objectMatrix[1][0] = new PanelButtonData(false);
    objectMatrix[2][0] = new PanelButtonData(false);

//    objectMatrix[0][1] = new PanelSliderData(10, 30, 40);
    objectMatrix[1][1] = new PanelSliderData(0, 20, 40);
    objectMatrix[2][1] = new PanelSliderData(30, 40, 60);

    JTable Mytable = new JTable(new MyTableModel(objectMatrix, hdrsObjects)) {
      @Override public void updateUI() {
        super.updateUI();
        setRowHeight(30);
        TableColumn tc;

        tc = getColumn("PanelSlider Class");
        tc.setCellRenderer(new MyTableCellRenderer());
        tc.setCellEditor(new MyTableCellEditor());
        tc = getColumn("PanelButton Class");
        tc.setCellRenderer(new MyTableCellRenderer());
        tc.setCellEditor(new MyTableCellEditor());
      }
    };

    JScrollPane scrollPane = new JScrollPane(Mytable);

    JPanel pH = new JPanel();
    pH.setLayout(new BoxLayout(pH, BoxLayout.LINE_AXIS));

    JPanel pV = new JPanel();
    pV.setLayout(new BoxLayout(pV, BoxLayout.PAGE_AXIS));

    JButton bInsert = new JButton("Insert");
    bInsert.addActionListener((ActionEvent e) -> {
      ((MyTableModel)Mytable.getModel()).addRow(
        new Object[] {
          new PanelButtonData(false),
          new PanelSliderData(0, 25, 50)
        }
      );
      Mytable.updateUI();
    });

    JButton bMetal = new JButton("Metal");
    bMetal.addActionListener((ActionEvent) -> {
      setLAF(TableButtonSlider.this, "javax.swing.plaf.metal.MetalLookAndFeel");
    });

    JButton bMotif = new JButton("Motif");
    bMotif.addActionListener((ActionEvent) -> {
      setLAF(TableButtonSlider.this, "com.sun.java.swing.plaf.motif.MotifLookAndFeel");
    });

    JButton bNimbus = new JButton("Nimbus");
    bNimbus.addActionListener((ActionEvent) -> {
      setLAF(TableButtonSlider.this, "javax.swing.plaf.nimbus.NimbusLookAndFeel");
    });

    JButton bMacOS = new JButton("mac");
    bMacOS.addActionListener((ActionEvent evt) -> {
      setLAF(TableButtonSlider.this, "com.apple.laf.AquaLookAndFeel");
    });

    JButton bWindows = new JButton("win");
    bWindows.addActionListener((ActionEvent) -> {
      setLAF(TableButtonSlider.this, "com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
    });

    JButton bLinux = new JButton("lnx");
    bLinux.addActionListener((ActionEvent) -> {
      setLAF(TableButtonSlider.this, "com.sun.java.swing.plaf.gtk.GTKLookAndFeel");
    });

    pH.add(bInsert);
    pH.add(Box.createRigidArea(new Dimension(1,0)));
    pH.add(new JSeparator(JSeparator.VERTICAL));
    pH.add(Box.createRigidArea(new Dimension(1,0)));
    pH.add(bLinux);
    pH.add(bMacOS);
    pH.add(bWindows);
    pH.add(Box.createRigidArea(new Dimension(1,0)));
    pH.add(new JSeparator(JSeparator.VERTICAL));
    pH.add(Box.createRigidArea(new Dimension(1,0)));
    pH.add(bMetal);
    pH.add(bMotif);
    pH.add(bNimbus);

    pV.add(pH);
    pV.add(scrollPane);
    return pV;
  }
  public static void main(String... args) {
    UIManager.put("Slider.paintValue", false);
    EventQueue.invokeLater(() -> {
      TableButtonSlider f = new TableButtonSlider();
      f.getContentPane().add(f.makeUI());
    });
  }
}

class PanelButton extends JPanel {
  JButton jbtWavRow = new JButton();
  private final JPanel panel = new JPanel();
  PanelButton(PanelButtonData data) {
    super();
    panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
    panel.add(Box.createRigidArea(new Dimension(2,0)));
    panel.add(jbtWavRow);
    panel.add(Box.createRigidArea(new Dimension(2,0)));
    jbtWavRow.setFont(new Font("Monospaced", Font.PLAIN, 10));
    setData(data);
    setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
    add(panel);
  }
  public PanelButtonData getData() {
    return new PanelButtonData(jbtWavRow.getActionCommand().equals("+"));
  }
  public void setData(PanelButtonData data) {
    for (ActionListener al : jbtWavRow.getActionListeners()) {
      jbtWavRow.removeActionListener(al);
    }

    if(data.getIns()) {
      jbtWavRow.setText("Insert");
      jbtWavRow.setActionCommand("+");
      jbtWavRow.addActionListener((ActionEvent e) -> {
        JTable table = (JTable)SwingUtilities.getAncestorOfClass(
            JTable.class, (Component) e.getSource());
        table.getCellEditor().stopCellEditing();
        ((MyTableModel)table.getModel()).addRow(
            new Object[] {
              new PanelButtonData(false),
              new PanelSliderData(0, 25, 50)
            }
        );
        table.updateUI();
      });
    } else {
      jbtWavRow.setText("Remove");
      jbtWavRow.setActionCommand("-");
      jbtWavRow.addActionListener((ActionEvent e) -> {
        JTable table = (JTable) SwingUtilities.getAncestorOfClass(
            JTable.class, (Component) e.getSource());

        int row = table.getEditingRow();
        table.getCellEditor().stopCellEditing();
        ((MyTableModel) table.getModel()).removeRow(row);
        // table.updateUI();
      });
    }
  }
}

class PanelButtonData {
  private boolean add = false;
  PanelButtonData(Boolean add) { this.add = add; }
  public void setIns(Boolean add) { this.add = add; }
  public boolean getIns() { return add; }
}

class PanelSlider extends JPanel {
  private final JSlider jslChanger = new JSlider(SwingConstants.HORIZONTAL);
  private final JPanel panel = new JPanel();
  PanelSlider(PanelSliderData data) {
    super();
    panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
    panel.add(Box.createRigidArea(new Dimension(2,0)));
    panel.add(jslChanger);
    panel.add(Box.createRigidArea(new Dimension(2,0)));
    setData(data);
    setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
    add(panel);
  }
  public void setData(PanelSliderData data) {
    jslChanger.setMinimum(data.getMin());
    jslChanger.setValue(data.getVal());
    jslChanger.setMaximum(data.getMax());
  }
  // Used in MyTableCellRenderer.getCellEditorValue()
  public PanelSliderData getData() {
    return new PanelSliderData(jslChanger.getMinimum(), jslChanger.getValue(), jslChanger.getMaximum());
  }
}

class PanelSliderData {
  private Integer Min = 0;
  private Integer Val = 25;
  private Integer Max = 50;
  PanelSliderData(int Min, int Val, int Max) {
    this.Min = Min;
    this.Val = Val;
    this.Max = Max;
  }
  public Integer getMin() { return Min; }
  public Integer getVal() { return Val; }
  public Integer getMax() { return Max; }
}

class MyTableCellRenderer implements TableCellRenderer {
  private final PanelSlider ps = new PanelSlider(new PanelSliderData(0, 25, 50));
  private final PanelButton pb = new PanelButton(new PanelButtonData(false));
  @Override public Component getTableCellRendererComponent(
      JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
    if (value instanceof PanelButtonData) {
      pb.setData((PanelButtonData) value);
      return pb;
    }
    if (value instanceof PanelSliderData) {
      ps.setData((PanelSliderData) value);
      return ps;
    }

    //if (value != null)
    //  return (Component) value;
    //return this;

    return null;

    //return table.getDefaultRenderer(String.class).getTableCellRendererComponent(
    //    table, value, isSelected, hasFocus, row, column);

    //return new JLabel();
  }
}

class MyTableCellEditor extends AbstractCellEditor implements TableCellEditor {
  protected Object output;
  private final PanelButton pb = new PanelButton(new PanelButtonData(false));
  private final PanelSlider ps = new PanelSlider(new PanelSliderData(0, 25, 50));
  @Override public Object getCellEditorValue() {
    if (output instanceof PanelButton) {
      return pb.getData();
    }
    if (output instanceof PanelSlider) {
      return ps.getData();
    }
    return null;
  }
  @Override public Component getTableCellEditorComponent(
      JTable table, Object value, boolean isSelected, int row, int column) {
    if (value instanceof PanelButtonData) {
      pb.setData((PanelButtonData) value);
      output = pb;
      return pb;
    }
    if (value instanceof PanelSliderData) {
      ps.setData((PanelSliderData) value);
      output = ps;
      return ps;
    }
    return null;
  }
}

class MyTableModel extends AbstractTableModel {
//class MyTableModel extends DefaultTableModel {
  private Object[][] data;
  private Object[] columns;
  public MyTableModel(Object[][] data, Object[] columns) {
    this.data = data;
    this.columns = columns;
  }
  @Override public Object getValueAt(int rowIndex, int columnIndex) {
    if (data != null) {
      if (data.length > 0) {
        return data[rowIndex][columnIndex];
      }
    }
    return null;
  }
  @Override public int getColumnCount() {
    return ((columns == null) ? 0: columns.length);
  }
  @Override public int getRowCount() {
    return ((data == null) ? 0: data.length);
  }
  @Override public Class getColumnClass(int columnIndex) {
    if (data != null) {
      if (data.length > 0) {
        if (data[0][columnIndex] instanceof PanelButton) {
          return PanelButton.class;
        }
        if (data[0][columnIndex] instanceof PanelSlider) {
          return PanelSlider.class;
        }
        //return data[0][columnIndex].getClass();
        return String.class;
      }
    }
    return Object.class;
  }
  @Override public boolean isCellEditable(int rowIndex, int columnIndex) {
    return true;
  }
  @Override public void setValueAt(Object value, int row, int col) {
    data[row][col] = value;
    fireTableCellUpdated(row, col);
  }
  @Override public String getColumnName(int columnIndex) {
    return (String)columns[columnIndex];
  }
  //@Override 
  public void removeRow(int row) {
    Object[][] newData = new Object[data.length - 1][data[0].length];
    int rown = 0;
    for (int row1 = 0; row1 <data.length; row1++) {
      if (row1 != row) {
        for (int col = 0; col < data[0].length; col++) {
          newData[rown][col] = data[row1][col];
        }
        rown++;
      }
    }
    data = newData;
  }
  //@Override 
  public void addRow(Object[] rowData) {
    Object[][] newData;
    int maxCol;
    if ((data != null) && (data.length > 0)) {
      newData = new Object[data.length + 1][data[0].length];
      for (int row = 0; row <data.length; row++) {
        for (int col = 0; col < data[0].length; col++) {
          newData[row][col] = data[row][col];
        }
      }
      maxCol = data[0].length < rowData.length?data[0].length:rowData.length;
    } else {
      newData = new Object[1][rowData.length];
      maxCol = rowData.length;
    }
    //Insert rowData objects
    for (int col = 0; col < maxCol; col++) {
      newData[newData.length - 1][col] = rowData[col];
    }
    data = newData;
  }
}

EDIT 1

  public static void main(String... args) {
    UIManager.put("Slider.paintValue", false);
    try {
      UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
    } catch (ClassNotFoundException | InstantiationException
        | IllegalAccessException | UnsupportedLookAndFeelException e) { }

    EventQueue.invokeLater(() -> {
      TableButtonSlider f = new TableButtonSlider();
      f.getContentPane().add(f.makeUI());
    });
  }

Establishing the nimbus LookAndFeel like first operation on main method the Exception raise up:

Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
  at javax.swing.plaf.synth.SynthTableUI.paintCell(SynthTableUI.java:684)
  at javax.swing.plaf.synth.SynthTableUI.paintCells(SynthTableUI.java:580)
  at javax.swing.plaf.synth.SynthTableUI.paint(SynthTableUI.java:364)
  at javax.swing.plaf.synth.SynthTableUI.update(SynthTableUI.java:275)
  at javax.swing.JComponent.paintComponent(JComponent.java:780)
  at javax.swing.JComponent.paint(JComponent.java:1056)
  at javax.swing.JComponent.paintChildren(JComponent.java:889)
  at javax.swing.JComponent.paint(JComponent.java:1065)
  at javax.swing.JViewport.paint(JViewport.java:728)
  at javax.swing.JComponent.paintChildren(JComponent.java:889)
  at javax.swing.JComponent.paint(JComponent.java:1065)
  at javax.swing.JComponent.paintChildren(JComponent.java:889)
  at javax.swing.JComponent.paint(JComponent.java:1065)
  at javax.swing.JComponent.paintChildren(JComponent.java:889)
  at javax.swing.JComponent.paint(JComponent.java:1065)
  at javax.swing.JComponent.paintChildren(JComponent.java:889)
  at javax.swing.JComponent.paint(JComponent.java:1065)
  at javax.swing.JLayeredPane.paint(JLayeredPane.java:586)
  at javax.swing.JComponent.paintChildren(JComponent.java:889)
  at javax.swing.JComponent.paintToOffscreen(JComponent.java:5217)
  at javax.swing.RepaintManager$PaintManager.paintDoubleBuffered(RepaintManager.java:1579)
  at javax.swing.RepaintManager$PaintManager.paint(RepaintManager.java:1502)
  at javax.swing.RepaintManager.paint(RepaintManager.java:1272)
  at javax.swing.JComponent.paint(JComponent.java:1042)
  at java.awt.GraphicsCallback$PaintCallback.run(GraphicsCallback.java:39)
  at sun.awt.SunGraphicsCallback.runOneComponent(SunGraphicsCallback.java:79)
  at sun.awt.SunGraphicsCallback.runComponents(SunGraphicsCallback.java:116)
  at java.awt.Container.paint(Container.java:1975)
  at java.awt.Window.paint(Window.java:3904)
  at javax.swing.RepaintManager$4.run(RepaintManager.java:842)
  at javax.swing.RepaintManager$4.run(RepaintManager.java:814)
  at java.security.AccessController.doPrivileged(Native Method)
  at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:80)
  at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:814)
  at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:789)
  at javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:738)
  at javax.swing.RepaintManager.access$1200(RepaintManager.java:64)
  at javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1732)
  at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
  at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:756)
  at java.awt.EventQueue.access$500(EventQueue.java:97)
  at java.awt.EventQueue$3.run(EventQueue.java:709)
  at java.awt.EventQueue$3.run(EventQueue.java:703)
  at java.security.AccessController.doPrivileged(Native Method)
  at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:80)
  at java.awt.EventQueue.dispatchEvent(EventQueue.java:726)
  at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
  at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
  at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
  at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
  at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
  at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)

Solution

  • Is it valid (or secure) to create a xxxTableModel(...) with nonuniform Matrix?

    Yes, you can do it, but it requires a lot of effort on your part to make it work, as JTable really isn't designed to work this way

    If Answer is YES for before Question. When the value is null and not defined (like cell in row:0, col:1) in my custom JPanel Classes, what Type Object I must return?

    You MUST return an instance of Component, you can NOT return null

    But, When the value Is not null and I don't know the Class type, How handle the return?

    This is the same answer as your second question, you MUST return an instance of Component, it's up to you as to how you want to handle the difference between null and "unknown" values

    Obsrvations

    After digging through your code, you are making calls to updateUI which are inappropriate, this is not a means to trigger a "update" to the UI when you change it's state, it's only meant to be used (by the system) to inform components that the Look and Feel delegate has been changed

    In your TableModel, you should be firing appropriate events when the model is update, this will notify the parent JTable and it will make appropriate changes.

    I removed ALL the calls to updateUI in your code and simply updated your addRow method to call fireTableRowsInserted

    public void addRow(Object[] rowData) {
        Object[][] newData;
        int maxCol;
        if ((data != null) && (data.length > 0)) {
            newData = new Object[data.length + 1][data[0].length];
            for (int row = 0; row < data.length; row++) {
                for (int col = 0; col < data[0].length; col++) {
                    newData[row][col] = data[row][col];
                }
            }
            maxCol = data[0].length < rowData.length ? data[0].length : rowData.length;
        } else {
            newData = new Object[1][rowData.length];
            maxCol = rowData.length;
        }
        //Insert rowData objects
        for (int col = 0; col < maxCol; col++) {
            newData[newData.length - 1][col] = rowData[col];
        }
        data = newData;
        fireTableRowsInserted(data.length - 1, data.length - 1);
    }
    

    and the NullPointerException went away. You have to make appropriate changes to removeRow as well