12.4 Swing简化的拖放功能

JDK1.4开始,Swing的部分组件已经提供了默认的拖放支持,从而能以更简单的方式进行拖放操作。Swing中支持拖放操作的组件如表12.1所示

Swing组件 作为拖放源导出 作为拖放目标接收
JColorChooser 导出颜色对象的本地引用 可接收任何颜色
JFilechooser 导出文件列表
JList 导出所选择节点的HTML描述
JTable 导出所选中的行
JTree 导出所选择节点的HTML描述
JTextComponent 导出所选文本 接收文本,其子类JTextArea还可接收文件列表,负责将文件打开

在默认情况下,表12.1中的这些Swing组件都没有启动拖放支持,可以调用这些组件的setDragEnabled(true)方法来启动拖放支持。

程序 Swing拖放

下面程序示范了Swing提供的拖放支持。

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
import java.awt.*;
import javax.swing.*;

public class SwingDndSupport {
JFrame jf = new JFrame("Swing的拖放支持");
JTextArea srcTxt = new JTextArea(8, 30);
JTextField jtf = new JTextField(34);

public void init() {
srcTxt.append("Swing的拖放支持.\n");
srcTxt.append("将该文本域的内容拖入其他程序.\n");
// 启动文本域和单行文本框的拖放支持
srcTxt.setDragEnabled(true);
jtf.setDragEnabled(true);
jf.add(new JScrollPane(srcTxt));
jf.add(jtf, BorderLayout.SOUTH);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.pack();
jf.setVisible(true);
}

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

上面程序中的

1
2
srcTxt.setDragEnabled(true);
jtf.setDragEnabled(true);

两行代码负责开始多行文本域和单行文本框的拖放支持。运行上面程序,会看到如图12.21所示的界面。
这里有一张图片

TransferHandler

除此之外,Swing还提供了一种非常特殊的类:TransferHandler,它可以直接将某个组件的指定属性设置成拖放目标,前提是该组件具有该属性的setter方法。例如,JTextArea类提供了一个setForeground(Color)方法,这样即可利用TransferHandlerforeground定义成拖放目标。代码如下:

1
2
//允许直接将一个co1or对象拖入该 jTextArea对象,并赋给它的 foreground属性
jTextArea.setTransferHandler(new TransferHandler("foreground"));

程序 拖动颜色来改变文本前景色

下面程序可以直接把颜色选择器面板中的颜色拖放到指定文本域中,用以改变指定文本域的前景色。

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
import java.awt.*;
import javax.swing.*;

public class TransferHandlerTest {
private JFrame jf = new JFrame("测试TransferHandler");
JColorChooser chooser = new JColorChooser();
JTextArea txt = new JTextArea("测试TransferHandler\n" + "直接将上面颜色拖入以改变文本颜色");

public void init() {
// 启动颜色选择器面板和文本域的拖放功能
chooser.setDragEnabled(true);
txt.setDragEnabled(true);
jf.add(chooser, BorderLayout.SOUTH);
// 允许直接将一个Color对象拖入该JTextArea对象
// 并赋给它的foreground属性
txt.setTransferHandler(new TransferHandler("foreground"));
jf.add(new JScrollPane(txt));
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.pack();
jf.setVisible(true);
}

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

上面程序中的粗体字代码将JTextAreaforeground属性转换成拖放目标,它可以接收任何Color对象。而JColorChooser启动拖放功能后可以导出颜色对象的本地引用,从而可以直接将该颜色对象拖给JTextAreaforeground属性。运行上面程序,会看到如图12.22所示的界面。
这里有一张图片

从图12.22中可以看出,当用户把颜色选择器面板中预览区的颜色拖到上面多行文本域后,多行文本域的颜色也随之发生改变

18.3. 2从Class中获取信息

Class类提供了大量的实例方法来获取该Class对象所对应类的详细信息, Class类大致包含如下方法,下面每个方法都可能包括多个重载的版本,读者应该查阅API文档来掌握它们。

获取构造器

下面4个方法用于获取Class对应类所包含的构造器

方法 描述
Constructor<T> getConstructor(Class<?>...parameter Types) 返回此Class对象对应类的、带指定形参列表的public构造器。
Constructor<?>[] getConstructors() 返回此 Class对象对应类的所有public构造器。
Constructor<T> getDeclaredConstructor( Class<?>...parameterTypes) 返回此 Class对象对应类的带指定形参列表的构造器,与构造器的访问权限无关。
Constructor<?>[] getDeclaredConstructors() 返回此 Class对象对应类的所有构造器,与构造器的访问权限无关。

获取方法

下面4个方法用于获取Class对应类所包含的方法。

方法 描述
Method getMethod(String name, Classs<?> ... parameterTypes) 返回此Class对象对应类的、带指定形参列表的 public方法。
Method[] getMethods() 返回此Class对象所表示的类的所有 public方法。
Method getDeclaredMethod(String name, Class<?> ... parameter Types) 返回此 Class对象对应类的、带指定形参列表的方法,与方法的访问权限无关。
Method[] getDeclaredMethods() 返回此Class对象对应类的全部方法,与方法的访问权限无关

获取成员变量

如下4个方法用于访问Class对应类所包含的成员变量。

方法 描述
Field getField(String name) 返回此 Class对象对应类的、指定名称的 public成员变量。
Field[] getFields() 返回此Class对象对应类的所有 public成员变量。
Field getDeclaredField(String name) 返回此 Class对象对应类的、指定名称的成员变量,与成员变量的访问权限无关。
Field[] getDeclaredFields() 返回此 Class对象对应类的全部成员变量,与成员变量的访问权限无关

获取Annotation

如下几个方法用于访问Class对应类上所包含的Annotation

方法 描述
<A extends Annotation> A getAnnotation(Class<A> annotationClass 尝试获取该 Class象对应类上存在的、指定类型的 Annotation;如果该类型的注解不存在,则返回null
Annotation[] getAnnotations() 返回修饰该 Class对象对应类上存在的所有 Annotation
<A extends annotation> A getDeclaredAnnotation(Class<A> annotationClass) 这是Java 8新增的方法,该方法尝试获取直接修饰该Class对象对应类的、指定类型的Annotation;如果该类型的注解不存在,则返回null
Annotation[] getDeclaredAnnotations() 返回直接修饰该Class对应类的所有 Annotation
<A extends Annotation> A getAnnotationsByType(Class<A> annotationClass) 该方法的功能与前面介绍的getAnnotation()方法基本相似。但由于Java 8增加了重复注解功能,因此需要使用该方法获取修饰该类的、指定类型的多个Annotation

获取包含的内部类

如下方法用于访问该Class对象对应类包含的内部类。

方法 描述
Class<?> getClasses() 返回该Class对象对应类里包含的全部内部类。

获取所在的外部类

如下方法用于访问该Class对象对应类所在的外部类。

方法 描述
Class<?> getDeclaringClass() 返回该Class对象对应类所在的外部类

访问该Class对象对应类所实现的接口

如下方法用于访问该Class对象对应类所实现的接口

方法 描述
Classs<?>[] getInterfaces() 返回该Class对象对应类所实现的全部接口。

获取所继承的父类

如下几个方法用于访问该Class对象对应类所继承的父类。

方法 描述
Class<? super T> getSuperClass() 返回该Class对象对应类的超类的Class对象。

获取修饰符 所在包 类名等基本信息

如下方法用于获取Class对象对应类的修饰符、所在包、类名等基本信息。

方法 描述
int getModifiers() 返回此类或接口的所有修饰符。修饰符由 publicprotectedprivatefinalstaticabstract等对应的常量组成,返回的整数应使用 Modifier工具类的方法来解码,才可以获取真实的修饰符。
Package getPackage() 获取此类的包。
String getName() 以字符串形式返回此Class象所表示的类的名称。
String getSimpleName() 以字符串形式返回此Class对象所表示的类的简称。

判断该类是否为接口 枚举 注解类型

除此之外, Class对象还可调用如下几个判断方法来判断该类是否为接口、枚举、注解类型等。

方法 描述
boolean isAnnotation() 返回此Class对象是否表示一个注解类型(由@interface定义)
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) 判断此Class对象是否使用了Annotation修饰
boolean isAnonymousClass() 返回此Class对象是否是一个匿名类。
boolean isArray() 返回此Class对象是否表示一个数组类。
boolean isEnum() 返回此Class象是否表示一个枚举(由enum关键字定义)。
boolean isInterface() 返回此 Class对象是否表示一个接口(使用 interface定义)。
boolean isInstance(Object object) 判断object是否是此Class对象的实例,该方法可以完全代替instanceof操作符。

如何通过反射获取一个方法

上面的多个getMethod()方法和getConstructor()方法中,都需要传入多个类型为Classs<?>的参数,用于获取指定的方法或指定的构造器。关于这个参数的作用,假设某个类内包含如下三个info方法签名。

