可以解決組件顯示不出來的問題!!!
內容如下:
現在我們要做一個簡單的界面。
包括一個進度條、一個輸入框、開始和停止按鈕。
需要實現的功能是:
當點擊開始按鈕,則更新進度條,並且在輸入框內把完成的百分比輸出(這裡只做例子,沒有真正去做某個工作)。
代碼1:
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JProgressBar;
import javax.swing.JTextField;
public class SwingThreadTest1 extends JFrame {
private static final long serialVersionUID = 1L;
private static final String STR = "Completed : ";
private JProgressBar progressBar = new JProgressBar();
private JTextField text = new JTextField(10);
private JButton start = new JButton("Start");
private JButton end = new JButton("End");
private boolean flag = false;
private int count = 0;
public SwingThreadTest1() {
this.setLayout(new FlowLayout());
add(progressBar);
text.setEditable(false);
add(text);
add(start);
add(end);
start.addActionListener(new Start());
end.addActionListener(new End());
}
private void go() {
while (count < 100) {
try {
Thread.sleep(100);//這裡比作要完成的某個耗時的工作
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新進度條和輸入框
if (flag) {
count++;
progressBar.setValue(count);
text.setText(STR + String.valueOf(count) + "%");
}
}
}
private class Start implements ActionListener {
public void actionPerformed(ActionEvent e) {
flag = true;//設置開始更新的標誌
go();//開始工作
}
}
private class End implements ActionListener {
public void actionPerformed(ActionEvent e) {
flag = false;//停止
}
}
public static void main(String[] args) {
SwingThreadTest1 fg = new SwingThreadTest1();
fg.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
fg.setSize(300, 100);
fg.setVisible(true);
}
}
運行代碼發現,
現象1:當點擊了開始按鈕,畫面就卡住了。按鈕不能點擊,進度條沒有被更新,輸入框上也沒有任何信息。
原因分析:Swing是線程不安全的,是單線程的設計,所以只能從事件派發線程訪問將要在屏幕上繪製的Swing組件。ActionListener的actionPerformed方法是在事件派發線程中調用執行的,而點擊了開始按鈕後,執行了go()方法,在go()里,雖然也去執行了更新組件的方法
progressBar.setValue(count);
text.setText(STR + String.valueOf(count) + "%");
但由於go()方法直到循環結束,它並沒有返回,所以更新組件的操作一直沒有被執行,這就造成了畫面卡住的現象。
現象2:過了一段時間(go方法里的循環結束了)後,畫面又可以操作,並且進度條被更新,輸入框也出現了我們想看到的信息。
原因分析:通過在現象1的分析,很容易聯想到,當go()方法返回了,則其他的線程(更新組件)可以被派發了,所以畫面上的組件被更新了。
為了讓畫面不會卡住,我們來修改代碼,將耗時的工作放在一個線程里去做。
代碼2:
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JProgressBar;
import javax.swing.JTextField;
public class SwingThreadTest2 extends JFrame {
private static final long serialVersionUID = 1L;
private static final String STR = "Completed : ";
private JProgressBar progressBar = new JProgressBar();
private JTextField text = new JTextField(10);
private JButton start = new JButton("Start");
private JButton end = new JButton("End");
private boolean flag = false;
private int count = 0;
GoThread t = null;
public SwingThreadTest2() {
this.setLayout(new FlowLayout());
add(progressBar);
text.setEditable(false);
add(text);
add(start);
add(end);
start.addActionListener(new Start());
end.addActionListener(new End());
}
private void go() {
while (count < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (flag) {
count++;
System.out.println(count);
progressBar.setValue(count);
text.setText(STR + String.valueOf(count) + "%");
}
}
}
private class Start implements ActionListener {
public void actionPerformed(ActionEvent e) {
flag = true;
if(t == null){
t = new GoThread();
t.start();
}
}
}
//執行複雜工作,然後更新組件的線程
class GoThread extends Thread{
public void run() {
//do something
go();
}
}
private class End implements ActionListener {
public void actionPerformed(ActionEvent e) {
flag = false;
}
}
public static void main(String[] args) {
SwingThreadTest2 fg = new SwingThreadTest2();
fg.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
fg.setSize(300, 100);
fg.setVisible(true);
}
}
我們執行了程序,結果和我們想要的一樣,畫面不會卡住了。
那這個程序是否沒有問題了呢?
我們自定義了一個線程GoThread,在這裡我們完成了那些耗時的工作,可以看作是「工作線程」,
而對於組件的更新,我們也放在了「工作線程」里完成了。
在這裡,在事件派發線程以外的線程里設置進度條,是一個危險的操作,運行是不正常的。(對於輸入框組件的更新是安全的。)
只有從事件派發線程才能更新組件,根據這個原則,我們來修改我們現有代碼。
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JProgressBar;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
public class SwingThreadTest3 extends JFrame {
private static final long serialVersionUID = 1L;
private static final String STR = "Completed : ";
private JProgressBar progressBar = new JProgressBar();
private JTextField text = new JTextField(10);
private JButton start = new JButton("Start");
private JButton end = new JButton("End");
private boolean flag = false;
private int count = 0;
private GoThread t = null;
private Runnable run = null;//更新組件的線程
public SwingThreadTest3() {
this.setLayout(new FlowLayout());
add(progressBar);
text.setEditable(false);
add(text);
add(start);
add(end);
start.addActionListener(new Start());
end.addActionListener(new End());
run = new Runnable(){//實例化更新組件的線程
public void run() {
progressBar.setValue(count);
text.setText(STR + String.valueOf(count) + "%");
}
};
}
private void go() {
while (count < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (flag) {
count++;
SwingUtilities.invokeLater(run);//將對象排到事件派發線程的隊列中
}
}
}
private class Start implements ActionListener {
public void actionPerformed(ActionEvent e) {
flag = true;
if(t == null){
t = new GoThread();
t.start();
}
}
}
class GoThread extends Thread{
public void run() {
//do something
go();
}
}
private class End implements ActionListener {
public void actionPerformed(ActionEvent e) {
flag = false;
}
}
public static void main(String[] args) {
SwingThreadTest3 fg = new SwingThreadTest3();
fg.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
fg.setSize(300, 100);
fg.setVisible(true);
}
}
解釋:SwingUtilities.invokeLater()方法使事件派發線程上的可運行對象排隊。當可運行對象排在事件派發隊列的隊首時,就調用其run方法。其效果是允許事件派發線程調用另一個線程中的任意一個代碼塊。
還有一個方法SwingUtilities.invokeAndWait()方法,它也可以使事件派發線程上的可運行對象排隊。
他們的不同之處在於:SwingUtilities.invokeLater()在把可運行的對象放入隊列後就返回,而SwingUtilities.invokeAndWait()一直等待知道已啟動了可運行的run方法才返回。如果一個操作在另外一個操作執行之前必須從一個組件獲得信息,則應使用SwingUtilities.invokeAndWait()方法。
----2009年02月16日