2005년 12월 20일자 테크 팁 테이블 정렬 및 필터링에서는 Java SE 6의 새로운 기능을 이용하여 JTable을 정렬하고 필터링을 하는 방법을 살펴보았다. Java SE 6은 JList 정렬과 필터링을 위한 새로운 기능을 제공하지는 않지만 본 팁에서는 J2SE 5.0에서 JList에 대한 유사한 작업을 수행하는 방법을 배우게 될 것이다.
사용자에게 긴 리스트에서 엘리먼트를 필터링하도록 프롬프트하는 데 일반적으로 사용되는 기법으로는 리스트에 JTextField를 표시하는 경우를 들 수 있다. 사용자가 JTextField에 입력을 하면 리스트에 표시된 엘리먼트가 일련의 매칭 엔트리로 단축된다.
JList를 위한 이런 종류의 기능을 구현하려면 일부 텍스트를 토대로 엘리먼트를 필터링하는 모델과 사용자가 필드에 입력할 때 필터링 동작을 트리거하는 텍스트 컴포넌트 등의 두 가지 지원 엘리먼트가 필요하다.
둘 중에서 인풋 필드를 구현하는 쪽이 더 쉬우므로 이를 먼저 살펴보기로 한다. Swing 컴포넌트 세트에서 JTextField를 위한 모델은 Document이다. Document에 대한 인풋을 모니터하려면 모델에 DocumentListener를 첨부한다. 이 리스너에는 이벤트의 인풋, 제거, 변경에 대해 서로 다르게 반응할 수 있도록 해주는 다음의 세 가지가 메소드가 있다.
public void insertUpdate(DocumentEvent event) public void removeUpdate(DocumentEvent event) public void changedUpdate(DocumentEvent event) changeUpdate() 메소드는 모델 내의 속성 변경과 관련이 있는데, 이는 무시해도 무방하다. 다른 두 메소드에 대해서 동일한 필터링 동작이 이루어져야 하기 때문에, 이들은 커스텀 모델에서 생성되어야 할 동일한 메소드를 호출해야 한다. 다음은 필터링 JList에 연결되어야 할 JTextField를 정의한 것이다.
JList에 의해 생성되는 JTextField에 사용을 제한하는 대신, 리스너를 주어진 컴포넌트에 연결하는 installJTextField() 메소드와 리스너를 제거하기 위한 언인스톨 메소드가 제공된다. 이 경우, 필터링 리스트 사용자는 기본값을 생성하는 대신 자체의 JTextField를 제공할 수 있게 된다.
public void installJTextField(JTextField input) { input.getDocument().addDocumentListener(listener); }
public void unnstallJTextField(JTextField input) { input.getDocument().removeDocumentListener(listener); }
다음으로 필터링 모델에서는 DocumentListener에 의해 filter() 메소드가 요구되는데, 이 경우에는 단지 소스 리스트와 필터링된 리스트의 2개 엘리먼트 리스트를 유지하면 된다. AbstractListModel의 도움으로 다음과 같은 메소드를 구현해야 한다.
생성자(constructor) 엘리먼트를 모델에 추가하기 위한 추가 메소드 사이즈를 얻기 위한 getSize() 엘리먼트를 다시 얻기 위한 getElementAt() 생성자는 2개의 List 오브젝트를 생성한다. List 내에 어떤 엘리먼트가 있는지는 문제가 되지 않으므로 다음과 같이 Object 타입의 List로 생성하면 된다.
List모델에 엘리먼트를 추가하는 작업은 엘리먼트를 소스 모델에 추가한 다음 필터 자체에 모델을 통지함으로써 가능하다. 이는 오직 새로운 엘리먼트를 필터링하기 위해서만 최적화될 수 있지만, 당분간 엘리먼트를 추가하는 작업은 전체 리스트 필터링을 위해 호출되는 것과 동일한 filter() 메소드를 호출한다. (DocumentListener를 통한 Document 입력은 전체 리스트를 필터링하기 위한 filter()를 호출한다는 점에 유의할 것.) 따라서 하나의 엘리먼트를 추가하는 경우라도 전체 리스트를 소거하고 마지막 검색 조건에 일치하는 각각의 엘리먼트를 추가하도록 한다(새로운 리스트의 경우, 비어 있을 수 있음). public void addElement(Object element) { list.add(element); filter(); } 리스트 모델의 사이즈는 소스 사이즈가 아니라 필터링된 리스트 사이즈이다. public int getSize() { return filteredList.size(); } 사이즈를 get할 때와 마찬가지로, 엘리멘트를 get할 때는 원래의 소스 리스트가 아니라 필터링된 리스트에서 가져오는데, 이는 리스트의 끝을 지나치지 않는 경우에만 가능하다. public Object getElementAt(int index) { Object returnValue; if (index < filteredList.size()) { returnValue = filteredList.get(index); } else { returnValue = null; } return returnValue; } 마지막 filter() 메소드가 대부분의 작업을 제공하는데, 검색 문자열이 결과 세트를 확대 또는 축소하는지 알 수 없기 때문에 필터링된 리스트를 소거하고 소스 리스트에서 인풋 필드에 일치하는 항목을 추가하는 것이 가장 수월한 방법이라 할 수 있겠다. 매칭은 문자열의 시작 부분이나 텍스트의 어느 곳에서도 가능하다. 다음은 후자 검색 방법의 예제이다. 이 경우 "A"를 이용하여 "A"로 시작하거나 대문자 "A"가 포함된 엘리먼트를 찾을 수 있다. 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() 결과를 이용한다(역시 정렬이 가능한 StringComparable 타입의 엘리먼트가 모델에 포함된 것으로 가정하지 않는다). 다음으로 ListModel을 이너 클래스로 하여 완벽한 필터링 JList가 표시된다. 커스텀 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()); }
/** * Associates filtering document listener to text * component. */
public void installJTextField(JTextField input) { if (input != null) { this.input = input; FilteringModel model = (FilteringModel)getModel(); input.getDocument().addDocumentListener(model); } }
/** * Disassociates filtering document listener from text * component. */
public void uninstallJTextField(JTextField input) { if (input != null) { FilteringModel model = (FilteringModel)getModel(); input.getDocument().removeDocumentListener(model); this.input = null; } }
/** * Doesn't let model change to non-filtering variety */
public void setModel(ListModel model) { if (!(model instanceof FilteringModel)) { throw new IllegalArgumentException(); } else { super.setModel(model); } }
/** * Adds item to model of list */ public void addElement(Object element) { ((FilteringModel)getModel()).addElement(element); }
/** * Manages filtering of list model */
private class FilteringModel extends AbstractListModel implements DocumentListener { ListFilteringJList와 Filters 클래스를 컴파일한 다음 Filters를 실행한다. 필터링 JList가 표시되어야 한다. 필터링 JList 및 연결된 JTextField를 위한 이 접근법은 여러분의 리스트 엘리먼트가 "양호한" toString() 표현을 가지는 경우에만 통용된다. 리스트 엘리먼트가 복잡한 경우에는 필터링 작업을 위해 모델로 패스될 수 있는 Filter 인터페이스를 정의하는 것이 좋다. 이 예제에서 다루어지지 않은 항목이 바로 선택의 관리인데, 기본적으로 모델 내용 변경 시 JList는 선택 사항을 변경하지 않는다. 원하는 동작에 따라, 필터링은 선택된 엘리먼트를 보존하려 할 수도 있고 항상 리스트의 첫 번째 항목으로 리셋할 수도 있다. 기본적인 JList 컴포넌트는 직접적으로 필터링을 지원하지는 않는 대신 유용한 미리 입력(type-ahead) 옵션을 제공한다. 기본값 동작이 마음에 들지 않으면 getNextMatch() 메소드(이 메소드는 J2SE 1.4에 추가되었음)를 오버라이드하면 된다.