Skip to content
This repository has been archived by the owner on Aug 25, 2019. It is now read-only.

Latest commit

 

History

History
5282 lines (4592 loc) · 261 KB

第13章.md

File metadata and controls

5282 lines (4592 loc) · 261 KB

第13章 創建窗口和程序片

在Java 1.0中,圖形用戶接口(GUI)庫最初的設計目標是讓程序員構建一個通用的GUI,使其在所有平臺上都能正常顯示。

但遺憾的是,這個目標並未達到。事實上,Java 1.0版的“抽象Windows工具包”(AWT)產生的是在各系統看來都同樣欠佳的圖形用戶接口。除此之外,它還限制我們只能使用四種字體,並且不能訪問操作系統中現有的高級GUI元素。同時,Jave1.0版的AWT編程模型也不是面向對象的,極不成熟。這類情況在Java1.1版的AWT事件模型中得到了很好的改進,例如:更加清晰、面向對象的編程、遵循Java Beans的範例,以及一個可輕鬆創建可視編程環境的編程組件模型。Java1.2為老的Java 1.0 AWT添加了Java基類(AWT),這是一個被稱為“Swing”的GUI的一部分。豐富的、易於使用和理解的Java Beans能經過拖放操作(像手工編程一樣的好),創建出能使程序員滿意的GUI。軟件業的“3次修訂版”規則看來對於程序設計語言也是成立的(一個產品除非經過第3次修訂,否則不會盡如人意)。

Java的主要設計目的之一是建立程序片,也就是建立運行在WEB 瀏覽器上的小應用程序。由於它們必須是安全的,所以程序片在運行時必須加以限制。無論怎樣,它們都是支持客戶端編程的強有力的工具,一個重要的應用便是在Web上。

在一個程序片中編程會受到很多的限制,我們一般說它“在沙箱內”,這是由於Java運行時一直會有某個東西——即Java運行期安全系統——在監視著我們。Jave 1.1為程序片提供了數字簽名,所以可選出能信賴的程序片去訪問主機。不過,我們也能跳出沙箱的限制寫出可靠的程序。在這種情況下,我們可訪問操作系統中的其他功能。在這本書中我們自始至終編寫的都是可靠的程序,但它們成為了沒有圖形組件的控制檯程序。AWT也能用來為可靠的程序建立GUI接口。

在這一章中我們將先學習使用老的AWT工具,我們會與許多支持和使用AWT的代碼程序樣本相遇。儘管這有一些困難,但卻是必須的,因為我們必須用老的AWT來維護和閱讀傳統的Java代碼。有時甚至需要我們編寫AWT代碼去支持不能從Java1.0升級的環境。在本章第二部分,我們將學習Java 1.1版中新的AWT結構並會看到它的事件模型是如此的優秀(如果能掌握的話,那么在編制新的程序時就可使用這最新的工具。最後,我們將學習新的能像類庫一樣加入到Java 1.1版中的JFC/Swing組件,這意味著不需要升級到Java 1.2便能使用這一類庫。

大多數的例程都將展示程序片的建立,這並不僅僅是因為這非常的容易,更因為這是AWT的主要作用。另外,當用AWT創建一個可靠的程序時,我們將看到處理程序的不同之處,以及怎樣創建能在命令行和瀏覽器中運行的程序。

請注意的是這不是為了描述類的所有程序的綜合解釋。這一章將帶領我們從摘要開始。當我們查找更復雜的內容時,請確定我們的信息瀏覽器通過查找類和方法來解決編程中的問題(如果我們正在使用一個開發環境,信息瀏覽器也許是內建的;如果我們使用的是SUN公司的JDK則這時我們要使用WEB瀏覽器並在Java根目錄下面開始)。附錄F列出了用於深入學習庫知識的其他一些參考資料。

13.1 為何要用AWT?

對於本章要學習的“老式”AWT,它最嚴重的缺點就是它無論在面向對象設計方面,還是在GUI開發包設計方面,都有不盡如人意的表現。它使我們回到了程序設計的黑暗年代(換成其他話就是“拙劣的”、“可怕的”、“惡劣的”等等)。必須為執行每一個事件編寫代碼,包括在其他環境中利用“資源”即可輕鬆完成的一些任務。

許多象這樣的問題在Java 1.1裡都得到了緩解或排除,因為:

(1)Java 1.1的新型AWT是一個更好的編程模型,並向更好的庫設計邁出了可喜的一步。而Java Beans則是那個庫的框架。

(2)“GUI構造器”(可視編程環境)將適用於所有開發系統。在我們用圖形化工具將組件置入窗體的時候,Java Beans和新的AWT使GUI構造器能幫我們自動完成代碼。其它組件技術如ActiveX等也將以相同的形式支持。

既然如此,為什麼還要學習使用老的AWT呢?原因很簡單,因為它的存在是個事實。就目前來說,這個事實對我們來說顯得有些不利,它涉及到面向對象庫設計的一個宗旨:一旦我們在庫中公佈一個組件,就再不能去掉它。如去掉它,就會損害別人已存在的代碼。另外,當我們學習Java和所有使用老AWT的程序時,會發現有許多原來的代碼使用的都是老式AWT。

AWT必須能與固有操作系統的GUI組件打交通,這意味著它需要執行一個程序片不可能做到的任務。一個不被信任的程序片在操作系統中不能作出任何直接調用,否則它會對用戶的機器做出不恰當的事情。一個不被信任的程序片不能訪問重要的功能。例如,“在屏幕上畫一個窗口”的唯一方法是通過調用擁有特殊接口和安全檢查的標準Java庫。Sun公司的原始模型創建的信任庫將僅僅供給Web瀏覽器中的Java系統信任關係自動授權器使用,自動授權器將控制怎樣進入到庫中去。

但當我們想增加操作系統中訪問新組件的功能時該怎麼辦?等待Sun來決定我們的擴展被合併到標準的Java庫中,但這不一定會解決我們的問題。Java 1.1版中的新模型是“信任代碼”或“簽名代碼”,因此一個特殊服務器將校驗我們下載的、由規定的開發者使用的公共密鑰加密系統的代碼。這樣我們就可知道代碼從何而來,那真的是Bob的代碼,還是由某人偽裝成Bob的代碼。這並不能阻止Bob犯錯誤或作某些惡意的事,但能防止Bob逃避匿名製造計算機病毒的責任。一個數字簽名的程序片——“被信任的程序片”——在Java 1.1版能進入我們的機器並直接控制它,正像一些其它的應用程序從信任關係自動授權機中得到“信任”並安裝在我們的機器上。

這是老AWT的所有特點。老的AWT代碼將一直存在,新的Java編程者在從舊的書本中學習時將會遇到老的AWT代碼。同樣,老的AWT也是值得去學習的,例如在一個只有少量庫的例程設計中。老的AWT所包括的範圍在不考慮深度和枚舉每一個程序和類,取而代之的是給了我們一個老AWT設計的概貌。

13.2 基本程序片

庫通常按照它們的功能來進行組合。一些庫,例如使用過的,便中斷擱置起來。標準的Java庫字符串和向量類就是這樣的一個例子。其他的庫被特殊地設計,例如構建塊去建立其它的庫。庫中的某些類是應用程序的框架,其目的是協助我們構建應用程序,在提供類或類集的情況下產生每個特定應用程序的基本活動狀況。然後,為我們定製活動狀況,必須繼承應用程序類並且廢棄程序的權益。應用程序框架的默認控制結構將在特定的時間調用我們廢棄的程序。應用程序的框架是“分離、改變和中止事件”的好例子,因為它總是努力去嘗試集中在被廢棄的所有特殊程序段。

程序片利用應用程序框架來建立。我們從類中繼承程序片,並且廢棄特定的程序。大多數時間我們必須考慮一些不得不運行的使程序片在WEB頁面上建立和使用的重要方法。這些方法是:

Method

Operation

init( )

Called when the applet is first created to perform first-time initialization of the applet

start( )

Called every time the applet moves into sight on the Web browser to allow the applet to start up its normal operations (especially those that are shut off by stop( )). Also called after init( ).

paint( )

Part of the base class Component (three levels of inheritance up). Called as part of an update( ) to perform special painting on the canvas of an applet.

stop( )

Called every time the applet moves out of sight on the Web browser to allow the applet to shut off expensive operations. Also called right before destroy( ).

destroy( )

Called when the applet is being unloaded from the page to perform final release of resources when the applet is no longer used
方法 作用
init() 程序片第一次被創建,初次運行初始化程序片時調用
start() 每當程序片進入Web瀏覽器中,並且允許程序片啟動它的常規操作時調用(特殊的程序片被stop()關閉);同樣在init()後調用
paint() 基類Component的一部分(繼承結構中上溯三級)。作為update()的一部分調用,以便對程序片的畫布進行特殊的描繪
stop() 每次程序片從Web瀏覽器的視線中離開時調用,使程序片能關閉代價高昂的操作;同樣在調用destroy()前調用
destroy() 程序片不再需要,將它從頁面中卸載時調用,以執行資源的最後清除工作

現在來看一看paint()方法。一旦Component(目前是程序片)決定自己需要更新,就會調用這個方法——可能是由於它再次迴轉屏幕,首次在屏幕上顯示,或者是由於其他窗口臨時覆蓋了你的Web瀏覽器。此時程序片會調用它的update()方法(在基類Component中定義),該方法會恢復一切該恢復的東西,而調用paint()正是這個過程的一部分。沒必要對paint()進行重載處理,但構建一個簡單的程序片無疑是方便的方法,所以我們首先從paint()方法開始。

update()調用paint()時,會向其傳遞指向Graphics對象的一個引用,那個對象代表準備在上面描繪(作圖)的表面。這是非常重要的,因為我們受到項目組件的外觀的限制,因此不能畫到區域外,這可是一件好事,否則我們就會畫到線外去。在程序片的例子中,程序片的外觀就是這界定的區域。

圖形對象同樣有一系列我們可對其進行的操作。這些操作都與在畫布上作圖有關。所以其中的大部分都要涉及圖像、幾何菜狀、圓弧等等的描繪(注意如果有興趣,可在Java文檔中找到更詳細的說明)。有些方法允許我們畫出字符,而其中最常用的就是drawString()。對於它,需指出自己想描繪的String(字符串),並指定它在程序片作圖區域的起點。這個位置用像素表示,所以它在不同的機器上看起來是不同的,但至少是可以移植的。

根據這些信息即可創建一個簡單的程序片:

//: Applet1.java
// Very simple applet
package c13;
import java.awt.*;
import java.applet.*;

public class Applet1 extends Applet {
  public void paint(Graphics g) {
    g.drawString("First applet", 10, 10);
  }
} ///:~

注意這個程序片不需要有一個main()。所有內容都封裝到應用程序框架中;我們將所有啟動代碼都放在init()裡。

必須將這個程序放到一個Web頁中才能運行,而只能在支持Java的Web瀏覽器中才能看到此頁。為了將一個程序片置入Web頁,需要在那個Web頁的代碼中設置一個特殊的標記(註釋①),以指示網頁裝載和運行程序片。這就是applet標記,它在Applet1中的樣子如下:

<applet
code=Applet1
width=200
height=200>
</applet>

①:本書假定讀者已掌握了HTML的基本知識。這些知識不難學習,有許多書籍和網上資源都可以提供幫助。

其中,code值指定了.class文件的名字,程序片就駐留在那個文件中。width和height指定這個程序片的初始尺寸(如前所述,以像素為單位)。還可將另一些東西放入applet標記:用於在因特網上尋找其他.class文件的位置(codebase)、對齊和排列信息(align)、使程序片相互間能夠通信的一個特殊標識符(name)以及用於提供程序片能接收的信息的參數。參數採取下述形式:

<Paramname=標識符 value ="信息">

可根據需要設置任意多個這樣的參數。

在簡單的程序片中,我們要做的唯一事情是按上述形式在Web頁中設置一個程序片標記(applet),令其裝載和運行程序片。

13.2.1 程序片的測試

我們可在不必建立網絡連接的前提下進行一次簡單的測試,方法是啟動我們的Web瀏覽器,然後打開包含了程序片標籤的HTML文件(Sun公司的JDK同樣包括一個稱為“程序片觀察器”的工具,它能挑出html文件的<applet>標記,並運行這個程序片,不必顯示周圍的HTML文本——註釋②)。html文件載入後,瀏覽器會發現程序片的標籤,並查找由code值指定的.class文件。當然,它會先在CLASSPATH(類路徑)中尋找,如果在CLASSPATH下找不到類文件,就在WEB瀏覽器狀態欄給出一個錯誤信息,告知不能找到.class文件。

②;由於程序片觀察器會忽略除APPLET標記之外的任何東西,所以可將那些標記作為註釋置入Java源碼:

// <applet code=MyApplet.class width=200 height=100></applet>

這樣就可直接執行appletviewer MyApplet.java,不必再創建小的HTML文件來完成測試。

若想在Web站點上試驗,還會碰到另一些麻煩。首先,我們必須有一個Web站點,這對大多數人來說都意味著位於遠程地點的一家服務提供商(ISP)。然後必須通過某種途徑將HTML文件和.class文件從自己的站點移至ISP機器上正確的目錄(WWW目錄)。這一般是通過採用“文件傳輸協議”(FTP)的程序來做成的,網上可找到許多這樣的免費程序。所以我們要做的全部事情似乎就是用FTP協議將文件移至ISP的機器,然後用自己的瀏覽器連接網站和HTML文件;假如程序片正確裝載和執行,就表明大功告成。但真是這樣嗎?

但這兒我們可能會受到愚弄。假如Web瀏覽器在服務器上找不到.class文件,就會在你的本地機器上搜尋CLASSPATH。所以程序片或許根本不能從服務器上正確地裝載,但在你看來卻是一切正常的,因為瀏覽器在你的機器上找到了它需要的東西。但在其他人訪問時,他們的瀏覽器就無法找到那些類文件。所以在測試時,必須確定已從自己的機器刪除了相關的.class文件,以確保測試結果的真實。

我自己就遇到過這樣的一個問題。當時是將程序片置入一個package(包)中。上載了HTML文件和程序片後,由於包名的問題,程序片的服務器路徑似乎陷入了混亂。但是,我的瀏覽器在本地類路徑(CLASSPATH)中找到了它。這樣一來,我就成了能夠成功裝載程序片的唯一一個人。後來我花了一些時間才發現原來是package語句有誤。一般地,應該將package語句置於程序片的外部。

13.2.2 一個更圖形化的例子

這個程序不會太令人緊張,所以讓我們試著增加一些有趣的圖形組件。

//: Applet2.java
// Easy graphics
import java.awt.*;
import java.applet.*;

public class Applet2 extends Applet {
  public void paint(Graphics g) {
    g.drawString("Second applet", 10, 15);
    g.draw3DRect(0, 0, 100, 20, true);
  }
} ///:~

這個程序用一個方框將字符串包圍起來。當然,所有數字都是“硬編碼”的(指數字固定於程序內部),並以像素為基礎。所以在一些機器上,框會正好將字符串圍住;而在另一些機器上,也許根本看不見這個框,因為不同機器安裝的字體也會有所區別。

Graphic類而言,可在幫助文檔中找到另一些有趣的內容。大多數涉及圖形的活動都是很有趣的,所有我將更多的試驗留給讀者自己去進行。

13.2.3 框架方法的演示

觀看框架方法的實際運作是相當有趣的(這個例子只使用init()start()stop(),因為paint()destroy()非常簡單,很容易就能掌握)。下面的程序片將跟蹤這些方法調用的次數,並用paint()將其顯示出來:

//: Applet3.java
// Shows init(), start() and stop() activities
import java.awt.*;
import java.applet.*;

public class Applet3 extends Applet {
  String s;
  int inits = 0;
  int starts = 0;
  int stops = 0;
  public void init() { inits++; }
  public void start() { starts++; }
  public void stop() { stops++; }
  public void paint(Graphics g) {
    s = "inits: " + inits +
      ", starts: " + starts +
      ", stops: " + stops;
    g.drawString(s, 10, 10);
  }
} ///:~

正常情況下,當我們重載一個方法時,需檢查自己是否需要調用方法的基類版本,這是十分重要的。例如,使用init()時可能需要調用super.init()。然而,Applet文檔特別指出init()start()stop()Applet中沒有用處,所以這裡不需要調用它們。

試驗這個程序片時,會發現假如最小化WEB瀏覽器,或者用另一個窗口將其覆蓋,那麼就不能再調用stop()start()(這一行為會隨著不同的實現方案變化;可考慮將Web瀏覽器的行為同程序片觀察器的行為對照一下)。調用唯一發生的場合是在我們轉移到一個不同的Web頁,然後返回包含了程序片的那個頁時。

13.3 製作按鈕

製作一個按鈕非常簡單:只需要調用Button構造器,並指定想在按鈕上出現的標籤就行了(如果不想要標籤,亦可使用默認構造器,但那種情況極少出現)。可參照後面的程序為按鈕創建一個引用,以便以後能夠引用它。

Button是一個組件,象它自己的小窗口一樣,會在更新時得以重繪。這意味著我們不必明確描繪一個按鈕或者其他任意種類的控件;只需將它們納入窗體,以後的描繪工作會由它們自行負責。所以為了將一個按鈕置入窗體,需要重載init()方法,而不是重載paint()

//: Button1.java
// Putting buttons on an applet
import java.awt.*;
import java.applet.*;

public class Button1 extends Applet {
  Button
    b1 = new Button("Button 1"),
    b2 = new Button("Button 2");
  public void init() {
    add(b1);
    add(b2);
  }
} ///:~

但這還不足以創建Button(或其他任何控件)。必須同時調用Applet add()方法,令按鈕放置在程序片的窗體中。這看起來似乎比實際簡單得多,因為對add()的調用實際會(間接地)決定將控件放在窗體的什麼地方。對窗體佈局的控件馬上就要講到。

13.4 捕獲事件

大家可注意到假如編譯和運行上面的程序片,按下按鈕後不會發生任何事情。必須進入程序片內部,編寫用於決定要發生什麼事情的代碼。對於由事件驅動的程序設計,它的基本目標就是用代碼捕獲發生的事件,並由代碼對那些事件作出響應。事實上,GUI的大部分內容都是圍繞這種事件驅動的程序設計展開的。

經過本書前面的學習,大家應該有了面向對象程序設計的一些基礎,此時可能會想到應當有一些面向對象的方法來專門控制事件。例如,也許不得不繼承每個按鈕,並重載一些“按鈕按下”方法(儘管這顯得非常麻煩有有限)。大家也可能認為存在一些主控“事件”類,其中為希望響應的每個事件都包含了一個方法。

在對象以前,事件控制的典型方式是switch語句。每個事件都對應一個獨一無二的整數編號;而且在主事件控制方法中,需要專門為那個值寫一個switch

Java 1.0的AWT沒有采用任何面向對象的手段。此外,它也沒有使用switch語句,沒有打算依靠那些分配給事件的數字。相反,我們必須創建if語句的一個嵌套系列。通過if語句,我們需要嘗試做的事情是偵測到作為事件“目標”的對象。換言之,那是我們關心的全部內容——假如某個按鈕是一個事件的目標,那麼它肯定是一次鼠標點擊,並要基於那個假設繼續下去。但是,事件裡也可能包含了其他信息。例如,假如想調查一次鼠標點擊的像素位置,以便畫一條引向那個位置的線,那麼Event對象裡就會包含那個位置的信息(也要注意Java 1.0的組件只能產生有限種類的事件,而Java 1.1和Swing/JFC組件則可產生完整的一系列事件)。

Java 1.0版的AWT方法串聯的條件語句中存在action()方法的調用。雖然整個Java 1.0版的事件模型不兼容Java 1.1版,但它在還不支持Java1.1版的機器和運行簡單的程序片的系統中更廣泛地使用,忠告您使用它會變得非常的舒適,包括對下面使用的action()程序方法而言。

action()擁有兩個參數:第一個是事件的類型,包括所有的觸發調用action()的事件的有關信息。例如鼠標單擊、普通按鍵按下或釋放、特殊按鍵按下或釋放、鼠標移動或者拖動、事件組件得到或丟失焦點,等等。第二個參數通常是我們忽略的事件目標。第二個參數封裝在事件目標中,所以它像一個參數一樣的冗長。

需調用action()時情況非常有限:將控件置入窗體時,一些類型的控件(按鈕、複選框、下拉列表單、菜單)會發生一種“標準行動”,從而隨相應的Event對象發起對action()的調用。比如對按鈕來說,一旦按鈕被按下,而且沒有再多按一次,就會調用它的action()方法。這種行為通常正是我們所希望的,因為這正是我們對一個按鈕正常觀感。但正如本章後面要講到的那樣,還可通過handleEvent()方法來處理其他許多類型的事件。

前面的例程可進行一些擴展,以便象下面這樣控制按鈕的點擊:

//: Button2.java
// Capturing button presses
import java.awt.*;
import java.applet.*;

public class Button2 extends Applet {
  Button
    b1 = new Button("Button 1"),
    b2 = new Button("Button 2");
  public void init() {
    add(b1);
    add(b2);
  }
  public boolean action(Event evt, Object arg) {
    if(evt.target.equals(b1))
      getAppletContext().showStatus("Button 1");
    else if(evt.target.equals(b2))
      getAppletContext().showStatus("Button 2");
    // Let the base class handle it:
    else
      return super.action(evt, arg);
    return true; // We've handled it here
  }
} ///:~

為了解目標是什麼,需要向Event對象詢問它的target(目標)成員是什麼,然後用equals()方法檢查它是否與自己感興趣的目標對象引用相符。為所有感興趣的對象寫好引用後,必須在末尾的else語句中調用super.action(evt, arg)方法。我們在第7章已經說過(有關多態性的那一章),此時調用的是我們重載過的方法,而非它的基類版本。然而,基類版本也針對我們不感興趣的所有情況提供了相應的控制代碼。除非明確進行,否則它們是不會得到調用的。返回值指出我們是否已經處理了它,所以假如確實與一個事件相符,就應返回true;否則就返回由基類event()返回的東西。

對這個例子來說,最簡單的行動就是打印出到底是什麼按鈕被按下。一些系統允許你彈出一個小消息窗口,但Java程序片卻防礙窗口的彈出。不過我們可以用調用Applet方法的getAppletContext()來訪問瀏覽器,然後用showStatus()在瀏覽器窗口底部的狀態欄上顯示一條信息(註釋③)。還可用同樣的方法打印出對事件的一段完整說明文字,方法是調用getAppletConext().showStatus(evt + "")。空字符串會強制編譯器將evt轉換成一個字符串。這些報告對於測試和調試特別有用,因為瀏覽器可能會覆蓋我們的消息。

③:ShowStatus()也屬於Applet的一個方法,所以可直接調用它,不必調用getAppletContext()

儘管看起來似乎很奇怪,但我們確實也能通過event()中的第二個參數將一個事件與按鈕上的文字相配。採用這種方法,上面的例子就變成了:

//: Button3.java
// Matching events on button text
import java.awt.*;
import java.applet.*;

public class Button3 extends Applet {
  Button
    b1 = new Button("Button 1"),
    b2 = new Button("Button 2");
  public void init() {
    add(b1);
    add(b2);
  }
  public boolean action (Event evt, Object arg) {
    if(arg.equals("Button 1"))
      getAppletContext().showStatus("Button 1");
    else if(arg.equals("Button 2"))
      getAppletContext().showStatus("Button 2");
    // Let the base class handle it:
    else
      return super.action(evt, arg);
    return true; // We've handled it here
  }
} ///:~

很難確切知道equals()方法在這兒要做什麼。這種方法有一個很大的問題,就是開始使用這個新技術的Java程序員至少需要花費一個受挫折的時期來在比較按鈕上的文字時發現他們要麼大寫了要麼寫錯了(我就有這種經驗)。同樣,如果我們改變了按鈕上的文字,程序代碼將不再工作(但我們不會得到任何編譯時和運行時的信息)。所以如果可能,我們就得避免使用這種方法。

13.5 文本字段

“文本字段”是允許用戶輸入和編輯文字的一種線性區域。文本字段從文本組件那裡繼承了讓我們選擇文字、讓我們像得到字符串一樣得到選擇的文字,得到或設置文字,設置文本字段是否可編輯以及連同我們從在線參考書中找到的相關方法。下面的例子將證明文本字段的其它功能;我們能注意到方法名是顯而易見的:

//: TextField1.java
// Using the text field control
import java.awt.*;
import java.applet.*;

public class TextField1 extends Applet {
  Button
    b1 = new Button("Get Text"),
    b2 = new Button("Set Text");
  TextField
    t = new TextField("Starting text", 30);
  String s = new String();
  public void init() {
    add(b1);
    add(b2);
    add(t);
  }
  public boolean action (Event evt, Object arg) {
    if(evt.target.equals(b1)) {
      getAppletContext().showStatus(t.getText());
      s = t.getSelectedText();
      if(s.length() == 0) s = t.getText();
      t.setEditable(true);
    }
    else if(evt.target.equals(b2)) {
      t.setText("Inserted by Button 2: " + s);
      t.setEditable(false);
    }
    // Let the base class handle it:
    else
      return super.action(evt, arg);
    return true; // We've handled it here
  }
} ///:~

有幾種方法均可構建一個文本字段;其中之一是提供一個初始字符串,並設置字符域的大小。

按下按鈕1 是得到我們用鼠標選擇的文字就是得到字段內所有的文字並轉換成字符串S。它也允許字段被編輯。按下按鈕2 放一條信息和字符串sText fields,並且阻止字段被編輯(儘管我們能夠一直選擇文字)。文字的可編輯性是通過setEditable()的真假值來控制的。

13.6 文本區域

“文本區域”很像文字字段,只是它擁有更多的行以及一些引人注目的更多的功能。另外你能在給定位置對一個文本字段追加、插入或者修改文字。這看起來對文本字段有用的功能相當不錯,所以設法發現它設計的特性會產生一些困惑。我們可以認為如果我們處處需要“文本區域”的功能,那麼可以簡單地使用一個線型文字區域在我們將另外使用文本字段的地方。在Java 1.0版中,當它們不是固定的時候我們也得到了一個文本區域的垂直和水平方向的滾動條。在Java 1.1版中,對高級構造器的修改允許我們選擇哪個滾動條是當前的。下面的例子演示的僅僅是在Java1.0版的狀況下滾動條一直打開。在下一章裡我們將看到一個證明Java 1.1版中的文字區域的例程。

//: TextArea1.java
// Using the text area control
import java.awt.*;
import java.applet.*;

public class TextArea1 extends Applet {
  Button b1 = new Button("Text Area 1");
  Button b2 = new Button("Text Area 2");
  Button b3 = new Button("Replace Text");
  Button b4 = new Button("Insert Text");
  TextArea t1 = new TextArea("t1", 1, 30);
  TextArea t2 = new TextArea("t2", 4, 30);
  public void init() {
    add(b1);
    add(t1);
    add(b2);
    add(t2);
    add(b3);
    add(b4);
  }
  public boolean action (Event evt, Object arg) {
    if(evt.target.equals(b1))
      getAppletContext().showStatus(t1.getText());
    else if(evt.target.equals(b2)) {
      t2.setText("Inserted by Button 2");
      t2.appendText(": " + t1.getText());
      getAppletContext().showStatus(t2.getText());
    }
    else if(evt.target.equals(b3)) {
      String s = " Replacement ";
      t2.replaceText(s, 3, 3 + s.length());
    }
    else if(evt.target.equals(b4))
      t2.insertText(" Inserted ", 10);
    // Let the base class handle it:
    else
      return super.action(evt, arg);
    return true; // We've handled it here
  }
} ///:~

程序中有幾個不同的“文本區域”構造器,這其中的一個在此處顯示了一個初始字符串和行號和列號。不同的按鈕顯示得到、追加、修改和插入文字。

13.7 標籤

標籤準確地運作:安放一個標籤到窗體上。這對沒有標籤的TextFieldsText areas 來說非常的重要,如果我們簡單地想安放文字的信息在窗體上也能同樣的使用。我們能像本章中第一個例程中演示的那樣,使用drawString()裡邊的paint()在確定的位置去安置一個文字。當我們使用的標籤允許我們通過佈局管理加入其它的文字組件。(在這章的後面我們將進入討論。)

使用構造器我們能創建一條包括初始化文字的標籤(這是我們典型的作法),一個標籤包括一行CENTER(中間)、LEFT(左)和RIGHT(右)(靜態的結果取整定義在類標籤裡)。如果我們忘記了可以用getText()getalignment()讀取值,我們同樣可以用setText()setAlignment()來改變和調整。下面的例子將演示標籤的特點:

//: Label1.java
// Using labels
import java.awt.*;
import java.applet.*;

public class Label1 extends Applet {
  TextField t1 = new TextField("t1", 10);
  Label labl1 = new Label("TextField t1");
  Label labl2 = new Label("                   ");
  Label labl3 = new Label("                    ",
    Label.RIGHT);
  Button b1 = new Button("Test 1");
  Button b2 = new Button("Test 2");
  public void init() {
    add(labl1); add(t1);
    add(b1); add(labl2);
    add(b2); add(labl3);
  }
  public boolean action (Event evt, Object arg) {
    if(evt.target.equals(b1))
      labl2.setText("Text set into Label");
    else if(evt.target.equals(b2)) {
      if(labl3.getText().trim().length() == 0)
        labl3.setText("labl3");
      if(labl3.getAlignment() == Label.LEFT)
        labl3.setAlignment(Label.CENTER);
      else if(labl3.getAlignment()==Label.CENTER)
        labl3.setAlignment(Label.RIGHT);
      else if(labl3.getAlignment() == Label.RIGHT)
        labl3.setAlignment(Label.LEFT);
    }
    else
      return super.action(evt, arg);
    return true;
  }
} ///:~

首先是標籤的最典型的用途:標記一個文本字段或文本區域。在例程的第二部分,當我們按下test 1按鈕通過setText()將一串空的空格插入到的字段裡。因為空的空格數不等於同樣的字符數(在一個等比例間隔的字庫裡),當插入文字到標籤裡時我們會看到文字將被省略掉。在例子的第三部分保留的空的空格在我們第一次按下test 2會發現標籤是空的(trim()刪除了每個字符串結尾部分的空格)並且在開頭的左列插入了一個短的標籤。在工作的其餘時間中我們按下按鈕進行調整,因此就能看到效果。

我們可能會認為我們可以創建一個空的標籤,然後用setText()安放文字在裡面。然而我們不能在一個空標籤內加入文字-這大概是因為空標籤沒有寬度-所以創建一個沒有文字的空標籤是沒有用處的。在上面的例子裡,blank標籤裡充滿空的空格,所以它足夠容納後面加入的文字。

同樣的,setAlignment()在我們用構造器創建的典型的文字標籤上沒有作用。這個標籤的寬度就是文字的寬度,所以不能對它進行任何的調整。但是,如果我們啟動一個長標籤,然後把它變成短的,我們就可以看到調整的效果。

這些導致事件連同它們最小化的尺寸被擠壓的狀況被程序片使用的默認佈局管理器所發現。有關佈局管理器的部分包含在本章的後面。

13.8 複選框

複選框提供一個製造單一選擇開關的方法;它包括一個小框和一個標籤。典型的複選框有一個小的X(或者它設置的其它類型)或是空的,這依靠項目是否被選擇來決定的。

我們會使用構造器正常地創建一個複選框,使用它的標籤來充當它的參數。如果我們在創建複選框後想讀出或改變它,我們能夠獲取和設置它的狀態,同樣也能獲取和設置它的標籤。注意,複選框的大寫是與其它的控制相矛盾的。

無論何時一個複選框都可以設置和清除一個事件指令,我們可以捕捉同樣的方法做一個按鈕。在下面的例子裡使用一個文字區域枚舉所有被選中的複選框:

//: CheckBox1.java
// Using check boxes
import java.awt.*;
import java.applet.*;

public class CheckBox1 extends Applet {
  TextArea t = new TextArea(6, 20);
  Checkbox cb1 = new Checkbox("Check Box 1");
  Checkbox cb2 = new Checkbox("Check Box 2");
  Checkbox cb3 = new Checkbox("Check Box 3");
  public void init() {
    add(t); add(cb1); add(cb2); add(cb3);
  }
  public boolean action (Event evt, Object arg) {
    if(evt.target.equals(cb1))
      trace("1", cb1.getState());
    else if(evt.target.equals(cb2))
      trace("2", cb2.getState());
    else if(evt.target.equals(cb3))
      trace("3", cb3.getState());
    else
      return super.action(evt, arg);
    return true;
  }
  void trace(String b, boolean state) {
    if(state)
      t.appendText("Box " + b + " Set\n");
    else
      t.appendText("Box " + b + " Cleared\n");
  }
} ///:~

trace()方法將選中的複選框名和當前狀態用appendText()發送到文字區域中去,所以我們看到一個累積的被選中的複選框和它們的狀態的列表。

13.9 單選鈕

單選鈕在GUI程序設計中的概念來自於老式的電子管汽車收音機的機械按鈕:當我們按下一個按鈕時,其它的按鈕就會彈起。因此它允許我們強制從眾多選擇中作出單一選擇。

AWT沒有單獨的描述單選鈕的類;取而代之的是複用複選框。然而將複選框放在單選鈕組中(並且修改它的外形使它看起來不同於一般的複選框)我們必須使用一個特殊的構造器象一個參數一樣的作用在checkboxGroup對象上。(我們同樣能在創建複選框後調用setCheckboxGroup()方法。)

一個複選框組沒有構造器的參數;它存在的唯一理由就是聚集一些複選框到單選鈕組裡。一個複選框對象必須在我們試圖顯示單選鈕組之前將它的狀態設置成true,否則在運行時我們就會得到一個異常。如果我們設置超過一個的單選鈕為true,只有最後的一個能被設置成真。

這裡有個簡單的使用單選鈕的例子。注意我們可以像其它的組件一樣捕捉單選鈕的事件:

//: RadioButton1.java
// Using radio buttons
import java.awt.*;
import java.applet.*;

public class RadioButton1 extends Applet {
  TextField t =
    new TextField("Radio button 2", 30);
  CheckboxGroup g = new CheckboxGroup();
  Checkbox
    cb1 = new Checkbox("one", g, false),
    cb2 = new Checkbox("two", g, true),
    cb3 = new Checkbox("three", g, false);
  public void init() {
    t.setEditable(false);
    add(t);
    add(cb1); add(cb2); add(cb3);
  }
  public boolean action (Event evt, Object arg) {
    if(evt.target.equals(cb1))
      t.setText("Radio button 1");
    else if(evt.target.equals(cb2))
      t.setText("Radio button 2");
    else if(evt.target.equals(cb3))
      t.setText("Radio button 3");
    else
      return super.action(evt, arg);
    return true;
  }
} ///:~

顯示的狀態是一個文字字段在被使用。這個字段被設置為不可編輯的,因為它只是用來顯示數據而不是收集。這演示了一個使用標籤的可取之道。注意字段內的文字是由最早選擇的單選鈕Radio button 2初始化的。

我們可以在窗體中擁有相當多的複選框組。

13.10 下拉列表

下拉列表像一個單選鈕組,它是強制用戶從一組可實現的選擇中選擇一個對象的方法。而且,它是一個實現這點的相當簡潔的方法,也最易改變選擇而不至使用戶感到吃力(我們可以動態地改變單選鈕,但那種方法顯然不方便)。Java的選擇框不像Windows中的組合框可以讓我從列表中選擇或輸入自己的選擇。在一個選擇框中你只能從列表中選擇僅僅一個項目。在下面的例子裡,選擇框從一個確定輸入的數字開始,然後當按下一個按鈕時,新輸入的數字增加到框裡。你將可以看到選擇框的一些有趣的狀態:

//: Choice1.java
// Using drop-down lists
import java.awt.*;
import java.applet.*;

public class Choice1 extends Applet {
  String[] description = { "Ebullient", "Obtuse",
    "Recalcitrant", "Brilliant", "Somnescent",
    "Timorous", "Florid", "Putrescent" };
  TextField t = new TextField(30);
  Choice c = new Choice();
  Button b = new Button("Add items");
  int count = 0;
  public void init() {
    t.setEditable(false);
    for(int i = 0; i < 4; i++)
      c.addItem(description[count++]);
    add(t);
    add(c);
    add(b);
  }
  public boolean action (Event evt, Object arg) {
    if(evt.target.equals(c))
      t.setText("index: " +  c.getSelectedIndex()
        + "   " + (String)arg);
    else if(evt.target.equals(b)) {
      if(count < description.length)
        c.addItem(description[count++]);
    }
    else
      return super.action(evt, arg);
    return true;
  }
} ///:~

文本字字段中顯示的selected index,也就是當前選擇的項目的序列號,在事件中選擇的字符串就像action()的第二個參數的字符串符描述的一樣好。

運行這個程序片時,請注意對Choice框大小的判斷:在windows裡,這個大小是在我們拉下列表時確定的。這意味著如果我們拉下列表,然後增加更多的項目到列表中,這項目將在那,但這個下拉列表不再接受(我們可以通過項目來滾動觀察——註釋④)。然而,如果我們在第一次拉下下拉列表前將所的項目裝入下拉列表,它的大小就會合適。當然,用戶在使用時希望看到整個的列表,所以會在下拉列表的狀態裡對增加項目到選擇框里加以特殊的限定。

④:這一行為顯然是一種錯誤,會Java以後的版本里解決。

13.11 列表框

列表框與選擇框有完全的不同,而不僅僅是當我們在激活選擇框時的顯示不同,列表框固定在屏幕的指定位置不會改變。另外,一個列表框允許多個選擇:如果我們單擊在超過一個的項目上,未選擇的則表現為高亮度,我們可以選擇象我們想要的一樣的多。如果我們想察看項目列表,我們可以調用getSelectedItem()來產生一個被選擇的項目列表。要想從一個組裡刪除一個項目,我們必須再一次的單擊它。列表框,當然這裡有一個問題就是它默認的動作是雙擊而不是單擊。單擊從組中增加或刪除項目,雙擊調用action()。解決這個問題的方法是象下面的程序假設的一樣重新培訓我們的用戶。

//: List1.java
// Using lists with action()
import java.awt.*;
import java.applet.*;

public class List1 extends Applet {
  String[] flavors = { "Chocolate", "Strawberry",
    "Vanilla Fudge Swirl", "Mint Chip",
    "Mocha Almond Fudge", "Rum Raisin",
    "Praline Cream", "Mud Pie" };
  // Show 6 items, allow multiple selection:
  List lst = new List(6, true);
  TextArea t = new TextArea(flavors.length, 30);
  Button b = new Button("test");
  int count = 0;
  public void init() {
    t.setEditable(false);
    for(int i = 0; i < 4; i++)
      lst.addItem(flavors[count++]);
    add(t);
    add(lst);
    add(b);
  }
  public boolean action (Event evt, Object arg) {
    if(evt.target.equals(lst)) {
      t.setText("");
      String[] items = lst.getSelectedItems();
      for(int i = 0; i < items.length; i++)
        t.appendText(items[i] + "\n");
    }
    else if(evt.target.equals(b)) {
      if(count < flavors.length)
        lst.addItem(flavors[count++], 0);
    }
    else
      return super.action(evt, arg);
    return true;
  }
} ///:~

按下按鈕時,按鈕增加項目到列表的頂部(因為addItem()的第二個參數為零)。增加項目到列表框比到選擇框更加的合理,因為用戶期望去滾動一個列表框(因為這個原因,它有內建的滾動條)但用戶並不願意像在前面的例子裡不得不去計算怎樣才能滾動到要要的那個項目。 然而,調用action()的唯一方法就是通過雙擊。如果我們想監視用戶在我們的列表中的所作所為(尤其是單擊),我們必須提供一個可供選擇的方法。

13.11.1 handleEvent()

到目前為止,我們已使用了action(),現有另一種方法handleEvent()可對每一事件進行嘗試。當一個事件發生時,它總是針對單獨事件或發生在單獨的事件對象上。該對象的handleEvent()方法是自動調用的,並且是被handleEvent()創建並傳遞到handleEvent()裡。默認的handleEvent()handleEvent()定義在組件裡,基類的所有控件都在AWT裡)將像我們以前一樣調用action()或其它同樣的方法去指明鼠標的活動、鍵盤活動或者指明移動的焦點。我們將會在本章的後面部分看到。

