用BeanTableModel簡化Swing
讓我們來檢視Swing TMF 框架,看看它是如何讓傳統 TableModel 過時的。設計該框架的第一部分是學習 JTable 的使用 —— 開發人員如何使用它,它顯示了什么內容,以便了理解哪些東西可以內化、通用化,哪些應當保留可配置狀態,以便開發人員配置。對于 TableModel,也要進行同樣的思考,我必須確定哪些東西可以從代碼中移出,哪些必須留在代碼中。一旦找出這些問題,接下來要做的就是確定能夠讓代碼足夠通用的最佳技術,以便所有人都能使用它,但是,還要讓代碼具備足夠的可配置性,這也是為了讓每個人都能使用它。
該框架分成三個基本部分:一個能夠處理任何類型數據的通用 TableModel、一個外部 XML 文件(負責對不同表中不同的表內容進行配置),以及模型與視圖之間的橋。
com.ibm.j2x.swing.table.BeanTableModel
BeanTableModel 是框架的第一部分。它充當的是通用 TableModel ,您可以用它來處理任何類型的數據。我知道,您可能會說,“您怎么這么肯定它適用于所有的數據呢?”確實,很明顯,我不能這么肯定,而且實際上,我確信有一些它不適用的例子。但是從我使用 JTables 的經驗來說,我愿意打賭(即使看起來我有點抬杠),實際使用中的 JTables,99% 都是用來顯示數據對象列表(也就是說,JavaBeans 組件的 ArrayList)。基于這個假設,我建立了一個通用表模型,它可以顯示任何數據對象列表,它就是 BeanTableModel。
BeanTableModel 大量使用了 Java 的內省機制,來檢查 bean 中的字段,顯示正確的數據。它還使用了來自 Jakarta Commons Collections 框架的兩個類來輔助設計。
在我深入研究代碼之前,請讓我解釋來自類的幾個概念。因為我可以在 bean 上使用內省機制,所以我需要了解 bean 本身的信息,主要是了解字段的名稱是什么。我可以通過普通的內省機制來完成這項工作:我可以檢查 bean ,找出其字段。但是,對于表來說,這還不夠好,因為多數開發人員想讓他們的表按照指定順序顯示字段。除此之外,還有一項表需要的信息,我無法通過內省機制從 bean 中獲得,即列名消息。所以,為了獲得正確顯示,對于表中的每個列,您需要兩條信息:列名和將要顯示的 bean 中的字段。我用鍵-值對的格式表示該信息,其中,將列名用作鍵,字段作為值。
正因為如此,我在這里使用了來自 Collections 框架的適合這項工作的兩個類。 BeanMap 用作實用工具類,負責處理內省機制,它接手了內省機制的所有繁瑣工作。普通的內省機制開發需要大量的 try / catch 塊,對于表來說,這是沒有必要的。 BeanMap 把 bean 作為輸入,像處理 HashMap 那樣來處理它,在這里,鍵是 bean 中的字段(例如, firstName ),值是 get 方法(例如, getFirstName() )的結果。BeanTableModel 廣泛地運用 BeanMap ,消除了操作內省機制的麻煩,也使得訪問 bean 中的信息更加容易。
LinkedMap 是另外一個在 BeanTableModel 中全面應用的類。我們還是回到為列名-字段映射所進行的鍵-值數據設置,對于數據對象來說,很明顯應當選擇 HashMap。但是,HashPap 沒有保留插入的順序,對于表來說,這是非常重要的一部分,開發人員希望在每次顯示表的時候,都能以指定的順序顯示列。這樣,插入的順序就必須保留。解決方案是 LinkedMap ,它是 LinkedList 與 HashMap 的組合,它既保留了列,也保留了列的順序信息。參見清單 1,可以查看我是如何用 LinkedMap 和 BeanMap 來設置表的信息的。
清單1. 用 LinkedMap 和 BeanMap 設置表信息
- protected List mapValues = new ArrayList();
- protected LinkedMap columnInfo = new LinkedMap();
- protected void initializeValues(Collection values)
- {
- List listValues = new ArrayList(values);
- mapValues.clear();
- for (Iterator i=listValues.iterator(); i.hasNext();)
- {
- mapValues.add(new BeanMap(i.next()));
- }
- }
在 BeanTableModel 中比較有趣的檢查代碼實際上是通用 TableModel 的那一部分,這部分代碼擴展了 AbstractTableModel 。將清單 2 中的代碼與您通常用來建立傳統 TableModel 的代碼進行比較,您可以看到一些類似之處。
清單 2. BeanTableModel 中的通用 TableModel 代碼
- /**
- *ReturnsthenumberofBeanMaps,thereforethenumberofJavaBeans
- */
- publicintgetRowCount()
- {
- returnmapValues.size();
- }
- /**
- *Returnsthenumberofkey-valuepairingsinthecolumnLinkedMap
- */
- publicintgetColumnCount()
- {
- returncolumnInfo.size();
- }
- /**
- *GetsthekeyfromtheLinkedMapatthespecifiedindex(anda
- *goodexampleofwhyaLinkedMapisneededinsteadofaHashMap)
- */
- publicStringgetColumnName(intcol)
- {
- returncolumnInfo.get(col).toString();
- }
- /**
- *Getstheclassofthecolumn.Alotofdeveloperswonderwhat
- *thisisevenusedfor.ItisusedbytheJTabletousecustom
- *cellrenderers,someofwhicharebuiltintoJTablesalready
- *(Boolean,Integer,Stringforexample).Ifyouwriteacustomcell
- *rendereritwouldgetloadedbytheJTableforuseindisplayifthat
- *specifiedclasswerereturnedhere.
- *ThefunctionusestheBeanMaptogettheactualvalueoutofthe
- *JavaBeananddetermineitsclass.However,becausetheBeanMap
- *autoboxesthings--itconvertstheprimitivestoObjectsforyou
- *(e.g.intstoIntegers)--thecodeneedstounautoboxit,sincethe
- *functionmustreturnaClassObject.Thus,itrecognizesanyprimitives
- *andconvertsthemtotheirrespectiveObjectclass.
- */publicClassgetColumnClass(intcol)
- {
- BeanMapmap=(BeanMap)mapValues.get(0);
- Classc=map.getType(columnInfo.getValue(col).toString());
- if(c==null)
- returnObject.class;
- elseif(c.isPrimitive())
- returnClassUtilities.convertPrimitiveToObject(c);
- else
- returnc;
- }
- /**
- *TheBeanTableModelautomaticallyreturnsfalse,andifyou
- *needtomakeaneditabletable,you'llhavetosubclass
- *BeanTableModelandoverridethisfunction.
- */
- publicbooleanisCellEditable(introw,intcol)
- {
- returnfalse;
- }
- /**
- *ThefunctionthatreturnsthevaluethatyouseeintheJTable.Itgets
- *theBeanMapwrappingtheJavaBeanbasedontherow,itusesthe
- *columnnumbertogetthefieldfromthecolumninformationLinkedMap,
- *andthenusesthefieldtoretrievethevalueoutoftheBeanMap.
- */
- publicObjectgetValueAt(introw,intcol)
- {
- BeanMapmap=(BeanMap)mapValues.get(row);
- returnmap.get(columnInfo.getValue(col));
- }
- /**
- *TheoppositefunctionofthegetValueAt--itduplicatestheworkofthe
- *getValueAt,butinsteadputstheObjectvalueintotheBeanMapinstead
- *ofretrievingitsvalue.
- */
- publicvoidsetValueAt(Objectvalue,introw,intcol)
- {
- BeanMapmap=(BeanMap)mapValues.get(row);
- map.put(columnInfo.getValue(col),value);
- super.fireTableRowsUpdated(row,row);
- }
- /**
- *TheBeanTableModelimplementstheCollectionListenerinterface
- *(1ofthe3partsoftheframework)andthuslistensforchangesinthe
- *dataitismodelingandautomaticallyupdatestheJTableandthe
- *modelwhenachangeoccurstothedata.
- */
- publicvoidcollectionChanged(CollectionEvente)
- {
- initializeValues((Collection)e.getSource());
- super.fireTableDataChanged();
- }
正如您所看到的,BeanTableModel 的整個 TableModel 足夠通用化,可以在任何表中使用。它充分利用了內省機制,省去了所有特定于 bean 的編碼工作,在傳統的 TableModel 中,這類編碼工作絕對是必需的 —— 同時也是完全冗余的。BeanTableModel 還可以在 TMF 框架之外使用,雖然在外面使用會喪失一些威力和靈活性。
#p#
看過這段代碼之后,您會提出兩個問題。首先,BeanTableModel 從哪里獲得列名-字段與鍵-值配對的信息?第二,到底什么是 ObservableCollection ?這些問題會將我們引入框架的接下來的兩個部分。這些問題的答案以及更多的內容,將在本文后面接下來的章節中出現。
Swing Castor XML 解析器
保存必需的列名-字段信息的最合理的位置位于 Java 類之外,這樣,不需要再重新編譯 Java 代碼,就可以修改這個信息。因為關于列名和字段的信息是 TMF 框架中惟一明確與表有關的信息,這意味著整個表格都可以在外部進行配置。
顯然,該解決方案會自然而然把 XML 作為配置文件的語言選擇。配置文件必須為多種表模型保存信息;您還需要能夠用這個文件指定每個列中的數據。配置文件還應當盡可能地易于閱讀,因為開發人員之外的人員有可能要修改它。
這些問題的最佳解決方案是 Castor XML 解析器。查看 Castor 實際使用的最佳方法就是查看如何在框架中使用它。
讓我們來考慮一下配置文件的目的:保存表模型和表中列的信息。 XML 文件應當盡可能簡單地顯示這些信息。TMF 框架中的 XML 文件用清單 3 所示的格式來保存表模型信息。
清單3. TMF 配置文件示例
- <model>
- <className>demo.hr.TableModelFreeExample< SPAN>className>
- <name>Hire< SPAN>name>
- <column>
- <name>First Name< SPAN>name>
- <field>firstName< SPAN>field>
- < SPAN>column>
- <name>Last Name< SPAN>name>
- <field>lastName< SPAN>field>
- < SPAN>column>
- < SPAN>model>
與這個目的相反的目標是,開發人員必須處理的 Java 對象應當像 XML 文件一樣容易理解。通過 Castor XML 解析器用來存儲http://storage.it168.com/" target=_blank>存儲列信息的三個 Java 對象,就可以看到這一點,這三個對象是: TableData (存儲文件中的所有表模型)、 TableModelData (存儲特定于表模型的信息)和 TableModelColumnData (存儲列信息)。這三個類提供了 Java 開發人員所需的所有包裝器,以便得到有關 TableModel 的所有必要信息。
將所有這些包裝在一起所缺少的一個環節就是 映射文件,它是一個 XML 文件,Castor 用它把簡單的 XML 映射到簡單的 Java 對象中。在完美的世界中,映射文件也應當很簡單,但事實要比這復雜得多。良好的映射文件要使別的一切東西都保持簡單;所以一般來說,映射文件越復雜,配置文件和 Java 對象就越容易處理。映射文件所做的工作顧名思義就是把 XML 對象映射到 Java 對象。清單 4 顯示了 TMF 框架使用的映射文件。
清單 4. TMF 框架使用的 Castor 映射文件
- xmlversionxmlversion="1.0"?>
- <mapping>
- <description>Amappingfileforexternalizedtablemodels< SPAN>description>
- <classnameclassname="com.ibm.j2x.swing.table.TableData">
- <map-toxmlmap-toxml="data"/>
- <fieldnamefieldname="tableModelData"collection="arraylist"type=
- "com.ibm.j2x.swing.table.TableModelData">
- <bind-xmlnamebind-xmlname="tableModelData"/>
- < SPAN>field>
- < SPAN>class>
- <classnameclassname="com.ibm.j2x.swing.table.TableModelData">
- <map-toxmlmap-toxml="model"/>
- <fieldnamefieldname="className"type="string">
- <bind-xmlnamebind-xmlname="className"/>
- < SPAN>field>
- <fieldnamefieldname="name"type="string">
- <bind-xmlnamebind-xmlname="name"/>
- < SPAN>field>
- <fieldnamefieldname="columns"collection="arraylist"type=
- "com.ibm.j2x.swing.table.TableModelColumnData">
- <bind-xmlnamebind-xmlname="columns"/>
- < SPAN>field>
- < SPAN>class>
- <classnameclassname="com.ibm.j2x.swing.table.TableModelColumnData">
- <map-toxmlmap-toxml="column"/>
- <fieldnamefieldname="name"type="string">
- <bind-xmlnamebind-xmlname="name"/>
- < SPAN>field>
- <fieldnamefieldname="field"type="string">
- <bind-xmlnamebind-xmlname="field"/>
- < SPAN>field>
- < SPAN>class>
- < SPAN>mapping>
僅僅通過觀察這段代碼,您就可以看出,映射文件清晰地勾劃出了每個用來存儲表模型信息的類,定義了類的類型,并將 XML 文件中的名稱連接到了 Java 對象中的字段。請保持相同的名稱,這樣會讓事情簡單、更好管理一些,但是沒必要保持名稱相同。
到現在為止,列名和字段信息都已外部化,可以讀入包含列信息的 Java 對象中,并且可以很容易地把信息發送給 BeanTableModel,并用它來設置列。
Swing ObservableCollection
TMF 框架的最后一個關鍵部分,就是 ObservableCollection 。您們當中的某些人可能熟悉 ObservableCollection 的概念,它是 Java Collections 框架的一個成員,在被修改的時候,它會拋出事件,從而允許其偵聽器根據這些事件執行操作。雖然從來沒有將它引入 Java 語言的正式發行版中,但在 Internet 上,這個概念已經有了一些第三方實現。就本文而言,我使用了自己的 ObservableCollection 實現,因為框架只需要一些最基本的功能。我的實現使用了一個稱為 collectionChanged() 的方法,每次發生修改時, ObservableCollection 都會在自己的偵聽器上調用該方法。也可以將該用法稱為 Collection 類的 Decorator(有關 Collections 的 Decorator 更多信息,請參閱 Collections 框架的站點),只需要增加幾行代碼,您就可以在普通的 Collection 類中創建 Collection 類的 Observable 實例。清單 5 顯示了 ObservableCollection 用法的示例。(這只是一個示例,沒有包含在 j2x.zip 中。)
清單 5. ObservableCollection 用法示例
- convert a normal list to an ObservableList
- ObservableList oList = CollectionUtilities.observableList(list);
- // A listener could then register for events from this list by calling
- oList.addCollectionListener(this);
- // trigger event
- oList.add(new Integer(3));
- // listener receives event
- public void collectionChanged(CollectionEvent e)
- {
- // event received here}
ObservableCollection 有許多 TMF 框架之外的應用程序。如果您決定采用 TMF 框架,您會發現,在開發代碼期間, ObservableCollection 框架有許多實際的用途。
但是,它在 TMF 框架中的用途,重點在于它能更好地定義視圖和模型之間的關系,當數據發生變化時,可以自動更新視圖。您可以回想一下,這正是傳統 TableModel 的最大限制,因為每當數據發生變化時,都必須用表模型的引用來更新視圖。而在 TMF 框架中使用 ObservableCollection 時,當數據發生變化時,視圖會自動更新,不需要維護一個到模型的引用。在 BeanTableModel 的 collectionChanged() 方法的實現中,您可以看到這一點。
Swing TableUtilities
在該框架中執行的最后一步操作,是將所有內容集成到一些實用方法中,讓 TMF 框架使用起來簡單明了。這些實用方法可以在 com.ibm.j2x.swing.table.TableUtilities 類中找到,該類提供了您將需要的所有輔助函數:
getColumnInfo() :該實用方法用 Castor XML 文件解析指定的文件,并返回指定表模型的所有列信息,返回的形式是 BeanTableModel 所需的 LinkedMap 。當開發人員選擇從 BeanTableModel 中派生子類時,這個方法很重要。
getTableModel() :該實用方法是建立在上面的 getColumnInfo() 方法之上,它獲得列的信息,然后把信息傳遞給 BeanTableModel,返回已經設置好所有信息的 BeanTableModel。
setViewToModel() :該實用方法是最重要的函數,也是 TMF 框架的主要吸引人的地方。它也是建立在 getTableModel() 方法之上,也有一個到 JTable 的引用(JTable 中有這個表的模型),以及一個到數據(要在表中顯示)的引用。它對 JTable 上的 TableModel 進行設置,并把數據傳遞給 TableModel,結果是:只需一行代碼,就為 JTable 完成了 TableModel 的設置。TMF 框架在該方法上得到了最佳印證,TableModel 將永遠地被下面這個簡單的方法所代替:
- TableUtilities.setViewToModel("table_config.xml", "Table", myJTable, myList);
【編輯推薦】