12.3.3 使用JLayeredPane、 JDesktopPane和JInternalFrame

12.3.3 使用JLayeredPane、 JDesktopPane和JInternalFrame

JLayeredPane 分层容器

JLayeredPane是一个代表有层次深度的容器,它允许组件在需要时互相重叠。当向JLayeredPane容器中添加组件时,需要为该组件指定一个深度索引,其中层次索引较高的层里的组件位于其他层的组件之上

JLayeredPane默认层

JLayeredPane还将容器的层次深度分成几个默认层,程序只是将组件放入相应的层,从而可以更容易地确保组件的正确重叠,无须为组件指定具体的深度索引。JLayeredPane提供了如下几个默认层。

默认层 描述
static Integer DEFAULT_LAYER 大多数组件位于的标准层。这是最底层
static Integer PALETTE_LAYER 调色板层位于默认层之上。该层对于浮动工具栏调色板很有用,因此可以位于其他组件之上。
static Integer MODAL_LAYER 该层用于显示模式对话框。它们将出现在容器中所有工具栏、调色板或标准组件的上面。
static Integer POPUP_LAYER 该层用于显示右键菜单,与对话框、工具提示和普通组件关联的弹出式窗口将出现在对应的对话框、工具提示和普通组件之上。
static Integer DRAG_LAYER 该层用于放置拖放过程中的组件(关于拖放操作请看下一节内容),拖放操作中的组件位于所有组件之上。一旦拖放操作结束后,该组件将重新分配到其所属的正常层
static Integer FRAME_CONTENT_LAYER Convenience object defining the Frame Content layer.
static String LAYER_PROPERTY Bound property

每一层都是一个不同的整数。可以在调用add()方法的过程中通过Integer参数指定该组件所在的层。也可以传入上面几个静态常量,它们分别等于0,100,200,300,400等值。

除此之外,也可以使用JLayeredPanemoveToFront()moveToBack()setPosition()方法在组件所在层中对其进行重定位,还可以使用setLayer()方法更改该组件所属的层。

添加到JLayeredPane中的组件大小和位置必须确定

JLayeredPane中添加组件时,必须显式设置该组件的大小和位置,否则该组件不能显示出来

程序 JLayeredPane分层容器

下面程序简单示范了JLayeredPane容器的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.awt.*;
import javax.swing.*;

public class JLayeredPaneTest {
JFrame jf = new JFrame("测试JLayeredPane");
JLayeredPane layeredPane = new JLayeredPane();

public void init() {
// 向layeredPane中添加3个组件
layeredPane.add(new ContentPanel(10, 20, "疯狂Java讲义", "ico/java.png"), JLayeredPane.MODAL_LAYER);
layeredPane.add(new ContentPanel(100, 60, "疯狂Android讲义", "ico/android.png"), JLayeredPane.DEFAULT_LAYER);
layeredPane.add(new ContentPanel(190, 100, "轻量级Java EE企业应用实战", "ico/ee.png"), 4);
layeredPane.setPreferredSize(new Dimension(400, 300));
layeredPane.setVisible(true);
jf.add(layeredPane);
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}

public static void main(String[] args) {
new JLayeredPaneTest().init();
}
}

// 扩展了JPanel类,可以直接创建一个放在指定位置,
// 且有指定标题、放置指定图标的JPanel对象
class ContentPanel extends JPanel {
private static final long serialVersionUID = 4535201982849108665L;

public ContentPanel(int xPos, int yPos, String title, String ico) {
setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), title));
JLabel label = new JLabel(new ImageIcon(ico));
add(label);
setBounds(xPos, yPos, 160, 220); // ①
}
}

上面程序中粗体字代码向JLayeredPane中添加了三个Panel组件,每个Panel组件都必须显式设置大小和位置(程序中①处代码设置了Panel组件的大小和位置),否则该组件不能被显示出来。
运行上面程序,会看到如图12.17所示的运行效果。
这里有一张图片

JDesktopPane 内部窗口

JLayeredPane的子类JDesktopPane容器更加常用,很多应用程序都需要启动多个内部窗口来显示信息(典型的如EclipseEditPlus都使用了这种内部窗口来分别显示每个Java源文件),这些内部窗口都属于同一个外部窗口,当外部窗口最小化时,这些内部窗口都被隐藏起来。在Windows环境中,这种用户界面被称为多文档界面(Multiple Document Interface,MDI)。
使用Swing可以非常简单地创建岀这种MDI界面,通常,内部窗口有自己的标题栏、标题、图标、三个窗口按钮,并允许拖动改变内部窗口的大小和位置,但内部窗口不能拖出外部窗口。

内部窗口与外部窗口表现方式上的唯一区别在于:

  • 外部窗口的桌面是实际运行平台的桌面,
  • 而内部窗口以外部窗口的指定容器作为桌面。