如果其它的方法-特別是action()-不能滿足我們的需要怎麼辦呢?至於列表框,例如,如果我想捕捉鼠標單擊,但action()只響應雙擊怎麼辦呢?這個解答是重載handleEvent(),畢竟它是從程序片中得到的,因此可以重載任何非確定的方法。當我們為程序片重載handleEvent()時,我們會得到所有的事件在它們發送出去之前,所以我們不能假設“這裡有我的按鈕可做的事件,所以我們可以假設按鈕被按下了”從它被action()設為真值。在handleEvent()中按鈕擁有焦點且某人對它進行分配都是可能的。不論它合理與否,我們可測試這些事件並遵照handleEvent()來進行操作。

為了修改列表樣本,使它會響應鼠標的單擊,在action()中按鈕測試將被重載,但代碼會處理的列表將像下面的例子被移進handleEvent()中去:

//: List2.java
// Using lists with handleEvent()
import java.awt.*;
import java.applet.*;

public class List2 extends Applet {
  String[] flavors = { "Chocolate", "Strawberry",
    "Vanilla Fudge Swirl", "Mint Chip",
    "Mocha Almond Fudge", "Rum Raisin",
    "Praline Cream", "Mud Pie" };
  // Show 6 items, allow multiple selection:
  List lst = new List(6, true);
  TextArea t = new TextArea(flavors.length, 30);
  Button b = new Button("test");
  int count = 0;
  public void init() {
    t.setEditable(false);
    for(int i = 0; i < 4; i++)
      lst.addItem(flavors[count++]);
    add(t);
    add(lst);
    add(b);
  }
  public boolean handleEvent(Event evt) {
    if(evt.id == Event.LIST_SELECT ||
       evt.id == Event.LIST_DESELECT) {
      if(evt.target.equals(lst)) {
        t.setText("");
        String[] items = lst.getSelectedItems();
        for(int i = 0; i < items.length; i++)
          t.appendText(items[i] + "\n");
      }
      else
        return super.handleEvent(evt);
    }
    else
      return super.handleEvent(evt);
    return true;
  }
  public boolean action(Event evt, Object arg) {
    if(evt.target.equals(b)) {
      if(count < flavors.length)
        lst.addItem(flavors[count++], 0);
    }
    else
      return super.action(evt, arg);
    return true;
  }
} ///:~

這個例子同前面的例子相同除了增加了handleEvent()外簡直一模一樣。在程序中做了試驗來驗證是否列表框的選擇和非選擇存在。現在請記住,handleEvent()被程序片所重載,所以它能在窗體中任何存在,並且被其它的列表當成事件來處理。因此我們同樣必須通過試驗來觀察目標。(雖然在這個例子中,程序片中只有一個列表框所以我們能假設所有的列表框事件必須服務於列表框。這是一個不好的習慣,一旦其它的列表框加入,它就會變成程序中的一個缺陷。)如果列表框匹配一個我們感興趣的列表框,像前面的一樣的代碼將按上面的策略來運行。注意handleEvent()的窗體與action()的相同:如果我們處理一個單獨的事件,將返回真值,但如果我們對其它的一些事件不感興趣,通過handleEvent()我們必須返回super.handleEvent()值。這便是程序的核心,如果我們不那樣做,其它的任何一個事件處理代碼也不會被調用。例如,試註解在上面的代碼中返回super.handleEvent(evt)的值。我們將發現action()沒有被調用,當然那不是我們想得到的。對action()handlEvent()而言,最重要的是跟著上面例子中的格式,並且當我們自己不處理事件時一直返回基類的方法版本信息。(在例子中我們將返回真值)。(幸運的是,這些類型的錯誤的僅屬於Java 1.0版,在本章後面將看到的新設計的Java 1.1消除了這些類型的錯誤。)

在windows裡,如果我們按下shift鍵,列表框自動允許我們做多個選擇。這非常的棒,因為它允許用戶做單個或多個的選擇而不是編程期間固定的。我們可能會認為我們變得更加的精明,並且當一個鼠標單擊被evt.shiftdown()產生時如果shift鍵是按下的將執行我們自己的試驗程序。AWT的設計妨礙了我們-我們不得不去了解哪個項目被鼠標點擊時是否按下了shift鍵,所以我們能取消其餘部分所有的選擇並且只選擇那一個。不管怎樣,我們是不可能在Java 1.0版中做出來的。(Java 1.1將所有的鼠標、鍵盤、焦點事件傳送到列表中,所以我們能夠完成它。)

13.12 佈局的控制

在Java裡該方法是安一個組件到一個窗體中去,它不同我們使用過的其它GUI系統。首先,它是全代碼的;沒有控制安放組件的“資源”。其次,該方法的組件被安放到一個被“佈局管理器”控制的窗體中,由“佈局管理器”根據我們add()它們的決定來安放組件。大小,形狀,組件位置與其它系統的佈局管理器顯著的不同。另外,佈局管理器使我們的程序片或應用程序適合窗口的大小,所以,如果窗口的尺寸改變(例如,在HTML頁面的程序片指定的規格),組件的大小,形狀和位置都會改變。

程序片和幀類都是來源於包含和顯示組件的容器。(這個容器也是一個組件,所以它也能響應事件。)在容器中,調用setLayout()方法允許我選擇不同的佈局管理器。

在這節裡我們將探索不同的佈局管理器,並安放按鈕在它們之上。這裡沒有捕捉按鈕的事件,正好可以演示如何佈置這些按鈕。

13.12.1 FlowLayout

到目前為止,所有的程序片都被建立,看起來使用一些不可思議的內部邏輯來佈置它們的組件。那是因為程序使用一個默認的方式:FlowLayout。這個簡單的Flow的組件安裝在窗體中,從左到右,直到頂部的空格全部再移去一行,並繼續循環這些組件。

這裡有一個例子明確地(當然也是多餘地)設置一個程序片的佈局管理器去FlowLayout,然後在窗體中安放按鈕。我們將注意到FlowLayout組件使用它們本來的大小。例如一個按鈕將會變得和它的字符串符一樣的大小。

//: FlowLayout1.java
// Demonstrating the FlowLayout
import java.awt.*;
import java.applet.*;

public class FlowLayout1 extends Applet {
  public void init() {
    setLayout(new FlowLayout());
    for(int i = 0; i < 20; i++)
      add(new Button("Button " + i));
  }
} ///:~

所有組件將在FlowLayout中被壓縮為它們的最小尺寸,所以我們可能會得到一些奇怪的狀態。例如,一個標籤會合適它自已的字符串的尺寸,所以它會右對齊產生一個不變的顯示。

13.12.2 BorderLayout

佈局管理器有四邊和中間區域的概念。當我們增加一些事物到使用BorderLayout的面板上時我們必須使用add()方法將一個字符串對象作為它的第一個參數,並且字符串必須指定(正確的大寫)North(上),South(下),west(左),East(右)或者Center。如果我們拼寫錯誤或沒有大寫,就會得到一個編譯時的錯誤,並且程序片不會像你所期望的那樣運行。幸運的是,我們會很快發現在Java 1.1中有了更多改進。

這是一個簡單的程序例子:

//: BorderLayout1.java
// Demonstrating the BorderLayout
import java.awt.*;
import java.applet.*;

public class BorderLayout1 extends Applet {
  public void init() {
    int i = 0;
    setLayout(new BorderLayout());
    add("North", new Button("Button " + i++));
    add("South", new Button("Button " + i++));
    add("East", new Button("Button " + i++));
    add("West", new Button("Button " + i++));
    add("Center", new Button("Button " + i++));
  }
} ///:~

除了Center的每一個位置,當元素在其它空間內擴大到最大時,我們會把它壓縮到適合空間的最小尺寸。但是,Center擴大後只會佔據中心位置。

BorderLayout是應用程序和對話框的默認佈局管理器。

13.12.3 GridLayout

GridLayout允許我們建立一個組件表。添加那些組件時,它們會按從左到右、從上到下的順序在網格中排列。在構造器裡,需要指定自己希望的行、列數,它們將按正比例展開。

//: GridLayout1.java
// Demonstrating the GridLayout
import java.awt.*;
import java.applet.*;

public class GridLayout1 extends Applet {
  public void init() {
    setLayout(new GridLayout(7,3));
    for(int i = 0; i < 20; i++)
      add(new Button("Button " + i));
  }
} ///:~

在這個例子裡共有21個空位,但卻只有20個按鈕,最後的一個位置作留空處理;注意對GridLayout來說,並不存在什麼“均衡”處理。

13.12.4 CardLayout

CardLayout允許我們在更復雜的擁有真正的文件夾卡片與一條邊相遇的環境裡創建大致相同於“卡片式對話框”的佈局,我們必須壓下一個卡片使不同的對話框帶到前面來。在AWT裡不是這樣的:CardLayout是簡單的空的空格,我們可以自由地把新卡片帶到前面來。(JFC/Swing庫包括卡片式的窗格看起來非常的棒,且可以我們處理所有的細節。)

(1) 聯合佈局(Combining layouts)

下面的例子聯合了更多的佈局類型,在最初只有一個佈局管理器被程序片或應用程序操作看起來相當的困難。這是事實,但如果我們創建更多的面板對象,每個面板都能擁有一個佈局管理器,並且像被集成到程序片或應用程序中一樣使用程序片或應用程序的佈局管理器。這就象下面程序中的一樣給了我們更多的靈活性:

//: CardLayout1.java
// Demonstrating the CardLayout
import java.awt.*;
import java.applet.Applet;

class ButtonPanel extends Panel {
  ButtonPanel(String id) {
    setLayout(new BorderLayout());
    add("Center", new Button(id));
  }
}

public class CardLayout1 extends Applet {
  Button
    first = new Button("First"),
    second = new Button("Second"),
    third = new Button("Third");
  Panel cards = new Panel();
  CardLayout cl = new CardLayout();
  public void init() {
    setLayout(new BorderLayout());
    Panel p = new Panel();
    p.setLayout(new FlowLayout());
    p.add(first);
    p.add(second);
    p.add(third);
    add("North", p);
    cards.setLayout(cl);
    cards.add("First card",
      new ButtonPanel("The first one"));
    cards.add("Second card",
      new ButtonPanel("The second one"));
    cards.add("Third card",
      new ButtonPanel("The third one"));
    add("Center", cards);
  }
  public boolean action(Event evt, Object arg) {
    if (evt.target.equals(first)) {
      cl.first(cards);
    }
    else if (evt.target.equals(second)) {
      cl.first(cards);
      cl.next(cards);
    }
    else if (evt.target.equals(third)) {
      cl.last(cards);
    }
    else
      return super.action(evt, arg);
    return true;
  }
} ///:~

這個例子首先會創建一種新類型的面板:BottonPanel(按鈕面板)。它包括一個單獨的按鈕,安放在BorderLayout的中央,那意味著它將充滿整個的面板。按鈕上的標籤將讓我們知道我們在CardLayout上的那個面板上。

在程序片裡,面板卡片上將存放卡片和佈局管理器CL因為CardLayout必須組成類,因為當我們需要處理卡片時我們需要訪問這些引用。

這個程序片變成使用BorderLayout來取代它的默認FlowLayout,創建面板來容納三個按鈕(使用FlowLayout),並且這個面板安置在程序片末尾的North。卡片面板增加到程序片的Center裡,有效地佔據面板的其餘地方。

當我們增加BottonPanels(或者任何其它我們想要的組件)到卡片面板時,add()方法的第一個參數不是NorthSouth等等。相反的是,它是一個描述卡片的字符串。如果我們想輕擊那張卡片使用字符串,我們就可以使用,雖然這字符串不會顯示在卡片的任何地方。使用的方法不是使用action();代之使用first()next()last()等方法。請查看我們有關其它方法的文件。

在Java中,使用的一些卡片式面板結構十分的重要,因為(我們將在後面看到)在程序片編程中使用的彈出式對話框是十分令人沮喪的。對於Java 1.0版的程序片而言,CardLayout是唯一有效的取得很多不同的“彈出式”的窗體。

13.12.5 GridBagLayout

很早以前,人們相信所有的恆星、行星、太陽及月亮都圍繞地球公轉。這是直觀的觀察。但後來天文學家變得更加的精明,他們開始跟蹤個別星體的移動,它們中的一些似乎有時在軌道上緩慢運行。因為天文學家知道所有的天體都圍繞地球公轉,天文學家花費了大量的時間來討論相關的方程式和理論去解釋天體對象的運行。當我們試圖用GridBagLayout來工作時,我們可以想像自己為一個早期的天文學家。基礎的條例是(公告:有趣的是設計者居然在太陽上(這可能是在天體圖中標錯了位置所致,譯者注))所有的天體都將遵守規則來運行。哥白尼日新說(又一次不顧嘲諷,發現太陽系內的所有的行星圍繞太陽公轉。)是使用網絡圖來判斷佈局,這種方法使得程序員的工作變得簡單。直到這些增加到Java裡,我們忍耐(持續的冷嘲熱諷)西班牙的GridBagLayoutGridBagConstraints狂熱宗教。我們建議廢止GridBagLayout。取代它的是,使用其它的佈局管理器和特殊的在單個程序裡聯合幾個面板使用不同的佈局管理器的技術。我們的程序片看起來不會有什麼不同;至少不足以調整GridBagLayout限制的麻煩。對我而言,通過一個例子來討論它實在是令人頭痛(並且我不鼓勵這種庫設計)。相反,我建議您從閱讀Cornell和Horstmann撰寫的《核心Java》(第二版,Prentice-Hall出版社,1997年)開始。

在這範圍內還有其它的:在JFC/Swing庫裡有一個新的使用Smalltalk的受人歡迎的“Spring and Struts”佈局管理器並且它能顯著地減少GridBagLayout的需要。

13.13 action的替代品

正如早先指出的那樣,action()並不是我們對所有事進行分類後自動為handleEvent()調用的唯一方法。有三個其它的被調用的方法集,如果我們想捕捉某些類型的事件(鍵盤、鼠標和焦點事件),因此我們不得不重載規定的方法。這些方法是定義在基類組件裡,所以他們幾乎在所有我們可能安放在窗體中的組件中都是有用的。然而,我們也注意到這種方法在Java 1.1版中是不被支持的,同樣儘管我們可能注意到繼承代碼利用了這種方法,我們將會使用Java 1.1版的方法來代替(本章後面有詳細介紹)。

組件方法 何時調用
action(Event evt, Object what) 當典型的事件針對組件發生(例如,當按下一個按鈕或下拉列表項目被選中)時調用
keyDown(Event evt, int key) 當按鍵被按下,組件擁有焦點時調用。第二個參數是按下的鍵並且是冗餘的是從evt.key處複製來的
keyup(Event evt, int key) 當按鍵被釋放,組件擁有焦點時調用
lostFocus(Event evt, Object what) 焦點從目標處移開時調用。通常,what是從evt.arg裡冗餘複製的
gotFocus(Event evt, Object what) 焦點移動到目標時調用
mouseDown(Event evt, int x,int y) 一個鼠標按下存在於組件之上,在X,Y座標處時調用
mouseUp(Event evt, int x, int y) 一個鼠標升起存在於組件之上時調用
mouseMove(Event evt, int x, int y) 當鼠標在組件上移動時調用
mouseDrag(Event evt, int x, int y) 鼠標在一次mouseDown事件發生後拖動。所有拖動事件都會報告給內部發生了mouseDown事件的那個組件,直到遇到一次mouseUp為止
mouseEnter(Event evt, int x, int y) 鼠標從前不在組件上方,但目前在
mouseExit(Event evt, int x, int y) 鼠標曾經位於組件上方,但目前不在

當我們處理特殊情況時——一個鼠標事件,例如,它恰好是我們想得到的鼠標事件存在的座標,我們將看到每個程序接收一個事件連同一些我們所需要的信息。有趣的是,當組件的handleEvent()調用這些方法時(典型的事例),附加的參數總是多餘的因為它們包含在事件對象裡。事實上,如果我們觀察component.handleEvent()的源代碼,我們能發現它顯然將增加的參數抽出事件對象(這可能是考慮到在一些語言中無效率的編碼,但請記住Java的焦點是安全的,不必擔心。)試驗對我們表明這些事件事實上在被調用並且作為一個有趣的嘗試是值得創建一個重載每個方法的程序片,(action()的重載在本章的其它地方)當事件發生時顯示它們的相關數據。

這個例子同樣向我們展示了怎樣製造自己的按鈕對象,因為它是作為目標的所有事件權益來使用。我可能會首先(也是必須的)假設製造一個新的按鈕,我們從按鈕處繼承。但它並不能運行。取而代之的是,我們從畫布組件處(一個非常普通組件)繼承,並在其上不使用paint()方法畫出一個按鈕。正如我們所看到的,自從一些代碼混入到畫按鈕中去,按鈕根本就不運行,這實在是太糟糕了。(如果您不相信我,試圖在例子中為畫布組件交換按鈕,請記住調用稱為super的基類構造器。我們會看到按鈕不會被畫出,事件也不會被處理。)

myButton類是明確說明的:它只和一個自動事件(AutoEvent)“父窗口”一起運行(父窗口不是一個基類,它是按鈕創建和存在的窗口。)。通過這個知識,myButton可能進入到父窗口並且處理它的文字字段,必然就能將狀態信息寫入到父窗口的字段裡。當然這是一種非常有限的解決方法,myButton僅能在連結AutoEvent時被使用。這種代碼有時稱為“高度結合”。但是,製造myButton更需要很多的不是為例子(和可能為我們將寫的一些程序片)擔保的努力。再者,請注意下面的代碼使用了Java 1.1版不支持的API。

//: AutoEvent.java
// Alternatives to action()
import java.awt.*;
import java.applet.*;
import java.util.*;

class MyButton extends Canvas {
  AutoEvent parent;
  Color color;
  String label;
  MyButton(AutoEvent parent,
           Color color, String label) {
    this.label = label;
    this.parent = parent;
    this.color = color;
  }
  public void paint(Graphics  g) {
    g.setColor(color);
    int rnd = 30;
    g.fillRoundRect(0, 0, size().width,
                    size().height, rnd, rnd);
    g.setColor(Color.black);
    g.drawRoundRect(0, 0, size().width,
                    size().height, rnd, rnd);
    FontMetrics fm = g.getFontMetrics();
    int width = fm.stringWidth(label);
    int height = fm.getHeight();
    int ascent = fm.getAscent();
    int leading = fm.getLeading();
    int horizMargin = (size().width - width)/2;
    int verMargin = (size().height - height)/2;
    g.setColor(Color.white);
    g.drawString(label, horizMargin,
                 verMargin + ascent + leading);
  }
  public boolean keyDown(Event evt, int key) {
    TextField t =
      (TextField)parent.h.get("keyDown");
    t.setText(evt.toString());
    return true;
  }
  public boolean keyUp(Event evt, int key) {
    TextField t =
      (TextField)parent.h.get("keyUp");
    t.setText(evt.toString());
    return true;
  }
  public boolean lostFocus(Event evt, Object w) {
    TextField t =
      (TextField)parent.h.get("lostFocus");
    t.setText(evt.toString());
    return true;
  }
  public boolean gotFocus(Event evt, Object w) {
    TextField t =
      (TextField)parent.h.get("gotFocus");
    t.setText(evt.toString());
    return true;
  }
  public boolean
  mouseDown(Event evt,int x,int y) {
    TextField t =
      (TextField)parent.h.get("mouseDown");
    t.setText(evt.toString());
    return true;
  }
  public boolean
  mouseDrag(Event evt,int x,int y) {
    TextField t =
      (TextField)parent.h.get("mouseDrag");
    t.setText(evt.toString());
    return true;
  }
  public boolean
  mouseEnter(Event evt,int x,int y) {
    TextField t =
      (TextField)parent.h.get("mouseEnter");
    t.setText(evt.toString());
    return true;
  }
  public boolean
  mouseExit(Event evt,int x,int y) {
    TextField t =
      (TextField)parent.h.get("mouseExit");
    t.setText(evt.toString());
    return true;
  }
  public boolean
  mouseMove(Event evt,int x,int y) {
    TextField t =
      (TextField)parent.h.get("mouseMove");
    t.setText(evt.toString());
    return true;
  }
  public boolean mouseUp(Event evt,int x,int y) {
    TextField t =
      (TextField)parent.h.get("mouseUp");
    t.setText(evt.toString());
    return true;
  }
}

public class AutoEvent extends Applet {
  Hashtable h = new Hashtable();
  String[] event = {
    "keyDown", "keyUp", "lostFocus",
    "gotFocus", "mouseDown", "mouseUp",
    "mouseMove", "mouseDrag", "mouseEnter",
    "mouseExit"
  };
  MyButton
    b1 = new MyButton(this, Color.blue, "test1"),
    b2 = new MyButton(this, Color.red, "test2");
  public void init() {
    setLayout(new GridLayout(event.length+1,2));
    for(int i = 0; i < event.length; i++) {
      TextField t = new TextField();
      t.setEditable(false);
      add(new Label(event[i], Label.CENTER));
      add(t);
      h.put(event[i], t);
    }
    add(b1);
    add(b2);
  }
} ///:~

我們可以看到構造器使用利用參數同名的方法,所以參數被賦值,並且使用this來區分:

this.label = label;

paint()方法由簡單的開始:它用按鈕的顏色填充了一個“圓角矩形”,然後畫了一個黑線圍繞它。請注意size()的使用決定了組件的寬度和長度(當然,是像素)。這之後,paint()看起來非常的複雜,因為有大量的預測去計算出怎樣利用“font metrics”集中按鈕的標籤到按鈕裡。我們能得到一個相當好的關於繼續關注方法調用的主意,它將程序中那些相當平凡的代碼挑出,當我們想集中一個標籤到一些組件裡時,我們正好可以對它進行剪切和粘貼。

