14: 創建窗口與Applet
設計的宗旨是"能輕松完成簡單的任務,有辦法完成復雜的任務"。
本章只介紹Java 2的Swing類庫,并且合理假定Swing是Java GUI類庫的發展方向。
本章的開頭部分會講,用Swing創建applet與創建應用程序有什么不同,以及怎樣創建一個既能當applet在瀏覽器里運行,又能當普通的應用程序,在命令行下運行程序。
Swing類庫的體系龐大,而本章的目的也只是想讓你從基礎開始理解并且熟悉這些概念。如果你有更高的要求,只要肯花精力研究,Swing大概都能做到。
你越了解Swing,你就越能體會到:
- 與其它語言或開發環境相比,Swing是一個好得多的編程模型。而JavaBeans (本章的臨近結尾的地方會作介紹)則是專為這個構架服務的類庫。
- 就整個Java開發環境來講,"GUI builders" (可視化編程環境)只是一個"社交"的層面。當你用圖形工具把組件放到窗體上的時候,實際上是GUI builder在調用JavaBeans和Swing為你編寫代碼。這樣不僅能可以加快GUI的開發速度,而且能讓你做更多實驗。這樣你就可以嘗試更多的方案,得到更好的效果了。
- Swing的簡單易用與設計合理,使你即便用GUI builder也能得到可讀性頗佳的代碼。這一點解決了GUI builder的一個老問題,那就是代碼的可讀性。
Swing囊括了所有比較時髦的用戶界面元素:從帶圖像的按鈕到樹型控件和表格控件,應有盡有??紤]到類庫的規模,其復雜性還是比較理想的。如果要做的東西比較簡單,代碼就不會很多,如果項目很復雜,那么代碼就會相應地變得復雜。也就是說入門很容易,但是如果有必要,它也可以變得很強大。
對Swing的好感,很大程度上源于其"使用的正交性"。也就是說,一旦領會了這個類庫的精神,你就可以把這種概念應用到任何地方。這一點首先就緣于其標準的命名規范。通常情況下,如果想把組件嵌套到其它組件里,直接插就行了。
為了照顧運行速度,組件都是"輕量級"的。為了能跨平臺,Swing是完全用Java寫的。
鍵盤支持是內置的;運行Swing應用的時候可以完全不用鼠標,這一點,不需要額外的編程。加滾動條,很容易,只要把控件直接嵌到JScrollPane里就行了。加Tooltip也只要一行代碼。
Swing還提供一種更前衛的,被稱為"pluggable look and
feel(可插接式外觀)"的功能,也就是說用戶界面的外觀可以根據操作系統或用戶的習慣動態地改變。你甚至可以自己發明一套外觀(當然是很難的)。
基本的applet
Java能創建applet,也就是一種能在Web瀏覽器里運行的小程序。Applet必須安全,所以它的功能很有限。但是applet是一種很強大的客戶端編程工具,而后者是Web開發的一個大課題。
Applet的限制
applet編程的限制是很多的,所以也經常被稱作關在"沙箱"里。因為時時刻刻都有一個人——也就是Java的運行時安全系統——在監視著你。
不過你也可以走出沙箱,編寫普通的應用程序而不是applet,這樣就可以訪問操作系統的其它功能了。迄今為止,我們寫的都是這種應用程序,只不過它們都是些沒有圖形界面的控制臺程序罷了。Swing也可以寫GUI界面的應用程序。
大體上說,要想知道applet能做什么,最好先去了解一下,為什么要有applet:用它來擴展瀏覽器里的Web頁面的功能。因為上網的人是不可能真正知道這個頁面是不是來自于一個無惡意的網站的,但是你又必須確保所有運行的代碼都是安全的。所以你會發現,它的最大的限制是:
- Applet不能訪問本地磁盤。Java為applet提供了數字簽名。你可以選擇讓有數字簽名(由可信的開發商簽署)的applet訪問你的硬盤,這樣applet的很多限制就被解除了。本章的后面在講Java Web Start的時候,會介紹一個這樣的例子的。Web Start是一種通過Internet將應用程序安全地送到客戶端的方案。
- Applet的啟動時間很長,因為每次都得下載所有東西,而且每下載一個類都要向服務器發一個請求?;蛟S瀏覽器會作緩存,但這并不是一定的。所以一定要把applet的全部組件打成一個JAR的(Java ARchive)卷宗(除了.class文件之外,還包括圖像,聲音),這樣只要發一個請求就可以下載整個applet了。JAR卷宗里的東西可以逐項地"數字簽名"。
Applet的優勢
如果你不在意這些限制,applet還是有明顯優勢的,特別是在構建client/server 或是其他網絡應用的時候:
- 沒有安裝的問題。Applet是真正平臺無關的(包括播放音頻文件) ,所以你用不著去為不同的平臺修改程序,用戶也用不著安裝完了之后再作調整。實際上每次載入有applet的Web頁面時,安裝就自動完成了。因此軟件的更新可以不驚動客戶自動地完成。為傳統的client/server系統構建和安裝一個新版的軟件,通常都是一場惡夢。
- 由于Java語言和applet內置了安全機制,因此你不用擔心錯誤代碼會破壞別人的機器。有了這兩個優勢,Java就能在intranet的client/server應用里大展身手了。所謂intranet的client/server應用,是指僅存在于公司內部的,或者可以限定和控制用戶環境的(Web瀏覽器和插件)特殊場合的client/server應用。
由于applet是自動集成到HTML里面的,因此你就有了一種與平臺無關的,能支持applet的文檔系統了(譯者注:指HTML)。這真是太有趣了,因為我們通常都認為文檔是程序的一部分,而不是相反。
應用框架
類庫通常按功能進行分類。有些類庫是拿來直接用的,比如Java標準類庫里面的String和ArrayList。有些類庫則是用來創建其它類的。此外還有一種被稱為應用框架(application framework)的
類庫。它的目的是,提供一個或一組具備某些基本功能的類,幫助程序員創建應用程序。而這些基本功能,是這類應用程序所必備的。于是你寫應用程序的時候,只
要繼承這個類,然后再根據需要,覆寫幾個你感興趣的方法,定制一下它的行為就可以了。應用框架的默認控制機制會在適當的時機,調用那些你寫的方法。應用框
架是一種"將會變和不會變的東西分開來"的絕好的例子。它的設計思想是,通過覆寫方法把程序的個性化部分留在本地。[76]
Applet是用應用框架創建的。你只要繼承JApplet類,再覆寫幾個方法就可以了。下面幾個方法可以控制Web頁面上的applet的創建和執行:
方法
|
操作
|
init( )
|
applet初始化的時候會自動調用,其任務包括裝載組件的布局。必須覆寫。
|
start( )
|
在Web瀏覽器上顯示applet的時候調用。顯示完畢之后,applet才開始正常工作,(特別是那些用stop( )關閉的applet)。(此外,應用框架)調用完init( )之后也會調用這個方法。
|
stop( )
|
讓applet從Web瀏覽器上消失的時候調用,這樣它就能關閉一些很耗資源的操作了。此外(應用框架調用)destroy( )之前也會先調用這個方法。
|
destroy( )
|
當(瀏覽器)不再需要這個applet了,要把它從頁面里卸載下來的時候,就會調用這個方法以釋放資源了。
|
注意applet不需要main()。它已經包括在應用框架里了;你只要把啟動代碼放到init( )里面就行了。
JLabel的構造函數需要一個String作參數。
init( )方法負責將組件add( )到表單上?;蛟S你會覺得,應該能直接調用它自己(JApplet)的add( )方法。實際上AWT就是這么做的。Swing要求你將所有的組件都加到表單的"內容面板(content pane)"上,所以add( )的時候,必須先調用getContentPane( )。
在Web瀏覽器里運行applet
要想運行程序,先得把applet放到Web頁面里,然后用一個能運行Java的Web瀏覽器打開頁面。你得用一種特殊的標記把applet放到Web頁面里,然后讓它來告訴頁面該怎樣裝載并運行這個applet。
這個步驟曾經非常簡單。那時Java自己就很簡單,所有人都用同一個Java,瀏覽器里的Java也都一樣。所以你只要稍微改一下HTML就行了,就像這樣:
<applet code=Applet1 width=100 height=50>
</applet>
但是隨后而來的瀏覽器和語言大戰使我們(不僅是程序員,還包括最終用戶)都成了輸家。不久,Sun發現再也不能指望靠瀏覽器來支持風味醇正的Java了,唯一的解決方案是利用瀏覽器的擴展機制來提供"插件(add-on)"。通過這個辦法(要想禁掉Java,除非把所有第三方的插件全都禁掉,但是為了獲取競爭優勢,沒有一個瀏覽器的提供商會這么做的),Sun確保了Java不會被敵對的瀏覽器提供商給禁掉。
對于Internet Explorer,這種擴展機制就是ActiveX的控件,而Netscape的則是plugin。你做頁面時必須為這兩個瀏覽器各寫一套標記,不過JDK自帶了一個能自動生成標記的HTMLconverter工具。下面就是用HTMLconverter處理過的Applet1的HTML頁面:
<!--"CONVERTED_APPLET"-->
<!-- HTML CONVERTER -->
<OBJECT
classid = "clsid:CAFEEFAC-0014-0001-0000-ABCDEFFEDCBA"
codebase = "http://java.sun.com/products/plugin/autodl/jinstall-1_4_1-windows-i586.cab#Version=1,4,1,0"
WIDTH = 100
HEIGHT = 50 >
<PARAM NAME
= CODE VALUE = Applet1 >
<PARAM NAME
= "type" VALUE = "application/x-java-applet;jpi-version=1.4.1">
<PARAM NAME
= "scriptable" VALUE = "false">
<COMMENT>
<EMBED
type = "application/x-java-applet;jpi-version=1.4.1"
CODE =
Applet1
WIDTH =
100
HEIGHT =
50
scriptable = false
pluginspage = "http://java.sun.com/products/plugin/index.html#download">
<NOEMBED>
</NOEMBED>
</EMBED>
</COMMENT>
</OBJECT>
<!--
<APPLET CODE = Applet1 WIDTH = 100 HEIGHT = 50>
</APPLET>
-->
<!--"END_CONVERTED_APPLET"-->
code的值是applet所處的.class文件的名字,width和height則表示applet的初始尺寸(和前面一樣,以象素為單位)。此外applet標記里面還可以放一些東西:到哪里去找.class文件(codebase),怎樣對齊(align),applet相互通訊的時候要用的標識符(name),以及提供給applet的參數。參數的格式是這樣的:
<param name="identifier"
value = "information">
你可以根據需要,加任意多個參數。
Appletviewer的用法
Sun的JDK包含一個名為Appletviewer的工具,它可以根據<applet>標記在HTML文件里找出applet,然后不顯示HTML文本,直接運行這個applet。由于Appletviewer忽略了除applet標記之外的所有其他東西,因此你可以直接把applet標記當作注釋放到Java的源文件里:
// <applet code=MyApplet width=200
height=100></applet>
這樣你就可以用"appletviewer MyApplet.java"來啟動applet了,不用再寫HTML的測試文件了。
Applet的測試
如果只是想簡單的測試一下,那么根本不用上網。直接啟動Web瀏覽器,打開含applet標記的HTML文件就可以了。瀏覽器裝載HTML的時候會發現applet標記,并且按照code的值去找相應的.class文件。當然,是在CLASSPATH里面找。如果CLASSPATH里沒有,它就在瀏覽器的狀態欄里給一個錯誤信息,告訴你找不到.class文件。
如果客戶端的瀏覽器不能在服務器上找到.class文件,它會到客戶機的CLASSPATH里面去找。這樣一來,就有可能發生,瀏覽器在服務器上找不到.class文件,因此用你機器上的文件啟動了applet的情形了。于是在你看來一切都很正常,而其他人則什么都看不見了。所以測試之前,一定要把本地的.class文件(或.jar文件)全部刪了,只有這樣才能知道程序是不是呆在正確的服務器目錄里。
我就曾經在這里栽過一個很冤枉的跟頭。有一次,我上載HTML和applet的時候搞錯了package的名字,因此把applet放錯了目錄。但是我的瀏覽器還能在本地的CLASSPATH里找到程序,所以我成了唯一一個能正常運行這個applet的人了。所以給applet標記的CODE參數賦值的時候,一定要給全名,這一點非常重要。很多公開發表的applet都沒有打包,但是實際工作中,最好還是打個包。
用命令行啟動applet
有時你還會希望讓GUI程序能做一些別的事情。比方說在保留Java引以為豪的跨平臺性的前提下,讓它做一些"普通"程序能作的事。
你可以用Swing類庫寫出與當前操作系統的外觀風格完全一致的應用程序。如果你要編寫GUI程序,先看看是不是能最新版的Java及與之相關的工具,只有是,那才值得去寫。因為只有這樣,才能寫出不會讓用戶覺得討厭的程序。。
你肯定會希望能編寫出既能在命令行下啟動,又能當applet運行的程序。這樣測試applet的時候,會非常方便。因為通常從命令行啟動要比從Web瀏覽器和Appletviewer啟動更快,也更方便。
要想創建能用命令行啟動的applet,只要在類里加一個main( ),讓它把這個applet的實例嵌到JFrame里面就行了。
我們只加了一個main( ),其它什么都沒動。main( )創建了applet的實例,并把它加到JFrame里面,這樣我們就能看到它了。
你會看到main( )明確地調用了applet的init(
)和start( )。這是因為,這兩步原先是交給瀏覽器做的。當然這并不能完全替代瀏覽器,因為前者還會調用stop( )和destroy( ),不過大致說來,這個辦法還是可行的。如果還有問題,你可以親自去調用。[80]
注意,要是沒有這行:
frame.setVisible(true);
那么你什么都看不見。
一個專門用來顯示Applet的框架
雖然將applet轉換成應用程序的代碼是非常有用的,但寫多了就既浪費紙張又讓人生厭了。
setDefaultCloseOperation( )的作用是,告訴程序,一旦JFrame被關掉了,程序也應該退出了。默認情況下關掉JFrame并不意味著退出程序,所以除非你調用setDefaultCloseOperation( )或者寫一些類似的代碼,否則程序是不會中止的。
制作按鈕
做按鈕很簡單:想讓按鈕顯示什么,就拿它作參數去調用JButton的構造函數。
通常按鈕是類的一個字段,這樣就能用它來表示按鈕了。
JButton是一個組件——它有自己的小窗口——更新的時候會自動刷新。也就是說,你不用明確地指示該如何顯示按鈕或是其他什么控件;只要把它們放到表單上,它們就能自己把自己給畫出來了。所以你得在init( )把按鈕放到表單上。
在往content pane里面加東西之前,我們先把它的"布局管理器(layout manager)"設成FlowLayout。布局管理器的作用是告訴面板將控件放在表單上的哪個位置。這里我們不能用applet默認的BorderLayout,這是因為如果用它,一旦你往表單上加了新的控件,舊控件就會被全部遮住了。而FlowLayout則會讓控件均勻地散布在表單上,從左到右,從上到下。
捕捉事件
編寫事件驅動的程序是GUI編程的一個重要重要組成部分,而驅動程序的基本思路就是,將事件與代碼聯系起來。
Swing的思路是,將接口(圖形組件)和實現(當組件發生了某個事件之后,你要運行的代碼)明明白白地分開來。Swing組件能通報在它身上可以發生什么事件,以及發生了什么事件。所以,如果你對某個事件不感興趣,比如鼠標從按鈕的上方經過,你完全可以不去理會這個事件。用這種方法能非常簡潔優雅地解決事件驅動的問題,一旦你理解了其基本概念,你甚至可以去直接使用過去從未看到過的Swing組件。實際上這個模型也適用于JavaBean。
我們還是先來關心一下我們要用的這個控件的主要事件。就這個例子而言,這個控件JButton,而"我們感興趣的事件"就是按下按鈕。要想表示你對按鈕被按下感興趣,可以調用addActionListener(
)。這個方法需要一個ActionListener參數。ActionListener是一個接口,它只有一個方法,actionPerformed( )。所以要想讓JButton執行任務,你就得實現先定制一個ActionListener,然后用addActionListener方法把這個類的實例注冊給JButton。當按鈕被按下的時候,這個方法就會啟動了。(這通常被稱作回調(callback))
JTextField這個組件可以顯示文本。它的文本既可以是用戶輸入的,也可以是程序插的。雖然創建JTextField的方法有很多,但是最簡單的還是把寬度直接傳給JTextField的構造函數。等到JTextField被放上了表單,你就可以用setText( )來修改內容了(JTextField還有很多方法,請參閱JDK文檔)。
JTextArea
與JTextField相比,JTextArea能容納多行文字,提供更多的功能,除此之外它們很相似。append( )方法很有用;你可以用它把程序的輸出直接導到JTextArea里,這樣你就能用Swing程序來改進我們先前寫的,往標準輸出上打印的命令行程序了(因為你能用滾動條看到前面的輸出了)。
在把JTextArea加入applet的時候,我們先把它嵌入JScrollPane,這樣一旦文本多了出來,滾動條就會自動顯現了。裝滾動條就這么簡單。相比其它GUI編程環境下的類似控件,JScrollPane的簡潔與優雅真是令人折服。
控制布局
用Java把組件放到表單上的方法可能與你見過的其他GUI系統的都不同。首先,它全部都是代碼,根本沒有"資源"這個故事。其次,組件在表單上的位置不是由絕對定位來控制的,而是由一個被稱為"布局管理器"的對象來控制的。布局管理器的作用是根據組件add( )的順序,決定其擺放的位置。組件的大小,形狀,擺放的位置會隨布局管理器的不同而有明顯的差別。此外布局管理器會根據applet或應用程序的窗口大小,自動調整組件的位置,這樣一旦窗口的大小改變了,組件的尺寸,形狀,擺放位置也會相應的作出調整。
JApplet,JFrame ,JWindow以及JDialog,這幾個組件都有一個getContentPane( )方法。這個方法能返回一個Container,而這個Container的作用是安置(contain)和顯示Component。Container有一個setLayout( )方法,你就是用它來選擇布局管理器的。其它類,比如JPanel,能直接安置和顯示組件,所以你得跳過內容面板(content
pane)直接設置它的布局管理器。
BorderLayout
Applet的默認布局管理器是BorderLayout。如果沒有其它指令,BorderLayout會把所有控件全都放到表單的正中,并且拉伸到最大。
好在BorderLayout的功能還不止這些。它還有四個邊界以及中央的概念。當你往BorderLayout的面板上加控件加時,你還可以選擇重載過的add( )方法。這時要給它一個常量。這個常量可以是下邊五個中的一個:
BorderLayout. NORTH
|
頂邊
|
BorderLayout. SOUTH
|
底邊
|
BorderLayout. EAST
|
右邊
|
BorderLayout. WEST
|
左邊
|
BorderLayout.CENTER
|
填滿當中,除非碰到其它組件或表單的邊緣
|
如果你不指明擺放對象的區域,默認它就使用CENTER。
除了CENTER,其它控件都會在一個方向上壓縮到最小,在另一個方向上拉伸到最大。而CENTER則會往兩個方向上拉伸,直至占滿整個中間區域。
FlowLayout
它會讓組件直接"流"到表單上,從左到右,從上到下,一行滿了再換一行。
用了FlowLayout之后,組件就會凍結在它的"固有"尺寸上。比如JButton就會根據字符串的長度來安排大小。
由于FlowLayout面板上的控件會被自動地壓縮到最小,因此時常會出現一些意外。比方說,JLabel是根據字符串來決定控件的大小的,所以當你對FlowLayout面板上的JLable控件的文本作右對齊時,顯示效果不會有任何變化。
GridLayout
GridLayout的意思是,把表單視作組件的表格,當你往里加東西的時候,它會按從左到右,從上到下的順序把組件放到格子里。創建布局管理器的時候,你得在構造函數里指明行和列的數目,這樣它就能幫你把表單劃分成相同大小的格子了。
GridBagLayout
GridBagLayout的作用是,當窗口的大小發生變化時,你能用它來準確地控制窗口各部分的反應。但與此同時,它也是最難和最復雜的布局管理器。它主要是供GUI builder生成代碼用的(或許GUI
builder用的不是絕對定位而是GridBagLayout)。如果你的設計非常復雜,以至于不得不用GridBagLayout,那么建議你使用GUI builder。
絕對定位
還可以這樣對圖形組件進行絕對定位:
- 將Container的布局管理器設成null:setLayout(null)。
- 往面板上加組件的時候,先調用setBounds( )或reshape( )方法(根據各個版本)為組件設一個以象素為單位的矩形。這個步驟可以放在構造函數里,也可以放在paint( )里面,看你的需要。
有些GUI builder主要用的就是這個方法,但是通常情況下,這并不是最好的方法。
BoxLayout
由于GridBagLayout實在是太難學難用了,所以Swing引入了一個新的BoxLayout。它保留了GridBagLayout的很多優點,但是卻沒那么復雜。所以當你想手工控制控件的布局時,可以優先考慮用它(再重申一遍,如果設計非常復雜,最好還是用GUI builder)。BoxLayout能讓你在垂直和水平兩個方向上控制組件的擺放,它用一些被稱為支柱(struts)和膠水(glue)的東西來控制組件間的距離。
BoxLayout的構造函數同其它布局管理器稍有些不同——它的第一個參數是這個BoxLayout將要控制的Container,第二個參數是布局的方向。
為了簡化起鑒,Swing還提供了一種內置BoxLayout的特殊容器,Box(譯者注:這里的容器是指java.awt.Container類)。Box有兩個能創建水平和垂直對齊的box的static的方法,下面我們就用它來排列組件。
有了Box之后,再要往content pane里加組件的時候,你就可以把它成第二的參數了。
支柱(struts)會把組件隔開,它是大小是按象素算的。用的時候,只要把它當作空格加在兩個組件的中間就行了。
與固定間距的struts不同,glue(膠水)會盡可能地將組件隔開。所以更準確的說,它應該是"彈簧(spring)"而不是"膠水"(再加上這種設計是基于被稱為"springs and struts"的思想的,因此這個術語就顯得有些奇怪了)。
strut只控制一個方向上的距離,而rigid area(剛性區域)則在兩個方向上固定組件間的距離。
應該告訴你,對rigid area的評價不是那么的統一。實際上它就是絕對定位,因此有些人認為,它根本就是多余的。
最佳方案?
Swing的功能非常強大,短短幾行代碼就能做很多事。實際上你完全可以把這幾種布局結合起來,完成一些比較復雜的任務。但是,有些時候用手寫代碼創建GUI表單就不那么明智了;首先是太復雜,其次也不值得。Java和Swing的設計者們是想用語言和類庫去支持GUI builder工具,然后讓你用工具來簡化編程。實際上只要知道布局是怎么一回事,以及事件該怎么處理(下面講)就行了,懂不懂手寫代碼控制控件的擺放,其實并不重要。你完全可以選一個趁手的工具(畢竟Java的初衷就是想讓你提高編程的效率)。
Swing的事件模型
在Swing的事件模型中,組件可以發起(或"射出")事件。各種事件都是類。當有事件發生時,一個或多個"監聽器(listener)"會得到通知,并做出反應。這樣事件的來源就同它的處理程序分隔開來了。一般說來,程序員是不會去修改Swing組件的,他們寫的都是些事件處理程序,當組件收到事件時,會自動調用這些代碼,因此Swing的事件模型可稱得上是將接口與實現分隔開來的絕好范例了。
實際上事件監聽器(event listener)就是一個實現listener接口的對象。所以,程序員要做的就是創建一個listener對象,然后向發起事件的組件注冊這個對象。注冊的過程就是調用組件的addXXXListener(
)方法,這里"XXX"表示組件所發起的事件的類型。只要看一眼"addListener"方法的名字就能知道組件能處理哪種事件了,所以如果你讓它聽錯了事件,那么編譯就根本通不過。到后面你就會看到,JavaBean在決定它能處理哪些事件時,也遵循"addListener"的命名規范。
事務邏輯都應該封裝成listener。創建listener的唯一的條件是,它必須實現接口。你完全可以創建一個"全局的listener(global listener)",但是內部類或許更合適。這么做不僅是因為要根據UI或事務邏輯對listener進行邏輯分組,更重要的是(你很快就會看到),要利用內部類可以引用宿主類對象的特性,這樣就能跨越類或子系統的邊界進行調用了。
我們前面的例子里已經涉及到Swing的事件模型了,下面我們把這個模型的細節補充完整。
事件與監聽器種類
所有Swing組件都有addXXXListener( )和removeXXXListener( )方法,因此組件都能添加和刪除監聽器。你會發現,這里的"XXX"也正好是方法的參數,例如addMyListener(MyListener
m)。下面這張表列出了基本的事件,監聽器和方法,以及與之相對應的,提供了addXXXListener( )和removeXXXListener( )方法的組件。要知道,這個事件模型在設計時就強調了擴展性,因此你很可能會遇到這張表里沒有講到過的事件和監聽器。
事件,listener接口以及add和remove方法
|
支持這一事件的組件
|
ActionEvent
ActionListener
addActionListener( )
removeActionListener( )
|
JButton, JList, JTextField, JMenuItem 以及它們的派生類JCheckBoxMenuItem,
JMenu,和JpopupMenu
|
AdjustmentEvent
AdjustmentListener
addAdjustmentListener( )
removeAdjustmentListener( )
|
JScrollbar以及實現Adjustable接口的組件
|
ComponentEvent
ComponentListener
addComponentListener( )
removeComponentListener( )
|
*Component及其派生類JButton, JCheckBox, JComboBox, Container, JPanel, JApplet,
JScrollPane, Window, JDialog, JFileDialog, JFrame, JLabel, JList, JScrollbar,
JTextArea,和JTextField
|
ContainerEvent
ContainerListener
addContainerListener( )
removeContainerListener( )
|
Container及其派生類JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog,和JFrame
|
FocusEvent
FocusListener
addFocusListener( )
removeFocusListener( )
|
Component及其"派生類(derivatives*)"
|
KeyEvent
KeyListener
addKeyListener( )
removeKeyListener( )
|
Component及其"派生類(derivatives*)"
|
MouseEvent(包括點擊和移動)
MouseListener
addMouseListener( )
removeMouseListener( )
|
Component及其"派生類(derivatives*)"
|
MouseEvent[81](包括點擊和移動)
MouseMotionListener
addMouseMotionListener( )
removeMouseMotionListener( )
|
Component及其"派生類(derivatives*)"
|
WindowEvent
WindowListener
addWindowListener( )
removeWindowListener( )
|
Window及其派生類JDialog, JFileDialog,和JFrame
|
ItemEvent
ItemListener
addItemListener( )
removeItemListener( )
|
JCheckBox, JCheckBoxMenuItem, JComboBox, JList, 以及實現了ItemSelectableinterface的組件
|
TextEvent
TextListener
addTextListener( )
removeTextListener( )
|
JTextComponent的派生類,包括JTextArea和JTextField
|
一旦你知道了組件所支持的事件,你就用不著再去查文檔了。你只要:
- 把event類的名字里的"Event"去掉,加上"Listener",這就是你要實現的接口的名字了。
- 實現上述接口,想捕捉哪個事件就實現它的接口。比方說,如果你對鼠標的移動感興趣,你可以去實現MouseMotionListener接口的mouseMoved( )方法。(你必須實現這個接口的全套方法,但是這種情況下,通常都會有捷徑,過一會就會看到了。)
- 創建一個listener的對象。在接口的名字前面加一個"add",然后用這個方法向組件注冊。比如,addMouseMotionListener(
)。
下面是部分listener接口的方法:
Listener接口/Adapter
|
接口所定義的方法
|
ActionListener
|
actionPerformed(ActionEvent)
|
AdjustmentListener
|
adjustmentValueChanged(AdjustmentEvent)
|
ComponentListener
ComponentAdapter
|
componentHidden(ComponentEvent)
componentShown(ComponentEvent)
componentMoved(ComponentEvent)
componentResized(ComponentEvent)
|
ContainerListener
ContainerAdapter
|
componentAdded(ContainerEvent)
componentRemoved(ContainerEvent)
|
FocusListener
FocusAdapter
|
focusGained(FocusEvent)
focusLost(FocusEvent)
|
KeyListener
KeyAdapter
|
keyPressed(KeyEvent)
keyReleased(KeyEvent)
keyTyped(KeyEvent)
|
MouseListener
MouseAdapter
|
mouseClicked(MouseEvent)
mouseEntered(MouseEvent)
mouseExited(MouseEvent)
mousePressed(MouseEvent)
mouseReleased(MouseEvent)
|
MouseMotionListener
MouseMotionAdapter
|
mouseDragged(MouseEvent)
mouseMoved(MouseEvent)
|
WindowListener
WindowAdapter
|
windowOpened(WindowEvent)
windowClosing(WindowEvent)
windowClosed(WindowEvent)
windowActivated(WindowEvent)
windowDeactivated(WindowEvent)
windowIconified(WindowEvent)
windowDeiconified(WindowEvent)
|
ItemListener
|
itemStateChanged(ItemEvent)
|
之所以這張表不是很全,部分是因為事件模型允許你創建自己的事件及相關的listener。所以你時常會碰到一些在事件類型方面自成體系的類庫,而你在本章所學到的知識會幫助你學會使用這些事件。
用listener的adapter簡化編程
可以看到上面那張表里的一些listener只有一個方法。實現這種接口的工作量并不大,因為寫完方法,接口也就實現了。但是如果你要使用有多個方法的listener的話,事情就不那么輕松愉快了。
為了解決這個問題,有些(但不是全部)多方法的listener接口提供了適配器(adapter)。從上面那張表已經列出了它們的名字。適配器會為接口提供默認的空方法。這樣,你只要繼承適配器,根據需要覆寫方法就可以了。
adapter就是用來簡化listener的創建的。
但是適配器這種東西也有缺點。
只是大小寫方面的一個疏漏,它就成為一個新方法了。還好,這還不是關閉窗口時會調用的方法,所以最壞的結果也只是不能實現預期的效果。雖然有種種不方便,但接口卻能保證讓你實現所有應該實現的方法。
跟蹤多個事件
Swing組件的一覽表
現在你已經知道布局管理器和事件模型了,接下來就要學習怎樣使用Swing組件了。這部分只是一個大致的介紹,我們講的都是常用的Swing組件及其特性。
記住:
- JDK文檔里有所有Swing組件的資料。
- Swing事件的命名規范比較合理,所以要猜該怎樣編寫和安裝事件處理程序也比較簡單。用我們前面講的ShowAddListeners.java來檢查組件。
- 如果事情開始變得復雜了,那么恭喜你畢業了,該用GUI Builder了。
Button
Swing收錄了很多Button,包括各種按鈕,check box, radio button,甚至連菜單項(menu item)都是繼承AbstractButton的(鑒于菜單項也牽涉進來了,(AbstractButton)可能還是叫"AbstractSelector"或其它什么名字更好一些)。
Button組
如果想讓radio button以"幾選一"的方式運行(譯者注:原文為exclusive or,字面的意思是"排他性的邏輯與"),你就必須把他們加到一個"button組(button group)"里。但只要是AbstractButton,都可以加進ButtonGroup。
這幾步給原本很簡單任務加了點難度。要讓button做到"幾選一",你必須先創建一個button組,然后把要加進去的button加進去。
Icon
Icon能用于JLabel和AbstractButton(包括JButton,JCheckBox,JRadioButton以及JMenuItem)。把Icon用于JLabel的語法非常簡單
你也可以使用你自己的gif文件。如果想打開文件讀取圖像,只要創建一個ImageIcon,并且把文件的名字傳給它就行了。接下來,你就可以在程序中使用這個Icon了。
很多Swing組件構造函數都可以拿Icon作參數,不過你也可以用setIcon( )方法添加或修改Icon。
Tool tips
幾乎所有與GUI相關的類都繼承自JComponent,而JComponent又包含了一個setToolText(String)方法。因此不管是哪種組件,只要能放到表單上,你幾乎都可以用(假設這是個JComponent的派生類對象jc)
jc.setToolTipText("My
tip");
來設置tool tip。這樣只要鼠標停在JComponent上一段時間,旁邊就會跳出一個提示框,里面就是你設置的文本。
Text fields
Borders
JComponent里面有一個setBorder( )方法,它能讓你為各種可視組件安上有趣的邊框。
你還可以創建你自己的邊框,然后把它放到按鈕(button),標簽(label)或者其它控件里面,——只要它是繼承JComponent的就行。
JScrollPanes
大多數時候,你只需要JScrollPane能干好它的本職工作,但是你也可以去控制它,告訴它顯示哪根滾動條——垂直的,水平的,還是兩個都顯示或者兩個都不顯示。
通過給JScrollPane的構造函數傳不同的參數,可以控制它的滾動條。
一個袖珍的編輯器
不用費多大的勁,JTextPane已經提供了很多編輯功能。
applet的默認布局是BorderLayout。所以,如果你什么都不說,直接往面板里面加組件,那么它會填滿整個面板。但是如果你指明其位置(NORTH,SOUTH, EAST,或WEST),控件就會被釘在這個區域里。
注意JTextPane的內置功能,比如自動換行。此外它還有很多其他功能,具體詳情請參閱JDK文檔。
Check boxes
Check box能讓你做逐項的開/關選擇。它由一個小框和一個標簽組成。一般來說,選中之后框里會有一個'x'(或者其它什么表示選中的標記),否則就是空的。
一般來說,你用構造函數創建JCheckBox的時候,會傳給它一個用作標簽的參數。JCheckBox創建完了之后,你還可以隨時讀取或設置它的狀態,或者讀取或重設它的標簽。
不管是選中還是清除,JCheckBox都會引發一個事件。捕捉方法同按鈕事件完全相同:用ActionListener。
Radio buttons
GUI編程里的radio button的概念來自于老式的汽車收音機里的機械式按鈕;當你按下一個按鈕之后,另一個就會彈出來。因此你只能選一個。
要想創建一組相關聯的JRadioButton,只要把它們加進ButtonGroup就可以了(一個表單里允許有任意數量的ButtonGroup)。如果你(用構造函數的第二個參數)把多個radio
button設成true了,那么只有最后一個才是有效的。
注意,捕捉radio button事件的方法同捕捉其它事件的完全相同。
text field它可以可以代替JLabel。
Combo boxes (下拉式列表)
同radio
button組一樣,下拉式列表只允許用戶在一組選項里面選取一個元素。但是這種做法更簡潔,而且能在不驚擾客戶的情況下修改列表中的元素。(你也可以動態地修改radio button,但是這個視覺效果就太奇怪了)。
JComboBox的默認行為同Windows的combo box不太一樣。在Windows里,你既可以從combo box的列表里選一個,也可以自己輸入,但是在JComboBox里,這么做就必須先調用setEditable( )了。此外你只能從列表中選擇一個。下面我們舉一個例子。我們先給JComboBox加幾個選項,然后每按一下按鈕就加一個選項。
List boxes
List box與JComboBox的不同不僅僅在外觀上,它們之間有著非常重大的區別。JComboBox激活之后會彈出下拉框而JList則總是會在屏幕上占一塊固定大小,永遠也不會變。如果你想列表里的東西,只要調用getSelectedValues( )就行了,它會返回一個String的數組,其中包含著被選中的元素。
JList能做多項選擇;如果你control-click了一個以上的選項(在用鼠標進行選擇的時候,一直按著"control"鍵),那么已選中的選項就會一直保持"高亮(highlighted)",這樣你能選任意多個了。如果你先選一項,然后再shift-click另一個,那么兩個選項之間的所有選項就都被選中了。要取消一個選項只要control-click就可以了。
如果你只是想把String數組放到JList里面,那么還有一個更簡單的辦法;就是把數組當作參數傳給JList的構造函數,這樣它就會自動創建一個列表了。
JList不會自動提供滾動軸。當然只要把它嵌到JScrollPane里它就會自動鑲上滾動軸了,具體的細節它自會打理。
Tabbed panes
JTabbedPane能創建"帶頁簽的對話框(tabbed dialog)",也就是對話框的邊上有一個像文件夾的頁簽一樣的東西,你只要點擊這個頁簽,對話框就把這一頁顯示出來。
在Java編程當中熟練使用頁簽面板(tabbed panel)相當重要,因為每當applet要彈出一個對話框的時候,它都會自動加上一段警告,所以彈出式對話框在applet里并不受歡迎。
程序運行起來你就會發現,如果tab太多了,JTabbedPane還會自動把它們堆起來以適應一定的行寬。
Message boxes
圖形界面系統通常都包含一套標準的,能讓你迅速地將消息傳給用戶,或者從用戶那里得到信息的對話框。對于Swing來說,這些消息框就包含在JOptionPane里面了。你有很多選擇(有些還相當復雜),但是最常用的可能還是"確認對話框(confirmation dialog)",它用static
JOptionPane.showMessageDialog( )和JOptionPane.showConfirmDialog(
)啟動。
注意showOptionDialog( )和showInputDialog( )會返回用戶輸入的信息。
菜單
所有能包含菜單的組件,包括JApplet,JFrame,JDialog以及它們所派生的組件,都有一個需要JMenuBar作參數的setJMenuBar( )方法(一個組件只能有一個JMenuBar)。你可以把JMenu加入JMenuBar,再把JMenuItem加入JMenu。每個JMenuItem都可以連一個ActionListener,當你選中菜單項(menu item)的時候,事件就發出了。
與使用資源的系統不同,Java和Swing要求你必須用源代碼來組裝菜單。
通常情況下每個JMenuItem都得有一個它自己的ActionListener。
JMenuItem是AbstractButton的派生類,所以它有一些類似按鈕的行為。JMenuItem本身就是一個可以置入下拉菜單的菜單選項。此外JMenuItem還有三個派生類:用來持有其它JMenuItem的JMenu(這樣你就可以做層疊式菜單了);有一個能表示是否被選中的"候選標記(checkmark)"的JCheckBoxMenuItem;以及包含一個radio button的JRadioButtonMenuItem。
為了演示在程序運行期間動態地交換菜單條,我們創建了兩個JMenuBar。你會發現JMenuBar是由JMenu組成的,而JMenu又是由JMenuItem,JCheckBoxMenuItem甚至JMenu(它會創建子菜單)組成的。等JMenuBar組裝完畢,你就可以用setJMenuBar( )方法把它安裝到當前程序里面了。注意當你按下鈕按的時候,它會用getJMenuBar(
)來判斷當前的菜單,然后把另一菜單換上去。
字符串的比較是引發編程錯誤的一大誘因。
程序會自動的將菜單項勾掉或恢復。JCheckBoxMenuItem的代碼演示了兩種怎樣決定該勾掉哪個菜單項的方法。一個是匹配字符串(雖然也管用,但就像上面我們所講的,不是很安全),另一個則是匹配事件目標對象。正如你所看到的,getState( )方法可以返回JCheckBoxMenuItem的狀態,而setState( )則可以設置它的狀態。
菜單事件不是很一致,這一點可能會引起混亂。JMenuItem使用ActionListener事件,而JCheckBoxMenuItem則使用ItemListener事件。JMenu也支持ActionListener,但通常沒什么用??傊?,你得為每個JMenuItem,JCheckBoxMenuItem或JRadioButtonMenuItem都制備一個listener,不過這里我們偷了點懶,把一個ItemListener和ActionListener連到多個菜單組件上了。
Swing支持助記符,或者說"快捷鍵",這樣你就可以扔掉鼠標用鍵盤來選取AbstractButton了(按鈕,菜單項等)。要這么做很容易,就拿JMenuItem舉例,你可以用它重載了的構造函數,把快捷鍵的標識符當作第二個參數傳給它。不過絕大多數AbstractButton都沒有提供類似的構造函數,所以比較通用的辦法還是用setMnemonic(
)方法。在上述例程中我們為按鈕和多個菜單項加上了快捷鍵,這些快捷鍵的提示會自動顯示在組件上。
好工具應該能幫你維護好菜單。
彈出式菜單
要想實現JPopupMenu,最直截了當的辦法就是創建一個繼承MouseAdapter的內部類,然后把這內部類的實例加到要提供彈出式菜單的組件里:
JMenuItem用的是同一個ActionListener,它負責從菜單標簽里面提文本并且把它插入JTextField。
畫屏幕
一個好的GUI框架能讓作圖相對而言比較簡單——確實如此,Swing就做到了。所有作圖問題都面臨同一個難點,那就是相比調用畫圖函數,計算該在哪里畫東西通常會更棘手,但不幸的是這種計算又常常和作圖函數的調用混在一起,所以作圖函數的接口的實際的復雜程度很可能會比你認為的要簡單。
雖然你可以在任何一個JComponent上作畫,也就是說它們都能充當畫布(canvas),但是如果你想要一塊能直接畫東西的白板,最好還是創建一個繼承JPanel的類。這樣你只需要覆寫一個方法,也就是paintComponent( )就行了。當系統需要重畫組件的時候,會自動調用這個方法(通常情況下,你不必為此操心,因為這是由Swing控制的)。調用的時候,Swing會傳一個Graphics對象給這個方法,這樣你就能用這個對象作畫了。
覆寫paintComponent( )的時候,必須首先調用其基類的同名方法。接下來再做什么就由你了決定了;通常是調用Graphics的方法在JPanel上畫畫或者是在其象素上著色。要想了解具體怎樣使用這些方法,可以查閱java.awt.Graphics文檔(可以到java.sun.com上面去找JDK文檔)。
如果問題非常復雜,那么還有一些更復雜的解決方案,比如第三方的JavaBean或者Java 2D API。
對話框
所謂對話框是指,能從其他窗口里面彈出來的窗口。其作用是,在不搞亂原先窗口前提下,具體處理其某一部分的細節問題。對話框在GUI編程中的用途很廣,但是在applet中用的不多。
要想創建對話框,你得先繼承JDialog。和JFrame一樣,JDialog也是另一種Window,它也有布局管理器(默認情況下是BorderLayout),也可以用事件監聽器來處理事件。它同JFrame有一個重要的區別,那就是在關對話框的時候別把程序也關了。相反你得用dispose( )方法將對話框窗口占用的資源全部釋放出來。
一旦創建完JDialog,你就得用show( )來顯示和激活它了。關閉對話框的時候,還得記住要dispose( )。
你會發現,對applet來說,包括對話框在內所有彈出的東西都是"不可信任的"。也就是說彈出來的窗口里面有一個警告。這是因為,從理論上講惡意代碼可以利用這個功能來愚弄用戶,讓他們覺得自己是在運行一個本地應用程序,然后誤導它們輸入自己的信用卡號碼,再通過Web傳出去。applet總是和網頁聯在一起,因此只能用瀏覽器運行,但是對話框卻可以脫離網頁,所以從理論上講這種欺騙手段是成立的。所以這么一來,applet就不太會用到對話框了。
由于static只能用于宿主類,因此內部類里不能再有static的數據或是嵌套類了。
paintComponent( )負責把panel的周圍的方框以及"x"或"o"畫出來。雖然充斥著單調的計算,但是還算簡明。
文件對話框
有些操作系統還內置了一些特殊的對話框,比如讓你選擇字體,顏色,打印機之類的對話框。實際上所有的圖形操作系統都提供了打開和存儲文件的對話框,所以為了簡化起鑒,Java把它們都封裝到JFileChooser里面了。
注意JFileChooser有很多變例可供選擇,比方說加一個過濾器濾文件名之類的。
要用"open file"對話框就調用showOpenDialog( ),要用"save file"對話框,就調用showSaveDialog(
)。在對話框關閉之前,這兩個函數是不會返回的。即便對話框關了,JFileChooser對象仍然還在,所以你還去讀它的數據。要想知道操作的結果,可以用getSelectedFile( )和getCurrentDirectory( )。如果返回null則說明用戶按了cancel。
Swing組件上的HTML
所有能顯示文件的組件都可以按照HTML的規則顯示HTML的文本。也就是說你可以很方便地讓Swing組件顯示很炫的文本。比如:
文本必須以"<html>"開頭,下面你就可以用普通的HTML標記了。注意,它沒有強制你一定要關閉標記。
JTabbedPane,JMenuItem,JToolTip,JRadioButton以及JCheckBox都支持HTML文本。
Slider和進程條
Slider能讓用戶通過來回移動一個點來輸入數據,有時這種做法還是很直觀的(比方說調節音量)。進程條(progress bar)則以一種用類比的方式顯示數據,它表示數據是"全部"還是"空的",這樣用戶就能有一個比較全面的了解了。
JProgressBar還比較簡單,而JSlider的選項就比較多了,比如擺放的方向,大小刻度等等。注意一下給Slider加帶抬頭的邊框的那行代碼,多簡潔。
樹
JTree的用法可以簡單到只有下面這行代碼:
add(new JTree(new Object[] {"this",
"that", "other"}));
這樣顯示的是一棵最基本的樹。JTree的API非常龐大,應該是Swing類庫里最大的之一了。雖然你可以用它來做任何事情,但是要想完成比較復雜任務,就需要一定的研究和實驗了。
好在這個類庫還提供了變通手段,也就是一個"默認"的,能滿足一般需求的樹型組件。所以絕大多數情況下你都可以使用這個組件,只有在特殊情況下,你才需要去深入研究樹。
Trees類包含一個用來創建多個Branch的兩維String數組,以及一個用來給數組定位的static int i。節點放在DefaultMutableTreeNode里面,但是實際在屏幕上顯示則是由JTree及與之相的model——DefaultTreeModel控制的。注意JTree在加入applet之前,先套了一件JScrollPane,這樣它就能提供自動的滾動軸了。
對JTree的操控是經由它的model來實現的。當model發生變化時,它會產生一個事件,讓JTree對樹的顯示作出必要的更新。init( )用getModel( )提取這個model。當你按下按鈕的時候,它會創建一個新的新的"branch" 。等它找到當前選中的那個節點(如果什么也沒選,就用根節點)之后,model的insertNodeInto(
)方法就會接管所有的任務了,包括修改樹,刷新顯示等等。
或許上述例程已能滿足你的需求了。但是樹的功能強大到只要你能想到它就能做到的地步。但是要知道:差不多每個類都有一個非常龐大的接口,所以你要花很多時間和精力去理解它的內部構造。但話說回來,它的設計還是很優秀的,其競爭者往往更糟。
表格
和樹一樣,Swing的表格控件也非常復雜強大。剛開始的時候,他們是想把它做成用JDBC連接數據庫時常用的"grid"接口,因此它具有極高的靈活性,不過代價就是復雜度了。它能讓你輕易創建一個全功能的電子表格程序,不過這要花整整一本書篇幅才能講清楚。但是如果你弄懂了基本原理,也可以用它來創建一個相對簡單的JTable。
JTable只負責怎樣顯示數據,而數據本身是由TableModel控制的。所以在創建JTable之前,你通常都得先創建一個TableModel。你可以從頭開始去實現TableModel接口,但是Java提供了一個AbstractTableModel的幫助類,繼承它會比較簡單。
選擇Look & Feel
所謂"可插接式的外觀風格(look &
feel)"是指,你可以讓程序模擬其他操作環境的外觀。你甚至可以做一些更炫的事情,比如在程序運行期間動態改變其外觀。但是通常情況下,你只會在下面兩項中選一個:選擇"跨平臺"的外觀(也就是Swing的"metal"),或者選當前操作系統的外觀,讓Java程序看上去就像是為這個操作系統定制的(絕大多數情況下,這幾乎是勿庸置疑的選擇,這樣用戶就不至于被搞糊涂了)。不管你選哪個,代碼都很簡單,但是必須先執行這些代碼再創建組件,因為組件是按照當前的look
and feel創建的,而且程序運行到一半的時候,你再去改look and feel,它就不會跟著你去改了。(這個過程非常復雜,而且并不實用,所以我們把它留給Swing專著了)。
實際上如果你認為跨平臺的("metal")外觀是Swing程序的特色,而你也想用它,那你就可以什么都不作了——它是默認的look and feel。但是如果你選擇當前操作系統的外觀風格,那么只要插入下面這段代碼就可以了,一般來說是放在main( )開頭的地方,但是最晚要在加第一個組件之前:
try {
UIManager.setLookAndFeel(UIManager.
getSystemLookAndFeelClassName());
} catch(Exception e) {
throw new
RuntimeException(e);
}
你根本不用在catch里面做任何事,因為如果選擇其他look and feel失敗的話,UIManager會回到默認的跨平臺的look and feel。但是在調試的時候這個異常還是很起作用的,最起碼你可以從catch里看到些什么。
下面是一個用命令行參數來選擇look and feel的程序,順便也看看這幾個組件在不同的look and feel下都是什么樣子:
假如你為一個對程序外觀有特殊要求的公司做一個framework的話,你甚至可以自創一套look and feel。不過這可是一個大工程,其難度遠遠超出了本書的范圍。
剪貼板
JFC與系統剪貼板的互動功能非常有限(在java.awt.datatransfer package里面)。你可以把String對象當作文本復制到剪貼板里,也可以把剪貼板里的文本粘貼到String對象里。當然剪貼板支持任何類型的數據,至于數據在剪貼板里該怎么表示,那是粘貼數據的程序的事。Java通過"flavor"這個概念加強了剪貼板API的擴展性。當數據進到剪貼板的時候還跟著一組與這個數據相關聯的,可以轉換這些數據的flavor(比方說一幅畫可以表示成一個全部有數字組成的字符串或一個image),這樣你就能知道剪貼板里的數據是否支持你感興趣的flavor了。
JTextField和JTextArea它們原本就已經支持剪貼板了。
可以期待,未來Java會提供更多的flavor。你能得到更多的數據flavor的支持。
將applet打成JAR卷宗
JAR的一個主要用途就是優化applet的裝載。在Java 1.0時代,程序員們都盡量把applet的代碼塞進一個類里,這樣當用戶下載applet的時候只需向服務器發一次請求就可以了。但這么做不僅使代碼變得非常難讀(也難維護),而且.class文件也是未經壓縮的,因此下載速度仍有提升的潛力。
JAR解決了這個問題,它把所有的.class文件全都壓縮在一個供瀏覽器下載的文件里。現在你可以大膽運用正確的設計方案而不用再擔心它會產生多少.class文件了,而用戶的下載速度也更快了。
簽發applet
由于沙箱安全模型的限制,未獲簽名的applet是不能在客戶端進行某些操作的,比如寫文件,連接本地網絡等。一旦你簽發了applet,用戶就可以去核對那個自稱創建了這個applet的人是不是真的就是創建者了,同時他們也可以確認JAR文件是不是在離開服務器之后被篡改了。沒有這些最起碼的保證,applet是根本不可能去做任何可能損壞計算機或泄漏個人隱私的事的。這層限制對于applet在Internet上的安全運用是至關重要的,但同時也削弱了applet的功能。
自從有了Java Plugin,簽發applet的步驟也變得更簡單也更標準化了,而applet也成為一種更簡便的部署應用程序的方法了。簽發applet已經變得非常簡單了,而且也有了標準的Java工具了。
早先plugin還沒出來的時候,你得用Netscape的工具為Netscape的用戶簽署.jar文件,用Microsoft的工具為Internet Explorer用戶簽署.cab文件,然后在HTML文件里面為兩個平臺各自準備一套標記。而用戶也必須在瀏覽器里安裝證書,這樣applet才能獲得信任。
Plugin不僅提供了標準化的簽署和部署applet的方法,而且能自動安裝證書,方便了用戶。
} ///:~
要想簽名,你必須先把它做成一個JAR文件(見本章前面講過的jar工具這一節),然后再簽署這個文件。
有了JAR文件,你就得用證書或是密鑰來簽名了。如果是一個大公司,那么你可以跟Verisign或Thawte這樣的"認證中心(signing
authority)"提申請,它們會給你發給你證書的。證書是用來給代碼簽名的,這樣用戶就能確信你確實是他所下載的這段代碼的提供者了,而且自你簽發之后,這段代碼未被篡改過。電子簽名的本質是一串兩進制的數,當有人要核對簽名的時候,那個給你發證書的認證中心會為你作證。
認證中心發的證書是要付錢的,而且得定期更新。就這個問題而言,我們可以自己給自己簽一張證書。這個證書會存在文件里(通常被稱為keychain)。你可以用下面這條命令:
keytool –list
訪問默認的文件。如果默認的文件不存在,那么你還得先建一個,或者告訴它去找哪個文件?;蛟S你應該去試試"cacerts"文件。
keytool -list -file <path/filename>
其默認路徑通常是
{java.home}/lib/security/cacerts
其中,java.home表示JRE所在的目錄。
你也可以用keytool給自己發一份證書,供測試用。如果PATH環境變量里已經有Java的"bin"目錄了,那么這個命令就是:
keytool –genkey –alias <keyname> -keystore
<url>
其中keyname表示key的別名,比如“mykeyname”,url表示存放密鑰的位置,通常就放在上面講的cacerts文件里。
它會提示你輸入(keystore的)密碼。默認是"changeit"(提醒你該做些什么)。然后是姓名,部門,單位,城市,州,國家。這些信息會被放進證書里。最后它會要你給證書設一個密碼。如果你對安全問題真的很在意,可以給它設一個單獨的密碼,默認情況下,證書的密碼就是"存證書的文件(keystore)"的密碼,一般來說這已經夠了。上面這些信息還可以用命令行提供給像Ant這樣的編譯工具使用。
如果你不給參數,直接在命令行下用keytool命令,那么它會把所有的選項全部都打印出來。你或許想用-valid 選項,看看證書的有效期還有多長。
如果想確認證書確實保存在cacerts文件里,用
keytool –list –keystore <url>
然后輸入前面設的密碼?;蛟S你的證書和別人的存放在一起(如果別人已經在這個keystore里存了證書的話)。
你剛獲得的那張證書是你自己簽發的,所以認證中心是不會認帳的。如果你用這張證書簽發JAR文件,最終用戶那里就會看到一個警告窗口,同時強烈建議他們不要使用這個程序。除非你去買一份有效力的證書,否則否則你和你的用戶就得忍著。
簽發JAR文件要用Java的jarsigner標準工具,命令如下:
jarsigner –keystore <url> <jarfile>
<keyname>
url表示cacerts文件的位置,jarfile表示JAR文件的名字,而keyname則是證書的別名。你還得再輸一遍密碼。
現在這個JAR文件就帶上你的這張證書的簽名了,而用戶也能知道它在簽發之后是不是被篡改了(包括修改,添加或刪除等)。
接下來你得操心一下HTML文件的applet標記的"archive"屬性了,JAR的文件名就在這里。
如果瀏覽器用的是Java的plugin,applet的標記還要更復雜一些,不過你可以創建一個簡單點的,就像這樣:
<APPLET
CODE=package.AppletSubclass.class
ARCHIVE =
myjar.jar
WIDTH=300
HEIGHT=200>
</APPLET>
然后用HTMLConverter過一遍,它會自動幫你生成正確的applet標記。
現在當用戶下載applet時,瀏覽器就會提醒他們現在正在裝載的是一個帶簽名的applet,并且問他是不是信任這個簽發者。正如我們前面所講的,測試用的證書并不具備很高的可信度,因此它會給一個警告。如果客戶信任了,applet就能訪問整個客戶系統了,于是它就和普通的程序沒什么兩樣了。
JNLP和Java
Web Start
雖然經過簽名的applet功能強大,甚至能在有效地取代應用程序,但它還是得在Web瀏覽器上運行。這不僅使客戶端增加了額外的運行瀏覽器的開銷,而且常常使用戶界面變得非常的單調和混亂。瀏覽器有它自己的菜單和工具條,而他們正好壓在applet的上面。
Java的網絡啟動協議(Java Network Launch
Protocol簡稱JNLP)能在不犧牲applet優點的前提下解決這個問題。你可以在客戶端上下載并安裝單獨的JNLP應用程序。它可以用命令行,桌面圖標,或隨JNLP一同分發的應用程序管理器啟動。程序甚至可以從最初下載的那個網站上啟動。
JNLP程序運行的時候會動態地從Internet上下載資源,并且自動檢查其版本(如果用戶連著Internet的話)。也就是說它同時具備applet和application的優點。
和applet一樣,客戶機在對待JNLP應用程序的時候也必須注意安全問題。JNLP應用程序是一種易于下載的,基于Web的應用程序,因此有可能會被惡意利用。有鑒于此,JNLP應用程序應該和applet一樣被放在沙箱里。同applet一樣,它可以用帶簽名的JAR文件部署,這時用戶可以選擇是不是信任簽發者。和applet的不同之處在于,即便沒有簽名,它仍然可以通過JNLP API去訪問客戶系統的某些資源(這就需要用戶在程序運行時認可這些請求了)。
JNLP是一個協議而非產品,因而得先把它實現了才能用。Java Web Start有稱JAWS就是Sun提供的,能免費下載的,JNLP的官方樣板實現。你只要下載安裝就行了,如果要做開發,不要忘了把JAR文件放到classpath里面。要想在網站上部署JNLP應用程序,只要確保服務器能認得application/x-java-jnlp-file的MIME類型就行了。如果是用最新版的Tomcat服務器(http://jakarta.apache.org/tomcat),那它應該已經幫你配置好了。否則就去查查服務器的用戶手冊。
創建JNLP應用程序并不難。先創建一個標準的應用程序,然后用JAR打包,最后再準備一個啟動文件就行了。啟動文件是一個很簡單的XML文件,它負責向客戶端傳遞下載和安裝應用程序的信息。如果你決定用不帶簽名的JAR文件來部署軟件,那還得用JNLP API來訪問客戶端系統上的資源。
注意,FileOpenService和FileCloseService是javax.jnlp里的類,要使用這兩個服務,不但要用ServiceManager.lookup(
)提出請求,而且要用這個方法所返回的對象來訪問客戶端資源。如果你不想受JNLP束縛,要直接使用這些類的話,那就必須使用簽名的JAR文件。
這個啟動文件的后綴名必須是.jnlp,此外它還必須和JAR文件呆在一個目錄里。
這是一個根節點為<jnlp>標記的XML文件。這個節點下面還包括了一些子元素,其中絕大部分是自解釋的。
jnlp元素的spec屬性告訴客戶端系統,這個應用程序需要哪個版本的JNLP。codebase屬性告訴客戶端到哪個目錄去找啟動文件和資源。通常它應該是一個指向Web服務器的HTTP URL,但這里為了測試需要,我們把它指到本機的目錄了。href屬性表示文件的名字。
information標記里有多個提供與程序相關的信息的子元素。它們是供Java Web
Start的管理控制臺或其它類似程序使用的。這些程序會把JNLP應用安裝到客戶端上,讓后讓用戶通過命令行,快捷方式或者其它什么方法啟動。
resource標記的功能HTML文件里的applet標記相似。j2se子元素指明程序運行所需的j2se的版本,jar子元素告訴客戶端class文件被打在哪個JAR文件里。此外jar元素還有一個download屬性,其值可以是"eager"或"lazy",它的作用是告訴JNLP是不是應該下載完這個jar再開始運行程序。
application-desc屬性告訴客戶端系統,可執行的class,也就是JAR文件的入口是哪個類。
jnlp標記還有一個很有用的子元素,那就是這里沒用到的security標記。下面我們來看看security標記長什么樣子:
<security>
<all-permissions/>
<security/>
只有在部署帶簽名的JAR文件時才能使用security標記。上面那段程序不需要這個標記,因為所有的本地資源都是通過JNLP服務來訪問的。
此外還有一些其它標記,具體細節可以參考http://java.sun.com/products/javawebstart/download-spec.htm
現在.jnlp文件也寫好了,接下來就是在網頁里加超鏈接了。這個頁面應該是個下載頁面。頁面上除了有復雜的格式和詳細介之外,千萬別忘了把這條加上:
<a href="classname.jnlp">click
here</a>
這樣你就可以點擊鏈接啟動JNLP應用程序的安裝進程了。你只要下載一次,以后就可以通過管理控制臺來進行配置了。如果你用的是Windows的Java Web Start的話,那么第二次啟動程序的時候,它會提示你,是不是創建一個快捷方式。這種東西是可以配置的。
我們這里只介紹了兩個JNLP服務,而當前版本里有七種。它們都是為特定的任務所設計的,比如像打印,剪貼板操作等。
編程技巧
由于Java的GUI編程是一個還在不斷改進的技術,Java 1.0/1.1與Java 2的Swing類庫之間就有著非常重大的區別,與舊模式相比,Swing能讓你用一種更好的方式編程。這里,我們會就其中一些問題做個介紹,同時檢驗一下這些編程技巧。
動態綁定事件
Swing的事件模型的優點就在于它的靈活性。你可以調用方法給組件添加或刪除事件。
- Button可以連不止一個listener。通常組件是以多播(multicast)方式處理事件的,也就是說你可以為一個事件注冊多個listener。但是對于一些特殊的,以單播(unicast)方式處理事件的組件,這么做就會引發TooManyListenersException了。
- 程序運行的時候能動態地往Button b2上面添加或刪除listener。你應該已經知道加listener的方法了,此外每個組件還有一個能用來刪listener的removeXXXListener( )方法。
這種靈活性為你帶來更大的便利
值得注意的是,listener的添加順序并不一定就是它們的調用順序(雖然絕大多數JVM確實是這么實現的)。
將業務邏輯(business logic)與用戶界面分離開來
一般情況下,設計類的時候總是強調一個類"只作一件事情"。涉及用戶界面的時候更是如此,因為你很可能會把"要作什么"同"要怎樣顯示"給混在一起了。這種耦合嚴重妨礙了代碼的復用。比較好的做法是將"業務邏輯(business login)"同GUI分離開來。這樣不僅方便了業務邏輯代碼的復用,也簡化了GUI的復用。
還有一種情況,就是多層系統(multitiered systems),也就是說”業務對象(business object)"完全貯存在另一臺機器上。業務規則的集中管理能使規則的更新立即對新交易生效,因此這是這類系統所追求的目標。但是很多應用程序都會用到這些業務對象,所以它們絕不能同特定的顯示模式連在一起。它們應該只做業務處理,別的什么都不管。
樹立了將UI同業務邏輯相分離的觀點之后,當你再碰到用Java去維護遺留下來的老代碼時,也能稍微輕松一點。
范式
內部類,Swing事件模型,還能繼續用下去的AWT事件模型,以及那些要我們用老辦法用的新類庫的功能,所有這些都使程序設計變得更混亂了?,F在就連大家寫亂七八糟的代碼的方式也變得五花八門了。
這些情況都是事實,但是你應該總是使用最簡單也最有條理的解決方案:用Listener(通常要寫成內部類)來處理事件。
用了這個模型,你可以少寫很多"讓我想想這個事件是誰發出的"這種代碼。所有代碼都在解決問題,而不是在做類型檢查。這是最佳的編程風格,寫出來的代碼不僅便于總結,可讀性和可維護性也高。
并發與Swing
寫Swing程序的時候,你很可能會忘了它還正用著線程。雖然你并沒有明確地創建過Thread對象,但它所引發的問題卻會乘你不備嚇你一跳。絕大多數情況下,你寫的Swing或其他帶窗口顯示的GUI程序都是事件驅動的,而且除非用戶用鼠標或鍵盤點擊GUI組件,否則什么事都不會發生。
只要記住Swing有一個事件分派線程就行了,它會一直運行下去,并且按順序處理Swing的事件。如果你想確保程序不會發生死鎖或者競爭的情形,那么倒是要考慮一下這個問題。
重訪Runnable
在第13章,我曾建議大家在實現Runnable接口時一定要慎重。 當然如果你設計的類必須繼承另一個類而這個類又得有線程的行為,那么選擇Runnable還是對的。
不同的JVM,在如何實現線程方面,存在著巨大的性能和行為差異。
管理并發
當你用main方法或另一個線程修改Swing組件的屬性時,一定要記住,有可能事件分派線程正在和你競爭同一個資源。
看來線程遇到Swing的時候,麻煩也跟著來了。要解決這個問題,你必須確保Swing組件的屬性只能由事件分派線程來修改。
這要比聽上去的容易一些。Swing提供了兩個方法,SwingUtilities.invokeLater( )和SwingUtilities.invokeandWait( ),你可以從中選一個。它們負責絕大多數的工作,也就是說你不用去操心那些很復雜的線程同步的事了。
這兩個方法都需要runnable對象作參數。當Swing的事件處理線程處理完隊列里的所有待處理事件之后,就會啟動它的run( )方法了。
能用這兩個方法來設置Swing組件的屬性。
可視化編程與JavaBeans
看到現在你已經知道Java在代碼復用方面的價值了。復用程度最高的代碼是類,因為它是由一組緊密相關的特征(字段field)和行為(方法)組成的,它既能以合成(composition),也能以繼承的方式被復用。
繼承和多態是面向對象編程的基礎,但是在構建應用程序的時候,絕大多數情況下,你真正需要的是能幫你完成特定任務的組件。你希望能把這些組件用到設計里面,就像電子工程師把芯片插到電路板上一樣。同樣,也應該有一些能加速這種"模塊化安裝"的編程方法。
Microsoft的Visual Basic為"可視化編程(Visual programming)"贏得了初次成功——非常巨大的成功,緊接著是第二代的Borland Delphi(直接啟發了JavaBean的設計) 。有了這些工具,組件就變得看得見摸的著了,而組件通常都表示像按鈕,文本框之類的可視組件,因此這樣一來組件編程也變得有意義了。實際上組件的外觀,通常是設計時是什么樣子運行時也就這樣,所以從控件框(palette)里往表單上拖放組件也就成了可視化編程的步驟了。而當你在這么做的時候,應用程序構造工具在幫你寫代碼,所以當程序運行時,它就會創建那些組件了。
通常簡單地把組件拖到表單上還不足以創建程序。你還得修改一些特征,比如它的顏色,上面的文字,所連接的數據庫等等。這些在設計時可以修改的特征被稱為屬性(properties)。你可以在應用程序的構建工具里控制組件的屬性。當程序創建完畢,這些配置信息也被保存下來,這樣程序運行時就能激活這些配置信息了。
看到現在你或許已經習慣這樣來理解對象了,也就是對象不僅是一組特征,還是一組行為。設計的時候,可視組件的行為部分的表現為事件,也就是說"是些能發生在這個組件上的事情"。一般來說你會用把代碼連到事件的方法來決定事件發生時該做些什么。
下面就是關鍵部分了:應用程序的構建工具用reflection動態地查詢組件,找出這個組件支持哪些屬性和事件。一旦知道它是誰,構建工具就能把這些屬性顯示出來,然后讓你作修改了(創建程序的時候會把這些狀態保存下來),當然還有事件??傊?,只要你在事件上雙擊鼠標或者其他什么操作,編程工具就會幫你準備好代碼的框架,然后連上事件?,F在,你只要編寫事件發生時該執行的代碼就可以了。
編程工具幫你做了這么多事,這樣你就能集中精力去解決程序的外觀和功能問題了,至于把各部分銜接起來的細節問題,就交給構建工具吧??梢暬幊坦ぞ咧阅塬@得如此巨大的成功,是因為它能極大的提高編程的效率,當然這一點首先體現在用戶界面,但是其它方面往往也受益頗豐。
JavaBean是干什么用的?
言歸正傳,組件實際上是一段封裝成類的代碼。關鍵在于,它能讓應用程序的構建工具把自己的屬性和事件提取出來。創建VB組件的時候,程序員必須按照特定的約定,用相當復雜的代碼把屬性和事件發掘出來。Delphi是第二代的可視化編程工具,而且整個語言是圍繞著可視化編程設計的,所以用它創建可視化組件要簡單得多。但是Java憑借其JavaBean在可視化組件的創建技術領域領先群雄。Bean只是一個類,所以你不用為創建一個Bean而去編寫任何額外的代碼,也不用去使用特殊的語言擴展。事實上你所要做的只是稍稍改變一下方法命名的習慣。是方法的名字告訴應用程序構建工具,這是一個屬性,事件還是一個普通的方法。
JDK文檔把命名規范(naming convention)錯誤地表述成"設計模式(design pattern)”。這真是不幸,設計模式(請參閱www.BruceEckel.com上的Thinking in Patterns (with Java))本身已經夠讓人傷腦筋的了,居然還有人來混淆視聽。重申一遍,這算不上是什么設計模式,只是命名規范而已,而且還相當簡單。
- 對于名為xxx的屬性,你通常都得創建兩個方法:getXxx( )和setXxx( )。注意構建工具會自動地將"get"和"set"后面的第一個字母轉換成小寫,以獲取屬性的名字。"get"所返回的類型與”set"所使用的參數的類型相同。屬性的名字同"get"和”set"方法返回的類型無關。
- 對于boolean型的屬性,你既可以使用上述的"get"和"set"方法,也可以用"is"來代替"get"。
- Bean的常規方法無需遵循上述命名規范,但它們都必須是public的。
- 用Swing的listener來處理事件。就是我們講到現在一直在用的這個方案:用addBounceListener(BounceListener)和removeBounceListener(BounceListener)來處理BounceListener。絕大多數情況下,內置的事件和監聽器已經可以滿足你的需要了,但是你也可以創建你自己的事件和監聽器接口。
第一點回答了你在比較新舊代碼時或許會注意的一個問題:很多方法的名字都有了一些很小的,但明顯沒什么意義的變化?,F在你應該知道了,為了把組件做成JavaBean,絕大多數修改是在同"get"和"set"的命名規范接軌。
用Introspector提取BeanInfo
當你把Bean從控件框(palette)里拖到表單上的時候,JavaBean架構中最重要的一環就開始工作了。應用程序構建工具必須能創建這個Bean(有默認構造函數的話就可以了),然后在不看Bean源代碼的情況下提取所有必須的信息,然后創建屬性表和事件句柄。
從第十章看,我們已經能部分地解決這個問題了:Java的reflection機制可以幫我們找出類的所有方法。我們不希望像別的可視化編程語言那樣用特殊的關鍵字來解決JavaBean的問題,因此這是個完美的解決方案。實際上給Java加上reflection的主要原因,就是為了支持JavaBean(雖然也是為了支持"對象的序列化(object serializaiton)"和"遠程方法調用(remote method invocation)"。所以也許你會想設計應用程序構建工具的人會逐個地reflect Bean,找出所有的方法,再在里面挑出Bean的屬性和事件。
這么做當然也可以,但是Java為我們提供了一個標準的工具。這不僅使Bean的使用變得更方便了,而且也為我們創建更復雜的Bean指出了一條標準通道。這個工具就是Introspector,其中最重要的方法就是static getBeanInfo( )。當你給這個方法傳一個Class對象時,它會徹底盤查這個類,然后返回一個BeanInfo對象,這樣你就可以通過這個對象找出Bean的屬性,方法和事件了。
通常你根本不用為此操心;絕大多數Bean都是從供應商那里直接買過來的,更何況你也不必知道它在底層都玩了什么花樣。你只要直接把Bean放到表單上,然后配置一下屬性,再寫個程序處理一下事件就可以了。
一個更復雜的Bean
所有的字段都是private的這是Bean的通常做法——也就是說做成"屬性"之后,通常只能用方法來訪問了。
JavaBeans和同步
只要你創建了Bean,你就得保證它能在多線程環境下正常工作,這就是說:
- 只要允許,所有Bean的public方法都必須是synchronized。當然這會影響性能(不過在最新版本的JDK里,這種影響已經明顯下降了)。如果性能下降確實是個問題,那么你可以把那些不致于引起問題的方法的synchronized給去掉,但是要記住,會不會引發問題不是一眼就能看出來的。這種方法首先是要小(就像上面那段程序里的getCircleSize( )),而且/或是"原子操作",就是說這個方法所調用的代碼如此之少,以至于執行期間對象不會被修改了。所以把這種方法做成非synchronized的,也不會對性能產生什么重大影響。所以你應該把Bean的所有public方法都做成synchronized,只有在有絕對必要,而且確實對性能提高有效的時候,才能把synchronized移掉。
- 當你將多播事件發送給一隊對此感興趣的listener時,必須做好準備,listener會隨時加入或從隊列中刪除。
第一個問題很好解決,但第二個問題就要好好想想了。
paintComponent( )也沒有synchronized。決定覆寫方法的時候是不是該加synchronized不像決定自己寫的方法那樣清楚。。這里,好像paintComponent(
)加不加synchronized一樣都能工作。但必須考慮的問題有:
- 這個方法是否會修改對象的"關鍵"變量?變量是否”關鍵"的判斷標準是,它們是否會被其它線程所讀寫。(這里,讀寫實際上都是由synchronized方法來完成的,所以你只要看這一點就可以了)在這段程序里,paintComponent( )沒有修改任何東西。
- 這個方法是否與這種"關鍵"變量的狀態有關?如果有一個synchronized方法修改了這個方法要用到的變量,那么最好是把這個方法也作成synchronized的。基于這點,你或許會發現cSize是由synchronized方法修改的,因此paintComponent( )也應該是synchronized。但是這里你應該問問"如果在執行paintComponent( )的時候,cSize被修改了,最糟糕的情況是什么呢?"如果問題并不嚴重,而且轉瞬即逝的,那么為了防止synchronized所造成的性能下降,你完全可以把paintComponent( )做成非synchronized的。
- 第三個思路是看基類的paintComponent(
)是不是synchronized,答案是"否"。這不是一個萬無一失的判斷標準,只是一個思路。就拿上面那段程序說吧,paintComponent( )里面混了一個通過synchronized方法修改的cSize字段,所以情況也改變了。但是請注意,synchronized不會繼承;也就是說派生類覆寫的基類synchronized方法不會自動成為synchronized方法。
- paint( )和paintComponent( )是那種執行得越快越好的方法。任何能夠提升性能的做法都是值得大力推薦的,所以如果你發覺不得不對這些方法用synchronized,那么很有可能是一個設計失敗的信號。
main( )的測試代碼是根據BangBeanTest修改而得的。為了演示BangBean2的多播功能,它多加了幾個監聽器。
封裝Bean
要想在可視化編程工具里面用JavaBean,必須先把它放入標準的Bean容器里。也就是把所有Bean的class文件以及一份申明"這是一個Bean"的"manifest"文件打成一個JAR的包。manifest文件是一種有一定格式要求的文本文件。對于BangBean,它的manifest文件是這樣的:
Manifest-Version: 1.0
Name: bangbean/BangBean.class
Java-Bean: True
第一行表明manifest的版本,除非Sun今后發通知,否則就是1.0。第二行(空行忽略不計)特別提到了BangBean.class文件,而第三行的意思是"這是y一個Bean"。沒有第三行,應用程序構建工具不會把它看成Bean。
唯一能玩點花樣的地方是,你必須在"Name:"里指明正確的路徑。如果你翻到前面去看BangBean.java,就會發覺它屬于bangbean package(因此必須放到classpath的某個目錄的"bangbean"的子目錄里),而manifest的name也必須包含這個package的信息。此外還必須將manifest文件放到package路徑的根目錄的上一層目錄里,這里就是將manifest放到"bangbean"子目錄的上一層目錄里。然后在存放manifest文件的目錄里打入下面這條jar命令:
jar cfm BangBean.jar BangBean.mf bangbean
這里假定JAR文件的名字是BangBean.jar,而manifest文件的名字是BangBean.mf。
或許你會覺得有些奇怪,"我編譯BangBean.java的時候還生成了一些別的class文件,它們都放到哪里去了?"是的,它們最后都放在bangbean子目錄里,而上面那條jar命令的最后一個參數就是bangbean。當你給jar一個子目錄做參數時,它會將整個子目錄都打進JAR文件里(這里還包括BangBean.java的源代碼——你自己寫Bean的時候大概不會想把源代碼打進包吧)。此外如果你把剛做好的JAR文件解開,就會發現你剛寫的那個manifest已經不在里面了,取而代之的是jar自己生成的(大致根據你寫的),名為MANIFEST.MF的manifest文件,而且它把它放在META-INF子目錄里 (意思是“meta-information”)。如果你打開這個manifest文件,就會發現jar給每個文件加了條簽名的信息,就像這樣:
Digest-Algorithms: SHA MD5
SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0=
MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==
總之,你不必為這些事擔心。你作修改的時候可以只改你自己寫的manifest文件,然后重新運行一遍jar,讓它來創建新的JAR文件。你也可以往JAR文件里加新的Bean,只是要把它們的信息加到manifest里面就行了。
值得注意的是,你應該為每個Bean創建一個子目錄。這是因為當你創建JAR文件的時候,你會把子目錄的名字交給jar,而jar又會把子目錄里的所有東西都放進JAR。所以Frog和BangBean都有它們自己的子目錄。
等你把Bean封裝成JAR文件之后,你就能把它們用到支持Bean的IDE里了。這個步驟會隨開發工具的不同有一些差別,不過Sun在他們的"Bean Builder"里提供了一個免費的JavaBean的測試床(可以到java.sun.com/beans去下載)。要把Bean加入Bean
Builer,只要把JAR文件拷貝到正確的目錄里就行了。
Bean的高級功能
你已經知道做一個Bean有多簡單了,但是它的功能并不僅限于此。JavaBean的架構能讓你很快上手,但是經過擴展,它也可以適應更復雜的情況。這些用途已經超出了本書的范圍,但是我會做一個簡單的介紹。你可以在java.sun.com/beans上找到更多的細節。
屬性是一個能加強的地方。在我們舉的例子里,屬性都是單個的,但是你也可以用一個數組來表示多個屬性。這被稱為索引化的屬性(indexed property)。你只要給出正確的方法(還是要遵循方法的命名規范),Introspector就能找到索引化的屬性,這樣應用程序構建工具就能作出正確的反映了。
屬性可以被綁定,也就是說它們能通過PropertyChangeEvent通知其它對象。而其它對象能根據Bean的變化,修改自己的狀態。
屬性是可以被限制的,也就是說如果其他對象認為屬性的這個變化是不可接受的,那么它們可以否決這個變化。Bean用PropertyChangeEvent通知其他對象,而其他對象則用PropertyVetoException來表示反對,并且命令它將屬性的值恢復到原來的狀態。
你也可以修改Bean在設計時的表示方式:
- 你可以為Bean提供自定義的屬性清單。當用戶選擇其它Bean的時候,構建工具會提供普通屬性清單,但是當他們選用你的Bean時,它會提供你定義的清單。
- 你可以為屬性創建一個自定義的編輯器,這樣雖然構建工具用的是普通的屬性清單,但當用戶要編輯這個特殊屬性時,編輯器就會自動啟動了。
- 你可以為Bean提供一個自定義的BeanInfo類,它返回的信息,可以同Introspector默認提供的BeanInfo不同。
- 還可以把所有FeatureDescriptor的"專家(expert)"模式打開,看看基本功能和高級功能有什么區別。
總結
這一章只是想跟你介紹一下Swing的強大功能然后領你入門,這樣當你知道相對而言Swing有多簡單之后,你就能自己去探路了。你看到的這些已經能大致滿足UI設計之需了。但是Swing不止于此;它的目標是要成為一種功能齊全的UI設計工具。只要你能想到,它都有辦法能作到。
如果你在這里找不到你想要的,那么研究一下Sun的JDK文檔吧,或者去搜Web,如果還不行,就去找一本Swing的專著。
PS:
TIJ
的學習暫時就告與段落了,因為3th目前網上翻譯只到第14章,第三次看這本書,算是比較明白了。但是從第8章開始,很多地方還是有一些不太懂的地方。特
別是I/O部分,需要稍后再盡快加強一下。總之,再第三次看了才發現這本書的好,真的實在太經典了,我覺得自己最少應該再看個2~3遍才差不多。
最后,感謝本書的作者Bruce
Eckel的無私奉獻,寫了這么好的一本書而且還免費放到網上。當然還要感謝shhgs,這位熱心的網友的翻譯,對于我這種英盲來說,這種幫助實在是太大
了。作者和譯者的無私奉獻的精神值得我們大家學習,和那些惟利是圖,見錢眼開的人來說。這兩位的風格,人品何止高出一兩倍,實在是小輩的榜樣和偶像,真的
是萬分的敬仰之情難于言表。
2005年04月06日 5:08 AM