ListSelectionEvent导致StackOverFlow后的fireTableDataChanged()

时间:2014-01-24 12:21:46

标签: java swing stack-overflow jtree listselectionlistener

基本上我需要做的是在我的ListSelectionListener上为行和列模型设置一个JTable,这样我就知道哪个单元格被点击了,我很好。

如果我点击的单元格恰好有一个JTree作为渲染器组件,那么我将展开/折叠JTree,调整表格行的高度并重新绘制表。所以我的ListSelectionListener看起来像这样

  static class TableCellSelectionListener implements ListSelectionListener {

    private JTable _t;

    @Override
    public void valueChanged(ListSelectionEvent lse) {
      if(!lse.getValueIsAdjusting()) {
        int rowSelected = _t.getSelectedRow();
        int colSelected = _t.getSelectedColumn();    
        if(rowSelected > -1 && colSelected > -1) {
          TableCellRenderer renderer = _t.getCellRenderer(rowSelected, colSelected);
          Object obj = _t.getValueAt(rowSelected, colSelected);
          Component c = renderer.getTableCellRendererComponent(_t, obj, true, false, rowSelected, colSelected);    
          if(c instanceof JTree) {
            JTree tree = (JTree) c;
            TreePath path = tree.getPathForRow(0);

            if(tree.isExpanded(path)) {
              tree.collapsePath(path);
              tree.fireTreeCollapsed(path);
            } else {
              tree.expandPath(path);
              tree.fireTreeExpanded(path);
            }          

            Enumeration<TableColumn> columns = _t.getColumnModel().getColumns();
            while(columns.hasMoreElements()) {
              TableColumn col = columns.nextElement();
              int colIndex = col.getModelIndex();
              if(_t.getColumnClass(colIndex) == JTree.class && colIndex != colSelected) {
                JTree t = (JTree) _t.getValueAt(rowSelected, colIndex);
                TreePath p = t.getPathForRow(0);
                if(tree.isExpanded(path) && !t.isExpanded(p)) {
                  t.expandPath(p);
                  t.fireTreeExpanded(p);
                } else if(!tree.isExpanded(path) && t.isExpanded(p)) {
                  t.collapsePath(p);
                  t.fireTreeCollapsed(p);
                }
              }
            }          

            _t.setRowHeight(rowSelected, c.getPreferredSize().height);
            //Line below causes StackOverFlow
            ((SortableTableModel)_t.getModel()).fireTableDataChanged();
            _t.clearSelection();
          }
        }
      }    
    }

    public void setTable(JTable t) {
      _t = t;
    }

  }

&#34;正确的&#34;这样做的方法?

因为我真正感兴趣的是正确的方法,这里有完整的可编译代码,运行它你很快就会看到我的目标(如前所述,表格单元格中有JTree组件可以展开/折叠,高度为表行也需要反映JTree状态)

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.Graphics;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;

import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.plaf.IconUIResource;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;

public class ProgressBarPCSTable {

  private ProgressBarValueTracker pbValueTracker = new ProgressBarValueTracker();

