2014年5月14日 星期三

Java Swing 測試 : 選單與 Look and Feel

選單是 GUI 程式必備的功能, 此處要用 Look and Feel 切換為例來測試 Swing 的選單功能. 關於 Look and Feel, 參考前次測試 JFrame 之文章 :

# Java Swing 測試 : JFrame

JMenuBar 位於 JRootPane 的 layeredPane 的 FRAME_CONTENT_LAYER 的上面, 此 FRAME_CONTENT_PANE 包含了 contentPane 與可有可無的 menuBar, 利用 JFrame, JApplet, JDialog, JRootPane, JInternalFrame 的 setMenuBar() 方法將一個選單加上去時, menuBar 就會出現在 contentPane 上面.

Swing 的選項功能表主要是用到 JMenuBar, JMenu, 以及 JMenuItem 這三個類別. 比較花俏部分則會用到 JCheckBoxMenuItem, JRadioButtonMenuItem 這兩個選項元件. JMenuBar 是 JFrame/JApplet 標題下面用來放選項的長條列 (bar), 而 JMenu 則是放在這長條列上的選單, 或者是選單套疊時, 作為選項的子選單; 真正觸發實際功能的是選項, 即 JMenuItem,  JCheckBoxMenuItem, 以及 JRadioButtonMenuItem 這三個元件.

JRadioButtonMenuItem 功能與 JRadionButton 一樣, 是單選按鈕; 而 JCheckBoxMenuItem 則與 JCheckBox 功能一樣, 用作複選. 但 JRadioButtonMenuItem 與 JRadioButton 都必須使用 ButtonGroup 綑綁在一起才會具有整體單選作用, 否則是各自獨立的按鈕, 相關 API 如下 :

# JMenuBar
# JMenu
# JMenuItem
# JRadioButtonMenuItem
# JCheckBoxMenuItem
# ButtonGroup
# LinkedHashMap
# LookAndFeel
MetalLookAndFeel
UIManager
SwingUtilities

此處要把 Swing 的內建 7 種 Look and Feel 介面以及 2 種主題 (theme) 做成 JRadioButtonMenuItem 來選擇, 我使用 LinkedHashMap 來模擬關聯式陣列 (不用 HashMap 的原因是它的元素順序無法控制), 儲存 7 種 Look&Feel 標題與類別名稱, 所以必須匯入 java.util 類別庫, 如下範例所示 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import java.util.*;
import javax.swing.plaf.metal.MetalLookAndFeel;
import javax.swing.plaf.metal.MetalTheme;
import javax.swing.plaf.metal.OceanTheme;
import javax.swing.plaf.metal.DefaultMetalTheme;

public class menutest implements ActionListener {
  JFrame f;
  JMenuBar mb;
  JMenu configMenu,LFmenu,themeMenu;
  LinkedHashMap LF;
  JRadioButtonMenuItem[] LFitem;
  JRadioButtonMenuItem[] themeItem;
  public static void main(String argv[]) {
    new menutest();
    }
  public menutest() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JMenuBar Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    cp.setLayout(null);

    //Build MenuBar
    LF=new LinkedHashMap();
    String plaf="com.sun.java.swing.plaf";
    LF.put("Metal","javax.swing.plaf.metal.MetalLookAndFeel");
    LF.put("CDE/Motif", plaf + ".motif.MotifLookAndFeel");
    LF.put("Windows XP", plaf + ".windows.WindowsLookAndFeel");
    LF.put("Windows Classic", plaf + ".windows.WindowsClassicLookAndFeel");
    LF.put("Nimbus","javax.swing.plaf.nimbus.NimbusLookAndFeel");
    LF.put("GTK+", plaf + ".gtk.GTKLookAndFeel");
    LF.put("Mac", plaf + ".mac.MacLookAndFeel");
    mb=new JMenuBar();
    configMenu=new JMenu("Config");
    LFmenu=new JMenu("Look & Feel");
    LFitem=new JRadioButtonMenuItem[LF.size()];
    ButtonGroup LFgroup=new ButtonGroup(); //包裹選項形成單選效果
    int i=0;
    for (String key:LF.keySet()) {
      LFitem[i]=new JRadioButtonMenuItem(key);
      LFitem[i].setEnabled(isLookAndFeelSupported(LF.get(key)));  //是否有支援此介面
      LFmenu.add(LFitem[i]);   //加入選單
      LFgroup.add(LFitem[i]);  //加入單選群組
      LFitem[i].addActionListener(this);  //註冊動作事件
      if (i==0) {LFitem[i].setSelected(true);}  //預設為 Metal 介面
      ++i;
      }
    configMenu.add(LFmenu);
    themeMenu=new JMenu("Theme");
    ButtonGroup themeGroup=new ButtonGroup();
    themeItem=new JRadioButtonMenuItem[2];
    themeItem[0]=new JRadioButtonMenuItem("Metal");
    themeItem[1]=new JRadioButtonMenuItem("Ocean");
    themeItem[0].addActionListener(this);
    themeItem[1].addActionListener(this);
    themeItem[0].setSelected(true);
    themeMenu.add(themeItem[0]);
    themeMenu.add(themeItem[1]);
    themeGroup.add(themeItem[0]);
    themeGroup.add(themeItem[1]);
    configMenu.add(themeMenu);
    mb.add(configMenu);
    f.setJMenuBar(mb);
    f.setVisible(true);

