本书的前面两章描述了一些低级的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对象会得到通知。