您直到注意到AutoEvent類才能正確地理解keyDown(),keyUp()及其它方法的運行。這包含一個Hashtable(譯者注:散列表)去控制字符串來描述關於事件處理的事件和TextField類型。當然,這些能被靜態的創建而不是放入Hashtable但我認為您會同意它是更容易使用和改變的。特別是,如果我們需要在AutoEvent中增加或刪除一個新的事件類型,我們只需要簡單地在事件列隊中增加或刪除一個字符串——所有的工作都自動地完成了。

我們查出在keyDown()keyup()及其它方法中的字符串的位置回到myButton中。這些方法中的任何一個都用父引用試圖回到父窗口。父類是一個AutoEvent,它包含Hashtable hget()方法,當擁有特定的字符串時,將對一個我們知道的TextField對象產生一個引用(因此它被選派到那)。然後事件對象修改顯示在TextField中的字符串陳述。從我們可以真正注意到舉出的例子在我們的程序中運行事件時以來,可以發現這個例子運行起來頗為有趣的。

13.14 程序片的侷限

出於安全緣故,程序片十分受到限制,並且有很多的事我們都不能做。您一般會問:程序片看起來能做什麼,傳聞它又能做什麼:擴展瀏覽器中WEB頁的功能。自從作為一個網上衝浪者,我們從未真正想了解是否一個WEB頁來自友好的或者不友好的站點,我們想要一些可以安全地行動的代碼。所以我們可能會注意到大量的限制:

(1) 一個程序片不能接觸到本地的磁盤。這意味著不能在本地磁盤上寫和讀,我們不想一個程序片通過WEB頁面閱讀和傳送重要的信息。寫是被禁止的,當然,因為那將會引起病毒的侵入。當數字簽名生效時,這些限制會被解除。

(2) 程序片不能擁有菜單。(注意:這是規定在Swing中的)這可能會減少關於安全和關於程序簡化的麻煩。我們可能會接到有關程序片協調利益以作為WEB頁面的一部分的通知;而我們通常不去注意程序片的範圍。這兒沒有幀和標題條從菜單處彈出,出現的幀和標題條是屬於WEB瀏覽器的。也許將來設計能被改變成允許我們將瀏覽器菜單和程序片菜單相結合起來——程序片可以影響它的環境將導致太危及整個系統的安全並使程序片過於的複雜。

(3) 對話框是不被信任的。在Java中,對話框存在一些令人難解的地方。首先,它們不能正確地拒絕程序片,這實在是令人沮喪。如果我們從程序片彈出一個對話框,我們會在對話框上看到一個附上的消息框“不被信任的程序片”。這是因為在理論上,它有可能欺騙用戶去考慮他們在通過WEB同一個老顧客的本地應用程序交易並且讓他們輸入他們的信用卡號。在看到AWT開發的那種GUI後,我們可能會難過地相信任何人都會被那種方法所愚弄。但程序片是一直附著在一個Web頁面上的,並可以在瀏覽器中看到,而對話框沒有這種依附關係,所以理論上是可能的。因此,我們很少會見到一個使用對話框的程序片。

在較新的瀏覽器中,對受到信任的程序片來說,許多限制都被放寬了(受信任程序片由一個信任源認證)。

涉及程序片的開發時,還有另一些問題需要考慮:

  • 程序片不停地從一個適合不同類的單獨的服務器上下載。我們的瀏覽器能夠緩存程序片,但這沒有保證。在Java 1.1版中的一個改進是JAR(Java ARchive)文件,它允許將所有的程序片組件(包括其它的類文件、圖像、聲音)一起打包到一個的能被單個服務器處理下載的壓縮文件。“數字簽字”(能校驗類創建器)可有效地加入每個單獨的JAR文件。
  • 因為安全方面的緣故,我們做某些工作更加困難,例如訪問數據庫和發送電子郵件。另外,安全限制規則使訪問多個主機變得非常的困難,因為每一件事都必須通過WEB服務器路由,形成一個性能瓶頸,並且單一環節的出錯都會導致整個處理的停止。
  • 瀏覽器裡的程序片不會擁有同樣的本地應用程序運行的控件類型。例如,自從用戶可以開關頁面以來,在程序片中不會擁有一個形式上的對話框。當用戶對一個WEB頁面進行改變或退出瀏覽器時,對我們的程序片而言簡直是一場災難——這時沒有辦法保存狀態,所以如果我們在處理和操作中時,信息會被丟失。另外,當我們離開一個WEB頁面時,不同的瀏覽器會對我們的程序片做不同的操作,因此結果本來就是不確定的。

13.14.1 程序片的優點

如果能容忍那些限制,那麼程序片的一些優點也是非常突出的,尤其是在我們構建客戶/服務器應用或者其它網絡應用時:

  • 沒有安裝方面的爭議。程序片擁有真正的平臺獨立性(包括容易地播放聲音文件等能力)所以我們不需要針對不同的平臺修改代碼也不需要任何人根據安裝運行任何的“tweaking”。事實上,安裝每次自動地將WEB頁連同程序片一起,因此安靜、自動地更新。在傳統的客戶端/服務器系統中,建立和安裝一個新版本的客戶端軟件簡直就是一場惡夢。
  • 因為安全的原因創建在核心Java語言和程序片結構中,我們不必擔心壞的代碼而導致毀壞某人的系統。這樣,連同前面的優點,可使用Java(可從JavaScript和VBScript中選擇客戶端的WEB編程工具)為所謂的Intrant(在公司內部使用而不向Internet轉移的企業內部網絡)客戶端/服務器開發應用程序。
  • 由於程序片是自動同HTML集成的,所以我們有一個內建的獨立平臺文件系統去支持程序片。這是一個很有趣的方法,因為我們慣於擁有程序文件的一部分而不是相反的擁有文件系統。

13.15 視窗化應用

出於安全的緣故,我們會看到在程序片我們的行為非常的受到限制。我們真實地感到,程序片是被臨時地加入在WEB瀏覽器中的,因此,它的功能連同它的相關知識,控件都必須加以限制。但是,我們希望Java能製造一個開窗口的程序去運行一些事物,否則寧願安放在一個WEB頁面上,並且也許我們希望它可以運行一些可靠的應用程序,以及誇張的實時便攜性。在這本書前面的章節中我們製造了一些命令行應用程序,但在一些操作環境中(例如:Macintosh)沒有命令行。所以我們有很多的理由去利用Java創建一個設置窗口,非程序片的程序。這當然是一個十分合理的要求。

一個Java設置窗口應用程序可以擁有菜單和對話框(這對一個程序片來說是不可能的和很困難的),可是如果我們使用一個老版本的Java,我們將會犧牲本地操作系統環境的外觀和感受。JFC/Swing庫允許我們製造一個保持原來操作系統環境的外觀和感受的應用程序。如果我們想建立一個設置窗口應用程序,它會合理地運作,同樣,如果我們可以使用最新版本的Java並且集合所有的工具,我們就可以發佈不會使用戶困惑的應用程序。如果因為一些原因,我們被迫使用老版本的Java,請在毀壞以建立重要的設置窗口的應用程序前仔細地考慮。

13.15.1 菜單

直接在程序片中安放一個菜單是不可能的(Java 1.0,Java1.1和Swing庫不允許),因為它們是針對應用程序的。繼續,如果您不相信我並且確定在程序片中可以合理地擁有菜單,那麼您可以去試驗一下。程序片中沒有setMenuBar()方法,而這種方法是附在菜單中的(我們會看到它可以合理地在程序片產生一個幀,並且幀包含菜單)。

有四種不同類型的MenuComponent(菜單組件),所有的菜單組件起源於抽象類:菜單條(我們可以在一個事件幀裡擁有一個菜單條),菜單去支配一個單獨的下拉菜單或者子菜單、菜單項來說明菜單裡一個單個的元素,以及起源於MenuItem,產生檢查標誌(checkmark)去顯示菜單項是否被選擇的CheckBoxMenuItem

不同的系統使用不同的資源,對Java和AWT而言,我們必須在源代碼中手工彙編所有的菜單。

//: Menu1.java
// Menus work only with Frames.
// Shows submenus, checkbox menu items
// and swapping menus.
import java.awt.*;

public class Menu1 extends Frame {
  String[] flavors = { "Chocolate", "Strawberry",
    "Vanilla Fudge Swirl", "Mint Chip",
    "Mocha Almond Fudge", "Rum Raisin",
    "Praline Cream", "Mud Pie" };
  TextField t = new TextField("No flavor", 30);
  MenuBar mb1 = new MenuBar();
  Menu f = new Menu("File");
  Menu m = new Menu("Flavors");
  Menu s = new Menu("Safety");
  // Alternative approach:
  CheckboxMenuItem[] safety = {
    new CheckboxMenuItem("Guard"),
    new CheckboxMenuItem("Hide")
  };
  MenuItem[] file = {
    new MenuItem("Open"),
    new MenuItem("Exit")
  };
  // A second menu bar to swap to:
  MenuBar mb2 = new MenuBar();
  Menu fooBar = new Menu("fooBar");
  MenuItem[] other = {
    new MenuItem("Foo"),
    new MenuItem("Bar"),
    new MenuItem("Baz"),
  };
  Button b = new Button("Swap Menus");
  public Menu1() {
    for(int i = 0; i < flavors.length; i++) {
      m.add(new MenuItem(flavors[i]));
      // Add separators at intervals:
      if((i+1) % 3 == 0)
        m.addSeparator();
    }
    for(int i = 0; i < safety.length; i++)
      s.add(safety[i]);
    f.add(s);
    for(int i = 0; i < file.length; i++)
      f.add(file[i]);
    mb1.add(f);
    mb1.add(m);
    setMenuBar(mb1);
    t.setEditable(false);
    add("Center", t);
    // Set up the system for swapping menus:
    add("North", b);
    for(int i = 0; i < other.length; i++)
      fooBar.add(other[i]);
    mb2.add(fooBar);
  }
  public boolean handleEvent(Event evt) {
    if(evt.id == Event.WINDOW_DESTROY)
      System.exit(0);
    else
      return super.handleEvent(evt);
    return true;
  }
  public boolean action(Event evt, Object arg) {
    if(evt.target.equals(b)) {
      MenuBar m = getMenuBar();
      if(m == mb1) setMenuBar(mb2);
      else if (m == mb2) setMenuBar(mb1);
    }
    else if(evt.target instanceof MenuItem) {
      if(arg.equals("Open")) {
        String s = t.getText();
        boolean chosen = false;
        for(int i = 0; i < flavors.length; i++)
          if(s.equals(flavors[i])) chosen = true;
        if(!chosen)
          t.setText("Choose a flavor first!");
        else
          t.setText("Opening "+ s +". Mmm, mm!");
      }
      else if(evt.target.equals(file[1]))
        System.exit(0);
      // CheckboxMenuItems cannot use String
      // matching; you must match the target:
      else if(evt.target.equals(safety[0]))
        t.setText("Guard the Ice Cream! " +
          "Guarding is " + safety[0].getState());
      else if(evt.target.equals(safety[1]))
        t.setText("Hide the Ice Cream! " +
          "Is it cold? " + safety[1].getState());
      else
        t.setText(arg.toString());
    }
    else
      return super.action(evt, arg);
    return true;
  }
  public static void main(String[] args) {
    Menu1 f = new Menu1();
    f.resize(300,200);
    f.show();
  }
} ///:~

在這個程序中,我避免了為每個菜單編寫典型的冗長的add()列表調用,因為那看起來像許多的無用的標誌。取而代之的是,我安放菜單項到數組中,然後在一個for的循環中通過每個數組調用add()簡單地跳過。這樣的話,增加和減少菜單項變得沒那麼討厭了。

作為一個可選擇的方法(我發現這很難令我滿意,因為它需要更多的分配)CheckboxMenuItems在數組的引用中被創建是被稱為安全創建;這對數組文件和其它的文件而言是真正的安全。

程序中創建了不是一個而是二個的菜單條來證明菜單條在程序運行時能被交換激活。我們可以看到菜單條怎樣組成菜單,每個菜單怎樣組成菜單項(MenuItems),chenkboxMenuItems或者其它的菜單(產生子菜單)。當菜單組合後,可以用setMenuBar()方法安裝到現在的程序中。值得注意的是當按鈕被壓下時,它將檢查當前的菜單安裝使用getMenuBar(),然後安放其它的菜單條在它的位置上。

當測試是open(即開始)時,注意拼寫和大寫,如果開始時沒有對象,Java發出no error(沒有錯誤)的信號。這種字符串比較是一個明顯的程序設計錯誤源。

校驗和非校驗的菜單項自動地運行,與之相關的CheckBoxMenuItems著實令人吃驚,這是因為一些原因它們不允許字符串匹配。(這似乎是自相矛盾的,儘管字符串匹配並不是一種很好的辦法。)因此,我們可以匹配一個目標對象而不是它們的標籤。當演示時,getState()方法用來顯示狀態。我們同樣可以用setState()改變CheckboxMenuItem的狀態。

我們可能會認為一個菜單可以合理地置入超過一個的菜單條中。這看似合理,因為所有我們忽略的菜單條的add()方法都是一個引用。然而,如果我們試圖這樣做,這個結果將會變得非常的彆扭,而遠非我們所希望得到的結果。(很難知道這是一個編程中的錯誤或者說是他們試圖使它以這種方法去運行所產生的。)這個例子同樣向我們展示了為什麼我們需要建立一個應用程序以替代程序片。(這是因為應用程序能支持菜單,而程序片是不能直接使用菜單的。)我們從幀處繼承代替從程序片處繼承。另外,我們為類建一個構造器以取代init()安裝事件。最後,我們創建一個main()方法並且在我們建的新型對象裡,調整它的大小,然後調用show()。它與程序片只在很小的地方有不同之處,然而這時它已經是一個獨立的設置窗口應用程序並且我們可以使用菜單。

13.15.2 對話框

對話框是一個從其它窗口彈出的窗口。它的目的是處理一些特殊的爭議和它們的細節而不使原來的窗口陷入混亂之中。對話框大量在設置窗口的編程環境中使用,但就像前面提到的一樣,鮮于在程序片中使用。

我們需要從對話類處繼承以創建其它類型的窗口、像幀一樣的對話框。和窗框不同,對話框不能擁有菜單條也不能改變光標,但除此之外它們十分的相似。一個對話框擁有佈局管理器(默認的是BorderLayout佈局管理器)和重載action()等等,或用handleEvent()去處理事件。我們會注意到handleEvent()的一個重要差異:當WINDOW_DESTORY事件發生時,我們並不希望關閉正在運行的應用程序!

相反,我們可以使用對話窗口通過調用dispace()釋放資源。在下面的例子中,對話框是由定義在那兒作為類的ToeButton的特殊按鈕組成的網格構成的(利用GridLayout佈局管理器)。ToeButton按鈕圍繞它自已畫了一個幀,並且依賴它的狀態:在空的中的X或者O。它從空白開始,然後依靠使用者的選擇,轉換成XO。但是,當我們單擊在按鈕上時,它會在XO之間來回交換。(這產生了一種類似填字遊戲的感覺,當然比它更令人討厭。)另外,這個對話框可以被設置為在主應用程序窗口中為很多的行和列變更號碼。

//: ToeTest.java
// Demonstration of dialog boxes
// and creating your own components
import java.awt.*;

class ToeButton extends Canvas {
  int state = ToeDialog.BLANK;
  ToeDialog parent;
  ToeButton(ToeDialog parent) {
    this.parent = parent;
  }
  public void paint(Graphics  g) {
    int x1 = 0;
    int y1 = 0;
    int x2 = size().width - 1;
    int y2 = size().height - 1;
    g.drawRect(x1, y1, x2, y2);
    x1 = x2/4;
    y1 = y2/4;
    int wide = x2/2;
    int high = y2/2;
    if(state == ToeDialog.XX) {
      g.drawLine(x1, y1, x1 + wide, y1 + high);
      g.drawLine(x1, y1 + high, x1 + wide, y1);
    }
    if(state == ToeDialog.OO) {
      g.drawOval(x1, y1, x1+wide/2, y1+high/2);
    }
  }
  public boolean
  mouseDown(Event evt, int x, int y) {
    if(state == ToeDialog.BLANK) {
      state = parent.turn;
      parent.turn= (parent.turn == ToeDialog.XX ?
        ToeDialog.OO : ToeDialog.XX);
    }
    else
      state = (state == ToeDialog.XX ?
        ToeDialog.OO : ToeDialog.XX);
    repaint();
    return true;
  }
}

class ToeDialog extends Dialog {
  // w = number of cells wide
  // h = number of cells high
  static final int BLANK = 0;
  static final int XX = 1;
  static final int OO = 2;
  int turn = XX; // Start with x's turn
  public ToeDialog(Frame parent, int w, int h) {
    super(parent, "The game itself", false);
    setLayout(new GridLayout(w, h));
    for(int i = 0; i < w * h; i++)
      add(new ToeButton(this));
    resize(w * 50, h * 50);
  }
  public boolean handleEvent(Event evt) {
    if(evt.id == Event.WINDOW_DESTROY)
      dispose();
    else
      return super.handleEvent(evt);
    return true;
  }
}

public class ToeTest extends Frame {
  TextField rows = new TextField("3");
  TextField cols = new TextField("3");
  public ToeTest() {
    setTitle("Toe Test");
    Panel p = new Panel();
    p.setLayout(new GridLayout(2,2));
    p.add(new Label("Rows", Label.CENTER));
    p.add(rows);
    p.add(new Label("Columns", Label.CENTER));
    p.add(cols);
    add("North", p);
    add("South", new Button("go"));
  }
  public boolean handleEvent(Event evt) {
    if(evt.id == Event.WINDOW_DESTROY)
      System.exit(0);
    else
      return super.handleEvent(evt);
    return true;
  }
  public boolean action(Event evt, Object arg) {
    if(arg.equals("go")) {
      Dialog d = new ToeDialog(
        this,
        Integer.parseInt(rows.getText()),
        Integer.parseInt(cols.getText()));
      d.show();
    }
    else
      return super.action(evt, arg);
    return true;
  }
  public static void main(String[] args) {
    Frame f = new ToeTest();
    f.resize(200,100);
    f.show();
  }
} ///:~

ToeButton類保留了一個引用到它ToeDialog型的父類中。正如前面所述,ToeButtonToeDialog高度的結合因為一個ToeButton只能被一個ToeDialog所使用,但它卻解決了一系列的問題,事實上這實在不是一個糟糕的解決方案因為沒有另外的可以記錄用戶選擇的對話類。當然我們可以使用其它的製造ToeDialog.turnToeButton的靜態的一部分)方法。這種方法消除了它們的緊密聯繫,但卻阻止了我們一次擁有多個ToeDialog(無論如何,至少有一個正常地運行)。

paint()是一種與圖形有關的方法:它圍繞按鈕畫出矩形並畫出XO。這完全是冗長的計算,但卻十分的直觀。

一個鼠標單擊被重載的mouseDown()方法所俘獲,最要緊的是檢查是否有事件寫在按鈕上。如果沒有,父窗口會被詢問以找出誰選擇了它並用來確定按鈕的狀態。值得注意的是按鈕隨後交回到父類中並且改變它的選擇。如果按鈕已經顯示這為XO,那麼它們會被改變狀態。我們能注意到本書第三章中描述的在這些計算中方便的使用的三個一組的If-else。當一個按鈕的狀態改變後,按鈕會被重畫。

ToeDialog的構造器十分的簡單:它像我們所需要的一樣增加一些按鈕到GridLayout佈局管理器中,然後調整每個按鈕每邊大小為50個像素(如果我們不調整窗口,那麼它就不會顯示出來)。注意handleEvent()正好為WINDOW_DESTROY調用dispose(),因此整個應用程序不會被關閉。

ToeTest設置整個應用程序以創建TextField(為輸入按鈕網格的行和列)和go按鈕。我們會領會action()在這個程序中使用不太令人滿意的“字符串匹配”技術來測試按鈕的按下(請確定我們拼寫和大寫都是正確的!)。當按鈕按下時,TextField中的數據將被取出,並且,因為它們在字符串結構中,所以需要利用靜態的Integer.paresInt()方法來轉變成中斷。一旦對話類被建立,我們就必須調用show()方法來顯示和激活它。

我們會注意到ToeDialog對象賦值給一個對話引用 d。這是一個向上轉換的例子,儘管它沒有真正地產生重要的差異,因為所有的事件都是show()調用的。但是,如果我們想調用ToeDialog中已經存在的一些方法,我們需要對ToeDialog引用賦值,就不會在一個上溯中丟失信息。

(1) 文件對話類

在一些操作系統中擁有許多的特殊內建對話框去處理選擇的事件,例如:字庫,顏色,打印機以及類似的事件。幾乎所有的操作系統都支持打開和保存文件,但是,Java的FileDialog包更容易使用。當然這會不再檢測所有使用的程序片,因為程序片在本地磁盤上既不能讀也不能寫文件。(這會在新的瀏覽器中交換程序片的信任關係。)

下面的應用程序運用了兩個文件對話類的窗體,一個是打開,一個是保存。大多數的代碼到如今已為我們所熟悉,而所有這些有趣的活動發生在兩個不同按鈕單擊事件的action()方法中。

//: FileDialogTest.java
// Demonstration of File dialog boxes
import java.awt.*;

public class FileDialogTest extends Frame {
  TextField filename = new TextField();
  TextField directory = new TextField();
  Button open = new Button("Open");
  Button save = new Button("Save");
  public FileDialogTest() {
    setTitle("File Dialog Test");
    Panel p = new Panel();
    p.setLayout(new FlowLayout());
    p.add(open);
    p.add(save);
    add("South", p);
    directory.setEditable(false);
    filename.setEditable(false);
    p = new Panel();
    p.setLayout(new GridLayout(2,1));
    p.add(filename);
    p.add(directory);
    add("North", p);
  }
  public boolean handleEvent(Event evt) {
    if(evt.id == Event.WINDOW_DESTROY)
      System.exit(0);
    else
      return super.handleEvent(evt);
    return true;
  }
  public boolean action(Event evt, Object arg) {
    if(evt.target.equals(open)) {
      // Two arguments, defaults to open file:
      FileDialog d = new FileDialog(this,
        "What file do you want to open?");
      d.setFile("*.java"); // Filename filter
      d.setDirectory("."); // Current directory
      d.show();
      String openFile;
      if((openFile = d.getFile()) != null) {
        filename.setText(openFile);
        directory.setText(d.getDirectory());
      } else {
        filename.setText("You pressed cancel");
        directory.setText("");
      }
    }
    else if(evt.target.equals(save)) {
      FileDialog d = new FileDialog(this,
        "What file do you want to save?",
        FileDialog.SAVE);
      d.setFile("*.java");
      d.setDirectory(".");
      d.show();
      String saveFile;
      if((saveFile = d.getFile()) != null) {
        filename.setText(saveFile);
        directory.setText(d.getDirectory());
      } else {
        filename.setText("You pressed cancel");
        directory.setText("");
      }
    }
    else
      return super.action(evt, arg);
    return true;
  }
  public static void main(String[] args) {
    Frame f = new FileDialogTest();
    f.resize(250,110);
    f.show();
  }
} ///:~

對一個“打開文件”對話框,我們使用構造器設置兩個參數;首先是父窗口引用,其次是FileDialog標題條的標題。setFile()方法提供一個初始文件名--也許本地操作系統支持通配符,因此在這個例子中所有的.java文件最開頭會被顯示出來。setDirectory()方法選擇文件決定開始的目錄(一般而言,操作系統允許用戶改變目錄)。

show()命令直到對話類關閉才返回。FileDialog對象一直存在,因此我們可以從它那裡讀取數據。如果我們調用getFile()並且它返回空,這意味著用戶退出了對話類。文件名和調用getDirectory()方法的結果都顯示在TextFields裡。

按鈕的保存工作使用同樣的方法,除了因為FileDialog而使用不同的構造器。這個構造器設置了三個參數並且第三的一個參數必須為FileDialog.SAVEFileDialog.OPEN

13.16 新型AWT

在Java 1.1中一個顯著的改變就是完善了新AWT的創新。大多數的改變圍繞在Java 1.1中使用的新事件模型:老的事件模型是糟糕的、笨拙的、非面向對象的,而新的事件模型可能是我所見過的最優秀的。難以理解一個如此糟糕的(老的AWT)和一個如此優秀的(新的事件模型)程序語言居然出自同一個集團之手。新的考慮事件的方法看來中止了,因此爭議不再變成障礙,從而輕易進入我們的意識裡;相反,它是一個幫助我們設計系統的工具。它同樣是Java Beans的精華,我們會在本章後面部分進入講述。

新的方法設計對象做為“事件源”和“事件接收器”以代替老AWT的非面向對象串聯的條件語句。正象我們將看到的內部類的用途是集成面向對象的原始狀態的新事件。另外,事件現在被描繪為在一個類體系以取代單一的類並且我們可以創建自己的事件類型。

我們同樣會發現,如果我們採用老的AWT編程,Java 1.1版會產生一些看起來不合理的名字轉換。例如,setsize()改成resize()。當我們學習Java Beans時這會變得更加的合理,因為Beans使用一個獨特的命名協議。名字必須被修改以在Beans中產生新的標準AWT組件。

剪貼板操作在Java 1.1版中也得到支持,儘管拖放操作“將在新版本中被支持”。我們可能訪問桌面色彩組織,所以我們的Java可以同其餘桌面保持一致。可以利用彈出式菜單,並且為圖像和圖形作了改進。也同樣支持鼠標操作。還有簡單的為打印的API以及簡單地支持滾動。

13.16.1 新的事件模型

在新的事件模型的組件可以開始一個事件。每種類型的事件被一個個別的類所描繪。當事件開始後,它受理一個或更多事件指明“接收器”。因此,事件源和處理事件的地址可以被分離。

每個事件接收器都是執行特定的接收器類型接口的類對象。因此作為一個程序開發者,我們所要做的是創建接收器對象並且在被激活事件的組件中進行註冊。event-firing組件調用一個addXXXListener()方法來完成註冊,以描述XXX事件類型接受。我們可以容易地瞭解到以addListened名的方法通知我們任何的事件類型都可以被處理,如果我們試圖接收事件我們會發現編譯時我們的錯誤。Java Beans同樣使用這種addListener名的方法去判斷那一個程序可以運行。

我們所有的事件邏輯將裝入到一個接收器類中。當我們創建一個接收器類時唯一的一點限制是必須執行專用的接口。我們可以創建一個全局接收器類,這種情況在內部類中有助於被很好地使用,不僅僅是因為它們提供了一個理論上的接收器類組到它們服務的UI或業務邏輯類中,但因為(正像我們將會在本章後面看到的)事實是一個內部類維持一個引用到它的父對象,提供了一個很好的通過類和子系統邊界的調用方法。

一個簡單的例子將使這一切變得清晰明確。同時思考本章前部Button2.java例子與這個例子的差異。

//: Button2New.java
// Capturing button presses
import java.awt.*;
import java.awt.event.*; // Must add this
import java.applet.*;

public class Button2New extends Applet {
  Button
    b1 = new Button("Button 1"),
    b2 = new Button("Button 2");
  public void init() {
    b1.addActionListener(new B1());
    b2.addActionListener(new B2());
    add(b1);
    add(b2);
  }
  class B1 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      getAppletContext().showStatus("Button 1");
    }
  }
  class B2 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      getAppletContext().showStatus("Button 2");
    }
  }
  /* The old way:
  public boolean action(Event evt, Object arg) {
    if(evt.target.equals(b1))
      getAppletContext().showStatus("Button 1");
    else if(evt.target.equals(b2))
      getAppletContext().showStatus("Button 2");
    // Let the base class handle it:
    else
      return super.action(evt, arg);
    return true; // We've handled it here
  }
  */
} ///:~

我們可比較兩種方法,老的代碼在左面作為註解。在init()方法裡,只有一個改變就是增加了下面的兩行:

b1.addActionListener(new B1());
b2.addActionListener(new B2());

按鈕按下時,addActionListener()通知按鈕對象被激活。B1B2類都是執行接口ActionListener的內部類。這個接口包括一個單一的方法actionPerformed()(這意味著當事件激活時,這個動作將被執行)。注意actionPreformed()方法不是一個普通事件,說得更恰當些是一個特殊類型的事件,ActionEvent。如果我們想提取特殊ActionEvent的信息,因此我們不需要故意去測試和向下轉換參數。

對編程者來說一個最好的事便是actionPerformed()十分的簡單易用。它是一個可以調用的方法。同老的action()方法比較,老的方法我們必須指出發生了什麼和適當的動作,同樣,我們會擔心調用基類action()的版本並且返回一個值去指明是否被處理。在新的事件模型中,我們知道所有事件測試推理自動進行,因此我們不必指出發生了什麼;我們剛剛表示發生了什麼,它就自動地完成了。如果我們還沒有提出用新的方法覆蓋老的方法,我們會很快提出。

13.16.2 事件和接收者類型

所有AWT組件都被改變成包含addXXXListener()removeXXXListener()方法,因此特定的接收器類型可從每個組件中增加和刪除。我們會注意到XXX在每個場合中同樣表示參數的方法,例如,addFooListener(FooListener fl)。下面這張表格總結了通過提供addXXXListener()removeXXXListener()方法,從而支持那些特定事件的相關事件、接收器、方法以及組件。

  • 事件,接收器接口及添加和刪除方法
    • 支持這個事件的組件
  • ActionEvent
  • ActionListener
  • addActionListener( )
  • removeActionListener( )
    • Button, List, TextField, MenuItem, and its derivatives including CheckboxMenuItem, Menu, and PopupMenu
  • AdjustmentEvent
  • AdjustmentListener
  • addAdjustmentListener( )
  • removeAdjustmentListener( )
  • Scrollbar
    • Anything you create that implements the Adjustable interface
  • ComponentEvent
  • ComponentListener
  • addComponentListener( )
  • removeComponentListener( )
    • Component and its derivatives, including Button, Canvas, Checkbox, Choice, Container, Panel, Applet, ScrollPane, Window, Dialog, FileDialog, Frame, Label, List, Scrollbar, TextArea, and TextField
  • ContainerEvent
  • ContainerListener
  • addContainerListener( )
  • removeContainerListener( )
    • Container and its derivatives, including Panel, Applet, ScrollPane, Window, Dialog, FileDialog, and Frame
  • FocusEvent
  • FocusListener
  • addFocusListener( )
  • removeFocusListener( )
    • Component and its derivatives, including Button, Canvas, Checkbox, Choice, Container, Panel, Applet, ScrollPane, Window, Dialog, FileDialog, Frame ``Label, List, Scrollbar, TextArea, and ``TextField
  • KeyEvent
  • KeyListener
  • addKeyListener( )
  • removeKeyListener( )
    • Component and its derivatives, including Button, Canvas, Checkbox, Choice, Container, Panel, Applet, ScrollPane, Window, Dialog, FileDialog, Frame, Label, List, Scrollbar, TextArea, and TextField
  • MouseEvent (for ``both ``clicks ``and ``motion)
  • MouseListener
  • addMouseListener( )
  • removeMouseListener( )
    • Component and its derivatives, including Button, Canvas, Checkbox, Choice, Container, Panel, Applet, ScrollPane, Window, Dialog, FileDialog, Frame, Label, List, Scrollbar, TextArea, and TextField
  • MouseEvent[55] (for ``both ``clicks ``and ``motion)
  • MouseMotionListener
  • addMouseMotionListener( )
  • removeMouseMotionListener( )
    • Component and its derivatives, including Button, Canvas, Checkbox, Choice, Container, Panel, Applet, ScrollPane, Window, Dialog, FileDialog, Frame, Label, List, Scrollbar, TextArea, and TextField
  • WindowEvent
  • WindowListener
  • addWindowListener( )
  • removeWindowListener( )
    • Window and its derivatives, including Dialog, FileDialog, and Frame
  • ItemEvent
  • ItemListener
  • addItemListener( )
  • removeItemListener( )
    • Checkbox, CheckboxMenuItem, Choice, List, and anything that implements the ItemSelectable interface
  • TextEvent
  • TextListener
  • addTextListener( )
  • removeTextListener( )
    • Anything derived from TextComponent, including TextArea and TextField