就其实现机制来看,外部窗口和内部窗口则完全不同,

  • 外部窗口需要部分依赖于本地平台的GUI组件,属于重量级组件;
  • 而内部窗口则采用100%的Java实现,属于轻量级组件。

JDesktopPane需要和JInternalFrame结合使用,其中JDesktopPane代表一个虚拟桌面,而JInternalFrame则用于创建内部窗口。

创建内部窗口的步骤

使用JDesktopPaneJInternalFrame创建内部窗口按如下步骤进行即可。

  1. 创建一个JDesktopPane对象。JDesktopPane类仅提供了一个无参数的构造器,通过该构造器创建JDesktopPane对象,该对象代表一个虚拟桌面。
  2. 使用JInternalFrame创建一个内部窗口。创建内部窗口与创建JFrame窗口有一些区别,创建JInternalFrame对象时除可以传入一个字符串作为该内部窗口的标题之外,还可以传入4个boolean值,用于指定该内部窗口是否允许改变窗口大小、是否允许关闭窗口、是否允许最大化窗口、是否允许最小化窗口。
    例如,下面代码可以创建一个内部窗口:
    1
    2
    3
    4
    5
    6
    //创建内部窗口
    final JInternalFrame iframe = new JInternalFrame("新文档",
    true,//可改变大小
    true,//可关闭
    true,//可最大化
    true);//可最小化
  3. 一旦获得了内部窗口之后,该窗口的用法和普通窗口的用法基本相似,一样可以指定该窗口的布局管理器,一样可以向窗口内添加组件、改变窗口图标等。关于操作内部窗口具体存在哪些方法,请参阅JInternalFrame类的API文档。
  4. 将该内部窗口以合适大小、在合适位置显示出来。与普通窗口类似的是,该窗口默认大小是0×0像素,位于0.0位置(虚拟桌面的左上角处),并且默认处于隐藏状态,程序可以通过如下代码将内部窗口显示出来。
    1
    2
    3
    4
    //同时设置窗口的大小和位置
    iframe.reshape(20, 20, 300, 400);
    //使该窗口可见,并尝试选中它
    iframe.show();
  5. 将内部窗口添加到JDesktopPane容器中,再将JDesktopPane容器添加到其他容器中

外部窗口的show方法过时了

外部窗口的show()方法已经过时了,不再推荐使用。但内部窗口的show()方法没有过时,该方法不仅可以让内部窗口显示出来,而且可以让该窗口处于选中状态。

JDesktopPane不能独立存在

JDesktopPane不能独立存在,必须将JDesktopPane添加到其他顶级容器中才可以正常使用。

程序 使用JDesktopPaneJInternalFrame创建多文档页面

下面程序示范了如何使用JDesktopPaneJInternalFrame来创建MDI界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import java.beans.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class JInternalFrameTest {
final int DESKTOP_WIDTH = 480;
final int DESKTOP_HEIGHT = 360;
final int FRAME_DISTANCE = 30;
JFrame jf = new JFrame("MDI界面");
// 定义一个虚拟桌面
private MyJDesktopPane desktop = new MyJDesktopPane();
// 保存下一个内部窗口的坐标点
private int nextFrameX;
private int nextFrameY;
// 定义内部窗口为虚拟桌面的1/2大小
private int width = DESKTOP_WIDTH / 2;
private int height = DESKTOP_HEIGHT / 2;
// 为主窗口定义两个菜单
JMenu fileMenu = new JMenu("文件");
JMenu windowMenu = new JMenu("窗口");
// 定义newAction用于创建菜单和工具按钮
Action newAction = new AbstractAction("新建", new ImageIcon("ico/new.png")) {
private static final long serialVersionUID = 7504047155791213931L;

public void actionPerformed(ActionEvent event) {
// 创建内部窗口
final JInternalFrame iframe = new JInternalFrame("新文档", true, // 可改变大小
true, // 可关闭
true, // 可最大化
true); // 可最小化
iframe.add(new JScrollPane(new JTextArea(8, 40)));
// 将内部窗口添加到虚拟桌面中
desktop.add(iframe);
// 设置内部窗口的原始位置(内部窗口默认大小是0X0,放在0,0位置)
iframe.reshape(nextFrameX, nextFrameY, width, height);
// 使该窗口可见,并尝试选中它
iframe.show();
// 计算下一个内部窗口的位置
nextFrameX += FRAME_DISTANCE;
nextFrameY += FRAME_DISTANCE;
if (nextFrameX + width > desktop.getWidth())
nextFrameX = 0;
if (nextFrameY + height > desktop.getHeight())
nextFrameY = 0;
}
};
// 定义exitAction用于创建菜单和工具按钮
Action exitAction = new AbstractAction("退出", new ImageIcon("ico/exit.png")) {
private static final long serialVersionUID = -254393382364931601L;

public void actionPerformed(ActionEvent event) {
System.exit(0);
}
};

public void init() {
// 为窗口安装菜单条和工具条
JMenuBar menuBar = new JMenuBar();
JToolBar toolBar = new JToolBar();
jf.setJMenuBar(menuBar);
menuBar.add(fileMenu);
fileMenu.add(newAction);
fileMenu.add(exitAction);
toolBar.add(newAction);
toolBar.add(exitAction);
menuBar.add(windowMenu);
JMenuItem nextItem = new JMenuItem("下一个");
nextItem.addActionListener(event -> desktop.selectNextWindow());
windowMenu.add(nextItem);
JMenuItem cascadeItem = new JMenuItem("级联");
cascadeItem.addActionListener(event ->
// 级联显示窗口,内部窗口的大小是外部窗口的0.75
desktop.cascadeWindows(FRAME_DISTANCE, 0.75));
windowMenu.add(cascadeItem);
JMenuItem tileItem = new JMenuItem("平铺");
// 平铺显示所有内部窗口
tileItem.addActionListener(event -> desktop.tileWindows());
windowMenu.add(tileItem);
final JCheckBoxMenuItem dragOutlineItem = new JCheckBoxMenuItem("仅显示拖动窗口的轮廓");
dragOutlineItem.addActionListener(event ->
// 根据该菜单项是否选择来决定采用哪种拖动模式
desktop.setDragMode(
dragOutlineItem.isSelected() ? JDesktopPane.OUTLINE_DRAG_MODE : JDesktopPane.LIVE_DRAG_MODE)); // ①
windowMenu.add(dragOutlineItem);
desktop.setPreferredSize(new Dimension(480, 360));
// 将虚拟桌面添加到顶级JFrame容器中
jf.add(desktop);
jf.add(toolBar, BorderLayout.NORTH);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.pack();
jf.setVisible(true);
}

public static void main(String[] args) {
new JInternalFrameTest().init();
}
}

