作為一個 Java 程序員,從論壇上感受到使用 Java 開發程序的人越來多,心中不免欣慰。但是,同樣是從論壇中,看到多數人提到 Java 就以為是網絡開發——不是這樣的,Java 也可以開發應用程序,而且可以開發出漂亮的圖形用戶界面的應用程序,也就是 Windows/XWindow 應用程序。因此,我寫下這篇文章,希望能帶你進入Java 圖形用戶界面設計之門。
一. AWT 和 SWING
AWT 和 SWING 是 Java 設計 GUI 用戶界面的基礎。與 AWT 的重量級組件不同,Swing 中大部分是輕量級組件。正是這個原因,Swing 幾乎無所不能,不但有各式各樣先進的組件,而且更為美觀易用。所以一開始使用 AWT 的程序員很快就轉向使用 Swing 了。
那為什麼 AWT 組件沒有消亡呢?因為 Swing 是架構在 AWT 之上的,沒有 AWT 就沒有 Swing。所以程序員可以根據自己的習慣選擇使用 AWT 或者是 Swing。但是,最好不要二者混用——除開顯示風格不同不說,還很可能造成層次 (Z-Order) 錯亂,比如下例:
/**
* TestPanels.java
* @author Fancy
*/
import javax.swing.*;
import java.awt.*;
public class TestPanels extends JFrame {
public TestPanels() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
JPanel panel = new JPanel();
for (int i = 0; i < 2; i++) {
panel.add(new JButton("Button 00" + i));
}
JTextArea textArea = new JTextArea(5, 15);
textArea.setLineWrap(true);
JScrollPane scrollPane = new JScrollPane(textArea);
getContentPane().add(panel, BorderLayout.NORTH);
getContentPane().add(scrollPane, BorderLayout.CENTER);
pack();
}
public static void main(String[] args) {
TestPanels tp = new TestPanels();
tp.show();
}
}
運行這個程序,並用鼠標拖動那個名為“cover”的子窗口,我們會發現一個非常有趣的現象,如圖:
顯然 cover 子窗口是在 controls 子窗口之上的,但是它只罩蓋住了 Swing Button,沒有罩蓋住 AWT Button。再看一會兒,你是不是有這樣一種感覺:Swing Button 是“畫”上去的,而 AWT Button 則是“貼”上去的。這就是二者混用造成層次錯亂的一個例子。
Swing 組件有美觀、易用、組件量大等特點,也有缺點——使用 Swing 組件的程序通常會比使用 AWT 組件的程序運行更慢。但是大家都還是更喜歡用 Swing 組件,原因何在?因為隨著計算機硬件的升級,一點點速度已經不是問題。相反的,用戶更需要美觀的用戶界面,開發人員則更需要易用的開發組件。
——好,我這就來教你使用 Swing 組件開發圖形用戶界面的 Java 應用程序。 二. 框架、監聽器和事件
框架 (Frame) 是 Java 圖形用戶界面的基礎,它就是我們通常所說的窗口,是 Windows/XWindow 應用程序的典型特征。說到 Windows/XWindow,大家很輕易聯想到“事件 (Event) 驅動”。Java 的圖形用戶界面正是事件驅動的,並且由各種各樣的監聽器 (Listener) 負責捕捉各種事件。
假如我們需要對某一個組件的某種事件進行捕捉和處理時,就需要為其添加監聽器。比如,我們要在一個窗口 (JFrame) 激活時改變它的標題,我們就需要為這個窗口 (JFrame 對象) 添加一個可以監聽到“激活窗口”這一事件的監聽器——WindowListener。
怎麼添加監聽器呢?這通常由組件類提供的一個 addXXXXXListener 的方法來完成。比如 JFrame 就提供有 addWindowListener 方法添加窗口監聽器 (WindowListener)。
一個監聽器經常不只監聽一個事件,而是可以監聽相關的多個事件。比如 WindowListener 除了監聽窗口激活事件 (windowActivate) 之外,還可以監聽窗口關閉事件 (windowClosing) 等。那麼這些事件怎麼區分呢?就靠重載監聽器類 (Class) 的多個方法 (Method) 了,監聽器監聽到某個事件後,會自動調用相關的方法。我們只要重載這個方法,就可以處理相應的事件了。
不妨先看一個例子:
/**
* TestFrame.java
* @author Fancy
*/
import javax.swing.*;
import java.awt.event.*;
public class TestFrame extends JFrame {
private int counter = 0;
public TestFrame() {
/* 使用匿名類添加一個窗口監聽器 */
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.out.println("Exit when Closed event");
System.exit(0); //退出應用程序
}
public void windowActivated(WindowEvent e) {
setTitle("Test Frame " + counter++); // 改變窗口標題
}
});
setResizable(false); // 設置窗口為固定大小
setSize(200, 150);
}
public static void main(String[] args) {
TestFrame tf = new TestFrame();
tf.show();
}
}
這個例子中,我們設計了一個窗口類(public class TestFrame extends JFrame { ...),並且為這個窗口添加了一個窗口監聽器 (addWindowListener(new WindowAdapter() ...)。而我們添加的這個窗口監聽器主要監聽了兩個事件:窗口關閉 (public void windowClosing(WindowEvent e) ...) 和窗口激活 (public void windowActivated(WindowEvent e) ...)。在窗口關閉事件中我們退出了整個應用程序(System.exit(0);),而在窗口激活事件中,我們改變了窗口的標題 (setTitle("Test Frame " + counter++);)。最後,我們在 main 方法中顯示了這窗口類的一個實例,運行得到下圖所示的結果:
這個程序的運行結果就是一個什麼東西都沒有加的框架,也就是一個空窗口。那麼,你知道顯示一個窗口最主要的幾句代碼嗎?不知道沒關系,我來告訴你,顯示一個窗口只需要做三件事:生成實例(對象) -> 設置大小 -> 顯示,相應的,就是下面的三句代碼:
JFrame frame = new JFrame("Frame's Title");
frame.setSize(400, 300);
frame.show();
也許你會說:第一句的意思我清楚,第三句的意思我也明白,為什麼一定要第二句呢?其實想想也就明白了,叫你畫一個沒法有大小的矩形你能畫出來嗎?不能。同樣,沒有大小的窗口,怎麼顯示?所以我們需要用 setSize(int width, int height) 方法為其設置大小。我們還有另一種方法:用 JFrame 的 pack() 方法讓它自己適配一個大小。pack() 在多數時候是令人滿足的,但有時,它也會讓你哭笑不得——多試試就知道了。
在 JFrame 中,我們使用 addWindowListener 方法加入一個監聽器 WindowListener (addWindowListener(new WindowAdapter() ...) 去監聽發生在 JFrame 上的窗口事件。WindowListener 是一個接口,在 java.awt.event 這個包中,但是上例中好象並沒有使用 WindowListener,而是使用的 WindowsAdapter 吧,這是怎麼回事?
WindowAdapter 是 WindowsListener 接口的一個最簡單的實現,也在包 java.awt.event 中。假如我們直接使用 WindowListener 產生一個匿名類,需要實現它的每一個方法 (一共 7 個)。但 WindowAdapter 作為 WindowListener 最簡單的實現,已經實現了它的每一個方法為空方法 (即只包含空語句,或者說沒有語句的方法)。用 WindowAdapter 就只需要重載可能用到的方法 (上例中只有 2 個) 就行了,而不需要再去實現每一個方法。優點顯而易見——減少代碼量。
在 JFrame 上發生的窗口事件 (WindowEvent) 包括:
windowActivated(WindowEvent e) 窗口得到焦點時觸發
windowClosed(WindowEvent e) 窗口關閉之後觸發
windowClosing(WindowEvent e) 窗口關閉時觸發
windowDeactivated(WindowEvent e) 窗口失去焦點時觸發
windowDeiconified(WindowEvent e)
windowIconified(WindowEvent e)
windowOpened(WindowEvent e) 窗口打開之後觸發
上例重載了其中兩個方法。假如在上例運行產生的窗口和另外一個應用程序窗口之間往返切換 (在 Windows 操作系統中你可以使用 Alt+Tab 進行切換)……試試看,你發現了什麼?有沒有現我們的示例窗口標題上的數字一直在增加,這便是在 windowActivated 事件中 setTitle("Test Frame " + counter++); 的功勞。
而另一個事件處理函數 windowClosing 中的 System.exit(0) 則保證了當窗口被關閉時退出當前的 Java 應用程序。假如不作這樣的處理會怎樣呢?試驗之後你會發現,窗口雖然關閉了,但程序並沒有結束,但此時,除了使用 ^C 強行結束之外,恐怕也沒有其它辦法了。所以,這一點非常重要:假如你想在關閉窗口的時候退出應用程序,需要你自己寫代碼處理 windowClosing 事件。……也不盡然,其實還有另外一個更簡單的辦法,讓 JFrame 自己處理這件事——你只需要如下調用 JFrame 的 setDefaultCloseOperation 即可:
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
我們可以在 JFrame 對象中添加 AWT 或者 Swing 組件。但是,雖然它有 add 方法,卻不能直接用於添加組件,否則會拋出異常——不信就試試。造成這個現象的原因只有一個解釋:JFrame 不是一個容器,它只是一個框架。那麼,應該怎麼添加組件呢?
JFrame 有一個 Content Pane,窗口是顯示的所有組件都是添加在這個 Content Pane 中。JFrame 提供了兩個方法:getContentPane 和 setContentPane 就是用於獲取和設置其 Content Pane 的。通常我們不需要重新設置 JFrame 的 Content Pane,只需要直接獲取這個 Content Pane 來添加組件等。如:(new JFrame()).getContentPane().add(new Button("test button")); 三. 按鈕、切換按鈕、復選按鈕和單選按鈕
按鈕,就是按鈕,不會連按鈕都不知道吧?
切換按鈕,有兩種狀態的按鈕,即按下狀態和彈起狀態,若稱為選擇狀態或未選擇狀態。
復選按鈕,又叫復選框,用一個小方框中是否打勾來表示兩種狀態。
單選按鈕,又叫收音機按鈕,以小圓框打點表示被選中。常成組出現,一組單選按鈕中只有一個能被選中。
發現什麼了嗎?——對了,這一部分是在講各種各樣的按鈕,而且後三種按鈕都有兩種狀態。先看看這些按鈕都長成什麼樣:
上圖中,從上到下,依次就是按鈕、切換按鈕、復選按鈕和單選按鈕。圖示的窗口,就是下面這個例子的運行結果:
/**
* TestButtons.java
* @author Fancy
*/
import javax.swing.*;
import java.awt.event.*;
public class TestButtons {
JFrame frame = new JFrame("Test Buttons");
JButton jButton = new JButton("JButton"); //按鈕
JToggleButton toggle = new JToggleButton("Toggle Button"); //切換按鈕
JCheckBox checkBox = new JCheckBox("Check Box"); //復選按鈕
JRadioButton radio1 = new JRadioButton("Radio Button 1"); //單選按鈕
JRadioButton radio2 = new JRadioButton("Radio Button 2");
JRadioButton radio3 = new JRadioButton("Radio Button 3");
JLabel label = new JLabel("Here is Status, look here."); //不是按鈕,是靜態文本
public TestButtons() {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().setLayout(new java.awt.FlowLayout());
/* 為一般按鈕添加動作監聽器 */
jButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
label.setText("You clicked jButton");
}
});
/* 為切換按鈕添加動作監聽器 */
toggle.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
JToggleButton toggle = (JToggleButton) ae.getSource();
if (toggle.isSelected()) {
label.setText("You selected Toggle Button");
} else {
label.setText("You deselected Toggle Button");
}
}
});
/* 為復選按鈕添加條目監聽器 */
checkBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
JCheckBox cb = (JCheckBox) e.getSource();
label.setText("Selected Check Box is " + cb.isSelected());
}
});
/* 用一個按鈕組對象包容一組單選按鈕 */
ButtonGroup group = new ButtonGroup();
/* 生成一個新的動作監聽器對象,備用 */
ActionListener al = new ActionListener() {
public void actionPerformed(ActionEvent ae) {
JRadioButton radio = (JRadioButton) ae.getSource();
if (radio == radio1) {
label.setText("You selected Radio Button 1");
} else if (radio == radio2) {
label.setText("You selected Radio Button 2");
} else {
label.setText("You selected Radio Button 3");
}
}
};
/* 為各單選按鈕添加動作監聽器 */
radio1.addActionListener(al);
radio2.addActionListener(al);
radio3.addActionListener(al);
/* 將單選按鈕添加到按鈕組中 */
group.add(radio1);
group.add(radio2);
group.add(radio3);
frame.getContentPane().add(jButton);
frame.getContentPane().add(toggle);
frame.getContentPane().add(checkBox);
frame.getContentPane().add(radio1);
frame.getContentPane().add(radio2);
frame.getContentPane().add(radio3);
frame.getContentPane().add(label);
frame.setSize(200, 250);
}
public void show() {
frame.show();
}
public static void main(String[] args) {
TestButtons tb = new TestButtons();
tb.show();
}
}
除一般按鈕外,其余三種按鈕都有兩種狀態,即選擇 (按下) 狀態和未選擇 (彈起) 狀態。那麼我們又該如何判定呢?切換按鈕 (JToggleButton) 提供了一個 isSelected() 方法用來判定當前所處的狀態,返回值為真 (true) 時表示它處於選擇狀態,返回值為假 (false) 時表示它處於未選擇狀態。而復選按鈕 (JCheckBox) 和單選按鈕 (JRadioButton) 都是從 JToggleButton 繼續的,所以也具有 isSelected() 方法。如上例中 if (toggle.isSelected()) { ... 等。
單選按鈕由自身的特點決定了它們必須成組出現,而且一組中只能有一個能被選中。因此我們需要用一個專門的類——ButtonGroup——來治理。添加到 ButtonGroup 的多個單選按鈕中,假如有一個被選擇中,同組中的其它單選按鈕都會自動改變其狀態為未選擇狀態。在 ButtonGroup 中添加按鈕,是使用它的 add 方法,如上例中的 group.add(radio1);。
既然我們已經將多個單選按鈕添加到一個 ButtonGroup 中了,那麼我們是不是可以將一個包含多個單選按鈕的 ButtonGroup 對象添加到 JFrame 的 Content Pane 中,以達到添加其中所有單選按鈕的目的呢?不行!ButtonGroup 不是一個可顯示的組件,它僅用於治理。所以,在往 JFrame 中添加一組 JRadioButton 的時候,需要一個一個的添加 JRadioButton,而不是籠統的添加一個 ButtonGroup。
上例中還用到了 JLabel,這不是按鈕,而是一個靜態文本組件,主要用於顯示提示文本。要獲得一個 JLabel 對象當前顯示的文本內容,可以使用它的 getText() 方法;反之,要改變一個 JLabel 對象顯示的文本內容,應該使用它的 setText(String text) 方法,如上例中的 label.setText("You selected Toggle Button");。
其實這兩個方法同樣可以用於 JButton 等類。比如上例中我們使用 new JButton("JButton") 構造了一個按鈕 jButton,假如使用 jButton.getText() 就可以得到字符串 "JButton"。而 jButton.setText("A Button"),則可以改變按鈕上顯示的文字為 "A Button"。這兩句代碼沒有寫出來,你可以自己試試。
上例中大量使用了動作監聽器 (ActionListener)。ActionListener 只監聽一個事件,這個事件在其相關組件上產生了動作時被觸發,因此叫作動作事件 (ActionEvent)。ActionListener 只有一個方法需要實現,就是 actionPerformed(ActionEvent ae)。按鈕、切換按鈕和單選按鈕被單擊時都會觸發動作事件,引起動作監聽器調用 actionPerformed 方法。因此,假如你想在單擊按鈕之後做什麼事,當然應該重載 ActionListener 的 actionPerformed 方法了。各種按鈕都提供了 addActionListener 方法以添加動作監聽器。
復選框就要非凡一些。雖然它也有 addActionListener 方法,意味著可以使用動作監聽器,但是使用之後你會發現動作監聽器並沒有起到預想的作用。為什麼?原來,單擊一個復選按鈕,觸發的不是動作事件,而是條目事件 (ItemEvent) 中的狀態變化 (itemStateChanged) 事件,由條目監聽器 (ItemListener) 監聽,相應需要重載的方法是 ItemListener 的 itemStateChanged 方法。
上例中我們將一個名為 al 的 ActionListener 添加到了每一個單選按鈕中,如何判定是哪個單選按鈕觸發了事件並被 al 監聽到了呢?我們可以從 ActionEvent 的 getSource() 方法得到觸發事件單選按鈕。由於 getSource() 返回的是一個 Object 引用,雖然這個引用指向的是一個單選按鈕的實例,但我們還是需要將這個引用的類型轉換為 JRadioButton,如上例中的:JRadioButton radio = (JRadioButton) ae.getSource();,只有這樣我們才能調用 JRadioButton 有而 Object 沒有的方法。
同時,還需要說明的一點是,每個單選按鈕都可以添加一個單獨的 ActionListener 實例,而不一定要添加同一個。同樣的道理,若干毫不相干的、需要添加 ActionListener 的若干組件,也可以添加同一個 ActionListener 實例。要害在於編程者對 actionPerformed 方法的重載。比如下面這段代碼就為一個 JButton 對象和一個 JRadioButton 對象添加了同一個動作監聽器實例:
/**
* Test.java
* @author Fancy
*/
import javax.swing.*;
import java.awt.event.*;
public class Test {
JButton b;
JRadioButton rb;
public Test() {
JFrame f = new JFrame("Test");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().setLayout(new java.awt.FlowLayout());
b = new JButton("JButton");
rb = new JRadioButton("RadioButton");
ActionListener a = new ActionListener() {
public void actionPerformed(ActionEvent ae) {
if (ae.getSource() == b) {
System.out.println("You clicked the JButton");
} else {
System.out.println("You clicked the RadioButton");
}
}
};
b.addActionListener(a);
rb.addActionListener(a);
f.getContentPane().add(b);
f.getContentPane().add(rb);
f.pack();
f.show();
}
public static void main(String[] args) {
new Test();
}
}
運行程序後,分別單擊兩個按鈕,相應的,在控制台能分別得到如下輸出:
You clicked the JButton
You clicked the RadioButton
這說明多個不用的組件添加同一個監聽器是可行的——不過前提是這些組件都能添加這個監聽器。 四. 文本輸入框、密碼輸入框
文本輸入框包括兩種,單行文本輸入框 (JTextField) 和多行文本輸入框 (JTextArea)。密碼輸入框則只有一種 (JPassWordField)。JPasswordField 是 JTextField 的子類,它們的主要區別是 JPasswordField 不會顯示出用戶輸入的東西,而只會顯示出程序員設定的一個固定字符,比如 '*'。
下面的示例圖和代碼是 JTextField、JPasswordField 和 JTextArea 的示例:
/**
* TestTexts.java
* @author Fancy
*/
import javax.swing.*;
import javax.swing.event.*;
public class TestTexts extends JFrame {
private JLabel label = new JLabel("Status");
private JTextField textField;
private JPasswordField pwdField;
private JTextArea textArea;
public TestTexts() {
super("Test Texts");
setDefaultCloseOperation(EXIT_ON_CLOSE);
getContentPane().setLayout(new java.awt.FlowLayout());
textField = new JTextField(15);
/* 監聽文本光標移動事件 */
textField.addCaretListener(new CaretListener() {
public void caretUpdate(CaretEvent e) {
// 假如改變了內容,就可以即時更新 label 顯示的內容
label.setText(textField.getText());
}
});
pwdField = new JPasswordField(15);
pwdField.setEchoChar('#');
textArea = new JTextArea(5, 15);
textArea.setLineWrap(true);
getContentPane().add(textField);
getContentPane().add(pwdField);
getContentPane().add(textArea);
getContentPane().add(label);
setSize(200, 200);
}
public static void main(String[] args) {
TestTexts tt = new TestTexts();
tt.show();
}
}
上例中,我們構造了一個寬度為 15 個字符的單行文本框 (textField = new JTextField(15);),並使用 addCaretListener 方法添加了一個 CaretListener (textField.addCaretListener ...)。CaretListener 監聽文本光標的移動事件。當用戶使用鍵盤、鼠標等移動了文本光標在 JTextField 中的位置時觸發這個事件。我們需要重載 caretUpdate(CaretEvent e) 對事件進行處理 (public void caretUpdate(CaretEvent e) ...)。這樣,我們可以在這裡做類似 VB 中 TextBox 的 OnChange 事件中做的事情。
JTextField 有 5 個構造方法,常用其中的四個:
JTextField()
JTextField(int columns),如上例 textField = new JTextField(15);
JTextField(String text)
JTextField(String text, int columns)
其中,參數 text 是單行文本框的初始內容,而 columns 指定了單行文本框的寬度,以字符為單位。JTextField 中的文本內容可以用 getText() 方法獲得。也可以用 setText 方法指定 JTextField 中的文本內容。
JPasswordField 是 JTextField 的子類,其構造方法也是類似的。JPasswordField 提供了 setEchoChar(char ch) 方法設置為了隱藏密碼而顯示的字符,默認為 '*' 字符,上例中則設置為了 '#' 字符 (pwdField.setEchoChar('#');)。與 JTextField 一樣,JPasswordField 也用 getText 方法和 setText 獲得或者設置文本內容 (當然在用戶界面上是隱藏的)。
JTextField 是單行文本框,不能顯示多行文本,假如想要顯示多行文本,就只好使用多行文本框 JTextArea 了。JTextArea 有六個構造方法,常用的也是四個:
JTextArea()
JTextArea(int rows, int columns)
JTextArea(String text)
JTextArea(String text, int rows, int columns)
text 為 JTextArea 的初始化文本內容;rows 為 JTextArea 的高度,以行為單位;columns 為 JTextArea 的寬度,以字符為單位。如上例中就構造了一個高 5 行,寬 15 個字符的多行文本框 (textArea = new JTextArea(5, 15);)。
多行文本框默認是不會自動折行的 (不過可以輸入回車符換行),我們可以使用 JTextArea 的 setLineWrap 方法設置是否答應自動折行。setLineWrap(true) 是答應自動折行,setLineWrap(false) 則是不答應自動折行。多行文本框會根據用戶輸入的內容自動擴展大小,不信,自己做個實驗——假如不自動折行,那麼多行文本框的寬度由最長的一行文字確定的;假如行數據超過了預設的行數,則多行文本框會擴展自身的高度去適應。換句話說,多行文本框不會自動產生滾動條。怎麼辦?後面講到滾動窗格 (JScrollPane) 的時候,你就知道了。
多行文本框裡文本內容的獲得和設置,同樣可以使用 getText 和 setText 兩個方法來完成。 五. 窗格、滾動窗格和布局治理
窗格 (JPanel) 和滾動窗格 (JScrollPane) 在圖形用戶界面設計中大量用於各種組件在窗口上的布置和安排。這裡所謂的布置和安排,就是布局 (Layout),因此不得不先說說布局。
將加入到容器(通常為窗口等) 的組件按照一定的順序和規則放置,使之看起來更美觀,這就是布局。布局由布局治理器 (Layout Manager) 來治理。那麼,我們在什麼時候應該使用布局治理器?應用選擇哪種布局治理器?又該怎樣使用布局治理器呢?
往往,我們設計一個窗口,其中是要添加若干組件的。為了治理好這些治理的布局,我們就要使用布局治理器。比如說,設計一個簡單的編輯器,這個編輯器中只需要放置兩個按鈕和一個多行文本框。這些組件是讓 Java 自己任意安排呢?還是按照一定的位置關系較規范的安排呢?當然應該選擇後者。那麼,為了按照一定的位置關系安排這些組件,我們就需要用到布局治理器了。
然後我們碰到了一個選擇題——使用哪種布局治理器。為此,我們首先要知道有些什麼布局治理器,它們的布局特點是什麼。常用的布局治理器有: FlowLayout、BorderLayout、GridLayout、BoxLayout 等,其中 FlowLayout 和 BorderLayout 最常用,本文主要也就只談談這兩種布局治理器。下面列表說明它們的布局特點:
布局治理器布局特點 FlowLayout 將組件按從左到右從上到下的順序依次排列,一行不能放完則折到下一行繼續放置 BorderLayout 將組件按東、南、西、北、中五個區域放置,每個方向最多只能放置一個組件 GridLayout 形似一個無框線的表格,每個單元格中放一個組件 BoxLayout 就像整潔放置的一行或者一列盒子,每個盒子中一個組件
就上述的編輯器為例,假如選用 FlowLayout,那麼兩個按鈕和一個多行文本框就會排列在一行——當然這是窗口足夠寬的情況;假如窗口稍窄一些,則可能分兩行排列,第一行有兩個按鈕,而第二行是多行文本框——這是最理想的情況;假如窗口再窄一些,就可能分三行排列,第一行和第二行分別放置一個按鈕,第三行放置多行文本框。因此,假如窗口大小可以改變,那麼三個組件的位置關系也可能隨著窗口大小的變化而變化。所以,FlowLayout 不適用。其實上面所舉的例程中,大部分都是用的 FlowLayout,那是因為我們沒有要求組件的布局。
假如選用 BorderLayout 的情況又如何呢?我們可以試著加入一個窗格 (JPanel,稍後講解),並將兩個按鈕放置在其中,然後將這個窗格加入到 BorderLayout 的北部 (即上部);再將多行文本框加入到 BorderLayout 中部。結果類似使用 FlowLayout 的第二種可能,是最理想的情況。而且,假如改變窗口大小,它們的位置關系仍然是北-中的關系,不會隨之改變。
剩下的兩種布局治理器,加以窗格 (JPanel) 的配合,也能夠很好的安排上述編輯器所需的三個組件。但是由於它們的使用稍為復雜一些,所以就不講了。下面就講講如何使用 FlowLayout 和 BorderLayout。
任何布局治理器,都需要用在容器上,比如 JFrame 的 Content Pane 和下面要說的 JPanel 都是容器。容器組件提供了一個 setLayout 方法,就是用來改變其布局治理器的。默認情況下,JFrame 的 Content Pane 使用的是 BorderLayout,而 JPanel 使用的是 FlowLayout。但不管怎樣,我們都可以調用它們的 setLayout 方法來改變其布局治理器。比如上述的編輯器中,我們要讓窗口 (JFrame 對象,假設為 frame) 使用 BorderLayout,就可以使用 frame.getContentPane().setLayout(new BorderLayout()); 來改變其布局治理器為一個新的 BorderLayout 對象。
然後,我們對布局治理器的直接操作就結束了,剩下的只需要往容器裡添加組件。假如使用 FlowLayout,我們只需要使用容器的 add(Component c) 方法添加組件就行了。但是,假如使用 BorderLayout 就不一樣了,因為要指定是把組件添加到哪個區域啊。那我們就使用容器的 add(Component c, Object o) 方法添加組件,該方法的第二個參數就是指明添加到的區域用的。例如,上述編輯器中要添加一個多行文本框到 BorderLayout 的中部,就可以用 frame.getContentPane().add(new JTextArea(5, 15), BorderLayout.CENTER) 來實現。
BorderLayout 的五個區域分別是用下列五個常量來描述的:
BorderLayout.EAST 東
BorderLayout.SOUTH 南
BorderLayout.WEST 西
BorderLayout.NORTH 北
BorderLayout.CENTER 中
剛才已經提到了使用 JPanel。JPanel 作為一個容器,可以包容一些組件,然後將這個 JPanel 對象作為一個組件添加到另一個容器 (稱作父容器) 中。這個功能有什麼好處呢?
上面不是提到 BorderLayout 的一個區域中只能添加一個組件嗎?但是我們的編輯器需要添加兩個按鈕到它的北部,怎麼辦?上面的例子中,我們就是用的一個 JPanel 包容了這兩個按鈕,然後再將這個 JPanel 對象作為一個組件添加到設置布局治理器為 BorderLayout 的 Content Pane 中。
上面說到各布局治理器的布局特點的時候,幾乎每一種都是一個區域只能添加一個組件,那我們想添加多個組件到一個區域的時候,就要用到 JPanel 了。假如還沒有明白,稍後看一段程序可能更易於理解。
而滾動窗格 (JScrollPane) 呢?它是一個能夠自己產生滾動條的容器,通常只包容一個組件,並且根據這個組件的大小自動產生滾動條。比如上面講 JTextArea 的時候提到:JTextAera 會隨用戶輸入的內容自動擴展大小,很輕易打破各組件的布局。但是,假如我們將它包容在一個滾動窗格中,它的擴展就不會直接反映在大小的變化上,而會反映在滾動窗格的滾動條上,也就不會打破各組件的布局了。稍後的例子會讓你清清楚楚。
是不是等著看例子了?好,例子來了:
/**
* TestPanels.java
* @author Fancy */
import javax.swing.*;
import java.awt.*;
public class TestPanels extends JFrame {
public TestPanels() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
JPanel panel = new JPanel();
for (int i = 0; i < 2; i++) {
panel.add(new JButton("Button 00" + i));
}
JTextArea textArea = new JTextArea(5, 15);
textArea.setLineWrap(true);
JScrollPane scrollPane = new JScrollPane(textArea);
getContentPane().add(panel, BorderLayout.NORTH);
getContentPane().add(scrollPane, BorderLayout.CENTER);
pack();
}
public static void main(String[] args) {
TestPanels tp = new TestPanels();
tp.show();
}
}
這個例子的運行結果如下圖,正是我們想要的結果——上面兩個按鈕,下面是一個可以滾動的多行文本框:
上例中首先產生了一個 JPanel 對象 (JPanel panel = new JPanel();),然後將兩個按鈕置於其中 (panel.add ...);然後產生了一個多行文本框 (JTextArea textArea = new JTextArea(5, 15);),並使用一個滾動窗格將它包裹起來 (JScrollPane scrollPane = new JScrollPane(textArea);),使之成為可以滾動的多行文本框。最後將兩個容器 (JPanel 對象和 JScrollPane 對象) 分別添加到了窗口的北部 (getContentPane().add(panel, BorderLayout.NORTH);) 和中部 (也就是剩余部分,getContentPane().add(scrollPane, BorderLayout.CENTER);)。
似乎有點不對勁,是什麼呢?對了,我們沒有設置 Content Pane 的布局治理器為 BorderLayout 啊,為什麼……剛才不是說了嗎,JFrame 的 Content Pane 的默認布局治理器就是 BorderLayout,所以不用再設置了。
好了,《Java 的圖形用戶界面設計》就告一段落了。由於篇幅有限,這裡說的都是初級知識,有此基礎,設計復雜一點的圖形用戶界面也就不是難事了!