2014年2月13日 星期四

Java Swing 測試 : JFrame

我從 2003 年 Java 1.2 版開始棄硬從軟學習程式設計迄今已十年矣, 當時買的第一本書是文魁出版的 "精通 Java 2 (林建銘, 高明揚)", 記得那年暑假剛考上研究所, 去報到排隊時還抱著這本書津津有味地猛啃, 新認識的同學很好奇, 英文研究所的學生為啥看 programming 的書? Java 可以說是我有從入門開始認真學習的語言了.

Java 圖形介面 Swing 大概玩了兩三年, 但自從玩網頁後就越來越少碰 Java, 久了真的就不會用了. 2009 年時曾經整理了兩份 Java 的操典 : Java Swing (47 頁) 與 Java Core (15 頁), 昨天找出來後重新列印, 要好好來複習測試一番.

Swing 是 Java 的輕量級 GUI 類別庫, 除了 java.awt.Window 的子類別如 JFrame 等外, 完全以 Java 設計, 不會被作業系統本身複雜的 GUI 功能綁住, 因此效能較重量級的 AWT 優越, 介面也較美觀, 還可以更換主題 (pluggable), 非常有彈性.

玩 Swing 首先要匯入 java.awt 與 javax.swing 這兩個類別庫, 加上各自的 event 事件處理類別庫共 4 項  :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

Swing 套件收容在 javax.swing 下, 為何又還要匯入 AWT 套件呢? 這是因為許多圖形介面功能像 layout 與 event 還是要承襲 awt 的實作之故. Swing 主要的容器是 JFrame 類別, 它提供一個視窗可用來擺放 GUI 元件. 注意, import 中使用 * 只匯入該路徑下有用到之類別, 不包括子目錄下的類別, 亦即匯入 javax.swing.* 並不會匯入 javax.swing.event.* 下的類別.

建立一個 JFrame 視窗的方法只需要三個步驟 :
  1. 建立 JFrame 物件
  2. 設定視窗大小
  3. 顯示視窗 
JFrame 的寫法基本上有兩種, 一是在類別定義中調用 JFrame 類別來建立物件, 二是直接繼承 JFrame 類別 (參考 : 為何在寫JFrame時 都要用class extends), 下列範例 1 用的是第一種方法 : 

測試範例 1 : http://tony1966.16mb.com/javatest/JFrame1.class [看原始碼]  

public class JFrame1 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame1();
    }
  public JFrame1() {
    f=new JFrame("JFrame 1");
    f.setBounds(0,0,400,300);
    f.setVisible(true);
    }
  }


建立視窗物件時只要把視窗標題傳進去就可以了. 當然也可以不傳任何參數, 在用 setTtile() 方法去設定標題, 只是能用一列解決的, 就不要用兩列. 注意, 如果沒有用 setBounds() 設定大小, 執行後你將看不到這個 JFrame 視窗. Java 是物件導向語言, 其主方法 main() 是作為程式執行的入口, 因此只要將類別實體化即呼叫建構子開始執行程式. 主要的邏輯是放在建構子. 另外要特別注意, 顯示視窗的 setVisible() 方法必須放在全部元件都放進 ContentPane 之後, 通常是最後一個指令, 否則視窗剛開啟時可能看不到這些元件, 要等視窗縮放之後才會顯現 (因為縮放時內容面板會重新渲染 repaint).

JFrame 的 setBounds(x, y, w, h) 方法用來設定視窗左上角座標 (x, y) 與大小 (w, h). 設為 (0, 0, 400, 300) 表示此長 300px 寬 400px 的視窗會對齊螢幕左上角. 也可以只用 setSize(w, h) 方法, 其預設左上角座標為 (0, 0). 事實上 setBounds() 方法可以看成是 setLocation(x, y) 與 setSize(w, h) 的合體, setLocation() 用來設定左上角座標, 而 setSize() 則是負責設定大小.

點選上面 JFrame1.class 超連結下載類別檔後, 開啟命令提示字元視窗, 用下列指令執行 (但必須先安裝 JRE 才能執行) :

