创建图形化的用户界面
通过Swing来创建一个GUI
Swing中的并发
讨论Swing编程中的并发性,主要包含事件调度线程(event dispatch thread,EDT)以及SwingWorker类的学习
initial threads
每个程序都有一组线程,应用逻辑从这里开始。在标准程序中,只有一个这样的线程:调用程序类的主方法的线程。在小程序中,初始线程是构建小程序对象并调用其init和start方法的线程;这些动作可能发生在一个线程上,也可能发生在两个或三个不同的线程上,取决于Java平台的实现。
在Swing程序中,初始线程并没有太多的事情要做。他们最基本的工作是创建一个初始化GUI的Runnable对象,并安排该对象在事件调度线程上执行。一旦GUI被创建,程序主要由GUI事件驱动,每个事件都会导致EDT上一个短任务的执行。应用程序代码可以在事件调度线程上安排额外的任务(如果它们快速完成,以便不干扰事件处理)或worker线程(对于长期运行的任务)。
一个初始线程通过调用javax.swing.SwingUtilities.invokeLater或javax.swingUtilities.invokeAndWait来安排GUI创建任务。这两个方法都只接受一个参数:定义新任务的Runnable。它们唯一的区别是由它们的名字表示的:invokeLater调度任务后返回(异步);invokeAndWait在返回之前等待任务完成(同步)。
例如:
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
在一个小程序中,GUI创建任务必须使用invokeAndWait从init方法启动;否则,init可能在GUI创建之前返回,这可能会给启动小程序的网络浏览器带来问题。在任何其他类型的程序中,调度GUI创建任务通常是初始线程做的最后一件事,所以它是否使用invokeLater或invokeAndWait都不重要。
为什么初始线程不自己创建GUI呢?因为几乎所有创建或与Swing组件交互的代码都必须在EDT上运行。
The Event Dispatch Thread
Swing事件处理代码在一个特殊的线程上运行,称为事件调度线程(EDT) ,大部分调用Swing方法的代码也运行在这个线程上。因为大部分Swing对象的方法都不是线程安全的,所以通过多线程调用他们会存在风险 thread interference或者 memory consistency errors. 。一些Swing组件方法在API规范里面被标记为线程安全的,这些方法可以被任何线程安全的调用。其他的方法必须被EDT调用。忽略这一规则的程序可能功能没问题,但是可能会被难预测,难重现的问题影响。 把运行在EDT上的代码看成是一系列的短任务是很有用的,大部分任务都是事件处理方法的调用,例如ActionListener.actionPerformed.其他任务的执行可以由应用的代码来调度,通过使用invokdeLater或者invokeAndWait。事件调度线程上的任务必须快速完成;如果不完成,未处理的事件将备份,用户界面将变得无响应。
可以通过调用javax.swing.SwingUtilities.isEventDispatchThread来确认你的代码是否运行在EDT上
Worker Threads and SwingWorker
如果Swing程序需要执行一个长时间运行的任务,通常使用工作线程中的一个(worker thread),也叫后台线程。每个运行在工作线程上面的任务都由一个javax.swing.SwingWorker实例表示。SwingWorker是一个抽象类,必须要定义一个子类才能创建SwingWorker对象,通常使用匿名内部类创建非常简单的SwingWorker对象。
SwingWorker提供了许多通信和控制的功能:
- SwingWorker子类会定义一个方法 done ,在后端任务完成的时候会在EDT上自动调用
- SwingWorker实现了
java.util.concurrent.Future,这个接口使后台任务可以返回一个结果给其余的线程。这个接口内的其他方法也支持取消后台任务,以及判断是否后台任务已经被取消或者已经完成了。 - 后台任务可以通过调用SwingWorker.publish来提供一个中间的结果,使EDT调用SwingWorker.process
- 后台任务可以定义绑定的属性,这些属性的变化会出发事件,导致EDT调用对应事件处理的方法。
这些特性将在后续小节中讨论
注意:
- javax.swing.SwingWorker类是在Java SE 6中被添加到Java平台的。在这之前,另一个也叫SwingWorker的类被广泛用于一些相同的目的。旧的SwingWorker不是Java平台规范的一部分,也没有作为JDK的一部分提供。
- 新的javax.swing.SwingWorker是一个全新的类。它的功能不是旧的SwingWorker的严格超集。两个类中具有相同功能的方法的名称并不相同。另外,旧的SwingWorker类的实例是可以重用的,而每个新的后台任务都需要一个新的javax.swing.SwingWorker类的实例。
- 在整个Java教程中,任何提到SwingWorker的地方现在都是指javax.swing.SwingWorker。
Simple Background Tasks
从一个很简单的任务开始。TumbleItem程序加载一系列用于动画中的图形文件。如果图形文件从一个初始线程加载,在GUI出现之前会有一个延迟。如果是从EDT加载,GUI会短暂的没有反应。
为了避免这些问题,TumbleItem通过他的初始线程创建并执行了一个SwingWorker实例,该对象的doInBackground方法。在一个工作线程中执行,加载图片到一个ImageIcon数组里面去,并且返回一个它的引用,然后再EDT中执行 done方法,调用 get检索这个引用,并且将其分配给你个名为imgs的字段。这就可以让TumbleItem立刻构建出GUI,不需要等待图片完成加载。
下面是定义和执行SwingWorker对象的代码。
SwingWorker worker = new SwingWorker<ImageIcon[], Void>() {
@Override
public ImageIcon[] doInBackground() {
final ImageIcon[] innerImgs = new ImageIcon[nimgs];
for (int i = 0; i < nimgs; i++) {
innerImgs[i] = loadImage(i+1);
}
return innerImgs;
}
@Override
public void done() {
//Remove the "Loading images" label.
animator.removeAll();
loopslot = -1;
try {
imgs = get();
} catch (InterruptedException ignore) {}
catch (java.util.concurrent.ExecutionException e) {
String why = null;
Throwable cause = e.getCause();
if (cause != null) {
why = cause.getMessage();
} else {
why = e.getMessage();
}
System.err.println("Error retrieving file: " + why);
}
}
};
SwingWorker的所有具体子类都实现了doInBackground;done的实现是可选的。
SwingWorker是一个泛型类,带有两个类型参数,第一个类型参数指定了doInBackground 和get(被其他线程调用来检索doInBackground返回值的方法)的返回类型。第二个类型参数是当后台任务仍在运行中的时候返回的临时结果的类型。由于这个例子没有返回临时结果,所以用void作为占位符。
你可能会想,设置imgs的代码是不是太复杂了。为什么要让doInBackground返回一个对象并使用done来获取它?为什么不直接让doInBackground设置imgs?问题在于,imgs所指的对象是在工作线程中创建的,并在事件调度线程中使用。当对象以这种方式在线程之间共享时,你必须确保在一个线程中做出的改变对另一个线程是可见的。使用get可以保证这一点,因为使用get在创建imgs的代码和使用它的代码之间建立了一种happens before关系。关于happens before关系的更多信息,请参考Memory Consistency Errors
疑问:为什么imgs所对应的对象一定要在EDT中使用呢?为什么不在doInBackground方法中直接赋值?,即imgs = innerImgs,这样也不用考虑并发的问题,此时EDT线程存在的意义是什么?
解答:首先要明确createGUI方法需要被EDT调用,这个方法里面用到了imgs(解答 为什么imgs所对应的对象一定要在EDT中使用呢?),在doInBackground方法中直接赋值只是在工作线程里面对该变量进行了赋值,还需要保证EDT使用imgs时,这个变量不能受到多线程的影响(解答为什么不在doInBackground方法中直接赋值?),官网解释get()方法可以保证,在EDT使用该变量时,该变量已经包含了工作线程的改动(赋值)。所以需要EDT去调用SwingWorker的done方法即调用get()方法。不过done方法里面的逻辑也可以拿到createUI里面去,都是EDT调用,只不过是入口不一样,第一种是后台任务完成的回调,即通过调用done方法初始化imgs,另外一种是在正常的流程里,判断后台任务已经结束之后,再去初始化imgs。从API调用的角度,直接调用已有的回调接口更合适点。
实际上有两种方法来检索由doInBackground返回的对象。
- 调用
SwingWorker.getwith no arguments.。如果后台任务没有完成,get就会阻塞,直到它完成。 - 调用
SwingWorker.getwith arguments indicating a timeout.。如果后台任务没有完成,get就会阻塞,直到它完成为止--除非超时先过,在这种情况下get会抛出java.util.concurrent.TimeoutException。 当从EDT调用get方法的时候要注意;在get返回之前,没有GUI事件被处理,GUI被 "冻结"。除非你确信后台任务已经完成或接近完成,否则不要调用没有参数的get。
更多TumbleItem例子,参考 How to Use Swing Timers
Tasks that Have Interim Results
有时候需要后台任务在运行中的时候提供一个临时结果,可以通过调用SwingWorker来达到这一目的。这个方法接受参数的数量是可变的,每一个参数的类型都必须是SwingWorker的第二个参数的类型,为了回去到publish方法提供的结果,需要重写SwingWorker.process方法。这个方法会被EDT调用。多次调用publish的结果经常会被累积到一次process调用中去。
看下面这个例子,这个程序通过再后台生成一系列的随机布尔值来测试java.util.Random的平衡性,这相当于抛掷硬币;因此被称为Flipper。为了报告其结果,后台任务使用一个类型为FlipPair的对象
private static class FlipPair {
private final long heads, total;
FlipPair(long heads, long total) {
this.heads = heads;
this.total = total;
}
}
head字段是随机值是true的次数;total字段是随机值的总数。
后台任务由FlipTask的一个实例表示:
private class FlipTask extends SwingWorker<Void, FlipPair> {
由于该任务不返回最终结果,所以第一个类型参数是什么并不重要;void被用来作为占位符。该任务在每次 "掷硬币 "后都会调用publish方法:
@Override
protected Void doInBackground() {
long heads = 0;
long total = 0;
Random random = new Random();
while (!isCancelled()) {
total++;
if (random.nextBoolean()) {
heads++;
}
publish(new FlipPair(heads, total));
}
return null;
}
下一节将讨论isCancelled方法。由于publish被频繁调用,在事件调度线程中调用process方法之前,可能会积累大量的FlipPair值。process只对每次报告的最后一个值感兴趣,用它来更新GUI:
protected void process(List<FlipPair> pairs) {
FlipPair pair = pairs.get(pairs.size() - 1);
headsText.setText(String.format("%d", pair.heads));
totalText.setText(String.format("%d", pair.total));
devText.setText(String.format("%.10g",
((double) pair.heads)/((double) pair.total) - 0.5));
}
如果Random是公平的,随着Flipper的运行,devText中显示的值应该越来越接近于0。Flipper中使用的setText方法实际上是 "线程安全 "的,正如其规范中所定义的。这意味着我们可以省去publish和process,直接从工作线程中调用setText。我们选择忽略这一事实,以便提供一个关于SwingWorker临时结果的简单演示
Canceling Background Tasks
要取消一个正在运行的后台任务,请调用SwingWorker.cancel 该任务必须与它自己的取消相配合。它有两种方式可以做到这一点:
- 当它收到一个中断时,就会终止。这个程序在Interrupts中有所描述。
- 通过在短时间内调用SwingWorker.isCancelled。如果这个SwingWorker的cancel被调用,该方法返回true。
cncel方法需要一个布尔参数。如果该参数为真,cancel将向后台任务发送一个interrupt。无论参数是true还是false,调用cancel都会将对象的cancellation状态变为真。这个就是isCancelled返回的值。一旦改变,cancellation状态就不能再改变了。对应源码如下:
// Future methods START
/**
* {@inheritDoc}
*/
public final boolean cancel(boolean mayInterruptIfRunning) {
return future.cancel(mayInterruptIfRunning);
}
/**
* {@inheritDoc}
*/
public final boolean isCancelled() {
return future.isCancelled();
}
参照上一章节的例子,当isCancelled返回true时,doInBackground的主循环就会退出。这将发生在用户点击 "取消 "按钮,触发了调用参数为false的取消的代码。
如果在一个SwingWorker对象的后台任务被取消后对其调用get,会抛出java.util.concurrent.CancellationException。
Bound Properties and Status Methods
SwingWorker支持绑定属性,这对与其他线程的通信很有用。有两个预定义的属性 progress和state。与所有绑定的属性一样, progress和state可以被用来触发EDT上的事件处理任务。通过实现一个属性变化监听器,程序可以跟踪progress,state和其他绑定属性的变化。更多的信息可以参考How to Write a Property Change Listener](docs.oracle.com/javase/tuto…)
process
process是一个int值,范围从0到100。它有一个预定义的setter方法(protected SwingWorker.setProgress)和一个预定义的getter方法(public SwingWorker.getProgress)。 ProgressBarDemo例子使用process来更新一个来自后台任务的ProgressBar控件。关于这个例子的详细讨论,请参考 How to Use Progress Bars
state
state表示SwingWorker对象在其生命周期中的位置。该绑定变量包含一个SwingWorker.StateValue类型的枚举值。可能的值是:
- PENDING 从对象的构建到doInBackground被调用之前这段时间的状态。
- STARTED 从doInBackground被调用到done被调用前这段时间的状态。
- DONE 该对象执行了DONE之后的状态。 state的当前值由SwingWorker.getState返回。
status method
status method是Future接口的一部分,也报告了后台任务的状态。正如我们在上一章节中所看到的,如果任务被取消,isCancelled返回true。此外,如果任务已经完成,无论是正常的还是被取消的,isDone都会返回true。
/**
* {@inheritDoc}
*/
public final boolean isCancelled() {
return future.isCancelled();
}
/**
* {@inheritDoc}
*/
public final boolean isDone() {
return future.isDone();
}