  • public void info()
  • public void info(String str)
  • public void info(String str,Integer num)

这三个同名方法属于重载,它们的方法名相同,但参数列表不同。在Java语言中如果需要确定一个方法,则应该由方法名和形参列表来确定,但形参名没有任何实际意义,所以只能由形参类型来确定。例如想指定第二个info方法,则必须指定:

  • 方法名为info,
  • 形参列表为String.class

因此在程序中获取该方法使用如下代码:

  • clazz.getMethod("info", String.class)
    • 前一个参数指定方法名,后面的个数可变的Class参数指定形参类型列表。

如果需要获取第三个info方法,则使用如下代码:

  • clazz.getMethod("info", String.class, Integer.class)
    • 前一个参数指定方法名,后面的个数可变的Class参数指定形参类型列表。

如何通过反射获取一个构造器

获取构造器时无须传入构造器名—同一个类的所有构造器的名字都是相同的,所以要确定一个构造器只要指定形参列表即可

程序示例

下面程序示范了如何通过该Class对象来获取对应类的详细信息。

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
import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;

// 定义可重复注解
@Repeatable(Annos.class)
@interface Anno {}
@Retention(value=RetentionPolicy.RUNTIME)
@interface Annos {
Anno[] value();
}
// 使用4个注解修饰该类
@SuppressWarnings(value="unchecked")
@Deprecated
// 使用重复注解修饰该类
@Anno
@Anno
public class ClassTest
{
// 为该类定义一个私有的构造器
private ClassTest()
{
}
// 定义一个有参数的构造器
public ClassTest(String name)
{
System.out.println("执行有参数的构造器");
}
// 定义一个无参数的info方法
public void info()
{
System.out.println("执行无参数的info方法");
}
// 定义一个有参数的info方法
public void info(String str)
{
System.out.println("执行有参数的info方法"
+ ",其str参数值:" + str);
}
// 定义一个测试用的内部类
class Inner
{
}
public static void main(String[] args)
throws Exception
{
// 下面代码可以获取ClassTest对应的Class
// 获取该Class对象所对应类的全部构造器
Class<ClassTest> clazz = ClassTest.class;
Constructor[] ctors = clazz.getDeclaredConstructors();
System.out.println("ClassTest的全部构造器如下:");
for (Constructor c : ctors)
{
System.out.println(c);
}
// 获取该Class对象所对应类的全部public构造器
Constructor[] publicCtors = clazz.getConstructors();
System.out.println("ClassTest的全部public构造器如下:");
for (Constructor c : publicCtors)
{
System.out.println(c);
}
// 获取该Class对象所对应类的全部public方法
Method[] mtds = clazz.getMethods();
System.out.println("ClassTest的全部public方法如下:");
for (Method md : mtds)
{
System.out.println(md);
}
// 获取该Class对象所对应类的指定方法
System.out.println("ClassTest里带一个字符串参数的info()方法为:"
// 获取该Class对象所对应类的上的全部注解
+ clazz.getMethod("info" , String.class));
Annotation[] anns = clazz.getAnnotations();
System.out.println("ClassTest的全部Annotation如下:");
for (Annotation an : anns)
{
System.out.println(an);
}
System.out.println("该Class元素上的@SuppressWarnings注解为:"
+ Arrays.toString(clazz.getAnnotationsByType(SuppressWarnings.class)));
System.out.println("该Class元素上的@Anno注解为:"
// 获取该Class对象所对应类的全部内部类
+ Arrays.toString(clazz.getAnnotationsByType(Anno.class)));
Class<?>[] inners = clazz.getDeclaredClasses();
System.out.println("ClassTest的全部内部类如下:");
for (Class c : inners)
{
System.out.println(c);
}
// 使用Class.forName方法加载ClassTest的Inner内部类
// 通过getDeclaringClass()访问该类所在的外部类
Class inClazz = Class.forName("ClassTest$Inner");
System.out.println("inClazz对应类的外部类为:" +
inClazz.getDeclaringClass());
System.out.println("ClassTest的包为:" + clazz.getPackage());
System.out.println("ClassTest的父类为:" + clazz.getSuperclass());
}
}

上面程序获取了ClassTest类对应的Class对象后,通过调用该Class对象的不同方法来得到该Class对象的详细信息。运行结果如下:

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
ClassTest的全部构造器如下:
private ClassTest()
public ClassTest(java.lang.String)
ClassTest的全部public构造器如下:
public ClassTest(java.lang.String)
ClassTest的全部public方法如下:
public static void ClassTest.main(java.lang.String[]) throws java.lang.Exception
public void ClassTest.info(java.lang.String)
public void ClassTest.info()
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
ClassTest里带一个字符串参数的info()方法为:public void ClassTest.info(java.lang.String)
ClassTest的全部Annotation如下:
@java.lang.Deprecated()
@Annos(value=[@Anno(), @Anno()])
该Class元素上的@SuppressWarnings注解为:[]
该Class元素上的@Anno注解为:[@Anno(), @Anno()]
ClassTest的全部内部类如下:
class ClassTest$Inner
inClazz对应类的外部类为:class ClassTest
ClassTest的包为:null
ClassTest的父类为:class java.lang.Object

从上面运行结果来看, Class提供的功能非常丰富,它可以获取该类里包含的构造器方法内部类注解等信息,也可以获取该类所包括的成员变量(Field)信息。

Class无法获取值显示在源代码上的注解

值得指出的是,虽然定义ClassTest类时使用了 @SuppressWarnings注解,但程序运行时无法分析出该类里包含的该注解,这是因为@SuppressWarnings使用了@Retention(value= SOURCE)修饰,这表明@SuppressWarnings只能保存在源代码级别上,而通过ClassTest.class取该类的运行时Class对象,所以程序无法访问到@SuppressWarnings注解。

对于只能在源代码上保留的注解,使用运行时获得的Class对象无法访问到该注解对象。

通过Class对象可以得到大量的MethodConstructorField等对象,这些对象分别代表该类所包括的方法、构造器和成员变量等,程序还可以通过这些对象来执行实际的功能,例如调用方法、创建实例。

本文重点

通过Class类,