D:\javatest\java JFrame1

如果要用原始碼自行編譯, 則必須安裝 JDK :

D:\javatest\javac JFrame1.java

這樣就會編譯出 JFrame1.class 的 byte code 了, 這才是 Java 虛擬機器要執行的對象. Java 已完全國際化 (i18n), 因此即使程式中顯示用的字串是中文, 程式以 ANSI 編碼格式存檔, 都可以直接用 javac 編譯成功. 但如果以 UTF-8 格式存檔, 則必須加上參數才行, 否則會出現莫名其妙的錯誤報告 :

D:\javatest\javac -encoding utf-8 JFrame1.java

上面的 JFrame 寫法是在我們所定義的 JFrame1 中調用 JFrame 類別, 也可以直接繼承 JFrame 來寫, 如下面範例 1-1 所示 :

測試範例 1-1 : http://tony1966.16mb.com/javatest/JFrame1_1.class [看原始碼]  

public class JFrame1_1 extends JFrame {
  public static void main(String argv[]) {    
    new JFrame1_1("JFrame 1_1");
    }
  public JFrame1_1(String title) {
    this.setBounds(0,0,400,300);
    this.setTitle(title);
    this.setVisible(true);
    }
  }

上例與範例 1 不同的地方是, 由於在 main() 中實體化類別時即建立了 JFrame 物件, 因此不可再 new JFrame() 了; 其次在建構子裡, 要用表示 JFrame 物件本身的 this 來呼叫其方法, 不過這 this 不是必要的, 直接呼叫 JFrame 的方法即可 :

  public JFrame1_1(String title) {
    setBounds(0,0,400,300);
    setTitle(title);
    setVisible(true);
    }

以上兩種寫法皆可, 如果欲在 JFrame 基礎上進行擴充, 例如新增或覆寫方法等, 使用 extends JFrame 較適合; 否則使用第一種寫法即可, 特別是要繼承其他類別 (例如繼承 Adapter 類別進行事件處理時), 由於 Java 僅支持單一繼承, 就不能繼承 JFrame 了.

JFrame 視窗預設套用作業系統的視覺風格 (Look and Feel), 範例 1 的圖是在 Win7 執行的結果. 這有一個缺點, 在不同作業系統執行時, 外觀會不一樣, 使用者的視覺經驗會不統一. 其實 Swing 提供好幾種視覺風格, 如果要套用 Swing 自己的 Metal 主題外觀, 必須加上下列指令將 Swing 設為預設風格 (即 Metal, 見下面範例 8) :

JFrame.setDefaultLookAndFeelDecorated(true);

如下列範例 2 所示 :

測試範例 2 : http://tony1966.16mb.com/javatest/JFrame2.class [看原始碼] 

public class JFrame2 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame2();
    }
  public JFrame2() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JFrame 2");
    f.setBounds(0,0,400,300);
    f.setVisible(true);
    }
  }


這樣就漂亮多了, 而且不管是在 MacOS, XP, 還是 Win8, JFrame 視窗看起來都是一個樣. 特別注意, 這個 setDefaultLookAndFeelDecorated() 是 JFrame 的靜態方法, 必須在建立 JFrame 之前呼叫, 否則無效.

如果要在建立 JFrame 物件之後再修改視覺風格, 則必須設定 RootPane, 如下列範例 2-1 所示 :

測試範例 2-1http://tony1966.16mb.com/javatest/JFrame2_1.class [看原始碼]

public class JFrame2_1 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame2_1();
    }
  public JFrame2_1() {
    f=new JFrame("JFrame 2_1");
    f.setUndecorated(true);
    f.getRootPane().setWindowDecorationStyle(JRootPane.FRAME);

    f.setSize(400,300);
    f.setVisible(true);
    }
  }


此例中, 先用 setUndecorated() 方法解除建立 JFrame 物件時所設定之 Windows 的視覺外觀, 然後取得視窗的 RootPane 物件後, 重新設定其外觀為 JRootPane 之視窗外觀, 結果與範例 2 是一樣的.