class MyJDesktopPane extends JDesktopPane {
// 将所有窗口以级联方式显示,
// 其中offset是两个窗口的位移距离, s
// cale是内部窗口与JDesktopPane的大小比例
public void cascadeWindows(int offset, double scale) {
// 定义级联显示窗口时内部窗口的大小
int width = (int) (getWidth() * scale);
int height = (int) (getHeight() * scale);
// 用于保存级联窗口时每个窗口的位置
int x = 0;
int y = 0;
for (JInternalFrame frame : getAllFrames()) {
try {
// 取消内部窗口的最大化,最小化
frame.setMaximum(false);
frame.setIcon(false);
// 把窗口重新放置在指定位置
frame.reshape(x, y, width, height);
x += offset;
y += offset;
// 如果到了虚拟桌面边界
if (x + width > getWidth())
x = 0;
if (y + height > getHeight())
y = 0;
} catch (PropertyVetoException e) {
}
}
}

// 将所有窗口以平铺方式显示
public void tileWindows() {
// 统计所有窗口
int frameCount = 0;
for (JInternalFrame frame : getAllFrames()) {
frameCount++;
}
// 计算需要多少行、多少列才可以平铺所有窗口
int rows = (int) Math.sqrt(frameCount);
int cols = frameCount / rows;
// 需要额外增加到其他列中的窗口
int extra = frameCount % rows;
// 计算平铺时内部窗口的大小
int width = getWidth() / cols;
int height = getHeight() / rows;
// 用于保存平铺窗口时每个窗口在横向、纵向上的索引
int x = 0;
int y = 0;
for (JInternalFrame frame : getAllFrames()) {
try {
// 取消内部窗口的最大化,最小化
frame.setMaximum(false);
frame.setIcon(false);
// 将窗口放在指定位置
frame.reshape(x * width, y * height, width, height);
y++;
// 每排完一列窗口
if (y == rows) {
// 开始排放下一列窗口
y = 0;
x++;
// 如果额外多出的窗口与剩下的列数相等,
// 则后面所有列都需要多排列一个窗口
if (extra == cols - x) {
rows++;
height = getHeight() / rows;
}
}
} catch (PropertyVetoException e) {
}
}
}

// 选中下一个非图标窗口
public void selectNextWindow() {
JInternalFrame[] frames = getAllFrames();
for (int i = 0; i < frames.length; i++) {
if (frames[i].isSelected()) {
// 找出下一个非最小化的窗口,尝试选中它,
// 如果选中失败,则继续尝试选中下一个窗口
int next = (i + 1) % frames.length;
while (next != i) {
// 如果该窗口不是处于最小化状态
if (!frames[next].isIcon()) {
try {
frames[next].setSelected(true);
frames[next].toFront();
frames[i].toBack();
return;
} catch (PropertyVetoException e) {
}
}
next = (next + 1) % frames.length;
}
}
}
}
}

