Последнее изменение: 30 сентября 2007г.
Сортируемая и фильтруемая модель таблицы
Так получилось, что GUI в Java в последние пару лет я занимался постольку-поскольку. А если быть честным – вообще практически не занимался. Соответственно, какой-то нехватки компонент я не испытывал, исповедуя к тому же принцип – все, чего мне не хватает, я могу написать сам.
Потому, когда в форуме возник вопрос – а нет ли у кого-нибудь таблицы с возможностью сортировки и фильтрации по содержимому строк – я в первый момент был в некотором недоумении. Написание такой таблицы занимает от силы дня два-три, и потому мне было несколько странно, что кто-то ее ищет, вместо того, чтобы написать самому. Однако позднее эта задача показалась мне довольно интересной с архитектурной точки зрения. Потратив пару вечеров, я таки написал модель таблицы с упомянутыми возможностями. Забавно, что на написание примера, иллюстрирующего использование этой модели, у меня ушло куда больше времени.
А еще позднее оказалось, что эти требования к таблице актуальны настолько, что их даже реализуют в версии Java 6.0. По этой причине я решил выложить свою компоненту для общего пользования.
Это описание делится на три части:
- Описание библиотеки – основные классы и интерфейсы
- Руководство по использованию – принципы работы и советы по реализации кода, взаимодействующего с библиотекой.
- Исходники и пример – архив с исходниками и тестовым примером, а так же инструкции по сборке и запуску теста.
Библиотека написана с использованием generics, потому может использоваться только с Java 5.0+. Для реализации фильтрации используется библиотека фильтрации, она находится в том же архиве.
* * *
Описание библиотеки
Все классы библиотеки находятся в пакете ru.skipy.ui.components.sft
. Пользователю доступны четыре из них –
SortMode
, ColumnSortPrefs
, TableRow
и SnFTableModel
Класс SortMode
представляет собой режим сортировки столбца таблицы. Вообще, это enumeration. В классе два
элемента, представляющих собой режимы – ASCENDING
и DESCENDING
. Эти элементы содержат также
знаковые константы – 1 и -1 соответственно. Эти константы используются при сортировке (поскольку comparator сортирует
элементы только в режиме ASCENDING
, для режима DESCENDING
нужно менять знак возвращаемого
им значения).
Класс ColumnSortPrefs
представляет режим сортировки для определенного столбца модели. Он содержит индекс
столбца, а так же режим сортировки для него.
Класс TableRow
, по-хорошему, вообще не должен быть открытым. Он сделан таковым по единственной причине –
фильтр должен быть параметризован этим типом. Однако public
-конструктора у него все равно нет. Использование
методов класса описано ниже.
Последний класс – SnFTableModel
. Это реализация интерфейса javax.swing.table.TableModel
.
Все особенности реализации и использования – в следующей части.
Руководство по использованию
В использовании модели можно выделить три основных момента:
Создание модели
Прежде всего, об отличиях от стандартной реализации (javax.swing.table.DefaultTableModel
). Они есть, и
существенные.
Во-первых, таблица не может быть создана пустой. Во всяком случае, пока. Она строится на основании реальных данных, передаваемых в конструкторе.
Во-вторых, требуется явное указание классов столбцов. Связано это с сортировкой. В стандартной таблице класс столбца рассчитывался как общий родитель всех объектов в столбце.
Более того, все объекты в столбце должны быть именно указанного класса. Не допускаются даже наследники. Это довольно жесткое ограничение, но оно имеет под собой серьезное обоснование1.В третьих, таблица неизменяемая. В будущем я планирую реализовать возможности добавить/удалить столбец/строку. Но пока это так.
В четвертых,
null
в качестве значения присутствовать не может. Это значение не пройдет проверку на принадлежность к классу столбца. Опять-таки это связано с сортировкой.
Нелишне будет также упомянуть, что длина массива имен столбцов должна быть равна длине массива классов столбцов. И точно такую же длину должны иметь все строки в массиве данных. Но это ограничение в достаточно степени логично.
В остальном это обычная модель таблицы. И создавать таблицу на ее основе нужно точно так же:
// data definition public static final String[] COLUMN_NAMES = new String[]{"Owner name", "Phone number", "Gender"}; public static final Object[][] DATA = new Object[][]{ {"Ann Mary Smith", new PhoneNumber(7, 926, 4610519), Gender.female}, {"John Doe", new PhoneNumber(7, 916, 2468876), Gender.male} }; public static final Class[] COLUMN_CLASSES = new Class[]{String.class, PhoneNumber.class, Gender.class}; // table model creation model = new SnFTableModel(COLUMN_CLASSES, COLUMN_NAMES, DATA); JTable table = new JTable(model);
На очереди следующий раздел, касающийся такого явления как
Сортировка
Идеология сортировки такова. Если класс столбца реализует интерфейс java.lang.Comparable
, то по этому
столбцу можно производить сортировку. Если же нет – для сортировки нужно регистрировать в модели соответствующую реализацию
интерфейса java.util.Comparator
. Comparator
регистрируется для определенного класса:
model.registerComparatorForClass(PhoneNumber.class, PhoneNumberComparators.INSTANCE);
Обратите внимание на объявление этого метода:
public synchronized <T> void registerComparatorForClass(Class<T> clazz, Comparator<T> comparator)
Это объявление означает, что при реализации интерфейса java.util.Comparator
он должен быть параметризован
тем же самым типом (классом), для которого он регистрируется. Попытка зарегистрировать comparator
для другого
класса приведет к ошибке еще на стадии компиляции. И что бы я делал без generics?.. :)
Для тех, кто не очень хорошо знаком с generics, хочу разъяснить один момент. Если у нас есть класс
A
, то класс Class<A>
в точности эквивалентен A.class
. Таким образом, в
этот метод надо передавать класс, получаемый через .class
, так, как это показано в примере чуть выше.
Перейдем теперь к методам задания параметров сортировки. Их у модели два:
setSortPreferences(ColumnSortPrefs[] prefs, boolean sortImmediately) dropSortPreferences()
Второй из методов, как это понятно, сбрасывает все установки. Остановимся более подробно на первом. Он принимает в
качестве параметров массив объектов ColumnSortPrefs
и булевское значение.
Объект класса ColumnSortPrefs
, как я уже говорил, представляет собой настройки сортировки для определенного
столбца. Он содержит индекс столбца в модели, а так же параметр, указывающий на направление сортировки. Параметр этот –
элемент enumeration SortMode
.
Если длина массива более 1, сортировка производится по всем указанным столбцам, начиная с первого (в массиве, не в таблице!). Сначала по первому из указанных столбцов, если наблюдается равенство – по второму, и т.д.
При вызове метода setSortPreferences
производится проверка – по всем ли столбцам из указанных в массиве
возможна сортировка. Если нет – будет инициировано исключение IllegalArgumentException
.
Для того, чтобы проверить возможность сортировки по определенному столбцу, в модели предусмотрен метод
public boolean canBeSortedByColumn(int columnIndex)
Второй параметр метода setSortPreferences
– boolean sortImmediately
. Этот параметр
позволяет отложить сортировку на более поздний момент. Это может быть полезным в том случае, если одновременно с параметрами
сортировки необходимо выставить еще и фильтр. Если сортировка отложена, ее можно инициировать вызовом
filterAndSort
. Но в этом случае сначала будет произведена фильтрация. Если же сортировка выполняется немедленно
– фильтрация не производится. В отличие от метода setSortPreferences
действие метода
dropSortPreferences
происходит незамедлительно.
О сортировке, пожалуй, всё. Ее реализация есть в примере. Перейдем к следующему разделу.
Фильтрация
При фильтрации используется внешняя библиотека. Собственно, эта библиотека вышла как раз из данного проекта.
Модели можно установить фильтр – реализацию интерфейса ru.skipy.filtering.Filter
. Единственное ограничение,
налагаемое на эту реализацию – Filter
должен быть параметризован типом
ru.skipy.ui.components.sft.TableRow
. Разумеется, можно использовать любые составляющие арифметики фильтров,
параметризовав их этим классом.
Сама фильтрация как процесс особо интереса не представляет, ибо по сути своей тривиальна. Остановимся более подробно на
классе ru.skipy.ui.components.sft.TableRow
. Метод getObjectAt
возвращает объект строки по индексу
столбца. Гораздо интереснее с точки зрения фильтрации следующий метод:
public <T> T getCastedObjectAt(int index, Class<T> clazz)
Этот метод позволяет вернуть объект, содержащийся в строке, в виде экземпляра класса, передаваемого как второй параметр.
Это может быть очень удобно для фильтрации, т.к. позволяет избежать лишних приведений типа и улучшает читаемость кода. Если
же объект не может быть приведен к нужному типу – будет инициировано исключение IllegalArgumentException
.
Пример использования этого метода:
public boolean accept(TableRow tableRow) { return tableRow.getCastedObjectAt(PHONE_IDX, PhoneNumber.class).getAreaCode() > 900; }
В данном случае вызов tableRow.getCastedObjectAt(PHONE_IDX, PhoneNumber.class)
возвращает объект из
столбца PHONE_IDX
строки tableRow
как экземпляр класса PhoneNumber
.
Как и в случае с сортировкой, при установке фильтра можно отложить его применение. В дальнейшем его можно инициировать
всё тем же вызовом filterAndSort
. Сбрасываются установки фильтрации вызовом dropFilter
, и
происходит это немедленно.
Никаких других тонкостей в реализации фильтров, пожалуй, нет. Два фильтра реализованы в примере.
Исходники и пример
Библиотеку можно взять вместе с исходным кодом, вот тут: sftable.zip. В архиве также
находятся тестовый пример и файл для ant
– build.xml
, с помощью которого можно собрать
библиотеку и запустить пример (команда run-test
). Напоминаю, что библиотека работает только с Java 5.0+,
потому переменная JAVA_HOME
должна быть настроена на директорию, в которой установлен Java SDK версии не
ниже 5.0.
Всё. Пользуйтесь. Надеюсь, эта библиотека пригодится кому-нибудь. С вопросами, комментариями, пожеланиями и т.п. – добро пожаловать в почту. Адрес есть внизу страницы.
В планах по развитию библиотеки – реализация возможности добавления и удаления строк/столбцов. Когда это будет сделано – пока не знаю. Когда руки дойдут!
1. Замечание по поводу ограничения на использование наследников. Существует
фундаментальная проблема сравнения родительского и дочернего классов. По определению сравнения должно выполняться
условие симметрии – если a == b
, то b == a
. Иначе говоря, если a.compareTo(b) == 0
,
то b.compareTo(a) == 0
. Однако это условие, как правило, нарушается при сравнении родительского и дочернего
объектов.
Пусть родительский объект – a
, дочерний – b
. Общие поля (поля класса
A
) у них равны. Рассмотрим сравнение a.compareTo(b)
. Родительский класс A
ничего не
знает о дочернем классе B
, потому он будет интерпретировать переданный ему объект как объект класса
A
. Поскольку общие поля у них равны, результат сравнения – 0
.
Рассмотрим теперь обратную ситуацию – сравнение b.compareTo(a)
. Объект класса
A
не может быть приведен к классу B
, потому по определению compareTo
должен
инициировать исключение ClassCastException
. Всё, симметрия нарушена.
Поскольку я не знаю, с какими объектами придется иметь дело в таблице – я не могу предложить общего решения для обхода данной проблемы. Единственный способ уберечься от нее – разрешить использование в столбце объекты только одного класса. Что и было сделано.