  private String[] columnNames = {"JobID","Progress", "Status"};  
  private DefaultTableModel tableModel = new DefaultTableModel(null, columnNames) {
    private static final long serialVersionUID = 1L;

    @Override
    public Class<?> getColumnClass(int column) {
      return getValueAt(0, column).getClass();
    }

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

  private JTable table = new JTable(tableModel);

  public JComponent makeUI() {
    TableColumn progressColumn = table.getColumnModel().getColumn(1);
    progressColumn.setCellRenderer(new JTreeRenderer(true, pbValueTracker));
    TableColumn statusColumn = table.getColumnModel().getColumn(2);
    statusColumn.setCellRenderer(new JTreeRenderer(false, pbValueTracker));

    //Setup row/column selection listeners
    table.setRowSelectionAllowed(true);
    TableCellSelectionListener cellSelectionListener = new TableCellSelectionListener(table);
    table.getSelectionModel().addListSelectionListener(cellSelectionListener);
    table.getColumnModel().getSelectionModel().addListSelectionListener(cellSelectionListener);

    EventQueue.invokeLater(new Runnable() {

      @Override
      public void run() {
        startTask(new Object[][]{{"000001"}, {0}, {"complete"}});
        startTask(new Object[][]{{"000002"}, {0,0}, {"processing","rendering pdf"}});
        startTask(new Object[][]{{"000003"}, {0,0,0}, {"processing","rendering pdf","rendering afp"}});
        startTask(new Object[][]{{"000004"}, {0,0,0,0}, {"processing","rendering pdf","rendering afp","rendering postscript"}});
        startTask(new Object[][]{{"000005"}, {0,0,0,0,0}, {"processing","normalsing","enhancing","sorting","rendering"}});
      }
    });
    JPanel p = new JPanel(new BorderLayout());
    p.add(new JScrollPane(table));
    return p;
  }

  private void startTask(final Object[][] job) {
    final int key = tableModel.getRowCount();
    final String jobID = (String) job[0][0];    

    final DefaultMutableTreeNode progressRoot = new DefaultMutableTreeNode((Integer)job[1][0]);
    final DefaultMutableTreeNode statusRoot = new DefaultMutableTreeNode((String)job[2][0]);

    final DefaultTreeModel progressTreeModel = new DefaultTreeModel(progressRoot);
    final DefaultTreeModel statusTreeModel = new DefaultTreeModel(statusRoot);

    final JTree progressTree = new JTree(progressTreeModel);
    final JTree statusTree = new JTree(statusTreeModel);

    if(job[1].length > 1) {
      initJTree(progressRoot, Arrays.copyOfRange(job[1], 1, job[1].length, Integer[].class));
      ((DefaultTreeModel)progressTree.getModel()).nodeChanged(progressRoot);
    }

    if(job[2].length > 1) {
      initJTree(statusRoot, Arrays.copyOfRange(job[2], 1, job[2].length, String[].class)); 
    }

    pbValueTracker.initPVMap(progressTree);

    EventQueue.invokeLater(new Runnable() {

      @Override
      public void run() {
        startProgressBarTask(progressTree, progressRoot, key, false);
        int childCount = progressRoot.getChildCount();        
        for(int i=0; i < childCount; i++) {
          startProgressBarTask(progressTree, (DefaultMutableTreeNode)progressRoot.getChildAt(i), key, i == 1 ? true : false);          
        }
      }
    });

    tableModel.addRow(new Object[]{jobID, progressTree, statusTree});


  }

  private <T> void initJTree(DefaultMutableTreeNode root, T[] list) {
    for(T t: list) {
      root.add(new DefaultMutableTreeNode(t));
    }
  }

  private void startProgressBarTask(final JTree progressTree, final DefaultMutableTreeNode node, 
      final int rowCount, final boolean error) {
    LoudCall<Void, JTree> progressShout = new LoudCall<Void, JTree>() {

      @Override
      public Void call() throws Exception {

        SwingWorker<Integer, Integer> progressWorker = new SwingWorker<Integer, Integer>() {

          private int sleepDummy = new Random().nextInt(100) + 1;
          private int lengthOfTask = 120;

          /**
           * Overrides the SwingWorker doInBackground, this version, increments 
           * the value of the % complete and publishes it, the process method will
           * pick up the published value so that the ProgressBarRenderer can
           * deal with it. It also triggers a nodeChanged event on the DefaultTreeModel
           * so that the JTree updates
           */
          @Override
          protected Integer doInBackground() throws Exception {            
            int current = 0;
            DefaultMutableTreeNode root = (DefaultMutableTreeNode) progressTree.getModel().getRoot();
            String suffix = root == node ? "_root" : "_" +root.getIndex(node);
            String key = String.valueOf(System.identityHashCode(progressTree)) +suffix; 
            while(current < lengthOfTask && !isCancelled()) {
              if(error && current >= 60) { //Error test
                cancel(true);
                publish(-1);
                pbValueTracker.putErrMap(key, -1);
                System.out.println(pbValueTracker.getErrMap());
                return -1;
              }
              current++;
              try {
                Thread.sleep(sleepDummy);
              } catch (InterruptedException ie) {
                break;
              }
              int value = 100 * current / lengthOfTask;
              publish(value);
              if(!pbValueTracker.getErrMap().containsKey(key)) {
                pbValueTracker.putPVMap(key, value);
              }              
            }
            return sleepDummy * lengthOfTask;
          }

          /**
           * Attach a user object to the node, in this case
           * it is an Integer with the latest value triggered by publish
           * process will fire getTreeCellRendererComponent of the
           * ProgressBarRenderer
           * 
           * @param c - a list of Integer to process, only process the last value set
           */
          @Override
          protected void process(List<Integer> c) {
            node.setUserObject(c.get(c.size() - 1));
            shoutOut(progressTree);            
            ((DefaultTreeModel)progressTree.getModel()).nodeChanged(node);
          }

          @Override
          protected void done() {
            int i = -1;
            if(!isCancelled()) {
              try {
                i = get();
              } catch (Exception e) {
                e.printStackTrace();
              }          
            }
            System.out.println("Value: " +i);
            shoutOut(progressTree);
            ((DefaultTreeModel)progressTree.getModel()).nodeChanged(node);
            System.out.println(pbValueTracker.getPVMap());
          }

        };

        progressWorker.execute();

        return null;
      }

    };

    (new ListenerTask<Void, JTree>(progressShout) {

      @Override
      protected void process(List<JTree> chunks) {
        tableModel.setValueAt(chunks.get(chunks.size() - 1), rowCount, 1);
        tableModel.fireTableDataChanged();
      }      

    }).execute();
  }

  public static void main(String... args) {
    EventQueue.invokeLater(new Runnable() {

      @Override
      public void run() {
        createAndShowGUI();
      }
    });
  }

  public static void createAndShowGUI() {
    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    frame.getContentPane().add(new ProgressBarPCSTable().makeUI());
    frame.setSize(669, 307);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
  }

}

/**
 * This class listens for selection of a table cell
 * if selected and the cells component is a JTree it
 * expands/collapses the JTree and adjusts the height
 * of the table row accordingly
 * 
 * @author pstatham
 *
 */
class TableCellSelectionListener implements ListSelectionListener {

  private JTable _t;

  public TableCellSelectionListener(JTable t) {
    _t = t;
  }

  @Override
  public void valueChanged(ListSelectionEvent lse) {
    if(!lse.getValueIsAdjusting()) {
      int rowSelected = _t.getSelectedRow();
      int colSelected = _t.getSelectedColumn();    
      if(rowSelected > -1 && colSelected > -1) {
        TableCellRenderer renderer = _t.getCellRenderer(rowSelected, colSelected);
        Object obj = _t.getValueAt(rowSelected, colSelected);
        Component c = renderer.getTableCellRendererComponent(_t, obj, true, false, rowSelected, colSelected);    
        if(c instanceof JTree) {
          JTree tree = (JTree) c;
          TreePath path = tree.getPathForRow(0);

          if(tree.isExpanded(path)) {
            tree.collapsePath(path);
            tree.fireTreeCollapsed(path);
          } else {
            tree.expandPath(path);
            tree.fireTreeExpanded(path);
          }          

          Enumeration<TableColumn> columns = _t.getColumnModel().getColumns();
          while(columns.hasMoreElements()) {
            TableColumn col = columns.nextElement();
            int colIndex = col.getModelIndex();
            if(_t.getColumnClass(colIndex) == JTree.class && colIndex != colSelected) {
              JTree t = (JTree) _t.getValueAt(rowSelected, colIndex);
              TreePath p = t.getPathForRow(0);
              if(tree.isExpanded(path) && !t.isExpanded(p)) {
                t.expandPath(p);
                t.fireTreeExpanded(p);
              } else if(!tree.isExpanded(path) && t.isExpanded(p)) {
                t.collapsePath(p);
                t.fireTreeCollapsed(p);
              }
            }
          }          

          _t.setRowHeight(rowSelected, c.getPreferredSize().height);
          ((DefaultTableModel)_t.getModel()).fireTableDataChanged();
          _t.clearSelection();
        }
      }
    }    
  }

}

/**
 * This class keeps track of a particular JProgressBar's previous
 * value in case it has gone in to error
 * @author pstatham
 *
 */
class ProgressBarValueTracker {

  private HashMap<String, Integer> pvMap = new HashMap<String, Integer>();
  private HashMap<String, Integer> errMap = new HashMap<String, Integer>();

  public void initPVMap(JTree tree) {
    String key = String.valueOf(System.identityHashCode(tree)) +"_root";
    DefaultMutableTreeNode root = (DefaultMutableTreeNode) ((DefaultTreeModel)tree.getModel()).getRoot();
    pvMap.put(key, (Integer) root.getUserObject());
    for(int i=0; i < root.getChildCount(); i++) {
      key = String.valueOf(System.identityHashCode(tree)) +"_" + i;
      DefaultMutableTreeNode child = (DefaultMutableTreeNode) root.getChildAt(i);
      pvMap.put(key, (Integer) child.getUserObject());
    }
  }

  public void putPVMap(String key, Integer value) {
    pvMap.put(key, value);
  }

  public void putErrMap(String key, Integer value) {
    errMap.put(key, value);
  }

  public HashMap<String, Integer> getPVMap() {
    return pvMap;
  }

  public HashMap<String, Integer> getErrMap() {
    return errMap;
  }

}


/**
 * This class extends the DefaultTreeCellRenderer and returns a JPanel with a
 * JProgressBar attached as its renderer component
 * @author pstatham
 *
 */
@SuppressWarnings("serial")
class ProgressBarRenderer extends DefaultTreeCellRenderer {

  private final JProgressBar progressBar = new JProgressBar(0, 100);

  private ProgressBarValueTracker pbValueTracker;

  public ProgressBarRenderer(ProgressBarValueTracker tracker) {
    super();    
    pbValueTracker = tracker;
    setOpaque(true);
    configureProgressBar(progressBar);    
    progressBar.setBackground(Color.YELLOW);    
  }

  @Override
  public Component getTreeCellRendererComponent(JTree tree, final Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
    if(((DefaultMutableTreeNode) value).getUserObject() instanceof String) {
      super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
      return this;
    }

    DefaultMutableTreeNode root = (DefaultMutableTreeNode) tree.getModel().getRoot();
    String suffix = root == value ? "_root" : "_" +root.getIndex((DefaultMutableTreeNode)value);
    String key = String.valueOf(System.identityHashCode(tree)) +suffix;    

    JPanel p = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
    p.setBackground(Color.WHITE);    

    JLabel l = (JLabel)super.getTreeCellRendererComponent(tree, null, selected, expanded, leaf, row, hasFocus);

    if(((DefaultMutableTreeNode)value).isRoot() && ((DefaultMutableTreeNode)value).getChildCount() > 0) {
      TreePath path = tree.getPathForRow(row);
      if(tree.isExpanded(path)) {
        l.setIcon(new IconUIResource(new NodeIcon('-')));
      } else {
        l.setIcon(new IconUIResource(new NodeIcon('+')));
      }
    } else {
      l.setIcon(new IconUIResource(new NodeIcon(' ')));
    }

    p.add(l);    

    Integer i = (Integer) ((DefaultMutableTreeNode) value).getUserObject();

    //
    // If Job is in error return a different
    // JProgressBar with a red background
    //
    if(i<0) {            
      JProgressBar errorProgressBar = new JProgressBar(0, 100);
      configureProgressBar(errorProgressBar);
      errorProgressBar.setBackground(Color.RED);      
      errorProgressBar.setValue(pbValueTracker.getPVMap().get(key));
      p.add(errorProgressBar);
    } else {
      progressBar.setValue(i);
      p.add(progressBar);
    }    
    return p;    
  }

  /**
   * Configure a JProgressBar with common options
   * 
   * @param pb JProgressBar to configure
   */
  private void configureProgressBar(JProgressBar pb) {
    pb.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
    pb.setForeground(Color.GREEN.darker());
    pb.setStringPainted(true);
    pb.setPreferredSize(new Dimension(pb.getPreferredSize().width, 16));
  }

}

/**
 * This class extends the DefaultTableCellRenderer and returns a JTree
 * as its renderer component. The JTrees renderer
 * can either be an instance of a ProgressBarRenderer or a 
 * DefaultTreeCellRenderer that returns a label with an open/close icon
 * as its renderer component.
 * @author pstatham
 *
 */
@SuppressWarnings("serial")
class JTreeRenderer extends DefaultTableCellRenderer {

  private JTree tree;
  private TreeCellRenderer renderer;  

  public JTreeRenderer(boolean progressTree, ProgressBarValueTracker tracker) {    
    if(progressTree) {
      renderer = new ProgressBarRenderer(tracker);      
    } else {
      renderer = new DefaultTreeCellRenderer() {        
        @Override
        public Component getTreeCellRendererComponent(final JTree tree, Object value,  
            boolean sel,boolean expanded,boolean leaf,int row,boolean hasFocus){
          JLabel label = (JLabel)super.getTreeCellRendererComponent(tree,value,  
              sel,expanded,leaf,row,hasFocus); 
          if(((DefaultMutableTreeNode)value).isRoot() && ((DefaultMutableTreeNode)value).getChildCount() > 0) {
            TreePath path = tree.getPathForRow(row);
            if(tree.isExpanded(path)) {
              label.setIcon(new IconUIResource(new NodeIcon('-')));
            } else {
              label.setIcon(new IconUIResource(new NodeIcon('+')));
            }
          } else {
            label.setIcon(new IconUIResource(new NodeIcon(' ')));
          }
          return label;  
        }                  
      };
    }    
  }

  @Override
  public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
    tree = (JTree) value;
    tree.setCellRenderer(renderer);
    table.setRowHeight(row, tree.getPreferredSize().height);
    return tree;
  }

}

/**
 * Pinched from http://stackoverflow.com/a/7984734/564045
 * @author pstatham
 *
 */
class NodeIcon implements Icon {

  private static final int SIZE = 9;

  private char type;

  public NodeIcon(char type) {
    this.type = type;
  }

  public void paintIcon(Component c, Graphics g, int x, int y) {
    if(type != ' ') {
      g.setColor(UIManager.getColor("Tree.background"));
      g.fillRect(x, y, SIZE - 1, SIZE - 1);

      g.setColor(UIManager.getColor("Tree.hash").darker());
      g.drawRect(x, y, SIZE - 1, SIZE - 1);

      g.setColor(UIManager.getColor("Tree.foreground"));
      g.drawLine(x + 2, y + SIZE / 2, x + SIZE - 3, y + SIZE / 2);
      if (type == '+') {
        g.drawLine(x + SIZE / 2, y + 2, x + SIZE / 2, y + SIZE - 3);
      }      
    }
  }

  public int getIconWidth() {
    return SIZE;
  }

  public int getIconHeight() {
    return SIZE;
  }
}

/**
 * Wrapper for the background logic
 * http://stackoverflow.com/a/6834797/564045
 * @author pstatham
 *
 * @param <T> return type
 * @param <S> intermediary type (the "shout out")
 */
abstract class LoudCall<T, S> implements Callable<T> {

  private PropertyChangeSupport pcs;
  private S shout;

  public LoudCall() {
    pcs = new PropertyChangeSupport(this);
  }

  public void shoutOut(S s) {
    pcs.firePropertyChange("shoutOut", this.shout, this.shout = s);
  }

  public void addListener(PropertyChangeListener listener) {
    pcs.addPropertyChangeListener(listener);
  }

  public void removeListener(PropertyChangeListener listener) {
    pcs.removePropertyChangeListener(listener);
  }

  @Override
  public abstract T call() throws Exception;

}

/**
 * Wrapper for the GUI listener.
 * http://stackoverflow.com/a/6834797/564045
 * @author pstatham
 * 
 * @param <T> return type
 * @param <S> intermediary type (the "shout out" to listen for)
 */
abstract class ListenerTask<T, S> extends SwingWorker<T, S> implements PropertyChangeListener {

  private LoudCall<T, S> aMethod;

  public ListenerTask(LoudCall<T, S> aMethod) {
    this.aMethod = aMethod;
  }

  @Override
  protected T doInBackground() throws Exception {
    aMethod.addListener(this);
    return aMethod.call();
  }

  @SuppressWarnings("unchecked")
  @Override
  public void propertyChange(PropertyChangeEvent evt) {
    if("shoutOut".equals(evt.getPropertyName())) {
      publish((S)evt.getNewValue());
    }
  }

  @Override
  protected abstract void process(List<S> chunks);
}

2 个答案:

答案 0 :(得分:0)

似乎EVERY DefaultTableModel.fire ...()方法也会导致ListSelectionEvent。想象一下,例如,删除之前选择的一些模型后,您的模型会更新。在这种情况下,必须重新计算列表选择,因此新的ListSelectionEvent。我猜JDK总是在TableModel.fire ...()。

之后重新计算列表选择

另请参阅JDK的this错误报告。 我希望至少对于fireTableCellUpdated()它没有发生,但确实如此。

你可以做的是使用如上所述的标志,或者根据观察到第一个事件是选择&#39;清除&#39;来过滤掉这些事件。 event(选定的行为空),下一个事件与之前的选择相同。 在单选案例的代码下面:

int prevSel = -1;
...
public void valueChanged(ListSelectionEvent lse) {

   if(lse.getValueIsAdjusting() || getSelectedRows().length==0 || getSelectedRows()[0]==prevSel ) 
     return;
   prevSel = getSelectedRows()[0];

   ... your code ...
   fireTableDataChanged();
}

答案 1 :(得分:-1)

我真的不明白你的评论,但我设法通过设置布尔标志来实现它的工作

public void valueChanged(ListSelectionEvent lse) {
   if(!lse.getValueIsAdjusting())
      if(!methodCalled) {
        //Logic for expanding/collapsing JTree and adjusting
        //JTable row height
        methodCalled = true;
        fireTableDataChanged();
        methodCalled = false;
      }
   }
}