12章探讨了支持滚动与输入或是显示某些边界范围值的边界范围组件。在本章中,我们将会探讨表示选项列表的两个选择控件:JList与JComboBox。这两个组件之间的主要区别在于JList组件支持多项选择,而JComboBox不支持。同时,JComboBox允许用户提供不在可用选项中的选项。
13.1 ListModel接口
图13-1显示了在本章中我们将要探讨的两个控件。
这两个组件之间所共享的数据模型是ListModel,从而形成了ListMode接口。AbstractListModel类通过支持ListDataListener对象的管理与通知提供了实现基础。
对于JList组件,数据模型的实现是DefaultListModel类。这个类添加了一个实际的数据仓库,其遵循Vector API,可以用于在JList组件内显示的不同的元素。
对于JComboBox组件,一个名为ComboBoxModel的ListModel接口扩展提供了在模型内选择项目的概念。DefaultComboBoxModel类通过另一个接口,MutableComboBoxModel实现了ComboBoxModel接口,MutableComboBoxModel为模型中元素的添加与移除提供了支持方法。
注意,BasicDirectoryModel类是另一个ListModel实现。这个实现为第9章所描述的文件选择器组件JFileChooser所用。
实际上ListModel接口非常简单。他提供了ListDataListener管理,并且访问模型特定元素的尺寸。
public interface ListModel {
// Properties
public int getSize();
// Listeners
public void addListDataListener(ListDataListener l);
public void removeListDataListener(ListDataListener l);
// Other methods
public Object getElementAt(int index);
}
13.1.1 AbstractListModel类
AbstractListModel类提供了ListModel接口的部分实现。我们只需要提供数据结构与数据。这个类为ListDataListener的列表管理提供对象并且当数据变化时为这些监听器的通知提供框架。我们可以使用public ListDataListener[] getListDataListener()方法获取监听器列表。当我们修改数据模型时,我们必须调用AbstractListModel的相应方法来通知监听在ListDataListener对象:
- protected void fireIntervalAdded(Object source, int index0, int index1):在向列表添加一个连续的范围值之后调用。
- protected void fireIntervalRemoved(Object source, int index0, int index1):在由列表移除一个连续的范围值之后调用。
- protected void fireContentsChanged(Object source, int index0, int index1):如果修改的范围对于插入,移除或是两者,不是连续的时调用。
如果我们的数据在一个已存在数据结构中,我们需要将其转换为Swing组件可以理解的格式或是我们自己实现ListModel接口。正如我们将要看到,数据或是Vector是直接为JList与JComboBox所支持的。我们可以将我们的数据结构包装进AbstractListModel。例如,如果我们的初始数据结构是集合框架中的ArrayList,我们可以使用下面的代码转换为一个ListModel:
final List arrayList = ...;
ListModel model = new AbstractListModel() {
public int getSize() {
return arrayList.size();
}
public Object getElementAt(int index) {
return arrayList.get(index);
}
}
另一个选择就是将List传递给Vector构造函数,然后将Vector传递给JList构造函数。事实上,我们已经完成了相同的事件。
13.1.2 DefaultListModel类
DefaultListModel类为我们提供了一个数据结构用来以Vector的形式存储内部数据。我们只需要添加数据,因为这个类为我们管理ListDataListener。
首先,我们使用无参数的构造函数创建数据结构:DefaultListModel model = new DefaultListModel()。然后我们进行冰封装填。如表13-1所示,DefaultListModel类只有两个属性。
DefaultListModel类通过一系列的公开方法提供了所有的操作方法。要添加元素,可以使用下面的方法:
public void add(int index, Object element)
public void addElement(Object element)
public void insertElementAt(Object element, int index)
DefaultListModel的addElement()方法将元素添加到数据模型的尾部。要修改元素,使用下面的方法:
public Object set(int index, Object element)
public void setElementAt(Object element, int index)
要移除元素,可以使用下面的方法:
public void clear()
public Object remove(int index)
public void removeAllElements()
public boolean removeElement(Object element)
public void removeElementAt(int index)
public void removeRange(int fromIndex, int toIndex)
removeElement()方法返回一个状态:如果他找到对象并且移除则返回true,否则返回false。
当我们并没有将数据存储在已存在数据结构中时,DefalutListModel类十分有用。例如,数据库查询的结果会作为JDBC ResultSet返回。如果我们希望使用这些结果作为显示在JList中的内容的基础,我们必须将其存储在某些地方。这就可以存储在DefaultListModel中,如下面的代码所示:
ResultSet results = aJDBCStatement.executeQuery(
"SELECT columnName FROM tableName");
DefaultListModel model = new DefaultListModel();
while (results.next()) {
model.addElement(result.getString(1));
}
13.1.3 使用ListDataListener监听ListModel事件
如果我们对确定列表模型的内容何时发生变化感兴趣,我们可以向模型注册一个ListDataListsener。接口的三个单独方法可以告诉我们内容何时被添加,被移除或是被修改。修改数据模型意味着由数据模型的一个或多个区域添加或移除内容或者是没有添加或是移除元素修改已存在的内容。接口定义如下:
public interface ListDataListener extends EventListener {
public void contentsChanged(ListDataEvent e);
public void intervalAdded(ListDataEvent e);
public void intervalRemoved(ListDataEvent e);
}
基于列表修改事件的通知,我们可以传递一个ListDataEvent实现,其包含三修改必,如表13-2所示。
索引并不是顺序所必须的,也不是修改区域的边界。在列表模型内容修改的例子中,并不是区域中的所有内容都会被修改。内容实际变化的区域是通过索引指定的边界区域。type属性的设置是表13-3中的三个常量之一,这直接映射到所调用的接口方法。
如果当DefaultListModel类的操作方法被调用时,ListDataListener对象被关联到数据模型,每一个监听器都会得到数据模型变化的通知。为了演示ListDataListesner的使用以及数据模型的动态更新,列表13-1中的ModifyModelSample程序使用的DefaultListModel类修改方法,以事件的形式发送输出并列出JTextArea的内容。
package swingstudy.ch13;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.PrintWriter;
import java.io.StringWriter;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
public class ModifyModelSample {
static String labels[] = {"Chardonnay", "Sauvignon", "Riesling", "Cabernet",
"Zinfandel", "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewurztraminer"
};
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Runnable runner = new Runnable() {
public void run() {
JFrame frame = new JFrame("Modifying Model");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Fill model
final DefaultListModel model = new DefaultListModel();
for(int i=0; i<labels.length; i++) {
model.addElement(labels[i]);
}
JList jList = new JList(model);
JScrollPane scrollPane1 = new JScrollPane(jList);
frame.add(scrollPane1, BorderLayout.WEST);
final JTextArea textArea = new JTextArea();
textArea.setEditable(false);
JScrollPane scrollPane2 = new JScrollPane(textArea);
frame.add(scrollPane2, BorderLayout.CENTER);
ListDataListener listDataListener = new ListDataListener() {
public void contentsChanged(ListDataEvent event) {
appendEvent(event);
}
public void intervalAdded(ListDataEvent event) {
appendEvent(event);
}
public void intervalRemoved(ListDataEvent event) {
appendEvent(event);
}
private void appendEvent(ListDataEvent event) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
switch(event.getType()) {
case ListDataEvent.CONTENTS_CHANGED:
pw.print("Type: contents Changed");
break;
case ListDataEvent.INTERVAL_ADDED:
pw.print("Type: Interval Added");
break;
case ListDataEvent.INTERVAL_REMOVED:
pw.print("Type: Interval Removed");
break;
}
pw.print(", Index0: "+event.getIndex0());
pw.print(", Index1 "+event.getIndex1());
DefaultListModel theModel = (DefaultListModel)event.getSource();
pw.println(theModel);
textArea.append(sw.toString());
}
};
model.addListDataListener(listDataListener);
// Set up buttons
JPanel jp = new JPanel(new GridLayout(2,1));
JPanel jp1 = new JPanel(new FlowLayout(FlowLayout.CENTER, 1,1));
JPanel jp2 = new JPanel(new FlowLayout(FlowLayout.CENTER, 1,1));
jp.add(jp1);
jp.add(jp2);
JButton jb = new JButton("add F");
jp1.add(jb);
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
model.add(0, "First");
}
});
jb = new JButton("addElement L");
jp1.add(jb);
jb.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent event) {
model.addElement("Last");
}
});
jb = new JButton("insertElementAt M");
jp1.add(jb);
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
int size = model.getSize();
model.insertElementAt("Middle", size/2);
}
});
jb = new JButton("set F");
jp1.add(jb);
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
int size = model.getSize();
if (size != 0) {
model.set(0, "New First");
}
}
});
jb = new JButton("setElementAt L");
jp1.add(jb);
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
int size = model.getSize();
if(size!=0)
model.setElementAt("New Last", size-1);
}
});
jb = new JButton("load 10");
jp1.add(jb);
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
for(int i=0, n=labels.length; i<n; i++) {
model.addElement(labels[i]);
}
}
});
jb = new JButton("clear");
jp2.add(jb);
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
model.clear();
}
});
jb = new JButton("remove F");
jp2.add(jb);
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
int size = model.getSize();
if(size !=0)
model.remove(0);
}
});
jb = new JButton("removeAllElements");
jp2.add(jb);
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
model.removeAllElements();
}
});
jb = new JButton("removeElement 'Last'");
jp2.add(jb);
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
model.removeElement("Last");
}
});
jb = new JButton("removeElementAt M");
jp2.add(jb);
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
int size = model.getSize();
if(size != 0)
model.removeElementAt(size/2);
}
});
jb = new JButton("removeRange FM");
jp2.add(jb);
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
int size = model.getSize();
if(size !=0)
model.removeRange(0, size/2);
}
});
frame.add(jp, BorderLayout.SOUTH);
frame.setSize(640, 300);
frame.setVisible(true);
}
};
EventQueue.invokeLater(runner);
}
}
图13-2显示了程序的运行结果。
DefaultListModel类的获取方法会随着他们的功能而变化。这个类具有基本的访问方法public Object get(int index), public Object getElementAt(int index)以及public Object elementAt(int index),这三个方法都可以完成相同的事情。DefaultListModel类同时也具有更为特殊的方法。例如,为了使用所有的元素,我们可以使用public Enumeration elements()方法获取Enumeration的实例。
或者是如果我们希望以数组的方式操作所有的元素,可以使用public Object[] toArray()或是public void copyInto(Object anArray[])。我们也可以使用方法来检测模型中是否存在某一个元素,public boolean contains(Object element), public int indexOf(Object element), public int indexOf(Object element, int index), public int lastIndexOf(Object element)以及public int lastIndexOf(Object element, int index)。
提示,一旦我们完成了向数据模型添加元素,使用public void trimToSize()方法修整其长度是个好主意。这会移除数据结构内部所分配的额外空间。另外,如果我们知道数据尺寸,我们可以调用public void ensureCapacity(int minCapacity)来预分配空间。这两种方法都可以用于DefaultListModel。
13.1.4 ComboBoxModel接口
ComboBoxModel接口扩展了ListModel接口。扩展的主要原因是因为实现ComboBoxModel接口的类需要通过selectedItem属性来管理被选中的项目,如下面的接口定义所示:
public interface ComboBoxModel extends ListModel {
// Properties
public Object getSelectedItem();
public void setSelectedItem(Object anItem);
}
13.1.5 MutableComboBoxModel接口
除了ComboBoxModel接口以外,另一个数据模型接口MutableComboBoxModel扩展了ComboBoxModel从而构成了可以修改数据模型的方法。
public interface MutableComboBoxModel extends ComboBoxModel {
// Other methods
public void addElement(Object obj);
public void insertElementAt(Object obj, int index);
public void removeElement(Object obj);
public void removeElementAt(int index);
}
JComboBox组件默认使用这个接口的实现。
13.1.6 DefaultComboBoxModel类
DefaultComboBoxModel类扩展了AbstractListModel类来为JComboBox提供相应的方法。由于这种扩展,他继承了ListDataListener列表的管理。
类似于DefaultListModel,DefaultComboBoxModel为我们添加了收集显示在组件中的元素所必需的数据结构。同时,由于模型是可修改的,实现MutableComboBoxModel会使得当模型中的数据元素发生变化时,数据模型调用AbstractListModel的各种fileXXX()方法。
注意,如果我们由一个数组创建了DefaultComboBoxModel,数组的元素会被拷贝到一个内部数据结构中。如果我们使用Vector,他们不会被拷贝;相反,在内部会使用实际的Vector。
要使用数据模型,我们必须首先使用下面的构造函数来创建模型:
public DefaultComboBoxModel()
DefaultComboBoxModel model = new DefaultComboBoxModel();
public DefaultComboBoxModel(Object listData[])
String labels[] = { "Chardonnay", "Sauvignon", "Riesling", "Cabernet", "Zinfandel",
"Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewürztraminer"};
DefaultComboBoxModel model = new DefaultComboBoxModel(labels);
public DefaultComboBoxModel(Vector listData)
Vector vector = aBufferedImage.getSources();
DefaultComboBoxModel model = new DefaultComboBoxModel(vector);
然后,我们操作模型。DefaultComboBoxModel类引入了两个新属性,如表13-4所示。
DefaultComboBoxModel的数据模型修改方法不同于DefaultListModel的模型修改方法。他们来自于MutableComboBoxModel接口:
public void addElement(Object element)
public void insertElementAt(Object element, int index)
public boolean removeElement(Object element)
public void removeElementAt(int index)
由于DefaultComboBoxModel的灵活性(以及功能性),通常并不需要创建我们自己的ComboBoxModel实现。只需要创建一个DefaultComboBoxModel实例,然后简单的使用相应的数据源对其进行装配。
注意,我们也许希望提供我们自己模型的一个例子就是当我们需要支持模型多个项目中相同项目的表示。对于DefaultComboBoxModel,如果我们在其equals()方法返回true的列表中有两个项目,模型不会正确的工作。
如果我们确实希望定义我们自己的模型实现,也许是因为在我们已经有数据存储在我们的数据结构中,最好的方法就是继承AbstractListModel并且实现ComboBoxModel或是MutableComboBoxModel接口方法。当继承AbstractListModel时,我们只需要提供数据结构以及对其的访问接口。因为数据模型的“选中项目”部分是在基本的数据结构之外进行管理的,我们也需要提供的一个位置进行存储。列表13-2中的程序源码演示了使用ArrayList作为数据结构的实现。程序包含main()方法来演示JComboBox中模型的使用。
package swingstudy.ch13;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.Collection;
import javax.swing.AbstractListModel;
import javax.swing.ComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JFrame;
public class ArrayListComboBoxModel extends AbstractListModel implements ComboBoxModel{
private Object selectedItem;
private ArrayList anArrayList;
public ArrayListComboBoxModel(ArrayList arrayList) {
anArrayList = arrayList;
}
public Object getSelectedItem() {
return selectedItem;
}
public void setSelectedItem(Object newValue) {
selectedItem = newValue;
}
public int getSize() {
return anArrayList.size();
}
public Object getElementAt(int i) {
return anArrayList.get(i);
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Runnable runner = new Runnable() {
public void run() {
JFrame frame = new JFrame("ArrayListComboBoxModel");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Collection<Object> col = System.getProperties().values();
ArrayList<Object> arrayList = new ArrayList<Object>(col);
ArrayListComboBoxModel model = new ArrayListComboBoxModel(arrayList);
JComboBox comboBox = new JComboBox(model);
frame.add(comboBox, BorderLayout.NORTH);
frame.setSize(300, 225);
frame.setVisible(true);
}
};
EventQueue.invokeLater(runner);
}
}
图13-3实际显示了使用当前的系统属性作为数据模型元素的数据源中模型。