  • 可以获取对应类的构造器
  • 可以获取对应类中方法
  • 获取对应类的成员变量
  • 获取对应类的Annotation
  • 获取对应类包含的内部类
  • 获取对应类所在的外部类
  • 获取对应类所实现的接口
  • 获取对应类所继承的父类
  • 获取对应类的修饰符 所在包 类名等基本信息
  • 可以判断对应类是否为接口 枚举 注解类型
  • 在获取方法时需要传入方法名和参数列表,
  • 在获取构造器时只需要传入参数列表。

Class无法获取值显示在源代码上的注解,例如@SuppressWarnings注解无法通过反射获取

18.3.1 获得Class对象

前面已经介绍过了,每个类被加载之后,系统就会为该类生成一个对应的Class对象,通过该Class对象就可以访问到JVM中的这个类。在Java程序中获得Class对象通常有如下三种方式。

  1. 使用Class类的forName(String clazzName)静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定类名(必须添加完整包名)。
  2. 调用某个类的class属性来获取该类对应的Class对象。例如, Person.class.将会返回Person类应的Class对象。
  3. 调用某个对象的getClass方法。getClass方法是java.lang.Object类中的一个方法,所以所有的Java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象。

对于第一种方式和第二种方式都是直接根据类来取得该类的Class对象,相比之下,第二种方式(通过类的class属性)有如下两种优势。

  • 代码更安全。程序在编译阶段就可以检査需要访问的Class对象是否存在。
  • 程序性能更好。因为这种方式无须调用方法,所以性能更好。

也就是说,大部分时候都应该使用第二种方式来获取指定类的Class对象。

但如果程序只能获得一个字符串,例如"java.lang.String",若需要获取该字符串对应的Class对象,则只能使用Class类的forName(String clazzName)方法获取Class对象,该方法可能抛出一个ClassNotFoundException异常。
一旦获得了某个类所对应的Class对象之后,程序就可以调用Class对象的方法来获得该对象和该类的真实信息了。

本文重点

  • 可以通过Class类的forName()方法根据全限定类名获取Class对象。
  • 可以通过类的class属性获取该类的Class对象。
  • 可以通过该类的对象的getClass()方法获取该类的Class对象。

18.3 通过反射查看类信息

Java程序中的许多对象在运行时都会出现两种类型:编译时类型运行时类型,例如代码: Person p= new Student();,这行代码将会生成一个p变量,

  • 该变量的编译时类型为Person,
  • 运行时类型为Student

除此之外,还有更极端的情形,程序在运行时接收到外部传入的一个对象,该对象的编译时类型是Object,但程序又需要调用该对象运行时类型的方法。
为了解决这些问题,程序需要在运行时发现对象和类的真实信息。解决该问题有以下两种做法。