⑤:即使表面上如此,但實際上並沒有MouseMotiionEvent(鼠標運動事件)。單擊和運動都組合到MouseEvent裡,所以MouseEvent在表格中的這種另類行為並非一個錯誤。

可以看到,每種類型的組件只為特定類型的事件提供了支持。這有助於我們發現由每種組件支持的事件,如下表所示:

  • 組件類型
    • 支持的事件
  • Adjustable
    • AdjustmentEvent
  • Applet
    • ContainerEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • Button
    • ActionEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • Canvas
    • FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • Checkbox
    • ItemEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • CheckboxMenuItem
    • ActionEvent, ItemEvent
  • Choice
    • ItemEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • Component
    • FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • Container
    • ContainerEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • Dialog
    • ContainerEvent, WindowEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • FileDialog
    • ContainerEvent, WindowEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • Frame
    • ContainerEvent, WindowEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • Label
    • FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • List
    • ActionEvent, FocusEvent, KeyEvent, MouseEvent, ItemEvent, ComponentEvent
  • Menu
    • ActionEvent
  • MenuItem
    • ActionEvent
  • Panel
    • ContainerEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • PopupMenu
    • ActionEvent
  • Scrollbar
    • AdjustmentEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • ScrollPane
    • ContainerEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • TextArea
    • TextEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • TextComponent
    • TextEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • TextField
    • ActionEvent, TextEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent
  • Window
    • ContainerEvent, WindowEvent, FocusEvent, KeyEvent, MouseEvent, ComponentEvent

一旦知道了一個特定的組件支持哪些事件,就不必再去尋找任何東西來響應那個事件。只需簡單地:

(1) 取得事件類的名字,並刪掉其中的Event字樣。在剩下的部分加入Listener字樣。這就是在我們的內部類裡需要實現的接收器接口。

(2) 實現上面的接口,針對想要捕獲的事件編寫方法代碼。例如,假設我們想捕獲鼠標的移動,所以需要為MouseMotiionListener接口的mouseMoved()方法編寫代(當然還必須實現其他一些方法,但這裡有捷徑可循,馬上就會講到這個問題)。

(3) 為步驟2中的接收器類創建一個對象。隨自己的組件和方法完成對它的註冊,方法是在接收器的名字裡加入一個前綴add。比如addMouseMotionListener()

下表是對接收器接口的一個總結:

  • 接收器接口
    • 接口中的方法
  • 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)
  • TextListener
    • textValueChanged(TextEvent)

(1) 用接收器適配器簡化操作

在上面的表格中,我們可以注意到一些接收器接口只有唯一的一個方法。它們的執行是無輕重的,因為我們僅當需要書寫特殊方法時才會執行它們。然而,接收器接口擁有多個方法,使用起來卻不太友好。例如,我們必須一直運行某些事物,當我們創建一個應用程序時對幀提供一個WindowListener,以便當我們得到windowClosing()事件時可以調用System.exit(0)以退出應用程序。但因為WindowListener是一個接口,我們必須執行其它所有的方法即使它們不運行任何事件。這真令人討厭。

為了解決這個問題,每個擁有超過一個方法的接收器接口都可擁有適配器,它們的名我們可以在上面的表格中看到。每個適配器為每個接口方法提供默認的方法。(WindowAdapter的默認方法不是windowClosing(),而是System.exit(0)方法。)此外我們所要做的就是從適配器處繼承並重載唯一的需要變更的方法。例如,典型的WindowListener我們會像下面這樣的使用。

class MyWindowListener extends WindowAdapter {
  public void windowClosing(WindowEvent e) {
    System.exit(0);
  }
}

適配器的全部宗旨就是使接收器的創建變得更加簡便。 但所謂的“適配器”也有一個缺點,而且較難發覺。假定我們象上面那樣寫一個WindowAdapter

class MyWindowListener extends WindowAdapter {
  public void WindowClosing(WindowEvent e) {
    System.exit(0);
  }
}

表面上一切正常,但實際沒有任何效果。每個事件的編譯和運行都很正常——只是關閉窗口不會退出程序。您注意到問題在哪裡嗎?在方法的名字裡:是WindowClosing(),而不是windowClosing()。大小寫的一個簡單失誤就會造成一個嶄新的方法。但是,這並非我們關閉窗口時調用的方法,所以當然沒有任何效果。

13.16.3 用Java 1.1 AWT製作窗口和程序片

我們經常都需要創建一個類,使其既可作為一個窗口調用,亦可作為一個程序片調用。為做到這一點,只需為程序片簡單地加入一個main()即可,令其在一個Frame(幀)裡構建程序片的一個實例。作為一個簡單的示例,下面讓我們來看看如何對Button2New.java作一番修改,使其能同時作為應用程序和程序片使用:

//: Button2NewB.java
// An application and an applet
import java.awt.*;
import java.awt.event.*; // Must add this
import java.applet.*;

public class Button2NewB extends Applet {
  Button
    b1 = new Button("Button 1"),
    b2 = new Button("Button 2");
  TextField t = new TextField(20);
  public void init() {
    b1.addActionListener(new B1());
    b2.addActionListener(new B2());
    add(b1);
    add(b2);
    add(t);
  }
  class B1 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText("Button 1");
    }
  }
  class B2 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText("Button 2");
    }
  }
  // To close the application:
  static class WL extends WindowAdapter {
    public void windowClosing(WindowEvent e) {
      System.exit(0);
    }
  }
  // A main() for the application:
  public static void main(String[] args) {
    Button2NewB applet = new Button2NewB();
    Frame aFrame = new Frame("Button2NewB");
    aFrame.addWindowListener(new WL());
    aFrame.add(applet, BorderLayout.CENTER);
    aFrame.setSize(300,200);
    applet.init();
    applet.start();
    aFrame.setVisible(true);
  }
} ///:~

內部類WLmain()方法是加入程序片的唯一兩個元素,程序片剩餘的部分則原封未動。事實上,我們通常將WL類和main()方法做一結小的改進複製和粘貼到我們自己的程序片裡(請記住創建內部類時通常需要一個外部類來處理它,形成它靜態地消除這個需要)。我們可以看到在main()方法裡,程序片明確地初始化和開始,因為在這個例子裡瀏覽器不能為我們有效地運行它。當然,這不會提供全部的瀏覽器調用stop()destroy()的行為,但對大多數的情況而言它都是可接受的。如果它變成一個麻煩,我們可以:

(1) 使程序片引用為一個靜態類(以代替局部可變的main()),然後:

(2) 在我們調用System.exit()之前在WindowAdapter.windowClosing()中調用applet.stop()applet.destroy()

注意最後一行:

aFrame.setVisible(true);

這是Java 1.1 AWT的一個改變。show()方法不再被支持,而setVisible(true)則取代了show()方法。當我們在本章後面部分學習Java Beans時,這些表面上易於改變的方法將會變得更加的合理。

這個例子同樣被使用TextField修改而不是顯示到控制檯或瀏覽器狀態行上。在開發程序時有一個限制條件就是程序片和應用程序我們都必須根據它們的運行情況選擇輸入和輸出結構。

這裡展示了Java 1.1 AWT的其它小的新功能。我們不再需要去使用有錯誤傾向的利用字符串指定BorderLayout定位的方法。當我們增加一個元素到Java 1.1版的BorderLayout中時,我們可以這樣寫:

aFrame.add(applet, BorderLayout.CENTER);

我們對位置規定一個BorderLayout的常數,以使它能在編譯時被檢驗(而不是對老的結構悄悄地做不合適的事)。這是一個顯著的改善,並且將在這本書的餘下部分大量地使用。

(2) 將窗口接收器變成匿名類

任何一個接收器類都可作為一個匿名類執行,但這一直有個意外,那就是我們可能需要在其它場合使用它們的功能。但是,窗口接收器在這裡僅作為關閉應用程序窗口來使用,因此我們可以安全地製造一個匿名類。然後,main()中的下面這行代碼:

aFrame.addWindowListener(new WL());

會變成:

aFrame.addWindowListener(
  new WindowAdapter() {
    public void windowClosing(WindowEvent e) {
      System.exit(0);
    }
  });

這有一個優點就是它不需要其它的類名。我們必須對自己判斷是否它使代碼變得易於理解或者更難。不過,對本書餘下部分而言,匿名內部類將通常被使用在窗口接收器中。

(3) 將程序片封裝到JAR文件裡

一個重要的JAR應用就是完善程序片的裝載。在Java 1.0版中,人們傾向於試法將它們的代碼填入到單個的程序片類裡,因此客戶只需要單個的服務器就可適合下載程序片代碼。但這不僅使結果凌亂,難以閱讀(當然維護也然)程序,但類文件一直不能壓縮,因此下載從來沒有快過。

JAR文件將我們所有的被壓縮的類文件打包到一個單個兒的文件中,再被瀏覽器下載。現在我們不需要創建一個糟糕的設計以最小化我們創建的類,並且用戶將得到更快地下載速度。

仔細想想上面的例子,這個例子看起來像Button2NewB,是一個單類,但事實上它包含三個內部類,因此共有四個。每當我們編譯程序,我會用這行代碼打包它到一個JAR文件:

jar cf Button2NewB.jar *.class

這是假定只有一個類文件在當前目錄中,其中之一來自Button2NewB.java(否則我們會得到特別的打包)。

現在我們可以創建一個使用新文件標籤來指定JAR文件的HTML頁,如下所示:

<head><title>Button2NewB Example Applet
</title></head>
<body>
<applet code="Button2NewB.class"
        archive="Button2NewB.jar"
        width=200 height=150>
</applet>
</body>

與HTML文件中的程序片標記有關的其他任何內容都保持不變。

13.16.4 再研究一下以前的例子

為注意到一些利用新事件模型的例子和為學習程序從老到新事件模型改變的方法,下面的例子回到在本章第一部分利用事件模型來證明的一些爭議。另外,每個程序包括程序片和應用程序現在都可以藉助或不借助瀏覽器來運行。

(1) 文本字段

這個例子同TextField1.java相似,但它增加了顯然額外的行為:

//: TextNew.java
// Text fields with Java 1.1 events
import java.awt.*;
import java.awt.event.*;
import java.applet.*;

public class TextNew extends Applet {
  Button
    b1 = new Button("Get Text"),
    b2 = new Button("Set Text");
  TextField
    t1 = new TextField(30),
    t2 = new TextField(30),
    t3 = new TextField(30);
  String s = new String();
  public void init() {
    b1.addActionListener(new B1());
    b2.addActionListener(new B2());
    t1.addTextListener(new T1());
    t1.addActionListener(new T1A());
    t1.addKeyListener(new T1K());
    add(b1);
    add(b2);
    add(t1);
    add(t2);
    add(t3);
  }
  class T1 implements TextListener {
    public void textValueChanged(TextEvent e) {
      t2.setText(t1.getText());
    }
  }
  class T1A implements ActionListener {
    private int count = 0;
    public void actionPerformed(ActionEvent e) {
      t3.setText("t1 Action Event " + count++);
    }
  }
  class T1K extends KeyAdapter {
    public void keyTyped(KeyEvent e) {
      String ts = t1.getText();
      if(e.getKeyChar() ==
          KeyEvent.VK_BACK_SPACE) {
        // Ensure it's not empty:
        if( ts.length() > 0) {
          ts = ts.substring(0, ts.length() - 1);
          t1.setText(ts);
        }
      }
      else
        t1.setText(
          t1.getText() +
            Character.toUpperCase(
              e.getKeyChar()));
      t1.setCaretPosition(
        t1.getText().length());
      // Stop regular character from appearing:
      e.consume();
    }
  }
  class B1 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      s = t1.getSelectedText();
      if(s.length() == 0) s = t1.getText();
      t1.setEditable(true);
    }
  }
  class B2 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t1.setText("Inserted by Button 2: " + s);
      t1.setEditable(false);
    }
  }
  public static void main(String[] args) {
    TextNew applet = new TextNew();
    Frame aFrame = new Frame("TextNew");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    aFrame.add(applet, BorderLayout.CENTER);
    aFrame.setSize(300,200);
    applet.init();
    applet.start();
    aFrame.setVisible(true);
  }
} ///:~

TextField t1的動作接收器被激活時,TextField t3就是一個需要報告的場所。我們注意到僅當我們按下enter鍵時,動作接收器才會為TextField所激活。

TextField t1附有幾個接收器。T1接收器從t1複製所有文字到t2,強制所有字符串轉換成大寫。我們會發現這兩個工作同是進行的,並且如果我們增加T1K接收器後我們再增加T1接收器,它就不那麼重要:在文字字段內的所有的字符串將一直被強制變為大寫。這看起來鍵盤事件一直在文字組件事件前被激活,並且如果我們需要保留t2的字符串原來輸入時的樣子,我們就必須做一些特別的工作。

T1K有著其它的一些有趣的活動。我們必須測試backspace(因為我們現在控制著每一個事件)並執行刪除。caret必須被明確地設置到字段的結尾;否則它不會像我們希望的運行。最後,為了防止原來的字符串被默認的機制所處理,事件必須利用為事件對象而存在的consume()方法所“耗盡”。這會通知系統停止激活其餘特殊事件的事件處理器。

這個例子同樣無聲地證明了設計內部類的帶來的諸多優點。注意下面的內部類:

  class T1 implements TextListener {
    public void textValueChanged(TextEvent e) {
      t2.setText(t1.getText());
    }
  }

t1t2不屬於T1的一部分,並且到目前為止它們都是很容易理解的,沒有任何的特殊限制。這是因為一個內部類的對象能自動地捕捉一個引用到外部的創建它的對象那裡,因此我們可以處理封裝類對象的方法和內容。正像我們看到的,這十分方便(註釋⑥)。

⑥:它也解決了“回調”的問題,不必為Java加入任何令人惱火的“方法指針”特性。

(2) 文本區域

Java 1.1版中Text Area最重要的改變就滾動條。對於TextArea的構造器而言,我們可以立即控制TextArea是否會擁有滾動條:水平的,垂直的,兩者都有或者都沒有。這個例子更正了前面Java 1.0版TextArea1.java程序片,演示了Java 1.1版的滾動條構造器:

//: TextAreaNew.java
// Controlling scrollbars with the TextArea
// component in Java 1.1
import java.awt.*;
import java.awt.event.*;
import java.applet.*;

public class TextAreaNew extends Applet {
  Button b1 = new Button("Text Area 1");
  Button b2 = new Button("Text Area 2");
  Button b3 = new Button("Replace Text");
  Button b4 = new Button("Insert Text");
  TextArea t1 = new TextArea("t1", 1, 30);
  TextArea t2 = new TextArea("t2", 4, 30);
  TextArea t3 = new TextArea("t3", 1, 30,
    TextArea.SCROLLBARS_NONE);
  TextArea t4 = new TextArea("t4", 10, 10,
    TextArea.SCROLLBARS_VERTICAL_ONLY);
  TextArea t5 = new TextArea("t5", 4, 30,
    TextArea.SCROLLBARS_HORIZONTAL_ONLY);
  TextArea t6 = new TextArea("t6", 10, 10,
    TextArea.SCROLLBARS_BOTH);
  public void init() {
    b1.addActionListener(new B1L());
    add(b1);
    add(t1);
    b2.addActionListener(new B2L());
    add(b2);
    add(t2);
    b3.addActionListener(new B3L());
    add(b3);
    b4.addActionListener(new B4L());
    add(b4);
    add(t3); add(t4); add(t5); add(t6);
  }
  class B1L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t5.append(t1.getText() + "\n");
    }
  }
  class B2L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t2.setText("Inserted by Button 2");
      t2.append(": " + t1.getText());
      t5.append(t2.getText() + "\n");
    }
  }
  class B3L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String s = " Replacement ";
      t2.replaceRange(s, 3, 3 + s.length());
    }
  }
  class B4L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t2.insert(" Inserted ", 10);
    }
  }
  public static void main(String[] args) {
    TextAreaNew applet = new TextAreaNew();
    Frame aFrame = new Frame("TextAreaNew");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    aFrame.add(applet, BorderLayout.CENTER);
    aFrame.setSize(300,725);
    applet.init();
    applet.start();
    aFrame.setVisible(true);
  }
} ///:~

我們發現只能在構造TextArea時能夠控制滾動條。同樣,即使TE AR沒有滾動條,我們滾動光標也將被制止(可通過運行這個例子中驗證這種行為)。

(3) 複選框和單選鈕

正如早先指出的那樣,複選框和單選鈕都是同一個類建立的。單選鈕和複選框略有不同,它是複選框安置到CheckboxGroup中構成的。在其中任一種情況下,有趣的ItemEvent事件為我們創建一個ItemListener項目接收器。

當處理一組複選框或者單選鈕時,我們有一個不錯的選擇。我們可以創建一個新的內部類去為每個複選框處理事件,或者創建一個內部類判斷哪個複選框被單擊並註冊一個內部類單獨的對象為每個複選對象。下面的例子演示了兩種方法:

//: RadioCheckNew.java
// Radio buttons and Check Boxes in Java 1.1
import java.awt.*;
import java.awt.event.*;
import java.applet.*;

public class RadioCheckNew extends Applet {
  TextField t = new TextField(30);
  Checkbox[] cb = {
    new Checkbox("Check Box 1"),
    new Checkbox("Check Box 2"),
    new Checkbox("Check Box 3") };
  CheckboxGroup g = new CheckboxGroup();
  Checkbox
    cb4 = new Checkbox("four", g, false),
    cb5 = new Checkbox("five", g, true),
    cb6 = new Checkbox("six", g, false);
  public void init() {
    t.setEditable(false);
    add(t);
    ILCheck il = new ILCheck();
    for(int i = 0; i < cb.length; i++) {
      cb[i].addItemListener(il);
      add(cb[i]);
    }
    cb4.addItemListener(new IL4());
    cb5.addItemListener(new IL5());
    cb6.addItemListener(new IL6());
    add(cb4); add(cb5); add(cb6);
  }
  // Checking the source:
  class ILCheck implements ItemListener {
    public void itemStateChanged(ItemEvent e) {
      for(int i = 0; i < cb.length; i++) {
        if(e.getSource().equals(cb[i])) {
          t.setText("Check box " + (i + 1));
          return;
        }
      }
    }
  }
  // vs. an individual class for each item:
  class IL4 implements ItemListener {
    public void itemStateChanged(ItemEvent e) {
      t.setText("Radio button four");
    }
  }
  class IL5 implements ItemListener {
    public void itemStateChanged(ItemEvent e) {
      t.setText("Radio button five");
    }
  }
  class IL6 implements ItemListener {
    public void itemStateChanged(ItemEvent e) {
      t.setText("Radio button six");
    }
  }
  public static void main(String[] args) {
    RadioCheckNew applet = new RadioCheckNew();
    Frame aFrame = new Frame("RadioCheckNew");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    aFrame.add(applet, BorderLayout.CENTER);
    aFrame.setSize(300,200);
    applet.init();
    applet.start();
    aFrame.setVisible(true);
  }
} ///:~

ILCheck擁有當我們增加或者減少複選框時自動調整的優點。當然,我們對單選鈕使用這種方法也同樣的好。但是,它僅當我們的邏輯足以普遍的支持這種方法時才會被使用。如果聲明一個確定的信號——我們將重複利用獨立的接收器類,否則我們將結束一串條件語句。

(4) 下拉列表

下拉列表在Java 1.1版中當一個選擇被改變時同樣使用ItemListener去告知我們:

//: ChoiceNew.java
// Drop-down lists with Java 1.1
import java.awt.*;
import java.awt.event.*;
import java.applet.*;

public class ChoiceNew extends Applet {
  String[] description = { "Ebullient", "Obtuse",
    "Recalcitrant", "Brilliant", "Somnescent",
    "Timorous", "Florid", "Putrescent" };
  TextField t = new TextField(100);
  Choice c = new Choice();
  Button b = new Button("Add items");
  int count = 0;
  public void init() {
    t.setEditable(false);
    for(int i = 0; i < 4; i++)
      c.addItem(description[count++]);
    add(t);
    add(c);
    add(b);
    c.addItemListener(new CL());
    b.addActionListener(new BL());
  }
  class CL implements ItemListener {
    public void itemStateChanged(ItemEvent e) {
      t.setText("index: " +  c.getSelectedIndex()
        + "   " + e.toString());
    }
  }
  class BL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(count < description.length)
        c.addItem(description[count++]);
    }
  }
  public static void main(String[] args) {
    ChoiceNew applet = new ChoiceNew();
    Frame aFrame = new Frame("ChoiceNew");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    aFrame.add(applet, BorderLayout.CENTER);
    aFrame.setSize(750,100);
    applet.init();
    applet.start();
    aFrame.setVisible(true);
  }
} ///:~

這個程序中沒什麼特別新穎的東西(除了Java 1.1版的UI類裡少數幾個值得關注的缺陷)。

(5) 列表

我們消除了Java 1.0中List設計的一個缺陷,就是List不能像我們希望的那樣工作:它會與單擊在一個列表元素上發生衝突。

//: ListNew.java
// Java 1.1 Lists are easier to use
import java.awt.*;
import java.awt.event.*;
import java.applet.*;

public class ListNew extends Applet {
  String[] flavors = { "Chocolate", "Strawberry",
    "Vanilla Fudge Swirl", "Mint Chip",
    "Mocha Almond Fudge", "Rum Raisin",
    "Praline Cream", "Mud Pie" };
  // Show 6 items, allow multiple selection:
  List lst = new List(6, true);
  TextArea t = new TextArea(flavors.length, 30);
  Button b = new Button("test");
  int count = 0;
  public void init() {
    t.setEditable(false);
    for(int i = 0; i < 4; i++)
      lst.addItem(flavors[count++]);
    add(t);
    add(lst);
    add(b);
    lst.addItemListener(new LL());
    b.addActionListener(new BL());
  }
  class LL implements ItemListener {
    public void itemStateChanged(ItemEvent e) {
      t.setText("");
      String[] items = lst.getSelectedItems();
      for(int i = 0; i < items.length; i++)
        t.append(items[i] + "\n");
    }
  }
  class BL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(count < flavors.length)
        lst.addItem(flavors[count++], 0);
    }
  }
  public static void main(String[] args) {
    ListNew applet = new ListNew();
    Frame aFrame = new Frame("ListNew");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    aFrame.add(applet, BorderLayout.CENTER);
    aFrame.setSize(300,200);
    applet.init();
    applet.start();
    aFrame.setVisible(true);
  }
} ///:~

我們可以注意到在列表項中無需特別的邏輯需要去支持一個單擊動作。我們正好像我們在其它地方所做的那樣附加上一個接收器。

(6) 菜單

為菜單處理事件看起來受益於Java 1.1版的事件模型,但Java生成菜單的方法常常麻煩並且需要一些手工編寫代碼。生成菜單的正確方法看起來像資源而不是一些代碼。請牢牢記住編程工具會廣泛地為我們處理創建的菜單,因此這可以減少我們的痛苦(只要它們會同樣處理維護任務!)。另外,我們將發現菜單不支持並且將導致混亂的事件:菜單項使用ActionListeners(動作接收器),但複選框菜單項使用ItemListeners(項目接收器)。菜單對象同樣能支持ActionListeners(動作接收器),但通常不那麼有用。一般來說,我們會附加接收器到每個菜單項或複選框菜單項,但下面的例子(對先前例子的修改)演示了一個聯合捕捉多個菜單組件到一個單獨的接收器類的方法。正像我們將看到的,它或許不值得為這而激烈地爭論。

//: MenuNew.java
// Menus in Java 1.1
import java.awt.*;
import java.awt.event.*;

public class MenuNew extends Frame {
  String[] flavors = { "Chocolate", "Strawberry",
    "Vanilla Fudge Swirl", "Mint Chip",
    "Mocha Almond Fudge", "Rum Raisin",
    "Praline Cream", "Mud Pie" };
  TextField t = new TextField("No flavor", 30);
  MenuBar mb1 = new MenuBar();
  Menu f = new Menu("File");
  Menu m = new Menu("Flavors");
  Menu s = new Menu("Safety");
  // Alternative approach:
  CheckboxMenuItem[] safety = {
    new CheckboxMenuItem("Guard"),
    new CheckboxMenuItem("Hide")
  };
  MenuItem[] file = {
    // No menu shortcut:
    new MenuItem("Open"),
    // Adding a menu shortcut is very simple:
    new MenuItem("Exit",
      new MenuShortcut(KeyEvent.VK_E))
  };
  // A second menu bar to swap to:
  MenuBar mb2 = new MenuBar();
  Menu fooBar = new Menu("fooBar");
  MenuItem[] other = {
    new MenuItem("Foo"),
    new MenuItem("Bar"),
    new MenuItem("Baz"),
  };
  // Initialization code:
  {
    ML ml = new ML();
    CMIL cmil = new CMIL();
    safety[0].setActionCommand("Guard");
    safety[0].addItemListener(cmil);
    safety[1].setActionCommand("Hide");
    safety[1].addItemListener(cmil);
    file[0].setActionCommand("Open");
    file[0].addActionListener(ml);
    file[1].setActionCommand("Exit");
    file[1].addActionListener(ml);
    other[0].addActionListener(new FooL());
    other[1].addActionListener(new BarL());
    other[2].addActionListener(new BazL());
  }
  Button b = new Button("Swap Menus");
  public MenuNew() {
    FL fl = new FL();
    for(int i = 0; i < flavors.length; i++) {
      MenuItem mi = new MenuItem(flavors[i]);
      mi.addActionListener(fl);
      m.add(mi);
      // Add separators at intervals:
      if((i+1) % 3 == 0)
        m.addSeparator();
    }
    for(int i = 0; i < safety.length; i++)
      s.add(safety[i]);
    f.add(s);
    for(int i = 0; i < file.length; i++)
      f.add(file[i]);
    mb1.add(f);
    mb1.add(m);
    setMenuBar(mb1);
    t.setEditable(false);
    add(t, BorderLayout.CENTER);
    // Set up the system for swapping menus:
    b.addActionListener(new BL());
    add(b, BorderLayout.NORTH);
    for(int i = 0; i < other.length; i++)
      fooBar.add(other[i]);
    mb2.add(fooBar);
  }
  class BL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      MenuBar m = getMenuBar();
      if(m == mb1) setMenuBar(mb2);
      else if (m == mb2) setMenuBar(mb1);
    }
  }
  class ML implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      MenuItem target = (MenuItem)e.getSource();
      String actionCommand =
        target.getActionCommand();
      if(actionCommand.equals("Open")) {
        String s = t.getText();
        boolean chosen = false;
        for(int i = 0; i < flavors.length; i++)
          if(s.equals(flavors[i])) chosen = true;
        if(!chosen)
          t.setText("Choose a flavor first!");
        else
          t.setText("Opening "+ s +". Mmm, mm!");
      } else if(actionCommand.equals("Exit")) {
        dispatchEvent(
          new WindowEvent(MenuNew.this,
            WindowEvent.WINDOW_CLOSING));
      }
    }
  }
  class FL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      MenuItem target = (MenuItem)e.getSource();
      t.setText(target.getLabel());
    }
  }
  // Alternatively, you can create a different
  // class for each different MenuItem. Then you
  // Don't have to figure out which one it is:
  class FooL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText("Foo selected");
    }
  }
  class BarL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText("Bar selected");
    }
  }
  class BazL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText("Baz selected");
    }
  }
  class CMIL implements ItemListener {
    public void itemStateChanged(ItemEvent e) {
      CheckboxMenuItem target =
        (CheckboxMenuItem)e.getSource();
      String actionCommand =
        target.getActionCommand();
      if(actionCommand.equals("Guard"))
        t.setText("Guard the Ice Cream! " +
          "Guarding is " + target.getState());
      else if(actionCommand.equals("Hide"))
        t.setText("Hide the Ice Cream! " +
          "Is it cold? " + target.getState());
    }
  }
  public static void main(String[] args) {
    MenuNew f = new MenuNew();
    f.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    f.setSize(300,200);
    f.setVisible(true);
  }
} ///:~

在我們開始初始化節(由註解Initialization code:後的右大括號指明)的前面部分的代碼同先前(Java 1.0版)版本相同。這裡我們可以注意到項目接收器和動作接收器被附加在不同的菜單組件上。

Java 1.1支持“菜單快捷鍵”,因此我們可以選擇一個菜單項目利用鍵盤替代鼠標。這十分的簡單;我們只要使用重載菜單項構造器設置第二個參數為一個MenuShortcut(菜單快捷鍵事件)對象即可。菜單快捷鍵構造器設置重要的方法,當它按下時不可思議地顯示在菜單項上。上面的例子增加了Control-EExit菜單項中。

我們同樣會注意setActionCommand()的使用。這看似一點陌生因為在各種情況下“action command”完全同菜單組件上的標籤一樣。為什麼不正好使用標籤代替可選擇的字符串呢?這個難題是國際化的。如果我們重新用其它語言寫這個程序,我們只需要改變菜單中的標籤,並不審查代碼中可能包含新錯誤的所有邏輯。因此使這對檢查文字字符串聯合菜單組件的代碼而言變得簡單容易,當菜單標籤能改變時“動作指令”可以不作任何的改變。所有這些代碼同“動作指令”一同工作,因此它不會受改變菜單標籤的影響。注意在這個程序中,不是所有的菜單組件都被它們的動作指令所審查,因此這些組件都沒有它們的動作指令集。

大多數的構造器同前面的一樣,將幾個調用的異常增加到接收器中。大量的工作發生在接收器裡。在前面例子的BL中,菜單交替發生。在ML中,“尋找ring”方法被作為動作事件(ActionEvent)的資源並對它進行轉換送入菜單項,然後得到動作指令字符串,再通過它去貫穿串聯組,當然條件是對它進行聲明。這些大多數同前面的一樣,但請注意如果Exit被選中,通過進入封裝類對象的引用(MenuNew.this)並創建一個WINDOW_CLOSING事件,一個新的窗口事件就被創建了。新的事件被分配到封裝類對象的dispatchEvent()方法,然後結束調用windowsClosing()內部幀的窗口接收器(這個接收器作為一個內部類被創建在main()裡),似乎這是“正常”產生消息的方法。通過這種機制,我們可以在任何情況下迅速處理任何的信息,因此,它非常的強大。

