Sortowanie i filtrowanie tabel.
Techniki łatwego ponownego użycia
Oprócz
informacji na temat sortowania i filtrowania tabel będą tu
przedstawione nieco bardziej zaawansowane sposoby programowania, pełną
garścią czerpiące z takich "narzędzi" jak interfejsy, klasy
abstrakcyjne, MVC i generics. Pozwoli to na łatwe tworzenie nowych
aplikacji (działających na róznych zestawach danych) poprzez ponowne
użycie dużych fragmentów już napisanego kodu.
1. Sort - najprościej
Java w wersji 6 zapewnia łatwe w użyciu i elastyczne środki sortowania i filtrowania tabel.
Po to by móc sortować tabelę wystarczy użyć metody:
tabela.setAutoCreateRowSorter(true);
Posortowanie
kolumny uzyskujemy poprzez kliknięcie w kolumnę nagłówka tabeli.
Kolejne kliknięcia zmieniają porządek sortowania (rosnąco-malejąco).
Poniższy kod ilustruje najprostszy sposób sortowania najprostszej tabeli.
public class Tab1 extends JFrame implements ActionListener {
public Tab1() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
// Dane przykładowej tabeli
Object[][] data = { { "Chopin Fryderyk", "M", 1789 },
{ "Kopernik Mikołaj", "M", 2900 },
{ "Słowacki Juliusz", "M", 2000 },
{ "Szymborska Wisława", "K", 5200 },
{ "Polański Roman", "M", 3000 },
{ "Skłodowska-Curie Maria", "K", 2000 },
{ "Modrzejewska Helena", "K", 2000 },
};
// Nazwy kolumn
String[] columnNames = { "Nazwisko i imię", "Płeć", "Pensja" };
JTable tab = new JTable(data, columnNames);
tab.setAutoCreateRowSorter(true);
add (new JScrollPane(tab));
pack();
setVisible(true);
}
public static void main(String[] args) {
new Tab1();
}
}
Wynik sortowania:
Uwaga: dane posortowane wg nazwiska i i imienia; aktualną kolumnę i porządek sortowania wskazuje ikonka w nagłówku.
2. Klucze sortowania
W jaki sposób odbywa się sortowanie?
Otóż wywołanie metody setAutoCreateRowSorter(true) tworzy obiekt klasy TableRowSorter (dziedziczącej klasę DefaultRowSorter, implementującą interfejs RowSorter), odpowiednio go konfiguruje i ustala jako "sorter" dla tabeli.
Przy sortowaniu sorter posługuje się kluczami sortowania. Jest to lista obiektów typu SortKey. Każdy SortKey zawiera indeks kolumny, której dane mają być sortowane i porządek ich sortowania (rosnąco-malejąco).
Pierwszy
element na liście wskazuje pierwszy klucz sortowania (kolumnę, której
dane podlegają porządkowaniu w pierwszej kolejności), drugi - kolumnę,
wedle której będą porządkowane wiersze, mające takie same wartości
w tej pierwszej kolumnie itd. Domyślnie dostępne są maksymalnie
trzy klucze sortowania, ale można to zmienić używając metody setMaxSortKeys(int n) z klasy DefaultRowSorter.
Kliknięcie w nagłówek kolumny powoduje wywołanie metody toggleSortOrder(int kolumna). Metoda ta:
- zmienia
porządek sortowania podanej kolumny (jeśli dane nie były posortowane to
porządek będzie rosnący, jeśli były - następuje przełączenie
rosnąco <-> malejąco.
- ustala jako główny klucz
sortowania podaną kolumnę - czyli jeśli lista kluczy jest pusta -
wpisuje na nią SortKey z indeksem kolumny i określonym porządkiem, a
jeśli na liście są już klucze (dane były wcześniej posortowane według
innej kolumny), to dopisuje nowy klucz na początku listy,
- wywołuje metodę setSortKeys(List), która ustala przekazaną listę kluczy jako obowiązująca i wywołuje faktyczne sortowanie (metodę sort()).
Oznacza
to, że kolejne kliknięcia w różne kolumny zmieniają listę kluczy
sortowania. Poniższy kod źródłowy
pozwala prześledzić te zmiany.
public class Tab2 extends JFrame {
public Tab2() {
Object[][] data = { { "Chopin Fryderyk", "M", 1789 },
{ "Chopin Fryderyk", "M", 3000 },
{ "Kopernik Mikołaj", "M", 2900 },
{ "Słowacki Juliusz", "M", 2000 },
{ "Szymborska Wisława", "K", 3200 },
{ "Polański Roman", "M", 3000 },
{ "Skłodowska-Curie Maria", "K", 2000 },
{ "Skłodowska-Curie Maria", "K", 3000 },
{ "Modrzejewska Helena", "K", 2000 },
};
String[] columnNames = { "Nazwisko i imię", "Płeć", "Pensja" };
final JTable tab = new JTable(data, columnNames);
tab.setAutoCreateRowSorter(true);
final JTextArea report = new JTextArea(20, 40);
// Nasłuch kliknięć w kolumny
tab.getTableHeader().addMouseListener(
new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
JTableHeader head = (JTableHeader) e.getSource();
int col = head.columnAtPoint(e.getPoint());
report.append("Kliknięcie w kolumnę '" + tab.getColumnName(col) +
"'\nDane posortowane wg:\n" );
TableRowSorter tsorter = (TableRowSorter) tab.getRowSorter();
List<SortKey> sortKeys = tsorter.getSortKeys();
for (SortKey key : sortKeys) {
report.append("-- " +tab.getColumnName(key.getColumn()) +
" w porządku " + key.getSortOrder() + '\n' );
}
}
}
);
tab.setPreferredScrollableViewportSize(tab.getPreferredSize());
add (new JScrollPane(tab), "North");
add(new JScrollPane(report));
setDefaultCloseOperation(EXIT_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
new Tab2();
}
}
Efekt
działania tego kodu po kolejnych kliknięciach w kolumny 'Pensja',
'Nazwisko i imię', 'Płeć', 'Płeć' wygląda następująco:
W programie tym pokazano przy okazji jak można uzyskać informacje, dotyczące:
- aktualnego sortera tabeli (metoda getRowSorter() z klasy JTable),
- listy kluczy sortowania (metoda getSortKeys() z klasy DefaultRowSorter),
- zawartości kluczy (metody getColumn() i getSortOrder() z klasy SortKey).
Oczywiście, jak zresztą zobaczymy dalej, wszystkie te właściwości można ustalać za pomocą odpowiednich metod set...
Jeszcze
jedna uwaga, dotycząca kodu: przy jego kompilacji wystąpią
ostrzeżenia, dotyczące niesprawdzonych konwersji. Rzecz w tym, że klasy
i metody związane z sortowaniem i filtrowaniem są "silnie"
sparametryzowane. Pominęliśmy na razie te parametry typów, ponieważ w
omawianym przykładzie nie przynoszą one żadnego pożytku, a - co gorsza
- generyzacja klas jest tutaj dość rozbudowana. Dla porządku jednak,
aby uniknąć ostrzeżeń kompilatora należałoby napisać tak:
TableRowSorter<? extends TableModel> tsorter =
(TableRowSorter<? extends TableModel>) tab.getRowSorter();
List<? extends SortKey> sortKeys = tsorter.getSortKeys();
Pokazany
na rysunku porządek uzyskaliśmy klikaniem w kolejne kolumny.
Czasem jest to jednak niewystarczające, albo zbyt nudne.
Zamiast klikać możemy np. zapisać sekwencję wywolań metody toggleSortOrder(...):
tsorter.toggleSortOrder(2);
tsorter.toggleSortOrder(0);
tsorter.toggleSortOrder(1);
tsorter.toggleSortOrder(1);
Można
by tę sekwencję przypisać do jakiegoś skrótu klawiaturowego lub
przycisku - i jednym ruchem uzyskamy to, co poprzednio wymagało
czterech kliknięc.
Ale nie jest to najlepsze rozwiązanie - prostsze
(i bardziej ogólne) będzie bezpośrednie posłużenie się kluczami
sortowania. Nie możemy ingerować w aktualną listę kluczy (zwracaną
przez getSortKeys()), bo sorter udostępnia wyłącznie jej
niemodyfikowalny widok.
Możemy jednak stworzyć własną listę i podać
ją sorterowi, powodując natychmiastowe uporządkowanie według ustalonych
na niej kryteriów.
// sortowanie po kluczach
List<SortKey> sortKeys = Arrays.asList(
new SortKey[] {
new SortKey(1, SortOrder.DESCENDING), // po płci - malejąco
new SortKey(0, SortOrder.ASCENDING), // następnie po nazwisku rosnąco
new SortKey(2, SortOrder.ASCENDING), // następnie po pensji rosnąco
});
tsorter.setSortKeys(sortKeys);
Klikanie jest też niewystarczające.
Nie przywrócimy, na przykład, w ten sposób inicjalnego (nieposortowanego) stanu danych.
Aby to zrobić należy podać sorterowi pustą listę kluczy, np:
// Przywrócenie niuporządkowanego stanu danych
TableRowSorter tsorter = (TableRowSorter) tab.getRowSorter();
List<SortKey> sortKeys = new ArrayList<SortKey>();
tsorter.setSortKeys(sortKeys);
A
dalej - czy użytkownik naszej aplikacji zawsze ma mieć możliwość
sortowania każdej kolumny tabeli przez kliknięcie w nagłówek?
Może
chcielibyśmy udostępnić mu bardziej wyszukany i specjalnie dostosowany
interfejs sortowania, a zabronić klikania w kolumny nagłówka?
Jest i na to sposób.
Wystarczy ustalić sortowalność kolumn (wszystkich lub wybranych) na false za pomoca metody setSortable(int col) z klasy DefaultRowSorter:
// Znieść sortowalnośc kolumn przez toggleSort()
TableRowSorter tsorter = (TableRowSorter) tab.getRowSorter();
for (int i=0; i < tab.getColumnCount(); i++)
tsorter.setSortable(i, false);
Teraz
klikanie w kolumny nie będzie miało żadnego efektu. Również metoda toggleSortOrder(...) nie będzie działać. Nie wyklucza to
jednak sortowania za pomocą ustalenia listy kluczy metodą setSortKeys(...)
i dzięki temu możemy dostarczyć wyspecjalizowanego, dostosowanego do
potrzeb aplikacji, interfejsu sortowania przy jednoczesnym zablokowaniu
możliwości porządkowania tabeli przez kliklanie w kolumny nagłówka.
3. Ikony porządku
Wreszcie: może domyślne ikonki pokazujące porządek sortowania w
kolumnie nie odpowiadają naszym potrzebom. Łatwo można je zmienić za
pomocą odwołań:
UIManager.put("Table.ascendingSortIcon", iconA);
UIManager.put("Table.descendingSortIcon", iconB);
gdzie : iconA i iconB są typu Icon (klas implementujących interfejs Icon, np. ImageIcon, której obiekty stanowią ikonki z plików graficznych).
Bardzo łatwe jest też pokolorowanie domyślnych ikonek. W tym celu nalezy użyć klasy SortArrowIcon z pakietu sun.swing.icon, np:
import sun.swing.icon.*;
// ...
UIManager.put("Table.ascendingSortIcon", new SortArrowIcon(true, Color.RED);
Tutaj true oznacza, że ikona dotyczy porządku rosnącego. Dla malejącego nalezy użyć słówka false.
Bardziej
zaawansowane sposoby wizualizacji uporządkowania danych w tabeli
uzyskamy poprzez zdefiniowanie własnych wykreślaczy nagłówka tabeli
)dla poszczególnych kolumn).
4. Komparatory
Czy wiemy już wszystko co trzeba?
Czy możemy już być zadowoleni z omówionych możliwości i sortowania przykładowej tabeli?
Absolutnie nie.
Zobaczmy
co by się stało, gdyby "pensje" pracowników w tabeli ustalić troszkę
inaczej. Dajmy Chopinowi 3, a Kopernikowi 10. Wynik sortowania wyglądałby następująco:
Nie uzyskaliśmy właściwego porządku. Dlaczego?
Aby odpowiedzieć na to pytanie trzeba rozważyć sposób stosowania przez TableRowSorter kryteriów sortowania.
TableRowSorter opiera porządkowanie na zastosowaniu komparatorów.
Przypomnijmy,
że komparator jest obiektem służącym do porównywania dwóch elementów
zestawu danych. Dokonuje się tego za pomocą metody compare(T o1, T o2)
implementowanej w klasie komparatora. Sortowanie polega na kolejnym
porównywaniu ze sobą dwóch elementów (wg takiego czy innego algorytmu),
zatem właśnie stosowany komparator przesądza o jego wyniku.
Komparatory dla poszczególnych kolumn możemy pobierać za pomocą metody getComparator(int col) i ustalać za pomocą metody setComparator(int col, Comparator) z klasy DefaultRowSorter.
Zobaczmy więc jakie komparatory stosuje wobec kolumn naszej tabeli automatycznie (przez setAutoCreate(...)) stworzony sorter:
// ....
Object[][] data = { { "Chopin Fryderyk", "M", 3 },
{ "Kopernik Mikołaj", "M", 2900 },
// ...
};
String[] columnNames = { "Nazwisko i imię", "Płeć", "Pensja" };
JTable tab = new JTable(data, columnNames);
tab.setAutoCreateRowSorter(true);
// Zobaczmy komparatory
TableRowSorter<? extends TableModel> tsorter =
(TableRowSorter<? extends TableModel>) tab.getRowSorter();
for (int i=0; i < tab.getColumnCount(); i++) {
System.out.println(tsorter.getComparator(i));
}
// ...
Otrzymamyw wyniku:
java.text.RuleBasedCollator@524aa00b
java.text.RuleBasedCollator@524aa00b
java.text.RuleBasedCollator@524aa00b
Okazuje
się, że jest to kolator (komparator stosowane do porównywania napisów z
uwzględnieniem lokalizacji - czyli ustawień regionalnych). Można
sprawdzić, że jest to kolator uzyskiwany dla domyślnej
lokalizacji za pomocą odwołania Collator.getInstance().
Dlaczego taki właśnie został wybrany? Odpowiedź tkwi w modelu danych naszej tabeli.
Modelem jest (tworzony w konstruktorze JTable(Object[][], Object[])) obiekt anonimowej klasy wewnętrznej dziedziczącej AbstractTabelModel.
W takim modelu metoda getColumnClass() zwraca Object, a wtedy sorter
stosuje domyślny kolator wobec wartości kolumn przekształconych w
napisy metodą toString().
Nie możemy się więc dziwić, że pensje potraktowane jako napisy mają inny porządek niż gdyby były traktowane jako liczby.
Ogólnie, sorter dobiera komparatory wedle następującego algorytmu.
- Jeżeli dla kolumny ustalono komparator za pomocą metody setComparator - będzie użyty ten właśnie komparator.
- W przeciwnym razie, jeżeli getColumnClass() modelu danych zwraca klasę String, użyty zostanie domyślny kolator (Collator.getInstance()).
- W przeciwnym razie jeżeli getColumnClass() modelu danych zwraca klasę, która implementuje interfejs Comparable, zostanie użyty prywatny komparator, stosujący metodę compareTo(...) implementowaną w zwróconej klasie.
- W przeciwnym razie, jeżeli dla danej kolumny ustalono sposób przekształcenia w napis za pomocą klasy TableStringConverter, zostanie dokonane to przekształcenie i wobec niego zastosowany domyślny kolator.
- W przeciwnym razie stosowany jest domyślny kolator wobec wartości uzyskanych metodą toString.
W naszym przykładzie miał miejsce przypadek 5.
Anlizując algorytm doboru komparatorów możemy "na szybko" rozwiązać problem sortowania pensji na dwa sposoby:
- zdefiniować i ustalić własny komparator dla pensji,
- zdefiniować TableStringConverter.
Oba
te sposoby nie są właściwe w omawianym przypadku (bowiem mamy kontrolę
nad modelem danych). Mogą sie one okazać przydatne w innych
okolicznościach i dlatego pokazuję je na prostym przykładzie, zwracając
przy okazji uwagę na niektóre pułapki kodowania.
Zacznijmy od własnego komparatora. Należy tu w metodzie compare przekształcić napisy-pensje w liczby całkowite i zwrócić wynik ich porównania.
Wiedząc o parametryzacji komparatorów mamy silną pokusę by napisać tak:
// Uwaga: błędne rozwiązanie!
int col = tab.getColumnModel().getColumnIndex("Pensja");
tsorter.setComparator(col, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.parseInt(s1) - Integer.parseInt(s2);
}
});
Spotka nas jednak niemiła niespodzianka: przy sortowaniu wystąpi wyjątek ClassCastException.
Stanie się tak dlatego, że komparatorowi przekazywane są dane typu zwracanego przez getColumnClass() modelu. A w naszym przypadku jest to typ Object i przy próbie podstawienia na String wystąpi wyjatek ClassCastException.
Poza tym model danych tworzony ad hoc przez konstruktor JTable(Object[][], Object[])
dopuszcza edycję wszystkich komórek tabeli. Łacno się może okazac więc,
że ktoś edytując pensję wprowadził napis nie dający się potraktowac
jako reprezentacja liczby całkowitej.
Lepszy (nie powodujący błędów) komparator winien wyglądać tak:
// ....
// Ustalenie komparatora dla pensji
int col = tab.getColumnModel().getColumnIndex("Pensja");
tsorter.setComparator(col, new Comparator() {
public int compare(Object o1, Object o2) {
int n1, n2;
try {
n1 = Integer.parseInt(String.valueOf(o1));
} catch (NumberFormatException exc) {
n1 = Integer.MIN_VALUE; // nie-liczby będą na końcu
}
try {
n2 = Integer.parseInt(String.valueOf(o2));
} catch (NumberFormatException exc) {
n2 = Integer.MIN_VALUE; // nie-liczby będą na końcu
}
return n1 - n2;
}
});
// ...
Z
tym komparatorem porządkowanie po pensjach będzie już właściwe, ale
oczywiście jest to złe rozwiązanie, bowiem tracimy znacznie na
efektywności przy przekształcebniach Object->String->int.
5. Konwertery napisowe
Drugi sposób szybkiej naprawy wadliwego porządku pensji może polegać na zdefiniowaniu odpowiedniego TableStringConvertera.
Implementując abstrakcyjną metodę tej klasy:
String toString(TableModel model,
int row,
int column)
określamy reprezentację napisową elementów modelu danych.
Będzie ona brana pod uwagę przy sortowaniu gdy tylko ustalimy konwerter dla sortera za pomocą metody setStringConverter. Pokazuje to poniższy kod żródlowy
final DecimalFormat df = new DecimalFormat("000000");
tsorter.setStringConverter( new TableStringConverter() {
@Override
public String toString(TableModel model, int row, int col) {
Object elt = model.getValueAt(row, col);
if (col == 2) return df.format(elt);
else return elt.toString();
}
});
Tutaj metoda format z klasy Format (którą dziedziczy DecimalFormat)
formatuje obiekty jako sześciocyfrowe liczby całkowite z
wiodącymi zerami (o tym mówi wzorzec w konstruktorze DecimalFormat).
Dotyczy to tylko elementów modelu, które są pensjami (trzecia kolumna modelu danych: if (col == 2) ... ). W pozostałych przypadkach StringConverter zwraca wartości uzyskiwane metodą toString().
W
ten sposób pensje (mniejsze od 999999) będą dobrze porządkowane, a inne
kryteria sortowania też będą działać właściwie (bo dane są napisami).
6. Model danych tabeli - klucz do właściwego sortowania i filtrowania
Oba
przedstawione powyżej rozwiązania problemu wadliwego porządkowania
pensji są niedobre, ponieważ wymagają dodatkowych przekształceń danych,
co zmniejsza efektywność sortowania.
Właściwym rozwiązaniem jest stworzenie odpowiedniego modelu danych tabeli.
Jest to zresztą zasada ogólna: w realnych aplikacjach zawsze należy posługiwać się własnym modelem danych.
Własny
model danych dostarcza odpowiednich. właściwych informacji o
typach danych w poszczególnych kolumnach. W związku z tym sorter może
posłużyć się gotowymi, efektywnymi metodami compareTo z klas danych.
Ponadto ewentualne definiowanie własnych komparatorów jest "dziedzinowo" zorientowane i bardziej przejrzyste.
Zróbmy
więc model danych dla tabeli opisującej "pracowników". Przy
okazji łatwo będzie rozbudowac poprzednie przykłady o takie właściwości
pracowników jak data urodzenia czy adres.
Oczywiście, pracownicy w programie powinni być obiektami. Kod źródłowy przedstawia ich klasę.
public class Employee
implements Serializable, Comparable<Employee> {
private String name;
private Address address;
private Date birthDate;
private Integer salary;
public Employee(String name, Address a, Date birthDate,
Integer salary) {
super();
this.name = name;
address = a;
this.birthDate = birthDate;
this.salary = salary;
}
public Employee() {
}
// gettery i setery, np:
public Date getBirthDate() {
return birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
// itd. ...
// ...
// standardowe implementacje metod hashCode() i equals()
@Override
public int hashCode() {
// ...
}
@Override
public boolean equals(Object obj) {
// ...
}
// Naturalny porządek
public int compareTo(Employee o) {
// ...
}
Łatwo
się domyślić, że w poszczególnych kolumnach tabeli pracowników będziemy
chcieli przedstawiać dane, opisywane przez pola klasy Employee. Mamy tu
pola typów String, Date, Integer. Wymienione klasy implementują
interfejs Comparable, zapewniając naturalny porządek, wobec tego sorter
tabeli będzie automatycznie to wykorzystywał. Mamy też pole typu
Address. Tę klasę trzeba zdefiniować i nie zapomnieć przy tym o
właściwiej implementacji interfejsu Comparable. Oto fragmenty definicji klasy Address.
public class Address
implements Serializable, Comparable<Address> {
private String town;
private String street;
private int nr;
public Address(String town, String street, int nr) {
this.town = town;
this.street = street;
this.nr = nr;
}
// ...
// gettery i settery (dla wszystkich pól)
// na przykład:
public int getNr() {
return nr;
}
public void setNr(int nr) {
this.nr = nr;
}
// ...
// Ważna metoda toString()!
public String toString() {
return town + "; " + street + "; " + nr;
}
// Naturalny porządek
public int compareTo(Address a) {
int r = town.compareTo(a.town);
if (r == 0) r = street.compareTo(a.street);
if (r == 0) r = nr - a.nr;
return r;
}
}
Możemy teraz przystąpić do definiowania modelu danych tabeli.
Zrobimy
to w taki sposób, aby łatwo tworzyć modele dal różnych danych i by
mogły one być wykorzystywany w uniwersalnym GUI (mogącym obsłużyć różne
modele). W tym celu zdefiniujemy klasę RowListModel, która opisuje
model tabeli dla dowolnych danych, reprezentowanych jako lista
obiektów. Każdy element listy (obiekt) będzie pokazywany w odrębnym
wierszu tabeli.
Klasa RowListModel
import java.util.*;
import javax.swing.table.*;
public abstract class RowListModel<E> extends AbstractTableModel {
private List<String> colNames;
private List<E> rows;
public RowListModel(List<E> elist, String[] cols) {
rows = new ArrayList<E>(elist);
colNames = Arrays.asList(cols);
}
@Override
public int getColumnCount() {
return colNames.size();
}
@Override
public int getRowCount() {
return rows.size();
}
@Override
public String getColumnName(int column) {
return colNames.get(column);
}
@Override
public Class<?> getColumnClass(int c) {
return getValueAt(0, c).getClass();
}
public E getRow(int r) {
return rows.get(r);
}
public void addRow(E newVal) {
rows.add(newVal);
this.fireTableDataChanged();
}
}
Teraz aby stworzyć model danych dla tabeli pracowników wystarczy zdefiniowac tylko dwie metody: getValueAt i setValueAt:
public class EmpTabModel extends RowListModel<Employee> {
public EmpTabModel(List<Employee> elist, String[] cols) {
super(elist, cols);
}
@Override
public Object getValueAt(int r, int c) {
Employee e = getRow(r);
switch (c) {
case 0 : return e.getName();
case 1 : return e.getAddress();
case 2 : return e.getBirthDate();
case 3 : return e.getSalary();
default : return null;
}
}
@Override
public void setValueAt(Object val, int r, int c) {
Employee emp = getRow(r);
switch (c) {
case 0 : emp.setName((String) val); break;
case 1 : emp.setAddress((Address) val); break;
case 2 : emp.setBirthDate((Date) val); break;
case 3 : emp.setSalary((Integer) val); break;
default : break;
}
this.fireTableCellUpdated(r, c);
}
}
Aby
zobaczyć jak działa przygotowany model danych w połączeniu z sortowalną
tabelą użyjemy przykładowej aplikacji GUI. Jest ona przygotowana w taki
sposób, by
można było także prześledzić dodatkowe niuanse sortowania, jak również
filtrowanie tabeli (o czym będzie dalej).
Działanie aplikacji przedstawia poniższy rysunek.
Przykładowa aplikacja - tablica z modelem danych EmpTabModel
Jak
widać sortowanie adresów działa bez zarzutu bez żadnych dodatkowych
starań z naszej strony (bo klasa Address implementuje właściwą metodę
compareTo() i jej właśnie używa sorter). Ponadto daty pokazywane są we
właściwym zlokalizowanym formacie, a liczby - są liczbami, co widac z
wyrównania do prawej. Wystarczyło ustalić model danych dla tabeli
i gotowe.
7. Uniwersalne GUI sortowania i filtrowania tabel
Kod GUI napisany jest
w sposób uniwersalny, tak by mógł obsługiwać różne modele danych.
Uniwersalne GUI sortowania i filtrowania tabel
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.table.*;
import static tabsort.GuiUtils.*;
public class TableGUI<M extends RowListModel<E>, E> extends JFrame implements ActionListener {
private static ImageIcon specFiltersImg =
new ImageIcon(tabsort.TableGUI.class.getResource("images/help.gif"));
// Model danych
private M model;
// Tabela
private JTable table;
// Sorter
private TableRowSorter<M> sorter;
private FilterDialog<M> filterDialog;
private SortPanel sortPanel;
public TableGUI(String title, final M model) {
super(title);
this.model = model;
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
// Utworzenie sortera
sorter = new TableRowSorter<M>(model);
// Tabela i ustalenie sortera
table = new JTable(model);
table.setRowSorter(sorter);
// Szybkie wyrównanie dat (jesli są) do prawej (bez własnego renderera)
TableCellRenderer cellRenderer = table.getDefaultRenderer(Date.class);
((JLabel) cellRenderer).setHorizontalAlignment(JLabel.RIGHT);
table.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isAltDown()) {
int r = table.rowAtPoint(e.getPoint());
r = table.convertRowIndexToModel(r);
SimpleDialog.show(TableGUI.this, "Edycja", model, r);
}
else if (e.isMetaDown()) {
JPopupMenu pm = getPopupSortKeysWindow();
pm.show(e.getComponent(), e.getX(), e.getY());
}
}
});
add(new JScrollPane(table));
Box controls = Box.createHorizontalBox();
controls.setBorder(BorderFactory.createEmptyBorder(10, 5, 5, 5));
Dimension filler = new Dimension(5,5);
controls.add(button("Sort ...", "cmd:sort", this));
controls.add(Box.createRigidArea(filler));
controls.add(button("Filter ...", "cmd:filter", this));
controls.add(Box.createGlue());
controls.add(button("Add record ...", "cmd:add", this));
controls.add(Box.createRigidArea(filler));
controls.add(check("Update on insert ?", "cmd:updateable", this));
controls.add(Box.createGlue());
controls.add(label(specFiltersImg, "hint:Special filters", showSpecialsPopupMenu));
add(controls, "South");
}
public M getModel() {
return model;
}
public TableRowSorter<M> getSorter() {
return sorter;
}
public JTable getTable() {
return table;
}
private JPopupMenu getPopupSortKeysWindow() { // panel sortowania dostępny jako popup
final JPopupMenu popup = new JPopupMenu();
if (sortPanel == null) sortPanel = new SortPanel(table);
sortPanel.setCloseButton(true);
popup.add(sortPanel);
return popup;
}
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
M model = ((M) table.getModel());
if (cmd.equals("add")) {
// obsługa dodoawnia nowego rekordu
} else if (cmd.equals("filter")) { // dialog filtrowania
if (filterDialog == null) filterDialog =
new FilterDialog<M>(TableGUI.this, "Filtrowanie", model);
RowFilter<? super M, ? super Integer> filtr = filterDialog.getFilter();
sorter.setRowFilter(filtr);
} else if (cmd.equals("sort")) { // dialog sortowania
JDialog sortDialog = new JDialog(TableGUI.this, "Sortowanie", true);
if (sortPanel == null) sortPanel = new SortPanel(table);
sortPanel.setCloseButton(false);
sortDialog.add(sortPanel);
sortDialog.pack();
sortDialog.setLocationRelativeTo(TableGUI.this);
sortDialog.setVisible(true);
} else if (cmd.equals("updateable")) {
JToggleButton tb = (JToggleButton) e.getSource();
sorter.setSortsOnUpdates(tb.isSelected());
}
}
// Menu dla specjalnych akcji, które można okreslić z zewnątrz
private JPopupMenu specialsPopupMenu;
MouseListener showSpecialsPopupMenu = new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if (specialsPopupMenu == null) return;
Dimension d = specialsPopupMenu.getPreferredSize();
specialsPopupMenu.show(e.getComponent(),
e.getX()-d.width , e.getY()-d.height);
}
};
public void setSpecialsPopupMenu(Action[] specialActions) {
specialsPopupMenu = new JPopupMenu();
for (Action action : specialActions) {
specialsPopupMenu.add(action);
}
}
}
Uwagi:
- wykorzystano specjalnie przygotowane klasy SimpleDialog, SortPanel i FilterDialog,
- w
specjalnie przygotowanej klasie GuiUtils znajdują się wygodne metody
button)...) label (...) itp. Dzięki niem kod tworzący komponenty
i nadający im właściwości jest bardzo zwięzły i klarowny,
- zamiast korzystać z metody setAutocreateRowSorter(...) utworzony został odrębny obiekt-sorter i ustalony dla tabeli metodą setRowSorter(...).
Następujący kod źródłowy pokazuje wykorzystanie odpowiedniego modelu danych i uniwersalnego GUI w konkretnej aplikacji.
Aplikacja tablicowa - "Pracownicy"
// odpowiednie importy
public class EmployeeApp {
private TableGUI<EmpTabModel, Employee> gui;
public EmployeeApp() {
// Metoda genarate tworzy w sposób losowy lisę pracowników
// tu o rozmiarze 100 elementów
List<Employee> elist = Employee.generate(100);
// Nazwy kolumn
String[] colNames = { "Nazwisko i imię", "Adres",
"Data urodzenia", "Pensja" };
// Utworzenie modelu danych
EmpTabModel model =
new EmpTabModel(elist, colNames);
// Utworzenie GUI
gui = new TableGUI<EmpTabModel, Employee>("Aplikacja tablicowa - pracownicy",
model);
gui.setSpecialsPopupMenu(specialActions);
gui.pack();
gui.setLocationRelativeTo(null);
gui.setVisible(true);
}
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(
"com.jgoodies.looks.plastic.PlasticXPLookAndFeel"
);
} catch (Exception e) {
e.printStackTrace();
}
SwingUtilities.invokeLater( new Runnable() {
public void run() {
new EmployeeApp();
}
});
}
}
8. Model + uniwersalne GUI = łatwe dostosowania i modyfikacje
Dzięki
zastosowanemu podejściu (prosty model danych i uniwersalne GUI)
bardzo łatwo i szybko można tworzyć sortowalne i filtrowalne tabele dla
innych niż przykład pracowników przypadków. Oto w kilka minut możemy
zbudować podobną do poprzedniej aplikację, tyle że teraz dotyczącą
samochodów. Jej dzialanie pokazuje rysunek.
Dzialanie aplikacji tablicowej - samochody (posortowana kolumna "Rocznik")
Poniższe kody źródłowe są wszystkim, co potrzebne, aby uzyskać taką funkcjonalność.
Klasa samochodów
import javax.swing.*;
public class Car {
private String make;
private String model;
private Integer year;
private Integer price;
private Icon icon;
public Car(String make, String model, Integer year, Integer price, Icon icon) {
super();
this.make = make;
this.model = model;
this.year = year;
this.price = price;
this.icon = icon;
}
public String getMake() {
return make;
}
public void setMake(String make) {
this.make = make;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public Integer getYear() {
return year;
}
public void setYear(Integer year) {
this.year = year;
}
public Icon getIcon() {
return icon;
}
public void setIcon(Icon icon) {
this.icon = icon;
}
}
Model danych dla tabeli samochodów
public class CarTabModel extends RowListModel<Car> {
public CarTabModel(List<Car> elist, String[] cols) {
super(elist, cols);
}
@Override
public Object getValueAt(int r, int c) {
Car o = getRow(r);
switch (c) {
case 0 : return o.getMake();
case 1 : return o.getModel();
case 2 : return o.getYear();
case 3 : return o.getPrice();
case 4 : return o.getIcon();
default : return null;
}
}
@Override
public void setValueAt(Object val, int r, int c) {
Car car = getRow(r);
switch (c) {
case 0 : car.setMake((String) val); break;
case 1 : car.setModel((String) val); break;
case 2 : car.setYear((Integer) val); break;
case 3 : car.setPrice((Integer) val); break;
case 4 : car.setIcon((Icon) val); break;
default : break;
}
this.fireTableCellUpdated(r, c);
}
}
Aplikacja "samochody"
public class CarApp {
private TableGUI<CarTabModel, Car> gui;
public CarApp() {
Class kl = getClass();
ImageIcon icon = new ImageIcon(kl.getResource("images/lockkey.gif"));
List<Car> clist = Arrays.asList( new Car[] {
new Car("Plymouth", "Barracuda", 1970, 300,
new ImageIcon(kl.getResource("images/plym.jpeg" ))),
new Car("Lamborghini", "Murcielago", 2007, 1000,
new ImageIcon(kl.getResource("images/lamborghini.jpeg"))),
new Car("Jeep", "Cherokee", 1997, 6, icon),
new Car("Jeep", "Cherokee", 2001, 12, icon),
new Car("Fiat", "Spider", 1982, 40, icon),
new Car("Fiat", "Bravo", 2005, 40, icon),
new Car("Mitsubishi", "Galant", 2007, 80,
new ImageIcon(kl.getResource("images/mitsubishi.jpg"))),
});
// Nazwy kolumn
String[] colNames = { "Marka", "Model",
"Rocznik", "Cena", "Foto" };
// Utworzenie modelu danych
CarTabModel model =
new CarTabModel(clist,colNames);
gui = new TableGUI<CarTabModel, Car>("Aplikacja tablicowa - samochody", model);
gui.getTable().setRowHeight(70);
gui.getTable().getColumn("Foto").setWidth(70);
gui.pack();
gui.setLocationRelativeTo(null);
gui.setVisible(true);
}
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(
"com.jgoodies.looks.plastic.PlasticXPLookAndFeel"
);
} catch (Exception e) {
e.printStackTrace();
}
SwingUtilities.invokeLater( new Runnable() {
public void run() {
new CarApp();
}
});
}
}
Zobacz działanie obu aplikacji, uruchamianych z programu testowego MainTest
9. Sortowanie po kluczach
Widoczny na poprzednich rysunkach przycisk "Sort..." otwiera dialog zaawansowanego sortowania.
Pokazuje go poniższy rysunek.
Lista
po lewej stronie zawiera nazwy kolumn - dostępne klucze (ten fragment
programu również jest generyczny, przygotowany na dzialanie z dowolnymi
tabelami - lista uzyskiwana jest dynamicznie od modelu tabeli).
Aktualne klucze dodajemy do prawej listy przyciskiem >>.
Przycisk "Sort" sortuje tabelę wg wybranych kluczy, "Unsort" przywraca jej nieposortowaną postać.
Domyślnie porządek sortowania jest rosnący, można to zmienić znacznikiem "Descending".
Poniższy kod żródłowy pokazuje obsługę przycisków "Sort" - "Unsort".
// ...
// Sorter
private TableRowSorter<? extends TableModel> tsorter;
// Model danych prawej listy (wybranych kluczy sortowania)
private DefaultListModel sortKeysListModel = new DefaultListModel();
// Lista kluczy sortowania
private List<SortKey> sortKeys = new ArrayList<SortKey>();
private JRadioButton rbSortOrder = new JRadioButton("Descending");
// ...
// Obsługa przycisków
ActionListener sortHandler = new ActionListener() {
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
// ...
if (cmd.endsWith("sort")) {
sortKeys.clear(); // zawsze świeża lista
if (!cmd.startsWith("un")) { // przycisk "Sort"
// Porządek sortowania
SortOrder sortOrder = SortOrder.ASCENDING;
if (rbSortOrder.isSelected()) sortOrder = SortOrder.DESCENDING;
// ile kluczy? gdy więcej od 3 trzeba ustalić MaxSortKeys
int skn = sortKeysListModel.getSize();
if (skn > 3) tsorter.setMaxSortKeys(skn);
// Wybrane nazwy kolumn
List<?> klist = Collections.list(sortKeysListModel.elements());
// Dla każdej wybranej kolumny ustalamy klucz sortowania
for (Object col : klist) {
int ind = tab.getColumnModel().getColumnIndex(col);
sortKeys.add(new SortKey(ind, sortOrder));
}
}
// Sortowanie
// (lub przywróoenie stanu "nieposortwana" gdy przycisk "Insort")
tsorter.setSortKeys(sortKeys);
}
// ...
}
};
Zwrócmy uwagę, że gdy liczba kluczy sortowania przekracza 3, nalezy użyć metody setMaxSortKeys.
10. Edycja
Jeśli chodzi o edycję posortowanej tabeli to warto zwrócić uwagę na pewne niuanse.
Gdy dodajemy nowe rekordy do tabeli domyślnie trafiają one na koniec.
Możemy zmienić to zachowanie (nowy rekord znajdzie się w aktualnym porządku sortowania) używając metody:
sorter.setSortsOnUpdates(true);
Przy
edycji istniejących danych tabeli bardzo ważną kwestią jest
uwzględnienie właściwych dla modelu indeksów wierszy. Edytory komórek
(w tym domyślne) sprawę załatwiają automatycznie. Jeśli jednak tworzymy
jakiś inny edytor, bazujący na zaznaczonym w widoku tabeli wierszu,
konwersję pomiędzy indeksem wiersza w widoku i indeksem wiersza w
modelu musimy zagwarantować sami. Jest tak dlatego, że po posortowaniu
tabeli indeksy wierszy w widoku nie odpowiadają indeksom w modelu.
Jest
to bardzo ważna i wygodna właściwość sortera: porządkowaniu podlega
widok, a nie model. Dzięki temu możemy mieć różne widoki na
ten sam model, inaczej uporządkowane. Ale oznacza to również, że
musimy zapewnić właściwą konwersję indeksów wierszy.
Służy do tego metoda convertRowIndexToModel(...).
Dla
przykładu, dodając do tabeli odpowiedni MouseListener możemy na
zaznaczonym wierszu alt-kliknięciem otworzyć dialog edycji, co wygląda tak:
W kodzie obsługi alt-kliknięcia ważne jest uzyskanie właściwego indeksu wiersza:
// ...
table.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isAltDown()) {
int r = table.rowAtPoint(e.getPoint());
r = table.convertRowIndexToModel(r);
SimpleDialog.show(TableGUI.this, "Edycja", model, r);
}
}
});
// ...
Edycję (a także wpisywanie nowych rekordów) zapewniamy poprzez własną klasę SimpleDialog (tutaj jej kod nie jest istotny).
11. Filtrowanie wierszy
Filtry służą do pokazywania tylko tych wierszy, które spełniają określone warunki.
Warunki określane są przez metodę boolean include(...), którą należy zdefiniować w klasie dziedziczącej abstrakcyjną klasę RowFilter.
Metoda otrzymuje jako argument obiekt typu RowFilter.Entry, od którego
można uzyskać informacje dotyczące wiersza i powinna zwracać true, jeśli dany wiersz ma być widoczny, false w przeciwnym razie.
Utworzony
obiekt-filtr ustala się za pomocą metody setRowFilter(..) wołanej wobec
sortera, co powoduje ukrycie tych wierszy, dla których include zwraca false.
Klasa
RowFilter dostarcza gotowych, łatwych w użyciu, filtrów, które
uzyskujemy statycznymi metodami numberFilter(...), dateFilter(...) i
regexFilter(...).
Metoda:
RowFilter.numberFilter(RowFilter.ComparisonType c, Number n, int ... indeksy)
daje
filtr, którego metoda include zwraca true tylko dla wierszy, które
zawierają choć jedną wartość numeryczną spełniającą kryterium zadawane
przez typ prównania (c) i liczbę (n).
Typy porównania to stałe wyliczeniowe z enumeracji RowFilter.ComparisonType:
- EQUAL - równe
- NOT_EQUAL - nierówne
- AFTER - większe,
- BEFORE - mniejsze
Jeśli
podano indeksy, będą brane pod uwagę tylko wartości wierszy w kolumnach
o podanych indeksach, w przeciwnym razie - we wszystkich kolumnach typu Number lub
pochodnego od Number.
Przykład: pokazać tylko tych pracowników, którzy mają pensję > 3000:
// sorter jest sorterem ustalonym dla tabeli pracowników
pensja = 3000;
kolumna = 3; // indeks kolumny pensji
sorter.setRowFilter(RowFilter.numberFilter(ComparisonType.AFTER, pensja, kolumna));
Tak samo działa gotowy filtr dat, z tym, że porównania dotyczą dat:
RowFilter.dateFilter(RowFilter.ComparisonType c, Date d, int ... indeksy)
Przykład: pokazać tylko tych pracowników, którzy urodzili się przed 1 stycznia 1980.
SimpleDateFormat dateFormat =
new SimpleDateFormat("yyyy-MM-dd");
// ...
try {
Date data = dateFormat.parse("1980-01-01");
sorter.setRowFilter(RowFilter.dateFilter(ComparisonType.BEFORE, data, 2));
} catch (ParseException exc) {
System.out.println("Wadliwa data");
}
Gotowy filtr regularnych wyrażeń:
RowFilter.regexFilter(String regex, int ... indeksy);
pokazuje
tylko te wiersze, w których znajduje się choć jedna wartość pasująca do
regularnego wyrażenia regex (dopasowanie metodą find(), a więc
odnajdującą wystąpienie, a nie dopasowującą cały tekst jak match()).
Warto
zauwazyć, że ten filtr posługuje się wartościami kolumn
przekształconymi na napisy za pomocą metody toString(). Zatem
przykładowy fragment:
// ...
String regex = JOptionPane.showInputDialog("Regex:");
if (regex != null)
sorter.setRowFilter(RowFilter.regexFilter(regex));
// ...
zastosowany wobec tabeli pracowników da następujący wynik.
Działanie filtra regex
Tutaj
np. widzimy p. Stefana Wabackiego dlatego, bo ma napis 80 w
końcówce pensji, a p. Tabackiego, bo ma 80 w końcówce roku urodzenia.
Zwróćmy uwagę, że nie podaliśmy żadnych indeksów kolumn, dlatego pod
uwagę są brane wartości ze wszystkich kolumn.
Tworząc własne filtry musimy odziedziczyć klasę RowFilter i dostarczyć implementacji metody include.
Warto
przy tym wiedzieć, że klasa RowFilter jest parametryzowana dwoma
parametrami. W zamierzeniu, przy użyciu z TableRowSorterem, pierwszy
oznacza typ modelu danych, drugi - typ identyfikatora wiersza (dla
TableRowSortera Integer, indeks wiersza).
Dzięki tej parametryzacji w metodzie include
bez zbędnych konwersji zawężających możemy sięgać do modelu
danych i do indeksów wierszy. Informacji dostarcza argument metody include - RowFilter.Entry. Klasa ta zawiera metody ich pobierania:
- getModel() - zwraca model,
- getIdentifier() - identyfikator wiersza (w modelu! dla tabeli będzie to indeks wiersza),
- getValueCount() - liczbę wartości w wierszu (gdy mowa o tabeli będzie to liczba kolumn),
- getValue(int i) - i-ta wartość w wierszu (dla tabeli wartość zapisana w i-ej kolumnie),
- getStringValue(int i) - i-ta wartość, przekształcona na napis metodą toString().
Poniższy kod
źródłowy przedstawia filtr, pozostawiający w widoku tabeli tylko
tych pracowników, którzy mają podane imię (jedno słowo) i
mieszkają przy podanej ulicy (też dla uproszczenia jedno słowo).
String in = showInputDialog("Street and firstName");
if (in == null) return;
final String[] data = in.split(" +");
RowFilter<EmpTabModel, Integer> filtr = new RowFilter<EmpTabModel, Integer>() {
@Override
public boolean include(RowFilter.Entry<? extends EmpTabModel,
? extends Integer> entry) {
EmpTabModel model = entry.getModel();
int row = entry.getIdentifier();
Employee emp = model.getRow(row);
String[] names = emp.getName().split(" +");
return emp.getAddress().getStreet().equals(data[0].trim()) &&
names[1].equals(data[1].trim());
}
};
JTable table = gui.getTable();
TableRowSorter<EmpTabModel> sorter = gui.getSorter();
sorter.setRowFilter(filtr);
Zauważmy,
że sorter też jest sparametryzowany, co właśnie wynika z - wygodnej
przy pisaniu metody include - parametryzacji filtrów.
Po
zastosowaniu filtra można nie tylko zobaczyć w tabeli efekty
filtrowania, łatwo można również uzyskać aktualnie widoczne wiersze, co
pokazuje poniższy fragment:
int rows = sorter.getViewRowCount();
for (int i=0; i < rows; i++) {
System.out.println(table.getValueAt(i, 0) + " - " +
table.getValueAt(i, 1));
}
Liczbę widocznych wierszy zwraca metoda sortera getViewRowCount().
Wartości pobieramy metodą getValueAt(int row, int col) z klasy JTable.
Należy pamiętać, że indeksy wierszy i kolumn dotyczą tu widoku, a
nie modelu (o co właśnie nam chodzi).
Przykładowy wynik - wynik filtracji po imionach i ulicach (ulica "Polna", imię "Jerzy")
Ubacki Jerzy - Poznań; Polna; 111
Gabacki Jerzy - Warszawa; Polna; 76
Ibacki Jerzy - Warszawa; Polna; 73
Abacki Jerzy - Kraków; Polna; 94
Babacki Jerzy - Kraków; Polna; 51
Dowolne filtry (i te gotowe z klasy RowFilter i nasze własne) można łączyć za pomocą metod andFilter i orFilter.
Metoda
andFilter łączy filtry, stosując logiczną koniunkcję wobec ich
działania (wynikowy filtr będzie pozostawiał w widoku te wiersze, które
spełniają kryteria wszystkich filtrów).
Metoda orFilter zwraca wynikowy filtr, który pozostawia wiersze, spełniające warunki zadawane przez którykolwiek z filtrów.
Argumentem
metod jest zestaw filtrów, podawany jako referencja do obiektu klasy
implementującej Iterable. Zazwyczaj będziemy tu stosowac jakieś
standardowe kolekcje (wszystkie implementują interfejs Iterable).
Kod
źródłowy pokazuje przykład łączenia filtrów.
Przykład łączenie filtrów:
String regex = showInputDialog("Enter regex for names");
if (regex == null) return;
String number = showInputDialog("Enter minimal salary");
if (number == null) return;
int minSal = new Scanner(number).nextInt();
// Lista filtrów łączonych za pomocą operacji AND lub OR
List<RowFilter<? super EmpTabModel, ? super Integer>> filterList =
new ArrayList<RowFilter<? super EmpTabModel, ? super Integer>>();
// Wynikowy filtr
RowFilter<? super EmpTabModel, ? super Integer> rowFilter;
filterList.add(RowFilter.regexFilter(regex, 0));
filterList.add(RowFilter.numberFilter(ComparisonType.AFTER, minSal, 3));
rowFilter = RowFilter.andFilter(filterList);
gui.getSorter().setRowFilter(rowFilter);
Niestety,
nieco zawikłana parametryzacja (użycie ? super) jest tu niezbędna,
po to choćby by móc dostarczać filtrów z parametrami <Object,
Object>.
Wreszcie, działanie każdego filtru (również uzyskanego metodami andFilter i orFilter mozna odwrócić za pomocą metody RowFilter notFilter(RowFilter).