本書的前面兩章描述了一些低級的Swing組件。本章將會深入Swing面向菜單的組件。菜單與工具欄通過提供一些可視化的命令選項可以使得我們的程序更為友好。盡管Swing組件可以支持多個按鍵的命令序列,菜單被設計用來提供使用鼠標的圖形化選擇,而不是通過鍵盤。
本章將要討論的菜單組件的使用如下:
- 對於級聯菜單,我們可以創建一個JMenu組件,並將其添加到JMenuBar。
- 對於JMenu中的可選菜單,我們可以創建一個JMenuItem組件,並將其添加到JMenu。
- 要創建子菜單,我們可以向JMenu添加一個新的JMenu,並向新菜單添加JMenuItem選項。
- 然后,當一個JMenu被選中時,系統會在JPopupMenu中顯示其當前的組件集合。
除了基本的JMenuItem元素,本章將還會討論其他的菜單項目,例如JCheckBoxMenuItem以及JRadioButtonMenuItem,我們可以將這兩個菜單項目放在JMenu中。同時我們還會探討JSeparator類,他可以將菜單項目進行邏輯分組。我們將會了解如何通過使用JPopupMenu類來為JMenu被選中后出現的彈出菜單,或是任何組件的環境中提供支持。與抽象按鈕類似,每一個菜單元素也有一個用於鍵盤選中的熱鍵與其相關聯。我們同進也會了解鍵盤快捷鍵支持,從而可以使得用記避免在多級菜單間進行遍歷。
除了單個的菜單相關的組件之外,在本章中我們會了解JMenuBar選中模型以及菜單特定的事件相關類。我們要了解的選中模型接口是SingleSelectionModel接口,以及其默認實現DefaultSingleSelectionModel。我們將會探討菜單特定的監聽器以及事件MenuListener/MenuEvent,MenuKeyListener/MenuKeyEvent以及MenuDragMouseListener/MenuDragMouseEvent。另外,我們還會了解使用Popup與PopupFactory創建其他的彈出組件,以及通過JToolBar類使用工具欄。
6.1 使用菜單
我們先來了解一個演示菜單組件是如何組合在一起的示例。要開始我們的學習,創建一個具有菜單欄的窗體,如圖6-1所示。
這個簡單的菜單示例具有下列特性:
- 在菜單欄上是兩個普通的菜單:File與Edit。在File菜單下,是我們較為熟悉的New,Open,Close與Exit。在Edit菜單下則是Cut,Copy,Paste與Find以及一個Find選項的子菜單。選項子菜單將包含查找方向子菜單--向前與向后--以及一個大小寫敏感的開關。
- 在不同菜單的各種位置,菜單分隔符將選項分邏輯集合。
- 每一個菜單選項都具有一個相關聯的熱鍵,通過熱鍵可以進行鍵盤瀏覽與選中。熱鍵可以使得用戶通過鍵盤進行菜單選擇,例如,在Windows平台下通過按下Alt-F可以打開File菜單。
- 除了鍵盤熱鍵,與多個選項相關聯的擊鍵可以作為鍵盤快捷鍵。與熱鍵不同,快捷鍵可以直接激活一個菜單選項,甚至菜單選項並不可見時也是如此。
- 選項子菜單具有一個與其相關聯的圖標。盡管在圖6-1中只顯示了一個圖標,所有的菜單組件都可以具有一個圖標,除了JSpearator與JPopupMenu組件。
注意,對於這個示例,這些菜單選項並不會完成任何有意義的事情,僅是輸出哪一個菜單選項被選中。例如,由Edit菜單中選中Copy選項會顯示Selected: Copy。
列表6-1顯示了圖6-1中生成示例類的完整代碼。
/** * */ package net.ariel.ch06; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import javax.swing.ButtonGroup; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JCheckBoxMenuItem; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JRadioButtonMenuItem; import javax.swing.KeyStroke; /** * @author mylxiaoyi * */ public class MenuSample { static class MenuActionListener implements ActionListener { public void actionPerformed(ActionEvent event ) { System.out.println("Selected: "+event.getActionCommand()); } } /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Runnable runner = new Runnable() { public void run() { MenuActionListener menuListener = new MenuActionListener(); JFrame frame = new JFrame("Menu Sample"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JMenuBar menuBar = new JMenuBar(); JMenu fileMenu = new JMenu("File"); fileMenu.setMnemonic(KeyEvent.VK_F); menuBar.add(fileMenu); JMenuItem newMenuItem = new JMenuItem("New", KeyEvent.VK_N); newMenuItem.addActionListener(menuListener); fileMenu.add(newMenuItem); JMenuItem openMenuItem = new JMenuItem("Open", KeyEvent.VK_O); openMenuItem.addActionListener(menuListener); fileMenu.add(openMenuItem); JMenuItem closeMenuItem = new JMenuItem("Close", KeyEvent.VK_C); closeMenuItem.addActionListener(menuListener); fileMenu.add(closeMenuItem); fileMenu.addSeparator(); JMenuItem saveMenuItem = new JMenuItem("Save", KeyEvent.VK_S); saveMenuItem.addActionListener(menuListener); fileMenu.add(saveMenuItem); fileMenu.addSeparator(); JMenuItem exitMenuItem = new JMenuItem("Exit", KeyEvent.VK_X); exitMenuItem.addActionListener(menuListener); fileMenu.add(exitMenuItem); JMenu editMenu = new JMenu("Edit"); editMenu.setMnemonic(KeyEvent.VK_E); menuBar.add(editMenu); JMenuItem cutMenuItem = new JMenuItem("Cut", KeyEvent.VK_T); cutMenuItem.addActionListener(menuListener); KeyStroke ctrlXKeyStroke = KeyStroke.getKeyStroke("control X"); cutMenuItem.setAccelerator(ctrlXKeyStroke); editMenu.add(cutMenuItem); JMenuItem copyMenuItem = new JMenuItem("Copy", KeyEvent.VK_C); copyMenuItem.addActionListener(menuListener); KeyStroke ctrlCKeyStroke = KeyStroke.getKeyStroke("control C"); copyMenuItem.setAccelerator(ctrlCKeyStroke); editMenu.add(copyMenuItem); JMenuItem pasteMenuItem = new JMenuItem("Paste", KeyEvent.VK_P); pasteMenuItem.addActionListener(menuListener); KeyStroke ctrlVKeyStroke = KeyStroke.getKeyStroke("control V"); pasteMenuItem.setAccelerator(ctrlVKeyStroke); editMenu.add(pasteMenuItem); editMenu.addSeparator(); JMenuItem findMenuItem = new JMenuItem("Find", KeyEvent.VK_F); findMenuItem.addActionListener(menuListener); KeyStroke f3KeyStroke = KeyStroke.getKeyStroke("F3"); findMenuItem.setAccelerator(f3KeyStroke); editMenu.add(findMenuItem); JMenu findOptionsMenu = new JMenu("Options"); Icon atIcon = new ImageIcon("at.gif"); findOptionsMenu.setIcon(atIcon); findOptionsMenu.setMnemonic(KeyEvent.VK_O); ButtonGroup directionGroup = new ButtonGroup(); JRadioButtonMenuItem forwardMenuItem = new JRadioButtonMenuItem("Forward", true); forwardMenuItem.addActionListener(menuListener); forwardMenuItem.setMnemonic(KeyEvent.VK_F); findOptionsMenu.add(forwardMenuItem); directionGroup.add(forwardMenuItem); JRadioButtonMenuItem backMenuItem = new JRadioButtonMenuItem("Back"); backMenuItem.addActionListener(menuListener); backMenuItem.setMnemonic(KeyEvent.VK_B); findOptionsMenu.add(backMenuItem); directionGroup.add(backMenuItem); findOptionsMenu.addSeparator(); JCheckBoxMenuItem caseMenuItem = new JCheckBoxMenuItem("Case Sensitive"); caseMenuItem.addActionListener(menuListener); caseMenuItem.setMnemonic(KeyEvent.VK_C); findOptionsMenu.add(caseMenuItem); editMenu.add(findOptionsMenu); frame.setJMenuBar(menuBar); frame.setSize(350, 250); frame.setVisible(true); } }; EventQueue.invokeLater(runner); } }
6.1.1 菜單類層次結構
現在我們已經了解如何為程序創建級聯菜單,我們應該已經了解了使用Swing菜單組件所涉及到的內容。為了表達更為清晰,圖6-2顯示了所有的Swing菜單組件內部是如何關聯的。
圖6-2所顯示的最重要的概念就是作為JComponent的子類的所有Swing菜單元素都是AWT組件。我們可以將JMenuItem,JMenu以及JMenuBar組件放在AWT組件可以放置的位置,而僅不是在窗體上。另外,因為JMenuItem是由AbstractButton繼承而來的,JMenuItem及其子類繼承了各種圖標以及HTML文本標簽的支持,正如第5章所述。
除了是基本的類層次結構的一部分以外,每一個可選擇的菜單組件都實現了MenuElement接口。這個接口描述了支持鍵盤與鼠標瀏覽所必須的菜單行為。預定義的菜單組件已經實現了這種行為,所以我們不必自己實現。但是如我們對這個接口是如何工作的比較感興趣,可以查看本章中的“MenuElement接口”一節。
下面我們來了解一下不同的Swing菜單組件。
6.1.2 JMenuBar類
Swing的菜單欄組件是JMenuBar。其操作要求我們使用具有JMenuItem元素的JMenu元素來填充菜單欄。然后我們將菜單欄添加到JFrame或是其他的需要菜單欄的用戶界面組件上。菜單然后會依賴於SingleSelectionModel的幫助來確定在其選中之后顯示或是發送哪一個JMenu。
創建JMenuBar組件
JMenuBar具有一個無參數的構造函數:public JMenuBar()。一旦我們創建了菜單欄,我們就可以使用JApplet,JDialog,JFrame,JInternalFrame或是JRootPane的setJMenuBar()方法將其添加到一個窗口。
JMenuBar menuBar = new JMenuBar(); // Add items to it ... JFrame frame = new JFrame("MenuSample Example"); frame.setJMenuBar(menuBar);
通過系統提供的觀感類型,通過setJMenuBar()方法,菜單欄顯示在窗體的上部,窗體標題的下部(如果有)。其他的觀感類型,例如Macintosh的Aqua,會將菜單欄放在其他的位置。
我們也可以使用Container的add()方法將JMenuBar添加到窗口。當通過add()方法添加時,JMenuBar會通過Container的布局管理器進行管理。
在我們擁有一個JMenuBar之后,要使用其余的菜單類來填充菜單欄。
向菜單欄添加與移除菜單
我們需要將JMenu對象添加到JMenuBar。否則,所顯示只是沒有任何內容的邊框。向JMenuBar添加菜單只有一個方法:
public JMenu add(JMenu menu)
在默認情況下,連續添加的菜單會由左向右顯示。這會使得第一個添加到的菜單會是最左邊的菜單,而最后添加的菜單則是最右邊的菜單。在這兩者之間添加的菜單則會以其添加的順序進行顯示。例如,列表6-1中的示例程序,菜單的添加順序如下:
JMenu fileMenu = new JMenu("File"); menuBar.add(fileMenu); JMenu editMenu = new JMenu("Edit"); menuBar.add(editMenu);
除了JMenuBar的add()方法以外,由Container繼承的多個重載的add()方法可以菜單的位置進行更多的控制。其中最有趣的就是add(Component component, int index)方法,這個方法可以使得我們指定新的JMenu的顯示位置。使用第二個add()方法可以使得我們以不同的順序將File與Edit的JMenu組件放置在JMenuBar中,但是會得到相同的結果:
menuBar.add(editMenu); menuBar.add(fileMenu, 0);
如果我們已經向JMenuBar添加了一個JMenu組件,我們可以使用remove(Component component)或是由Container繼承的remove(int index)方法來移除菜單:
bar.remove(edit); bar.remove(0);
JMenuBar屬性
表6-1顯示了JMenuBar的11個屬性。其中的半數屬性是只讀的,只允許我們查詢當前的菜單欄狀態。其余的屬性允許我們通過確定菜單欄邊框是否繪制以及選擇菜單元素之間的空白尺寸來修改菜單欄的外觀。selected屬性與selectionModel可以控制菜單欄上當前被選中的菜單是哪一個。當被選中的組件設置為菜單欄上的一個菜單,菜單組件會以彈出菜單的方式顯示在窗口中。
JMenuBar屬性
屬性名 |
數據類型 |
可訪問性 |
accessibleContext |
AccessibleContext |
只讀 |
borderPainted |
boolean |
讀寫 |
component |
Component |
只讀 |
helpMenu |
JMenu |
只讀 |
margin |
Insets |
讀寫 |
menuCount |
int |
只讀 |
selected |
boolean/Component |
讀寫 |
selectionModel |
SingleSelectionModel |
讀寫 |
subElements |
MenuElement[] |
只讀 |
UI |
MenuBarUI |
讀寫 |
UIClassID |
String |
只讀 |
自定義JMenuBar觀感
每一個預定義的Swing觀感都為JMenuBar以及菜單組件提供了一個不同的外觀以及一個默認的UIResource值集合。圖6-3顯示了預安裝的觀感類型集合的菜單組件外觀:Motif,Windows以及Ocean。
考慮JMenuBar的特定外觀,表6-2顯示了UIResource相關屬性的集合。JMenuBar組件有12個屬性。
JMenuBar UIResource元素
屬性字符串 |
對象類型 |
MenuBar.actionMap |
ActionMap |
MenuBar.background |
Color |
MenuBar.border |
Border |
MenuBar.borderColor |
Color |
MenuBar.darkShadow |
Color |
MenuBar.font |
Font |
MenuBar.foreground |
Color |
MenuBar.gradient |
List |
MenuBar.highlight |
Color |
MenuBar.shadow |
Color |
MenuBar.windowBindings |
Object[] |
MenuBarUI |
String |
如果我們需要一個垂直菜單欄,而不是一個水平菜單欄,我們只需要簡單的改變菜單欄組件的LayoutManager。例如0行1列的GridLayout可以完成這個工作,如下面的示例所示,因為由於JMenu的添加,行數會無限增長:
import java.awt.*; import javax.swing.*; public class VerticalMenuBar extends JMenuBar { private static final LayoutManager grid = new GridLayout(0,1); public VerticalMenuBar() { setLayout(grid); } }
將圖6-1所示的菜單欄移動到BorderLayout的東側,並使用VerticalMenuBar來替換JMenuBar所產生的結果如圖6-4所示。盡管垂直菜單欄在這里看起來並不舒服,但是在窗口的右側(或左側)更需要使得菜單項目垂直堆放而不是水平堆放。然而,我們也許會需要修改MenuBar.border屬性來修改邊框。
6.1.3 SingleSelectionModel接口
SingleSelectionModel將索引描述為一個整數索引的數據結構,其中的元素可以被選中。接口后面的數據結構類似於數據或是向量,其中重復訪問相同位置可以獲得相同的對象。SingleSelectionModel接口是JMenuBar與JPopupMenu的選擇模型。在JMenuBar中,接口描述了當前被選中的需要繪制的JMenu。在JPopupMenu中,接口描述了當前被選中的JMenuItem。
SingleSelectionModel的接口定義如下:
public interface SingleSelectionModel { // Listeners public void addChangeListener(ChangeListener listener); public void removeChangeListener(ChangeListener listener); // Properties public int getSelectedIndex(); public void setSelectedIndex(int index); public boolean isSelected(); // Other Methods public void clearSelection(); }
正如我們所看到的,除了選擇索引外,接口需要維護一個當選擇索引變化時需要通知的ChangeListener列表。
默認的Swing提供的SingleSelectionModel實現是DefaultSingleSelectionModel類。對於JMenuBar與JPopupMenu,我們通常並不需要修改由其默認實現所獲得的選擇模型。
DefaultSingleSelectionModel實現管理一個ChangeListener對象列表。另外,模型使用-1來標識當前並沒有任何內容被選中。當選中的索引為-1時,isSelected()會返回false;否則,此方法會返回true。當選中索引變化時,所注冊的ChangeListener對象會得到通知。