Фильтрация моделей компонента JLIST

В статье от 15 ноября 2005 года Sorting and Filtering Tables было показано как новые функции сортировки и фильтрации моделей компонента JTable в Java SE 6. В Java SE 6 не включены функции фильтрации и сортировки компонента JList. Однако в данной статье рассматриваются способы реализации данных функций в версии J2SE 5.0 для компонента JList.

Обычно способ фильтрации элементов в длинном списке реализуется при помощи компонента JTextField. Пока пользователь набирает текст в компоненте JTextField, набор элементов, отображаемых в списке, сокращается к набору элементов соответствующих вводимым данным.

Filtering List

Для реализации данной функции компонента JList необходимы два сопутствующих элемента: модель фильтрующая набор элементов на основании некоторого текста и текстовый компонент, вызывающий функцию фильтрации при вводе пользователем текста.

Реализация поля ввода является более легкой задачей, таким образом начнем обзор реализации с нее. Для набора компонентов Swing, моделью компонента JTextField является Document. Для отслеживания ввода в Document, вам необходимо реализовать в модели интерфейс DocumentListener. В нем определены три метода, позволяющие отслеживать события ввода, изменения и удалении текста:

  • public void insertUpdate(DocumentEvent event)
  • public void removeUpdate(DocumentEvent event)
  • public void changedUpdate(DocumentEvent event)

Метод changedUpdate() вызывается при изменении атрибутов модели. Он может не реализовываться. Так как действие фильтрации аналогично для двух оставшихся методов, то они просто вызывают общий метод, создаваемый в настраиваемой модели. Ниже приводится полное описание компонента JTextField, используемого для фильтрации в компоненте JList:

JTextField input = new JTextField();
   String lastSearch =
"";
   DocumentListener listener =
new DocumentListener() {
    
public void insertUpdate(DocumentEvent event) {
      
Document doc = event.getDocument();
       lastSearch = doc.getText
(0, doc.getLength());
      
((FilteringModel)getModel()).filter(lastSearch);
    
}
    
public void removeUpdate(DocumentEvent event) {
      
Document doc = event.getDocument();
       lastSearch = doc.getText
(0, doc.getLength());
      
((FilteringModel)getModel()).filter(lastSearch);
    
}
    
public void changedUpdate(DocumentEvent event) {
     }
   }

   input.getDocument
().addDocumentListener(listener);

Для того чтобы не ограничиваться использованием компонента JTextField, создаваемого при помощи JList, применяется метод installJTextField(), связующий блок прослушивания событий с соответствующим компонентом. Также реализован метод, удаляющий данное соответствие. Применение данных методов позволяет пользователю компонента JList, поддерживающего фильтрацию, использовать собственный компонент JTextField, вместо создаваемого по умолчанию.

public void installJTextField(JTextField input) {
    
input.getDocument().addDocumentListener(listener);
  
}

  
public void unnstallJTextField(JTextField input) {
    
input.getDocument().removeDocumentListener(listener);
  
}

Далее рассматривается модель фильтрации. В ней реализуется метод filter() вызываемый методами, реализующими интерфейс DocumentListener. Для реализации данного метода вам необходимо поддерживать два списка элементов: исходный и фильтрованный списки. Применение наследования от класса AbstractListModel, требует от вас реализации нескольких следующих методов:

  • Конструктора
  • Реализация метода добавления элементов в модель
  • getSize() для получения размеров
  • getElementAt() для получения элемента

В конструкторе создаются два объекта типа List. Не важно, какие объекты хранятся в виде элементов List, поэтому объекты List создаются для хранения типа Object:

List<Object> list;
List<Object> filteredList;

public FilteringModel() {
 
list = new ArrayList<Object>();
  filteredList =
new ArrayList<Object>();
}

Добавление элементов в модель производится путем добавления их в исходную модель и затем фильтрацией данной модели. Данный механизм может быть оптимизирован реализацией метода для фильтрации одного элемента при его добавлении, однако в данной реализации при добавлении элемента вызывается метод filter(), который применяется и для фильтрации всего списка. (Заметьте, что метод filter() вызывается и событием реализованным в DocumentListener). Таким образом, даже при добавлении одного элемента происходит фильтрация всего списка, добавляющая каждый элемент, удовлетворяющий критерию поиска в фильтрованный список (который может быть пуст).