但是還有個問題, JFrame 視窗能不能置中呢? 這可以用 Toolkit 物件取得螢幕尺寸後, 配合視窗大小來計算左上角座標, 如下範例 3 所示 :

測試範例 3 : http://tony1966.16mb.com/javatest/JFrame3.class [看原始碼] 

public class JFrame3 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame3();
    }
  public JFrame3() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JFrame 1");
    f.setSize(400,300);
    Dimension d=Toolkit.getDefaultToolkit().getScreenSize();
    Dimension s=f.getSize();
    f.setLocation((d.width-s.width)/2,(d.height-s.height)/2);
    f.setVisible(true);
    }
  }

上例中, 先用 setSize() 設定視窗大小, 再呼叫 Toolkit.getDefaultToolkit().getScreenSize() 方法傳回螢幕的 Dimension 物件, 它有兩個欄位, width 為螢幕寬度, height 為高度, 而 JFrame 的 getSize() 方法也是傳回一樣的 Dimension 物件. 螢幕寬度減去視窗寬度再平分就是視窗到螢幕左右邊的距離; 同理可算出視窗到螢幕上下邊之距離, 把它拿來作為視窗左上角座標, 就可以將螢幕置中了.

讓視窗置中還有一個更簡單的方法, 先設定視窗大小後, 再呼叫 setLocationRelativeTo() 方法, 傳入 null 取消視窗左上角預設相對於螢幕左上角之設定即可, 如下列範列 3-1 所示 :

測試範例 3-1 : http://tony1966.16mb.com/javatest/JFrame3_1.zip [看原始碼] 

public class JFrame3_1 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame3_1();
    }
  public JFrame3_1() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JFrame3_1");
    f.setSize(400,300);  //先設定大小
    f.setLocationRelativeTo(null);  //再取消預設之視窗相對於螢幕左上角
    f.setVisible(true);
    }
  }

特別注意, setSize() 一定要比 setLocationRelativeTo() 先呼叫, 否則視窗將不會置中, 而是視窗左上角置中, 亦即視窗左上角將會位於螢幕正中心.

如果要讓 JFrame 視窗一開啟就是全螢幕最大化該怎麼做? 這只要將視窗的大小設為與螢幕一樣大即可, 如下列範例 4 所示 :

測試範例 4 : http://tony1966.16mb.com/javatest/JFrame4.class [看原始碼] 

public class JFrame4 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame4();
    }
  public JFrame4() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JFrame 4");
    Dimension d=Toolkit.getDefaultToolkit().getScreenSize();
    f.setBounds(0,0,d.width,d.height);
    f.setVisible(true);
    }
  }

視窗最大化還有一個簡單的方法, 就是呼叫 JFrame 的 setExtendedState() 方法, 傳入 JFrame 的起始狀態常數 Frame.MAXIMISED_BOTH 為參數, 即可將視窗設為全螢幕, 如下範例 4-1 所示 :

測試範例 4-1 : http://tony1966.16mb.com/javatest/JFrame4_1.zip [看原始碼] 

import javax.swing.*;
import javax.swing.event.*;

public class JFrame4_1 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame4_1();
    }
  public JFrame4_1() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JFrame4_1");
    f.setExtendedState(Frame.MAXIMIZED_BOTH);
    f.setVisible(true);
    }
  }

JFrame 可用的起始狀態如下 :

  1. Frame.MAXIMIZED_HORIZ (水平最大化)
  2. Frame.MAXIMIZED_VERT (垂直最大化)
  3. Frame.MAXIMIZED_BOTH (水平垂直均最大化)
  4. Frame.ICONIFIED (視窗最小化)
  5. Frame.NORMAL (預設)

除了預設的 Frame.NORMAL 外, 其他起始狀態使用 setBounds(), setSize(), setLocation() 均無作用.

