Swing组件库提供了许多实用工具,SwingUtilities类就是其中一个,是Swing 实用方法的集合。它从接口javax.swing.SwingConstants 继承常量字段。SwingUtilities类的继承层次图:
在Swing图形界面程序里,常有一个顶级框架容器,如JFrame或JDialog实例,负责启动一个EventDispatchThread(事件分派线程,简称EDT),这是个单线程,这个线程负责处理UI(图形用户界面)事件的维护。
首先,图形界面的Swing组件向EDT的EventQueue(事件队列)提交一个event(事件),由EDT负责调度各个event事件的派发和执行。
例如,点击按钮时,JButton向EventQueue提交一个ActionEvent。EDT线程根据调度算法轮到执行该ActionEvent时,会把ActionEvent派发给JButton注册的监听器,监听器再调用事件处理器(actionPerformed)进行处理,这个过程并没有创建新线程,事件处理器是在EDT线程内完成的。
所以,如果我们在任何ActionListener、MouseListener等监听器对象中编写耗时的事务处理逻辑,整个图形界面应用系统就会响应迟钝,甚至不响应。如果在处理器中执行系统调用wait(),以等待另一个线程锁定的资源或计算结果,事件分派线程就会被阻塞,应用系统将处于无响应状态。
要弄懂SwingUtilities类的作用首先要理解事件派发线程的概念。
当运行一个 Swing 图形界面程序时,Java虚拟机(JVM)会自动创建三个线程:
(1)主线程:main线程。main方法作为程序的入口,该线程主要作用是初始化程序环境,并启动程序的GUI(图形用户界面)。
(2)Toolkit 线程:负责捕捉系统事件,比如键盘、鼠标移动、窗口移动和放缩等,程序员编写的任何代码不会在这个线程上执行。Toolkit线程的作用是把捕获的事件Event传递给EDT线程(事件派发线程)。
(3)EDT线程(Event Dispatcher Thread):事件派发线程。EDT线程接收Toolkit线程传递来的Event事件,并将事件插入事件队列(EventQueue)中。它根据调度算法来派发事件,把队列中的event(事件)派发给事件监听器,事件监听器调用事件处理器回调函数来处理。所有的事件处理代码并非在主线程main中处理,而是在EDT线程中执行的。一个应用程序有且只有一个EDT线程。
由于EDT线程是单线程操作,所以只有等前面事件处理器的回调函数执行完毕后,才能执行GUI组件更新操作,然后周而复始继续派发后面的事件。
如果事件处理器的回调函数中有耗时操作,UI界面就会因此停住,出现应用程序无响应的现象。
解决办法是:在事件处理器中将耗时的操作放到新线程(一般称之为工作线程)中执行,而不是让其在EDT中执行。
Swing应用程序中的线程大致可分为三种类型:
初始化线程(Initial Thread):比如main线程,该线程主要作用是初始化程序环境,并启动程序的GUI(图形用户界面)。
事件调度派发线程(EDT):Swing程序中只有一个EDT线程,它不但负责事件的派发、调用事件处理器来响应用户的请求;而且负责GUI组件的绘制和更新。程序中所有的事件处理都是由EDT分派和回调执行的,应用程序与图形界面组件及其基本数据模型的交互只允许在EDT线程上进行。
任务线程(Worker Thread):EDT线程上不能运行耗时的任务,以便UI及时响应用户请求和操作。耗时的工作任务要放到任务线程中去处理。
Swing 编程规则:
1 用户界面(UI)绘制和更新必须通过EDT线程处理。因为Swing的API不是线程安全的,Swing线程安全靠事件队列(EventQueue)和EDT来保证,由EDT线程统一处理可避免并发问题。其他线程直接更新UI组件会出现错误。
2 禁止在EDT线程中执行耗时任务。
3 耗时的任务放到独立的任务线程中执行。
EDT线程除了处理各种事件(Event)外,还可以处理线程对象(Runnnable)。当任务线程需要更新UI时,可调用静态方法invokeLater()或invokeAndWait()把更新UI请求封装成Runnnable对象,放入EDT线程的事件队列(EventQueue)末尾,当EDT线程派发处理完事件队列中的所有事件之后,才会来执行任务线程(Runnable)的UI更新工作。
通常有两种调用方式,都是正确的,选择任何一种都可以:
- 通过SwingUtilities类调用,以invokeLater为例:SwingUtilities.invokeLater
- 通过EventQueue类调用,以invokeLater为例:EventQueue.invokeLater
实际上,SwingUtilities版本只是做了简单的封装,它在方法内直接调用EventQueue.invokeLater。因为Swing框架本身经常调用SwingUtilities,使用SwingUtilities可以减少程序引入的类。
SwingUtilities 常用方法的说明:
- invokeAndWait(runnable)
用途:与 invokeLater方法 类似,它可将 Runnable 对象添加到EDT中。
但这是同步执行的方法,线程调用该方法时,当前的调用线程将立即阻塞,直等到EDT处理完它的请求,调用线程才会继续执行后续代码。常用于需要取得Swing组件状态数据的情形。
准则:不能在EDT线程中调用invokeAndWait,会引起死锁,导致程序崩溃。
可利用SwingUtilities.isEventDispatchThread()方法判断当前线程是否是EDT线程。
样例代码:
if(SwingUtilities.isEventDispatchThread()) //是EDT线程
{
myTree.RefreshNode();
}
else //不是EDT线程
{
SwingUtilities.invokeAndWait(new Runnable()
{
@Override
public void run()
{
myTree.RefreshNode();
}
});
}
- invokeLater(runnable)
用途:将 Runnable 对象添加到EDT的事件队列中,待EDT处理完其他事件后再执行。与invokeAndWait方法不同,它是个异步方法。调用线程会立即返回。
场景:当一个线程需要更新GUI时,可以调用 invokeLater 把更新操作委托给EDT执行
样例代码:
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run()
{
myTree.RefreshNode();
}
});
-
isEventDispatchThread()
用途:检查当前线程是否为EDT线程。
场景:当不能确定是否为EDT线程时,可以使用这个方法来判断。 -
updateComponentTreeUI(Component c)
用途:更新组件及其所有子组件的UI。
场景:切换组件外观感觉(Look and Feel)后,需要调用此方法来通知组件树重绘界面
SwingUtilities.updateComponentTreeUI(frame); -
getAncestorOfClass(Class<?> ancestor, Component comp)
用途:返回指定组件的最近的祖先。
场景:在嵌套的组件结构中,快速寻找特定类型的父组件
JFrame frame = (JFrame) SwingUtilities.getAncestorOfClass(JFrame.class, someComponent); -
convertMouseEvent(Component source, MouseEvent sourceEvent, Component destination)
用途:将一个鼠标事件从一个组件的坐标系统转换到另一个组件的坐标系统。
场景:在复杂的组件层次中,需要将事件从一个组件转换到另一个组件。 -
convertPoint(Component source, int x, int y, Component destination)
用途:将点的坐标从一个组件的坐标系统转换到另一个组件的坐标系统。
场景:计算组件之间的相对位置。
这些都是 SwingUtilities 中常用的方法,它们在开发Swing应用程序时极为有用。遵循Swing的线程规则可以避免许多常见的多线程问题,如数据不一致、程序不响应、非预期不确定的行为等。使用 SwingUtilities 类是改善这些问题的关键工具之一。
典型启动框架窗口问题
在Java典型的Swing应用程序中main()方法作为程序的入口,该线程是初始化线程,主要作用是初始化程序环境,并启动程序的GUI(图形用户界面)。main线程GUI(图形用户界面)启动后,这也是应用程序将控制权转交EDT线程的地方,往往也是同EDT交互出现问题的地方。
许多Swing应用程序使用下面方式启动程序,但这样写有BUG的,是错误的写法:
public class MainFrame extends javax.swing.JFrame {
…
public static void main(String[] args)
{
new MainFrame().setVisible(true);
}
}
尽管在我们写简短的学习例程时,由于图形界面处理代码很少,问题虽然不会马上暴露出来,几乎发现不了异常。但是我们还是要注意避免这样书写,或者说至少要知道所以然。
正确启动GUI图形用户界面的正确写法如下:
public class MainFrame extends javax.swing.JFrame
{
…
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
new MainFrame().setVisible(true);
}
});
}
}