    //Close JFrame      
    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);}
        }  
      });
    }
  public void actionPerformed(ActionEvent e) {
    String cmd=e.getActionCommand();
    boolean isLF=cmd.equals("Metal")||cmd.equals("CDE/Motif")||
                 cmd.equals("Windows XP")||cmd.equals("Windows Classic")||
                 cmd.equals("Nimbus")||cmd.equals("GTK+")||
                 cmd.equals("Mac");
    JOptionPane.showConfirmDialog(f,cmd,"Info",-1);
    if (isLF) {
      if (cmd.equals("Metal")) {  //此 if-else 無效可刪除
        JFrame.setDefaultLookAndFeelDecorated(true);
        JDialog.setDefaultLookAndFeelDecorated(true);
        }
      else {
        JFrame.setDefaultLookAndFeelDecorated(false);
        JDialog.setDefaultLookAndFeelDecorated(false);
        }
      try {
        for (String key:LF.keySet()) {
          if (cmd.equals(key)) {
            UIManager.setLookAndFeel(LF.get(key));  //設定介面
            SwingUtilities.updateComponentTreeUI(f);  //更新 UI 設定
            //f.pack();
            }
          }
        }
      catch(Exception uie) {uie.printStackTrace();}  
      }
    boolean isTheme=cmd.equals("Metal")||cmd.equals("Ocean");
    if (isTheme) {
      if (cmd.equals("Metal")) { //更新主題
        MetalLookAndFeel.setCurrentTheme(new DefaultMetalTheme());
        }
      else {MetalLookAndFeel.setCurrentTheme(new OceanTheme());}
      try {
        UIManager.setLookAndFeel(new MetalLookAndFeel()); //設定 Metal 介面
        SwingUtilities.updateComponentTreeUI(f); //更新 UI 設定
        }
      catch(Exception uie) {uie.printStackTrace();}
      LFitem[0].setSelected(true);
      }
    }
  public boolean isLookAndFeelSupported(String lnfname) { //檢查作業系統是否支持
    try {
      Class lnfclass=Class.forName(lnfname);
      LookAndFeel lnf=(LookAndFeel)(lnfclass.newInstance());
      return lnf.isSupportedLookAndFeel();
      }
    catch(Exception e) {return false;}
    }
  }

選項功能表的製作很簡單, 就是用 JMenu 選單物件的 add() 方法加入 JMenuItem 選項物件, 再用JMenuBar 選單列物件的 add() 方法加入選單, 最後用 JFrame 的 setMenuBar() 方法加入選單列即可. 選單也可以當作選項加入另一個選單中 (套疊), 形成子選單效果.

下圖為預設 Metal 介面之兩個內建主題, Ocean 與 Metal, 只在 Metal 介面有效 :


但是如果從預設的 Metal 介面切換到別的介面, JFrame 的邊框與標題列就會消失, 如下圖所示 :




這樣就沒辦法移動, 縮放, 關閉視窗了, 但只要切回 Metal 介面就會恢復了. 原因出在上列程式中, 在 JFrame 起始之前, 將 JFrame 與 JDialog 的預設裝飾設為 true 之故, 這原先是為了讓 JFrame 與 JDialog 的外框徹底 Swing 化, 不套用作業系統視窗外框的緣故 :

    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);