JFrame 視窗右上角有最小化, 最大化, 以及關閉視窗按鈕. 但是關閉視窗時, 卻發現命令提示字元視窗的命令列沒有釋放, 必須按 Ctrl+C 才行, 表示雖然 JFrame 視窗消失了, 事實上視窗物件還存在記憶體中為銷毀, 控制權還沒有交還給作業系統. 這可以呼叫 JFrame 類別的靜態方法設定預設之視窗關閉行為來解決 :

f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

如下列範例 5 所示 :

測試範例 5 : http://tony1966.16mb.com/javatest/JFrame5.class [看原始碼] 

public class JFrame5 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame5();
    }
  public JFrame5() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JFrame 1");
    f.setBounds(0,0,400,300);
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.setVisible(true);
    }
  }

這樣關閉視窗同時, 視窗物件也被銷毀了, 主控權還給作業系統, 命令列就會馬上釋放了. 事實上, JFrame 的預設視窗關閉處理是 JFrame.HIDE_ON_CLOSE, 亦即只是把視窗隱藏起來而已, 程式仍存在, 相當於呼叫 f.setVisible(false), 所以範例 4 的視窗關閉後, 還必須在命令提示字元視窗按 Ctrl+C 才能跳出來. 參見 Java 文件說明 (搜尋 setDefaultCloseOperation) :

# http://docs.oracle.com/javase/7/docs/api/javax/swing/JFrame.html#setDefaultCloseOperation%28int%29

若要以指令關閉 Java 程式, 可在視窗事件處理中呼叫 System.exit(0) 來完成, 上例所用的 f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) 其實就跟呼叫 System.exit(0) 功能是一樣的. 如果用事件處理方式來關閉視窗該怎麼做呢? 這有兩種方法 :
  1. 實作 WindowListener 介面 (需實作全部方法, 不建議)
  2. 使用 WindowAdapter 類別 (僅需覆蓋所需方法, 較方便)
以下範例 6 採用實作 WindowListener 介面的方式來關閉視窗.

測試範例 6 : http://tony1966.16mb.com/javatest/JFrame6.class [看原始碼] 

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JFrame6 implements WindowListener{
  JFrame f;
  public static void main(String argv[]) {
    new JFrame6("JFrame 6");
    }
  public JFrame6(String title) {
    JFrame.setDefaultLookAndFeelDecorated(true);
    f=new JFrame(title);
    f.setBounds(0,0,400,300);
    f.setVisible(true);
    f.addWindowListener(this);
    }
public void windowClosing(WindowEvent e) {System.exit(0);}
  public void windowClosed(WindowEvent e) {}
  public void windowOpened(WindowEvent e) {}
  public void windowActivated(WindowEvent e) {}
  public void windowDeactivated(WindowEvent e) {}
  public void windowIconified(WindowEvent e) {}
  public void windowDeiconified(WindowEvent e) {}
  }

WindowListener 介面定義了如上 7 個方法, 實作這個介面就是要在類別中覆蓋這 7 個方法, 但我們實際會用到的只有 windowClosing() 這個方法而已, 當偵測到視窗關閉動作時, 監聽器就會呼叫 windowClosing() 方法, 執行 System.exit(0) 來關閉程式. 注意, 這 7 個方法一個都不能少, 即使其中六個都沒用到, 也是要寫出來, 否則編譯會失敗. 注意, 在此例中建構子改為傳入視窗標題, 故於 main() 中建立物件時要傳入標題.

接著來測試事件處理的第二種方法 : 使用 Adapter. 由於實做 Listener 介面很麻煩, 因此 Java 為一些較低階的事件定義了相對應的調適器類別 (Adapter), 實做了該介面所有的方法 (其實只是空的實做), 共有七個 :

  1. WindowListener -> WindowAdapter
  2. MouseListener -> MouseAdapter
  3. MouseMotionListener -> MouseMotionAdapter
  4. KeyListener -> KeyAdapter
  5. ContainerListener -> ContainerAdapter
  6. FocusListener -> FocusAdapter
  7. ComponentListener -> ComponentAdapter

而四種語意型事件 (semantic event) 因為只有一個方法, 所以不需要特別定義調適器類別 :

  1. ActionListener
  2. AdjustmentListener
  3. ItemListener
  4. TextListener