  • 第一种做法是假设在编译时和运行时都完全知道类型的具体信息,在这种情况下,可以先使用instanceof运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量即可。关于这种方式请参考5.7节的内容。
  • 第二种做法是编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。

18.2.4 URLCLassLoader类

JavaClassLoader提供了一个URLClassloader实现类,URLClassloader类是系统类加载器和扩展类加载器的父类(此处的父类,就是指类与类之间的继承关系)。 URLClassloader功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类
在应用程序中可以直接使用URLClassLoader加载类, URLClassLoader类提供了如下两个构造器。

构造方法 描述
URLClassLoader(URL[] urls) 使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询并加载类。
URLClassLoader(URL[] urls, ClassLoader parent) 使用指定的父类加载器创建一个ClassLoader对象,其他功能与前一个构造器相同

一旦得到了URLClassLoader对象之后,就可以调用该对象的loadClass()方法来加载指定类。

下面程序示范了如何直接从文件系统中加载MySQL驱动,并使用该驱动来获取数据库连接。通过这种方式来获取数据库连接,可以无须将MySQL驱动添加到CLASSPATH环境变量中.

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.sql.*;
import java.util.*;
import java.net.*;

public class URLClassLoaderTest
{
private static Connection conn;
// 定义一个获取数据库连接方法
public static Connection getConn(String url ,
String user , String pass) throws Exception
{
if (conn == null)
{
// 创建一个URL数组
URL[] urls = {new URL(
"file:mysql-connector-java-5.1.30-bin.jar")};
// 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
// 加载MySQL的JDBC驱动,并创建默认实例
Driver driver = (Driver)myClassLoader.
loadClass("com.mysql.jdbc.Driver").newInstance();
// 创建一个设置JDBC连接属性的Properties对象
Properties props = new Properties();
// 至少需要为该对象传入user和password两个属性
props.setProperty("user" , user);
props.setProperty("password" , pass);
// 调用Driver对象的connect方法来取得数据库连接
conn = driver.connect(url , props);
}
return conn;
}
public static void main(String[] args)throws Exception
{
System.out.println(getConn("jdbc:mysql://localhost:3306/mysql"
, "root" , "root"));
}
}

上面程序中的前两行粗体字代码创建了一个URLClassloader对象,该对象使用默认的父类加载器该类加载器的类加载路径是当前路径下的mysql-connector-java-5.1.30-bin.jar文件,将MySQL驱动复制到该路径下,这样保证该ClassLoader可以正常加载到com.mysql.jdbc Driver
程序的第三行粗体字代码使用ClassLoaderloadClass()方法加载指定类,并调用Class对象的newInstance()方法创建了一个该类的默认实例也就是得到com.mysql.jdbc.Driver类的对象,当然该对象的实现类实现了java.sql.Driver接口,所以程序将其强制类型转换为Driver.程序的最后一行粗体字代码通过Driver而不是DriverManager来获取数据库连接,关于Driver接口的用法读者可以自行查阅API文档。
正如前面所看到的,创建URLClassLoader时传入了一个URL数组参数,该ClassLoader就可以从这系列URL指定的资源中加载指定类,这里的URL:

  • 可以以file:为前缀,表明从本地文件系统加载;
  • 可以以http:为前缀,表明从互联网通过HTTP访问来加载;
  • 也可以以ftp:为前缀,表明从互联网通过FTP访问来加载;

本文重点

  • URLClassloader类是系统类加载器和扩展类加载器的父类
  • 在应用程序中可以直接使用URLClassLoader来加载类,URLClassLoader既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类.

18.2.3 创建并使用自定义的类加载器

JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。查阅API文档中关于ClassLoader的方法不难发现, ClassLoader中包含了大量的protected方法,这些方法都可被子类重写。

ClassLoader类关键方法

ClassLoader类有如下两个关键方法。

方法 描述
loadClass(String name, boolean resolve) 该方法为ClassLoader的入口点,根据指定名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的 Class对象。
findClass(String name) 根据指定名称来查找类

重写findClass方法自定义ClassLoader

如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,通常推荐重写findClass()方法,而不是重写loadClass()方法

loadClass方法的执行步骤

loadClass()方法的执行步骤如下。

