构成用户界面组件的各个组成部分,如按钮、复选框、文本域或复杂的树控件,每个组件都有三个特征:
这三个特征之间存在相当复杂的交互,即使是最简单的组件(如按钮)也能体现出这一点。很明显,按钮的外观显示取决于它的观感。Metal按钮的外观与Windows按钮或者
Moif按钮的外观就不一样。另外,外观显示还要取决于按钮的状态:当按钮被按下时,按钮需要重新绘制,使它看起来不一样。而状态取决于按钮接收到的事件。当用户在按钮上点击鼠标时,按钮就被按下。
Swing设计者采用了MVC模式,这种设计模式要求我们提供三个不同的对象:
这种模式明确地规定了三个对象如何交互。模型存储内容,它没有用户界面。按钮的内容非常简单,只有很少的一组标志,用来表示当前按钮是否按下,是否处于活动状态,等等。文本域的内容更有意思,它与内容的视图不同:如果内容的长度大于文本域的大小,用户就只能看到文本域可以显示的那一部分。
对大多数Swing组件来说,模型类将实现一个名字以Model
结尾的接口,在这里,接口就名为ButtonModel
接口。实现了此接口的类可以定义各种按钮的状态。实际上,按钮并不复杂,另外Swing库中有一个名为DefaultButtonModel
的类实现了这个接口。
可以通过查看ButtonModel接口的属性来了解按钮模型维护着什么类型的数据:
每个JButton对象都存储着一个按钮模型对象,可以如下访问:
var button = new JButton("Blue"); ButtonModel model = button.getModel();
实际上,不必关心按钮状态的详细信息,只有绘制它的视图才对此感兴趣。所有重要的信息(如按钮是否启用)可以通过JButton类得到(当然,JButton类会向它的模型获取这些信息)。
下面查看ButtonModel接口中不包含的信息。模型不存储按钮标签或者图标。对于一个按钮来说,仅凭模型无法知道它的外观。
另外还需要注意,同样的模型(即DefaultButtonModel)可用于下压按钮、单选按钮、复选框,甚至菜单项。当然,这些按钮都有各自不同的视图和控制器。当使用Metal观感时,JButton类用BasicButtonUI类作为其视图;用ButtonUIListener类作为其控制器。通常,每个Swing组件都有一个相关的后缀为UI的视图对象,但并不是所有的Swing组件都有专门的控制器对象。
JButton就是一个继承自JComponent的包装器类,其中包含DefaultButtonModel对象,一些视图数据(例如按钮标签和图标)以及一个负责按钮视图的BasicButtonUI对象。
通常,组件放置在容器中,布局管理器决定容器中组件的位置和大小。按钮、文本域和其他的用户界面元素都会扩展Component类,组件可以放置在容器(如面板)中。由于Container类扩展自Component类,所以容器本身也可以放置在另一个容器中。
每个容器都有一个默认的布局管理器,但可以重新进行设置:
panel.setLayout(new GridLayout(4, 4));
会使用GridLayout类按4行4列摆放组件。往容器中添加组件时,容器的add方法将把组件和所有位置要求传递给布局管理器。
java.awt.Container:
java.awt.FlowLayout
边框布局管理器(border layout manager)是每个JFrame的内容窗格的默认布局管理器。流布局管理器会完全控制每个组件的位置,边框布局管理器则不然,它允许为每个组件选择一个位置。可以选择把组件放在内容窗格的中央、北部、南部、东部或者西部。
例如:
frame.add(component, BorderLayout.SOUTH);
先放置边缘组件,剩余的可用空间由中间组件占据。当容器调整大小时,边缘组件的尺寸不会改变,而中间组件的大小会发生变化。添加组件时可以指定BorderLayout类的CENTER
、NORTH
、SOUTH
、EAST
和WEST
常量。不是所有的位置都会被占用,如果没有提供任何值,系统默认为CENTER。
与流布局不同,边框布局会扩展所有组件的尺寸以便填满可用空间(流布局将维持每个组件的最佳尺寸)。
添加一个按钮时,这会有问题:
frame.add(yellowButton, BorderLayout.SOUTH);
按钮会扩展至填满窗体的整个南部区域。而且,如果再将另外一个按钮添加到南部区域,就会取代第一个按钮。
public class BorderFrameDemo { public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new BorderFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } } class BorderFrame extends JFrame { private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 200; public BorderFrame() throws HeadlessException { setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); JButton button = new JButton("Button"); add(button, BorderLayout.SOUTH); } }
解决这个问题的常见方法是使用另外的面板(panel),因为Panel的默认布局管理器是FlowLayout(流式布局)。
public BorderFrame() throws HeadlessException { setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); JButton button1 = new JButton("Button1"); JButton button2 = new JButton("Button2"); JButton button3 = new JButton("Button3"); JPanel panel = new JPanel(); panel.add(button1); panel.add(button2); panel.add(button3); add(panel, BorderLayout.SOUTH); }
java.awt.BorderLayout:
网格布局像电子数据表一样,按行列排列所有的组件。不过,所有组件的大小都是一样的。
public class GridFrame extends JFrame { private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 200; public GridFrame() throws HeadlessException { setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); JButton b1 = new JButton("1"); JButton b2 = new JButton("2"); JButton b3 = new JButton("3"); JButton b4 = new JButton("4"); JButton b5 = new JButton("5"); JButton b6 = new JButton("6"); JButton b7 = new JButton("7"); JButton b8 = new JButton("8"); JButton b9 = new JButton("9"); setLayout(new GridLayout(3, 3)); add(b1); add(b2); add(b3); add(b4); add(b5); add(b6); add(b7); add(b8); add(b9); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new GridFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } }
java.awt.GridLayout:
这三个类都继承自JTextComponent类:
把文本域添加到窗口的常用办法是将它添加到一个面板或者其他容器中,这与添加按钮完全一样:
JPanel panel = new JPanel(); JTextField textField = new JTextField("Default input", 20); panel.add(textField);
这段代码将添加一个文本域,初始化时在其中放入字符串"Default input"。构造器的第二个参数设置了文本域的宽度。在这个示例中,宽度值为20“列”。但是,这里所说的列不是一个精确的测量单位。一列就是指当前使用的字体一个字符的宽度。
如果希望文本域最多能够输入n个字符,就应该把宽度设置为n列。在实际中,这样做效果并不理想,应该将最大输人长度再多加1~2个字符。另外要记住,列数只是给AWT设定首选(preferred)大小的一个提示。
如果布局管理器需要缩放这个文本域,它会调整文本域的大小。在JTextField的构造器中设定的宽度并不是用户能输入的字符个数的上限。用户仍然可以输入一个更长的字符串,但是当文本长度超过文本域长度时输入就会滚动。用户通常不喜欢滚动文本域,因此应该尽量把文本域设置得宽一些。如果需要在运行时重新设置列数,可以使用setColumns
方法。
可以在任何时候调用setText方法来改变文本域中的内容。
textField.setText("Hello!");
可以调用getText方法来获取用户键入的文本。这个方法原样返回用户输入的文本。如果想要将文本域中内容的前后空格去掉,可以对getText的返回值应用trim方法:
String text = textField.getText().trim();
如果想改变显示文本的字体,可以使用setFont方法。
javax.swing.JTextField:
javax.swing.JComponent:
java.awt.Component:
标签是容纳文本的组件,它们没有任何的修饰(例如没有边缘),也不能响应用户输入。
可以利用标签标识组件。例如,与按钮不同,文本域没有标识它们的标签。要想用标识符标识这种本身不带标签的组件:
JLabel的构造器允许指定初始文本和图标,也可以选择内容的排列方式。可以用SwingConstants
接口中的常量来指定排列方式。在这个接口中定义了几个很有用的常量,如LEFT
、RIGHT
、CENTER
、NORTH
、EAST
等。
JLabel类是实现这个接口的众多Swing类之一。因此,
可以指定右对齐标签:
JLabel label = new JLabel("User name:", SwingConstants.RIGHT); //或者 JLabel label = new JLabel("User name:", JLabel.RIGHT);
利用setText和setIcon方法可以在运行期间设置标签的文本和图标。
javax.swing.JLabel:
密码域是一种特殊类型的文本域。为了避免有不良企图的人站在一旁看到密码,用户输入的字符不真正显示出来。每个输入的字符都用回显字符(echo character)表示,典型的回显字符是星号(*)。
Swing提供了JPasswordField类来实现这样的文本域。
密码域采用与常规文本域相同的模型来存储数据,但是,它的视图却改为显示回显字符,而不是实际的字符。
javax.swing.JPasswordField:
在JTextArea组件的构造器中,可以指定文本区的行数
和列数。例如:
textArea = new JTextArea(8, 40);
这里参数columns与之前的做法相同,而且出于稳妥的考
虑,应该再增加几列。另外,用户并不受限于输入指定的行数和列数。当输入过长时,文本会滚动。还可以用setColumns方法改变列数,用setRows方法改变行数。这些值只是指示首选大小—布局管理器可能会对文本区进行缩放。
如果文本区的文本超出显示的范围,那么剩下的文本就会被剪裁掉。可以通过开启换行特性来避免裁剪过长的行:
textArea.setLineWrap(true);
换行只是视觉效果,文档中的文本没有改变,并没有在文本中自动插入'\n'
字符。
在Swing中,文本区没有滚动条。如果需要滚动条,可以将文本区放在滚动窗格(scroll pane)中。
textArea = new JTextArea(8, 40); JScrollPane scrollPane = new JScrollPane(textArea);
现在滚动窗格管理文本区的视图。如果文本超出了文本区可以显示的范围,滚动条就会自动地出现,删除部分文本后,如果文本能够显示在文本区范围内,滚动条会再次自动地消失。滚动是由滚动窗格内部处理的,编写程序时无须处理滚动事件。
这是一种适用于所有组件的通用机制,而不是文本区特有的。也就是说,要想为组件添加滚动条,只需将它们放入一个滚动窗格中即可。
public class TextComponentFrame extends JFrame { private static final int TEXTAREA_ROWS = 8; private static final int TEXTAREA_COLUMNS = 20; private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 600; public TextComponentFrame() throws HeadlessException { setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); //North JTextField textField = new JTextField(); JPasswordField passwordField = new JPasswordField(); JPanel northPanel = new JPanel(); northPanel.setLayout(new GridLayout(2, 2)); northPanel.add(new JLabel("User name:", SwingConstants.RIGHT)); northPanel.add(textField); northPanel.add(new JLabel("Password:", SwingConstants.RIGHT)); northPanel.add(passwordField); add(northPanel, BorderLayout.NORTH); //Center JTextArea textArea = new JTextArea(TEXTAREA_ROWS, TEXTAREA_COLUMNS); JScrollPane scrollPane = new JScrollPane(textArea); add(scrollPane, BorderLayout.CENTER); //South JPanel southPanel = new JPanel(); JButton insertButton = new JButton("Insert"); southPanel.add(insertButton); insertButton.addActionListener(event -> textArea.append("User name:" + textField.getText() + " Password:" + new String(passwordField.getPassword()) + "\n")); add(southPanel, BorderLayout.SOUTH); pack(); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new TextComponentFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } }
javax.swing.JTextArea:
javax.swing.JScrollPane:
如果想要接收的输入只是“是”或“否”,就可以使用复选框组件。复选框自动带有标识标签。用户通过点击一个复选框将它选中,再点击则取消选中。当复选框获得焦点时,用户也可以通过按空格键来切换选择。
复选框需要一个紧邻的标签来说明其用途。在构造器中指定标签文本。
bold = new JCheckBOx("Bold");
可以使用setSelected方法来选中或取消选中复选框:
bold.setSelected(true);
isSelected方法将获取每个复选框的当前状态。如果没有选中则为false,如果选中这个复选框则为true。
当用户点击复选框时将触发一个动作事件。与以往一样,可以为复选框关联一个动作监听器。
public class CheckBoxFrame extends JFrame { private JLabel label; private JCheckBox bold; private JCheckBox italic; private static final int FONTSIZE = 24; public CheckBoxFrame() throws HeadlessException { label = new JLabel("The quick brown fox jumps over the lazy dog."); label.setFont(new Font("Serif", Font.BOLD, FONTSIZE)); add(label, BorderLayout.CENTER); ActionListener listener = event -> { int mode = 0; if (bold.isSelected()) { mode += Font.BOLD; } if (italic.isSelected()) { mode += Font.ITALIC; } label.setFont(new Font("Serif", mode, FONTSIZE)); }; JPanel buttonPanel = new JPanel(); bold = new JCheckBox("Bold"); bold.addActionListener(listener); bold.setSelected(true); buttonPanel.add(bold); italic = new JCheckBox("Italic"); italic.addActionListener(listener); buttonPanel.add(italic); add(buttonPanel, BorderLayout.SOUTH); pack(); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new CheckBoxFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } }
javax.swing.JCheckBox:
在很多情况下,我们需要用户只选择几个选项当中的一个。当用户选择另一项的时候,前一项就自动地取消选中。这样一组选项通常称为单选按钮组(Radio Button Group)。
在Swing中实现单选按钮组非常简单。为单选按钮组构造一个ButtonGroup类型的对象。然后,再将JRadioButton类型的对象添加到按钮组中。按钮组对象负责在新按钮被按下时取消前一个选中按钮的选择状态。
注意,按钮组仅仅控制按钮的行为,如果想把这些按钮摆放在一起,需要把它们添加到容器中,和JPanel一样。
public class RadioButtonFrame extends JFrame { private JPanel buttonPanel; private ButtonGroup group; private JLabel label; private static final int DEFAULT_SIZE = 36; public RadioButtonFrame() { label = new JLabel("The quick brown fox jumps over the lazy dog."); label.setFont(new Font("Serif", Font.PLAIN, DEFAULT_SIZE)); add(label, BorderLayout.CENTER); //添加按钮组 buttonPanel = new JPanel(); group = new ButtonGroup(); addRadioButton("Small", 8); addRadioButton("Medium", 12); addRadioButton("Large", 18); addRadioButton("Extra", 36); add(buttonPanel, BorderLayout.SOUTH); pack(); } public void addRadioButton(String name, int size) { boolean selected = size == DEFAULT_SIZE; JRadioButton button = new JRadioButton(name, selected); group.add(button); buttonPanel.add(button); button.addActionListener(event -> { label.setFont(new Font("Serif", Font.PLAIN, size)); }); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new RadioButtonFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } }
javax.swing.JRadioButton:
javax.swing.ButtonGroup:
javax.swing.ButtonModel:
javax.swing.AbstractButton:
如果在一个窗口中有多组单选按钮,就需要用可见的方式来指明哪些按钮属于同一组。Swing提供了一组很有用的边框(border)来解决这个问题。可以在任何继承了JComponent的组件上应用边框。最常用的用法是在面板周围放置一个边框,然后用其他用户界面元素(如单选按钮)填充面板。
有几种不同的边框可供选择,但是使用它们的步骤完全一样:
把一个带有标题的蚀刻边框添加到一个面板:
Border etched = BorderFactory.createEtchedBorder(); Border titled = BorderFactory.createTitledBorder(etched, "A Title"); panel.setBorder(titled);
java.swing.BorderFactory:
LEFT
、CENTER
、RIGHT
、LEADING
、TRAILING
或DEFAULT_JUSTIFICATION(左X对齐)
之一,position是ABOVE_TOP
、TOP
、BELOW_TOP
、ABOVE_BOTTOM
、BOTTOM
、BELOW_BOTTOM
或DEFAULT_POSITION(上)
之一。javax.swing.border.SoftBevelBorder:
javax.swing.border.LineBorder:
javax.swing.JComponent:
如果有多个选择项,使用单选按钮就不太适宜了,原因是会占据太多屏幕空间。这时就可以选择组合框。当用户点击这个组件时,会下拉一个选择列表,用户可以从中选择一项。
如果下拉列表框被设置成可编辑(editable),就可以像这
是一个文本域一样编辑当前的选项内容。鉴于这个原因,这种组件被称为组合框(combo box),它将文本域的灵活性与一组预定义的选项组合起来。JComboBox类提供了组合框组件。
在Java7中,JComboBox类是一个泛型类。例如,JComboBox
包含String类型的对象,JComboBox
包含整数。
调用setEditable方法可以让组合框可编辑。注意,编辑
只会影响选择的项,而不会改变选择列表的内容。
可以调用getSelectedItem方法获取当前的选项,如果组合框是可编辑的,当前选项可能已经编辑过。不过,对于可编辑组合框,其中的选项可以是任何类型,这取决于编辑器(即由编辑器获取用户输入并将结果转换为一个对象)。如果组合框不是可编辑的,最好调用:
combo.getItemAt(combo.getSelectedIndex());
public class ComboBoxFrame extends JFrame { private JComboBox faceCombo; private JLabel label; private static final int DEFAULT_SIZE = 24; public ComboBoxFrame() { //添加一个标签 label = new JLabel("The quick brown fox jumps over the lazy dog."); label.setFont(new Font("Serif", Font.PLAIN, DEFAULT_SIZE)); add(label, BorderLayout.CENTER); //添加组合框 faceCombo = new JComboBox<>(); faceCombo.addItem("Serif"); faceCombo.addItem("SansSerif"); faceCombo.addItem("Monospaced"); faceCombo.addItem("Dialog"); faceCombo.addItem("DialogInput"); faceCombo.addActionListener(event -> { label.setFont(new Font(faceCombo.getItemAt(faceCombo.getSelectedIndex()), Font.PLAIN, DEFAULT_SIZE)); }); JPanel comboPanel = new JPanel(); comboPanel.add(faceCombo); add(comboPanel, BorderLayout.SOUTH); pack(); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new ComboBoxFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } }
javax.swing.JComboBox:
滑动条允许用户从连续值中进行选择。
JSlider slider = new JSlider(min, max, initialValue);
如果省略最小值、最大值和初始值,其默认值分别为0、100和50。或者如果需要垂直滑动条,可以按照以下方式调用构造器:
JSlider slider = new JSlider(SwingConstants.VERICAL, min, max, initialValue);
当用户滑动滑动条时,滑动条的值就会在最小值和最大值之间变化。当值发生变化时,ChangeEvent就会发送给所有变更监听器。为了得到这些变更的通知,需要调用addChangeListener方法并且安装一个实现了
ChangeListener接口的对象。在这个回调中,可以获取滑
动条的值:
ChangeListener listener = event -> { JSlider slider = (JSlider)event.getSource(); int value = slider.getValue(); ... };
可以通过显示刻度(tick)对滑动条进行修饰:
slider.setMajorTickSpacing(20); slider.setMinorTickSpacing(5);
上述滑动条在每20个单位的位置显示一个大刻度标记,每5个单位的位置显示一个小刻度标记。所谓单位是指滑动条值,而不是像素。这些指令只设置了刻度标记的单位数,要想将它们真正显示出来,还需要调用:
slider.setPaintTicks(true);
大刻度和小刻度标记是相互独立的。例如,可以每20个单位设置一个大刻度标记,同时每7个单位设置一个小刻度尺标记,但是这样设置滑动条看起来会显得非常凌乱。
可以强制滑动条对齐刻度(snap to tick)。这样一来,只要用户采用对齐模式完成拖放滑动条的操作,它就会立即自动地移到最接近的刻度。激活这种模式需要调用:
slider.setPaintLabels(true);
public class SliderFrame extends JFrame { private JPanel sliderPanel; private JTextField textField; private ChangeListener listener; public SliderFrame() { sliderPanel = new JPanel(); sliderPanel.setLayout(new GridBagLayout()); //滑动条的通用监听器 listener = event -> { JSlider source = (JSlider)event.getSource(); textField.setText("" + source.getValue()); }; //滑动条1 JSlider slider = new JSlider(); addSlider(slider, "Plain"); //滑动条2 slider = new JSlider(); slider.setPaintTicks(true); slider.setMajorTickSpacing(20); slider.setMinorTickSpacing(5); addSlider(slider, "Ticks"); //滑动条3 slider = new JSlider(); slider.setPaintTicks(true); slider.setSnapToTicks(true); slider.setMajorTickSpacing(20); slider.setMinorTickSpacing(5); addSlider(slider, "Snap to ticks"); //滑动条4 slider = new JSlider(); slider.setPaintTicks(true); slider.setMajorTickSpacing(20); slider.setMinorTickSpacing(5); slider.setPaintTrack(false); addSlider(slider, "No track"); //滑动条5 slider = new JSlider(); slider.setPaintTrack(true); slider.setMajorTickSpacing(20); slider.setMinorTickSpacing(5); slider.setInverted(true); addSlider(slider, "Inverted"); //滑动条6 slider = new JSlider(); slider.setPaintTicks(true); slider.setPaintLabels(true); slider.setMajorTickSpacing(20); slider.setMinorTickSpacing(5); addSlider(slider, "Labels"); //滑动条7 slider = new JSlider(); slider.setPaintLabels(true); slider.setPaintTicks(true); slider.setMajorTickSpacing(20); slider.setMinorTickSpacing(5); Hashtable labelTable = new Hashtable<>(); labelTable.put(0, new JLabel("A")); labelTable.put(20, new JLabel("B")); labelTable.put(40, new JLabel("C")); labelTable.put(60, new JLabel("D")); labelTable.put(80, new JLabel("E")); labelTable.put(100, new JLabel("F")); slider.setLabelTable(labelTable); addSlider(slider, "Custom labels"); //滑动条8 slider = new JSlider(); slider.setPaintTicks(true); slider.setPaintLabels(true); slider.setSnapToTicks(true); slider.setMajorTickSpacing(20); slider.setMinorTickSpacing(20); labelTable = new Hashtable<>(); labelTable.put(0, new JLabel(new ImageIcon("icon.png"))); labelTable.put(20, new JLabel(new ImageIcon("icon.png"))); labelTable.put(40, new JLabel(new ImageIcon("icon.png"))); labelTable.put(60, new JLabel(new ImageIcon("icon.png"))); labelTable.put(80, new JLabel(new ImageIcon("icon.png"))); labelTable.put(100, new JLabel(new ImageIcon("icon.png"))); slider.setLabelTable(labelTable); addSlider(slider, "Icon labels"); textField = new JTextField(); add(sliderPanel, BorderLayout.CENTER); add(textField, BorderLayout.SOUTH); pack(); } public void addSlider(JSlider slider, String description) { slider.addChangeListener(listener); JPanel panel = new JPanel(); panel.add(slider); panel.add(new JLabel(description)); panel.setAlignmentX(Component.LEFT_ALIGNMENT); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridy = sliderPanel.getComponentCount(); gbc.anchor = GridBagConstraints.WEST; sliderPanel.add(panel, gbc); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new SliderFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } }
javax.swing.JSlider:
位于窗口顶部的莱单栏(menu bar)包括了下拉菜单的名
字。点击一个名字就可以打开包含菜单项(menu item)和子菜单(submenu)的菜单。当用户点击菜单项时,所有的菜单都会被关闭并且将一条消息发送给程序。
构建菜单是一件非常容易的事情。首先要创建一个菜单栏:
var menuBar = new JMenuBar();
菜单栏是一个可以添加到任何位置的组件。正常情况下会放置在窗体的顶部。可以调用setJMenuBar方法将菜单栏添加到这里:
frame.setJMenuBar(menuBar);
需要为每个菜单创建一个菜单对象:
var editMenu = new JMenu("Edit");
然后将顶层菜单添加到菜单栏中:
menuBar.add(editMenu);
向菜单对象中添加菜单项、分隔符和子菜单:
var pasteItem new JMenuItem("Paste"); editMenu.add(pasteItem); editMenu.addSeparator(); JMenu optionsMenu =...;//a submenu editMenu.add(optionsMenu);
当用户选择菜单时,将触发一个动作事件。需要为每个菜单项安装一个动作监听器:
ActionListener listener =... pasteItem.addActionListener(listener);
JMenu.add(String s)方法可以很方便地将菜单项增加到菜单的尾部,例如:
editMenu.add("Paste");
add方法返回创建的菜单项。可以获取这个菜单项,并添加监听器,如下所示:
JMenuItem pasteItem = editMenu.add("Paste"); pasteItem.addActionListener(listener);
在通常情况下,菜单项触发的命令也可以通过其他用户界面元素(如工具条按钮)激活。要定义一个实现Action接
口的类,为此通常会扩展便利的AbstractAction类,在AbstractAction对象的构造器中指定菜单项标签,并且覆盖actionPerformed方法来获得菜单动作处理器。例如:
var exitAction new AbstractAction("Exit") { //menu item text goes here public void actionPerformed(ActionEvent event){ //action code goes here System.exit(0); } };
然后将动作添加到菜单中:
JMenuItem exitItem = fileMenu.add(exitAction);
这个命令利用动作名将一个菜单项添加到菜单中。这个动作对象将作为它的监听器。上面这条语句是下面两条语句的快捷形式:
var exitItem = new JMenuItem(exitAction); fileMenu.add(exitItem)
javax.swing.JMenu:
javax.swing.JMenuItem:
javax.swing.AbstractButton:
javax.swing.JFrame:
菜单项与按钮很相似。实际上,JMenuItem类扩展了AbstractButton类。与按钮一样,菜单可以只包含文本标签、只包含图标,或者两者都包含。可以使用JMenuItem(String,Icon)或者JMenuItem(Icon)构造器为菜单指定一个图标,也可以使用JMenuItem类从AbstractButton类继承的setIcon方法指定一个图标。例如:
var cutItem = new JMenuItem("Cut",new ImageIcon("cut.gif"));
在默认情况下,菜单项文本放在图标的右侧。如果喜欢将文本放置在左侧,可以调用JMenuItem类从AbstractButton类继承的setHorizontalTextPosition方法。例如:
cutItem.setHorizontalTextPosition(SwingConstants.LEFT);
这个调用把菜单项文本移动到图标的左侧。也可以为动作增加一个图标:
cutAction.putValue(Action.SMALL ICON,new ImageIcon("cut.gif"));
当使用动作构造菜单项时,Action.NAME值将会作为菜单项的文本,而Action.SMALL ICON将会作为图标。
或者,可以在AbstractAction构造器中设置图标:
cutAction = new AbstractAction("Cut",new ImageIcon("cut.gif")) { public void actionPerformed(ActionEvent event){ ... } };
javax.swing.JMenuItem:
javax.swing.AbstractButton:
javax.swing.AbstractAction:
复选框和单选按钮菜单项会在菜单名旁边显示了一个复选框或一个单选按钮。当用户选择一个菜单项时,菜单项就会自动地在选择和未选择间进行切换。
除了按钮装饰外,复选框和单选按钮菜单项同其他菜单项的处理一样。例如,可以如下创建复选框菜单项:
var readonlyItem = new JCheckBoxMenuItem("Read-only"); optionsMenu.add(readonlyItem);
单选按钮菜单项与普通单选按钮的工作方式一样,必须将它们加入到按钮组中。当按钮组中的一个按钮被选中时,其他按钮都自动地变为未选中。
var group = new ButtonGroup(); var insertItem = new JRadioButtonMenuItem("Insert"); insertItem.setSelected(true); var overtypeItem = new JRadioButtonMenuItem("Overtype"); group.add(insertItem); group.add(overtypeItem); optionsMenu.add(insertItem) optionsMenu.add(overtypeItem);
使用这些菜单项,不需要立刻得到用户选择菜单项的通知。实际上,可以使用isSelected方法来测试菜单项的当前状态(当然,这意味着应该保留这个菜单项的一个引用,保存在一个实例字段中)。可以使用setSelected方法设置状态。
javax.swing.JCheckBoxMenuItem:
javax.swing.JRadioButtonMenuItem:
javax.swing.AbstractButton:
弹出菜单(pop-up menu)是不固定在菜单栏中随处浮动的菜单。
创建一个弹出菜单与创建一个常规菜单的方法类似,但
是弹出菜单没有标题。
var popup = new JPopupMenu();
然后用常规的方法添加菜单项:
var item = new JMenultem("Cut"); item.addActionListener(listener); popup.add(item);
弹出菜单并不像常规菜单栏那样总是显示在窗体的顶部,必须调用show方法显式地显示弹出菜单。需要指定父组件,并使用父组件的坐标系统指定弹出菜单的位置。例如:
popup.show(panel,x,y);
通常,你可能希望当用户点击某个鼠标键时弹出菜单,这就是所谓的弹出式触发器(pop-up trigger)。在Windows或者Linux中,弹出式触发器是鼠标次键(通常是右键)。要想在用户点击一个组件时弹出菜单,只需要调用方法:
component.setComponentPopupMenu(popup);
偶尔也可能需要把一个组件放在另一个带弹出菜单的组件中。通过调用以下方法,这个子组件可以继承父组件的弹出菜单:
child.setInheritsPopupMenu(true);
javax.swing.JPopuMenu:
java.awt.event.MouseEvent:
javax.swing.JComponent:
对于有经验的用户来说,通过键盘助记符选择菜单项确实非常便捷。可以通过在菜单项的构造器中指定一个助记字母来为菜单项设置键盘助记符:
var aboutItem = new JMenuItem("About",'A');
键盘助记符会在菜单中自动显示,助记字母下面有一条下划线。例如,在上面的例子中,菜单项中的标签显示为“About”,字母A带有一个下划线。菜单显示时,用户只需要按下“A”键就可以选择这个菜单项(如果助记字母不在菜单字符串中,同样可以按下这个字母选择菜单项,不过助记符不会在菜单中,同样可以按下这个字母选择菜单项,不过助记符不会在菜单中显示。
javax.swing.JMenuItem:
javax.swing.AbstractButton:
有些时候,某个特定的菜单项可能只在某种特定的环境下才能选择。例如,当文档以只读方式打开时,Save菜单项就没有意义。当然,可以使用JMenu.remove方法将这个菜单项从菜单中删掉,但用户会对内容不断变化的菜单感到奇怪。实际上,最好禁用这个菜单项,以免触发暂时不适用的命令。被禁用的菜单项显示为灰色,不允许选择。
启用或禁用菜单项需要调用setEnabled方法:
saveItem.setEnabled(false);
启用和禁用菜单项有两种策略。每次环境发生变化时,就可以对相关的菜单项或动作调用setEnabled。例如:一旦文档以只读方式打开,可以找到并禁用Save和Save As菜单项。另一种方法是在显示菜单之前禁用这些菜单项。为此,必须为“菜单选中”事件注册一个监听器。javax.swing.event包定义了一个MenuListener接口,它包含三个方法:
menuSelected方法在菜单显示之前调用,所以可以用这个方法禁用或启用菜单项。下面的代码显示了选中只读复选框菜单项时如何禁用Save和Save As动作。
public void menuSelected(MenuEvent event) { saveAction.setEnabled(!readonlyItem.isSelected()); saveAsAction.setEnabled(!readonlyItem.isSelected()); }
javax.swing.JMenuItem:
javax.swing.event.MenuListener:
public class MenuFrame extends JFrame { private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 200; private Action saveAction; private Action saveAsAction; private JCheckBoxMenuItem readonlyItem; private JPopupMenu popup; class TestAction extends AbstractAction { public TestAction(String name) { super(name); } @Override public void actionPerformed(ActionEvent e) { System.out.println(getValue(Action.NAME) + " selected."); } } public MenuFrame() { setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); JMenu fileMenu = new JMenu("File"); fileMenu.add(new TestAction("New")); JMenuItem openItem = fileMenu.add(new TestAction("Open")); openItem.setAccelerator(KeyStroke.getKeyStroke("ctrl 0")); fileMenu.addSeparator(); saveAction = new TestAction("Save"); JMenuItem saveItem = fileMenu.add(saveAction); saveItem.setAccelerator(KeyStroke.getKeyStroke("ctrl S")); saveAsAction = new TestAction("Save As"); fileMenu.add(saveAction); fileMenu.addSeparator(); fileMenu.add(new AbstractAction("Exit") { @Override public void actionPerformed(ActionEvent e) { System.exit(0); } }); readonlyItem = new JCheckBoxMenuItem("Read-only"); readonlyItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { boolean saveOk = !readonlyItem.isSelected(); saveAction.setEnabled(saveOk); saveAction.setEnabled(saveOk); } }); ButtonGroup group = new ButtonGroup(); JRadioButtonMenuItem insertItem = new JRadioButtonMenuItem("Insert"); insertItem.setSelected(true); JRadioButtonMenuItem overtypeItem = new JRadioButtonMenuItem("Overtype"); group.add(insertItem); group.add(overtypeItem); TestAction cutAction = new TestAction("Cut"); cutAction.putValue(Action.SMALL_ICON, new ImageIcon("icon.png")); TestAction copyAction = new TestAction("Copy"); copyAction.putValue(Action.SMALL_ICON, new ImageIcon("icon.png")); TestAction pasteAction = new TestAction("Paste"); pasteAction.putValue(Action.SMALL_ICON, new ImageIcon("icon.png")); JMenu editMenu = new JMenu("Edit"); editMenu.add(cutAction); editMenu.add(copyAction); editMenu.add(pasteAction); JMenu optionMenu = new JMenu("Options"); optionMenu.add(readonlyItem); optionMenu.addSeparator(); optionMenu.add(insertItem); optionMenu.add(overtypeItem); editMenu.addSeparator(); editMenu.add(optionMenu); JMenu helpMenu = new JMenu("Help"); helpMenu.setMnemonic('H'); JMenuItem indexItem = new JMenuItem("Index"); indexItem.setMnemonic('I'); helpMenu.add(indexItem); TestAction aboutAction = new TestAction("About"); aboutAction.putValue(Action.MNEMONIC_KEY, new Integer('A')); helpMenu.add(aboutAction); JMenuBar menuBar = new JMenuBar(); setJMenuBar(menuBar); menuBar.add(fileMenu); menuBar.add(editMenu); menuBar.add(helpMenu); popup = new JPopupMenu(); popup.add(cutAction); popup.add(copyAction); popup.add(pasteAction); JPanel panel = new JPanel(); panel.setComponentPopupMenu(popup); add(panel); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new MenuFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } }
工具条是一个按钮条,通过它可以快速访问程序中最常用的命令,工具条的特殊之处在于可以将它随处移动。可以将它拖曳到窗体的四个边框上。释放鼠标按钮后,工具条将会落在新的位置上。
工具条甚至可以完全脱离窗体。这种分离的工具条包含在自己的窗体中,关闭包含分离式工具条的窗体时,工具条会回到原窗体中。
可以将组件添加到工具条:
JToolBar toolbar = new JToolBar(); toolbar.add(blueButton);
JToolBar类还有一个添加Action对象的方法,可以用Action对象填充工具条,如下所示:
toolbar.add(blueAction);
这个动作的小图标将会出现在工具条中。可以用分隔符将按钮分组:
toolbar.addSeparator();
还可以指定工具条的标题,当工具条未锁定时就会显示这个标题:
toolbar new JToolBar(titleString);
在默认情况下,工具条初始为水平的。如果希望工具条初始是垂直的,可以使用以下代码:
toolbar new JToolBar(SwingConstants.VERTICAL)
或者
toolbar = new JToolBar(titleString,SwingConstants.VERTICAL)
按钮是工具条中最常用的组件。不过对于工具条中可以增加哪些组件并没有任何限制。例如,可以在工具条中加入组合框。
工具条有一个缺点,这就是用户常常需要猜测工具条中小图标的含义。为了解决这个问题,用户界面设计者发明了工具提示(tooltip)。当光标在一个按钮上停留片刻时,工具提示就会被激活。工具提示文本显示在一个有颜色的矩形里。当用户移开鼠标时,工具提示就会消失。
在Swing中,可以调用setToolTipText方法将工具提示添
加到任何JComponent上:
exitButton.setToolTipText("Exit");
或者,如果使用Action对象,可以用SHORT_DESCRIPTION关联工具提示:
exitAction.putValue(Action.SHORT DESCRIPTION,"Exit");
javax.swing.JToolBar:
javax.swing.JComponent:
自从Java 1.0以来,AWT就含有网格包布局(grid bag layout),这种布局将组件按行和列排列。行和列的大小可以灵活改变,而且组件可以跨多行多列。这种布局管理器非常灵活,但也非常复杂。仅仅提到“网格包布局”一词就会让一些Java程序员胆战心惊。
Swig设计者有一个失败的尝试:为了设计一个布局管理器,能够将程序员从使用网格包布局的麻烦中解脱出来,他们提出了一种箱式布局(box layout)。根据BoxLayout类的JDK文档所述:“采用水平和垂直[sic]的不同组合嵌套多个面板可以获得与GridBagLayout类似的效果,而且降低了复杂度。”不过,由于每个箱子是独立放置的,所以不能使用箱式布局排列水平和垂直方向都相邻的组件。
Java 1.4还做了一个尝试:设计网格包布局的一种替代布局一弹性布局(spring layout)。这种布局使用假想的弹簧连接一个容器中的组件。当容器改变大小时,弹簧会伸展
或收缩,从而调整组件的位置。这听起来似乎很枯燥而且让人很困惑,其实也确实如此。弹性布局很快就变得含糊不清。
NetBeans IDE结合了一个布局工具(名为“Matisse”)和一个布局管理器。用户界面设计者可以使用工具将组件拖放到一个容器中,并指出组件的排列方式。工具再将设计者的意图转换成组布局管理器可以理解的指令。与手动编写布局管理代码相比,这样要便捷得多。
网格包布局是所有布局管理器之母。可以将网格包布局看成是没有任何限制的网格布局。在网格包布局中,行和列的大小可以改变。可以将相邻的单元合并以适应较大的组件(很多字处理器以及HTML为表格提供了类似的功能:可以先建立一个表格,然后根据需要合并相邻的单元格)。组件不需要填充整个单元格区域,而且可以指定它们在单元格内的对齐方式。
考虑上图所示的字体选择器,其中包含下面的组件:
现在将容器分解为由单元格组成的网格,(行和列的大小不必相同)。每个复选框横跨两列,文本区跨四行:
为了向网格包管理器描述这个布局,需要完成以下过程:
add(component,constraints);
下面给出所需的示例代码:
var layout = new GridBagLayout(); panel.setLayout(layout); var constraints = new GridBagConstraints(); constraints.weightx 100; constraints.weighty 100; constraints.gridx 0; constraints.gridy 2; constraints.gridwidth 2; constraints.gridheight 1; panel.add(component,constraints);
gridx、gridy、gridwidth和gridheight约束定义了组件在网格中的位置:
网格的坐标从0开始。具体地,gridx=0
和gridy=0
指示最左上角。例如,示例程序中,文本区的gridx=2
,gridy=0
。这是因为这个文本区起始于0行2列(即第3列),它的girdwidth=1, gridheight=4
,因为它占据4行1列。
总是需要为网格包布局中的每个区域设置增量字段(weightx和weighty)。如果将增量设置为0,那么这个区域在该方向上永远为初始大小,不会扩大或收缩。
另一方面,如果将所有区域的增量都设置为0,容器就会挤在所分配区域的中间,而不会拉伸来填充空间。
从概念上讲,增量参数的问题在于增量是行和列的属性,而不是各个单元格的属性。但你却需要为单元格指定增量,因为网格包布局并不对外提供行和列。行和列的增量等于每行或每列中单元格增量的最大值。因此,如果想让一行或一列的大小保持不变,就需要将其中所有组件的增量都设置为0。
注意,增量并不实际给出列的相对大小。当容器超过首选大小时,增量会指出“闲散”空间按什么比例分配给各个区域。这么说不太直观。我们建议将所有的增量设置为10,然后运行程序,查看布局情况。调整这个对话框的大小,再来看行和列是如何调整的。如果发现某行或某列不应该扩大,就将那一行或那一列中的所有组件的增量设置为0。也可以调整为其他增量值,但是那么做的意义并不大。
如果不希望组件拉伸至填满整个区域,就需要设置fill约束。这个参数有4个可能的取值:
如果组件没有填充整个区域,可以通过设置anchor字段指定它在这个区域中的位置。有效值为:
可以通过设置GridBagConstraints的insets字段在组件周围增加额外的空白区域。通过设置Insets对象的left、top、right和bottom值指定组件周围的空间大小。这被称作外部填充(external padding)。
ipadx和ipady值可以指定内部填充(internal padding)。这两个值会加到组件的最小宽度和最小高度上,这样可以保证组件不会收缩至其最小尺寸以下。
AWT文档建议不要将gridx和gridy设置为绝对位置,而应该将它们设置为常量GridBagConstraints.RELATIVE
。然后,按照标准的顺序将组件添加到网格包布局中,即首先在第一行从左向右增加,然后再转到新的一行,依此类推。
还需要为gridheight和gridwidth字段提供适当的值来指定组件所跨的行数和列数。不过,如果组件扩展至最后一行或最后一列,则不需要指定具体的数,而是可以使用常量
GridBagConstraints.REMAINDER,这样会告诉布局管理器这个组件是该行上的最后一个组件。
这种方案看起来是可行的,但似乎有点笨拙。这是因为这样做会对布局管理器隐藏具体的位置信息,并希望它能够重新发现这些信息。
在实际中,利用下面的技巧,可以让网格包布局的使用没那么麻烦:
网格包布局最乏味的方面就是要编写代码设置约束。为此,大多数程序员会编写帮助函数或者一个小帮助类。下面将在字体对话框示例的代码后面给出一个帮助类。这个类有以下特性:
add(component,new GBC(1,2))
有两个构造器可以用来设置最常用的参数:gridx和gridy,或者gridx、gridy、gridwidth和gridheight
add (component,new GBC(1,2,1,4))
对于以xy值对出现的字段,提供了便捷的设置方法:
add(component,new GBC(1,2).setweight(100,100));
设置器方法将返回this,所以可以串链这些方法调用:
add(component,new GBC(1,2).setAnchor(GBC.EAST).setWeight(100,100));
setInsets方法将构造Insets对象。要想获取1个像素的insets,可以调用:
add(component,new GBC(1,2).setAnchor(GBC.EAST).setInsets(1));
public class GBC extends GridBagConstraints { public GBC(int gridx, int gridy) { this.gridx = gridx; this.gridy = gridy; } public GBC(int gridx, int gridy, int gridwidth, int gridheight) { this.gridx = gridx; this.gridy = gridy; this.gridwidth = gridwidth; this.gridheight = gridheight; } public GBC setAnchor(int anchor) { this.anchor = anchor; return this; } public GBC setFill(int fill) { this.fill = fill; return this; } public GBC setWeight(double weightx, double weighty) { this.weightx = weightx; this.weighty = weighty; return this; } public GBC setInsets(int distance) { this.insets = new Insets(distance, distance, distance, distance); return this; } public GBC setInsets(int top, int left, int bottom, int right) { this.insets = new Insets(top, left, bottom, right); return this; } public GBC setIpad(int ipadx, int ipady) { this.ipadx = ipadx; this.ipady = ipady; return this; } }
public class FontFrame extends JFrame { public static final int TEXT_ROWS = 10; public static final int TEXT_COLUMS = 20; private JComboBox face; private JComboBox size; private JCheckBox bold; private JCheckBox italic; private JTextArea sample; public FontFrame() { GridBagLayout layout = new GridBagLayout(); setLayout(layout); ActionListener listener = event -> updateSample(); JLabel faceLabel = new JLabel("Face:"); face = new JComboBox<>(new String[] {"Serif", "SansSerif", "Monospaced", "Dialog", "DialogInput"}); face.addActionListener(listener); JLabel sizeLabel = new JLabel("Size:"); size = new JComboBox<>(new Integer[]{8, 10, 12, 15, 18, 24, 36, 48}); size.addActionListener(listener); bold = new JCheckBox("Bold"); bold.addActionListener(listener); italic = new JCheckBox("Italic"); italic.addActionListener(listener); sample = new JTextArea(TEXT_ROWS, TEXT_COLUMS); sample.setText("The quick brown fox jumps over the lazy dog"); sample.setEditable(false); sample.setLineWrap(true); sample.setBorder(BorderFactory.createEtchedBorder()); add(faceLabel, new GBC(0, 0).setAnchor(GBC.EAST)); add(face, new GBC(1, 0).setFill(GBC.HORIZONTAL).setWeight(100, 0).setInsets(1)); add(sizeLabel, new GBC(0, 1).setAnchor(GBC.EAST)); add(size, new GBC(1, 1).setFill(GBC.HORIZONTAL).setWeight(100, 0).setInsets(1)); add(bold, new GBC(0, 2, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100)); add(italic, new GBC(0, 3, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100)); add(sample, new GBC(2, 0, 1, 4).setFill(GBC.BOTH).setWeight(100, 100)); pack(); updateSample(); } public void updateSample() { String fontFace = (String) face.getSelectedItem(); int fontStyle = (bold.isSelected() ? Font.BOLD : 0) + (italic.isSelected() ? Font.ITALIC : 0); int fontSize = size.getItemAt(size.getSelectedIndex()); Font font = new Font(fontFace, fontStyle, fontSize); sample.setFont(font); sample.repaint(); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new FontFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } }
java.awt.GridBagConstraints:
可以设计你自己的LayoutManager类以一种特殊的方式管理组件。作为一个有趣的例子,可以将容器中的组件排列成一个圆形。
定制布局管理器必须实现LayoutManager接口,并且需要覆盖下面5个方法:
void addLayoutComponent(String s,Component c) void removeLayoutComponent(Component c) Dimension preferredLayoutSize(Container parent) Dimension minimumLayoutSize(Container parent) void layoutContainer(Container parent)
public class CircleLayout implements LayoutManager { private int minWidth = 0; private int minHeight = 0; private int preferredWidth = 0; private int preferredHeight = 0; private boolean sizesSet = false; private int maxComponentWidth = 0; private int maxComponentHeight = 0; @Override public void addLayoutComponent(String name, Component comp) { } @Override public void removeLayoutComponent(Component comp) { } @Override public Dimension preferredLayoutSize(Container parent) { setSizes(parent); Insets insets = parent.getInsets(); int width = preferredWidth + insets.left + insets.right; int height = preferredHeight + insets.top + insets.bottom; return new Dimension(width, height); } @Override public Dimension minimumLayoutSize(Container parent) { setSizes(parent); Insets insets = parent.getInsets(); int width = minWidth + insets.left + insets.right; int height = minHeight + insets.top + insets.bottom; return new Dimension(width, height); } @Override public void layoutContainer(Container parent) { setSizes(parent); Insets insets = parent.getInsets(); int containerWidth = parent.getSize().width - insets.left - insets.right; int containerHeight = parent.getSize().height - insets.top - insets.bottom; int xcenter = insets.left + containerWidth / 2; int ycenter = insets.top + containerHeight / 2; int xradius = (containerWidth - maxComponentWidth) / 2; int yradius = (containerHeight - maxComponentHeight) / 2; int radius = Math.min(xradius, yradius); int n = parent.getComponentCount(); for (int i = 0; i < n; i++) { Component c = parent.getComponent(i); if (c.isVisible()) { double angle = 2 * Math.PI * i / n; int x = xcenter + (int)(Math.cos(angle) * radius); int y = ycenter + (int)(Math.sin(angle) * radius); Dimension d = c.getPreferredSize(); c.setBounds(x - d.width / 2, y - d.height / 2, d.width, d.height); } } } public void setSizes(Container parent) { if (sizesSet) { return; } int n = parent.getComponentCount(); preferredWidth = 0; preferredHeight = 0; minWidth = 0; minHeight = 0; maxComponentWidth = 0; maxComponentHeight = 0; for (int i = 0; i < n; i++) { Component c = parent.getComponent(i); if (c.isVisible()) { Dimension d = c.getPreferredSize(); maxComponentWidth = Math.max(maxComponentWidth, d.width); maxComponentHeight = Math.max(maxComponentHeight, d.height); preferredWidth += d.width; preferredHeight += d.height; } } minWidth = preferredWidth / 2; minHeight = preferredHeight / 2; sizesSet = true; } }
public class CircleLayoutFrame extends JFrame { public CircleLayoutFrame() { setLayout(new CircleLayout()); add(new JButton("Yellow")); add(new JButton("Blue")); add(new JButton("Red")); add(new JButton("Green")); add(new JButton("Orange")); add(new JButton("Fuchsia")); add(new JButton("Indigo")); pack(); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new CircleLayoutFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } }
java.awt.LayoutManager:
到目前为止,所有的用户界面组件都显示在应用程序创建的窗体窗口中。如果编写运行在Web浏览器中的applet,这是最常见的情况。但是,如果你要编写应用程序,通常就需要有不同的对话框向用户显示信息或者获取用户提供的信息。
与大多数的窗口系统一样,AWT也分为模式对话框和无模式对话框。所谓模式对话框是指在结束对它的处理之前,不允许用户与应用程序的其余窗口进行交互。模式对话框主要用于在程序继续运行之前获取用户提供的信息。例如,当用户想要读取一个文件时,就会弹出一个模式文件对话框。用户必须给定一个文件名,然后程序才能够开始读操作。只有用户关闭这个模式对话框之后,应用才能够继续执行。
无模式对话框允许用户同时在对话框和应用程序的其他部分输入信息。使用无模式对话框的一个例子就是工具条。只要需要,工具条可以停靠在任何地方,而且用户可以根据需要同时与应用窗口和工具条进行交互。
Swing有一组现成的简单对话框,足以让用户提供一些信息。JOptionPane有4个用于显示这些对话框的静态方法:
对话框有以下组件:
输入对话框有一个用于接收用户输人的额外组件。这可能是一个文本域,用户可以输入任何的字符串,也可能是一个组合框,用户可以从中选择一项。
这些对话框的具体布局和为标准消息类型选择的图标都取决于可插接式观感。左侧的图标取决于下面5种消息类型:
PLAIN_MESSAGE类型没有图标。每个对话框类型都有一个方法,可以用来提供自己的图标,以替代原来的图标。可以为每个对话框类型指定一条消息。这里的消息既可以是字符串、图标、用户界面组件,也可以是其他类型的对象。可以如下显示消息对象:
当然,提供字符串消息是目前为止最常见的情况,而提供一个Component会带来更大的灵活性,这是因为可以让paintComponent方法绘制你想要的任何内容。位于底部的按钮取决于对话框类型和选项类型。当调用showMessageDialog和showInputDialog时,只能看到一组标准按钮(分别是OK和OK/Cancel)。当调用showConfirmDialog时,可以在下面四种选项类型中选择:
使用showOptionDialog时,可以指定一组任意的选项。你要提供一个对象数组作为选项。
每个数组元素会如下显示:
这些方法的返回值如下:
showConfirmDialog和showOptionDialog返回一个整数,表示用户选择了哪个按钮。对于选项对话框来说,这个值就是所选选项的索引值,或者是CLOSED0_PTION(此时用户没有选择选项,而是关闭了对话框)。对于确认对话框,返回值可以是以下值之一:
这些选项看上去让人有些困惑,实际上非常简单。步骤如下:
例如,对话框显示了一条消息,并请求用户确认或者取消。这是一个确认对话框。图标是一个问题图标,消息是字符串,选项类型是OK_CANCEL_OPTION。调用如下:
int selection = JOptionPane.showConfirmDialog(parent, "Message","Title", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); if (selection =JOptionPane.OK OPTION){ ... }
javax.swing.JoptionPane:
要想实现一个对话框,需要扩展JDialog类。这与应用程
序窗口扩展JFrame的过程完全一样。具体过程如下:
调用超类构造器时,需要提供所有者窗体(owner frame)
对话框标题及模式特征。
所有者窗体控制对话框的显示位置,如果提供null作为所有者,那么这个对话框将属于一个隐藏窗体。模式特征将指定显示这个对话框时阻塞应用程序的哪些其他窗口。无模式对话框不会阻塞其他窗口,而模式对话框将阻塞应用的所有其他窗口(除当前对话框的子窗口外)。用户经常使用的工具条要使用无模式对话框实现。另一方面,如果想强制用户在继续操作之前必须提供一些必要的信息,就应该使用模式对话框。
public class AboutDialog extends JDialog { public AboutDialog(JFrame owner) { super(owner, "About DialogTest", true); add(new JLabel("Core Java
By Cay Horstmann"), BorderLayout.CENTER); JButton ok = new JButton("OK"); ok.addActionListener(event -> setVisible(false)); JPanel panel = new JPanel(); panel.add(ok); add(panel, BorderLayout.SOUTH); pack(); } }
public class DialogFrame extends JFrame { private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 200; private AboutDialog dialog; public DialogFrame() { setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); JMenuBar menuBar = new JMenuBar(); setJMenuBar(menuBar); JMenu fileMenu = new JMenu("File"); menuBar.add(fileMenu); JMenuItem aboutItem = new JMenuItem("About"); aboutItem.addActionListener(event -> { if (dialog == null) { dialog = new AboutDialog(DialogFrame.this); } dialog.setVisible(true); }); fileMenu.add(aboutItem); JMenuItem exitItem = new JMenuItem("Exit"); exitItem.addActionListener(event -> System.exit(0)); fileMenu.add(exitItem); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new DialogFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } }
javax.swing.JDialog:
@Data @NoArgsConstructor @AllArgsConstructor public class User { private String username; private char[] password; }
public class PasswordChooser extends JPanel { private JTextField username; private JPasswordField password; private JButton okButton; private boolean ok; private JDialog dialog; public PasswordChooser() { setLayout(new BorderLayout()); JPanel panel = new JPanel(); panel.setLayout(new GridLayout(2, 2)); panel.add(new JLabel("User name:")); panel.add(username = new JTextField("")); panel.add(new JLabel("Password:")); panel.add(password = new JPasswordField("")); add(panel, BorderLayout.CENTER); okButton = new JButton("OK"); okButton.addActionListener(event -> { ok = true; dialog.setVisible(false); }); JButton cancelButton = new JButton("Cancel"); cancelButton.addActionListener(event -> dialog.setVisible(false)); JPanel buttonPanel = new JPanel(); buttonPanel.add(okButton); buttonPanel.add(cancelButton); add(buttonPanel, BorderLayout.SOUTH); } public void setUser(User u) { username.setText(u.getUsername()); } public User getUser() { return new User(username.getText(), password.getPassword()); } public boolean showDialog(Component parent, String title) { ok = false; Frame owner = null; if (parent instanceof Frame) { owner = (Frame)parent; } else { owner = (Frame)SwingUtilities.getAncestorOfClass(Frame.class, parent); } if (dialog == null || dialog.getOwner() != owner) { dialog = new JDialog(owner, true); dialog.add(this); dialog.getRootPane().setDefaultButton(okButton); dialog.pack(); } dialog.setTitle(title); dialog.setVisible(true); return ok; } }
public class DataExchangeFrame extends JFrame { public static final int TEXT_ROWS = 20; public static final int TEXT_COLUMNS = 40; private PasswordChooser dialog = null; private JTextArea textArea; public DataExchangeFrame() { JMenuBar mbar = new JMenuBar(); setJMenuBar(mbar); JMenu fileMenu = new JMenu("File"); mbar.add(fileMenu); JMenuItem connectItem = new JMenuItem("Connect"); connectItem.addActionListener(new ConnectAction()); fileMenu.add(connectItem); JMenuItem exitItem = new JMenuItem("Exit"); exitItem.addActionListener(event -> System.exit(0)); fileMenu.add(exitItem); textArea = new JTextArea(TEXT_ROWS, TEXT_COLUMNS); add(new JScrollPane(textArea), BorderLayout.CENTER); pack(); } private class ConnectAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { if (dialog == null) { dialog = new PasswordChooser(); } dialog.setUser(new User("yourname", null)); if (dialog.showDialog(DataExchangeFrame.this, "Connect")) { User u = dialog.getUser(); textArea.append("user name = " + u.getUsername() + ", password = " + (new String(u.getPassword())) + "\n"); } } } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame frame = new DataExchangeFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); }); } }
javax.swing.SwingUtilities:
javax.swing.JComponent:
JRootPane getRootPane():获得包含这个组件的根窗格,如果这个组件没有带根窗格的祖先,则返回null。
javax.swing.JRootPane:
void setDefaultButton(JButton button):设置根窗格的默认按钮。要想禁用默认按钮,可以提供null参数来调用这个方法。
javax.swing.JButton:
在一个应用中,通常希望可以打开和保存文件。一个好的文件对话框应该可以显示文件和目录,允许用户浏览文件系统,这样一个文件对话框很难编写,人们肯定不愿意从头做起。很幸运,Swing中提供了JFileChooser类,它显示的文件对话框类似于大多数原生应用所用的对话框。JFileChooser对话框总是模式对话框。注意,JFileChooser类并不是JDialog类的子类。需要调用show0 penDialog
显示打开文件的对话框,或者调用showSaveDialog显示保存文件的对话框,而不是调用setVisible(true)。接收文件的按钮会自动地使用标签Open或者Save。也可以调用showDialog方法为按钮提供你自己的标签。图11-33是文件选择器对话框的一个示例。
下面是建立文件对话框并获取用户选择信息的步骤:
var chooser = new JFileChooser();
chooser.setCurrentDirectory(new File("."));
需要提供一个File对象。File对象将在卷Ⅱ的第2章中详细介绍。这里只需要知道构造器File(String fileName)能够将一个文件或目录名转化为一个File对象。
chooser.setSelectedFile(new File(filename));
chooser.setMultiSelectionEnabled(true);
int result = chooser.showOpenDialog(parent);
或者
int result = chooser.showSaveDialog(parent);
这些调用的唯一区别是“确认按钮”的标签不同。“确认按钮”就是用户点击来完成文件选择的那个按钮。也可以调用showDialog方法,并将一个显式的文本传递给确认按钮:
int result chooser.showDialog(parent,"Select");
仅当用户确认、取消或者关闭对话框时这些调用才返回。返回值可以是JFileChooser.
APPROVE_OPTION、JFileChooser.CANCEL_OPTION或者JFileChooser.ERROR_OPTION。
8.调用getSelectedFile()或者getSelectedFiles()方法获取用户选择的一个或多个文件。这些方法将返回一个File对象或者一个File对象数组。如果需要知道文件对象名,可以调用
getPath方法。例如:
String filename chooser.getSelectedFile().getPath();
在大多数情况下,这些步骤都很简单。使用文件对话框的主要困难在于指定一个要让用户从中选择文件的文件子集。例如,假设用户应该选择GF图像文件。那么,文件选择器就应该只显示扩展名为,9f的文件。另外,还应该为用户提供反馈信息,来说明所显示的文件属于某个特定文件类别,如“GF图像”。不过,情况有可能会更加复杂。如果用户应该选择JPFG图像文件,扩展名就可以是.jpg或者.jpeg。文件选择器的设计者没有编写代码来实现这种复杂性,而是提供了一种更优雅的机制:要想限制所显示的文件,可以提供一个扩展了抽象类javax.swing…filechooser.FileFilter的对象。文件选择器将各个文件传递给文件过滤器,只有文件过滤器接受的文件才会显示。
有两个子类:一个是可以接受所有文件的默认过滤器,另一个过滤器可以接受给定扩展名的所有文件。不过,很容易编写专用文件过滤器,只需实现FileFilter超类中的两个抽象方法:
第一个方法检测是否应该接受一个文件,第二个方法返回可能在文件选择对话框中显示的文件类型的描述信息。
一旦有了文件过滤器对象,可以调用JFileChooser类的setFileFilter方法,将这个对象安装到文件选择器对象中:
chooser.setFileFilter(new FileNameExtensionFilter("Image files","gif","jpg"));
可以为一个文件选择器安装多个过滤器,如下:
chooser.addChoosableFileFilter(filterl); chooser.addChoosableFileFilter(filter2);
用户可以从文件对话框底部的组合框中选择过滤器。在默认情况下,组合框中总是显示“All files”过滤器。这是一个好主意,因为使用这个程序的用户有可能需要选择一个具有非标准扩展名的文件。不过,如果你想禁用All files过滤器,需要调用:
chooser.setAcceptAllFileFilterUsed(false)
最后,可以为文件选择器显示的每个文件提供特定的图标和文件描述来定制文件选择器。为此,需要提供-一个类对象,这个类要扩展javax.swing.filechooser包中的FileView类。
这显然是一种高级技术。通常情况下,并不需要你来提供文件视图一可插接式观感会为你提供一个视图。不过,如果想为特殊的文件类型显示不同的图标,也可以安装你自己的文件视图。需要扩展FileView类并实现下面5个方法:
Icon getIcon(File f) String getName(File f) String getDescription(File f) String getTypeDescription(File f) Boolean isTraversable(File f)
然后,使用setFileView方法将文件视图安装到文件选择器中。
文件选择器会为希望显示的每个文件或目录调用这些方法。如果方法返回的图标、名字或描述信息为ul1,那么文件选择器会使用当前观感的默认文件视图。这样处理很好,因为这意味着只需要处理那些希望有不同显示的文件类型。
文件选择器调用isTraversable方法来决定用户点击一个目录时是否打开这个目录。请注意,这个方法返回一个Boolean对象,而不是boolean值。看起来似乎有点怪,但实际上很方便一如果只需要使用默认文件视图,则返回ull。文件选择器就会使用默认的文件视图。换句话说,这个方法返回的Boolean.对象能给出3种选择:真(Boolean.TRUE)、假(Boolean.FALSE)和不关心(nuLL)。
示例程序中包含了一个简单的文件视图类。只要一个文件匹配文件过滤器,这个类将会显示一个特定的图标。可以利用这个类为所有图像文件显示一个调色板图标。
class FileIconView extends FileView { private FileFilter filter; private Icon icon; public FileIconView(FileFilter aFilter,Icon anIcon) { filter = aFilter; icon = anIcon; } public Icon getIcon(File f){ if (!f.isDirectory()&&filter.accept(f)) return icon; } else return null; } }
可以调用setFileView方法将这个文件视图安装到文件选择器:
chooser.setFileView(new FileIconView(filter, new ImageIcon("palette.gif")));
文件选择器会在通过filter过滤的所有文件旁边显示调色板图标,而使用默认的文件视图显示所有其他的文件。很自然地,我们使用了文件选择器中设置的过滤器。
javax.swing.JFileChooser:
javax.swing.filechooser.FileFilter:
javax.swing.filechooser.FileNameExtensionFilter:
javax.swing.filechooser.FileView: