Живая сортировка JTable

Я выяснил, как правильно отсортировать JTable, но я не могу понять, как заставить его автоматически обновлять порядок сортировки при изменении ячейки таблицы. Прямо сейчас у меня есть этот (по общему признанию длинный) код, в основном основанный на том, что в учебнике по Java Как использовать таблицы. Я выделил изменения, которые я сделал с помощью // ADDED. В этом случае вновь добавленные значения сортируются правильно, но когда я вхожу для редактирования значения, кажется, что оно не прибегает, даже если я вызываю fireTableCellUpdated?

Короче говоря, как я могу заставить таблицу пересортироваться при изменении значения данных в модели?

/*
 * Copyright (c) 1995 - 2008 Sun Microsystems, Inc.  All rights reserved.
 * See the standard BSD license.
 */ 

package components;

/*
 * TableSortDemo.java requires no other files.
 */

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;

public class TableSortDemo extends JPanel {
    private boolean DEBUG = false;

    public TableSortDemo() {
        super();
        setLayout(new BoxLayout(TableSortDemo.this, BoxLayout.PAGE_AXIS));
        final MyTableModel m = new MyTableModel();
        JTable table = new JTable(m);
        table.setPreferredScrollableViewportSize(new Dimension(500, 70));
        table.setFillsViewportHeight(true);
        table.setAutoCreateRowSorter(true);

        //Create the scroll pane and add the table to it.
        JScrollPane scrollPane = new JScrollPane(table);

        //Add the scroll pane to this panel.
        add(scrollPane);

        // ADDED: button to add a value
        JButton addButton = new JButton("Add a new value");
        addButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                m.addValue(
                        JOptionPane.showInputDialog(
                                TableSortDemo.this, "Value?"));
            }
        });

        // ADDED button to change a value
        JButton setButton = new JButton("Change a value");
        setButton.addActionListener(new ActionListener() {
            /* (non-Javadoc)
             * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
             */
            public void actionPerformed(ActionEvent e) {
                m.setValueAt(
                        JOptionPane.showInputDialog(
                                TableSortDemo.this, "Value?"),
                        Integer.parseInt(
                                JOptionPane.showInputDialog(
                                        TableSortDemo.this, "Which?")), 0);
            }
        });
        add(addButton);
        add(setButton);
    }

    class MyTableModel extends AbstractTableModel {
        private static final long serialVersionUID = -7053335255134714625L;
        private String[] columnNames = {"Column"};
        // ADDED data as mutable ArrayList
        private ArrayList<String> data = new ArrayList<String>();

        public MyTableModel() {
            data.add("Anders");
            data.add("Lars");
            data.add("Betty");
            data.add("Anna");
            data.add("Jon");
            data.add("Zach");
        }

        // ADDED
        public void addValue(Object v) {
            data.add(v.toString());
            int row = data.size() - 1;
            fireTableRowsInserted(row, row);
        }

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

        public int getRowCount() {
            return data.size();
        }

        public String getColumnName(int col) {
            return columnNames[col];
        }

        public Object getValueAt(int row, int col) {
            return data.get(row) + " " + row;
        }

        /*
         * JTable uses this method to determine the default renderer/
         * editor for each cell.  If we didn't implement this method,
         * then the last column would contain text ("true"/"false"),
         * rather than a check box.
         */
        public Class<String> getColumnClass(int c) {
            return String.class;
        }

        /*
         * Don't need to implement this method unless your table's
         * editable.
         */
        public boolean isCellEditable(int row, int col) {
            //Note that the data/cell address is constant,
            //no matter where the cell appears onscreen.
            if (col < 2) {
                return false;
            } else {
                return true;
            }
        }

        /*
         * Don't need to implement this method unless your table's
         * data can change.
         */
        public void setValueAt(Object value, int row, int col) {
            if (DEBUG) {
                System.out.println("Setting value at " + row + "," + col
                                   + " to " + value
                                   + " (an instance of "
                                   + value.getClass() + ")");
            }

            data.set(row, value.toString());

            // ADDED: uncommented this line, despite warnings to the contrary
            fireTableCellUpdated(row, col);

            if (DEBUG) {
                System.out.println("New value of data:");
                printDebugData();
            }
        }

        private void printDebugData() {
            int numRows = getRowCount();
            int numCols = getColumnCount();

            for (int i=0; i < numRows; i++) {
                System.out.print("    row " + i + ":");
                for (int j=0; j < numCols; j++) {
                    System.out.print("  " + data.get(i));
                }
                System.out.println();
            }
            System.out.println("--------------------------");
        }
    }

    /**
     * Create the GUI and show it.  For thread safety,
     * this method should be invoked from the
     * event-dispatching thread.
     */
    private static void createAndShowGUI() {
        //Create and set up the window.
        JFrame frame = new JFrame("TableSortDemo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        //Create and set up the content pane.
        TableSortDemo newContentPane = new TableSortDemo();
        newContentPane.setOpaque(true); //content panes must be opaque
        frame.setContentPane(newContentPane);

        //Display the window.
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        //Schedule a job for the event-dispatching thread:
        //creating and showing this application's GUI.
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }
}

person Paul Fisher    schedule 15.06.2009    source источник


Ответы (4)


Это потребовало двухэтапного решения:

Сначала у меня была сортировка TableSorter при изменении данных, используя это, а не autoCreateRowSorter:

sorter = new TableRowSorter<MyTableModel>(m);
table.setRowSorter(sorter);
sorter.setSortsOnUpdates(true);

Затем мне пришлось изменить метод обновления, чтобы обновить всю таблицу. fireTableCellUpdated и fireTableRowsUpdated будут перерисовывать только определенные строки, которые были обновлены, а не всю таблицу (это означает, что вы получите повторяющуюся запись, которая изменится, как только она будет перерисована позже. Итак, я изменил

fireTableCellUpdated(row, col);

to

fireTableRowsUpdated(0, data.size() - 1);

и теперь он сортируется правильно, даже при изменении данных, и выбор сохраняется.

person Paul Fisher    schedule 15.06.2009

Это давняя ошибка в JTable, о которой сообщалось в 2007 году (удивлен, что это не исправлено, даже в jdk7)

Запуск обновления для всех строк является разумным быстрым решением, если оно не слишком сильно снижает производительность (из-за частых полных курортов). Для бесстрашных вот частичное исправление для JTable — частичное, потому что еще не все возможные сценарии захвачены. По этой причине он так и не попал в JXTable (или, может быть, тогда у меня были другие приоритеты :-)

public static class JTableRepaintOnUpdate extends JTable {

  private UpdateHandler beforeSort;

  @Override
  public void sorterChanged(RowSorterEvent e) {
      super.sorterChanged(e);
      maybeRepaintOnSorterChanged(e);
  } 

  private void beforeUpdate(TableModelEvent e) {
      if (!isSorted()) return;
      beforeSort = new UpdateHandler(e);
  }

  private void afterUpdate() {
      beforeSort = null;
  }

  private void maybeRepaintOnSorterChanged(RowSorterEvent e) {
      if (beforeSort == null) return;
      if ((e == null) || (e.getType() != RowSorterEvent.Type.SORTED)) return;
      UpdateHandler afterSort = new UpdateHandler(beforeSort);
      if (afterSort.allHidden(beforeSort)) {
          return;
      } else if (afterSort.complex(beforeSort)) {
          repaint();
          return;
      }
      int firstRow = afterSort.getFirstCombined(beforeSort);
      int lastRow = afterSort.getLastCombined(beforeSort);
      Rectangle first = getCellRect(firstRow, 0, false);
      first.width = getWidth();
      Rectangle last = getCellRect(lastRow, 0, false);
      repaint(first.union(last));
  }

  private class UpdateHandler {
      private int firstModelRow;
      private int lastModelRow;
      private int viewRow;
      private boolean allHidden;

      public UpdateHandler(TableModelEvent e) {
          firstModelRow = e.getFirstRow();
          lastModelRow = e.getLastRow();
          convert();
      }

      public UpdateHandler(UpdateHandler e) {
          firstModelRow = e.firstModelRow;
          lastModelRow = e.lastModelRow;
          convert();
      }

      public boolean allHidden(UpdateHandler e) {
          return this.allHidden && e.allHidden;
      }

      public boolean complex(UpdateHandler e) {
          return (firstModelRow != lastModelRow);
      }

      public int getFirstCombined(UpdateHandler e) {
          if (allHidden) return e.viewRow;
          if (e.allHidden) return viewRow;
          return Math.min(viewRow, e.viewRow);
      }

      public int getLastCombined(UpdateHandler e) {
          if (allHidden || e.allHidden) return getRowCount() - 1;
          return Math.max(viewRow, e.viewRow);

      }

      private void convert() {
          // multiple updates
          if (firstModelRow != lastModelRow) {
              // don't bother too much - calculation not guaranteed to do anything good
              // just check if the all changed indices are hidden
              allHidden = true;
              for (int i = firstModelRow; i <= lastModelRow; i++) {
                  if (convertRowIndexToView(i) >= 0) {
                      allHidden = false;
                      break;
                  }
              }
              viewRow = -1;
              return;
          }
          // single update
          viewRow = convertRowIndexToView(firstModelRow);
          allHidden = viewRow < 0;
      }

  }

  private boolean isSorted() {
      // JW: not good enough - need a way to decide if there are any sortkeys which
      // constitute a sort or any effective filters  
      return getRowSorter() != null;
  }

  @Override
  public void tableChanged(TableModelEvent e) {
      if (isUpdate(e)) {
          beforeUpdate(e);
      }
      try {
          super.tableChanged(e);
      } finally {
          afterUpdate();
      }
  }

  /**
   * Convenience method to detect dataChanged table event type.
   * 
   * @param e the event to examine. 
   * @return true if the event is of type dataChanged, false else.
   */
  protected boolean isDataChanged(TableModelEvent e) {
      if (e == null) return false;
      return e.getType() == TableModelEvent.UPDATE && 
          e.getFirstRow() == 0 &&
          e.getLastRow() == Integer.MAX_VALUE;
  }

  /**
   * Convenience method to detect update table event type.
   * 
   * @param e the event to examine. 
   * @return true if the event is of type update and not dataChanged, false else.
   */
  protected boolean isUpdate(TableModelEvent e) {
      if (isStructureChanged(e)) return false;
      return e.getType() == TableModelEvent.UPDATE && 
          e.getLastRow() < Integer.MAX_VALUE;
  }

  /**
   * Convenience method to detect a structureChanged table event type.
   * @param e the event to examine.
   * @return true if the event is of type structureChanged or null, false else.
   */
  protected boolean isStructureChanged(TableModelEvent e) {
      return e == null || e.getFirstRow() == TableModelEvent.HEADER_ROW;
  }

}
person kleopatra    schedule 16.11.2011
comment
Вау, отличный ответ. Это был кооператив многолетней давности, и в моем случае это хакерское обновление ВСЕХ вещей! решение в конечном итоге сработало очень хорошо с точки зрения производительности. - person Paul Fisher; 19.11.2011
comment
Небольшое обновление: ошибка была закрыта в 2018 году, так как не будет исправлена. - person toolforger; 13.01.2021
comment
@toolforger спасибо за обновление! - person kleopatra; 13.01.2021

вероятно, самым простым способом отсортировать его будет вызов fireTableDataChanged() вместо fireTableCellUpdated().

person boxofrats    schedule 15.06.2009

Есть несколько вещей, которые вы должны сделать здесь.

  1. Поскольку модель таблицы оборачивает вашу коллекцию, она должна быть сортируемой. Это означает, что ваш объект (строка) должен реализовать интерфейс Comparable, чтобы коллекцию можно было правильно отсортировать.
  2. В вашем методе setValueAt вы должны обновить соответствующий атрибут и использовать коллекцию с помощью Collections.sort. Затем, очевидно, вам нужно вызвать fireTableDataChanged, чтобы сообщить таблице, что ей нужно перерисовать.
  3. То же самое должно произойти при добавлении данных.
  4. Когда данные удаляются, вам не нужно прибегать к помощи, но все равно нужно запускатьTableDataChanged
  5. Если ваша коллекция слишком велика, вы можете подумать о том, чтобы сначала добавить данные в соответствующее место, а не прибегать к ним.

Надеюсь это поможет

person Eugene Ryzhikov    schedule 15.06.2009
comment
За исключением этого случая, я не хочу, чтобы TableModel отвечал за сортировку, я хочу, чтобы это делал Sorter в представлении. В нынешнем виде этот метод правильно сортирует таблицу, правильно обрабатывает вставку, но не обрабатывает должным образом изменения. - person Paul Fisher; 15.06.2009
comment
Почему бы вам тогда просто не указать метод сортировки в модели? - person Eugene Ryzhikov; 15.06.2009
comment
ИМО, это будет правильный дизайн MVC. Табличная модель представляет данные и должна инкапсулировать связанные операции. Это позволит вам запускать правильные события. Сделав его общедоступным, вы можете вызывать сортировку извне. - person Eugene Ryzhikov; 15.06.2009
comment
Сортировка должна выполняться в слое просмотра. Пользователь может указать, я хочу отсортировать по X. Упс, это выглядит некрасиво. ХОРОШО. Теперь я хочу просмотреть его по сортировке по Y. - person Cheok Yan Cheng; 06.01.2011