上面程序中示范了创建JDesktopPane虚拟桌面创建JInternatFrame内部窗口,并将内部窗口添加到虚拟桌面中,最后将虚拟桌面添加到顶级JFrame容器中的过程。
运行上面程序,会看到如图12.18所示的内部窗口效果。
这里有一张图片

该变内部窗口的拖动模式

在默认情况下,当用户拖动窗口时,内部窗口会紧紧跟随用户鼠标的移动,这种操作会导致系统不断重绘虚拟桌面的内部窗口,从而引起性能下降。为了改变这种拖动模式,可以设置当用户拖动内部窗口时,虚拟桌面上仅绘出该内部窗口的轮廓。可以通过调用JDesktopPanesetDragMode()方法来改变内部窗口的拖动模式

方法 描述
void setDragMode(int dragMode) Sets the “dragging style” used by the desktop pane.

该方法接收如下两个参数值。

dragMode参数值 描述
JDesktopPane.OUTLINE_DRAG_MODE 拖动过程中仅显示内部窗口的轮廓。
JDesktopPane.LIVE_DRAG_MODE 拖动过程中显示完整窗口,这是默认选项。

上面程序中①处代码允许用户根据CheckBoxMenuitem的状态来决定窗口采用哪种拖动模式。
读者可能会发现,程序创建虚拟桌面时并不是直接创建JDesktopPane对象,而是先扩展JDesktopPane类,为该类增加了如下三个方法。

  • cascadeWindows():级联显示所有的内部窗口。
  • tileWindows():平铺显示所有的内部窗口。
  • selectNextWindow():选中当前窗口的下一个窗口。

JDesktopPane没有提供这三个方法,但这三个方法在MDI应用里又是如此常用,以至于开发者总需要自己来扩展JDesktopPane类,而不是直接使用该类。这是一个非常有趣的地方:Oracle似乎认为这些方法太过简单,不屑为之,于是开发者只能自己实现,这给编程带来一些麻烦。

级联 显示窗口

级联显示窗口其实很简单,先根据内部窗口与JDesktopPane的大小比例计算出每个内部窗口的大小,然后以此重新排列每个窗口,重排之前让相邻两个窗口在横向、纵向上产生一定的位移即可。

平铺 显示窗口

平铺显示窗口相对复杂一点,程序先计算需要几行、几列可以显示所有的窗口,如果还剩下多余(不能整除)的窗口,则依次分布到最后几列中。图12.19显示了平铺窗口的效果。
这里有一张图片

程序 弹出内部对话框

前面介绍JOptionPane时提到该类包含了多个重载的showInternalXxxDialog()方法,这些方法用于弹出内部对话框,当使用该方法来弹出内部对话框时通常需要指定一个父组件,这个父组件既可以是虚拟桌面(JDesktopPane对象),也可以是内部窗口(JInternalFrame对象)。下面程序示范了如何弹出内部对话框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.awt.BorderLayout;
import java.awt.Dimension;
import javax.swing.JButton;
import javax.swing.JDesktopPane;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

public class InternalDialogTest {
private JFrame jf = new JFrame("测试内部对话框");
private JDesktopPane desktop = new JDesktopPane();
private JButton internalBn = new JButton("内部窗口的对话框");
private JButton deskBn = new JButton("虚拟桌面的对话框");
// 定义一个内部窗口,该窗口可拖动,但不可最大化、最小化、关闭
private JInternalFrame iframe = new JInternalFrame("内部窗口");

public void init() {
// 向内部窗口中添加组件
iframe.add(new JScrollPane(new JTextArea(8, 40)));
desktop.setPreferredSize(new Dimension(400, 300));
// 把虚拟桌面添加到JFrame窗口中
jf.add(desktop);
// 设置内部窗口的大小、位置
iframe.reshape(0, 0, 300, 200);
// 显示并选中内部窗口
iframe.show();
desktop.add(iframe);
JPanel jp = new JPanel();
deskBn.addActionListener(event ->
// 弹出内部对话框,以虚拟桌面作为父组件
JOptionPane.showInternalMessageDialog(desktop, "属于虚拟桌面的对话框"));
internalBn.addActionListener(event ->
// 弹出内部对话框,以内部窗口作为父组件
JOptionPane.showInternalMessageDialog(iframe, "属于内部窗口的对话框"));
jp.add(deskBn);
jp.add(internalBn);
jf.add(jp, BorderLayout.SOUTH);
jf.pack();
jf.setVisible(true);
}

public static void main(String[] args) {
new InternalDialogTest().init();
}
}

上面程序中两个按钮可以弹出两个内部对话框,这两个对话框一个以虚拟桌面作为父窗口,一个以内部窗口作为父组件。运行上面程序会看到如图12.20所示的内部窗口的对话框
这里有一张图片