Последнее изменение: 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)

Второй параметр метода setSortPreferencesboolean 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. В архиве также находятся тестовый пример и файл для antbuild.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. Всё, симметрия нарушена.

Поскольку я не знаю, с какими объектами придется иметь дело в таблице – я не могу предложить общего решения для обхода данной проблемы. Единственный способ уберечься от нее – разрешить использование в столбце объекты только одного класса. Что и было сделано.