以上這四種語意事件沒有相對應的 Adapter, 仍需實作其介面.

視窗事件有調適器類別 WindowAdapter, 只要繼承此類別, 然後覆寫所需要的方法即可, 不用像上面那樣必須實做全部方法. 如下列範例 7 所示 :

測試範例 7http://tony1966.16mb.com/javatest/JFrame7.zip [看原始碼]

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JFrame7 { //主類別
  JFrame f;
  public static void main(String argv[]) {
    new JFrame7(); 
    }
  public JFrame7() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JFrame 7");
    f.setBounds(0,0,400,300);
    f.setVisible(true);
    f.addWindowListener(new WindowHandler()); //WindowHandler 物件向 f 註冊監聽視窗事件
    }
  }

class WindowHandler extends WindowAdapter { //次類別
  public void windowClosing(WindowEvent e) {System.exit(0);}
  }


在範例 7 中, 程式裡除了主類別 JFrame7 外, 還定義了一個繼承 WindowAdapter 的次類別 WindowHandler (此類別不可以是 public, 因為一個 Java 程式中只有主類別才可以是 public). 範例 7 編譯後會產生兩個 class 檔 : JFrame7.class 與 WindowHandler.class, 我把它們壓縮成 JFrame7.zip.

也可以把次類別寫成內部類別 (inner class), 如下列範例 7-2 所示 :

測試範例 7-1 : http://tony1966.16mb.com/javatest/JFrame7_1.zip [看原始碼]

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JFrame7_1 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame7_1();
    }
  public JFrame7_1() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JFrame 7-1");
    f.setBounds(0,0,400,300);
    f.setVisible(true);
    f.addWindowListener(new MyWindowAdapter());
    }
  class MyWindowAdapter extends WindowAdapter { //內部類別成員
    public void windowClosing(WindowEvent e) {System.exit(0);}
    }
  }

此例中我們建立一個匿名的 MyWindowAdapter 物件當引數傳給 JFrame 物件的 addWindowListener() 方法, 意思是 MyWindowAdapter 物件向 JFrame 物件註冊, 要監聽其視窗事件. 反過來也可以說, JFrame 物件將其視窗事件委託給 MyWindowAdapter 處理. 範例 7-1 編譯後會產生兩個類別檔 : JFrame7_1.class 與 JFrame7_1$WindowHandler.class, 內部類別會以一個 $ 串接外部類別名稱作為其 class 檔名, 我將此兩 class 壓縮成 JFrame7_1.zip 如上.

事實上我們並不需要定義一個繼承 WindowAdapter 的類別, 利用匿名內部類別即可, 如下列範例 7-2 所示 :

測試範例 7-2 : http://tony1966.16mb.com/javatest/JFrame7_2.zip [看原始碼]

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JFrame7_2 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame7_2();
    }
  public JFrame7_2() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JFrame 7-2");
    f.setBounds(0,0,400,300);
    f.setVisible(true);
    f.addWindowListener(new WindowAdapter() { //匿名內部類別
      public void windowClosing(WindowEvent e) {System.exit(0);}    
      });
    }
  }

此例程式編譯後也會產生兩個 class 檔 : JFrame7_2.class 與 JFrame7_2$1.class, 可見內部匿名類別會以出現次序由 1 起編號, 附在主類別名稱之後作為其檔名. 我將其壓縮為 JFrame7_2.zip.

OK, 既然要用事件來處理視窗關閉事件, 一定要有比較有說服力的誘因吧! 例如彈出確認
框詢問是否真的要關閉視窗. 下面範例 7-3 我們就覆寫 windowClosing() 方法, 讓使用者還有機會決定要不要結束程式 :