public void addElement(Object element) {
 
list.add(element);
  filter
();
}

Возвращаемый размер модели представляет собой размер фильтрованного списка, но не исходного:

public int getSize() {
 
return filteredList.size();
}

Как и в случае с получением размера модели, метод получения элемента из списка возвращает элементы из фильтрованного списка, а не из исходного. Он реализован таким образом, что не нужно просматривать весь список:

public Object getElementAt(int index) {
 
Object returnValue;
 
if (index < filteredList.size()) {
   
returnValue = filteredList.get(index);
 
} else {
   
returnValue = null;
 
}
 
return returnValue;
}

И, наконец, в методе filter() реализуется основная работа. Так как вам не известно, будет ли новая строка поиска расширять или сужать набор элементов, то проще всего удалить весь фильтрованный список и добавить элементы, соответствующие критерию поиска, из исходного списка. Соответствие может быть найдено, как в начале строки, так и в любом ее месте. Ниже приводится пример поиска буквы "А". Данный метод позволяет найти элементы начинающиеся с заглавной буквы "А" или имеющие данный символ в любом месте строки.

void filter(String search) {
 
filteredList.clear();
 
for (Object element: list) {
   
if (element.toString().indexOf(search, 0) != -1) {
     
filteredList.add(element);
   
}
  }
 
fireContentsChanged(this, 0, getSize());
}

Поиск в данном методе является чувствительным к регистру. В добавление к реализации начала поиска с начала строки, вы можете также изменить метод для реализации не чувствительного к регистру поиска.

Также вы можете сортировать результаты, после добавления элементов в фильтрованный список. Данное действие требует от вас знания содержания модели. В настоящий момент, поиск использует преобразование методом toString() и не подразумевается, что в нем могут содержаться содержаться элементы совместимого типа, которые также могут быть отсортированы.

Далее представлена полная реализация фильтрующего элемента JList с внутренним классом ListModel. В классе ListModel реализуется интерфейс DocumentListener для текстового компонента. На первый взгляд включение данного класса может показаться излишним, так как фильтрация выполняется для только для данной модели, но на самом деле определение поведения в данной реализации является наиболее верным.

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import java.util.*;

public class FilteringJList extends JList {
 
private JTextField input;

 
public FilteringJList() {
   
FilteringModel model = new FilteringModel();
    setModel
(new FilteringModel());
 
}

 
public void installJTextField(JTextField input) {
   
if (input != null) {
     
this.input = input;
      FilteringModel model =
(FilteringModel) getModel();
      input.getDocument
().addDocumentListener(model);
   
}
  }

 
public void uninstallJTextField(JTextField input) {
   
if (input != null) {
     
FilteringModel model = (FilteringModel) getModel();
      input.getDocument
().removeDocumentListener(model);
     
this.input = null;
   
}
  }

 
public void setModel(ListModel model) {
   
if (!(model instanceof FilteringModel)) {
     
throw new IllegalArgumentException();
   
} else {
     
super.setModel(model);
   
}
  }

 
public void addElement(Object element) {
    ((
FilteringModel) getModel()).addElement(element);
 
}

 
private class FilteringModel extends AbstractListModel implements
     
DocumentListener {
   
List<Object> list;
    List<Object> filteredList;
    String lastFilter =
"";

   
public FilteringModel() {
     
list = new ArrayList<Object>();
      filteredList =
new ArrayList<Object>();
   
}

   
public void addElement(Object element) {
     
list.add(element);
      filter
(lastFilter);
   
}

   
public int getSize() {
     
return filteredList.size();
   
}

   
public Object getElementAt(int index) {
     
Object returnValue;
     
if (index < filteredList.size()) {
       
returnValue = filteredList.get(index);
     
} else {
       
returnValue = null;
     
}
     
return returnValue;
   
}

   
void filter(String search) {
     
filteredList.clear();
     
for (Object element : list) {
       
if (element.toString().indexOf(search, 0) != -1) {
         
filteredList.add(element);
       
}
      }
     
fireContentsChanged(this, 0, getSize());
   
}

   
public void insertUpdate(DocumentEvent event) {
     
Document doc = event.getDocument();
     
try {
       
lastFilter = doc.getText(0, doc.getLength());
        filter
(lastFilter);
     
} catch (BadLocationException ble) {
       
System.err.println("Bad location: " + ble);
     
}
    }

   
public void removeUpdate(DocumentEvent event) {
     
Document doc = event.getDocument();
     
try {
       
lastFilter = doc.getText(0, doc.getLength());
        filter
(lastFilter);
     
} catch (BadLocationException ble) {
       
System.err.println("Bad location: " + ble);
     
}
    }

   
public void changedUpdate(DocumentEvent event) {
    }
  }
}

