Java图形界面开发:高级Swing容器(一)
第10章探讨了AWT与Swing中的布局管理器。在本章中,我们将会了解一些构建在这些布局管理器之上的容器以及其他的一些无需布局管理器的容器。
我们的探讨由Box类开始,我们将会发现使用BoxLayout管理器来创建一个单行或单列组件的最好方法。接下来我们会了解JSplitPane容器,他类似于其中只有两个组件的特殊的Box。JSplitPane提供了一个分隔栏,用户可以拖动这个分隔栏来调整组件的大小以满足各自的需求。
然后我们会探讨JTabbedPane容器,其工作方工式类似于一个由CardLayout布局管理器管理的容器,所不同的是容器内建的标签可以使得我们由一个卡片移动到一个卡片。我们也可以使用JTabbedPane创建多个屏幕,属性页对话框用于用户输入。
最后讨论的两个高级Swing容器是JScrollPane与JViewport。这两个组件都提供了在有限的屏幕真实状态之内显示大组件集合的能力。JScrollPane为显示区域添加滚动条,从而我们可以在一个小区域内在大组件周围移动。事实上,JScrollPane使用JViewport来分割本看不见的大组件部分。
下面我们就开始了解第一个容器,Box类。
11.1 Box类
作为JComponent类的子类,Box类是借助于BoxLayout管理器创建单行或单列组件的一个特殊Java Container。Box容器的作用类似于JPanel(或Panel),但是具有一个不同的默认布局管理器,BoxLayout。在Box之外使用BoxLayout有一些麻烦,而Box简化了BoxLayout的使用。我们只需三步就可以将BoxLayout管理器与容器相关联:手动创建容器,创建布局管理器,然后将管理器与容器相关联。当我们创建一个Box的实例时,我们一次就执行了这三个步骤。另外,我们可以使用Box的名为Box.Filler的内联类来更好的放置容器内的组件。
11.1.1 创建Box
我们有三种方法来创建Box,一个构造函数以及两个静态工厂方法:
public Box(int direction) Box horizontalBox = new Box(BoxLayout.X_AXIS); Box verticalBox = new Box(BoxLayout.Y_AXIS); public static Box createHorizontalBox() Box horizontalBox = Box.createHorizontalBox(); public static Box createVerticalBox() Box verticalBox = Box.createVerticalBox();
注意 ,Box类并没有被设计用来作为JavaBean组件使用。在IDE中这个容器的使用十分笨拙。
不经常使用的构造函数需要布局管理器主坐标的方向。这个方向是通过BoxLayout的两个常量来指定的:X_AXIS或Y_AXIS,分别用来创建水平或垂直盒子。我们无需手动指定方向,我们可以简单的通过所提供的工厂方法来创建所需方向的盒子:createHorizontalBox()或createVerticalBox()。
使用JLabel,JTextField与JButton填充一个水平与垂直Box演示了BoxLayout的灵活性,如图11-1所示。
对于水平容器,标签与按钮以其最优的宽度显示,因为他们的最大尺寸与最优尺寸相同。文本域使用余下的空间。
在垂直容器中,标签与按钮的尺寸也是他们的最优尺寸,因为他们的最大尺寸依然与他们的最优尺寸相同。文本的高度填充了标签与按钮没有使用的高度,而其宽度与容器的宽度相同。
用于创建图11-1所示屏幕的源码显示在列表11-1中。
package swingstudy.ch11; import java.awt.BorderLayout; import java.awt.EventQueue; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JTextField; public class BoxSample { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Runnable runner = new Runnable() { public void run() { JFrame verticalFrame = new JFrame("Vertical"); verticalFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Box verticalBox = Box.createVerticalBox(); verticalBox.add(new JLabel("Top")); verticalBox.add(new JTextField("Middle")); verticalBox.add(new JButton("Bottom")); verticalFrame.add(verticalBox, BorderLayout.CENTER); verticalFrame.setSize(150, 150); verticalFrame.setVisible(true); JFrame horizontalFrame = new JFrame("Horizontal"); horizontalFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Box horizontalBox = Box.createHorizontalBox(); horizontalBox.add(new JLabel("Left")); horizontalBox.add(new JTextField("Middle")); horizontalBox.add(new JButton("Right")); horizontalFrame.add(horizontalBox, BorderLayout.CENTER); horizontalFrame.setSize(150, 150); horizontalFrame.setVisible(true); } }; EventQueue.invokeLater(runner); } }
11.1.2 Box属性
如表11-1所示,Box只有两个属性。尽管布局属性由其父类Container继承了setLayout(LayoutManager)方法,但是如果在Box对象上调用,这个类会抛出一个AWTError。一旦BoxLayout管理器在其构造函数中被设置,那么就能再改变,其方向也不能改变。
11.1.3 使用Box.Filer
Box类具有一个内联类Box.Filler,可以帮助我们创建不可见的组件从而更好的为采用BoxLayout布局管理器的容器内的组件进行位置布局。通过直接操作所创建组件的最小,最大与最优尺寸,我们可以创建可以增长来填充未用的空间或是保持固定尺寸的组件,从而屏幕更为用户所接受。
注意,由技术上来说,Box.Filler的使用并没有局限于使用BoxLayout布局管理器的容器。我们可以将其用在其他任何使用Component的地方。只是组件是不可见的。
我们无需直接使用Box.Filler类,Box类的一些静态方法可以帮助我们创建合适的填充器组件。工厂方法可以使得我们通过类型对组件进行分类,而不是通过最小值,最大值或是最优尺寸进行分类。我们将会在接下来的两节中了解这些方法。
如果我们对类定义感兴趣,Box.Filler的类定义显示如下。类似于Box类,Box.Filler本来也不是作为JavaBean组件来使用的。
public class Box.Filler extends Component implements Accessible { // Constructors public Filler(Dimension minSize, Dimension prefSize, Dimension maxSize); // Properties public AccessibleContext getAccessibleContext(); public Dimension getMaximumSize(); public Dimension getMinimumSize(); public Dimension getPreferredSize(); // Others protected AccessibleContext accessibleContext; public void changeShape(Dimension minSize, Dimension prefSize, Dimension maxSize); }
11.1.4 创建扩展区域
如果一个组件具有较小的最小尺寸与最优尺寸,而最大尺寸要大于屏幕尺寸,组件可以在一个或是两个方向上进行扩展以占用容器中组件之间的未用空间。在Box的情况下,或者更确切的说,布局管理器为BoxLayout的容器,扩展出现在布局管理器初始选择的方向上(BoxLayout.X_AXIS或BoxLayout.Y_AXIS)。对于水平的盒子,扩展影响了组件的宽度。对于垂直的盒子,扩展反映在组件的高度上。
通常为这种扩展组件类型指定的名字为胶水(glue)。glue的两种类型为独立于方向的glue与方向相关的glue。下面的Box工厂方法用于创建胶合组件:
public static Component createGlue() // Direction independent Component glue = Box.createGlue(); aBox.add(glue); public static Component createHorizontalGlue(); // Direction dependent: horizontal Component horizontalGlue = Box.createHorizontalGlue(); aBox.add(horizontalGlue); public static Component createVerticalGlue() // Direction dependent: vertical Component verticalGlue = Box.createVerticalGlue(); aBox.add(verticalGlue);
一旦我们创建了glue,我们就可以像添加其他的组件一样将其添加到容器中,通过Container.add(Component)或是其他的add()方法。glue可以使得我们在容器内对齐组件,如图11-2所示。
我们可以将胶合组件添加到任何其布局管理器考虑到组件的最小尺寸,最大尺寸与最优尺寸的容器中,例如BoxLayout。例如,图11-3演示了当我们将一个胶合组件添加到JMenuBar而在最后一个JMenu之前的样子。因为JMenuBar的布局管理器为BoxLayout(实际上是子类javax.swing.plaf.basic.DefaultMenuLayout),这一操作可以将最后一个菜单推到工具栏的右边,类似于Motif/CDE风格的帮助菜单。
注意,我们推荐避免使用胶合组件的这种功能来设置菜单栏上的菜单。事实上JMenuBar的public void sethelpMenu(JMenu menu)将会实现这种行为而且不会抛出Error。当然,我们中的许多人仍在等待这种操作。
11.1.5 创建固定区域
因为胶合组件会扩展来填充可用的空间,如果我们希望组件之间有一段固定的距离,我们需要创建一个固定组件,或strut。当我们这样做时,我们需要指定strut的尺寸。strut可以是二维的,需要我们指定组件的宽度或调试;或者也可以是一维的,需要我们指定宽度或高度。
public static Component createRigidArea(Dimension dimension) // Two-dimensional Component rigidArea = Box. createRigidArea(new Dimension(10, 10)); aBox.add(rigidArea); public static Component createHorizontalStrut(int width) // One-dimensional: horizontal Component horizontalStrut = Box. createHorizontalStrut(10); aBox.add(horizontalStrut); public static Component createVerticalStrut(int height) // One-dimensional: vertical Component verticalStrut = Box. createVerticalStrut(10); aBox.add(verticalStrut);
注意,尽管使用createGule()方法创建的方向无关的胶合组件在我们修改容器方向时并没有副作用,然而创建固定区域会在修改坐标时引起布局问题。(想像一下拖动菜单栏)这是因为组件具有一个最小尺寸。使用createRigidArea()方法并不推荐,除非我们确实需要一个二维的空组件。
图11-4显示了一些固定组件。注意,我们可以变化不同的组件之间的固定距离,而且容器最末的固定组件并没有效果。在用户调整屏幕之后,组件之间的固定距离会保持不变,如图11-4所示。
11.2 JSplitPane类
类似于Box容器,JSplitPane容器允许我们在单行或单列中显示组件。然而Box可以包含任意数量的组件,JSplitPane只可以用来显示两个组件。组件可以变化尺寸并通过一个可移动的分隔栏进行分隔。分隔栏可以使得用户可以通过拖拽分隔栏来调整所包含组件的尺寸。图11-5显示了垂直与水平分割面板,同时显示在移动分隔栏之前与之后的样子。
11.2.1 创建JSplitPane
JSplitPane有五个构造函数。通过这些构造函数,我们可以初始化所包含组件对的方向,设置continuousLayout属性或是为容器初始化组件对。
public JSplitPane() JSplitPane splitPane = new JSplitPane(); public JSplitPane(int newOrientation) JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); public JSplitPane(int newOrientation, boolean newContinuousLayout) JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true); public JSplitPane(int newOrientation, Component newLeftComponent, Component newRightComponent) JComponent topComponent = new JButton("Top Button"); JComponent bottomComponent = new JButton("Bottom Button"); JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, topComponent, bottomComponent); public JSplitPane(int newOrientation, boolean newContinuousLayout, Component newLeftComponent, Component newRightComponent) JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, topComponent, bottomComponent);
除非特别指定,默认方向为水平方向。方向可以通过JSplitPane的常量VERTICAL_SPLIT或HORIZONTAL_SPLIT来指定。continuousLayout属性设置瘊定了当用户拖动分隔栏时分隔面板如何响应。当设置为false(默认)时,在拖动时只有分隔符被重绘。当设置为true时,在用户拖拽分隔栏时,JSplitPane会调整尺寸并重绘分隔栏每一边的组件。
注意,如果方向为JSplitPane.VERTICAL_SPLIT,我们可以将上部的组件看作左侧组件,而将下部组件看作右侧组件。
如果我们使用无参数的构造函数,分隔面板内的初始组件集合由按钮组成(两个JButton组件)。其他的两个构造函数显示的设置了初始的两个组件。奇怪的是,其余的两个构造函数默认情况下并没有提供容器内的组件。要添加或修改JSplitPane内的组件,请参看稍后的“修改JSplitPane组件”一节。
11.2.2 JSplitPane属性
表11-2显示了JSplitPane的17个属性。
设置方向
除了在构造函数中初始化方向以外,我们可以通过将方向属性修改为JSplitPane.VERTICAL_SPLIT或是JSplitPane.HORIZONTAL_SPLIT来修改JSplitPane方向。如果我们试着将属性修改为非等同的设置,则会抛出IllegalArgumentException。
不推荐在运行时动态修改方向,因为这会使用户感到迷惑。然而,如果我们正在使用可视化开发工具,我们可以在创建JSplitPane之后显示设置方向属性。当没有进行可视化编程时,我们通常在创建JSplitPane时初始化方向。
修改JSplitPane组件
有四个读写属性可以用来处理JSplitPane内组件的不同位置:bottomComponent, leftComponent, rightComponent与topComponent。事实上,这四个属性表示两种内部组件:左边与上部组件是一种;右边与下部组件表示另一种。
我们应该使用与我们的JSplitPane的方向相适应的属性。使用不合适的属性方法会使得程序员的维护生命十分困难。想像一下,在创建用户界面之后,在六个月之后看到如下的代码:
JComponent leftButton = new JButton("Left"); JComponent rightButton = new JButton("Right"); JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); splitPane.setLeftComponent(leftButton); splitPane.setRightComponent(rightButton);
如果我们看一下代码,基于变量名以及setXXXComponent()方法的使用,我们也许会认为屏幕在左边包含一个按钮,而右边也是一个按钮。但是实例化的JSplitPane具有一个垂直方向,所创建的界面如图11-6所示。所用的变量是按钮的标签,而不是他们的位置。
如果setTopComponent()与setBottomComponent()方法使用更好的变量名,代码会更容易理解:
JComponent topButton = new JButton("Left"); JComponent bottomButton = new JButton("Right"); JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); splitPane.setTopComponent(topButton); splitPane.setBottomComponent(bottomButton);
移动JSplitPane分隔符
初始时,分隔符显示在上部组件的下面或是左边组件的右边合适尺寸处。任何时候,我们可以通过调用JSplitPane的restToPreferredSizes()方法来重新设置分隔位置。如果我们要编程来定位分隔符,我们可以通过setDividerLocation(newLocation)来修改dividerLocation属性。这个属性可以修改一个int位置,表示距离上部或左边的绝对距离,或者是设置为一个0.0与1.0之间的double值,表示JSplitPane容器宽度的百分比。
注意,如果将属性设置为0.0与1.0范围之外的double值则会抛出IllegalArgumentException。
如果我们要设置分隔符的位置,我们必须等到组件已经被实现。本质上,这就意味着组件必须可见。有多种方法可以实现这一操作,最直接的方法就是向JSplitPane关联一个HierarchyListener,并且监听HierarchyEvent何时变为SHOWING_CHANGED类型。下面的代码片段演示了这一操作,将分隔符位置修改为75%。
HierarchyListener hierarchyListener = new HierarchyListener() { public void hierarchyChanged(HierarchyEvent e) { long flags = e.getChangeFlags(); if ((flags & HierarchyEvent.SHOWING_CHANGED) == HierarchyEvent.SHOWING_CHANGED) { splitPane.setDividerLocation(.75); } } }; splitPane.addHierarchyListener(hierarchyListener);
尽管我们可以使用double值设置dividerLocation属性,我们只会获得了一个标识绝对位置的int值。
调整组件尺寸与使用可扩展的分隔符
对于JSplitPane内的组件调整尺寸存在限制。JSplitPane会考虑到每一个所包含组件的最小尺寸。如果拖动分隔符使得一个组件缩小到小于其最小尺寸,则滚动面板不会允许用户拖动分隔符超过这个最小尺寸。
注意,我们可以编程实现将分隔符放在任意位置,甚至是使得组件小于其最小尺寸。然而这并不是一个好主意,因为组件最小尺寸的存在是有原因的。
如果组件的最小维度对于JSplitPane来说过大,我们需要修改组件的最小尺寸,从而分隔符可以使用组件的空间。对于AWT组件,修改一个标准组件的最小尺寸需要子类派生。对于Swing组件,我们可以简单的通过一个新的Dimension来调用JComponent的setMinimumSize()方法。然而,最小尺寸的设置要合理。如果我们显式的缩小其最小尺寸,组件就不会正常的工作。
有一个更好的方法可以使得一个组件比其他组件占用更多的空间:将JSplitPane的onTouchExpandable属性设置为true。当这个属性为真时,就会为分隔符添加一个图标,从而使得用户可以完全折叠起两个组件中的一个来为另一个组件指定全部的空间。在图11-7的盒子中,图标是一个上下箭头的组合。
图11-7显示了这个图标显示的样子(通过Ocean观感渲染)并且演示了在选择分隔符上的向上箭头来将下部的组件扩展为其全部尺寸时的样子。再一次点击分隔符上的图标会使得组件又回到其先前的位置。点击分隔符上图标以外的位置会将分隔符定位到使得折叠的组件位于其最优尺寸处。
注意,并没有较容易的方法来修改扩展分隔符的图标或是修改分隔符如何渲染。这两方面都是通过BasicSplitPaneDivider子类来定义并且在用于特定观感类型的BasicSplitPaneUI子类的createDefaultDivider()方法中创建的。我们可以简单修改分隔符周围的边框,这是一个自定义边框。
lastDividerLocation属性可以使得我们或是系统查询前一个分隔符位置。当用户选择maximizer图标来取消JSplitPane中的一个组件的最小化时,JSplitPane会使用这个属性。
小心,要小心其最小尺寸是基于容器尺寸或是其初始尺寸的组件。将这些属性放置在JSplitPane中也许会要求我们手动设置组件的minimum或是最优尺寸。当用在JSplitPane中时最常引起问题的组件就是JTextArea与JScrollPane。
调整JSplitPane尺寸
如果在JSplitPane中存在其所包含的组件的最优尺寸所不需要的额外空间时,这个空间会依据resizeWeight属性设置进行分配。这个属性的初始设置为0.0,意味着右边或是下边的组件会获得额外的空间。将这个设置修改为1.0会将所有的空间指定给左边或上部的组件。0.5则会在两个组件之间分隔面板。图11-8显示了这些变化的效果。
11.2.3 监听JSplitPane属性变化
JSplitPane类定义了下列的常量来帮助监听边界属性的变化:
- CONTINUOUS_LAYOUT_PROPERTY
- DIVIDER_LOCATION_PROPERTY
- DIVIDER_SIZE_PROPERTY
- LAST_DIVIDER_LOCATION_PROPERTY
- ONE_TOUCH_EXPANDABLE_PROPERTY
- ORIENTATION_PROPERTY
- RESIZE_WEIGHT_PROPERTY
监听用户何时移动分隔符的一个方法就是监听lastDividerLocation属性的变化。列表11-2中的示例将一个PropertyChangeListener关联到JSplitPane,从而显示当前的分隔符位置,当前的最后位置以及前一个最后位置。分隔符上面与下面的组件是OvalPanel类(在第四章中讨论),绘制来填充组件的维度。这个组件有助于演示将continuousLayout属性设置true的效果状态。
package swingstudy.ch11; import java.awt.BorderLayout; import java.awt.EventQueue; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JSplitPane; import swingstudy.ch04.OvalPanel; public class PropertySplit { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Runnable runner = new Runnable() { public void run() { JFrame frame = new JFrame("Property Split"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // create/configure split pane JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); splitPane.setContinuousLayout(true); splitPane.setOneTouchExpandable(true); // create top component JComponent topComponent = new OvalPanel(); splitPane.setTopComponent(topComponent); // create bottom component JComponent bottomComponent = new OvalPanel(); splitPane.setBottomComponent(bottomComponent); // create PropertyChangeListener PropertyChangeListener propertyChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent event) { JSplitPane sourceSplitPane = (JSplitPane)event.getSource(); String propertyName = event.getPropertyName(); if(propertyName.equals(JSplitPane.LAST_DIVIDER_LOCATION_PROPERTY)){ int current = sourceSplitPane.getDividerLocation(); System.out.println("Current: "+current); Integer last = (Integer)event.getNewValue(); System.out.println("Last: "+last); Integer priorLast = (Integer)event.getOldValue(); System.out.println("Prior last: "+priorLast); } } }; // attach listener splitPane.addPropertyChangeListener(propertyChangeListener); frame.add(splitPane, BorderLayout.CENTER); frame.setSize(300, 150); frame.setVisible(true); } }; EventQueue.invokeLater(runner); } }
如下面的示例输出所示,当我们运行前面的程序时,我们会注意到lastDividerLocation属性的变化来反映分隔符的拖动。当用户停止拖动分隔符时,最后设置被设置为dividerLocation属性的前一个设置,而不是用户开始拖动时的初始设置值。当用户拖动分隔符时,当前值变为最后一个值然后变为前一个最后值。
Current: 11 Last: -1 Prior last: 0 Current: 12 Last: 11 Prior last: -1 Current: 12 Last: 12 Prior last: 11 Current: 12 Last: 11 Prior last: 12 Current: 15 Last: 12 Prior last: 11 Current: 15 Last: 15 Prior last: 12 Current: 15 Last: 12 Prior last: 15 Current: 112 Last: 15 Prior last: 12 Current: 112 Last: 112 Prior last: 15 Current: 112 Last: 15 Prior last: 112
注意,PropertyChangeListener并不支持JSplitPane类的BOTTOM, DIVIDER, LEFT, RIGHT与TOP常量。相反,他们是为add(Component component, Object constraints)方法所用的内部约束。
11.2.4 自定义JSplitPane类型
每一个可安装的Swing观感提供了不同的JSplitPane外观以及组件的默认UIResource值集合。图11-9显示了预安装的观感类型集合的JSplitPane容器外观:Motif,Windows以及Ocean。
表11-3显示了JSplitPane可用的UIResource相关的属性集合。对于JSplitPane组件,有25个不同的属性,包括3个分隔符特定的属性。