測試範例 7-3 : http://tony1966.16mb.com/javatest/JFrame7_3.zip [看原始碼]

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JFrame7_3 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame7_3();
    }
  public JFrame7_3() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JFrame 7-3");
    f.setBounds(0,0,400,300);
    f.setVisible(true);
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        int result=JOptionPane.showConfirmDialog(f,   //或用 (Component)e.getSource() 亦可
                   "確定要結束程式嗎?",
                   "確認訊息",
                   JOptionPane.YES_NO_OPTION,
                   JOptionPane.WARNING_MESSAGE);
        if (result==JOptionPane.YES_OPTION) {System.exit(0);}
        }  
      });
    }
  }



因為顯示原始碼的 php 程式使用 utf-8, 因此範例 7-3 改存成 utf-8 格式, 故編譯時也需指定 utf-8 編碼才不會報錯 :

javac -encoding utf-8 JFrame7-3.java

此例中當按下視窗右上角的 x 鈕時, 不會直接關閉, 而是彈出確認框, 按是才會真正結束程式, 關於 Swing 的對話框, 參見 "Java Swing 測試 : JOptionPane 對話框". 特別注意, 要在事件處理中決定是否結束程式時, 必須呼叫 setDefaultCloseOperation() 並傳入 WindowConstant.DO_NOTIONG_ON_CLOSE, 否則不論按是或否, 視窗都會消失, 但事實上程式並未結束, 因為 JFrame 的預設視窗關閉動作是 WindowConstant.HIDE_ON_CLOSE. 若將其設為 DO_NOTIONG_ON_CLOSE, 視窗就不會消失, 等使用者按是後再用 System.exit(0) 來結束程式.

以上範例 6~7 只是順便複習一下事件處理的寫法, 如果想要不囉嗦, 按 X 立刻結束程式, 那麼只要用範例 5 的 setDefaultCloseOperation() 方法直接關閉視窗程式即可; 但若要保險一點 (例如可能編輯器物件裡有資料尚未儲存), 那麼就要用範例 7-3, 以事件處理方式來彈出確認框, 讓使用者有機會做最後的決定.

上例中的 JOptionPane 的母容器是 JFrame 物件 f, 也可以改成 (Component)e.getSource(), 亦即從事件物件 e 取得事件來源 (也就是 f 物件). 另外, 因為 JOptionPane 源自 JDialog, 因此我們也跟 JFrame 一樣將其預設外觀風格改為 Swing 預設風格, 否則對話框將會是作業系統外觀 :

JDialog.setDefaultLookAndFeelDecorated(true);

其實 Swing 內建支援七種外觀風格, 我們可以透過 UIManager 類別的靜態方法 setLookAndFeel() 來設定, 只要將下列風格字串當參數傳入即可 :
  1. javax.swing.plaf.metal.MetalLookAndFeel (預設, 不須特別指定, 但須開啟預設值)
  2. com.sun.java.swing.plaf.motif.MotifLookAndFeel
  3. com.sun.java.swing.plaf.gtk.GTKLookAndFeel (Windows 不支援)
  4. com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel
  5. com.sun.java.swing.plaf.windows.WindowsLookAndFeel  
  6. javax.swing.plaf.nimbus.NimbusLookAndFeel (Java 6 開始支援)
  7. javax.swing.plaf.mac.MacLookAndFeel (Windows 不支援)

下列範例 8 我們測試 Swing 預設的 Metal 風格, 其餘各風格只要修改 laf 字串之值即可, 各風格之對話框如下圖所示, 其中 GTK可編譯完成, 但執行時卻報錯, 應該是作業系統 (Windows XP) 不支援 :

測試範例 8http://tony1966.16mb.com/javatest/JFrame8.zip [看原始碼]

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JFrame8 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame8();
    }
  public JFrame8() {
    String laf="javax.swing.plaf.nimbus.NimbusLookAndFeel"; //Metal 時不需要
    try {UIManager.setLookAndFeel(laf);}  //Metal 時不需要
    catch(Exception e) {e.printStackTrace();}   //Metal 時不需要
    //JFrame.setDefaultLookAndFeelDecorated(true); //僅 Metal 時需要
    //JDialog.setDefaultLookAndFeelDecorated(true); //僅 Metal 時需要
    f=new JFrame("JFrame 8");
    f.setBounds(0,0,400,300);
    f.setVisible(true);
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        int result=JOptionPane.showConfirmDialog(f,
                   "確定要結束程式嗎?",
                   "確認訊息",
                   JOptionPane.YES_NO_OPTION,
                   JOptionPane.WARNING_MESSAGE);
        if (result==JOptionPane.YES_OPTION) {System.exit(0);}
        }   
      });
    }
  }

Metal

 Motif


WindowsClassic

 Windows

 Nimbus

由於作業系統不一定支援指定的主題系統 (例如 Windows 就不支援 GTK), 所以 UIManager. setLookAndFeel() 方法必須在 try-catch 中呼叫以捕捉可能的例外. 其次, 因為 Swing 預設的主題風格為 Metal, 因此使用 Metal 風格時不須使用 setLookAndFeel() 去設定, 只需要呼叫所有容器 (這裡有用到的是 JFrame 與 JDialog) 的 setDefaultLookAndFeel() 方法, 並傳入 true 即可.

除了在程式一開始時設定 Look And Feel 主題風格, 也可以隨時動態改變它, 只要在 setLookAndFeel() 後, 再呼叫下列方法即可 :

SwingUtilities.updateComponentTreeUI(f);
f.pack();

因為改變主題風格可能會改變元件佈局位置, 故可再呼叫 pack() 方法. 

最後來看看如何改變視窗標題列上的小圖, 這要先準備一張小圖檔, 然後呼叫 JFrame 的 setIconImage() 方法 :

f.setIconImage(new ImageIcon("icon.jpg").getImage());

這裡需將圖檔傳入 ImageIcon 類別的建構子產生圖檔物件, 然後呼叫 getImage() 方法, 如下面範例 9 所示 :

測試範例 9 : http://tony1966.16mb.com/javatest/JFrame9.zip [看原始碼]

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JFrame9 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame9();
    }
  public JFrame9() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true); 
    f=new JFrame("JFrame9");
    f.setSize(400,300); 
    f.setLocationRelativeTo(null);
    f.setIconImage(new ImageIcon("icon.jpg").getImage());
    f.setVisible(true); 
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        int result=JOptionPane.showConfirmDialog(f,
                   "確定要結束程式嗎?",
                   "確認訊息",
                   JOptionPane.YES_NO_OPTION,
                   JOptionPane.WARNING_MESSAGE);
        if (result==JOptionPane.YES_OPTION) {System.exit(0);}
        }    
      });
    }
  }


可見視窗左上角的預設圖檔已經被換掉了, 而且在 Win7 工作列上也是取代預設的咖啡杯, 改為我們所指定的圖檔了.


以上的 JFrame 視窗皆可調整大小, 如果要禁止使用者調整視窗大小該怎麼做呢? 可以呼叫 setResizable() 方法, 傳入 false 即可, 如下列範例 10 所示 :

測試範例 10http://tony1966.16mb.com/javatest/JFrame10.zip [看原始碼]

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JFrame10 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame10();
    }
  public JFrame10() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JFrame10");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    f.setResizable(false);
    f.setVisible(true);
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        int result=JOptionPane.showConfirmDialog(f,
                   "確定要結束程式嗎?",
                   "確認訊息",
                   JOptionPane.YES_NO_OPTION,
                   JOptionPane.WARNING_MESSAGE);
        if (result==JOptionPane.YES_OPTION) {System.exit(0);}
        }   
      });
    }
  }





可見邊框上的細線不見了, 表示無法改變視窗大小, 同時右上角的最大化按鈕也消失了. 這在自行排版時可能會用到.

以上便是 JFrame 視窗較常用的方式.

參考資料 :

Java主程式結構(main)的個人理解
暨南大學資管的 Java 教學 (首頁)
# warning: unmappable character for encoding MS950 - JAVA學習筆記
# JOptionPane showMessageDialog examples (part 2)
# 浅谈Swing中提供了JOptionPane
# 理解::JAVA::.setDefaultCloseOperation();
# http://tntxia.iteye.com/blog/295901
# 邱小新的工作筆記