FL接收器是很簡單儘管它能處理特殊菜單的所有不同的特色。如果我們的邏輯十分的簡單明瞭,這種方法對我們就很有用處,但通常,我們使用這種方法時需要與FooLBarLBazL一道使用,它們每個都附加到一個單獨的菜單組件上,因此必然無需測試邏輯,並且使我們正確地辨識出誰調用了接收器。這種方法產生了大量的類,內部代碼趨向於變得小巧和處理起來簡單、安全。

(7) 對話框

在這個例子裡直接重寫了早期的ToeTest.java程序。在這個新的版本里,任何事件都被安放進一個內部類中。雖然這完全消除了需要記錄產生的任何類的麻煩,作為ToeTest.java的一個例子,它能使內部類的概念變得不那遙遠。在這點,內嵌類被嵌套達四層之深!我們需要的這種設計決定了內部類的優點是否值得增加更加複雜的事物。另外,當我們創建一個非靜態的內部類時,我們將捆綁非靜態類到它周圍的類上。有時,單獨的類可以更容易地被複用。

//: ToeTestNew.java
// Demonstration of dialog boxes
// and creating your own components
import java.awt.*;
import java.awt.event.*;

public class ToeTestNew extends Frame {
  TextField rows = new TextField("3");
  TextField cols = new TextField("3");
  public ToeTestNew() {
    setTitle("Toe Test");
    Panel p = new Panel();
    p.setLayout(new GridLayout(2,2));
    p.add(new Label("Rows", Label.CENTER));
    p.add(rows);
    p.add(new Label("Columns", Label.CENTER));
    p.add(cols);
    add(p, BorderLayout.NORTH);
    Button b = new Button("go");
    b.addActionListener(new BL());
    add(b, BorderLayout.SOUTH);
  }
  static final int BLANK = 0;
  static final int XX = 1;
  static final int OO = 2;
  class ToeDialog extends Dialog {
    // w = number of cells wide
    // h = number of cells high
    int turn = XX; // Start with x's turn
    public ToeDialog(int w, int h) {
      super(ToeTestNew.this,
        "The game itself", false);
      setLayout(new GridLayout(w, h));
      for(int i = 0; i < w * h; i++)
        add(new ToeButton());
      setSize(w * 50, h * 50);
      addWindowListener(new WindowAdapter() {
        public void windowClosing(WindowEvent e){
          dispose();
        }
      });
    }
    class ToeButton extends Canvas {
      int state = BLANK;
      ToeButton() {
        addMouseListener(new ML());
      }
      public void paint(Graphics  g) {
        int x1 = 0;
        int y1 = 0;
        int x2 = getSize().width - 1;
        int y2 = getSize().height - 1;
        g.drawRect(x1, y1, x2, y2);
        x1 = x2/4;
        y1 = y2/4;
        int wide = x2/2;
        int high = y2/2;
        if(state == XX) {
          g.drawLine(x1, y1,
            x1 + wide, y1 + high);
          g.drawLine(x1, y1 + high,
            x1 + wide, y1);
        }
        if(state == OO) {
          g.drawOval(x1, y1,
            x1 + wide/2, y1 + high/2);
        }
      }
      class ML extends MouseAdapter {
        public void mousePressed(MouseEvent e) {
          if(state == BLANK) {
            state = turn;
            turn = (turn == XX ? OO : XX);
          }
          else
            state = (state == XX ? OO : XX);
          repaint();
        }
      }
    }
  }
  class BL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      Dialog d = new ToeDialog(
        Integer.parseInt(rows.getText()),
        Integer.parseInt(cols.getText()));
      d.show();
    }
  }
  public static void main(String[] args) {
    Frame f = new ToeTestNew();
    f.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    f.setSize(200,100);
    f.setVisible(true);
  }
} ///:~

由於“靜態”的東西只能位於類的外部一級,所以內部類不可能擁有靜態數據或者靜態內部類。

(8) 文件對話框

這個例子是直接用新事件模型對FileDialogTest.java修改而來。

//: FileDialogNew.java
// Demonstration of File dialog boxes
import java.awt.*;
import java.awt.event.*;

public class FileDialogNew extends Frame {
  TextField filename = new TextField();
  TextField directory = new TextField();
  Button open = new Button("Open");
  Button save = new Button("Save");
  public FileDialogNew() {
    setTitle("File Dialog Test");
    Panel p = new Panel();
    p.setLayout(new FlowLayout());
    open.addActionListener(new OpenL());
    p.add(open);
    save.addActionListener(new SaveL());
    p.add(save);
    add(p, BorderLayout.SOUTH);
    directory.setEditable(false);
    filename.setEditable(false);
    p = new Panel();
    p.setLayout(new GridLayout(2,1));
    p.add(filename);
    p.add(directory);
    add(p, BorderLayout.NORTH);
  }
  class OpenL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      // Two arguments, defaults to open file:
      FileDialog d = new FileDialog(
        FileDialogNew.this,
        "What file do you want to open?");
      d.setFile("*.java");
      d.setDirectory("."); // Current directory
      d.show();
      String yourFile = "*.*";
      if((yourFile = d.getFile()) != null) {
        filename.setText(yourFile);
        directory.setText(d.getDirectory());
      } else {
        filename.setText("You pressed cancel");
        directory.setText("");
      }
    }
  }
  class SaveL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      FileDialog d = new FileDialog(
        FileDialogNew.this,
        "What file do you want to save?",
        FileDialog.SAVE);
      d.setFile("*.java");
      d.setDirectory(".");
      d.show();
      String saveFile;
      if((saveFile = d.getFile()) != null) {
        filename.setText(saveFile);
        directory.setText(d.getDirectory());
      } else {
        filename.setText("You pressed cancel");
        directory.setText("");
      }
    }
  }
  public static void main(String[] args) {
    Frame f = new FileDialogNew();
    f.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    f.setSize(250,110);
    f.setVisible(true);
  }
} ///:~

如果所有的改變是這樣的容易那將有多棒,但至少它們已足夠容易,並且我們的代碼已受益於這改進的可讀性上。

13.16.5 動態綁定事件

新AWT事件模型給我們帶來的一個好處就是靈活性。在老的模型中我們被迫為我們的程序動作艱難地編寫代碼。但新的模型我們可以用單一方法調用增加和刪除事件動作。下面的例子證明了這一點:

//: DynamicEvents.java
// The new Java 1.1 event model allows you to
// change event behavior dynamically. Also
// demonstrates multiple actions for an event.
import java.awt.*;
import java.awt.event.*;
import java.util.*;

public class DynamicEvents extends Frame {
  Vector v = new Vector();
  int i = 0;
  Button
    b1 = new Button("Button 1"),
    b2 = new Button("Button 2");
  public DynamicEvents() {
    setLayout(new FlowLayout());
    b1.addActionListener(new B());
    b1.addActionListener(new B1());
    b2.addActionListener(new B());
    b2.addActionListener(new B2());
    add(b1);
    add(b2);
  }
  class B implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      System.out.println("A button was pressed");
    }
  }
  class CountListener implements ActionListener {
    int index;
    public CountListener(int i) { index = i; }
    public void actionPerformed(ActionEvent e) {
      System.out.println(
        "Counted Listener " + index);
    }
  }    
  class B1 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      System.out.println("Button 1 pressed");
      ActionListener a = new CountListener(i++);
      v.addElement(a);
      b2.addActionListener(a);
    }
  }
  class B2 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      System.out.println("Button 2 pressed");
      int end = v.size() -1;
      if(end >= 0) {
        b2.removeActionListener(
          (ActionListener)v.elementAt(end));
        v.removeElementAt(end);
      }
    }
  }
  public static void main(String[] args) {
    Frame f = new DynamicEvents();
    f.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e){
          System.exit(0);
        }
      });
    f.setSize(300,200);
    f.show();
  }
} ///:~

這個例子採取的新手法包括:

(1) 在每個按鈕上附著不少於一個的接收器。通常,組件把事件作為多轉換處理,這意味著我們可以為單個事件註冊許多接收器。當在特殊的組件中一個事件作為單一轉換被處理時,我們會得到TooManyListenersException(即太多接收器異常)。

(2) 程序執行期間,接收器動態地被從按鈕B2中增加和刪除。增加用我們前面見到過的方法完成,但每個組件同樣有一個removeXXXListener()(刪除XXX接收器)方法來刪除各種類型的接收器。

這種靈活性為我們的編程提供了更強大的能力。

我們注意到事件接收器不能保證在命令他們被增加時可被調用(雖然事實上大部分的執行工作都是用這種方法完成的)。

13.16.6 將事務邏輯與UI邏輯區分開

一般而言,我們需要設計我們的類如此以至於每一類做“一件事”。當涉及用戶接口代碼時就更顯得尤為重要,因為它很容易地封裝“您要做什麼”和“怎樣顯示它”。這種有效的配合防止了代碼的重複使用。更不用說它令人滿意的從GUI中區分出我們的“事物邏輯”。使用這種方法,我們可以不僅僅更容易地重複使用事物邏輯,它同樣可以更容易地重複使用GUI。

其它的爭議是“動作對象”存在的完成分離機器的多層次系統。動作主要的定位規則允許所有新事件修改後立刻生效,並且這是如此一個引人注目的設置系統的方法。但是這些動作對象可以被在一些不同的應用程序使用並且因此不會被一些特殊的顯示模式所約束。它們會合理地執行動作操作並且沒有多餘的事件。

下面的例子演示了從GUI代碼中多麼地輕鬆的區分事物邏輯:

//: Separation.java
// Separating GUI logic and business objects
import java.awt.*;
import java.awt.event.*;
import java.applet.*;

class BusinessLogic {
  private int modifier;
  BusinessLogic(int mod) {
    modifier = mod;
  }
  public void setModifier(int mod) {
    modifier = mod;
  }
  public int getModifier() {
    return modifier;
  }
  // Some business operations:
  public int calculation1(int arg) {
    return arg * modifier;
  }
  public int calculation2(int arg) {
    return arg + modifier;
  }
}

public class Separation extends Applet {
  TextField
    t = new TextField(20),
    mod = new TextField(20);
  BusinessLogic bl = new BusinessLogic(2);
  Button
    calc1 = new Button("Calculation 1"),
    calc2 = new Button("Calculation 2");
  public void init() {
    add(t);
    calc1.addActionListener(new Calc1L());
    calc2.addActionListener(new Calc2L());
    add(calc1); add(calc2);
    mod.addTextListener(new ModL());
    add(new Label("Modifier:"));
    add(mod);
  }
  static int getValue(TextField tf) {
    try {
      return Integer.parseInt(tf.getText());
    } catch(NumberFormatException e) {
      return 0;
    }
  }
  class Calc1L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText(Integer.toString(
        bl.calculation1(getValue(t))));
    }
  }
  class Calc2L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText(Integer.toString(
        bl.calculation2(getValue(t))));
    }
  }
  class ModL implements TextListener {
    public void textValueChanged(TextEvent e) {
      bl.setModifier(getValue(mod));
    }
  }
  public static void main(String[] args) {
    Separation applet = new Separation();
    Frame aFrame = new Frame("Separation");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    aFrame.add(applet, BorderLayout.CENTER);
    aFrame.setSize(200,200);
    applet.init();
    applet.start();
    aFrame.setVisible(true);
  }
} ///:~

可以看到,事物邏輯是一個直接完成它的操作而不需要提示並且可以在GUI環境下使用的類。它正適合它的工作。區分動作記錄了所有UI的詳細資料,並且它只通過它的公共接口與事物邏輯交流。所有的操作圍繞中心通過UI和事物邏輯對象來回獲取信息。因此區分,輪流做它的工作。因為區分中只知道它同事物邏輯對象對話(也就是說,它沒有高度的結合),它可以被強迫同其它類型的對象對話而沒有更多的煩惱。 思考從事物邏輯中區分UI的條件,同樣思考當我們調整傳統的Java代碼使它運行時,怎樣使它更易存活。

13.16.7 推薦編碼方法

內部類是新的事件模型,並且事實上舊的事件模型連同新庫的特徵都被它好的支持,依賴老式的編程方法無疑增加了一個新的混亂的因素。現在有更多不同的方法為我們編寫討厭的代碼。湊巧的是,這種代碼顯現在本書中和程序樣本中,並且甚至在文件和程序樣本中同SUN公司區別開來。在這一節中,我們將看到一些關於我們會和不會運行新AWT的爭執,並由向我們展示除了可以原諒的情況,我們可以隨時使用接收器類去解決我們的事件處理需要來結束。因為這種方法同樣是最簡單和最清晰的方法,它將會對我們學習它構成有效的幫助。

在看到任何事以前,我們知道盡管Java 1.1向後兼容Java 1.0(也就是說,我們可以在1.1中編譯和運行1.0的程序),但我們並不能在同一個程序裡混合事件模型。換言之,當我們試圖集成老的代碼到一個新的程序中時,我們不能使用老式的action()方法在同一個程序中,因此我們必須決定是否對新程序使用老的,難以維護的方法或者升級老的代碼。這不會有太多的競爭因為新的方法對老的方法而言是如此的優秀。

(1) 準則:運行它的好方法

為了給我們一些事物來進行比較,這兒有一個程序例子演示向我們推薦的方法。到現在它會變得相當的熟悉和舒適。

//: GoodIdea.java
// The best way to design classes using the new
// Java 1.1 event model: use an inner class for
// each different event. This maximizes
// flexibility and modularity.
import java.awt.*;
import java.awt.event.*;
import java.util.*;

public class GoodIdea extends Frame {
  Button
    b1 = new Button("Button 1"),
    b2 = new Button("Button 2");
  public GoodIdea() {
    setLayout(new FlowLayout());
    b1.addActionListener(new B1L());
    b2.addActionListener(new B2L());
    add(b1);
    add(b2);
  }
  public class B1L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      System.out.println("Button 1 pressed");
    }
  }
  public class B2L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      System.out.println("Button 2 pressed");
    }
  }
  public static void main(String[] args) {
    Frame f = new GoodIdea();
    f.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e){
          System.out.println("Window Closing");
          System.exit(0);
        }
      });
    f.setSize(300,200);
    f.setVisible(true);
  }
} ///:~

這是頗有點微不足道的:每個按鈕有它自己的印出一些事物到控制檯的接收器。但請注意在整個程序中這不是一個條件語句,或者是一些表示“我想要知道怎樣使事件發生”的語句。每塊代碼都與運行有關,而不是類型檢驗。也就是說,這是最好的編寫我們的代碼的方法;不僅僅是它更易使我們理解概念,至少是使我們更易閱讀和維護。剪切和粘貼到新的程序是同樣如此的容易。

(2) 將主類作為接收器實現

第一個壞主意是一個通常的和推薦的方法。這使得主類(有代表性的是程序片或幀,但它能變成一些類)執行各種不同的接收器。下面是一個例子:

//: BadIdea1.java
// Some literature recommends this approach,
// but it's missing the point of the new event
// model in Java 1.1
import java.awt.*;
import java.awt.event.*;
import java.util.*;

public class BadIdea1 extends Frame
    implements ActionListener, WindowListener {
  Button
    b1 = new Button("Button 1"),
    b2 = new Button("Button 2");
  public BadIdea1() {
    setLayout(new FlowLayout());
    addWindowListener(this);
    b1.addActionListener(this);
    b2.addActionListener(this);
    add(b1);
    add(b2);
  }
  public void actionPerformed(ActionEvent e) {
    Object source = e.getSource();
    if(source == b1)
      System.out.println("Button 1 pressed");
    else if(source == b2)
      System.out.println("Button 2 pressed");
    else
      System.out.println("Something else");
  }    
  public void windowClosing(WindowEvent e) {
    System.out.println("Window Closing");
    System.exit(0);
  }
  public void windowClosed(WindowEvent e) {}
  public void windowDeiconified(WindowEvent e) {}
  public void windowIconified(WindowEvent e) {}
  public void windowActivated(WindowEvent e) {}
  public void windowDeactivated(WindowEvent e) {}
  public void windowOpened(WindowEvent e) {}  

  public static void main(String[] args) {
    Frame f = new BadIdea1();
    f.setSize(300,200);
    f.setVisible(true);
  }
} ///:~

這樣做的用途顯示在下述三行裡:

addWindowListener(this);
b1.addActionListener(this);
b2.addActionListener(this);

因為Badidea1執行動作接收器和窗中接收器,這些程序行當然可以接受,並且如果我們一直堅持設法使少量的類去減少服務器檢索期間的程序片載入的作法,它看起來變成一個不錯的主意。但是:

(1) Java 1.1版支持JAR文件,因此所有我們的文件可以被放置到一個單一的壓縮的JAR文件中,只需要一次服務器檢索。我們不再需要為Internet效率而減少類的數量。

(2) 上面的代碼的組件更加的少,因此它難以抓住和粘貼。注意我們必須不僅要執行各種各樣的接口為我們的主類,但在actionPerformed()方法中,我們利用一串條件語句測試哪個動作被完成了。不僅僅是這個狀態倒退,遠離接收器模型,除此之外,我們不能簡單地重複使用actionPerformed()方法因為它是指定為這個特殊的應用程序使用的。將這個程序例子與GoodIdea.java進行比較,我們可以正好捕捉一個接收器類並粘貼它和最小的焦急到任何地方。另外我們可以為一個單獨的事件註冊多個接收器類,允許甚至更多的模塊在每個接收器類在每個接收器中運行。

(3) 方法的混合

第二個bad idea混合了兩種方法:使用內嵌接收器類,但同樣執行一個或更多的接收器接口以作為主類的一部分。這種方法無需在書中和文件中進行解釋,而且我可以臆測到Java開發者認為他們必須為不同的目的而採取不同的方法。但我們卻不必——在我們編程時,我們或許可能會傾向於使用內嵌接收器類。

//: BadIdea2.java
// An improvement over BadIdea1.java, since it
// uses the WindowAdapter as an inner class
// instead of implementing all the methods of
// WindowListener, but still misses the
// valuable modularity of inner classes
import java.awt.*;
import java.awt.event.*;
import java.util.*;

public class BadIdea2 extends Frame
    implements ActionListener {
  Button
    b1 = new Button("Button 1"),
    b2 = new Button("Button 2");
  public BadIdea2() {
    setLayout(new FlowLayout());
    addWindowListener(new WL());
    b1.addActionListener(this);
    b2.addActionListener(this);
    add(b1);
    add(b2);
  }
  public void actionPerformed(ActionEvent e) {
    Object source = e.getSource();
    if(source == b1)
      System.out.println("Button 1 pressed");
    else if(source == b2)
      System.out.println("Button 2 pressed");
    else
      System.out.println("Something else");
  }
  class WL extends WindowAdapter {
    public void windowClosing(WindowEvent e) {
      System.out.println("Window Closing");
      System.exit(0);
    }
  }
  public static void main(String[] args) {
    Frame f = new BadIdea2();
    f.setSize(300,200);
    f.setVisible(true);
  }
} ///:~

因為actionPerformed()動作完成方法同主類緊密地結合,所以難以複用代碼。它的代碼讀起來同樣是凌亂和令人厭煩的,遠遠超過了內部類方法。不合理的是,我們不得不在Java 1.1版中為事件使用那些老的思路。

(4) 繼承一個組件

創建一個新類型的組件時,在運行事件的老方法中,我們會經常看到不同的地方發生了變化。這裡有一個程序例子來演示這種新的工作方法:

//: GoodTechnique.java
// Your first choice when overriding components
// should be to install listeners. The code is
// much safer, more modular and maintainable.
import java.awt.*;
import java.awt.event.*;

class Display {
  public static final int
    EVENT = 0, COMPONENT = 1,
    MOUSE = 2, MOUSE_MOVE = 3,
    FOCUS = 4, KEY = 5, ACTION = 6,
    LAST = 7;
  public String[] evnt;
  Display() {
    evnt = new String[LAST];
    for(int i = 0; i < LAST; i++)
      evnt[i] = new String();
  }
  public void show(Graphics g) {
    for(int i = 0; i < LAST; i++)
      g.drawString(evnt[i], 0, 10 * i + 10);
  }
}

class EnabledPanel extends Panel {
  Color c;
  int id;
  Display display = new Display();
  public EnabledPanel(int i, Color mc) {
    id = i;
    c = mc;
    setLayout(new BorderLayout());
    add(new MyButton(), BorderLayout.SOUTH);
    addComponentListener(new CL());
    addFocusListener(new FL());
    addKeyListener(new KL());
    addMouseListener(new ML());
    addMouseMotionListener(new MML());
  }
  // To eliminate flicker:
  public void update(Graphics g) {
    paint(g);
  }
  public void paint(Graphics  g) {
    g.setColor(c);
    Dimension s = getSize();
    g.fillRect(0, 0, s.width, s.height);
    g.setColor(Color.black);
    display.show(g);
  }
  // Don't need to enable anything for this:
  public void processEvent(AWTEvent e) {
    display.evnt[Display.EVENT]= e.toString();
    repaint();
    super.processEvent(e);
  }
  class CL implements ComponentListener {
    public void componentMoved(ComponentEvent e){
      display.evnt[Display.COMPONENT] =
        "Component moved";
      repaint();
    }
    public void
    componentResized(ComponentEvent e) {
      display.evnt[Display.COMPONENT] =
        "Component resized";
      repaint();
    }
    public void
    componentHidden(ComponentEvent e) {
      display.evnt[Display.COMPONENT] =
        "Component hidden";
      repaint();
    }
    public void componentShown(ComponentEvent e){
      display.evnt[Display.COMPONENT] =
        "Component shown";
      repaint();
    }
  }
  class FL implements FocusListener {
    public void focusGained(FocusEvent e) {
      display.evnt[Display.FOCUS] =
        "FOCUS gained";
      repaint();
    }
    public void focusLost(FocusEvent e) {
      display.evnt[Display.FOCUS] =
        "FOCUS lost";
      repaint();
    }
  }
  class KL implements KeyListener {
    public void keyPressed(KeyEvent e) {
      display.evnt[Display.KEY] =
        "KEY pressed: ";
      showCode(e);
    }
    public void keyReleased(KeyEvent e) {
      display.evnt[Display.KEY] =
        "KEY released: ";
      showCode(e);
    }
    public void keyTyped(KeyEvent e) {
      display.evnt[Display.KEY] =
        "KEY typed: ";
      showCode(e);
    }
    void showCode(KeyEvent e) {
      int code = e.getKeyCode();
      display.evnt[Display.KEY] +=
        KeyEvent.getKeyText(code);
      repaint();
    }
  }
  class ML implements MouseListener {
    public void mouseClicked(MouseEvent e) {
      requestFocus(); // Get FOCUS on click
      display.evnt[Display.MOUSE] =
        "MOUSE clicked";
      showMouse(e);
    }
    public void mousePressed(MouseEvent e) {
      display.evnt[Display.MOUSE] =
        "MOUSE pressed";
      showMouse(e);
    }
    public void mouseReleased(MouseEvent e) {
      display.evnt[Display.MOUSE] =
        "MOUSE released";
      showMouse(e);
    }
    public void mouseEntered(MouseEvent e) {
      display.evnt[Display.MOUSE] =
        "MOUSE entered";
      showMouse(e);
    }
    public void mouseExited(MouseEvent e) {
      display.evnt[Display.MOUSE] =
        "MOUSE exited";
      showMouse(e);
    }
    void showMouse(MouseEvent e) {
      display.evnt[Display.MOUSE] +=
        ", x = " + e.getX() +
        ", y = " + e.getY();
      repaint();
    }
  }
  class MML implements MouseMotionListener {
    public void mouseDragged(MouseEvent e) {
      display.evnt[Display.MOUSE_MOVE] =
        "MOUSE dragged";
      showMouse(e);
    }
    public void mouseMoved(MouseEvent e) {
      display.evnt[Display.MOUSE_MOVE] =
        "MOUSE moved";
      showMouse(e);
    }
    void showMouse(MouseEvent e) {
      display.evnt[Display.MOUSE_MOVE] +=
        ", x = " + e.getX() +
        ", y = " + e.getY();
      repaint();
    }
  }
}

class MyButton extends Button {
  int clickCounter;
  String label = "";
  public MyButton() {
    addActionListener(new AL());
  }
  public void paint(Graphics g) {
    g.setColor(Color.green);
    Dimension s = getSize();
    g.fillRect(0, 0, s.width, s.height);
    g.setColor(Color.black);
    g.drawRect(0, 0, s.width - 1, s.height - 1);
    drawLabel(g);
  }
  private void drawLabel(Graphics g) {
    FontMetrics fm = g.getFontMetrics();
    int width = fm.stringWidth(label);
    int height = fm.getHeight();
    int ascent = fm.getAscent();
    int leading = fm.getLeading();
    int horizMargin =
      (getSize().width - width)/2;
    int verMargin =
      (getSize().height - height)/2;
    g.setColor(Color.red);
    g.drawString(label, horizMargin,
      verMargin + ascent + leading);
  }
  class AL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      clickCounter++;
      label = "click #" + clickCounter +
        " " + e.toString();
      repaint();
    }
  }
}

public class GoodTechnique extends Frame {
  GoodTechnique() {
    setLayout(new GridLayout(2,2));
    add(new EnabledPanel(1, Color.cyan));
    add(new EnabledPanel(2, Color.lightGray));
    add(new EnabledPanel(3, Color.yellow));
  }
  public static void main(String[] args) {
    Frame f = new GoodTechnique();
    f.setTitle("Good Technique");
    f.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e){
          System.out.println(e);
          System.out.println("Window Closing");
          System.exit(0);
        }
      });
    f.setSize(700,700);
    f.setVisible(true);
  }
} ///:~

這個程序例子同樣證明了各種各樣的發現和顯示關於它們的信息的事件。這種顯示是一種集中顯示信息的方法。一組字符串去獲取關於每種類型的事件的信息,並且show()方法對任何圖像對象都設置了一個引用,我們採用並直接地寫在外觀代碼上。這種設計是有意的被某種事件重複使用。

激活面板代表了這種新型的組件。它是一個底部有一個按鈕的彩色的面板,並且它由利用接收器類為每一個單獨的事件來引發捕捉所有發生在它之上的事件,除了那些在激活面板重載的老式的processEvent()方法(注意它應該同樣調用super.processEvent())。利用這種方法的唯一理由是它捕捉髮生的每一個事件,因此我們可以觀察持續發生的每一事件。processEvent()方法沒有更多的展示代表每個事件的字符串,否則它會不得不使用一串條件語句去尋找事件。在其它方面,內嵌接收類早已清晰地知道被發現的事件。(假定我們註冊它們到組件,我們不需要任何的控件的邏輯,這將成為我們的目的。)因此,它們不會去檢查任何事件;這些事件正好做它們的原材料。

每個接收器修改顯示字符串和它的指定事件,並且調用重畫方法repaint()因此將顯示這個字符串。我們同樣能注意到一個通常能消除閃爍的祕訣:

public void update(Graphics g) {
paint(g);
}

我們不會始終需要重載update(),但如果我們寫下一些閃爍的程序,並運行它。默認的最新版本的清除背景然後調用paint()方法重新畫出一些圖畫。這個清除動作通常會產生閃爍,但是不必要的,因為paint()重畫了整個的外觀。

我們可以看到許多的接收器——但是,對接收器輸入檢查指令,但我們卻不能接收任何組件不支持的事件。(不像BadTechnuque.java那樣我們能時時刻刻看到)。

試驗這個程序是十分的有教育意義的,因為我們學習了許多的關於在Java中事件發生的方法。一則它展示了大多數開窗口的系統中設計上的瑕疵:它相當的難以去單擊和釋放鼠標,除非移動它,並且當我們實際上正試圖用鼠標單擊在某物體上時開窗口的會常常認為我們是在拖動。一個解決這個問題的方案是使用mousePressed()鼠標按下方法和mouseReleased()鼠標釋放方法去代替mouseClicked()鼠標單擊方法,然後判斷是否去調用我們自己的以時間和4個像素的鼠標滯後作用的“mouseReallyClicked()真實的鼠標單擊”方法。

(5) 蹩腳的組件繼承

另一種做法是調用enableEvent()方法,並將與希望控制的事件對應的模型傳遞給它(許多參考書中都曾提及這種做法)。這樣做會造成那些事件被髮送至老式方法(儘管它們對Java 1.1來說是新的),並採用象processFocusEvent()這樣的名字。也必須要記住調用基類版本。下面是它看起來的樣子。

//: BadTechnique.java
// It's possible to override components this way,
// but the listener approach is much better, so
// why would you?
import java.awt.*;
import java.awt.event.*;

class Display {
  public static final int
    EVENT = 0, COMPONENT = 1,
    MOUSE = 2, MOUSE_MOVE = 3,
    FOCUS = 4, KEY = 5, ACTION = 6,
    LAST = 7;
  public String[] evnt;
  Display() {
    evnt = new String[LAST];
    for(int i = 0; i < LAST; i++)
      evnt[i] = new String();
  }
  public void show(Graphics g) {
    for(int i = 0; i < LAST; i++)
      g.drawString(evnt[i], 0, 10 * i + 10);
  }
}