但在切換至預設之 Metal 以外的介面時, 即使將其設為 false 也沒用, 無法恢復套用作業系統視窗外框. 如果拿掉這兩行 (或改為 false), 就會正常套用作業系統視窗外框, 但切換至 Metal 時, 即使程式中將其設為 true, 也不會套用純 Java 外框. 可見這兩個指令在 JFrame 建立後再改就無效了. 以下是將上面兩個指令 remark 掉後之執行結果 :





Swing 的預設介面 Metal 除了內建的 Ocean 與 Metal 兩種主題外, 也可以自訂主題. 只要繼承 DefaultMetalTheme 類別, 並覆寫其屬性與方法即可, 相關類別之 API 如下 :

# DefaultMetalTheme
# ColorUIResource
OceanTheme
# MetalTheme

我將上面範例做些修改, 加入一個 GreenTheme 類別, 繼承 DefaultMetalTheme 類別, 建立 ColorUIResource 物件來設定新主題的配色, 並覆寫 getPrimary1(), getPrimary2(), getPrimary3(), getSecondary1(), getSecondary2(), getSecondary3() 這六個方法, 但要注意, 此六方法權限為 protected, 覆寫時必須加上去, 否則編譯失敗 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import java.util.*;
//for theme
import javax.swing.plaf.*;
import javax.swing.plaf.metal.*;
import javax.swing.border.*;

public class menutest implements ActionListener {
  JFrame f;
  JMenuBar mb;
  JMenu configMenu,LFmenu,themeMenu;
  LinkedHashMap LF;
  JRadioButtonMenuItem[] LFitem;
  JRadioButtonMenuItem[] themeItem;
  public static void main(String argv[]) {
    new menutest();
    }
  public menutest() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JMenuBar Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    cp.setLayout(null);

    //Build MenuBar
    LF=new LinkedHashMap();
    String plaf="com.sun.java.swing.plaf";
    LF.put("Metal","javax.swing.plaf.metal.MetalLookAndFeel");
    LF.put("CDE/Motif", plaf + ".motif.MotifLookAndFeel");
    LF.put("Windows XP", plaf + ".windows.WindowsLookAndFeel");
    LF.put("Windows Classic", plaf + ".windows.WindowsClassicLookAndFeel");
    LF.put("Nimbus","javax.swing.plaf.nimbus.NimbusLookAndFeel");
    LF.put("GTK+", plaf + ".gtk.GTKLookAndFeel");
    LF.put("Mac", plaf + ".mac.MacLookAndFeel");
    mb=new JMenuBar();
    configMenu=new JMenu("Config");
    LFmenu=new JMenu("Look & Feel");
    LFitem=new JRadioButtonMenuItem[LF.size()];
    ButtonGroup LFgroup=new ButtonGroup();
    int i=0;
    for (String key:LF.keySet()) {
      LFitem[i]=new JRadioButtonMenuItem(key);
      LFitem[i].setEnabled(isLookAndFeelSupported(LF.get(key)));
      LFmenu.add(LFitem[i]);
      LFgroup.add(LFitem[i]);
      LFitem[i].addActionListener(this);  
      if (i==0) {LFitem[i].setSelected(true);}
      ++i;
      }
    configMenu.add(LFmenu);
    themeMenu=new JMenu("Theme");
    ButtonGroup themeGroup=new ButtonGroup();
    themeItem=new JRadioButtonMenuItem[3];
    themeItem[0]=new JRadioButtonMenuItem("Metal");
    themeItem[1]=new JRadioButtonMenuItem("Ocean");
    themeItem[2]=new JRadioButtonMenuItem("Green");
    themeItem[0].addActionListener(this);
    themeItem[1].addActionListener(this);
    themeItem[2].addActionListener(this);
    themeItem[0].setSelected(true);
    themeMenu.add(themeItem[0]);
    themeMenu.add(themeItem[1]);
    themeMenu.add(themeItem[2]);
    themeGroup.add(themeItem[0]);
    themeGroup.add(themeItem[1]);
    themeGroup.add(themeItem[2]);
    configMenu.add(themeMenu);
    mb.add(configMenu);
    f.setJMenuBar(mb);
    f.setVisible(true);

