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 modelfinal 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 buttonsJPanel 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實際顯示了使用當前的系統屬性作為資料模型元素的資料來源中模型。