class EnabledPanel extends Panel {
  Color c;
  int id;
  Display display = new Display();
  public EnabledPanel(int i, Color mc) {
    id = i;
    c = mc;
    setLayout(new BorderLayout());
    add(new MyButton(), BorderLayout.SOUTH);
    // Type checking is lost. You can enable and
    // process events that the component doesn't
    // capture:
    enableEvents(
      // Panel doesn't handle these:
      AWTEvent.ACTION_EVENT_MASK |
      AWTEvent.ADJUSTMENT_EVENT_MASK |
      AWTEvent.ITEM_EVENT_MASK |
      AWTEvent.TEXT_EVENT_MASK |
      AWTEvent.WINDOW_EVENT_MASK |
      // Panel can handle these:
      AWTEvent.COMPONENT_EVENT_MASK |
      AWTEvent.FOCUS_EVENT_MASK |
      AWTEvent.KEY_EVENT_MASK |
      AWTEvent.MOUSE_EVENT_MASK |
      AWTEvent.MOUSE_MOTION_EVENT_MASK |
      AWTEvent.CONTAINER_EVENT_MASK);
      // You can enable an event without
      // overriding its process method.
  }
  // To eliminate flicker:
  public void update(Graphics g) {
    paint(g);
  }
  public void paint(Graphics  g) {
    g.setColor(c);
    Dimension s = getSize();
    g.fillRect(0, 0, s.width, s.height);
    g.setColor(Color.black);
    display.show(g);
  }
  public void processEvent(AWTEvent e) {
    display.evnt[Display.EVENT]= e.toString();
    repaint();
    super.processEvent(e);
  }
  public void
  processComponentEvent(ComponentEvent e) {
    switch(e.getID()) {
      case ComponentEvent.COMPONENT_MOVED:
        display.evnt[Display.COMPONENT] =
          "Component moved";
        break;
      case ComponentEvent.COMPONENT_RESIZED:
        display.evnt[Display.COMPONENT] =
          "Component resized";
        break;
      case ComponentEvent.COMPONENT_HIDDEN:
        display.evnt[Display.COMPONENT] =
          "Component hidden";
        break;
      case ComponentEvent.COMPONENT_SHOWN:
        display.evnt[Display.COMPONENT] =
          "Component shown";
        break;
      default:
    }
    repaint();
    // Must always remember to call the "super"
    // version of whatever you override:
    super.processComponentEvent(e);
  }
  public void processFocusEvent(FocusEvent e) {
    switch(e.getID()) {
      case FocusEvent.FOCUS_GAINED:
        display.evnt[Display.FOCUS] =
          "FOCUS gained";
        break;
      case FocusEvent.FOCUS_LOST:
        display.evnt[Display.FOCUS] =
          "FOCUS lost";
        break;
      default:
    }
    repaint();
    super.processFocusEvent(e);
  }
  public void processKeyEvent(KeyEvent e) {
    switch(e.getID()) {
      case KeyEvent.KEY_PRESSED:
        display.evnt[Display.KEY] =
          "KEY pressed: ";
        break;
      case KeyEvent.KEY_RELEASED:
        display.evnt[Display.KEY] =
          "KEY released: ";
        break;
      case KeyEvent.KEY_TYPED:
        display.evnt[Display.KEY] =
          "KEY typed: ";
        break;
      default:
    }
    int code = e.getKeyCode();
    display.evnt[Display.KEY] +=
      KeyEvent.getKeyText(code);
    repaint();
    super.processKeyEvent(e);
  }
  public void processMouseEvent(MouseEvent e) {
    switch(e.getID()) {
      case MouseEvent.MOUSE_CLICKED:
        requestFocus(); // Get FOCUS on click
        display.evnt[Display.MOUSE] =
          "MOUSE clicked";
        break;
      case MouseEvent.MOUSE_PRESSED:
        display.evnt[Display.MOUSE] =
          "MOUSE pressed";
        break;
      case MouseEvent.MOUSE_RELEASED:
        display.evnt[Display.MOUSE] =
          "MOUSE released";
        break;
      case MouseEvent.MOUSE_ENTERED:
        display.evnt[Display.MOUSE] =
          "MOUSE entered";
        break;
      case MouseEvent.MOUSE_EXITED:
        display.evnt[Display.MOUSE] =
          "MOUSE exited";
        break;
      default:
    }
    display.evnt[Display.MOUSE] +=
      ", x = " + e.getX() +
      ", y = " + e.getY();
    repaint();
    super.processMouseEvent(e);
  }
  public void
  processMouseMotionEvent(MouseEvent e) {
    switch(e.getID()) {
      case MouseEvent.MOUSE_DRAGGED:
        display.evnt[Display.MOUSE_MOVE] =
          "MOUSE dragged";
        break;
      case MouseEvent.MOUSE_MOVED:
        display.evnt[Display.MOUSE_MOVE] =
          "MOUSE moved";
        break;
      default:
    }
    display.evnt[Display.MOUSE_MOVE] +=
      ", x = " + e.getX() +
      ", y = " + e.getY();
    repaint();
    super.processMouseMotionEvent(e);
  }
}

class MyButton extends Button {
  int clickCounter;
  String label = "";
  public MyButton() {
    enableEvents(AWTEvent.ACTION_EVENT_MASK);
  }
  public void paint(Graphics g) {
    g.setColor(Color.green);
    Dimension s = getSize();
    g.fillRect(0, 0, s.width, s.height);
    g.setColor(Color.black);
    g.drawRect(0, 0, s.width - 1, s.height - 1);
    drawLabel(g);
  }
  private void drawLabel(Graphics g) {
    FontMetrics fm = g.getFontMetrics();
    int width = fm.stringWidth(label);
    int height = fm.getHeight();
    int ascent = fm.getAscent();
    int leading = fm.getLeading();
    int horizMargin =
      (getSize().width - width)/2;
    int verMargin =
      (getSize().height - height)/2;
    g.setColor(Color.red);
    g.drawString(label, horizMargin,
                 verMargin + ascent + leading);
  }
  public void processActionEvent(ActionEvent e) {
    clickCounter++;
    label = "click #" + clickCounter +
      " " + e.toString();
    repaint();
    super.processActionEvent(e);
  }
}

public class BadTechnique extends Frame {
  BadTechnique() {
    setLayout(new GridLayout(2,2));
    add(new EnabledPanel(1, Color.cyan));
    add(new EnabledPanel(2, Color.lightGray));
    add(new EnabledPanel(3, Color.yellow));
    // You can also do it for Windows:
    enableEvents(AWTEvent.WINDOW_EVENT_MASK);
  }
  public void processWindowEvent(WindowEvent e) {
    System.out.println(e);
    if(e.getID() == WindowEvent.WINDOW_CLOSING) {
      System.out.println("Window Closing");
      System.exit(0);
    }
  }
  public static void main(String[] args) {
    Frame f = new BadTechnique();
    f.setTitle("Bad Technique");
    f.setSize(700,700);
    f.setVisible(true);
  }
} ///:~

的確,它能夠工作。但卻實在太蹩腳,而且很難編寫、閱讀、調試、維護以及複用。既然如此,為什麼還不使用內部接收器類呢?

13.17 Java 1.1用戶接口API

Java 1.1版同樣增加了一些重要的新功能,包括焦點遍歷,桌面色彩訪問,打印“沙箱內”及早期的剪貼板支持。

焦點遍歷十分的簡單,因為它顯然存在於AWT庫裡的組件並且我們不必為使它工作而去做任何事。如果我們製造我們自己組件並且想使它們去處理焦點遍歷,我們重載isFocusTraversable()以使它返回真值。如果我們想在一個鼠標單擊上捕捉鍵盤焦點,我們可以捕捉鼠標按下事件並且調用requestFocus()需求焦點方法。

13.17.1 桌面顏色

利用桌面顏色,我們可知道當前用戶桌面都有哪些顏色選擇。這樣一來,就可在必要的時候通過自己的程序來運用那些顏色。顏色都會得以自動初始化,並置於SystemColorstatic成員中,所以要做的唯一事情就是讀取自己感興趣的成員。各種名字的意義是不言而喻的:desktopactiveCaptionactiveCaptionTextactiveCaptionBorderinactiveCaptioninactiveCaptionTextinactiveCaptionBorderwindowwindowBorderwindowTextmenumenuTexttexttextTexttextHighlighttextHighlightTexttextInactiveTextcontrolcontrolTextcontrolHighlightcontrolLtHighlightcontrolShadowcontrolDkShadowscrollbarinfo(用於幫助)以及infoText(用於幫助文字)。

13.17.2 打印

非常不幸,打印時沒有多少事情是可以自動進行的。相反,為完成打印,我們必須經歷大量機械的、非OO(面向對象)的步驟。但打印一個圖形化的組件時,可能多少有點兒自動化的意思:默認情況下,print()方法會調用paint()來完成自己的工作。大多數時候這都已經足夠了,但假如還想做一些特別的事情,就必須知道頁面的幾何尺寸。

下面這個例子同時演示了文字和圖形的打印,以及打印圖形時可以採取的不同方法。此外,它也對打印支持進行了測試:

//: PrintDemo.java
// Printing with Java 1.1
import java.awt.*;
import java.awt.event.*;

public class PrintDemo extends Frame {
  Button
    printText = new Button("Print Text"),
    printGraphics = new Button("Print Graphics");
  TextField ringNum = new TextField(3);
  Choice faces = new Choice();
  Graphics g = null;
  Plot plot = new Plot3(); // Try different plots
  Toolkit tk = Toolkit.getDefaultToolkit();
  public PrintDemo() {
    ringNum.setText("3");
    ringNum.addTextListener(new RingL());
    Panel p = new Panel();
    p.setLayout(new FlowLayout());
    printText.addActionListener(new TBL());
    p.add(printText);
    p.add(new Label("Font:"));
    p.add(faces);
    printGraphics.addActionListener(new GBL());
    p.add(printGraphics);
    p.add(new Label("Rings:"));
    p.add(ringNum);
    setLayout(new BorderLayout());
    add(p, BorderLayout.NORTH);
    add(plot, BorderLayout.CENTER);
    String[] fontList = tk.getFontList();
    for(int i = 0; i < fontList.length; i++)
      faces.add(fontList[i]);
    faces.select("Serif");
  }
  class PrintData {
    public PrintJob pj;
    public int pageWidth, pageHeight;
    PrintData(String jobName) {
      pj = getToolkit().getPrintJob(
        PrintDemo.this, jobName, null);
      if(pj != null) {
        pageWidth = pj.getPageDimension().width;
        pageHeight= pj.getPageDimension().height;
        g = pj.getGraphics();
      }
    }
    void end() { pj.end(); }
  }
  class ChangeFont {
    private int stringHeight;
    ChangeFont(String face, int style,int point){
      if(g != null) {
        g.setFont(new Font(face, style, point));
        stringHeight =
          g.getFontMetrics().getHeight();
      }
    }
    int stringWidth(String s) {
      return g.getFontMetrics().stringWidth(s);
    }
    int stringHeight() { return stringHeight; }
  }
  class TBL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      PrintData pd =
        new PrintData("Print Text Test");
      // Null means print job canceled:
      if(pd == null) return;
      String s = "PrintDemo";
      ChangeFont cf = new ChangeFont(
        faces.getSelectedItem(), Font.ITALIC,72);
      g.drawString(s,
        (pd.pageWidth - cf.stringWidth(s)) / 2,
        (pd.pageHeight - cf.stringHeight()) / 3);

      s = "A smaller point size";
      cf = new ChangeFont(
        faces.getSelectedItem(), Font.BOLD, 48);
      g.drawString(s,
        (pd.pageWidth - cf.stringWidth(s)) / 2,
        (int)((pd.pageHeight -
           cf.stringHeight())/1.5));
      g.dispose();
      pd.end();
    }
  }
  class GBL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      PrintData pd =
        new PrintData("Print Graphics Test");
      if(pd == null) return;
      plot.print(g);
      g.dispose();
      pd.end();
    }
  }
  class RingL implements TextListener {
    public void textValueChanged(TextEvent e) {
      int i = 1;
      try {
        i = Integer.parseInt(ringNum.getText());
      } catch(NumberFormatException ex) {
        i = 1;
      }
      plot.rings = i;
      plot.repaint();
    }
  }
  public static void main(String[] args) {
    Frame pdemo = new PrintDemo();
    pdemo.setTitle("Print Demo");
    pdemo.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    pdemo.setSize(500, 500);
    pdemo.setVisible(true);
  }
}

class Plot extends Canvas {
  public int rings = 3;
}

class Plot1 extends Plot {
  // Default print() calls paint():
  public void paint(Graphics g) {
    int w = getSize().width;
    int h = getSize().height;
    int xc = w / 2;
    int yc = w / 2;
    int x = 0, y = 0;
    for(int i = 0; i < rings; i++) {
      if(x < xc && y < yc) {
        g.drawOval(x, y, w, h);
        x += 10; y += 10;
        w -= 20; h -= 20;
      }
    }
  }
}

class Plot2 extends Plot {
  // To fit the picture to the page, you must
  // know whether you're printing or painting:
  public void paint(Graphics g) {
    int w, h;
    if(g instanceof PrintGraphics) {
      PrintJob pj =
        ((PrintGraphics)g).getPrintJob();
      w = pj.getPageDimension().width;
      h = pj.getPageDimension().height;
    }
    else {
      w = getSize().width;
      h = getSize().height;
    }
    int xc = w / 2;
    int yc = w / 2;
    int x = 0, y = 0;
    for(int i = 0; i < rings; i++) {
      if(x < xc && y < yc) {
        g.drawOval(x, y, w, h);
        x += 10; y += 10;
        w -= 20; h -= 20;
      }
    }
  }
}

class Plot3 extends Plot {
  // Somewhat better. Separate
  // printing from painting:
  public void print(Graphics g) {
    // Assume it's a PrintGraphics object:
    PrintJob pj =
      ((PrintGraphics)g).getPrintJob();
    int w = pj.getPageDimension().width;
    int h = pj.getPageDimension().height;
    doGraphics(g, w, h);
  }
  public void paint(Graphics g) {
    int w = getSize().width;
    int h = getSize().height;
    doGraphics(g, w, h);
  }
  private void doGraphics(
      Graphics g, int w, int h) {
    int xc = w / 2;
    int yc = w / 2;
    int x = 0, y = 0;
    for(int i = 0; i < rings; i++) {
      if(x < xc && y < yc) {
        g.drawOval(x, y, w, h);
        x += 10; y += 10;
        w -= 20; h -= 20;
      }
    }
  }
} ///:~

這個程序允許我們從一個選擇列表框中選擇字體(並且我們會注意到很多有用的字體在Java 1.1版中一直受到嚴格的限制,我們沒有任何可以利用的優秀字體安裝在我們的機器上)。它使用這些字體去打出粗體,斜體和不同大小的文字。另外,一個新型組件調用過的繪圖被創建,以用來示範圖形。當打印圖形時,繪圖擁有的ring將顯示在屏幕上和打印在紙上,並且這三個派生類Plot1Plot2Plot3用不同的方法去完成任務以便我們可以看到我們選擇的事物。同樣,我們也能在一個繪圖中改變一些ring——這很有趣,因為它證明了Java 1.1版中打印的脆弱。在我的系統裡,當ring計數顯示too high(究竟這是什麼意思?)時,打印機給出錯誤信息並且不能正確地工作,而當計數給出low enough信息時,打印機又能工作得很好。我們也會注意到,當打印到看起來實際大小不相符的紙時頁面的大小便產生了。這些特點可能被裝入到將來發行的Java中,我們可以使用這個程序來測試它。

這個程序為促進重複使用,不論何時都可以封裝功能到內部類中。例如,不論何時我想開始打印工作(不論圖形或文字),我必須創建一個PrintJob打印工作對象,該對象擁有它自己的連同頁面寬度和高度的圖形對象。創建的PrintJob打印工作對象和提取的頁面尺寸一起被封裝進PrintData class打印類中。

(1) 打印文字

打印文字的概念簡單明瞭:我們選擇一種字體和大小,決定字符串在頁面上存在的位置,並且使用Graphics.drawSrting()方法在頁面上畫出字符串就行了。這意味著,不管怎樣我們必須精確地計算每行字符串在頁面上存在的位置並確定字符串不會超出頁面底部或者同其它行衝突。如果我們想進行字處理,我們將進行的工作與我們很相配。ChangeFont封裝進少量從一種字體到其它的字體的變更方法並自動地創建一個新字體對象和我們想要的字體,款式(粗體和斜體——目前還不支持下劃線、空心等)以及點陣大小。它同樣會簡單地計算字符串的寬度和高度。當我們按下Print text按鈕時,TBL接收器被激活。我們可以注意到它通過迭代創建ChangeFont對象和調用drawString()來在計算出的位置打印出字符串。注意是否這些計算產生預期的結果。(我使用的版本沒有出錯。)

(2) 打印圖形

按下Print graphics按鈕時,GBL接收器會被激活。我們需要打印時,創建的PrintData對象初始化,然後我們簡單地為這個組件調用print()打印方法。為強制打印,我們必須為圖形對象調用dispose()處理方法,並且為PrintData對象調用end()結束方法(或改變為為PrintJob調用end()結束方法。)

這種工作在繪圖對象中繼續。我們可以看到基類繪圖是很簡單的——它擴展畫布並且包括一箇中斷調用ring來指明多少個集中的ring需要畫在這個特殊的畫布上。這三個派生類展示了可達到一個目的的不同的方法:畫在屏幕上和打印的頁面上。

Plot1採用最簡單的編程方法:忽略繪畫和打印的不同,並且重載paint()繪畫方法。使用這種工作方法的原因是默認的print()打印方法簡單地改變工作方法轉而調用Paint()。但是,我們會注意到輸出的尺寸依賴於屏幕上畫布的大小,因為寬度和高度都是在調用Canvas.getSize()方法時決定是,所以這是合理的。如果我們圖像的尺寸一值都是固定不變的,其它的情況都可接受。當畫出的外觀的大小如此的重要時,我們必須深入瞭解的尺寸大小的重要性。不湊巧的是,就像我們將在Plot2中看到的一樣,這種方法變得很棘手。因為一些我們不知道的好的理由,我們不能簡單地要求圖形對象以它自己的大小畫出外觀。這將使整個的處理工作變得十分的優良。相反,如果我們打印而不是繪畫,我們必須利用RTTI instanceof關鍵字(在本書11章中有相應描述)來測試PrintGrapics,然後向下轉換並調用這獨特的PrintGraphics方法:getPrintJob()方法。現在我們擁有PrintJob的引用並且我們可以發現紙張的高度和寬度。這是一種hacky的方法,但也許這對它來說是合理的理由。(在其它方面,到如今我們看到一些其它的庫設計,因此,我們可能會得到設計者們的想法。)

我們可以注意到Plot2中的paint()繪畫方法對打印和繪圖的可能性進行審查。但是因為當打印時Print()方法將被調用,那麼為什麼不使用那種方法呢?這種方法同樣也在Plot3中也被使用,並且它消除了對instanceof使用的需求,因為在Print()方法中我們可以假設我們能對一個PrintGraphics對象轉換。這樣也不壞。這種情況被放置公共繪畫代碼到一個分離的doGraphics()方法的辦法所改進。

(2) 在程序片內運行幀

如果我們想在一個程序片中打印會怎以樣呢?很好,為了打印任何事物我們必須通過工具組件對象的getPrintJob()方法擁有一個PrintJob對象,設置唯一的一個幀對象而不是一個程序片對象。於是它似乎可能從一個應用程序中打印,而不是從一個程序片中打印。但是,它變為我們可以從一個程序片中創建一個幀(相反的到目前為止,我在程序片或應用程序例子中所做的,都可以生成程序片並安放幀。)。這是一個很有用的技術,因為它允許我們在程序片中使用一些應用程序(只要它們不妨礙程序片的安全)。但是,當應用程序窗口在程序片中出現時,我們會注意到WEB瀏覽器插入一些警告在它上面,其中一些產生“Warning:Applet Window.(警告:程序片窗口)”的字樣。

我們會看到這種技術十分直接的安放一個幀到程序片中。唯一的事是當用戶關閉它時我們必須增加幀的代碼(代替調用System.exit()):

//: PrintDemoApplet.java
// Creating a Frame from within an Applet
import java.applet.*;
import java.awt.*;
import java.awt.event.*;

public class PrintDemoApplet extends Applet {
  public void init() {
    Button b = new Button("Run PrintDemo");
    b.addActionListener(new PDL());
    add(b);
  }
  class PDL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      final PrintDemo pd = new PrintDemo();
      pd.addWindowListener(new WindowAdapter() {
        public void windowClosing(WindowEvent e){
          pd.dispose();
        }
      });
      pd.setSize(500, 500);
      pd.show();
    }
  }
} ///:~

伴隨Java 1.1版的打印支持功能而來的是一些混亂。一些宣傳似乎聲明我們能在一個程序片中打印。但Java的安全系統包含了一個特點,可停止一個正在初始化打印工作的程序片,初始化程序片需要通過一個Web瀏覽器或程序片瀏覽器來進行。在寫作這本書時,這看起來像留下了一個未定的爭議。當我在WEB瀏覽器中運行這個程序時,printdemo(打印樣本)窗口正好出現,但它卻根本不能從瀏覽器中打印。

13.17.3 剪貼板

Java 1.1對系統剪貼板提供有限的操作支持(在Java.awt.datatransfer package裡)。我們可以將字符串作這文字對象複製到剪貼板中,並且我們可以從剪貼板中粘貼文字到字符中對角中。當然,剪貼板被設計來容納各種類型的數據,存在於剪貼板上的數據通過程序運行剪切和粘貼進入到程序中。雖然剪切板目前只支持字符串數據,Java的剪切板API通過“特色”概念提供了良好的可擴展性。當數據從剪貼板中出來時,它擁有一個相關的特色集,這個特色集可以被修改(例如,一個圖形可以被表示成一些字符串或者一幅圖像)並且我們會注意到如果特殊的剪貼板數據支持這種特色,我們會對此十分的感興趣。

下面的程序簡單地對TextArea中的字符串數據進行剪切,複製,粘貼的操作做了示範。我們將注意到的是我們需要按照剪切、複製和粘貼的順序進行工作。但如果我們看見一些其它程序中的TextField或者TextArea,我們會發現它們同樣也自動地支持剪貼板的操作順序。程序中簡單地增加了剪貼板的程序化控制,如果我們想用它來捕捉剪貼板上的文字到一些非文字組件中就可以使用這種技術。

//: CutAndPaste.java
// Using the clipboard from Java 1.1
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;

public class CutAndPaste extends Frame {
  MenuBar mb = new MenuBar();
  Menu edit = new Menu("Edit");
  MenuItem
    cut = new MenuItem("Cut"),
    copy = new MenuItem("Copy"),
    paste = new MenuItem("Paste");
  TextArea text = new TextArea(20,20);
  Clipboard clipbd =
    getToolkit().getSystemClipboard();
  public CutAndPaste() {
    cut.addActionListener(new CutL());
    copy.addActionListener(new CopyL());
    paste.addActionListener(new PasteL());
    edit.add(cut);
    edit.add(copy);
    edit.add(paste);
    mb.add(edit);
    setMenuBar(mb);
    add(text, BorderLayout.CENTER);
  }
  class CopyL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String selection = text.getSelectedText();
      StringSelection clipString =
        new StringSelection(selection);
      clipbd.setContents(clipString, clipString);
    }
  }
  class CutL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String selection = text.getSelectedText();
      StringSelection clipString =
        new StringSelection(selection);
      clipbd.setContents(clipString, clipString);
      text.replaceRange("",
        text.getSelectionStart(),
        text.getSelectionEnd());
    }
  }
  class PasteL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      Transferable clipData =
        clipbd.getContents(CutAndPaste.this);
      try {
        String clipString =
          (String)clipData.
            getTransferData(
              DataFlavor.stringFlavor);
        text.replaceRange(clipString,
          text.getSelectionStart(),
          text.getSelectionEnd());
      } catch(Exception ex) {
        System.out.println("not String flavor");
      }
    }
  }
  public static void main(String[] args) {
    CutAndPaste cp = new CutAndPaste();
    cp.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    cp.setSize(300,200);
    cp.setVisible(true);
  }
} ///:~

創建和增加菜單及TextArea到如今似乎已變成一種單調的活動。這與通過工具組件創建的剪貼板字段clipbd有很大的區別。

所有的動作都安置在接收器中。CopyLCupl接收器同樣除了最後的CutL線以外刪除被複制的線。特殊的兩條線是StringSelection對象從字符串從創建並調用StringSelectionsetContents()方法。說得更準確些,就是放一個字符串到剪切板上。

PasteL中,數據被剪貼板利用getContents()進行分解。任何返回的對象都是可移動的匿名的,並且我們並不真正地知道它裡面包含了什麼。有一種發現的方法是調用getTransferDateFlavors(),返回一個DataFlavor對象數組,表明特殊對象支持這種特點。我們同樣能要求它通過我們感興趣的特點直接地使用IsDataFlavorSupported()。但是在這裡使用一種大膽的方法:調用getTransferData()方法,假設裡面的內容支持字符串特色,並且它不是個被分類在異常處理器中的難題 。

在將來,我們希望更多的數據特色能夠被支持。

13.18 可視編程和Beans

迄今為止,我們已看到Java對創建可重複使用的代碼片工作而言是多麼的有價值。“最大限度地可重複使用”的代碼單元擁有類,因為它包含一個緊密結合在一起的單元特性(字段)和單元動作(方法),它們可以直接經過混合或通過繼承被重複使用。

繼承和多態態性是面向對象編程的精華,但在大多數情況下當我們創建一個應用程序時,我們真正最想要的恰恰是我們最需要的組件。我們希望在我們的設計中設置這些部件就像電子工程師在電路板上創造集成電路塊一樣(在使用Java的情況下,就是放到WEB頁面上)。這似乎會成為加快這種“模塊集合”編制程序方法的發展。

“可視化編程”最早的成功——非常的成功——要歸功於微軟公司的Visual Basic(VB,可視化Basic語言),接下來的第二代是Borland公司Delphi(一種客戶/服務器數據庫應用程序開發工具,也是Java Beans設計的主要靈感)。這些編程工具的組件的像徵就是可視化,這是不容置疑的,因為它們通常展示一些類型的可視化組件,例如:一個按慣或一個TextField。事實上,可視化通常表現為組件可以非常精確地訪問運行中程序。因此可視化編程方法的一部分包含從一個調色盤從拖放一個組件並將它放置到我們的窗體中。應用程序創建工具像我們所做的一樣編寫程序代碼,該代碼將導致正在運行的程序中的組件被創建。

簡單地拖放組件到一個窗體中通常不足以構成一個完整的程序。一般情況下,我們需要改變組件的特性,例如組件的色彩,組件的文字,組件連結的數據庫,等等。特性可以參照屬性在編程時進行修改。我們可以在應用程序構建工具中巧妙處置我們組件的屬性,並且當我們創建程序時,構建數據被保存下來,所以當該程序被啟動時,數據能被重新恢復。

到如今,我們可能習慣於使用對象的多個特性,這也是一個動作集合。在設計時,可視化組件的動作可由事件部分地代表,意味著“任何事件都可以發生在組件上”。通常,由我們決定想發生的事件,當一個事件發生時,對所發生的事件連接代碼。

這是關鍵性的部分:應用程序構建工具可以動態地詢問組件(利用映象)以發現組件支持的事件和屬件。一旦它知道它們的狀態,應用程序構建工具就可以顯示組件的屬性並允許我們修改它們的屬性(當我們構建程序時,保存它們的狀態),並且也顯示這些事件。一般而言,我們做一些事件像雙擊一個事件以及應用程序構建工具創建一個代碼並連接到事件上。當事件發生時,我們不得不編寫執行代碼。應用程序構建工具累計為我們做了大量的工作。結果我們可以注意到程序看起來像它所假定的那樣運行,並且依賴應用程序構建工具去為我們管理連接的詳細資料。可視化的編程工具如此成功的原因是它們明顯加快構建的應用程序的處理過程——當然,用戶接口作為應用程序的一部分同樣的好。

13.18.1 什麼是Bean

在經細節處理後,一個組件在類中被獨特的具體化,真正地成為一塊代碼。關鍵的爭議在於應用程序構建工具發現組件的屬性和事件能力。為了創建一個VB組件,程序開發者不得不編寫正確的同時也是複雜煩瑣的代碼片,接下來由某些協議去展現它們的事件和屬性。Delphi是第二代的可視化編程工具並且這種開發語言主動地圍繞可視化編程來設計因此它更容易去創建一個可視化組件。但是,Java帶來了可視化的創作組件做為Java Beans最高級的“裝備”,因為一個Bean就是一個類。我們不必再為製造任何的Bean而編寫一些特殊的代碼或者使用特殊的編程語言。事實上,我們唯一需要做的是略微地修改我們對我們方法命名的辦法。方法名通知應用程序構建工具是否是一個屬性,一個事件或是一個普通的方法。

在Java的文件中,命名規則被錯誤地曲解為“設計模式”。這十分的不幸,因為設計模式(參見第16章)惹來不少的麻煩。命名規則不是設計模式,它是相當的簡單:

(1) 因為屬性被命名為xxx,我們代表性的創建兩個方法:getXxx()setXxx()。注意getset後的第一個字母小寫以產生屬性名。getset方法產生同樣類型的參數。setget的屬性名和類型名之間沒有關係。

(2) 對於布爾邏輯型屬性,我們可以使用上面的getset方法,但我們也可以用is代替 get

(3) Bean的普通方法不適合上面的命名規則,但它們是公用的。

