2014年5月22日 星期四

Java Swing 測試 : 分割面板 JSplitPane

Swing 的 JSplitPane 類別可將視窗在垂直或水平方向分割成兩塊區域, 可同時在兩個區域中顯示不同的內容, 例如檔案閱讀器, 可以在左方放置檔案樹狀結構, 右方顯示所點選的檔案內容. 透過層層套疊, 又可以將基本的兩區塊分割成較複雜的區塊, 類似網頁設計中的 Frameset 的作用, 例如 Java 的 API 文件首頁架構即是 :

http://docs.oracle.com/javase/7/docs/api/

JSplitPane 繼承自 JComponent, 其 API 如下 :

http://docs.oracle.com/javase/7/docs/api/javax/swing/JSplitPane.html

JSplitPane 有五個建構子如下 :
  1. JSplitPane() :
    預設為水平分割, 各放一個按鈕.
  2. JSplitPane(int oreintation) :
    傳入一個整數指定分割方式, 有兩個值 :
    JSplitPane.HORIZONTAL_SPLIT (水平分割=)
    JSplitPane.VERTICAL_SPLIT (垂直分割=)
  3. JSplitPane(int oreintation, boolean continousLayout) :
    傳入的第二參數為布林值, true 表示移動分割邊框調整分割區域大小時, 各區域內的元件會同時調整大小, false 表示移動時元件大小不變直到停止移動時才改變.
  4. JSplitPane(int oreintation, Component a, Component b) :
    依上下 (垂直) 或左右 (水平) 順序將 a, b 兩元件放入分割區域中. 
  5. JSplitPane(int oreintation, boolean continousLayout, Component a, Component b) :
# Manning : Swing second edition 目錄  (ch8 p220)

首先測試無參數建構子, 預設是水平分割, Swing 會自動在左右分割各放置一個按鈕, 範例如下 :

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

public class JSplitPane1 implements ActionListener {
  JFrame f;
  public static void main(String argv[]) {
    new JSplitPane1();
    }
  public JSplitPane1() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JSplitPane Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    JSplitPane split=new JSplitPane();
    cp.add(split,BorderLayout.CENTER);
    f.setVisible(true); //must be the last
    String msg="getDividerLocation()=" +
      split.getDividerLocation() + "\n" +
      "getDividerSize()=" +
      split.getDividerLocation() + "\n" +
      "getMaximumDividerLocation()=" +
      split.getMaximumDividerLocation() + "\n" +
      "getMinimumDividerLocation()=" +
      split.getMinimumDividerLocation() + "\n" +
      "getOrientation()=" +
      split.getOrientation() + "\n";
    JOptionPane.showMessageDialog(f,msg);
    //f.pack(); //for using Layout Mgr.

    //Close JFrame      
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f));
    }
  public void actionPerformed(ActionEvent e) {
    //if (e.getSource()==id) {}
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  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);}
    }  
  }

此例中我同時測試 JSplitPane 的七個方法 :
  1. int getDividerLocation() : 傳回從左方或上方算起的分割線位置 (px)
  2. int getDividerSize() : 傳回分割線粗細 (px)
  3. int getMaximumDividerLocation() : 傳回可設定的最大分割線位置 (px)
  4. int getMinimumDividerLocation() : 傳回可設定的最小分割線位置 (px)
  5. int getOrientation() : 傳回分割方向, 其值如下 :
    JSplitPane.HORIZONTAL_SPLIT (=1)
    JSplitPane.VERTICAL_SPLIT (=0)
  6. boolean isContinuousLayout() : 傳回是否可連續調整元件大小
  7. boolean isOneTouchExpandable() : 傳回是否可單鍵展開
由上可知, 未傳入參數時, 分割位置與分割線大小都傳回 -1 (unknown?). JSplitPane 也有一些 setter 方法可以如下 :
  1. setDividerLocation()
  2. setDividerSize()
  3. setOrientation()
  4. setOneTouchExpandable()
  5. setContinuousLayout()
  6. setTopComponent()
  7. setBottomComponent()
  8. setLeftComponent()
  9. setRightComponent()
其中 setTopComponent() 與 setBottomComponent() 原則上是用來設定垂直分割面板上的元件的, 而 setLeftComponent() 與 setRightComponent() 是用來設定水平分割面板上的元件的, 但實際上這四個方法與分割方向無關, setTopComponent() 與 setLeftComponent() 不管在哪種分割均可交互使用, 同理 setBottomComponent() 與 setRightComponent() 亦然. 例如在水平分割中, 使用 setTopComponent() 相當於使用 setLeftComponent(), 總之, 左與上等效, 右與下等效.

如果要在移動 JSplitPane 的分割線時即時將分隔線位置顯示在 JLabel 上該怎麼做? 我參考了下面這篇文章 :

Listening for JSplitPane Property Changes

可知移動 JSplitPane 的分割線會觸發 Property Change 事件, 此事件由 PropertyChangeEvent 類別負責處理, 而其事件監聽器為 PropertyChangeListener 介面, 其 API 參考 :

# PropertyChangeEvent
# PropertyChangeListener

此二者均屬於 java.beans 套件, 所以必須另行匯入 :

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

我們的類別必須實作 PropertyChangeListener 介面, 覆寫 propertyChange() 方法以監聽 JSplitPane 分隔線的移動動作. 當其移動時, PropertyChangeEvent 物件會記錄 JSplitPane 的 LAST_DIVIDER_LOCATION_PROPERTY 屬性, 只要呼叫事件物件的 getPropertyName(), 若傳回字串與此屬性相同, 就去更新 JLabel 的顯示文字, 動態呈現分隔線位置, 範例程式如下 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

public class JSplitPane2  implements PropertyChangeListener {
  JFrame f;
  JSplitPane split;
  JLabel lb;
  public static void main(String argv[]) {
    new JSplitPane2();
    }
  public JSplitPane2() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JSplitPane Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();

    //Build Elements
    split=new JSplitPane();
    split.setOrientation(JSplitPane.VERTICAL_SPLIT);
    split.setDividerLocation(0.5);
    split.setDividerSize(30);
    split.setOneTouchExpandable(true);
    split.setContinuousLayout(true);
    lb=new JLabel("Divider Location=");
    split.setTopComponent(lb);
    cp.add(split,BorderLayout.CENTER);
    split.addPropertyChangeListener(this);
    f.setVisible(true); //must be the last

    //Close JFrame      
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f));
    }
  public void propertyChange(PropertyChangeEvent e) {
    String property=e.getPropertyName();
    if (property.equals(JSplitPane.LAST_DIVIDER_LOCATION_PROPERTY)) {
      lb.setText("Divider Location=" + split.getDividerLocation());
      }
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  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);}
    }  
  }

此例中, 我們將視窗改成垂直分割, 並將一個 JLabel 放到上方區域中, 當移動分隔線時, JLabel 會不斷更新顯示分隔線的位置. 注意, 因為要在事件處理函式中存取 JSplitPane 與 JLabel 物件, 故而需宣告為類別成員. 其次, setOneTouchExpandable(true) 會在分隔線上出現兩個三角形小按鈕, 按一下就會朝指定方向擴展區域至最大. 而 setContinuousLayout(true) 則會在移動分隔線時, 個區域內的元件會同時調整大小. 如果設為 false, 則移動時不會同步調整元件大小, 而是在移動停止時才調整. 當視窗中元件數目非常多時, 最好設為 false, 以免效能降低.

接下來要來測試較複雜的分割面板, 例如在上面的垂直分割中, 上方與下方區域再做水平分割, 程式碼如下 :

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

public class JSplitPane3 {
  JFrame f;
  public static void main(String argv[]) {
    new JSplitPane3(); 
    }
  public JSplitPane3() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JSplitPane Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    JLabel topLeft=new JLabel("左上",JLabel.CENTER);
    JLabel topRight=new JLabel("右上",JLabel.CENTER);
    JSplitPane top=new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
                                  true,
                                  topLeft,
                                  topRight);

    top.setOneTouchExpandable(true);
    top.setDividerLocation(200);
    JLabel bottomLeft=new JLabel("左下",JLabel.CENTER);
    JLabel bottomRight=new JLabel("右下",JLabel.CENTER);
    JSplitPane bottom=new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
                                     true,
                                     bottomLeft,
                                     bottomRight);

    bottom.setOneTouchExpandable(true);
    bottom.setDividerLocation(200);
    JSplitPane split=new JSplitPane(JSplitPane.VERTICAL_SPLIT,
                                    true,
                                    top,
                                    bottom);

    split.setOneTouchExpandable(true);
    split.setDividerLocation(120);
    cp.add(split,BorderLayout.CENTER);
    f.setVisible(true); //must be the last

    //Close JFrame       
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f)); 
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  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);}
    }   
  }

此例中利用套疊將視窗分割成四部份, 先各自製作了兩個水平分割面板, 再將它們當作元件放進外層的垂直分割面板. 當然也可以先製作兩個垂直分割面板, 再放進一個水平分割面板中, 效果一樣. 注意, 這裡分割線的位置設定採用 px 單位, 我試過用比例 (0.0~1.0) 卻無效, why? 再研究.

最後來測試一個較實際的應用-圖片瀏覽程式, 製作一個水平分割面板, 左邊用 JList 列出圖檔項目, 右邊顯示所選之圖檔, 因為要處理 JList 的選項動作, 故必須實作 ListSelectionListener 介面,程式碼如下 :

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

public class JSplitPane4 implements ListSelectionListener {
  JFrame f;
  JLabel img;
  String[] pictures;
  JSplitPane jsp;
  public static void main(String argv[]) {
    new JSplitPane4();
    }
  public JSplitPane4() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JSplitPane Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    pictures=new String[]{"cat.jpg","family.jpg"};
    JList list=new JList(pictures);
    list.addListSelectionListener(this);
    jsp=new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
    jsp.setDividerLocation(100);
    jsp.setLeftComponent(new JScrollPane(list));
    img=new JLabel(new ImageIcon("cat.jpg"));
    jsp.setRightComponent(new JScrollPane(img));
    cp.add(jsp,BorderLayout.CENTER);
    f.setVisible(true); //must be the last

    //Close JFrame      
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f));
    }
  public void valueChanged(ListSelectionEvent e) {
    JList list=(JList)e.getSource();
    img.setIcon(new ImageIcon(pictures[list.getSelectedIndex()]));
    jsp.setDividerLocation(100); //避免分割線位置變化
    jsp.setRightComponent(new JScrollPane(img));
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  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);}
    }  
  }

此例中, 左方分割的 JList 與右方分割的 JLabel 都用 JScrollPane 包起來, 再放進 JSplitPane 中, 這樣就會在內容長度或寬度超過時自動出現捲軸. 而實作 ListSelectionListener 介面只要覆寫 valueChanged() 方法, 利用呼叫 JList 的 getSelectedIndex() 方法取得所選擇之選項索引, 取得圖檔名稱後更新右方分割中 JLabel 的圖檔. 因為要在事件處理方法中存取 JLabel, 圖檔名稱陣列, 以及 JSplitPane 物件, 故將此三者宣告為類別成員.


沒有留言 :