在開發Java Swing應用程序的過程中,有兩個原則是必須要牢記的:
1.耗時的操作(例如從數據庫查詢大量數據,讀取URI資源等)一定不能運行在EDT(事件派發線程)上,否則會導致Swing用戶界面失去響應。2.只能在EDT線程上對Swing Components進行訪問。
基於上面兩點原因,在一個Java Swing程序中,要想使用戶界面響應靈敏,至少應該有兩個線程;一個線程用來執行耗時操作,EDT線程用來執行所有與Swing Components的交互,例如更新文本,重繪圖形等等。這就要求兩個線程之間要相互通訊,給程序的開發帶來了不少的難度,Swing Worker的出現從根本上解決了這個問題,使程序員快速開發反應靈敏的的Swing程序成爲可能。SwingWoker被設計應用在此種場景下,你有一個耗時操作需要運行在後臺,在該操作完成或部分完成時,你要利用操作返回的結果去更新用戶界面。
讓我們假想有這樣一個應用場景,我有一個保存聯繫人的文件,我需要從中讀取並解析出所有聯繫人的信息,並及時更新在一個JTable中;假設這個文件非常的大,解析出所有聯繫人的信息需要花費幾分鐘的時間,如果不能很好的協調這個任務和EDT線程,則很有可能會造成用戶在幾分鐘時間裏得不到結果,而Swing界面處於無響應狀態。在這種情況下,SwingWorker就是一個絕佳的選擇。我們首先看一下SwingWorker的定義:
public abstract class SwingWorker<T,V> extends Object implements RunnableFuture<T>
顯然,這時一個抽象的模板類,在應用的時候,我們需要繼承SwingWorker並實例化模板參數。那麼,這兩個模板類型究竟是什麼意思呢,T參數代表的是你的耗時任務執行完成時返回的結果類型,V代表的是你的耗時任務部分完成時返回的結果類型。在我們的場景中,假設任務完成時我們需要一個List<BeanContact>(BeanContact是一個保存聯繫人信息的JavaBean),每當從文件中解析出一個聯繫人信息時,我們會新建一個BeanContact並需要更新到JTable中。那麼我們的T就是List<BeanContact>,而V就是BeanContact,則應該定義如下的類:
public class LoadContactsTask extends SwingWorker<List<BeanContact>, BeanContact> {
@Override
protected List<BeanContact> doInBackground() throws Exception {
//To do the task and return the result
}
}
從上面可以看到,我們還必須覆蓋SwingWorker的doInBackground方法,該方法執行我們的耗時操作,並且返回模板實例化時的T類型結果。下面是具體的代碼實現:
@Override
protected List<BeanContact> doInBackground() throws Exception {
BufferedReader reader = new BufferedReader(new FileReader("c:/contacts.cff"));
String line = null;
while ((line = reader.readLine()) != null) {
String[] strContacts = line.split(",");
BeanContact contact = new BeanContact();
contact.setName(strContacts[0]);
contact.setSex(strContacts[1]);
contact.setPhone(strContacts[2]);
contact.setEmail(strContacts[3]);
lineCnt++;
publish(contact);/*********/
contacts.add(contact);
Thread.sleep(100);
}
return contacts;
}
@Override
protected void process(List<BeanContact> chunks) {
if (progressHandle != null) {
progressHandle.processInProgress(chunks, lineCnt * 100 / 10000);
}
}
我們的實現中,process中會調用IProgressHandle(自定義的一個接口,用來更新用戶界面,詳見後面代碼)的processInProgress方法來更新用戶界面,大家會注意到process方法的參數是一個List<BeanContact>,爲什麼不是一個BeanContact呢,答案就是我們在上面講過的,有可能publish若干次後才調用process方法。
與此類似,在doInBackground完成後,SwingWorker會自動調用done方法,下面是我們的實現:
@Override
protected void done() {
if (progressHandle != null) {
progressHandle.processComplete(contacts);
}
}
客戶端如何來使用用SwingWork呢,很簡單,只需要新建一個實例並且調用它的execute方法即可,他會自動調用doInBackground方法來完成操作;以下是完整的代碼實現:
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package swingworkertest;
/**
*
* @author Administrator
*/
public class BeanContact {
private String name=null;
private String sex=null;
private String phone=null;
private String email=null;
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the sex
*/
public String getSex() {
return sex;
}
/**
* @param sex the sex to set
*/
public void setSex(String sex) {
this.sex = sex;
}
/**
* @return the phone
*/
public String getPhone() {
return phone;
}
/**
* @param phone the phone to set
*/
public void setPhone(String phone) {
this.phone = phone;
}
/**
* @return the email
*/
public String getEmail() {
return email;
}
/**
* @param email the email to set
*/
public void setEmail(String email) {
this.email = email;
}
}
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
/*
* Contacts.java
*
* Created on 2011-6-25, 10:40:13
*/
package swingworkertest;
import javax.swing.JFileChooser;
/**
*
* @author Administrator
*/
public class Contacts extends javax.swing.JFrame {
/** Creates new form Contacts */
public Contacts() {
initComponents();
handle = new DefaultProgressHandle();
handle.setTable(jTable1);
handle.setProgressBar(jProgressBar1);
}
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {
jButton1 = new javax.swing.JButton();
jTextField1 = new javax.swing.JTextField();
jScrollPane1 = new javax.swing.JScrollPane();
jTable1 = new javax.swing.JTable();
jButton2 = new javax.swing.JButton();
jProgressBar1 = new javax.swing.JProgressBar();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
jButton1.setText("瀏覽");
jButton1.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButton1ActionPerformed(evt);
}
});
jTextField1.setEditable(false);
jTable1.setModel(new javax.swing.table.DefaultTableModel(
new Object [][] {
},
new String [] {
"姓名", "性別", "電話", "電子郵件"
}
) {
Class[] types = new Class [] {
java.lang.String.class, java.lang.String.class, java.lang.String.class, java.lang.String.class
};
public Class getColumnClass(int columnIndex) {
return types [columnIndex];
}
});
jScrollPane1.setViewportView(jTable1);
jButton2.setText("加載聯繫人");
jButton2.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButton2ActionPerformed(evt);
}
});
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
.addComponent(jScrollPane1, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 557, Short.MAX_VALUE)
.addComponent(jProgressBar1, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 557, Short.MAX_VALUE)
.addComponent(jButton2, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 557, Short.MAX_VALUE)
.addGroup(layout.createSequentialGroup()
.addComponent(jButton1, javax.swing.GroupLayout.DEFAULT_SIZE, 97, Short.MAX_VALUE)
.addGap(18, 18, 18)
.addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, 442, javax.swing.GroupLayout.PREFERRED_SIZE)))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(jButton1)
.addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addComponent(jButton2)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 120, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jProgressBar1, javax.swing.GroupLayout.PREFERRED_SIZE, 22, javax.swing.GroupLayout.PREFERRED_SIZE)
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {jButton1, jButton2, jTextField1});
pack();
}// </editor-fold>
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
ContactsFileFilter filter = new ContactsFileFilter();
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(filter);
int returnVal = chooser.showOpenDialog(null);
if(returnVal == JFileChooser.APPROVE_OPTION){
jTextField1.setText(chooser.getSelectedFile().getAbsolutePath());
}
}
private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
LoadContactsTask task = new LoadContactsTask(jTextField1.getText());
task.setProgressHandle(handle);
task.execute();
}
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
Contacts contacts = new Contacts();
contacts.setTitle("Contacts");;
contacts.setVisible(true);
}
});
}
DefaultProgressHandle handle = null;
// Variables declaration - do not modify
private javax.swing.JButton jButton1;
private javax.swing.JButton jButton2;
private javax.swing.JProgressBar jProgressBar1;
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JTable jTable1;
private javax.swing.JTextField jTextField1;
// End of variables declaration
}
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package swingworkertest;
import java.io.File;
import javax.swing.filechooser.FileFilter;
/**
*
* @author Administrator
*/
public class ContactsFileFilter extends FileFilter{
public boolean accept(File pathname) {
if(pathname.isDirectory()){
return true;
}else{
return pathname.getName().endsWith(".cff");
}
}
@Override
public String getDescription() {
return "Text Files";
}
}
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package swingworkertest;
import java.util.List;
import javax.swing.JProgressBar;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;
/**
*
* @author Administrator
*/
public class DefaultProgressHandle implements IProgressHandle {
private JTable table = null;
private JProgressBar progressBar = null;
public void processInProgress(List<BeanContact> contacts, int progress) {
DefaultTableModel model = (DefaultTableModel) table.getModel();
for (BeanContact contact : contacts) {
String[] strArray = {contact.getName(), contact.getSex(), contact.getPhone(), contact.getEmail()};
model.addRow(strArray);
}
progressBar.setValue(progress);
}
public void processComplete(List<BeanContact> contacts) {
progressBar.setValue(progressBar.getMaximum());
}
/**
* @param table the table to set
*/
public void setTable(JTable table) {
this.table = table;
}
/**
* @param progressBar the progressBar to set
*/
public void setProgressBar(JProgressBar progressBar) {
this.progressBar = progressBar;
}
}
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package swingworkertest;
import java.util.List;
/**
*
* @author Administrator
*/
public interface IProgressHandle {
public abstract void processInProgress(List<BeanContact> contacts,int progress);
public abstract void processComplete(List<BeanContact> contacts);
}
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package swingworkertest;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import javax.swing.SwingWorker;
/**
*
* @author Administrator
*/
public class LoadContactsTask extends SwingWorker<List<BeanContact>, BeanContact> {
private String fileName = null;
private IProgressHandle progressHandle = null;
private List<BeanContact> contacts = null;
private int lineCnt = 0;
public LoadContactsTask(String fileName) {
this.fileName = fileName;
contacts = new ArrayList<BeanContact>();
}
@Override
protected List<BeanContact> doInBackground() throws Exception {
BufferedReader reader = new BufferedReader(new FileReader("c:/contacts.cff"));
String line = null;
while ((line = reader.readLine()) != null) {
String[] strContacts = line.split(",");
BeanContact contact = new BeanContact();
contact.setName(strContacts[0]);
contact.setSex(strContacts[1]);
contact.setPhone(strContacts[2]);
contact.setEmail(strContacts[3]);
lineCnt++;
publish(contact);
contacts.add(contact);
Thread.sleep(100);
}
return contacts;
}
/**
* @param progressHandle the progressHandle to set
*/
public void setProgressHandle(IProgressHandle progressHandle) {
this.progressHandle = progressHandle;
}
@Override
protected void process(List<BeanContact> chunks) {
if (progressHandle != null) {
progressHandle.processInProgress(chunks, lineCnt * 100 / 10000);
}
}
@Override
protected void done() {
if (progressHandle != null) {
progressHandle.processComplete(contacts);
}
}
}