(4)對於事件,我們使用listener(接收器)方法。這種方法完全同我們看到過的方法相同:(addFooBarListener(FooBarListener)removeFooBarListener(FooBarListener)方法用來處理FooBar事件。大多數時候內建的事件和接收器會滿足我們的需要,但我們可以創建自己的事件和接收器接口。

上面的第一點回答了一個關於我們可能注意到的從Java 1.0到Java 1.1的改變的問題:一些方法的名字太過於短小,顯然改寫名字毫無意義。現在我們可以看到為了製造Bean中的特殊的組件,大多數的這些修改不得不適合於getset命名規則。 現在,我們已經可以利用上面的這些指導方針去創建一個簡單的Bean:

//: Frog.java
// A trivial Java Bean
package frogbean;
import java.awt.*;
import java.awt.event.*;

class Spots {}

public class Frog {
  private int jumps;
  private Color color;
  private Spots spots;
  private boolean jmpr;
  public int getJumps() { return jumps; }
  public void setJumps(int newJumps) {
    jumps = newJumps;
  }
  public Color getColor() { return color; }
  public void setColor(Color newColor) {
    color = newColor;
  }
  public Spots getSpots() { return spots; }
  public void setSpots(Spots newSpots) {
    spots = newSpots;
  }
  public boolean isJumper() { return jmpr; }
  public void setJumper(boolean j) { jmpr = j; }
  public void addActionListener(
      ActionListener l) {
    //...
  }
  public void removeActionListener(
      ActionListener l) {
    // ...
  }
  public void addKeyListener(KeyListener l) {
    // ...
  }
  public void removeKeyListener(KeyListener l) {
    // ...
  }
  // An "ordinary" public method:
  public void croak() {
    System.out.println("Ribbet!");
  }
} ///:~

首先,我們可看到Bean就是一個類。通常,所有我們的字段會被作為專用,並且可以接近的唯一辦法是通過方法。緊接著的是命名規則,屬性是jumpcolorjumperspots(注意這些修改是在第一個字母在屬性名的情況下進行的)。雖然內部確定的名字同最早的三個例子的屬性名一樣,在jumper中我們可以看到屬性名不會強迫我們使用任何特殊的內部可變的名字(或者,真的擁有一些內部的可變的屬性名)。

Bean事件的引用是ActionEventKeyEvent,這是根據有關接收器的addremove命名方法得出的。最後我們可以注意到普通的方法croak()一直是Bean的一部分,僅僅是因為它是一個公共的方法,而不是因為它符合一些命名規則。

13.18.2 用Introspector提取BeanInfo

當我們拖放一個Bean的調色板並將它放入到窗體中時,一個Bean的最關鍵的部分的規則發生了。應用程序構建工具必須可以創建Bean(如果它是默認的構造器的話,它就可以做)然後,在此範圍外訪問Bean的源代碼,提取所有的必要的信息以創立屬性表和事件處理器。

解決方案的一部分在11章結尾部分已經顯現出來:Java 1.1版的映象允許一個匿名類的所有方法被發現。這完美地解決了Bean的難題而無需我們使用一些特殊的語言關鍵字像在其它的可視化編程語言中所需要的那樣。事實上,一個主要的原因是映象增加到Java 1.1版中以支持Beans(儘管映象同樣支持對象串聯和遠程方法調用)。因為我們可能希望應用程序構建工具的開發者將不得不映象每個Bean並且通過它們的方法搜索以找到Bean的屬性和事件。

這當然是可能的,但是Java的研製者們希望為每個使用它的用戶提供一個標準的接口,而不僅僅是使Bean更為簡單易用,不過他們也同樣提供了一個創建更復雜的Bean的標準方法。這個接口就是Introspector類,在這個類中最重要的方法靜態的getBeanInfo()。我們通過一個類處理這個方法並且getBeanInfo()方法全面地對類進行查詢,返回一個我們可以進行詳細研究以發現其屬性、方法和事件的BeanInfo對象。

通常我們不會留意這樣的一些事物——我們可能會使用我們大多數的現成的Bean,並且我們不需要了解所有的在底層運行的技術細節。我們會簡單地拖放我們的Bean到我們窗體中,然後配置它們的屬性並且為事件編寫處理器。無論如何它都是一個有趣的並且是有教育意義的使用Introspector來顯示關於Bean信息的練習,好啦,閒話少說,這裡有一個工具請運行它(我們可以在forgbean子目錄中找到它):

//: BeanDumper.java
// A method to introspect a Bean
import java.beans.*;
import java.lang.reflect.*;

public class BeanDumper {
  public static void dump(Class bean){
    BeanInfo bi = null;
    try {
      bi = Introspector.getBeanInfo(
        bean, java.lang.Object.class);
    } catch(IntrospectionException ex) {
      System.out.println("Couldn't introspect " +
        bean.getName());
      System.exit(1);
    }
    PropertyDescriptor[] properties =
      bi.getPropertyDescriptors();
    for(int i = 0; i < properties.length; i++) {
      Class p = properties[i].getPropertyType();
      System.out.println(
        "Property type:\n  " + p.getName());
      System.out.println(
        "Property name:\n  " +
        properties[i].getName());
      Method readMethod =
        properties[i].getReadMethod();
      if(readMethod != null)
        System.out.println(
          "Read method:\n  " +
          readMethod.toString());
      Method writeMethod =
        properties[i].getWriteMethod();
      if(writeMethod != null)
        System.out.println(
          "Write method:\n  " +
          writeMethod.toString());
      System.out.println("====================");
    }
    System.out.println("Public methods:");
    MethodDescriptor[] methods =
      bi.getMethodDescriptors();
    for(int i = 0; i < methods.length; i++)
      System.out.println(
        methods[i].getMethod().toString());
    System.out.println("======================");
    System.out.println("Event support:");
    EventSetDescriptor[] events =
      bi.getEventSetDescriptors();
    for(int i = 0; i < events.length; i++) {
      System.out.println("Listener type:\n  " +
        events[i].getListenerType().getName());
      Method[] lm =
        events[i].getListenerMethods();
      for(int j = 0; j < lm.length; j++)
        System.out.println(
          "Listener method:\n  " +
          lm[j].getName());
      MethodDescriptor[] lmd =
        events[i].getListenerMethodDescriptors();
      for(int j = 0; j < lmd.length; j++)
        System.out.println(
          "Method descriptor:\n  " +
          lmd[j].getMethod().toString());
      Method addListener =
        events[i].getAddListenerMethod();
      System.out.println(
          "Add Listener Method:\n  " +
        addListener.toString());
      Method removeListener =
        events[i].getRemoveListenerMethod();
      System.out.println(
        "Remove Listener Method:\n  " +
        removeListener.toString());
      System.out.println("====================");
    }
  }
  // Dump the class of your choice:
  public static void main(String[] args) {
    if(args.length < 1) {
      System.err.println("usage: \n" +
        "BeanDumper fully.qualified.class");
      System.exit(0);
    }
    Class c = null;
    try {
      c = Class.forName(args[0]);
    } catch(ClassNotFoundException ex) {
      System.err.println(
        "Couldn't find " + args[0]);
      System.exit(0);
    }
    dump(c);
  }
} ///:~

BeanDumper.dump()是一個可以做任何工作的方法。首先它試圖創建一個BeanInfo對象,如果成功地調用BeanInfo的方法,就產生關於屬性、方法和事件的信息。在Introspector.getBeanInfo()中,我們會注意到有一個另外的參數。由它來通知Introspector訪問繼承體系的地點。在這種情況下,它在分析所有對象方法前停下,因為我們對看到那些並不感興趣。

因為屬性,getPropertyDescriptors()返回一組的屬性描述符號。對於每個描述符號我們可以調用getPropertyType()方法徹底的通過屬性方法發現類的對象。這時,我們可以用getName()方法得到每個屬性的假名(從方法名中提取),getname()方法用getReadMethod()getWriteMethod()完成讀和寫的操作。最後的兩個方法返回一個可以真正地用來調用在對象上調用相應的方法方法對象(這是映象的一部分)。對於公共方法(包括屬性方法),getMethodDescriptors()返回一組方法描述字符。每一個我們都可以得到相當的方法對象並可以顯示出它們的名字。

對於事件而言,getEventSetDescriptors()返回一組事件描述字符。它們中的每一個都可以被查詢以找出接收器的類,接收器類的方法以及增加和刪除接收器的方法。BeanDumper程序打印出所有的這些信息。

如果我們調用BeanDumperFrog類中,就像這樣:

java BeanDumper frogbean.Frog

它的輸出結果如下(已刪除這兒不需要的額外細節):

class name: Frog
Property type:
  Color
Property name:
  color
Read method:
  public Color getColor()
Write method:
  public void setColor(Color)
====================
Property type:
  Spots
Property name:
  spots
Read method:
  public Spots getSpots()
Write method:
  public void setSpots(Spots)
====================
Property type:
  boolean
Property name:
  jumper
Read method:
  public boolean isJumper()
Write method:
  public void setJumper(boolean)
====================
Property type:
  int
Property name:
  jumps
Read method:
  public int getJumps()
Write method:
  public void setJumps(int)
====================
Public methods:
public void setJumps(int)
public void croak()
public void removeActionListener(ActionListener)
public void addActionListener(ActionListener)
public int getJumps()
public void setColor(Color)
public void setSpots(Spots)
public void setJumper(boolean)
public boolean isJumper()
public void addKeyListener(KeyListener)
public Color getColor()
public void removeKeyListener(KeyListener)
public Spots getSpots()
======================
Event support:
Listener type:
  KeyListener
Listener method:
  keyTyped
Listener method:
  keyPressed
Listener method:
  keyReleased
Method descriptor:
  public void keyTyped(KeyEvent)
Method descriptor:
  public void keyPressed(KeyEvent)
Method descriptor:
  public void keyReleased(KeyEvent)
Add Listener Method:
  public void addKeyListener(KeyListener)
Remove Listener Method:
  public void removeKeyListener(KeyListener)
====================
Listener type:
  ActionListener
Listener method:
  actionPerformed
Method descriptor:
  public void actionPerformed(ActionEvent)
Add Listener Method:
  public void addActionListener(ActionListener)
Remove Listener Method:
  public void removeActionListener(ActionListener)
====================

這個結果揭示出了Introspector在從我們的Bean產生一個BeanInfo對象時看到的大部分內容。我們可注意到屬性的類型和它們的名字是相互獨立的。請注意小寫的屬性名。(當屬性名開頭在一行中有超過不止的大寫字母,這一次程序就不會被執行。)並且請記住我們在這裡所見到的方法名(例如讀和與方法)真正地從一個可以被用來在對象中調用相關方法的方法對象中產生。

通用方法列表包含了不相關的事件或者屬性,例如croak()。列表中所有的方法都是我們可以有計劃的為Bean調用,並且應用程序構建工具可以選擇列出所有的方法,當我們調用方法時,減輕我們的任務。

最後,我們可以看到事件在接收器中完全地分析研究它的方法、增加和減少接收器的方法。基本上,一旦我們擁有BeanInfo,我們就可以找出對Bean來說任何重要的事物。我們同樣可以為Bean調用方法,即使我們除了對象外沒有任何其它的信息(此外,這也是映象的特點)。

13.18.3 一個更復雜的Bean

接下的程序例子稍微複雜一些,儘管這沒有什麼價值。這個程序是一張不論鼠標何時移動都圍繞它畫一個小圓的,並且一個動作接收器被激活。畫布。當按下鼠標鍵時,我們可以改變的屬性是圓的大小,除此之外還有被顯示文字的色彩,大小,內容。BangBean同樣擁有它自己的addActionListener()removeActionListener()方法,因此我們可以附上自己的當用戶單擊在BangBean上時會被激活的接收器。這樣,我們將能夠確認可支持的屬性和事件:

//: BangBean.java
// A graphical Bean
package bangbean;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

public class BangBean extends Canvas
     implements Serializable {
  protected int xm, ym;
  protected int cSize = 20; // Circle size
  protected String text = "Bang!";
  protected int fontSize = 48;
  protected Color tColor = Color.red;
  protected ActionListener actionListener;
  public BangBean() {
    addMouseListener(new ML());
    addMouseMotionListener(new MML());
  }
  public int getCircleSize() { return cSize; }
  public void setCircleSize(int newSize) {
    cSize = newSize;
  }
  public String getBangText() { return text; }
  public void setBangText(String newText) {
    text = newText;
  }
  public int getFontSize() { return fontSize; }
  public void setFontSize(int newSize) {
    fontSize = newSize;
  }
  public Color getTextColor() { return tColor; }
  public void setTextColor(Color newColor) {
    tColor = newColor;
  }
  public void paint(Graphics g) {
    g.setColor(Color.black);
    g.drawOval(xm - cSize/2, ym - cSize/2,
      cSize, cSize);
  }
  // This is a unicast listener, which is
  // the simplest form of listener management:
  public void addActionListener (
      ActionListener l)
        throws TooManyListenersException {
    if(actionListener != null)
      throw new TooManyListenersException();
    actionListener = l;
  }
  public void removeActionListener(
      ActionListener l) {
    actionListener = null;
  }
  class ML extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      Graphics g = getGraphics();
      g.setColor(tColor);
      g.setFont(
        new Font(
          "TimesRoman", Font.BOLD, fontSize));
      int width =
        g.getFontMetrics().stringWidth(text);
      g.drawString(text,
        (getSize().width - width) /2,
        getSize().height/2);
      g.dispose();
      // Call the listener's method:
      if(actionListener != null)
        actionListener.actionPerformed(
          new ActionEvent(BangBean.this,
            ActionEvent.ACTION_PERFORMED, null));
    }
  }
  class MML extends MouseMotionAdapter {
    public void mouseMoved(MouseEvent e) {
      xm = e.getX();
      ym = e.getY();
      repaint();
    }
  }
  public Dimension getPreferredSize() {
    return new Dimension(200, 200);
  }
  // Testing the BangBean:
  public static void main(String[] args) {
    BangBean bb = new BangBean();
    try {
      bb.addActionListener(new BBL());
    } catch(TooManyListenersException e) {}
    Frame aFrame = new Frame("BangBean Test");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    aFrame.add(bb, BorderLayout.CENTER);
    aFrame.setSize(300,300);
    aFrame.setVisible(true);
  }
  // During testing, send action information
  // to the console:
  static class BBL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      System.out.println("BangBean action");
    }
  }
} ///:~

最重要的是我們會注意到BangBean執行了這種串聯化的接口。這意味著應用程序構建工具可以在程序設計者調整完屬性值後利用串聯為BangBean貯藏所有的信息。當Bean作為運行的應用程序的一部分被創建時,那些被貯藏的屬性被重新恢復,因此我們可以正確地得到我們的設計。

我們能看到通常同Bean一起運行的所有的字段都是專用的——允許只能通過方法來訪問,通常利用“屬性”結構。

當我們注視著addActionListener()的簽名時,我們會注意到它可以產生出一個TooManyListenerException(太多接收器異常)。這個異常指明它是一個單一的類型的,意味著當事件發生時,它只能通知一個接收器。一般情況下,我們會使用具有多種類型的事件,以便一個事件通知多個的接收器。但是,那樣會陷入直到下一章我們才能準備好的結局中,因此這些內容會被重新回顧(下一個標題是“Java Beans 的重新回顧”)。單一類型的事件迴避了這個難題。

當我們按下鼠標鍵時,文字被安入BangBean中間,並且如果動作接收器字段存在,它的actionPerformed()方法就被調用,創建一個新的ActionEvent對象在處理過程中。無論何時鼠標移動,它的新座標將被捕捉,並且畫布會被重畫(像我們所看到的抹去一些畫布上的文字)。

main()方法增加了允許我們從命令行中測試程序的功能。當一個Bean在一個開發環境中,main()方法不會被使用,但擁有它是絕對有益的,因為它提供了快捷的測試能力。無論何時一個ActionEvent發生,main()方法都將創建了一個幀並安置了一個BangBean在它裡面,還在BangBean中附上了一個簡單的動作接收器以打印到控制檯。當然,一般來說應用程序構建工具將創建大多數的Bean的代碼。當我們通過BeanDumper或者安放BangBean到一個可激活Bean的開發環境中去運行BangBean時,我們會注意到會有很多額外的屬性和動作明顯超過了上面的代碼。那是因為BangBean從畫布中繼承,並且畫布就是一個Bean,因此我們看到它的屬性和事件同樣的合適。

13.18.4 Bean的封裝

在我們可以安放一個Bean到一個可激活Bean的可視化構建工具中前,它必須被放入到標準的Bean容器裡,也就是包含Bean類和一個表示“這是一個Bean”的清單文件的JAR(Java ARchive,Java文件)文件中。清單文件是一個簡單的緊隨事件結構的文本文件。對於BangBean而言,清單文件就像下面這樣:

Manifest-Version: 1.0

Name: bangbean/BangBean.class
Java-Bean: True

其中,第一行指出清單文件結構的版本,這是SUN公司在很久以前公佈的版本。第二行(空行忽略)對文件命名為BangBean.class。第三行表示“這個文件是一個Bean”。沒有第三行,程序構建工具不會將類作為一個Bean來認可。

唯一難以處理的部分是我們必須肯定Name:字段中的路徑是正確的。如果我們回顧BangBean.java,我們會看到它在package bangbean(因為存放類路徑的子目錄稱為bangbean)中,並且這個名字在清單文件中必須包括封裝的信息。另外,我們必須安放清單文件在我們封裝路徑的根目錄上,在這個例子中意味著安放文件在bangbean子目錄中。這之後,我們必須從同一目錄中調用Jar來作為清單文件,如下所示:

jar cfm BangBean.jar BangBean.mf bangbean

這個例子假定我們想產生一個名為BangBean.jar的文件並且我們將清單放到一個稱為BangBean.mf文件中。

我們可能會想“當我編譯BangBean.java時,產生的其它類會怎麼樣呢?”哦,它們會在bangbean子目錄中被中止,並且我們會注意到上面jar命令行的最後一個參數就是bangbean子目錄。當我們給jar子目錄名時,它封裝整個的子目錄到jar文件中(在這個例子中,包括BangBean.java的源代碼文件——對於我們自己的Bean我們可能不會去選擇包含源代碼文件。)另外,如果我們改變主意,解開打包的JAR文件,我們會發現我們清單文件並不在裡面,但jar創建了它自己的清單文件(部分根據我們的文件),稱為MAINFEST.MF並且安放它到META-INF子目錄中(代表“meta-information”)。如果我們打開這個清單文件,我們同樣會注意到jar為每個文件加入數字簽名信息,其結構如下:

Digest-Algorithms: SHA MD5
SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0=
MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==

一般來說,我們不必擔心這些,如果我們要做一些修改,可以修改我們的原始的清單文件並且重新調用jar以為我們的Bean創建了一個新的JAR文件。我們同樣也可以簡單地通過增加其它的Bean的信息到我們清單文件來增加它們到JAR文件中。

值得注意的是我們或許需要安放每個Bean到它自己的子目錄中,因為當我們創建一個JAR文件時,分配JAR應用目錄名並且JAR放置子目錄中的任何文件到JAR文件中。我們可以看到FrogBangBean都在它們自己的子目錄中。

一旦我們將我們的Bean正確地放入一個JAR文件中,我們就可以攜帶它到一個可以激活Bean的編程環境中使用。使用這種方法,我們可以從一種工具到另一種工具間交替變換,但SUN公司為Java Beans提供了免費高效的測試工具在它們的“Bean Development Kit,Bean開發工具”(BDK)稱為beanbox。(我們可以從www.javasoft.com處下載。)在我們啟動beanbox前,放置我們的Bean到beanbox中,複製JAR文件到BDK的jars子目錄中。

13.18.5 更復雜的Bean支持

我們可以看到創建一個Bean顯然多麼的簡單。在程序設計中我們幾乎不受到任何的限制。Java Bean的設計提供了一個簡單的輸入點,這樣可以提高到更復雜的層次上。這些高層次的問題超出了這本書所要討論的範圍,但它們會在此做簡要的介紹。我們可以在http://java.sun.com/beans上找到更多的詳細資料。

我們增加更加複雜的程序和它的屬性到一個位置。上面的例子顯示一個獨特的屬性,當然它也可能代表一個數組的屬性。這稱為索引屬性。我們簡單地提供一個相應的方法(再者有一個方法名的命名規則)並且Introspector認可索引屬性,因此我們的應用程序構建工具相應的處理。

屬性可以被捆綁,這意味著它們將通過PropertyChangeEvent通知其它的對象。其它的對象可以隨後根據對Bean的改變選擇修改它們自己。

屬性可以被束縛,這意味著其它的對象可以在一個屬性的改變不能被接受時,拒絕它。其它的對象利用一個PropertyChangeEvent來通知,並且它們產生一個ProptertyVetoException去阻止修改的發生,並恢復為原來的值。

我們同樣能夠改變我們的Bean在設計時的被描繪成的方法:

(1) 我們可以為我們特殊的Bean提供一個定製的屬性表。這個普通的屬性表將被所有的Bean所使用,但當我們的Bean被選擇時,它會自動地調用這張屬性表。

(2) 我們可以為一個特殊的屬性創建一個定製的編輯器,因此普通的屬性表被使用,但當我們指定的屬性被調用時,編輯器會自動地被調用。

(3)我們可以為我們的Bean提供一個定製的BeanInfo類,產生的信息不同於由Introspector默認產生的。

(4) 它同樣可能在所有的FeatureDescriptors中改變expert的開關模式,以辨別基本特徵和更復雜的特徵。

13.18.6 Bean更多的知識

另外有關的爭議是Bean不能被編址。無論何時我們創建一個Bean,都希望它會在一個多線程的環境中運行。這意味著我們必須理解線程的出口,我們將在下一章中介紹。我們會發現有一段稱為“Java Beans的回顧”的節會注意到這個問題和它的解決方案。

13.19 Swing入門(註釋⑦)

通過這一章的學習,當我們的工作方法在AWT中發生了巨大的改變後(如果可以回憶起很久以前,當Java第一次面世時SUN公司曾聲明Java是一種“穩定,牢固”的編程語言),可能一直有Java還不十分的成熟的感覺。的確,現在Java擁有一個不錯的事件模型以及一個優秀的組件複用設計——JavaBeans。但GUI組件看起來還相當的原始,笨拙以及相當的抽象。

⑦:寫作本節時,Swing庫顯然已被Sun“固定”下來了,所以只要你下載並安裝了Swing庫,就應該能正確地編譯和運行這裡的代碼,不會出現任何問題(應該能編譯Sun配套提供的演示程序,以檢測安裝是否正確)。若遇到任何麻煩,請訪問http://www.BruceEckel.com,瞭解最近的更新情況。

而這就是Swing將要佔領的領域。Swing庫在Java 1.1之後面世,因此我們可以自然而然地假設它是Java 1.2的一部分。可是,它是設計為作為一個補充在Java 1.1版中工作的。這樣,我們就不必為了享用好的UI組件庫而等待我們的平臺去支持Java 1.2版了。如果Swing庫不是我們的用戶的Java 1.1版所支持的一部分,並且產生一些意外,那他就可能真正的需要去下載Swing庫了。

Swing包含所有我們缺乏的組件,在整個本章餘下的部分中:我們期望領會現代化的UI,來自按鈕的任何事件包括到樹狀和網格結構中的圖片。它是一個大庫,但在某些方面它為任務被設計得相應的複雜——如果任何事都是簡單的,我們不必編寫更多的代碼但同樣設法運行我們的代碼逐漸地變得更加的複雜。這意味著一個容易的入口,如果我們需要它我們得到它的強大力量。

Swing相當的深奧,這一節不會去試圖讓讀者理解,但會介紹它的能力和Swing簡單地使我們著手使用庫。請注意我們有意識的使用這一切變得簡單。如果我們需要運行更多的,這時Swing能或許能給我們所想要的,如果我們願意深入地研究,可以從SUN公司的在線文檔中獲取更多的資料。

13.19.1 Swing有哪些優點

當我們開始使用Swing庫時,會注意到它在技術上向前邁出了巨大的一步。Swing組件是Bean,因此他們可以支持Bean的任何開發環境中使用。Swing提供了一個完全的UI組件集合。因為速度的關係,所有的組件都很小巧的(沒有“重量級”組件被使用),Swing為了輕便在Java中整個被編寫。

最重要的是我們會希望Swing被稱為“正交使用”;一旦我們採用了這種關於庫的普遍的辦法我們就可以在任何地方應用它們。這主要是因為Bean的命名規則,大多數的時候在我編寫這些程序例子時我可以猜到方法名並且第一次就將它拼寫正確而無需查找任何事物。這無疑是優秀庫設計的品質證明。另外,我們可以廣泛地插入組件到其它的組件中並且事件會正常地工作。

鍵盤操作是自動被支持的——我們可以使用Swing應用程序而不需要鼠標,但我們不得不做一些額外的編程工作(老的AWT中需要一些可怕的代碼以支持鍵盤操作)。滾動被毫不費力地支持——我們簡單地將我們的組件到一個JScrollPane中,同樣我們再增加它到我們的窗體中即可。其它的特徵,例如工具提示條只需要一行單獨的代碼就可執行。

Swing同樣支持一些被稱為“可插入外觀和效果”的事物,這就是說UI的外觀可以在不同的平臺和不同的操作系統上被動態地改變以符合用戶的期望。它甚至可以創造我們自己的外觀和效果。

13.19.2 方便的轉換

如果我們長期艱苦不懈地利用Java 1.1版構建我們的UI,我們並不需要扔掉它改變到Swing陣營中來。幸運的是,庫被設計得允許容易地修改——在很多情況下我們可以簡單地放一個J到我們老AWT組件的每個類名前面即可。下面這個例子擁有我們所熟悉的特色:

//: JButtonDemo.java
// Looks like Java 1.1 but with J's added
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import javax.swing.*;

public class JButtonDemo extends Applet {
  JButton
    b1 = new JButton("JButton 1"),
    b2 = new JButton("JButton 2");
  JTextField t = new JTextField(20);
  public void init() {
    ActionListener al = new ActionListener() {
      public void actionPerformed(ActionEvent e){
        String name =
          ((JButton)e.getSource()).getText();
        t.setText(name + " Pressed");
      }
    };
    b1.addActionListener(al);
    add(b1);
    b2.addActionListener(al);
    add(b2);
    add(t);
  }
  public static void main(String args[]) {
    JButtonDemo applet = new JButtonDemo();
    JFrame frame = new JFrame("TextAreaNew");
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e){
        System.exit(0);
      }
    });
    frame.getContentPane().add(
      applet, BorderLayout.CENTER);
    frame.setSize(300,100);
    applet.init();
    applet.start();
    frame.setVisible(true);
  }
} ///:~

這是一個新的輸入語句,但此外任何事物除了增加了一些J外,看起都像這Java 1.1版的AWT。同樣,我們不恰當的用add()方法增加到Swing JFrame中,除此之外我們必須像上面看到的一樣先準備一些“content pane”。我們可以容易地得到Swing一個簡單的改變所帶來的好處。

因為程序中的封裝語句,我們不得不調用像下面所寫的一樣調用這個程序:

java c13.swing.JbuttonDemo

在這一節裡出現的所有的程序都將需要一個相同的窗體來運行它們。

13.19.3 顯示框架

儘管程序片和應用程序都可以變得很重要,但如果在任何地方都使用它們就會變得混亂和毫無用處。這一節餘下部分取代它們的是一個Swing程序例子的顯示框架:

//: Show.java
// Tool for displaying Swing demos
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Show {
  public static void
  inFrame(JPanel jp, int width, int height) {
    String title = jp.getClass().toString();
    // Remove the word "class":
    if(title.indexOf("class") != -1)
      title = title.substring(6);
    JFrame frame = new JFrame(title);
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e){
        System.exit(0);
      }
    });
    frame.getContentPane().add(
      jp, BorderLayout.CENTER);
    frame.setSize(width, height);
    frame.setVisible(true);
  }
} ///:~

那些想顯示它們自己的類將從JPanel處繼承並且隨後為它們自己增加一些可視化的組件。最後,它們創建一個包含下面這一行程序的main()

Show.inFrame(new MyClass(), 500, 300);

最後的兩個參數是顯示的寬度和高度。

注意JFrame的標題是用RTTI產生的。

13.19.4 工具提示

幾乎所有我們利用來創建我們用戶接口的來自於JComponent的類都包含一個稱為setToolTipText(string)的方法。因此,幾乎任何我們所需要表示的(對於一個對象jc來說就是一些來自JComponent的類)都可以安放在窗體中:

jc.setToolTipText("My tip");

並且當鼠標停在JComponent上一個超過預先設置的一個時間,一個包含我們的文字的小框就會從鼠標下彈出。

13.19.5 邊框

JComponent同樣包括一個稱為setBorder()的方法,該方法允許我們安放一些各種各樣有趣的邊框到一些可見的組件上。下面的程序例子利用一個創建JPanel並安放邊框到每個例子中的被稱為showBorder()的方法,示範了一些有用的不同的邊框。同樣,它也使用RTTI來找我們使用的邊框名(剔除所有的路徑信息),然後將邊框名放到面板中間的JLable裡:

//: Borders.java
// Different Swing borders
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;

public class Borders extends JPanel {
  static JPanel showBorder(Border b) {
    JPanel jp = new JPanel();
    jp.setLayout(new BorderLayout());
    String nm = b.getClass().toString();
    nm = nm.substring(nm.lastIndexOf('.') + 1);
    jp.add(new JLabel(nm, JLabel.CENTER),
      BorderLayout.CENTER);
    jp.setBorder(b);
    return jp;
  }
  public Borders() {
    setLayout(new GridLayout(2,4));
    add(showBorder(new TitledBorder("Title")));
    add(showBorder(new EtchedBorder()));
    add(showBorder(new LineBorder(Color.blue)));
    add(showBorder(
      new MatteBorder(5,5,30,30,Color.green)));
    add(showBorder(
      new BevelBorder(BevelBorder.RAISED)));
    add(showBorder(
      new SoftBevelBorder(BevelBorder.LOWERED)));
    add(showBorder(new CompoundBorder(
      new EtchedBorder(),
      new LineBorder(Color.red))));
  }
  public static void main(String args[]) {
    Show.inFrame(new Borders(), 500, 300);
  }
} ///:~

這一節中大多數程序例子都使用TitledBorder,但我們可以注意到其餘的邊框也同樣易於使用。能創建我們自己的邊框並安放它們到按鈕、標籤等等內——任何來自JComponent的東西。

13.19.6 按鈕

Swing增加了一些不同類型的按鈕,並且它同樣可以修改選擇組件的結構:所有的按鈕、複選框、單選鈕,甚至從AbstractButton處繼承的菜單項(這是因為菜單項一般被包含在其中,它可能會被改進命名為AbstractChooser或者相同的什麼名字)。我們會注意使用菜單項的簡便,下面的例子展示了不同類型的可用的按鈕:

//: Buttons.java
// Various Swing buttons
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.plaf.basic.*;
import javax.swing.border.*;

public class Buttons extends JPanel {
  JButton jb = new JButton("JButton");
  BasicArrowButton
    up = new BasicArrowButton(
      BasicArrowButton.NORTH),
    down = new BasicArrowButton(
      BasicArrowButton.SOUTH),
    right = new BasicArrowButton(
      BasicArrowButton.EAST),
    left = new BasicArrowButton(
      BasicArrowButton.WEST);
  public Buttons() {
    add(jb);
    add(new JToggleButton("JToggleButton"));
    add(new JCheckBox("JCheckBox"));
    add(new JRadioButton("JRadioButton"));
    JPanel jp = new JPanel();
    jp.setBorder(new TitledBorder("Directions"));
    jp.add(up);
    jp.add(down);
    jp.add(left);
    jp.add(right);
    add(jp);
  }
  public static void main(String args[]) {
    Show.inFrame(new Buttons(), 300, 200);
  }
} ///:~

JButton看起來像AWT按鈕,但它沒有更多可運行的功能(像我們後面將看到的如加入圖像等)。在com.sun.java.swing.basic裡,有一個更合適的BasicArrowButton按鈕,但怎樣測試它呢?有兩種類型的“指針”恰好請求箭頭按鈕使用:Spinner修改一箇中斷值,並且StringSpinner通過一個字符串數組來移動(當它到達數組底部時,甚至會自動地封裝)。ActionListeners附著在箭頭按鈕上展示它使用的這些相關指針:因為它們是Bean,我們將期待利用方法名,正好捕捉並設置它們的值。

當我們運行這個程序例子時,我們會發現觸發按鈕保持它最新狀態,開或時關。但複選框和單選鈕每一個動作都相同,選中或沒選中(它們從JToggleButton處繼承)。

13.19.7 按鈕組

如果我們想單選鈕保持“異或”狀態,我們必須增加它們到一個按鈕組中,這幾乎同老AWT中的方法相同但更加的靈活。在下面將要證明的程序例子是,一些AbstruactButton能被增加到一個ButtonGroup中。

為避免重複一些代碼,這個程序利用映射來生不同類型的按鈕組。這會在makeBPanel中看到,makeBPanel創建了一個按鈕組和一個JPanel,並且為數組中的每個String就是makeBPanel的第二個參數增加一個類對象,由它的第一個參數進行聲明:

//: ButtonGroups.java
// Uses reflection to create groups of different
// types of AbstractButton.
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
import java.lang.reflect.*;

public class ButtonGroups extends JPanel {
  static String[] ids = {
    "June", "Ward", "Beaver",
    "Wally", "Eddie", "Lumpy",
  };
  static JPanel
  makeBPanel(Class bClass, String[] ids) {
    ButtonGroup bg = new ButtonGroup();
    JPanel jp = new JPanel();
    String title = bClass.getName();
    title = title.substring(
      title.lastIndexOf('.') + 1);
    jp.setBorder(new TitledBorder(title));
    for(int i = 0; i < ids.length; i++) {
      AbstractButton ab = new JButton("failed");
      try {
        // Get the dynamic constructor method
        // that takes a String argument:
        Constructor ctor = bClass.getConstructor(
          new Class[] { String.class });
        // Create a new object:
        ab = (AbstractButton)ctor.newInstance(
          new Object[]{ids[i]});
      } catch(Exception ex) {
        System.out.println("can't create " +
          bClass);
      }
      bg.add(ab);
      jp.add(ab);
    }
    return jp;
  }
  public ButtonGroups() {
    add(makeBPanel(JButton.class, ids));
    add(makeBPanel(JToggleButton.class, ids));
    add(makeBPanel(JCheckBox.class, ids));
    add(makeBPanel(JRadioButton.class, ids));
  }
  public static void main(String args[]) {
    Show.inFrame(new ButtonGroups(), 500, 300);
  }
} ///:~