  1. findClass(String)来检查是否已经加载类,如果已经加载则直接返回.
  2. 在父类加载器上调用loadClass()方法。如果父类加载器为null,则使用根类加载器来加载。
  3. 调用findClass(String)方法查找类。

从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂

defineClass方法

ClassLoader里还有一个核心的defineClass方法

方法 描述
Class defineClass(String name, byte[] b, int off,int len) 将指定类的字节码文件(即Class文件,如Hello.class)读入字节数组b内,并把它转换为Class对象,该字节码文件可以来源于文件、网络等.

defineClass方法管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。不过不用担心该方法是final方法,所以程序员无须重写该方法。

ClassLoader普通方法

除此之外, ClassLoader里还包含如下一些普通方法。

方法 描述
protected Class<?> findSystemClass(String name) 从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass方法将原始字节转换成Class对象,以将该文件转换成类
static ClassLoader getSystemClassLoader() 返回系统类加载器。
ClassLoader getParent() 获取该类加载器的父类加载器。
protected void resolveClass(Class<?> c) 链接指定的类。类加载器可以使用此方法来链接类c。读者无须理会关于此方法的太多细节。
protected Class<?> findLoadedClass(String name) 如果此Java虚拟机已加载了名为name的类,则直接返回该类对应的Class实例,否则返回null。该方法是Java类加载缓存机制的体现。

程序示例 自定义ClassLoader

下面程序开发了一个自定义的ClassLoader,该ClassLoader通过重写findClass()方法来实现自定义的类加载机制。这个ClassLoader可以在加载类之前先编译该类的源文件,从而实现运行Java之前先编译该程序的目标,这样即可通过该ClassLoader直接运行Java源文件。

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
import java.io.*;
import java.lang.reflect.*;

public class CompileClassLoader extends ClassLoader
{
// 读取一个文件的内容
private byte[] getBytes(String filename)
throws IOException
{
File file = new File(filename);
long len = file.length();
byte[] raw = new byte[(int)len];
try(
FileInputStream fin = new FileInputStream(file))
{
// 一次读取class文件的全部二进制数据
int r = fin.read(raw);
if(r != len)
throw new IOException("无法读取全部文件:"
+ r + " != " + len);
return raw;
}
}
// 定义编译指定Java文件的方法
private boolean compile(String javaFile)
throws IOException
{
System.out.println("CompileClassLoader:正在编译 "
+ javaFile + "...");
// 调用系统的javac命令
Process p = Runtime.getRuntime().exec("javac " + javaFile);
try
{
// 其他线程都等待这个线程完成
p.waitFor();
}
catch(InterruptedException ie)
{
System.out.println(ie);
}
// 获取javac线程的退出值
int ret = p.exitValue();
// 返回编译是否成功
return ret == 0;
}
// 重写ClassLoader的findClass方法
protected Class<?> findClass(String name)
throws ClassNotFoundException
{
Class clazz = null;
// 将包路径中的点(.)替换成斜线(/)。
String fileStub = name.replace("." , "/");
String javaFilename = fileStub + ".java";
String classFilename = fileStub + ".class";
File javaFile = new File(javaFilename);
File classFile = new File(classFilename);
// 当指定Java源文件存在,且class文件不存在、或者Java源文件
// 的修改时间比class文件修改时间更晚,重新编译
if(javaFile.exists() && (!classFile.exists()
|| javaFile.lastModified() > classFile.lastModified()))
{
try
{
// 如果编译失败,或者该Class文件不存在
if(!compile(javaFilename) || !classFile.exists())
{
throw new ClassNotFoundException(
"ClassNotFoundExcetpion:" + javaFilename);
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
// 如果class文件存在,系统负责将该文件转换成Class对象
if (classFile.exists())
{
try
{
// 将class文件的二进制数据读入数组
byte[] raw = getBytes(classFilename);
// 调用ClassLoader的defineClass方法将二进制数据转换成Class对象
clazz = defineClass(name,raw,0,raw.length);
}
catch(IOException ie)
{
ie.printStackTrace();
}
}
// 如果clazz为null,表明加载失败,则抛出异常
if(clazz == null)
{
throw new ClassNotFoundException(name);
}
return clazz;
}
// 定义一个主方法
public static void main(String[] args) throws Exception
{
// 如果运行该程序时没有参数,即没有目标类
if (args.length < 1)
{
System.out.println("缺少目标类,请按如下格式运行Java源文件:");
System.out.println("java CompileClassLoader ClassName");
}
// 第一个参数是需要运行的类
String progClass = args[0];
// 剩下的参数将作为运行目标类时的参数,
// 将这些参数复制到一个新数组中
String[] progArgs = new String[args.length-1];
System.arraycopy(args , 1 , progArgs
, 0 , progArgs.length);
CompileClassLoader ccl = new CompileClassLoader();
// 加载需要运行的类
Class<?> clazz = ccl.loadClass(progClass);
// 获取需要运行的类的主方法
Method main = clazz.getMethod("main" , (new String[0]).getClass());
Object[] argsArray = {progArgs};
main.invoke(null,argsArray);
}
}

上面程序中的重写了findClass()方法,通过重写该方法就可以实现自定义的类加载机制。在本类的findClass()方法中先检查需要加载类的Class文件是否存在,如果不存在则先编译源文件,再调用ClassLoaderdefineClass()方法来加载这个Class文件,并生成相应的Class对象。
接下来可以随意提供一个简单的主类,该主类无须编译就可以使用上面的CompileClassLoader来运行它。

1
2
3
4
5
6
7
8
9
10
public class Hello
{
public static void main(String[] args)
{
for (String arg : args)
{
System.out.println("运行Hello的参数:" + arg);
}
}
}

无须编译该Hello.java,可以直接使用如下命令来运行该Hello.java程序

1
java CompileClassLoader Hello 这是命令行参数

运行结果如下:

1
2
3
G:\Desktop\codes\18\18.2>java CompileClassLoader Hello 这是命令行参数
CompileClassLoader:正在编译 Hello.java...
运行Hello的参数:这是命令行参数

本示例程序提供的类加载器功能比较简单,仅仅提供了在运行之前先编译Java源文件的功能。

自定义类加载器可以实现什么功能

实际上,使用自定义的类加载器,可以实现如下常见功能。

  • 执行代码前自动验证数字签名
  • 根据用户提供的密码解密代码,从而可以实现代码混淆器来避免反编译*.class文件。
  • 根据用户需求来动态地加载类。
  • 根据应用需求把其他数据以字节码的形式加载到应用中。

18.2 类加载器

类加载器的作用

类加载器负责将.class文件加载到内存中,并为之生成对应的java.lang.Class对象。

18.2.1 类加载器简介

类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦个类被载入JVM中,同一个类就不会被再次载入了。现在的问题是,怎么样才算”同一个类”。
正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。

Java中用 全限定类名 作为类的标识

Java中,一个类用其全限定类名(包括包名和类名)作为标识;

JVM中用 全限定类名和该类的类加载器 作为类的标识

JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例k1负责加载,则该Person类对应的Class象在JVM中表示为(Person,pg,k1)
这意味着两个类加载器加载的同名类:(Person,pg,k1)(Person,pg,k2)是不同的、它们所加载的类也是完全不同、互不兼容的。

三个类加载器 根类加载器 扩展类加载器 系统类加载器

JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构.

  • Bootstrap Classloader:根类加载器。
  • Extension ClassLoader:扩展类加载器。
  • System ClassLoader:系统类加载器。

JVM类加载机制

JVM的类加载机制主要有如下三种

  1. 全盘负责。所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入
  2. 父类委托。所谓父类委托,则是先让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  3. 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系

除了可以使用Java提供的类加载器之外,开发者也可以实现自己的类加载器,自定义的类加载器通过继承ClassLoader来实现。

类加载器层次结构

JVM中这4种类加载器的层次结构如图18.1所示。
这里有一张图片

程序示例 访问JVM类加载器

下面程序示范了访问JVM的类加载器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;
import java.net.*;
import java.io.*;

public class ClassLoaderPropTest {
public static void main(String[] args) throws IOException {
// 获取系统类加载器
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + systemLoader);
/*
* 获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定 如果操作系统没有指定CLASSPATH环境变量,默认以当前路径作为
* 系统类加载器的加载路径
*/
Enumeration<URL> em1 = systemLoader.getResources("");
while (em1.hasMoreElements()) {
System.out.println(em1.nextElement());
}
// 获取系统类加载器的父类加载器:得到扩展类加载器
ClassLoader extensionLader = systemLoader.getParent();
System.out.println("扩展类加载器:" + extensionLader);
System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的parent: " + extensionLader.getParent());
}
}

运行上面程序,会看到如下运行结果:

1
2
3
4
5
系统类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader@15615099
file:/C:/Users/lan/AppData/Roaming/Code/User/workspaceStorage/df816d125d28dbce7d1e9b6ffdd792d2/redhat.java/jdt_ws/18.2_b30957d7/bin/
扩展类加载器:jdk.internal.loader.ClassLoaders$PlatformClassLoader@1c6b6478
扩展类加载器的加载路径:null
扩展类加载器的parent: null

从上面运行结果可以看出,系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的加载路径是null(与Java8有区别),但此处看到扩展类加载器的父加载器是null,并不是根类加载器。这是因为根类加载器并没有继承ClassLoader抽象类,所以扩展类加载器的getParent()方法返回null,但实际上,扩展类加载器的父类加载器是根类加载器,只是根类加载器并不是Java实现的.

从运行结果可以看出,系统类加载器是AppClassLoader的实例,扩展类加载器PlatformClassLoader的实例。实际上,这两个类都是URLClassloader类的实例。
JVM的根类加载器并不是Java实现的,而且由于程序通常无须访问根类加载器,因此访问扩展类加载器的父类加载器时返回null

类加载器加载Class步骤

类加载器加载Class大致要经过如下8个步骤。

  1. 检测此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第8步,否则接着执行第2步。
  2. 如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第4步执行;如果父类加载器存在,则接着执行第3步。
  3. 请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步。
  4. 请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步。
  5. 当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步。
  6. 从文件中载入Class,成功载入后跳到第8步。
  7. 抛出ClassNotFoundException异常。
  8. 返回对应的java.Lang.Class对象。

其中,第5、6步允许重写ClassLoaderfindClass()方法来实现自己的载入策略,甚至重写loadclass方法来实现自己的载入过程。

Bootstrap ClassLoader 引导类加载器

Bootstrap ClassLoader被称为引导类加载器,(也称为原始类加载器根类加载器),它负责加载Java的核心类。在SunJVM中,当执行java.exe命令时,使用-Xbootclasspath选项或使用-D选项指定sun.boot.classpath系统属性值可以指定加载附加的类

根类加载器(Bootstrap ClassLoader)非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。

程序示例 根类加载器

下面程序可以获得根类加载器所加载的核心类库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.net.*;

public class BootstrapTest
{
public static void main(String[] args)
{
// 获取根类加载器所加载的全部URL数组
URL[] urls = sun.misc.Launcher.
getBootstrapClassPath().getURLs();
// 遍历、输出根类加载器加载的全部URL
for (int i = 0; i < urls.length; i++)
{
System.out.println(urls[i].toExternalForm());
}
}
}

运行结果如下:

1
2
3
4
5
6
7
8
file:/E:/java/jdk1.8.0_91/jre/lib/resources.jar
file:/E:/java/jdk1.8.0_91/jre/lib/rt.jar
file:/E:/java/jdk1.8.0_91/jre/lib/sunrsasign.jar
file:/E:/java/jdk1.8.0_91/jre/lib/jsse.jar
file:/E:/java/jdk1.8.0_91/jre/lib/jce.jar
file:/E:/java/jdk1.8.0_91/jre/lib/charsets.jar
file:/E:/java/jdk1.8.0_91/jre/lib/jfr.jar
file:/E:/java/jdk1.8.0_91/jre/classes

看到这个运行结果,读者应该明白为什么程序中可以使用StringSystem这些核心类库—因为这些核心类库都在rt.jar文件中。

Extension ClassLoader类加载器

Extension ClassLoader被称为扩展类加载器,它负责加载JRE的扩展目录中JAR包的类,扩展目录为:%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系统属性指定的目录。

System ClassLoader类加载器

System ClassLoader被称为系统类加载器,也称为应用类加载器,它负责在JVM启动时加载来自java命令的 -classpath选项、 java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoadergetSystemClassLoader静态方法来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以系统类加载器作为父加载器。

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所示的内部窗口的对话框
这里有一张图片

12.3.2 使用JTabbledPane 标签页

JTabbledPane可以很方便地在窗口上放置多个标签页,每个标签页相当于获得了一个与外部容器具有相同大小的组件摆放区域。通过这种方式,就可以在一个容器里放置更多的组件,例如右击桌面上的我的电脑”图标,在弹出的快捷菜单里单击“属性”菜单项,就可以看到一个“系统属性”对话框,这个对话框里包含了7个标签页。

创建标签页的步骤

如果需要使用JTabbedPane在窗口上创建标签页,则可以按如下步骤进行。

1 创建JTabbedPane对象

创建一个JTabbedPane对象,JTabbedPane提供了几个重载的构造器

方法 描述
JTabbedPane() Creates an empty TabbedPane with a default tab placement of JTabbedPane.TOP.
JTabbedPane(int tabPlacement) Creates an empty TabbedPane with the specified tab placement of either: JTabbedPane.TOP, JTabbedPane.BOTTOM, JTabbedPane.LEFT, or JTabbedPane.RIGHT.
JTabbedPane(int tabPlacement, int tabLayoutPolicy) Creates an empty TabbedPane with the specified tab placement and tab layout policy.

这些构造器里一共包含如下两个参数。

  • tabPlacement:该参数指定标签页标题的放置位置,例如前面介绍的“系统属性”对话框里标签页的标题放在窗口顶部。Swing支持将标签页标题放在窗口的4个方位:
    • JTabbedPane.TOP(顶部)、
    • JTabbedPane.LEFT(左边)、
    • JTabbedPane.BOTTOM(下部)
    • JTabbedPane.RIGHT(右边)
  • tabLayoutPolicy:指定标签页标题的布局策略。当窗口不足以在同一行摆放所有的标签页标题时Swing有两种处理方式:
    • 将标签页标题换行(JTabbedPane.WRAP_TAB_LAYOUT )排列
    • 使用滚动条来控制标签页标题的显示(JTabbedPane.SCROLL_TAB_LAYOUT)。

例如,下面代码创建一个JTabbedPane对象,该JTabbedPane的标签页标题位于窗口左侧,当窗口的一行不能摆放所有的标签页标题时,JTabbedPane将采用换行方式来排列标签页标题:

1
JTabbedPane tabPane = new JTabbedPane (JTabbedPane.LEFT,JTabbedPane.WRAP_TAB_LAYOUT);

2 增加 插入 修改 删除 标签页

调用JTabbedPane对象的addAbb()insertTab()setComponentAt()removeSabAt()方法来增加、插入、修改和删除标签页。其中

增加标签页

addAbb()方法总是在最前面增加标签页:

方法 描述
void addTab(String title, Component component) Adds a component represented by a title and no icon.
void addTab(String title, Icon icon, Component component) Adds a component represented by a title and/or icon, either of which can be null.
void addTab(String title, Icon icon, Component component, String tip) Adds a component and tip represented by a title and/or icon, either of which can be null.

添加标签页时可以指定该标签页的标题(title)、图标(icon,以及该Tab页面的组件(component)及提示信息(tip),这4个参数都可以是null:如果某个参数是null,则对应的内容为空。

不要使用add方法来添加标签页

不要使用JTabbedPaneadd()方法来添加组件,该方法是JTabbedPane重写Containner容器中的add方法,如果使用该ad()方法来添加Tab页面,毎次添加的标签页会直接覆盖原有的标签页。

插入 修改 删除标签页

insertTab()表示在指定位置插入标签页、setComponentAt()修改指定位置的标签页,removeTabAt()方法删除指定位置的标签页。

方法 描述
void insertTab(String title, Icon icon, Component component, String tip, int index) Inserts a new tab for the given component, at the given index, represented by the given title and/or icon, either of which may be null.
void setComponentAt(int index, Component component) Sets the component at index to component.
void removeTabAt(int index) Removes the tab at index.

先将组建放到容器中 再讲容器放到标签页中

不管使用增加、插入、修改哪种操作来改变JTabbedPane中的标签页,都是传入一个Component组件作为标签页。也就是说,如果希望在某个标签页内放置更多的组件,则必须先将这些组件放置到个容器(例如JPanel)里,然后将该容器设置为JTabbedPane指定位置的组件

3 显示某个标签页

如果需要让某个标签页显示出来,则可以通过调用JTabbedPanesetSelectedIndex()方法来实现:

方法 描述
void setSelectedIndex(int index) Sets the selected index for this tabbedpane.

例如如下代码

1
2
3
4
// 设置第三个Tab页面处于显示状态
tabPane.setSelectedIndex(2);
//设置最后一个Tab页面处于显示状态
tabPane.setSelectedIndex(tabPanel.getTabCount()-1);

4 操作JTabbedPane属性

程序还可通过JTabbedPane提供的一系列方法来操作JTabbedPane的相关属性。例如,有如下几个常用方法

方法 描述
void setDisabledIconAt(int index, Icon disabledIcon) 将指定位置的禁用图标设置为icon,该图标也可以是null,表示不使用禁用图标。
void setEnabledAt(int index, boolean enabled) 设置指定位置的标签页是否启用。
void setForegroundAt(int index, Color foreground) 设置指定位置标签页的前景色为foreground。该颜色可以是nul,这时将使用该JTabbedPane的前景色作为此标签页的前景色。
void setIconAt(int index, Icon icon) 设置指定位置标签页的图标。
void setTitleAt(int index, String title) 设置指定位置标签页的标题为title,该title可以是null,这表明设置该标签页的标题为空。
void setToolTipTextAt(int index, String toolTipText) 设置指定位置标签页的提示文本。

5 监听标签页事件

如果程序需要监听用户单击标签页的事件,例如,当用户单击某个标签页时才载入该标签页的内容,则可以使用ChangeListener监听器来监听JTabbedPane对象

例如如下代码:

1
tabPane.addChangeListener(listener);

当用户单击标签页时,系统将把该事件封装成ChangeEvent对象,并作为参数来触发ChangeListener里的stateChanged事件处理器方法:

方法 描述
void stateChanged(ChangeEvent e) Invoked when the target of the listener has changed its state.

程序 标签页

下面程序定义了具有5个标签页的JTabbedPane面板,该程序可以让用户选择标签布局策略、标签位置。

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
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTabbedPane;

public class JTabbedPaneTest {
JFrame jf = new JFrame("测试Tab页面");
// 创建一个Tab页面的标签放在左边,采用换行布局策略的JTabbedPane
JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.LEFT, JTabbedPane.WRAP_TAB_LAYOUT);
ImageIcon icon = new ImageIcon("ico/close.gif");
String[] layouts = { "换行布局", "滚动条布局" };
String[] positions = { "左边", "顶部", "右边", "底部" };
Map<String, String> books = new LinkedHashMap<>();

public void init() {
books.put("疯狂Java讲义", "java.png");
books.put("轻量级Java EE企业应用实战", "ee.png");
books.put("疯狂Ajax讲义", "ajax.png");
books.put("疯狂Android讲义", "android.png");
books.put("经典Java EE企业应用实战", "classic.png");
String tip = "可看到本书的封面照片";
// 向JTabbedPane中添加5个标签页面,指定了标题、图标和提示
// 但该标签页面的组件为null
for (String bookName : books.keySet()) {
tabbedPane.addTab(bookName, icon, null, tip);
}
jf.add(tabbedPane, BorderLayout.CENTER);
// 为JTabbedPane添加事件监听器
tabbedPane.addChangeListener(event -> {
// 如果被选择的组件依然是空
if (tabbedPane.getSelectedComponent() == null) {
// 获取所选标签页
int n = tabbedPane.getSelectedIndex();
// 为指定标前页加载内容
loadTab(n);
}
});
// 系统默认选择第一页,加载第一页内容
loadTab(0);
tabbedPane.setPreferredSize(new Dimension(500, 300));
// 增加控制标签布局、标签位置的单选按钮
JPanel buttonPanel = new JPanel();
ChangeAction action = new ChangeAction();
buttonPanel.add(new ButtonPanel(action, "选择标签布局策略", layouts));
buttonPanel.add(new ButtonPanel(action, "选择标签位置", positions));
jf.add(buttonPanel, BorderLayout.SOUTH);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.pack();
jf.setVisible(true);
}

// 为指定标签页加载内容
private void loadTab(int n) {
String title = tabbedPane.getTitleAt(n);
// 根据标签页的标题获取对应图书封面
ImageIcon bookImage = new ImageIcon("ico/" + books.get(title));
tabbedPane.setComponentAt(n, new JLabel(bookImage));
// 改变标签页的图标
tabbedPane.setIconAt(n, new ImageIcon("ico/open.gif"));
}

// 定义改变标签页的布局策略,放置位置的监听器
class ChangeAction implements ActionListener {
public void actionPerformed(ActionEvent event) {
JRadioButton source = (JRadioButton) event.getSource();
String selection = source.getActionCommand();
// 设置标签页的标题的布局策略
if (selection.equals(layouts[0])) {
tabbedPane.setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT);
} else if (selection.equals(layouts[1])) {
tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
}
// 设置标签页上的标题的放置位置
else if (selection.equals(positions[0])) {
tabbedPane.setTabPlacement(JTabbedPane.LEFT);
} else if (selection.equals(positions[1])) {
tabbedPane.setTabPlacement(JTabbedPane.TOP);
} else if (selection.equals(positions[2])) {
tabbedPane.setTabPlacement(JTabbedPane.RIGHT);
} else if (selection.equals(positions[3])) {
tabbedPane.setTabPlacement(JTabbedPane.BOTTOM);
}
}
}

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

// 定义一个JPanel类扩展类,该类的对象包含多个纵向排列的JRadioButton控件
// 且JPanel扩展类可以指定一个字符串作为TitledBorder
class ButtonPanel extends JPanel {
private static final long serialVersionUID = -6408828937684129034L;
private ButtonGroup group;

public ButtonPanel(JTabbedPaneTest.ChangeAction action, String title, String[] labels) {
setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), title));
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
group = new ButtonGroup();
for (int i = 0; labels != null && i < labels.length; i++) {
JRadioButton b = new JRadioButton(labels[i]);
b.setActionCommand(labels[i]);
add(b);
// 添加事件监听器
b.addActionListener(action);
group.add(b);
b.setSelected(i == 0);
}
}
}

程序运行后会看到如图12.15所示的标签页效果

12.3 Swing中的特殊容器

Swing提供了一些具有特殊功能的容器,这些特殊容器可以用于创建一些更复杂的用户界面。下面将依次介绍这些特殊容器。

12.3.1 使用JsplitPane 分割面板

JSplitPane用于创建一个分割面板,它可以将一个组件(通常是一个容器)分割成两个部分,并提供一个分割条,用户可以拖动该分割条来调整两个部分的大小。图12.13显示了分割面板效果,图中所示的窗口先被分成左右两块,其中左边一块又被分为上下两块

这里有一张图片

如何创建分割面板

从图12.13中可以看岀,分割面板的实质是一个特殊容器,该容器只能容纳两个组件,而且分割面板又分为上下分割、左右分割两种情形,所以创建分割面板的代码非常简单,如下代码所示。

1
new JSplitPane(方向,左/上组件,右/下组件);

除此之外,创建分割面板时可以指定一个newContinuousLayout参数,该参数指定该分割面板是否支持“连续布局”。

如果分割面板支持连续布局,则用户拖动分割条时两边组件将会不断调整大小

如果不支持连续布局,则拖动分割条时两边组件不会调整大小,而是只看到一条虚拟的分割条在移动,如图12.14所示。

这里有一张图片

默认不连续分布

JSplitPane默认关闭连续布局特性,因为使用连续布局需要不断重绘两边的组件,因此运行效率很低。

如何打开连续分布

如果需要打开指定JSplitPane面板的连续布局特性,则可以使用如下代码:

1
2
//打开JSplitPane的连续布局特性
jSplitPane.setContinuousLayout(true);

一键展开特性

除此之外,正如图12.13中看到的,上下分割面板的分割条中还有两个三角箭头,这两个箭头被称为“一触即展”键,当用户单击某个三角箭头时,将看到箭头所指的组件慢慢缩小到没有,而另一个组件则扩大到占据整个面板。如果需要打开“一触即展”特性,使用如下代码即可:

1
2
//打开“一触即展”特性
jSplitPane.setOneTouchExpandable(true);
方法 描述
void setDividerLocation(double proportionalLocation) 设置分隔条的位置JSplitPane的某个百分比
void setDividerLocation(int location) 通过像素值设置分隔条的位置
void setDividerSize(int newSize) 通过像素值设置分隔条的大小
void setLeftComponent(Component comp) 将指定组件放置到分割面板的左边或者上面。
void setRightComponent(Component comp) 将指定组件放置到分割面板的右边或者下面。

程序

下面程序简单示范了JSplitPane的用法。

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
import java.awt.Dimension;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;

class Book {
private String name;
private Icon ico;
private String desc;

public Book() {
}

public Book(String name, Icon ico, String desc) {
this.name = name;
this.ico = ico;
this.desc = desc;
}

// name的setter和getter方法
public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

// ico的setter和getter方法
public void setIco(Icon ico) {
this.ico = ico;
}

public Icon getIco() {
return this.ico;
}

// desc的setter和getter方法
public void setDesc(String desc) {
this.desc = desc;
}

public String getDesc() {
return this.desc;
}

public String toString() {
return name;
}
}

public class SplitPaneTest {
Book[] books = new Book[] { new Book("疯狂Java讲义", new ImageIcon("ico/java.png"), "国内关于Java编程最全面的图书\n看得懂,学得会"),
new Book("轻量级Java EE企业应用实战", new ImageIcon("ico/ee.png"), "SSH整合开发的经典图书,值得拥有"),
new Book("疯狂Android讲义", new ImageIcon("ico/android.png"), "全面介绍Android平台应用程序\n开发的各方面知识") };
JFrame jf = new JFrame("测试JSplitPane");
JList<Book> bookList = new JList<>(books);
JLabel bookCover = new JLabel();
JTextArea bookDesc = new JTextArea();

public void init() {
// 为三个组件设置最佳大小
bookList.setPreferredSize(new Dimension(150, 300));
bookCover.setPreferredSize(new Dimension(300, 150));
bookDesc.setPreferredSize(new Dimension(300, 150));
// 为下拉列表添加事件监听器
bookList.addListSelectionListener(event -> {
Book book = (Book) bookList.getSelectedValue();
bookCover.setIcon(book.getIco());
bookDesc.setText(book.getDesc());
});
// 创建一个垂直的分割面板,
// 将bookCover放在上面,将bookDesc放在下面, 支持连续布局
JSplitPane left = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, bookCover, new JScrollPane(bookDesc));
// 打开“一触即展”的特性
left.setOneTouchExpandable(true);
// 下面代码设置分割条的大小。
// left.setDividerSize(50);
// 设置该分割面板根据所包含组件的最佳大小来调整布局
left.resetToPreferredSizes();
// 创建一个水平的分割面板
// 将left组件放在左边,将bookList组件放在右边
JSplitPane content = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, left, bookList);
jf.add(content);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.pack();
jf.setVisible(true);
}

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

上面代码中粗体字代码创建了两个JSplitPane,其中一个水平分隔条支持连续布局,垂直不支持连续布局
运行上面程序,将可看到如图12.13所示的界面。

这里有一张图片