Теперь вам необходимо создать тестовую программу. Ключевыми в ней будут следующие шесть строк. В них создается компонент JList, который добавляется в компонент JScrollPane, а затем добавляется связанное текстовое поле:

   FilteringJList list = new FilteringJList();
   JScrollPane pane =
new JScrollPane(list);
   frame.add
(pane, BorderLayout.CENTER);
   JTextField text =
new JTextField();
   list.installJTextField
(text);
   frame.add
(text, BorderLayout.NORTH);

Основная часть программы добавляет элементы в модель. Ниже представлена модель, состоящая из списка подарков на рождество, имен оленей Санта-Клауса, линий лондонского метро и греческого алфавита.

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class Filters {
 
public static void main(String args[]) {
   
Runnable runner = new Runnable() {
     
public void run() {
       
JFrame frame = new JFrame("Filtering List");
        frame.setDefaultCloseOperation
(JFrame.EXIT_ON_CLOSE);

        FilteringJList list =
new FilteringJList();
        JScrollPane pane =
new JScrollPane(list);
        frame.add
(pane, BorderLayout.CENTER);
        JTextField text =
new JTextField();
        list.installJTextField
(text);
        frame.add
(text, BorderLayout.NORTH);

        String elements
[] = { "Partridge in a pear tree",
           
"Turtle Doves", "French Hens", "Calling Birds",
           
"Golden Rings", "Geese-a-laying", "Swans-a-swimming",
           
"Maids-a-milking", "Ladies dancing", "Lords-a-leaping",
           
"Pipers piping", "Drummers drumming", "Dasher",
           
"Dancer", "Prancer", "Vixen", "Comet", "Cupid",
           
"Donner", "Blitzen", "Rudolf", "Bakerloo", "Center",
           
"Circle", "District", "East London",
           
"Hammersmith and City", "Jubilee", "Metropolitan",
           
"Northern", "Piccadilly Royal", "Victoria",
           
"Waterloo and City", "Alpha", "Beta", "Gamma", "Delta",
           
"Epsilon", "Zeta", "Eta", "Theta", "Iota", "Kapa",
           
"Lamda", "Mu", "Nu", "Xi", "Omikron", "Pi", "Rho",
           
"Sigma", "Tau", "Upsilon", "Phi", "Chi", "Psi", "Omega" };

       
for (String element : elements) {
         
list.addElement(element);
       
}

       
frame.setSize(250, 150);
        frame.setVisible
(true);
     
}
    }
;
    EventQueue.invokeLater
(runner);
 
}
}

Скомпилируйте классы FilteringJList и Filters. Затем запустите Filters и вы должны получить фильтрующий компенет JList.

Filtering List 2

Если элементы в вашем списке имеют верное отображение при вызове метода toString(), то данный подход фильтрации в компоненте JList, использующий связанный компонент JTextField, работает верно. Для более сложных операций фильтрации, можно создать интерфейс Filter, который передается в модель при операции фильтрации.

Единственная вещь, которая не рассматривается в данном примере - это управление выбором. По умолчанию, компонент JList не изменяет выбор при изменении содержания списка модели. В зависимости от желаемого поведения, при фильтрации может сохранятся выбранный элемент или выделятся первый элемент из списка.

Хотя исходный компонент JList и не поддерживает фильтрацию напрямую, в нем существуют методы ее реализации. Если вам не подходит поведение по умолчанию, то вы можете переопределить метод getNextMatch() (добавлен в J2SE 1.4).

Теги: java JFrame swing