Swing编程中JTable应该是个经常被用到的组件,进度条也不赖,有了它,不至于给用户“程序是不是死掉了”的疑惑,当然如果能做到像迅雷等下载工具那样,把表格和进度条组合在一起,那就太酷了!
好,下面就来看下这种界面在swing中是如何使用Jtable和JprogressBae实现的。
首先来看JTable,需要定制一个模型,既一个实现了TableModel接口的数据模型类
/** * 状态区表格模型类 * @author SavageGarden * */ class StatusTableModel extends DefaultTableModel { public Object[] rowData = {"..", 0, "", "", ""}; public StatusTableModel() { super(); addColumn("Name"); addColumn("Status"); addColumn("Size"); addColumn("Speed"); } /** * 设置为不可编辑 */ public boolean isCellEditable(int row, int column) { return false; } /** * 向状态区添加一个进度条 * */ public void addProgressBar() { rowData[0] = "文件" + (this.getRowCount() + 1); addRow(rowData); } }
如果想把某列显示成进度条的话,还需要定制某列单元格的渲染器,既一个实现了TableCellRenderer接口的类
/** * 工具条的渲染器 * @author SavageGarden * */ class ProgressBarRenderer extends DefaultTableCellRenderer{ private static final long serialVersionUID = 1L; private final JProgressBar b; public ProgressBarRenderer(){ super(); setOpaque(true); b = new JProgressBar(); //是否显示进度字符串 b.setStringPainted(true); b.setMinimum(0); b.setMaximum(100); //是否绘制边框 b.setBorderPainted(true); b.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Integer i = (Integer) value; b.setValue(i); return b; } }
然后来做一个按钮的监听类,实现ActionListener接口,当点击按钮时就调用表格模型的addProgressBar()方法,
/** * 按钮的事件响应类 * @author SavageGarden * */ class AddButtonActionListener implements ActionListener { public void actionPerformed(ActionEvent e) { statusTableModel.addProgressBar(); } }
然后来把这些代码组织到一起
/** * 主面板,显示表格和按钮 * @author SavageGarden * */ class FrameTest extends JFrame { private static int WIDTH; private static int HEIGHT; public JButton addButton; public JScrollPane statusScrollPane1; public static JTable statusTable; public static StatusTableModel statusTableModel; public FrameTest() { Toolkit kit = Toolkit.getDefaultToolkit(); Dimension screenSize = kit.getScreenSize(); WIDTH = screenSize.width/2; HEIGHT = screenSize.height/2; setSize(WIDTH, HEIGHT); setLocationRelativeTo(null); setResizable(false); setLayout(null); setTitle("FrameTest"); setVisible(false); createStatusPanel(); statusScrollPane1.setBounds(0, 0, WIDTH, 330); getContentPane().add(statusScrollPane1); addButton = new JButton("添加"); addButton.setBounds(WIDTH/2 -40, 350 , 80, 20); addButton.addActionListener(new AddButtonActionListener()); getContentPane().add(addButton); } /** * 创建状态区 * */ private void createStatusPanel() { statusTableModel = new StatusTableModel(); statusTable = new JTable(statusTableModel); //设置"Status"列由定制的ProgressBarRenderer渲染 TableColumn statusColumn = statusTable.getColumn("Status"); statusColumn.setCellRenderer(new ProgressBarRenderer()); statusScrollPane1 = new JScrollPane(statusTable); } }
最后写个主程序来调用
/** * 主程序 * @author SavageGarden * */ public class SwingTest { public static void main(String args[]) { FrameTest frame = new FrameTest(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }
好了,现在可以看到这样的界面了
进度条是添加上了,但是要怎么样才能让它动起来呢?
因为我们使用了定制的渲染器ProgressBarRenderer,它实现了TableCellRenderer接口的
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column)
方法
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Integer i = (Integer) value; b.setValue(i); return b; }
所以只需要调用statusTableModel的setValueAt(Object aValue, int row, int column)方法即可
为了模拟进度,将AddButtonActionListener的按钮事件处理修改为以下代码
public void actionPerformed(ActionEvent event) { statusTableModel.addProgressBar(); int price = 0; int index = statusTableModel.getRowCount()-1; while (price < 100) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } price ++; statusTableModel.setValueAt(price, index, 1); } }
点击“添加”按钮,程序好像死掉了, 100 * 10 ms后,表格中突然出现了新添加的一行数据,而且进度条已经到达了百分之百的状态。这是什么原因呢?
《JFC核心编程第2版》中665页“进度条和事件线程”一小节写的非常透彻,摘抄如下:
理想情况下,在使用进度条时,应用程序代码只须用适当的值来调用setValue方法,重新绘制的工作是由控件自身完成的,以反映工作的进度。但实际上,事情并非如此简单。有两种不同的情况需要考虑,这取决于操作时在AWT事件线程中执行,还是在另一个线程中执行。
在AWT事件线程中执行的操作
当应用程序在事件线程中执行长时间的操作时,会阻塞正常的AWT事件处理,因此阻止了重绘操作的发生。这通常会发生在下列情况下发生:应用程序响应一个来自于用户界面的请求时,在连接到一个按钮或其它GUI组件的事件处理程序中执行任务,任务的内容可能涉及计算质数的循环或在网络上进行一系列同步调用,后者会使事件线程挂起,直至远程系统发出答复为止。当应用程序调用JprogressBar的setValue方法时,进度条可能更新其内部状态并调用repaint,这样做会把一个事件放置到AWT事件队列中。不幸的是,直至应用程序的事件处理程序完成其处理并把控制权返回到线程的事件处理循环,才能处理该事件。
然后还有667页的一段
在另一个线程中执行的操作
要避免前一个例子中的问题,另一种方法就是不允许在事件线程中进行计算。如果在一个单独的线程中执行该操作,当调用进度条的setValue方法,它的更新就不会出现任何问题,因为即使工作线程正忙于计算或连接网络资源,事件线程的执行也不会受到影响。这种途径的问题在于,后台线程必须调用JprogressBar的setValue,而我们在第2章中提出过,Swing组件只有在事件线程中才能安全的访问。因此,从执行实际工作的线程调用setValue方法时不安全的!幸而有一个简单的途径可以绕过该问题---你只需要使用SwingUtilities的invokeLater方法,让AWT事件线程稍后进行setValue调用。
书中还给出了几个例子,理论加实践,非常容易理解,那么现在结合上面的理论,再把AddButtonActionListener的按钮事件处理修改一下
public void actionPerformed(ActionEvent event) { statusTableModel.addProgressBar(); Thread t = new ProgressBarThread(); t.start(); }
来看下ProgressBarThread的run方法中做了什么
/** * 进度条线程 * @author SavageGarden * */ class ProgressBarThread extends Thread { int price = 0; int index = statusTableModel.getRowCount()-1; public void run() { while (price < 100) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } price ++; SwingUtilities.invokeLater(new Runnable(){ public void run() { statusTableModel.setValueAt(price, index, 1); } }); } } }
可以看到每点击一次“添加”就会在表格中多一行,进度条之间并没有影响。
问题好像已经得到了完美的解决,但是常见的一种情况是:点击按钮或菜单,后台线程开始连接网络或者进行复杂的运算时,后台程序还要调用statusTableModel的setValueAt方法,如果将对前台界面组件的调用也写在后台程序中,对于习惯了“MVC”人们好像有点会感觉很不爽,那么就用观察者模式来解决这个问题吧,而且java.util. Observable、java.util. Observer已经做了观察对象和观察者的实现。
/** * 用于实现观察者观察的对象---进度条的显示值 * @author SavageGarden * */ class ProgressBarObservable extends Observable { private String price; public String getPrice() { return price; } public void setPrice(String price) { this.price = price; setChanged(); notifyObservers(price); } } /** * 用于实现观察者 ---进度条的显示值变化时更新statusTableModel * @author SavageGarden * */ class ProgressBarObserver implements Observer { public void update(Observable o, Object arg) { String showValue = (String)arg; statusTableModel.setValueAt(Integer.parseInt(showValue.split(":")[1]), Integer.parseInt(showValue.split(":")[0]), 1); } }
然后在FrameTest的构造函数中添加调用
progressBarObservable = new ProgressBarObservable(); progressBarObserver = new ProgressBarObserver(); progressBarObservable.addObserver(progressBarObserver);
然后将ProgressBarThread中的run方法修改为
SwingUtilities.invokeLater(new Runnable(){ public void run() { //statusTableModel.setValueAt(price, index, 1); progressBarObservable.setPrice(rowIndex + ":" + price); } });
运行,即可达到图三相同的效果。