邊框標題由類名剔除了所有的路徑信息而來。AbstractButton初始化為一個JButtonJButtonr的標籤發生“失效”,因此如果我們忽略這個異常信息,我們會在屏幕上一直看到這個問題。getConstructor()方法產生了一個通過getConstructor()方法安放參數數組類型到類數組的構造器對象,然後所有我們要做的就是調用newInstance(),通過它一個數組對象包含我們當前的參數——在這種例子中,就是ids數組中的字符串。

這樣增加了一些更復雜的內容到這個簡單的程序中。為了使“異或”行為擁有按鈕,我們創建一個按鈕組並增加每個按鈕到我們所需的組中。當我們運行這個程序時,我們會注意到所有的按鈕除了JButton都會向我們展示“異或”行為。

13.19.8 圖標

我們可在一個JLable或從AbstractButton處繼承的任何事物中使用一個圖標(包括JButtonJCheckboxJradioButton及不同類型的JMenuItem)。利用JLables的圖標十分的簡單容易(我們會在隨後的一個程序例子中看到)。下面的程序例子探索了我們可以利用按鈕的圖標和它們的派生物的其它所有方法。

我們可以使用任何我們需要的GIF文件,但在這個例子中使用的這個GIF文件是這本書編碼發行的一部分,可以在www.BruceEckel.com處下載來使用。為了打開一個文件和隨之帶來的圖像,簡單地創建一個圖標並分配它文件名。從那時起,我們可以在程序中使用這個產生的圖標。

//: Faces.java
// Icon behavior in JButtons
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Faces extends JPanel {
  static Icon[] faces = {
    new ImageIcon("face0.gif"),
    new ImageIcon("face1.gif"),
    new ImageIcon("face2.gif"),
    new ImageIcon("face3.gif"),
    new ImageIcon("face4.gif"),
  };
  JButton
    jb = new JButton("JButton", faces[3]),
    jb2 = new JButton("Disable");
  boolean mad = false;
  public Faces() {
    jb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        if(mad) {
          jb.setIcon(faces[3]);
          mad = false;
        } else {
          jb.setIcon(faces[0]);
          mad = true;
        }
        jb.setVerticalAlignment(JButton.TOP);
        jb.setHorizontalAlignment(JButton.LEFT);
      }
    });
    jb.setRolloverEnabled(true);
    jb.setRolloverIcon(faces[1]);
    jb.setPressedIcon(faces[2]);
    jb.setDisabledIcon(faces[4]);
    jb.setToolTipText("Yow!");
    add(jb);
    jb2.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        if(jb.isEnabled()) {
          jb.setEnabled(false);
          jb2.setText("Enable");
        } else {
          jb.setEnabled(true);
          jb2.setText("Disable");
        }
      }
    });
    add(jb2);
  }
  public static void main(String args[]) {
    Show.inFrame(new Faces(), 300, 200);
  }
} ///:~

一個圖標可以在許多的構造器中使用,但我們可以使用setIcon()方法增加或更換圖標。這個例子同樣展示了當事件發生在JButton(或者一些AbstractButton)上時,為什麼它可以設置各種各樣的顯示圖標:當JButton被按下時,當它被失效時,或者“滾過”時(鼠標從它上面移動過但並不擊它)。我們會注意到那給了按鈕一種動畫的感覺。 注意工具提示條也同樣增加到按鈕中。

13.19.9 菜單

菜單在Swing中做了重要的改進並且更加的靈活——例如,我們可以在幾乎程序中任何地方使用他們,包括在面板和程序片中。語法同它們在老的AWT中是一樣的,並且這樣使出現在老AWT的在新的Swing也出現了:我們必須為我們的菜單艱難地編寫代碼,並且有一些不再作為資源支持菜單(其它事件中的一些將使它們更易轉換成其它的編程語言)。另外,菜單代碼相當的冗長,有時還有一些混亂。下面的方法是放置所有的關於每個菜單的信息到對象的二維數組裡(這種方法可以放置我們想處理的任何事物到數組裡),這種方法在解決這個問題方面領先了一步。這個二維數組被菜單所創建,因此它首先表示出菜單名,並在剩餘的列中表示菜單項和它們的特性。我們會注意到數組列不必保持一致——只要我們的代碼知道將發生的一切事件,每一列都可以完全不同。

//: Menus.java
// A menu-building system; also demonstrates
// icons in labels and menu items.
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Menus extends JPanel {
  static final Boolean
    bT = new Boolean(true),
    bF = new Boolean(false);
  // Dummy class to create type identifiers:
  static class MType { MType(int i) {} };
  static final MType
    mi = new MType(1), // Normal menu item
    cb = new MType(2), // Checkbox menu item
    rb = new MType(3); // Radio button menu item
  JTextField t = new JTextField(10);
  JLabel l = new JLabel("Icon Selected",
    Faces.faces[0], JLabel.CENTER);
  ActionListener a1 = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      t.setText(
        ((JMenuItem)e.getSource()).getText());
    }
  };
  ActionListener a2 = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      JMenuItem mi = (JMenuItem)e.getSource();
      l.setText(mi.getText());
      l.setIcon(mi.getIcon());
    }
  };
  // Store menu data as "resources":
  public Object[][] fileMenu = {
    // Menu name and accelerator:
    { "File", new Character('F') },
    // Name type accel listener enabled
    { "New", mi, new Character('N'), a1, bT },
    { "Open", mi, new Character('O'), a1, bT },
    { "Save", mi, new Character('S'), a1, bF },
    { "Save As", mi, new Character('A'), a1, bF},
    { null }, // Separator
    { "Exit", mi, new Character('x'), a1, bT },
  };
  public Object[][] editMenu = {
    // Menu name:
    { "Edit", new Character('E') },
    // Name type accel listener enabled
    { "Cut", mi, new Character('t'), a1, bT },
    { "Copy", mi, new Character('C'), a1, bT },
    { "Paste", mi, new Character('P'), a1, bT },
    { null }, // Separator
    { "Select All", mi,new Character('l'),a1,bT},
  };
  public Object[][] helpMenu = {
    // Menu name:
    { "Help", new Character('H') },
    // Name type accel listener enabled
    { "Index", mi, new Character('I'), a1, bT },
    { "Using help", mi,new Character('U'),a1,bT},
    { null }, // Separator
    { "About", mi, new Character('t'), a1, bT },
  };
  public Object[][] optionMenu = {
    // Menu name:
    { "Options", new Character('O') },
    // Name type accel listener enabled
    { "Option 1", cb, new Character('1'), a1,bT},
    { "Option 2", cb, new Character('2'), a1,bT},
  };
  public Object[][] faceMenu = {
    // Menu name:
    { "Faces", new Character('a') },
    // Optinal last element is icon
    { "Face 0", rb, new Character('0'), a2, bT,
      Faces.faces[0] },
    { "Face 1", rb, new Character('1'), a2, bT,
      Faces.faces[1] },
    { "Face 2", rb, new Character('2'), a2, bT,
      Faces.faces[2] },
    { "Face 3", rb, new Character('3'), a2, bT,
      Faces.faces[3] },
    { "Face 4", rb, new Character('4'), a2, bT,
      Faces.faces[4] },
  };
  public Object[] menuBar = {
    fileMenu, editMenu, faceMenu,
    optionMenu, helpMenu,
  };
  static public JMenuBar
  createMenuBar(Object[] menuBarData) {
    JMenuBar menuBar = new JMenuBar();
    for(int i = 0; i < menuBarData.length; i++)
      menuBar.add(
        createMenu((Object[][])menuBarData[i]));
    return menuBar;
  }
  static ButtonGroup bgroup;
  static public JMenu
  createMenu(Object[][] menuData) {
    JMenu menu = new JMenu();
    menu.setText((String)menuData[0][0]);
    menu.setMnemonic(
      ((Character)menuData[0][1]).charValue());
    // Create redundantly, in case there are
    // any radio buttons:
    bgroup = new ButtonGroup();
    for(int i = 1; i < menuData.length; i++) {
      if(menuData[i][0] == null)
        menu.add(new JSeparator());
      else
        menu.add(createMenuItem(menuData[i]));
    }
    return menu;
  }
  static public JMenuItem
  createMenuItem(Object[] data) {
    JMenuItem m = null;
    MType type = (MType)data[1];
    if(type == mi)
      m = new JMenuItem();
    else if(type == cb)
      m = new JCheckBoxMenuItem();
    else if(type == rb) {
      m = new JRadioButtonMenuItem();
      bgroup.add(m);
    }
    m.setText((String)data[0]);
    m.setMnemonic(
      ((Character)data[2]).charValue());
    m.addActionListener(
      (ActionListener)data[3]);
    m.setEnabled(
      ((Boolean)data[4]).booleanValue());
    if(data.length == 6)
      m.setIcon((Icon)data[5]);
    return m;
  }
  Menus() {
    setLayout(new BorderLayout());
    add(createMenuBar(menuBar),
      BorderLayout.NORTH);
    JPanel p = new JPanel();
    p.setLayout(new BorderLayout());
    p.add(t, BorderLayout.NORTH);
    p.add(l, BorderLayout.CENTER);
    add(p, BorderLayout.CENTER);
  }
  public static void main(String args[]) {
    Show.inFrame(new Menus(), 300, 200);
  }
} ///:~

這個程序的目的是允許程序設計者簡單地創建表格來描述每個菜單,而不是輸入代碼行來建立菜單。每個菜單都產生一個菜單,表格中的第一列包含菜單名和鍵盤快捷鍵。其餘的列包含每個菜單項的數據:字符串存在在菜單項中的位置,菜單的類型,它的快捷鍵,當菜單項被選中時被激活的動作接收器及菜單是否被激活等信息。如果列開始處是空的,它將被作為一個分隔符來處理。

為了預防浪費和冗長的多個Boolean創建的對象和類型標誌,以下的這些在類開始時就作為static final被創建:bTbF描述Booleans和啞類MType的不同對象描述標準的菜單項(mi),複選框菜單項(cb),和單選鈕菜單項(rb)。請記住一組Object可以擁有單一的Object引用,並且不再是原來的值。

這個程序例子同樣展示了JLablesJMenuItems(和它們的派生事物)如何處理圖標的。一個圖標經由它的構造器置放進JLable中並當對應的菜單項被選中時被改變。

菜單條數組控制處理所有在文件菜單清單中列出的,我們想顯示在菜單條上的文件菜單。我們通過這個數組去使用createMenuBar(),將數組分類成單獨的菜單數據數組,再通過每個單獨的數組去創建菜單。這種方法依次使用菜單數據的每一行並以該數據創建JMenu,然後為菜單數據中剩下的每一行調用createMenuItem()方法。最後,createMenuItem()方法分析菜單數據的每一行並且判斷菜單類型和它的屬性,再適當地創建菜單項。終於,像我們在菜單構造器中看到的一樣,從表示createMenuBar(menuBar)的表格中創建菜單,而所有的事物都是採用遞歸方法處理的。

這個程序不能建立串聯的菜單,但我們擁有足夠的知識,如果我們需要的話,隨時都能增加多級菜單進去。

13.19.10 彈出式菜單

JPopupMenu的執行看起來有一些彆扭:我們必須調用enableEvents()方法並選擇鼠標事件代替利用事件接收器。它可能增加一個鼠標接收器但MouseEventisPopupTrigger()處不會返回真值——它不知道將激活一個彈出菜單。另外,當我們嘗試接收器方法時,它的行為令人不可思議,這或許是鼠標單擊活動引起的。在下面的程序例子裡一些事件產生了這種彈出行為:

//: Popup.java
// Creating popup menus with Swing
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Popup extends JPanel {
  JPopupMenu popup = new JPopupMenu();
  JTextField t = new JTextField(10);
  public Popup() {
    add(t);
    ActionListener al = new ActionListener() {
      public void actionPerformed(ActionEvent e){
        t.setText(
          ((JMenuItem)e.getSource()).getText());
      }
    };
    JMenuItem m = new JMenuItem("Hither");
    m.addActionListener(al);
    popup.add(m);
    m = new JMenuItem("Yon");
    m.addActionListener(al);
    popup.add(m);
    m = new JMenuItem("Afar");
    m.addActionListener(al);
    popup.add(m);
    popup.addSeparator();
    m = new JMenuItem("Stay Here");
    m.addActionListener(al);
    popup.add(m);
    PopupListener pl = new PopupListener();
    addMouseListener(pl);
    t.addMouseListener(pl);
  }
  class PopupListener extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      maybeShowPopup(e);
    }
    public void mouseReleased(MouseEvent e) {
      maybeShowPopup(e);
    }
    private void maybeShowPopup(MouseEvent e) {
      if(e.isPopupTrigger()) {
        popup.show(
          e.getComponent(), e.getX(), e.getY());
      }
    }
  }
  public static void main(String args[]) {
    Show.inFrame(new Popup(),200,150);
  }
} ///:~

相同的ActionListener被加入每個JMenuItem中,使其能從菜單標籤中取出文字,並將文字插入JTextField

13.19.11 列表框和組合框

列表框和組合框在Swing中工作就像它們在老的AWT中工作一樣,但如果我們需要它,它們同樣被增加功能。另外,它也更加的方便易用。例如,JList中有一個顯示String數組的構造器(奇怪的是同樣的功能在JComboBox中無效!)。下面的例子顯示了它們基本的用法。

//: ListCombo.java
// List boxes & Combo boxes
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class ListCombo extends JPanel {
  public ListCombo() {
    setLayout(new GridLayout(2,1));
    JList list = new JList(ButtonGroups.ids);
    add(new JScrollPane(list));
    JComboBox combo = new JComboBox();
    for(int i = 0; i < 100; i++)
      combo.addItem(Integer.toString(i));
    add(combo);
  }
  public static void main(String args[]) {
    Show.inFrame(new ListCombo(),200,200);
  }
} ///:~

最開始的時候,似乎有點兒古怪的一種情況是JLists居然不能自動提供滾動特性——即使那也許正是我們一直所期望的。增加對滾動的支持變得十分容易,就像上面示範的一樣——簡單地將JList封裝到JScrollPane即可,所有的細節都自動地為我們照料到了。

13.19.12 滑桿和進度指示條

滑桿用戶能用一個滑塊的來回移動來輸入數據,在很多情況下顯得很直觀(如聲音控制)。進程條從“空”到“滿”顯示相關數據的狀態,因此用戶得到了一個狀態的透視。我最喜愛的有關這的程序例子簡單地將滑動塊同進程條掛接起來,所以當我們移動滑動塊時,進程條也相應的改變:

//: Progress.java
// Using progress bars and sliders
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;

public class Progress extends JPanel {
  JProgressBar pb = new JProgressBar();
  JSlider sb =
    new JSlider(JSlider.HORIZONTAL, 0, 100, 60);
  public Progress() {
    setLayout(new GridLayout(2,1));
    add(pb);
    sb.setValue(0);
    sb.setPaintTicks(true);
    sb.setMajorTickSpacing(20);
    sb.setMinorTickSpacing(5);
    sb.setBorder(new TitledBorder("Slide Me"));
    pb.setModel(sb.getModel()); // Share model
    add(sb);
  }
  public static void main(String args[]) {
    Show.inFrame(new Progress(),200,150);
  }
} ///:~

JProgressBar十分簡單,但JSlider卻有許多選項,例如方法、大或小的記號標籤。注意增加一個帶標題的邊框是多麼的容易。

13.19.13 樹

使用一個JTree可以簡單地像下面這樣表示:

add(new JTree(
new Object[] {"this", "that", "other"}));

這個程序顯示了一個原始的樹狀物。樹狀物的API是非常巨大的,可是——當然是在Swing中的巨大。它表明我們可以做有關樹狀物的任何事,但更復雜的任務可能需要不少的研究和試驗。幸運的是,在庫中提供了一個妥協:“默認的”樹狀物組件,通常那是我們所需要的。因此大多數的時間我們可以利用這些組件,並且只在特殊的情況下我們需要更深入的研究和理解。

下面的例子使用了“默認”的樹狀物組件在一個程序片中顯示一個樹狀物。當我們按下按鈕時,一個新的子樹就被增加到當前選中的結點下(如果沒有結點被選中,就用根結節):

//: Trees.java
// Simple Swing tree example. Trees can be made
// vastly more complex than this.
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.tree.*;

// Takes an array of Strings and makes the first
// element a node and the rest leaves:
class Branch {
  DefaultMutableTreeNode r;
  public Branch(String[] data) {
    r = new DefaultMutableTreeNode(data[0]);
    for(int i = 1; i < data.length; i++)
      r.add(new DefaultMutableTreeNode(data[i]));
  }
  public DefaultMutableTreeNode node() {
    return r;
  }
}  

public class Trees extends JPanel {
  String[][] data = {
    { "Colors", "Red", "Blue", "Green" },
    { "Flavors", "Tart", "Sweet", "Bland" },
    { "Length", "Short", "Medium", "Long" },
    { "Volume", "High", "Medium", "Low" },
    { "Temperature", "High", "Medium", "Low" },
    { "Intensity", "High", "Medium", "Low" },
  };
  static int i = 0;
  DefaultMutableTreeNode root, child, chosen;
  JTree tree;
  DefaultTreeModel model;
  public Trees() {
    setLayout(new BorderLayout());
    root = new DefaultMutableTreeNode("root");
    tree = new JTree(root);
    // Add it and make it take care of scrolling:
    add(new JScrollPane(tree),
      BorderLayout.CENTER);
    // Capture the tree's model:
    model =(DefaultTreeModel)tree.getModel();
    JButton test = new JButton("Press me");
    test.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        if(i < data.length) {
          child = new Branch(data[i++]).node();
          // What's the last one you clicked?
          chosen = (DefaultMutableTreeNode)
            tree.getLastSelectedPathComponent();
          if(chosen == null) chosen = root;
          // The model will create the
          // appropriate event. In response, the
          // tree will update itself:
          model.insertNodeInto(child, chosen, 0);
          // This puts the new node on the
          // currently chosen node.
        }
      }
    });
    // Change the button's colors:
    test.setBackground(Color.blue);
    test.setForeground(Color.white);
    JPanel p = new JPanel();
    p.add(test);
    add(p, BorderLayout.SOUTH);
  }
  public static void main(String args[]) {
    Show.inFrame(new Trees(),200,500);
  }
} ///:~

最重要的類就是分支,它是一個工具,用來獲取一個字符串數組併為第一個字符串建立一個DefaultMutableTreeNode作為根,其餘在數組中的字符串作為葉。然後node()方法被調用以產生“分支”的根。樹狀物類包括一個來自被製造的分支的二維字符串數組,以及用來統計數組的一個靜態中斷iDefaultMutableTreeNode對象控制這個結節,但在屏幕上表示的是被JTree和它的相關(DefaultTreeModel)模式所控制。注意當JTree被增加到程序片時,它被封裝到JScrollPane中——這就是它全部提供的自動滾動。

JTree通過它自己的模型來控制。當我們修改這個模型時,模型產生一個事件,導致JTree對可以看見的樹狀物完成任何必要的升級。在init()中,模型由調用getModel()方法所捕捉。當按鈕被按下時,一個新的分支被創建了。然後,當前選擇的組件被找到(如果沒有選擇就是根)並且模型的insertNodeInto()方法做所有的改變樹狀物和導致它升級的工作。

大多數的時候,就像上面的例子一樣,程序將給我們在樹狀物中所需要的一切。不過,樹狀物擁有力量去做我們能夠想像到的任何事——在上面的例子中我們到處都可看到“default(默認)”字樣,我們可以取代我們自己的類來獲取不同的動作。但請注意:幾乎所有這些類都有一個具大的接口,因此我們可以花一些時間努力去理解這些錯綜複雜的樹狀物。

13.19.14 表格

和樹狀物一樣,表格在Swing相當的龐大和強大。它們最初有意被設計成以Java數據庫連結(JDBC,在15章有介紹)為媒介的“網格”數據庫接口,並且因此它們擁有的巨大的靈活性,使我們不再感到複雜。無疑,這是足以成為成熟的電子數據表的基礎條件而且可能為整本書提供很好的根據。但是,如果我們理解這個的基礎條件,它同樣可能創建相關的簡單的Jtable

JTable控制數據的顯示方式,但TableModel控制它自己的數據。因此在我們創建JTable前,應先創建一個TableModel。我們可以全部地執行TableModel接口,但它通常從helper類的AbstractTableModel處簡單地繼承:

//: Table.java
// Simple demonstration of JTable
package c13.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.event.*;

// The TableModel controls all the data:
class DataModel extends AbstractTableModel {
  Object[][] data = {
    {"one", "two", "three", "four"},
    {"five", "six", "seven", "eight"},
    {"nine", "ten", "eleven", "twelve"},
  };
  // Prints data when table changes:
  class TML implements TableModelListener {
    public void tableChanged(TableModelEvent e) {
      for(int i = 0; i < data.length; i++) {
        for(int j = 0; j < data[0].length; j++)
          System.out.print(data[i][j] + " ");
        System.out.println();
      }
    }
  }
  DataModel() {
    addTableModelListener(new TML());
  }
  public int getColumnCount() {
    return data[0].length;
  }
  public int getRowCount() {
    return data.length;
  }
  public Object getValueAt(int row, int col) {
    return data[row][col];
  }
  public void
  setValueAt(Object val, int row, int col) {
    data[row][col] = val;
    // Indicate the change has happened:
    fireTableDataChanged();
  }
  public boolean
  isCellEditable(int row, int col) {
    return true;
  }
};       

public class Table extends JPanel {
  public Table() {
    setLayout(new BorderLayout());
    JTable table = new JTable(new DataModel());
    JScrollPane scrollpane =
      JTable.createScrollPaneForTable(table);
    add(scrollpane, BorderLayout.CENTER);
  }
  public static void main(String args[]) {
    Show.inFrame(new Table(),200,200);
  }
} ///:~

DateModel包括一組數據,但我們同樣能從其它的地方得到數據,例如從數據庫中。構造器增加了一個TableModelListener用來在每次表格被改變後打印數組。剩下的方法都遵循Bean的命名規則,並且當JTable需要在DateModel中顯示信息時調用。AbstractTableModel提供了默認的setValueAt()isCellEditable()方法以防止修改這些數據,因此如果我們想修改這些數據,就必須重載這些方法。

一旦我們擁有一個TableModel,我們只需要將它分配給JTable構造器即可。所有有關顯示,編輯和更新的詳細資料將為我們處理。注意這個程序例子同樣將JTable放置在JScrollPane中,這是因為JScrollPane需要一個特殊的JTable方法。

13.19.15 卡片式對話框

在本章的前部,向我們介紹了老式的CardLayout,並且注意到我們怎樣去管理我們所有的卡片開關。有趣的是,有人現在認為這是一種不錯的設計。幸運的是,Swing用JTabbedPane對它進行了修補,由JTabbedPane來處理這些卡片,開關和其它的任何事物。對比CardLayoutJTabbedPane,我們會發現驚人的差異。

下面的程序例子十分的有趣,因為它利用了前面例子的設計。它們都是做為JPanel的派生物來構建的,因此這個程序將安放前面的每個例子到它自己在JTabbedPane的窗格中。我們會看到利用RTTI製造的程序十分的小巧精緻:

//: Tabbed.java
// Using tabbed panes
package c13.swing;
import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;

public class Tabbed extends JPanel {
  static Object[][] q = {
    { "Felix", Borders.class },
    { "The Professor", Buttons.class },
    { "Rock Bottom", ButtonGroups.class },
    { "Theodore", Faces.class },
    { "Simon", Menus.class },
    { "Alvin", Popup.class },
    { "Tom", ListCombo.class },
    { "Jerry", Progress.class },
    { "Bugs", Trees.class },
    { "Daffy", Table.class },
  };
  static JPanel makePanel(Class c) {
    String title = c.getName();
    title = title.substring(
      title.lastIndexOf('.') + 1);
    JPanel sp = null;
    try {
      sp = (JPanel)c.newInstance();
    } catch(Exception e) {
      System.out.println(e);
    }
    sp.setBorder(new TitledBorder(title));
    return sp;
  }
  public Tabbed() {
    setLayout(new BorderLayout());
    JTabbedPane tabbed = new JTabbedPane();
    for(int i = 0; i < q.length; i++)
      tabbed.addTab((String)q[i][0],
        makePanel((Class)q[i][1]));
    add(tabbed, BorderLayout.CENTER);
    tabbed.setSelectedIndex(q.length/2);
  }
  public static void main(String args[]) {
    Show.inFrame(new Tabbed(),460,350);
  }
} ///:~

再者,我們可以注意到使用的數組構造式樣:第一個元素是被置放在卡片上的String,第二個元素是將被顯示在對應窗格上JPanel類。在Tabbed()構造器裡,我們可以看到兩個重要的JTabbedPane方法被使用:addTab()插入一個新的窗格,setSelectedIndex()選擇一個窗格並從它開始。(一個在中間被選中的窗格證明我們不必從第一個窗格開始)。

當我們調用addTab()方法時,我們為它提供卡片的String和一些組件(也就是說,一個AWT組件,而不是一個來自AWT的JComponent)。這個組件會被顯示在窗格中。一旦我們這樣做了,自然而然的就不需要更多管理了——JTabbedPane會為我們處理其它的任何事。

makePanel()方法獲取我們想創建的類Class對象和用newInstance()去創建並轉換為JPanel(當然,假定那些類是必須從JPanel繼承才能增加的類,除非在這一節中為程序例子的結構所使用)。它增加了一個包括類名並返回結果的TitledBorder,以作為一個JPaneladdTab()被使用。

當我們運行程序時,我們會發現如果卡片太多,填滿了一行,JTabbedPane自動地將它們堆積起來。

13.19.16 Swing消息框

開窗的環境通常包含一個標準的信息框集,允許我們很快傳遞消息給用戶或者從用戶那裡捕捉消息。在Swing裡,這些信息窗被包含在JOptionPane裡的。我們有一些不同的可能實現的事件(有一些十分複雜),但有一點,我們必須儘可能的利用static JOptionPane.showMessageDialog() JOptionPane.showConfirmDialog()方法,調用消息對話框和確認對話框。

13.19.17 Swing更多的知識

這一節意味著唯一向我們介紹的是Swing的強大力量和我們的著手處,因此我們能注意到通過庫,我們會感覺到我們的方法何等的簡單。到目前為止,我們已看到的可能足夠滿足我們UI設計需要的一部分。不過,這裡有許多有關Swing額外的情況——它有意成為一全功能的UI設計工具箱。如果我們沒有發現我們所需要的,請到SUN公司的在線文件中去查找,並搜索WEB。這個方法幾乎可以完成我們能想到的任何事。

本節中沒有涉及的一些要點:

  • 更多特殊的組件,例如JColorChooser,JFileChooser,JPasswordField,JHTMLPane(完成簡單的HTML格式化和顯示)以及JTextPane(一個支持格式化,字處理和圖像的文字編輯器)。它們都非常易用。
  • Swing的新的事件類型。在一些方法中,它們看起來像異常:類型非常的重要,名字可以被用來表示除了它們自己之外的任何事物。
  • 新的佈局管理:Springs & Struts以及BoxLayout
  • 分裂控制:一個間隔物式的分裂條,允許我們動態地處理其它組件的位置。
  • JLayeredPaneJInternalFrame被一起用來在當前幀中創建子幀,以產生多文件接口(MDI)應用程序。
  • 可插入的外觀和效果,因此我們可以編寫單個的程序可以像期望的那樣動態地適合不同的平臺和操作系統。
  • 自定義光標。
  • JToolbar API提供的可拖動的浮動工具條。
  • 雙緩存和為平整屏幕重新畫線的自動重畫批次。
  • 內建“取消”支持。
  • 拖放支持。

13.20 總結

對於AWT而言,Java 1.1到Java 1.2最大的改變就是Java中所有的庫。Java 1.0版的AWT曾作為目前見過的最糟糕的一個設計被徹底地批評,並且當它允許我們在創建小巧精緻的程序時,產生的GUI“在所有的平臺上都同樣的平庸”。它與在特殊平臺上本地應用程序開發工具相比也是受到限制的,笨拙的並且也是不友好的。當Java 1.1版納入新的事件模型和Java Beans時,平臺被設置——現在它可以被拖放到可視化的應用程序構建工具中,創建GUI組件。另外,事件模型的設計和Bean無疑對輕鬆的編程和可維護的代碼都非常的在意(這些在Java 1.0 AWT中不那麼的明顯)。但直至GUI組件-JFC/Swing類-顯示工作結束它才這樣。對於Swing組件而言,交叉平臺GUI編程可以變成一種有教育意義的經驗。

現在,唯一的情況是缺乏應用程序構建工具,並且這就是真正的變革的存在之處。微軟的Visual Basic和Visual C++需要它們的應用程序構建工具,同樣的是Borland的Delphi和C++構造器。如果我們需要應用程序構建工具變得更好,我們不得不交叉我們的指針並且希望自動授權機會給我們所需要的。Java是一個開放的環境,因此不但考慮到同其它的應用程序構建環境競爭,而且Java還促進它們的發展。這些工具被認真地使用,它們必須支持Java Beans。這意味著一個平等的應用領域:如果一個更好的應用程序構建工具出現,我們不需要去約束它就可以使用——我們可以採用並移動到新的工具上工作即可,這會提高我們的工作效率。這種競爭的環境對應用程序構建工具來說從未出現過,這種競爭能真正提高程序設計者的工作效率。

13.21 練習

(1)創建一個有文字字段和三個按鈕的程序片。當我們按下每個按鈕時,使不同的文字顯示在文字段中。

(2)增加一個複選框到練習1創建的程序中,捕捉事件,並插入不同的文字到文字字段中。

(3)創建一個程序片並增加所有導致action()被調用的組件,然後捕捉他們的事件並在文字字段中為每個組件顯示一個特定的消息。

(4)增加可以被handleEvent()方法測試事件的組件到練習3中。重載handleEvent()並在文字字段中為每個組件顯示特定的消息。

(5)創建一個有一個按鈕和一個TextField的程序片。編寫一個handleEvent(),以便如果按鈕有焦點,輸入字符到將顯示的TextField中。

(6)創建一個應用程序並將本章所有的組件增加主要的幀,包括菜單和對話框。

(7)修改TextNew.java,以便字母在t2中保持輸入時的樣子,取代自動變成大寫。

(8)修改CardLayout1.java以便它使用Java 1.1的事件模型。

(9)增加Frog.class到本章出現的清單文件中並運行jar以創建一個包括FrogBangBean的JAR文件。現在從SUN公司處下載並安裝BDK或者使用我們自己的可激活Bean的程序構建工具並增加JAR文件到我們的環境中,因此我們可以測試兩個Bean。

(10)創建我們自己的包括兩個屬性:一個布爾值為on,另一個為整型level,稱為Valve的Java Bean。創建一個清單文件,利用jar打包我們的Bean,然後讀入它到beanbox或到我們自己的激活程序構建工具裡,因此我們可以測試它。

(11)修改Menus.java,以便它處理多級菜單。這要假設讀者已經熟悉了HTML的基礎知識。但那些東西並不難理解,而且有一些書和資料可供參考。