    //Close JFrame      
    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);}
        }  
      });
    }
  public void actionPerformed(ActionEvent e) {
    String cmd=e.getActionCommand();
    boolean isLF=cmd.equals("Metal")||cmd.equals("CDE/Motif")||
                 cmd.equals("Windows XP")||cmd.equals("Windows Classic")||
                 cmd.equals("Nimbus")||cmd.equals("GTK+")||
                 cmd.equals("Mac");
    JOptionPane.showConfirmDialog(f,cmd,"Info",-1);
    if (isLF) {
      if (cmd.equals("Metal")) {
        JFrame.setDefaultLookAndFeelDecorated(true);
        JDialog.setDefaultLookAndFeelDecorated(true);
        }
      else {
        JFrame.setDefaultLookAndFeelDecorated(false);
        JDialog.setDefaultLookAndFeelDecorated(false);
        }
      try {
        for (String key:LF.keySet()) {
          if (cmd.equals(key)) {
            UIManager.setLookAndFeel(LF.get(key));
            SwingUtilities.updateComponentTreeUI(f);
            //f.pack();
            }
          }
        }
      catch(Exception uie) {uie.printStackTrace();}  
      }
    boolean isTheme=cmd.equals("Metal")||cmd.equals("Ocean")||
                    cmd.equals("Green");
    if (isTheme) {
      if (cmd.equals("Metal")) {
        MetalLookAndFeel.setCurrentTheme(new DefaultMetalTheme());
        }
      else if (cmd.equals("Ocean")) {
        MetalLookAndFeel.setCurrentTheme(new OceanTheme());
        }
      else {MetalLookAndFeel.setCurrentTheme(new GreenTheme());}
      try {
        UIManager.setLookAndFeel(new MetalLookAndFeel());
        SwingUtilities.updateComponentTreeUI(f);
        }
      catch(Exception uie) {uie.printStackTrace();}
      LFitem[0].setSelected(true);
      f.setTitle("Theme : " + cmd);
      }
    }
  public boolean isLookAndFeelSupported(String lnfname) {
    try {
      Class lnfclass=Class.forName(lnfname);
      LookAndFeel lnf=(LookAndFeel)(lnfclass.newInstance());
      return lnf.isSupportedLookAndFeel();
      }
    catch(Exception e) {return false;}
    }
  }
class GreenTheme extends DefaultMetalTheme {
  ColorUIResource primary1=new ColorUIResource(50, 100, 50);
  ColorUIResource primary2=new ColorUIResource(100, 150, 100);
  ColorUIResource primary3=new ColorUIResource(155, 205, 155);
  ColorUIResource secondary1=new ColorUIResource(110, 110, 110);
  ColorUIResource secondary2=new ColorUIResource(160, 160, 160);
  ColorUIResource secondary3=new ColorUIResource(205, 230, 205);  
  protected ColorUIResource getPrimary1() {return primary1;}  
  protected ColorUIResource getPrimary2() {return primary2;} 
  protected ColorUIResource getPrimary3() {return primary3;} 
  protected ColorUIResource getSecondary1() {return secondary1;}
  protected ColorUIResource getSecondary2() {return secondary2;}
  protected ColorUIResource getSecondary3() {return secondary3;}
  }



可見只要設定 ColorUIResource 的配色, 就可以自訂新的 Metal 介面的主題風格.

參考資料 :

http://docs.oracle.com/javase/7/docs/api/
JFrame setDefaultLookAndFeelDecorated(true)
# Changing Look and Feel of Swing Application
# [原創]Swing技巧8:完美的LookAndFeel解決方案
关于HashMap和LinkedHashMap的工作心得
# HashMap的應用及資料排序
Getting the Default Values for a Look and Feel
# Java Look and Feel Design Guidelines (pdf)
Application Graphics Java look and feel Graphics Repository

2 則留言:

  1. 版主您好
    我在執行您的程式碼時
    LF.keySet() 有關這個函示這個都會有問題 並顯示
    Type mismatch: cannot convert from element type Object to String
    可以請教是甚麼問題嗎

    回覆刪除
  2. Sorry, 我重新編譯得到的卻是 :
    error: incompatible types
    for (String key:LF.keySet()) {
    ^
    required: String
    found: Object
    以前是 OK 的, 可能因為 JDK 不斷改版的緣故. 我已經不再使用 Java 了, 改用 Python.

    回覆刪除