利用 JTable 类,可以以表格的形式展示数据,可设置允许用户编辑数据。JTable 本身不拥有或者缓存数据;它只是数据的视图。这里有一个放在滚动面板上的典型表格:
本文展示如何完成一些常见的表格相关的任务:包括以下部分:
(1)创建一个简单的表格
(2)向容器添加表格
(3)改变每每一栏的宽度
(4)用户选择
(5)创建表格模型
(6)监听数据改变
(7)点燃数据改变事件
(8)概念:编辑器和渲染器
(9)使用自定义渲染器
(10)为单元格指定提示工具
(11)为列头指定提示工具
(12)排序和过滤
(13)使用组合框作为编辑器
(14)使用其他编辑器
(15)使用编辑器验证用户的文本输入
(16)打印
(17)例子列表
SimpleTableDemo.java
中的表格在一个字符串数组中声明各个列名
String[] columnNames = {"First Name", "Last Name", "Sport", "# of Years", "Vegetarian"};
数据初始化并存储在二维数组:
Object[][] data = { {"Kathy", "Smith", "Snowboarding", new Integer(5), new Boolean(false)}, {"John", "Doe", "Rowing", new Integer(3), new Boolean(true)}, {"Sue", "Black", "Knitting", new Integer(2), new Boolean(false)}, {"Jane", "White", "Speed reading", new Integer(20), new Boolean(true)}, {"Joe", "Brown", "Pool", new Integer(10), new Boolean(false)}};
接着表格使用这些数据和列名构造一个表格:
JTable table = new JTable(data, columnNames);
有两个接收数据的 JTable 构造器:
这些构造函数的好处是容易实现,而缺点是:
如果你想避免这些限制,你需要实现你自己的表格模型,见“(5)创建表格模型”。
这里有一段创建滚动面板作为表格容器的常规代码:
JScrollPane scrollPane = new JScrollPane(table);table.setFillsViewportHeight(true);
这两行代码实现了:表格对象的引用作为 JScrollPane 构造函数的参数,创建一个容纳table的容器,table添加到容器中;JTable.setFillsViewportHeight 方法设置了 fillsViewportHeight 属性。当这个属性为 true 时,表格会占据容器整个高度,即便表格没有足够的行去使用这些垂直空间。这使得表格更容易实现拖拉操作。
滚动面板自动把表格头放置在视窗的顶端。当表格数据垂直滚动时,列名保持在视窗顶端可视。
如果你想要使用一个没有滚动面板的表格,你必须获得表格头组件,然后自己放置它,例如:
container.setLayout(new BorderLayout());container.add(table.getTableHeader(), BorderLayout.PAGE_START);container.add(table, BorderLayout.CENTER);
默认情况下,表格所有列等宽,切这些列自动填满整个表格的宽度。当表格变宽或者变窄时(通常是用户调整包含表格的窗口大小),所有的列宽自动调整到适当宽度。
当用户通过拖动列头的右边界来调整某一列的宽度时,要么别的列的宽度会受到影响而改变,要么整个表格的宽度会改变。默认情况下 ,表格整体宽度保持不变,“拖动点“的右侧各列利用增加或减少的空间自我调整,拖动的那一列的左侧各列保持不变。
要定义各列初始化宽度,你可以对表格各列调用 setPreferredWidth 方法。可以设置各列首选宽度,和他们的相对宽度。例如,向demo增加下面代码,是的第三列比其他列更宽:
TableColumn column = null;for (int i = 0; i < 5; i++) { column = table.getColumnModel().getColumn(i); if (i == 2) { column.setPreferredWidth(100); //third column is bigger } else { column.setPreferredWidth(50); }}
如上面代码所示,每一列代表一个 TableColumn 对象, TableColumn 提供 getter 和 setter 方法设置和获取列的最小、首选、最大宽度 和 目前宽度。 基于估计单元格内容需要的空间调整单元格宽度,查看TableRenderDemo.java
. 中的 initColumnSizes 方法。
当用户明确的调整列宽度,列的”首选宽度“就被设置为用户指定的”新的当前宽度“。不过,当表格因视窗调整而改变大小是,列的”首选宽度“不会改变。”首选宽度“的存在是用于计算新的列的宽度,来填充可用空间。
你可以通过调用 setAutoResizeMode 改变一个表格的调整模式。
默认配置情况下,一个表格支持选择一行或多行。用户可以选择一组连续的或不连续的行。用户最后指示的那个单元格,在 Metal 样式中,会被outlined(轮廓虚线)。这个单元格被称为 ”lead selection“(导联选择(器, 钮));有时候也称为 ”聚焦单元格“ 或 ”当前单元格“。
用户使用鼠标键盘实现选择,选择的方式描述如下
操作 | 鼠标动作 | 键盘动作 |
---|---|---|
选择单行 | 点击 | 向上或向下 |
选中连续多行 | Shift—点击/拖拉 | Shitf-向上 或 Shift-向下 |
向选中的行集增加行/切换选择 |
Control-点击 | Control+向上或向下, 使用空格键增加当前行或切换选择. |
下面的例子程序TableSelectionDemo.java
展示了类似的表格,允许用户操纵某些 JTable 选项。还有一个文本面板记录”选择事件“。(这个demo里面的有关复选框事件处理代码写的好好)
在下面的截图中,这事默认的Metal样式,选中的行highlight,选择的单元格outline
在下面的”Selection Mode“下,有一些复选框,选择”Single Selection“。现在你只能在某一时刻选中一行,如果你选中”Single Interval Selection“,你可以选择连续的多行。
所有的”Selection Mode“下面的复选框按钮,调用 JTable.setSelectionMode
. 这个方法带一个参数,为javax.swing.ListSelectionModel
:MULTIPLE_INTERVAL_SELECTION
, SINGLE_INTERVAL_SELECTION
, andSINGLE_SELECTION
.中的一个(依次为,多行可间隔,多行无间隔,单行)
回到我们的 demo,注意,在”Selection Options“下三个复选框,每个复选框控制一个由 JTable 定义的绑定属性的boolean类型状态值:
setRowSelectionAllowed
和getRowSelectionAllowed
设置和读取。当这个绑定属性为 true (同时 columnSelectionAllowed属性为 false)时,用户可以选择行。setColumnSelectionAllowed
和getColumnSelectionAllowed 设置和读取。当这个绑定属性为 true 时,用户可以选择单个单元格,或者呈矩阵块地
选择多个单元格
setCellSelectionEnabled
andgetCellSelectionEnabled 设置和获取。当这个绑定属性为 true 是,用户可以选择单个单元格,或是以矩阵块的形式选择多个单元格。
提醒:JTable使用很简单的选择原则来管理 行 和 列 的交集,它并非设计成全面处理独立的单元格选择。(就是说,有些多单元格的选择是不被handle的,你也选不到)
如果你清空三个复选框,就没有selection了,只有lead selection表现而已。(我觉得lead selection只是形式上的选择,是一种导航观察的形式,而selection是确切选中表格中某些单元格的事实。我无法确切地解释出lead selection 和 selection的区别,我只能意会呀)
你可能注意到”Cell Selection“复选框在”multiple interval selection“选择模式中是不可用的。只是在这个demo的模式中是不被支持的。你可以在”multiple interval selection“模式中指定单元格选择,但是表格也不会产生有效的selection。
你或许还注意到,改变这”selection option“中某个选项可能影响其他选项。这是因为允许行选择和列选择,就意味着允许单元格原则。JTable自动更新三个绑定属性,以保持它们的一致性。
提醒:设置 cellSelectionEnabled 的值会附带同时设置 rowSelectionEnabled 和 columnSelectionEnabled 的效果。同样,设置后两者的值同样会影响 cellSelectionEnabled 的值。设置 row……和 cloumn……为不同值,同时设置 cell……为 false,可以测试一下。
要获得当前的selection,使用 JTable.getSelectedRows,返回一个带索引行数的数组,使用 JTable.getSelectedColumns 返回 列索引。 要获得 lead selection 的坐标,需要引用table本身的 selection model 和 table 的 column model。下面代码格式化一个包含一个lead selection的行和列的字符串:
String.format("Lead Selection: %d, %d. ", table.getSelectionModel().getLeadSelectionIndex(), table.getColumnModel().getSelectionModel().getLeadSelectionIndex());
使用selections产生一些时间。参考 How to Write a List Selection Listener in the Writing Event Listeners
每个 table 对象 使用一个 table model 对象来管理表格中真实的数据。一个 table model 对象一定要实现 TableModel 接口,如果程序没有提供一个 table model 对象,JTable自动创建一个 DefaultTableModel实例。这种关系可用下面的图来解释
SimpleTableDemo 中 JTable 的构造器如下面代码一样,创建它的 table model:
new AbstractTableModel() { public String getColumnName(int col) { return columnNames[col].toString(); } public int getRowCount() { return rowData.length; } public int getColumnCount() { return columnNames.length; } public Object getValueAt(int row, int col) { return rowData[row][col]; } public boolean isCellEditable(int row, int col) { return true; } public void setValueAt(Object value, int row, int col) { rowData[row][col] = value; fireTableCellUpdated(row, col); }}
上面代码,简单的实现了一个 table model。通常在 AbstractTableModel 的子类中实现 table model。
你的模型可以支持 数组、vector 或 hash map类型的数据。甚至是从外资资源,如数据库中获得数据。他甚至可以在运行期间产生数据。
这个TableDemo.java
例子中的表格与前面 SimpleTableDemo 中的表格有几点区别:
观察 TableDemo.java
的代码,粗体部分是区别于 SimpleTableDemo自动创建的 table model:
public TableDemo() { ... JTable table = new JTable(new MyTableModel()); ...}class MyTableModel extends AbstractTableModel { private String[] columnNames = ...//same as before... private Object[][] data = ...//same as before... public int getColumnCount() { return columnNames.length; } public int getRowCount() { return data.length; } public String getColumnName(int col) { return columnNames[col]; } public Object getValueAt(int row, int col) { return data[row][col]; } public Class getColumnClass(int c) { return getValueAt(0, c).getClass(); } /* * Don't need to implement this method unless your table's * editable. */ public boolean isCellEditable(int row, int col) { //Note that the data/cell address is constant, //no matter where the cell appears onscreen. if (col < 2) { return false; } else { return true; } } /* * Don't need to implement this method unless your table's * data can change. */ public void setValueAt(Object value, int row, int col) { data[row][col] = value; fireTableCellUpdated(row, col); } ...}
一个 table model 可以有多个监听器,无论何时,只要表格数据被改变,都会通知这些监听器。监听器是TableModelListener
类的实例。在下面的例子代码中, SimpleTableDemo 增加了一个监听器,粗体部分是新的代码:
import javax.swing.event.*;import javax.swing.table.TableModel;public class SimpleTableDemo ... implements TableModelListener { ... public SimpleTableDemo() { ... table.getModel().addTableModelListener(this); ... } public void tableChanged(TableModelEvent e) { int row = e.getFirstRow(); int column = e.getColumn(); TableModel model = (TableModel)e.getSource(); String columnName = model.getColumnName(column); Object data = model.getValueAt(row, column); ...// Do something with the data... } ...}
为了唤醒数据改变事件,table model一定要知道如果构造 TableModelEvent
对象。这是个复杂的过程,但是已经在 DefaultTableModel 中实现了。你可以让 JTable 使用他自己默认的 DefaultTableModel 实例,或者创建自定义的 DefaultTableModel 子类。
如果 DefaultTableModel 不适合作为自定义 table model 类的基类,考虑使用 AbstractTableModel
作为基类。这个类实现了构造 TableModelEvent 对象的简单框架。(DefaultTableModel 是该抽象类的子类)当外界改变了表格数据的时候,你的自定义类仅仅需要调用 AbstractTableModel 方法中的一个,如下:
Method | Change |
---|---|
fireTableCellUpdated |
Update of specified cell. 单元格更新 |
fireTableRowsUpdated |
Update of specified rows 行更新 |
fireTableDataChanged |
Update of entire table (data only). 表格范围内的数据更新 |
fireTableRowsInserted |
New rows inserted. 插入新行 |
fireTableRowsDeleted |
Existing rows Deleted 删除存在的行 |
fireTableStructureChanged |
Invalidate entire table, both data and structure. 使表格无效,包括数据和结构 |
在进行后面的学习前,你需要理解表格是如何绘制它的单元格的。你可能会认为表格中每个单元格都是一个组件,但是,考虑性能的原因,Swing的表格并不这么做。
取而代之的是,一个 single cell renderer(单一单元格绘制器)一般用来绘制所有包含同类型数据的单元格。你可以想象这个 renderer 是一个可配置的墨水打印,表格使用它将格式化的数据合适地印在每个单元格上。当用于开始编辑一个单元格的数据时, cell editor 接管这个单元格,控制单元格的编辑行为。
例如,TableDemo 的 # of Years 列中的每个单元格包含数字数据——具体是一个Integer对象。默认情况下,对于数字列,渲染器使用单个 JLabel 实例在列上的单元格绘制恰当的居右的数字。如果用户开始编辑一个单元格,则默认的单元格编辑器使用一个 居右的 JTextField 来控制单元格的编辑动作。
如何选择 render 处理某一列的单元格,表格首先会确定,对于该列,你是否已经指定了一个 renderer。如果你未指定,那么 table 会调用 table model 的 getColumnClass 方法,获得该列的单元格的数据的类型。接着,table 会将该列的数据类型与一个数据类型列表对比,该列表注册了多种 cell renderers。该表由 table 初始化,你可以向该表增加renderer。通常,table 会把下列类型放到列表中:
NumberFormat
的实例来执行。 DateFormat
的实例来执行。 单元格编辑器使用类似的法则。
注意,如果让 table 自己创建它的 model,它会把 Object 作为各列的类型。为了指定更明确列类型,table model一定要定义适当的 getColumnClass 方法,像TableDemo.java
. 中的定义那样。
记住,尽管 render 决定有多少单元格和列头被指定了它的 tool tip text(鼠标指在上面显示的提示文本),但是 render 本身不处理事件。如果你需要获得 table 内发生的事件,你使用的技术就是在下面分类的事件中做变化:
Situation | How to Get Events |
---|---|
To detect events from a cell that is being edited... | Use the cell editor (or register a listener on the cell editor). |
To detect row/column/cell selections and deselections... | Use a selection listener as described in Detecting User Selections . |
To detect mouse events on a column header... | Register the appropriate type of mouse listener on the table's JTableHeader object. (See TableSorter.java for an example.) |
To detect other events... | Register the appropriate listener on the JTable object. |
这节的内容将告诉你如何创建和指定一个 cell renderer。你可以使用 JTable 的 setDefaultRenderer 方法设置一个类型明确的 cell renderer。使用 TableColumn 的 setCellRenderer 方法,可以指定某列中的单元格使用的 renderer。你甚至可以通过创建 JTable 的子类来指定 cell-specific renderer(针对某个单元格的renderer)。
通过默认的renderer, DefaultTableCellRenderer,很容易自定义 text 和 image renderer。你只需要创建一个子类,实现 setValue 方法,这样它就会调用 setText(合适的字符串参数) 或 setIcon(合适的图像)。例如,这里给出默认的 date renderer 的实现:
static class DateRenderer extends DefaultTableCellRenderer { DateFormat formatter; public DateRenderer() { super(); } public void setValue(Object value) { if (formatter==null) { formatter = DateFormat.getDateInstance(); } setText((value == null) ? "" : formatter.format(value)); }}
如果只是继承 DefaultTableCellRenderer 是不够的,你可以使用另外一个超类来构建 renderer。最简单的方法就是创建一个存在的空间的子类,让该子类实现TableCellRenderer
接口。 TableCellRenderer 只要求一个方法: getTableCellRendererComponent。这个方法的实现了 建立 渲染组件 绘制具体的状态,然后返回这个组件。
在下面的 TableDialogEditDemo.java
的截图中, 用于处理列 Favorite Color一栏的单元格的 renderer,是 JLabel 的子类,名为ColorRenderer。
这里引用 ColorRenderer.java
中的代码:
public class ColorRenderer extends JLabel implements TableCellRenderer { ... public ColorRenderer(boolean isBordered) { this.isBordered = isBordered; setOpaque(true); //MUST do this for background to show up. } public Component getTableCellRendererComponent( JTable table, Object color, boolean isSelected, boolean hasFocus, int row, int column) { Color newColor = (Color)color; setBackground(newColor); if (isBordered) { if (isSelected) { ... //selectedBorder is a solid border in the color //table.getSelectionBackground(). setBorder(selectedBorder); } else { ... //unselectedBorder is a solid border in the color //table.getBackground(). setBorder(unselectedBorder); } } setToolTipText(...); //Discussed in the following section return this; }}
下面这句代码是TableDialogEditDemo.java
中注册 ColorRender实例为 所有 Color 类数据的 默认 renderer。
table.setDefaultRenderer(Color.class, new ColorRenderer(true));
要指定一个 cell-specific renderer,你需要定义一个 JTable 子类,覆盖 getCellRenderer 方法。例如,下面代码指定第一列第一个单元格使用一个自定义的 renderer:
TableCellRenderer weirdRenderer = new WeirdRenderer();table = new JTable(...) { public TableCellRenderer getCellRenderer(int row, int column) { if ((row == 0) && (column == 0)) { return weirdRenderer; } // else... return super.getCellRenderer(row, column); }};
默认情况下,tool tip text(提示文本) 是否展示取决于单元格的 renderer。不过,有时候可以通过覆盖 JTable 的 getToolTipText(MouseEvent) 方法来指定 tool tip text。这节将告诉你这两种技术:
使用单元格的 renderer 增加文本提示,首先你要获得或创建一个 cell renderer。然后,在确保 这个 rendering component 是一个 JComponent后,调用 setToolTipText。(之前的ColorRender 继承了 JLabel,所以它是个JComponent,同时它也实现了TableCellRenderer,所以它是一个 rendering component)
TableRenderDemo.java
.的源代码。它对 Sport 列 增加了文本提示:
//Set up tool tips for the sport cells.DefaultTableCellRenderer renderer = new DefaultTableCellRenderer();renderer.setToolTipText("Click for combo box");sportColumn.setCellRenderer(renderer);
虽然这个文本提示设置是静态的,但是你可以实现 依赖于单元格或者程序的 动态文本提示(前面的ColorRender中有关tool tip 的设置也是一种方法):
TableDialogEditDemo 对Color类型栏使用一个renderer,见 ColorRenderer.java
, 粗体部分为设置tool tip text 部分的代码:
public class ColorRenderer extends JLabel implements TableCellRenderer { ... public Component getTableCellRendererComponent( JTable table, Object color, boolean isSelected, boolean hasFocus, int row, int column) { Color newColor = (Color)color; ... setToolTipText("RGB value: " + newColor.getRed() + ", " + newColor.getGreen() + ", " + newColor.getBlue()); return this; }}
tool tip的效果如下:
你可以通过覆盖 JTable 的 getToolTipText(MouseEvent)方法指定 tool tip text。
这个demo设置了 Sport 和 Vegetarian栏中的单元格给出文本提示:
TableToolTipsDemo.java
中实现了Sport 和 VegeTarian 栏中单元格给出文本提示的代码如下:
JTable table = new JTable(new MyTableModel()) { //Implement table cell tool tips. public String getToolTipText(MouseEvent e) { String tip = null; java.awt.Point p = e.getPoint(); int rowIndex = rowAtPoint(p); int colIndex = columnAtPoint(p); int realColumnIndex = convertColumnIndexToModel(colIndex); if (realColumnIndex == 2) { //Sport column tip = "This person's favorite sport to " + "participate in is: " + getValueAt(rowIndex, colIndex); } else if (realColumnIndex == 4) { //Veggie column TableModel model = getModel(); String firstName = (String)model.getValueAt(rowIndex,0); String lastName = (String)model.getValueAt(rowIndex,1); Boolean veggie = (Boolean)model.getValueAt(rowIndex,4); if (Boolean.TRUE.equals(veggie)) { tip = firstName + " " + lastName + " is a vegetarian"; } else { tip = firstName + " " + lastName + " is not a vegetarian"; } } else { //another column //You can omit this part if you know you don't //have any renderers that supply their own tool //tips. tip = super.getToolTipText(e); } return tip; } ...}
除了 converColumnIndexToModel 的调用意外,这段代码很容易明白。这个方法是必需的,因为用户可能在界面上移动了某些列,视图上的某列索引并不匹配 table model 中该列的索引,而数据处理是在 table model 上操作的,所以要获得对应于 table model 中该列的索引。
你可以通过设置 table 的 JTableHeader 对象,增加列头的文本提示。不同的列头常常需要不同的文本提示,你可以覆盖 table header 的 getToolTipText 方法来改变提示文本。你也可以 调用 TableColumn.setHeaderRenderer, 对 header 指定自定义的 renderer.
TableToolTipsDemo.java
中也有根据不同列显示不同列头文本提示的例子,如下图,当你将鼠标移动到后三列的列头上时,将显示提示文本。而前两列的列头未提供文本提示(名字已经充分说明这列数据,无需别的提示说明),以下是功能截图:
下面代码实现了上面的文本提示功能。创建一个 JTableHeader 子类,覆盖 getToolTipText(MouseEvent)方法,这样就能对当前列返回文本。要与 table 关联这个修订过的 header,使用 JTable 的 createDefaultTableHeader 方法,返回一个 JTableHeader 子类实例。
protected String[] columnToolTips = { null, // "First Name" assumed obvious null, // "Last Name" assumed obvious "The person's favorite sport to participate in", "The number of years the person has played the sport", "If checked, the person eats no meat"};...JTable table = new JTable(new MyTableModel()) { ... //Implement table header tool tips. protected JTableHeader createDefaultTableHeader() { return new JTableHeader(columnModel) { public String getToolTipText(MouseEvent e) { String tip = null; java.awt.Point p = e.getPoint(); int index = columnModel.getColumnIndexAtX(p.x); int realIndex = columnModel.getColumn(index).getModelIndex(); return columnToolTips[realIndex]; } }; }};
提醒:(有关单元格或列头文本提示)上面代码,getToolTipText(MouseEvent e) 和 createDefaultTableHeader 方法都是 JTable 的方法。用了很多匿名类的写法,要注意看仔细。
表格 sorting 和 filtering 是由 一个 sorter 对象管理的。获得一个 sorter 对象的最简单方法是设置 autoCreateRowSorter 绑定属性 为true:
JTable table = new JTable();table.setAutoCreateRowSorter(true);
这段代码定义了一个 row sorter,他是 javax.swing.table.TableRowSorter 的实例。当用户点击某列列头时,表格会做一个 locale-specific sort。
,例子的截图:TableSortDemo.java
你可以构造一个 TableRowSorter 实例,然后指定它为你的 table 的sorter,这样你就能获得更多的分类控制。
TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());table.setRowSorter(sorter);
TableRowSorter 使用 java.util.Comparator
(实现了该接口的)对象来排序。实现该接口,必须提供一个名为 compare 的方法,该方法定义两个了两个对象的比较值,用于排序。例如,下面代码创建了一个 Comparator,根据字符串最后一个单词来排序。(String实现了Comparable接口)
Comparator<String> comparator = new Comparator<String>() { public int compare(String s1, String s2) { String[] strings1 = s1.split("\\s"); String[] strings2 = s2.split("\\s"); return strings1[strings1.length - 1] .compareTo(strings2[strings2.length - 1]); }};
这个例子太简单了,更具典型意义的是,实现了Comparator接口的类,同时也是 java.text.Collator
.的子类,你可以定义自己的子类,或者使用 Collator 的工厂方法,获得一个支持本地语言的 Comparator,又或是使用java.text.RuleBasedCollator
. ,该类是 Collator 的具体子类。
为了确定某一列使用哪个 Comparator, TableRowSorter 尝试轮流使用一些规则规则。这些规则按顺序的列在下面;第一条规则为 sorter 提供了一个 Comparator,……:
Comparable.compareTo
.返的值对字符串排序。 setStringConverter
为 table 指定一个字符串转换器,则对象转换所得的字符串值代表对象,参加基于本地语言的排序。 对于更复杂的排序,查阅TableRowSorter 和他的父类 javax.swing.DefaultRowSorter
.
调用 setSortKeys
,指定排序规则和优先排序。(有关”键“的概念,对于某一列,使用比较器排序时,无法得出某几行的顺序时,则按照键列表中的键顺序,根据这些键,其实就是列,在原来基础上再次此对这几行排序…………你懂的)这里有一个有一个根据前两列排序的例子。哪一列优先排序是取决于”排序键列表“中的“排序键”顺序。在这个例子中,第二列是第一排序键,所以根据第二列优先排序,然后再根据第一列排序:
List <RowSorter.SortKey> sortKeys = new ArrayList<RowSorter.SortKey>();sortKeys.add(new RowSorter.SortKey(1, SortOrder.ASCENDING));sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));sorter.setSortKeys(sortKeys);
除了对结果集二次排序外,一个 table sorter 可以指定过滤器,让哪些行不显示。TableRowSorter 使用 javax.swing.RowFilter
对象实现过滤功能。 RowFilter 实现了几个工厂方法,可以创建集中常用的 filter。例如regexFilter
方法返回一个基于 regular expression .(正则表达式)的 RowFilter。
在下面的例子代码中,明确的创建了一个 sorter 对象,接着可以给它指定一个 filter:
MyTableModel model = new MyTableModel();sorter = new TableRowSorter<MyTableModel>(model);table = new JTable(model);table.setRowSorter(sorter);
接着你基于当前文本值进行过滤:
private void newFilter() { RowFilter<MyTableModel, Object> rf = null; //If current expression doesn't parse, don't update. try { rf = RowFilter.regexFilter(filterText.getText(), 0); } catch (java.util.regex.PatternSyntaxException e) { return; } sorter.setRowFilter(rf);}
filterText 文本框的值每次改变时,newFilter() 都会被调用。try catch 防止了用户在界面的文本框中输进错误的正则表达式。
当一个 table 使用一个 sorter 时,用户看到的数据顺序可能跟 data model指定的顺序不一样,也许没有包含 data model 指定的所有行。 用户真正看到的数据被称为 “view”,拥有自己的一套坐标。 JTable 提供了方法用于转换 model 坐标 到 view 坐标——convertColumnIndexToView
and convertRowIndexToView
方法。当然也提供了model 到 view 的转换——convertColumnIndexToModel
and convertRowIndexToModel
.
提醒:每次使用 sorter 时,记得转换单元格的索引,数据真正是要在 model 上处理的。
下面代码整合这节所讨论的技术。
对 TableDemo 做了一些修改,包括前面提到的一些代码。这个例子给 table 提供了一个 sorter ,使用一个文本框提供过滤的正则表达式。下面是截图是:未排序 和 未过滤,注意,model中的第三行依然为view 的第三行。TableFilterDemo.java
如果用户点击第二列两次,第四行就会变成第一行——这只是view的改变,model中的列顺序没改变。
如前面所描述的一样,用户向“Filter Text”文本域输入正则表达式,符合这些表达式的行将被显示。跟排序一样,过滤也是产生 view,与 mode 分离。
下面的代码,根据当前 selection 更新 status 文本框:
table.getSelectionModel().addListSelectionListener( new ListSelectionListener() { public void valueChanged(ListSelectionEvent event) { int viewRow = table.getSelectedRow(); if (viewRow < 0) { //Selection got filtered away. statusText.setText(""); } else { int modelRow = table.convertRowIndexToModel(viewRow); statusText.setText( String.format("Selected Row in view: %d. " + "Selected Row in model: %d.", viewRow, modelRow)); } } });
让 combo box 作为一个 editor 是简单的,下面粗体部分的代码,指定某列的editor 为一个 combo box
TableColumn sportColumn = table.getColumnModel().getColumn(2);...JComboBox comboBox = new JComboBox();comboBox.addItem("Snowboarding");comboBox.addItem("Rowing");comboBox.addItem("Chasing toddlers");comboBox.addItem("Speed reading");comboBox.addItem("Teaching high school");comboBox.addItem("None");sportColumn.setCellEditor(new DefaultCellEditor(comboBox));
效果图:
无论你是设置一个列的 editor (使用 TableColumn.setCellEditor 方法),还是为一个数据类型设置 eidtor (使用 JTable.setDefaultEditor 方法),你可以指定你个实现了TableCellEditor 的类作为 editor。幸运的是 DefaultCellEditor 类实现了这个借口,并且提供了参数为编辑组件(JTextField,JCheckBox 或 JComboBox)的构造函数。通常你不需要明确的指定一个check box 为 editor,因为 Boolean 类型的数据自动使用 单选框 render 和 editor。
你的单元格 ediotr 类需要定义至少两个方法—— getCellEditorValue 和 getTableCellEditorComponent。getCellEditorValue 返回单元格当前值, 该方法是 CellEditor 接口要求的; getTableCellRendererComponent 返回你想要用作编辑器的组件,该方法是 TableCellEditor 接口要求的。
下面截图包含一个表格和一个对话框,表格使用对话框间接地作为单元格编辑器。当用户开始编辑 Favorite Color 列是,呈现一个按钮(真正的cell editor),带出对话框,让用户选择不同的颜色。
下面是 ColorEditor.java
, 中的代码:
public class ColorEditor extends AbstractCellEditor implements TableCellEditor, ActionListener { Color currentColor; JButton button; JColorChooser colorChooser; JDialog dialog; protected static final String EDIT = "edit"; public ColorEditor() { button = new JButton(); button.setActionCommand(EDIT); button.addActionListener(this); button.setBorderPainted(false); //Set up the dialog that the button brings up. colorChooser = new JColorChooser(); dialog = JColorChooser.createDialog(button, "Pick a Color", true, //modal colorChooser, this, //OK button handler null); //no CANCEL button handler } public void actionPerformed(ActionEvent e) { if (EDIT.equals(e.getActionCommand())) { //The user has clicked the cell, so //bring up the dialog. button.setBackground(currentColor); colorChooser.setColor(currentColor); dialog.setVisible(true); fireEditingStopped(); //Make the renderer reappear. } else { //User pressed dialog's "OK" button. currentColor = colorChooser.getColor(); } } //Implement the one CellEditor method that AbstractCellEditor doesn't. public Object getCellEditorValue() { return currentColor; } //Implement the one method defined by TableCellEditor. public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { currentColor = (Color)value; return button; }}
这段代码很简单。比较难懂的是 fireEditingStopped。如果没有这句,editor 保持激活,即便对话框不再可视。它的调用让 table 知道 editor 已经可以无效,让 renderer 来处理单元格。
如果一个单元格默认的 editor 允许空文本, 当文本不是你指定的某些东西,你想获得错误提示。错误检查伴随发生在输入的文本转换为合适类型的对象的时候。
当默认editor视图创建一个关联单元格列的 class 实例时,这种对用户输入的字符串自动检测就会发生。这个默认的 editor 使用字符串为参数的够着函数创建该实例。例如,在单元格的数据类型为Integer的列中,当用户敲入“123”,默认的 editor 创建对应的Integer类型,使用等同于 new Integer(“123”)。如果构造器抛出一个异常,单元格的outline变成红,该单元格不允许失去聚焦。如果你的列数据类型对应的 class 可以使用字符串参数构造实例,则可以是哦那个默认的 editor 完成检测。
如果你喜欢用 文本域 作为单元格的 editor,又想自定义某些检测方式,或是定义发现错误时的不同表现。你可以使用 formatted text field . ,formatted text field 可以检测在输入期间或者输入完成时检测是否错误。
例子 TableFTFEditDemo.java
,,建立一个 formatted text field 作为 editor。限制所有整数值只能为 0~100之间:
下面这句代码使 formatted text field 成为所有包含 Integer 类型数据的列的 editor。IntegerEditor.java
查看
table.setDefaultEditor(Integer.class, new IntegerEditor(0, 100));
IntegerEditor 是 DefaultCellEditor
的子类。使用 DefaultCellEditor 支持的 JFormattedTextField 代替 JTextField。使用 integer format 建立一个 formatted text field,然后指定最大最小值。参考 How to Use Formatted Text Fields . 接着覆盖 DefaultCellEditor 要求的 getCellEditorValue 和 stopCellEditing 方法
getTableCellEditorComponent 这个覆盖的方法,在 editor 现实之前,正确的设置 formatted text field 的值(不是简单的继承JTextField的值)。
getCellEditorValue
方法则保持 单元格的值为 一个Integer,而不是 formatted text field 试图返回的long或者之类的。最后stopCellEditing 是的可以检测文本是否合法,可能防止了 editor 被解除。如果文本不合法,你的 stopCellEditing 给出对话框,让用户选择是继续编辑还是会退到最后输入的合法值。代码太长,请查看:
IntegerEditor.java
JTable 提供了一个简单的 API 用于打印表格。打印表格的最简单的方法就是直接调用无参数的 JTable.print
try { if (! table.print()) { System.err.println("User cancelled printing"); }} catch (java.awt.print.PrinterException e) { System.err.format("Cannot print %s%n", e.getMessage());}
在一个标准Swing应用程序中,调用 print 方法会弹出一个标准的打印对话框。返回值指示用户继续还是取消打印作业。 JTable.print 可能抛出 java.awt.print.PrinterException ,是check Exception,所以要trycatch
JTable 提供了多种 print 的重载。 来自
的代码战士如何定义一个 page header:TablePrintDemo.java
MessageFormat header = new MessageFormat("Page {0,number,integer}");try { table.print(JTable.PrintMode.FIT_WIDTH, header, null);} catch (java.awt.print.PrinterException e) { System.err.format("Cannot print %s%n", e.getMessage());}
有关更复杂的打印应用,使用 JTable.getPrintable
获得一个 Printable 对象。有关 Printable 的内容,参考refer to thePrinting lesson in the 2D Graphics trail.
Example | Where Described | Notes |
---|---|---|
SimpleTableDemo |
Creating a Simple Table | A basic table with no custom model. Does not include code to specify column widths or detect user editing . |
SimpleTable- |
Detecting User Selections | Adds single selection and selection detection to SimpleTableDemo . By modifying the program'sALLOW_COLUMN_SELECTION and ALLOW_ROW_SELECTION constants, you can experiment with alternatives to the table default of allowing only rows to be selected. |
TableDemo |
Creating a Table Model | A basic table with a custom model. |
TableFTFEditDemo |
Using an Editor to Validate User-Entered Text | Modifies TableDemo to use a custom editor (a formatted text field variant) for allInteger data. |
TableRenderDemo |
Using a Combo Box as an Editor | Modifies TableDemo to use a custom editor (a combo box) for all data in theSport column. Also intelligently picks column sizes. Uses renderers to display tool tips for the sport cells. |
TableDialogEditDemo |
Using Other Editors | Modifies TableDemo to have a cell renderer and editor that display a color and let you choose a new one, using a color chooser dialog. |
TableToolTipsDemo |
Specifying Tool Tips for Cells ,Specifying Tool Tips for Column Headers , | Demonstrates how to use several techniques to set tool tip text for cells and column headers. |
TableSortDemo |
Sorting and Filtering | Demonstrates the default sorter, which allows the user to sort columns by clicking on their headers. |
TableFilterDemo |
Sorting and Filtering | Demonstrates sorting and filtering, and how this can cause the view coordinates to diverge from the model coordinates. |
TablePrintDemo |
Printing | Demonstrates table printing. |
ListSelectionDemo |
How to Write a List Selection Listener | Shows how to use all list selection modes, using a list selection listener that's shared between a table and list. |
SharedModelDemo |
Nowhere | Builds on ListSelectionDemo making the data model be shared between the table and list. If you edit an item in the first column of the table, the new value is reflected in the list. |
TreeTable, TreeTable II | Creating TreeTables in Swing ,Creating TreeTables: Part 2 | Examples that combine a tree and table to show detailed information about a hierarchy such as a file system. The tree is a renderer for the table. |
本文讨论适用于Swing应用程序的并发。假设你已经对线程“并发”有所了解。(下文中,“并发”常用作名词)
小心使用并发对于Swing程序设计者来说是很重要的。一个好的Swing程序使用并发创建绝不会“愣住”的用户接口——用于与用户交互的应用程序。要创建一个应答式的程序,程序员必须了解Swing框架如何使用threads。
一个Swing程序员通常要处理以下几种线程:
程序员不需要明确地提供创建这些线程的代码;这部分代码虚拟机或Swing框架给出。程序员的工作是利用这些线程创建一些可响应、可维护的Swing程序。
像运行在java平台的其他程序一样,Swing程序可以创建额外的线程和线程池(使用Concurrency 一节中描述的工具)。不过,对于基本Swing程序来说,下面描述的线程是非常重要的。
一次讨论三种线程。Worker线程占据较多的篇幅,因为在其上运行的任务使用java.swing.SwingWorker创建。这个类有很多有用的功能,包括worker线程上的任务和别的线程上的任务之间的 通信 和 协作。
(1)Initial Thread (初始化线程)
(2)The Event Dispatch Thread (EDT事件分派线程)
(3)Worker Threads and SwingWorker (工作线程 和 Swing工作线程)
(4)Simple Background Tasks (简单的后台任务)
(5)Tasks that Have Interim Results (返回中间结果的任务)
(6)Canceling BackGround Tasks (取消后台任务)
(7)Bound Properties and Status Methods (绑定属性 和 状态方法)
每一个应用程序在开始逻辑设计的时候都会有一系列线程。在标准程序中,仅有一个这样的线程:一个调用程序类的main方法的线程。applets的 intial threads 构造applet对象和调用它的 init 和 start 方法;这些动作都发生在一个单线程,或是两个三个不同的线程中,这依赖于java平台的实现。在本文中,我们称这类线程为 initial threads。
在Swing程序中, intial threads 并没有太多的工作。他们大多数必要的工作是创建一个 Runnale 对象,以此(触发)初始化GUI,然后将它安排他给 event dispatch thread(事件分派线程EDT) 上的执行任务,GUI真正初始在EDT上完成。
一旦GUI被创建,程序基本上受 GUI 事件所驱动,每个小 GUI 事件 触发 EDT 上的小任务的执行。 程序代码会被安排为一些任务并挂到 EDT (if they complete quickly, so as not to interfere with event processing如果完成的较快的话,就不用涉及事件处理“囧”) 或 挂到 worker thread (对于一些耗时任务)。
一个 initial thread 通过调用 javax.swing.SwingUtilities.invokeLater
or javax.swing.SwingUtilities.invokeAndWait
来安排 ”GUI创建任务“。这两个方法都有一个参数:定义了新任务的Runnable。 由他们名字的不同容易知道:invokeLater简单地安排任务并返回; invokeAndWait 在返回前等待任务。(前者异步,后者同步)
你常在demo中看到下面代码:
SwingUtilities.invokeLater(new Runnable() { public void run() { createAndShowGUI(); }});
对于applet,创建GUI的任务一定使用 invokeAndWaite方法从 init 方法开始; 不然的话,init 方法会在 GUI 创建前返回,这回给浏览器加载applet带来一些问题。在其他程序中,安排 “GUI创建” 任务 通常是 initial thread 最后做的一件事。 所以 使用 invokeLater 或 invokeAndWaite 都无关紧要。
为什么 initial thread 自己不创建 GUI 呢? 因为几乎所有访问影响Swing组件的代码都要在 EDT 上跑。 这种严格的要求在下节中讨论。
Swing事件处理代码在一个特别的线程上跑,我们称之为 event dispatch thread(事件分派/指派线程)。大多数调用SwingAPI的代码都在这个线程上跑。这是必须的,因为几乎所有Swing对象方法都是“线程不安全的”:调用它们可能会遇到thread interference or memory consistency errors 的问题。一些Swing组件方法在文档中被标记未“线程安全”;这些可以被任何线程安全的调用。所有其他Swing组件方法都必须通过 EDT 调用。如果忽略这个准则,或许大多数时间程序运行无误,不过将会遭受不可预知且难于重现的错误。
注释: 大家是否觉得怪异?java平台中如此重要的一部分居然是线程不安全的。事实证明:任何尝试创建线程安全的GUI库,将面临一些基本问题。有关这方面的内容,可参考:MultiThreaded toolkits: A failed dream? (我翻译了此文,在我的博客-JAVA-GUI)
思考“代码如一组小任务在事件分派线程上运行”是很有帮助的。大多数任务是事件处理方法的调用,例如ActionListener.actionPerformed。其他任务通过应用程序代码通过调用 invokeLater 或 invokeAndWait 来安排。事件分派线程上的仍无一定是较快完成的;如果不是的话,未处理事件会堵塞,用户接口变得无法响应。
如果你需要确定你的代码是否在 EDT 上跑的话,可以调用javax.swing.SwingUtilities.isEventDispatchThread
方法。
先阅读此文:http://vearn.iteye.com/blog/344591 了解SwingWorker
当一个Swing程序需要运行一个耗时任务的时候,它通常需要一个woker threads,也就是我们所知道的 background threads。每个工作在worker thread上的任务代表一个javax.swing.SwingWorker实例。SwingWorker类是一个抽象类;你必须定义一个子类来创建SwingWorker对象;匿名内部类是常常用于创建简单的SwingWorker对象。
SwingWorker提供了一些通信和控制功能:
结合开头的那篇文章http://vearn.iteye.com/blog/344591,在看看“如何使用进度条”一文中的第一个demo,很容易理解SwingWorker的作用。
提醒:java.swing.SwingWorker类收录在JAVA SE 6.比这更重要的是,另一个同样叫SwingWorker的类,基于同样的目的被广泛使用。旧的SwingWorker已经不再是java平台的一部分,也不作为 Jdk 部分。
新的javax.swing.SwingWorker是一个全新的类。在功能上,他不是过去旧的Swing‘Worker的扩展。两个类的方法拥有同样功能却不一样的名字。同时,旧的SwingWorker实例可以重复使用,而现在,对于一个新任务则需要一个新的java.swing.SwingWorker实例。
拷贝JDK文档中的内容:
使用 Swing 编写多线程应用程序时,要记住两个约束条件:(有关详细信息,请参阅 How to Use Threads ):
这些约束意味着需要时间密集计算操作的 GUI 应用程序至少需要以下两个线程:1) 执行长时间任务的线程; 2) 所有 GUI 相关活动的事件指派线程 (EDT)这涉及到难以实现的线程间通信。
SwingWorker
设计用于需要在后台线程中运行长时间运行任务的情况,并可在完成后或者在处理过程中向 UI 提供更新。SwingWorker
的子类必须实现doInBackground()
方法,以执行后台计算。
工作流
SwingWorker
的生命周期中包含三个线程:
当前 线程:在此线程上调用 execute()
方法。它调度 SwingWorker
以在 worker 线程上执行并立即返回。可以使用get
方法等待 SwingWorker
完成。
Worker 线程:在此线程上调用 doInBackground()
方法。所有后台活动都应该在此线程上发生。要通知 PropertyChangeListeners
有关绑定 (bound) 属性的更改,请使用firePropertyChange
和 getPropertyChangeSupport()
方法。默认情况下,有两个可用的绑定属性:state
和progress
。
事件指派线程 :所有与 Swing 有关的活动都在此线程上发生。SwingWorker
调用 process
和 done()
方法,并通知此线程的所有 PropertyChangeListener
。
通常,当前 线程就是事件指派线程 。
在 worker 线程上调用 doInBackground
方法之前,SwingWorker
通知所有PropertyChangeListener
有关对 StateValue.STARTED
的 state
属性更改。doInBackground
方法完成后,执行done
方法。然后 SwingWorker
通知所有 PropertyChangeListener
有关对StateValue.DONE
的 state
属性更改。
SwingWorker
被设计为只执行一次。多次执行 SwingWorker
将不会调用两次 doInBackground
方法。
//拷贝完毕
我们从一个非常简单却可能有些费时的任务开始。
applet 在播放动画中加载一组图片文件。如果图片文件在 initial 线程中加载的话, 可能延迟GUI的出现。如果图片文件是在 EDT 中加载,则GUI可能暂时无法响应。TumbleItem
为了避免这些问题,
在 initial threads 中创建一个 SwingWorker 实例并运行。 这个实例对象的 doInBackground方法,在 worker thread 上运行, 把图片加载到一个 ImageIcon 数组,并返回数组引用。 而 done 方法则在 EDT 上执行, 调用 get 方法 重新获得这个引用,赋值给 applet 类的字段 imgs。这样就是的 TunbleItem快速构建 GUI,不需要等待图片加载完成。TumbleItem
以下代码定义并运行SwingWorker对象,其中展示了如 SwingWorker 的 doInBackground 方法返回一个中间结果:
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 方法返回值的类型, get方法被其他线程调用获得一个结果,该结果是doInBackground 方法的返回值。SwingWorker 的第二个类型参数是: 当后台任务还在进行时,返回的中间结果的类型。 因为这个demo没有返回中间结果,所以 Void 被用作参数,类似占位符。(若要仔细了解,请查看JDK文档javax.swing.SwingWorker
.)
你可能会质疑,赋值 imgs 的代码是不必要的麻烦。 为什么 让 doInBackground 方法 返回一个对象,然后又用 done 方法重新获得它呢? 为什么不在 doInBackground 方法中直接 赋值 imgs 呢? 问题是:imgs对象的引用在 worker thread 中创建, 并在 EDT 中使用。当对象按照直接赋值这种方法来共享的话,你必须保证一个线程中的改变对于另一个线程是透明的。使用 get 方法可以确保透明, 因为使用 get 方法在 创建imgs 和 使用 imgs 之间 建立一个 hapens-before 关系。
(我参他人的一段话
Do not write a reference to the object being constructed in a place where another thread can see it before the object's constructor is finished. If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object's final fields.
不要在对象构造完成前,在其他线程的任何地方使用未构造完成的对象。如果你这么做的话,当对象被其他线程“看到”,则看到它的线程 今后都只能看到这个对象的 final字段而已)
这里有两种实际的方式去重新获得 doInBackground 方法返回的对象:
注意:当你在 EDT 上调用过多 get 方法是, 在 get 方法未返回前, EDT不会处理任何 GUI 事件,GUI就像冰冻了一样。 另外,如果你不能肯定后台任务能够完成或结束,请使用带参数的 get 方法。
我们常常用到:当后台任务还在进行时,后台任务提供一个中间结果。任务可以通过调用 SwingWorker.publish
来实现。这个方法接受多个参数,每个参数的类型要跟 SwingWorker 的第二个参数类一样((4)提到了)。
为了收集 publish 提供的结果, 可以重写 SwingWorker.process
。这个方法将会被 EDT 调用。多个 publish 调用 通常会 合并为一个 process 调用。
观察
例子如何使用 publish 方法提供中间结果。这个程序通过 在后台任务 中生产一组随机的boolean值来验证Flipper.java
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; }}
heads字段是随机值为true的次数,total 是总次数。
后台任务代表一个 FlipTask 实例
private class FlipTask extends SwingWorker<Void, FlipPair> {
由于任务不是返回最终结果,所以 SwingWorker 的第一个类型参数无关紧要, 用Void 作占位符。 每次“抛硬币”后,任务调用 publish 方法:
@Overrideprotected 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 被 EDT 调用前,大量的 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 方法是如文所描述的一样,是真正 “线程安全的”。这意味着,我们可以在worker 线程中指派 publish、 process 、设置文本值 这些动作。 我们忽略这个事实,只是为了给出一个简单SwingWorker中间结果的演示。
调用 SwingWorker.cancel
可以取消一个正在运行的后台任务, 这个任务也需要配合自个的取消。这里有两种途径实现:
SwingWorker.isCanceled
迅速实现。如果 cancel 时竟被调用,则返回 true。
cancel 方法带一个 boolean 类型参数,如果参数是 true, cancel 向后台任务发送一个 interrupt。可以通过调用 Thread.interrupted 方法判定是否接受了一个interrupt
while (true) { if (Thread.interrupted()) { System.out.println("receive an interrupt"); break; } System.out.println("still working"); }
不管这个 cancel 方法的参数是 true 还是 false, 调用 cancel 方法 都会改变对象的 取消状态 为 true。这是 isCanceled 方法返回的值。 一旦改变, 取消状态不能回退。
上一节的 Flipper 例子 使用 status-only 编码风格。 当 isCancelled 返回 true的时候, doInBackground 方法里的循环退出。 用户点击 “Cancel” 按钮 ,上面的所说的情况就会发生。
对于 Flipper 应用程序,这种 status-only 方法
protected Void doInBackground() { ....... while (!isCancelled()) { total++; if (random.nextBoolean()) { heads++; } publish(new FlipPair(heads, total)); } ...... return null; }
之所以有效的原因是: 它的 doInBackground 没有包含任何可能抛出 InterruptedException 的代码。要对 接收到的 interrupt 做出响应, 后他任务只需要简单的调用 Thread.isInterrupted(),如前面所讲。
提醒:如果 get 方法在 后台任务取消后调用,则抛出java.util.concurrent.CancellationException
异常。
SwingWorker 支持bound properties ,这有利于与其它进程通信。对于SwingWorker,两个预先绑定的属性:progress 和 state. 跟其他绑定属性一样,progress 和 state 可以在 EDT 上触发事件处理任务。
通过实现一个属性改变监听器,程序可以跟踪 progress,state和别的绑定属性的变化。更多信息请参考 How to Write a Property Change Listener in Writing Event Listeners .
progress绑定变量是一个从0到100的正数。已经预先定义了setter和geter方法(SwingWorker.setProgress和SwingWorker.getProgress).
示例使用 progress 在后台任务更新进度条,参考How to Use Progress Bars inUsing Swing Components .(我的博文中有此文的翻译)ProgressBarDemo
state绑定变量只是SwingWorker对象在其生命周期中的状态。这个绑定属性可选值包含在一个枚举中,SwingWorker.StateValue.可能值为:
PENDING:调用doInBackground 之前,对象构造期间的状态
STARTED:调用doInBackground 的前一刻(很短时间)知道 调用done的前一刻
DONE:剩余的状态(调用done的前一刻之后)
state当前值可以通过调用 SwingWorker.getState 方法返回
两个方法,是 Future 接口的一部分, 同样在后台任务对状态的反馈。isCancelled 方法,在(6)Canceling BackGround Tasks中讲了。还有 isDone 方法,如果任务完成,无论是正常完成还是被关闭取消而完成,都返回true。
Question 1: For each of the following tasks, specify which thread it should be executed in and why.
Answer 1:
- Initializing the GUI. The event dispatch thread; most interactions with the GUI framework must occur on this thread.
- Loading a large file. A worker thread. Executing this task on the event dispatch thread would prevent GUI events from being processed, "freezing" the GUI until the task is finished. Executing this task on an initial thread would cause a delay in creating the GUI.
- Invoking
javax.swing.JComponent.setFont
to change the font of a component. The event dispatch thread. As with most Swing methods, it is not safe to invokesetFont
from any other thread.- Invoking
javax.swing.text.JTextComponent.setText
to change the text of a component. This method is documented as thread-safe, so it can be invoked from any thread.Question 2: One thread is not the preferred thread for any of the tasks mentioned in the previous question. Name this thread and explain why its applications are so limited.
Answer 2: The initial threads launch the first GUI task on the event dispatch thread. After that, a Swing program is primarily driven by GUI events, which trigger tasks on the event dispatch thread and the worker thread. Usually, the initial threads are left with nothing to do.Question 3:
SwingWorker
has two type parameters. Explain how these type parameters are used, and why it often doesn't matter what they are.
Answer 3: The type parameters specify the type of the final result (also the return type of thedoInBackground
method) and the type of interim results (also the argument types forpublish
andprocess
). Many background tasks do not provide final or interim results.
Question 1: Modify theexample so that it pauses 5 seconds between "coin flips." If the user clicks the "Cancel", the coin-flipping loop terminates immediately.
Flipper
Answer 1: See the source code for. The modified program adds a delay in the central
Flipper2
doInBackground
loop:Theprotected Object doInBackground() { long heads = 0; long total = 0; Random random = new Random(); while (!isCancelled()) { try { Thread.sleep(5000); } catch (InterruptedException e) { //Cancelled! return null; } total++; if (random.nextBoolean()) { heads++; } publish(new FlipPair(heads, total)); } return null;}try ... catch
causesdoInBackground
to return if an interrupt is received while the thread is sleeping. Invokingcancel
with an argument oftrue
ensures that an interrupt is sent when the task is cancelled.
(1)使用确定模式进度条
(2)使用不确定模式进度条
(3)使用进度监视器
(4)决定使用进度条还是进度监视器
(5)ProgressMonitorInputStream输入流进度监视器的使用
有时候程序中的任务需要一段时间才能完成。一个好的程序应该向用户提供了一些指示信息:任务正在运行、任务需要多长时间、任务已经完成多少。这种指示工作可以使用进度的估算来描述(进度条),本质上是一种动态地图片。
另一种方法是使用等待光标(wait cursor),使用Cursor类和setCursor方法。例如,下面代码使得wait cursor显示在container上(这个container可以使任何未指定cursor的组件)
container.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR))
你可以像下面这样使用进度条,告知用户任务已经完成了多少
有时候,你不能立刻确定一个大任务所需的时间,或者仍无会阻塞在某一个状态很长时间。你可以不通过度量进度,而是把progress bar放入不确定模式(indeterminate mode)。不确定模式的进度条会动态指示任务正在完成。随着进度条可以显示更多有意义的信息,你应该把他转变为默认的确定模式。在Java样式中,不确定模式的进度条是这样的:
(深色块滚动)
Swing提供了三个类帮助你使用progress bars:
一个可视化的组件,图形化显示已经完成总任务的多少。参考“(1)使用确定模式进度条” 使用典型的进度条。参考“(2)使用不确定模式进度条”在任务大小确定前滚动进度条
一个非可视化组件。该类的实例检测任务进度,并且在必要的时候弹出对话框,参考“(3)使用进度监视器”
一个关联进度监视器的输入流,监视器从该输入流中读取数据。你可以像其他基本I/O输入流一样使用它。你可以想”(3)使用进度监视器“中描述那样,通过调用getProgressMonitor和配置它获得该流的进度监视器。
接着介绍“(4)决定使用进度条还是进度监视器”
使用进度条度量本线程任务进度:
下面是代码来自ProgressBarDemo.java
创建和设置进度条:
//Where member variables are declared:JProgressBar progressBar;...//Where the GUI is constructed:progressBar = new JProgressBar(0, task.getLengthOfTask());progressBar.setValue(0);progressBar.setStringPainted(true);
这个构造器创建了进度条,设置了进度条的最大最小值。你同样可以通过setMinimum和setMaximun来设置这些值。这个例子程序中的进度条最大值和最小是分别是0和任务的长度。不过,进度条的最大最小是可以使任意值,包括附属。这段代码也设置了进度条的当前值为0.
setStringPainted的调用使得进度条展现为边界、已完成任务百分比文字描述。默认情况下,进度条显示getPercentComplete方法返回的值。可以选择的是,你可以通过调用setString改变默认的字符串(默认:1%,2%……)。例如:
if (/*...half way done...*/) progressBar.setString("Half way there!");
当用户点击开始,一个内部类任务实例就被创建并运行:
public void actionPerformed(ActionEvent evt) { startButton.setEnabled(false); setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); done = false; task = new Task(); task.addPropertyChangeListener(this); task.execute();}
Task是javax.swing.SwingWorker
的子类。对于这个demo来讲Task实例实现了三件重要的事情:
1.实例在分离线程中调用doInBackground。这事大任务真正开始执行。使用后台进程代替事件分派进程,防止任务在进行时,用户任务阻塞了。
2.当后台进程完成后,task实例在事件分派进程中调用done方法。
3.这个实例保持一个绑定属性,progress,它更新指示任务进度。另外,propertyChange方法在每次progress改变时都会被调用
(下一篇文章会介绍Worker Threads and SwingWorker (工作者线程和Swing工作者)和Concurrency in Swing (Swing中的并发))
demo程序中的后台任务,冒充一个真正的任务,她在随机时间间隔之间反馈进度数量。properChange方法通过更新进度条,对任务的progress属性的改变做出响应。
public void propertyChange(PropertyChangeEvent evt) { if (!done) { int progress = task.getProgress(); progressBar.setValue(progress); taskOutput.append(String.format( "Completed %d%% of task.\n", progress)); }
注意:done方法设置done字段为true,防止propertyChange对进度条作更新。这是必须的,因为progress属性的最后更新发生在done调用之后,而progress属性的改变会调用propertyChange。
在ProgressBarDemo2.java
中,模式设置为不确定,知道真正的进度开始位置:
public void propertyChange(PropertyChangeEvent evt) { if (!done) { int progress = task.getProgress(); if (progress == 0) { progressBar.setIndeterminate(true); taskOutput.append("No progress yet\n"); } else { progressBar.setIndeterminate(false); progressBar.setString(null); progressBar.setValue(progress); taskOutput.append(String.format( "Completed %d%% of task.\n", progress)); } }}
代码改变部分与关字符串显示相关。一个显示字符串的进度条应该比demo设计者设计的进度条高,我们自个决定,进度条应该仅在默认情况且为确定模式情况下,才显示字符串。然而,我们想要避免因为模式的改变而使得进度条高度改变,使得布局难看,因此,代码使用了setStringPainted(true),同时增加一个setString(“”)的调用,这样,就没有文本被显示了。接着,当进度条从不确定模式转变为确定模式时,调用setString(null)使得进度条显示默认字符串。
我们没有移除事件处理器中progressBar.setValue的调用。这个调用不会造成危害,因为一个不确定模式不会使用自己的值属性,除非用它在状态字符串中显示。实际上,忧郁某些外观样式不支持不确定模式,所以保持进度条的数据尽可能更新是一种好的措施。
现在,我们重写ProgressBarDemo.java
,使用一个进度监视器代替进度条。以下是截图:
一个进度监视器不能重复使用,因此每一次新任务开始时,一定有一个新的进度监视器被创建。demo中,每点击开始按钮,程序都会创建一个进度监视器。
这句代码创建了进度监视器
progressMonitor = new ProgressMonitor(ProgressMonitorDemo.this, "Running a Long Task", "", 0, task.getLengthOfTask());
这句代码使用ProgressMonitor唯一的构造器创建监视器并初始化几个参数。
int progress = task.getProgress();String message = String.format("Completed %d%%.\n", progress);progressMonitor.setNote(message);progressMonitor.setProgress(progress);taskOutput.append(message);
默认情况下,一个进度监视器在决定是否弹出对话框前,至少等待一个最小时间,500毫秒。它同样花大于最小时间去等待progress作改变。如果计算出任务将会花费超过2000毫秒才能完成,进度对话框会显示。调用setMillisToDecidedToPopup调整“确定是否弹出进度监视器”之前要等待的时间量。调用setMillisToPopup调整“显示弹出监视器花费的时间量
”。
这个例子使用了一个进度监视器,它增加了一个功能。用户可以通过点击对话框上的cannel按钮取消任务。这里有一段代码,查询用户是否取消任务或是任务正常退出:
if (progressMonitor.isCanceled() || task.isDone()) { progressMonitor.close(); Toolkit.getDefaultToolkit().beep(); if (progressMonitor.isCanceled()) { task.cancel(true); taskOutput.append("Task canceled.\n"); } else { taskOutput.append("Task completed.\n"); } startButton.setEnabled(true);}
注意:并不是进度监视器自身取消任务。可以利用GUI和API使程序容易实现这些功能。
下列情况可能更适合使用进度条:
下列情况可能更适合使用进度监视器:
如果你决定使用进度监视器,并且你所监视的任务是从一个输入流中读取数据,你可以使用ProgressMonitorInputStream类
public class ProgressMonitorInputStreamDemo { public static void main(String[] args) { int len = 0; String filePath = "C:\\Documents and Settings\\Administrator\\桌面\\http小记.txt"; byte[] b = new byte[30]; JTextArea text = new JTextArea(20,20); JFrame f = new JFrame(); f.setSize(330,300); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.add(text,BorderLayout.CENTER); f.setVisible(true); try{ FileInputStream input = new FileInputStream(filePath); ProgressMonitorInputStream in = new ProgressMonitorInputStream(f,"读取某某某文件",input); while( (len=in.read(b)) != -1){ String s = new String(b,0,len); text.append(s); //delay 3 seconds to see progress bar clearly since the file is too small Thread.sleep(3000); } }catch(InterruptedException e){} catch(IOException e){} }}
效果:
英文文档地址: http://download.oracle.com/javase/tutorial/uiswing/components/tree.html
(1)创建树
(2)对节点的选择做出响应
(3)自定义树的外观表现
(4)动态改变一棵树
(5)创建树的数据模型
(6)懒加载孩子
(7)如何写expansion linstener
(8)如何写tree-will-expand listener
利用 JTree 类,你可以显示等级体系的数据。一个 JTree 对象并没有包含实际的数据;它只是提供了数据的一个视图。像其他非平凡的( nontrivial ) Swing 组件一样,这种 Jtree 通过查询她的数据模型获得数据。这是一个 Jtree :
如上面的图片所显示, Jtree 垂直显示它的数据。树中显示的每一行包含一项数据,称之为节点( node )。每颗树有一个根节点( root node ),其他所有节点是它的子孙。默认情况下,树只显示根节点,但是你可以设置改变默认显示方式。一个节点可以拥有孩子也可以不拥有任何子孙。我们称那些可以拥有孩子(不管当前是否有孩子)的节点为“分支节点”( branch nodes ),而不能拥有孩子的节点为“叶子节点”( leaf nodes )。
分支节点可以有任意多个孩子。通常,用户可以通过点击实现展开或者折叠分支节点,使得他们的孩子可见或者不可见。默认情况下,除了根节点以外的所有分支节点默认呈现折叠状态。程序中,通过监听 tree expansion 或者 tree-will-expand 事件可以检测分支节点的展开状态。监听事件在下面两节内容中描述How to Write a Tree Expansion Listener and How to Write a Tree-Will-Expand Listener .
在树中,一个节点可以通过 TreePath (一个囊括该节点和他所有祖先节点的路径对象)或者他的折叠行来识别。
展开节点( expanded node )就是一个非叶子节点,当他的所有祖先都展开时,他将显示他的孩子。
折叠节点( collapsed node )是隐藏了孩子们得的节点。
隐藏节点( hidden node )就是折叠节点下的一个孩子
这里是一个应用程序的截图,上半部分展示了一个滚动面板( scroll pane )中的树( Jtree )。
接下来的代码是从
http://download.oracle.com/javase/tutorial/uiswing/examples/components/TreeDemoProject/src/components/TreeDemo.java 获得,创建了一个JTree 对象,并将之放到一个scroll pane 上
//Where instance variables are declared: private JTree tree; ... public TreeDemo() { ... DefaultMutableTreeNode top = new DefaultMutableTreeNode("The Java Series"); createNodes(top); tree = new JTree(top); ... JScrollPane treeView = new JScrollPane(tree); ... }
这段代码创建了一个DefaultMutableTreeNode 实例作为根节点。接着创建树中剩下的其他节点。创建完节点后,通过指定刚才创建的根节点为JTree 构造函数的参数,创建一棵树。最后,将树放到滚动面板中,这是一个通常的策略,因为需要显示完一个树,而展开树需要另外比较大的空间。
以下代码创建根节点以下的节点
private void createNodes(DefaultMutableTreeNode top) { DefaultMutableTreeNode category = null; DefaultMutableTreeNode book = null; category = new DefaultMutableTreeNode("Books for Java Programmers"); top.add(category); //original Tutorial book = new DefaultMutableTreeNode(new BookInfo ("The Java Tutorial: A Short Course on the Basics", "tutorial.html")); category.add(book); //Tutorial Continued book = new DefaultMutableTreeNode(new BookInfo ("The Java Tutorial Continued: The Rest of the JDK", "tutorialcont.html")); category.add(book); //JFC Swing Tutorial book = new DefaultMutableTreeNode(new BookInfo ("The JFC Swing Tutorial: A Guide to Constructing GUIs", "swingtutorial.html")); category.add(book); //...add more books for programmers... category = new DefaultMutableTreeNode("Books for Java Implementers"); top.add(category); //VM book = new DefaultMutableTreeNode(new BookInfo ("The Java Virtual Machine Specification", "vm.html")); category.add(book); //Language Spec book = new DefaultMutableTreeNode(new BookInfo ("The Java Language Specification", "jls.html")); category.add(book);}
DefaultMutableTreeNode 构造函数的参数是一个用户自定义的类对象,它包含或指向了关联树节点的数据。这个用户对象可以是一个字符串,或者是一个自定义的类。如果它实现了一个自定义对象,你应该要重新实现覆盖他的 toString 方法,这样他才能返回对应字符串作为节点显示的字符串。 Jtree 默认情况下,每个节点都是用 toString 的返回值作为显示。所以,让 toString 返回一些有意义的值是很重要的。有时候,覆盖 toString 方法是不可行的;在某些场景你可以通过重写 Jtree 的 convertValueToText 方法,映射模型对象到一个可显示的字符串。
例如,前面 demo 中的 BookInfo 类是一个自定义类,它包含了两个字段:书名和描述该书本的 HTML 文件的 URL 路径。 toString 方法也重新实现,返回书名。从而,每个节点关联了一个 BookInfo 对象,并且显示书名。
总之,你可以调用 Jtree 的构造函数创建一棵树,指定一个实现了 TreeNode 的类作为参数。你应该尽量把这棵树放到一个滚动面板中( scroll pane ),这样树就不会占用太大的空间。对于树节点相应用户点击而展开和折叠的功能,你不需要做任何事情。但是,你一定要添加一些代码使得树在用户点击选择一个节点时能够作出反应,例如:
对于树节点的选择做出响应是简单的。你可以实现一个树节点选择监听器,并且注册在这棵树上。接下来的代码显示了 TreeDemo.java 中有关选择的代码:
//Where the tree is initialized: tree.getSelectionModel().setSelectionMode (TreeSelectionModel.SINGLE_TREE_SELECTION); //Listen for when the selection changes. tree.addTreeSelectionListener(this); ... public void valueChanged(TreeSelectionEvent e) { //Returns the last path element of the selection. //This method is useful only when the selection model allows a single selection. DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); if (node == null) //Nothing is selected. return; Object nodeInfo = node.getUserObject(); if (node.isLeaf()) { BookInfo book = (BookInfo)nodeInfo; displayURL(book.bookURL); } else { displayURL(helpURL); }}
上面的代码执行了一下任务:
1 .获得树的默认 TreeSelectionModel (节点选择模式),然后设置它,使得在某一时刻只有一个节点被选中。
2 .注册了一个事件处理器。事件处理器是一个实现了 TreeSelectionListener 接口的对象。
3. 在事件处理器中,通过调用 Tree 的 getLastSelectedPathComponent 方法获得选中的节点。
4 .使用 getUserObject 方法获得节点关联的数据。(节点 node 是一个非平凡组件,要通过它关联的数据模型获得真正的数据)
这里给出一些树节点的图片,分别通过 Java 、 Windows 和 MacOS 样式绘得。
(依次为 java look 、 windows look 和 MacOS look )
像之前图片显示一样,一棵树按照惯例,对于每个基点显示了一个图标和一些文字。像我们简短的展示一样,你可以指定这些样式。
一棵树通常表现一些外观和样式特效,通过不同的绘制图形指示节点间的关系。你可以在限制范围内自定义这些图形。首先,你可以使用 tree.setRootVisible(true) 设置显示根节点或者 tree.setRootVisible(false) 隐藏根节点。其次,你可以使用 tree.setShowsRootHandles(true) 请求设置树的顶层节点具有句柄( +- 图标,点击句柄使其展开折叠)。如果顶层节点是根节点的话,需要保证它是可视的,如果是顶层节点则每个孩子都显示句柄。
如果你使用 Java 样式,你可以自定是否在节点间显示行线来表现他们的关系。默认情况下, Java 样式使用“角线”(类似“ L ”)。通过设置 Jtree.lineStyle 的客户端属性,你可以指定一种不同的标准。例如,通过以下代码,这只 JAVA 样式仅使用水平线隔开一组节点:
tree.putClientProperty(“Jtree.lineStyle”, “Horizontal”);
指定 JAVA 样式在节点间不显示任何行线,则使用以下代码:
tree.putClientProperty(“Jtree.lineStyle”, “None”);
接下来的一些截图显示了设置不同的 Jtree.lineStyle 属性(使用 JAVA 样式)
不管你使用那种样式( java 、 windows 、 mac ) , 默认情况下,节点显示的图标决定于节点是否为叶子节点和是否可展开。例如,在 windwos 样式中,每个叶子节点的默认图标是一个点;在 JAVA 样式中,叶子节点默认图标是一个类似白纸的符号。在所有样式中,分支节点被一个文件夹符号所标识。不同样式对于可展开分支和对应的可折叠分支,可能有不同的图标。
你可以很容易的改变叶子节点、可展开分支节点和可折叠分支节点的默认图标。如果要这样做的话,首先,你要创建一个 DefaultTreeCellRenderer 实例。你总是可以创建自己的 TreeCellRender ,让你喜欢的任何组件重复利用。接着,通过调用以下一个或多个方法去指定图标: setLeafIcon (对于叶子节点), setOpenIcon (对于可展开分支节点), setClosedIcon (对于可折叠节点)。如果你想要这棵树中各种节点都不显示图标,你就要指定图标为 null 。
一定你创建了这些图标,使用树的 setCellRender 方法去指定这个 DefaultTreeCellRender 来绘制它的节点。这里有一个来自 TreeIconDemo 的例子
ImageIcon leafIcon = createImageIcon("images/middle.gif");if (leafIcon != null) { DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer(); renderer.setLeafIcon(leafIcon); tree.setCellRenderer(renderer);}
这是一个截图:
如果你想更精巧的控制节点图标,或者你想提供一些工具,你可以创建 DefaultTreeCellRender 的子类,然后覆盖他的getTreeCellRendererComponent 方法。因为DefaultTreeCellRenderer 是Jlabel 的一个子类,你可以使用任何Jlabel 的方法,例如setIcon 。
下面代码来自 TreeIconDemo2.java ,创建了一个单元绘制器( cell renderer ),它根据节点的文本数据是否包含单词“ Tutorial ”来改变了叶子节点的图标。这个 renderer 同样可以指定提示文本( tool-tip ) --- 鼠标移到上面,出现提示。
//...where the tree is initialized: //Enable tool tips. ToolTipManager.sharedInstance().registerComponent(tree); ImageIcon tutorialIcon = createImageIcon("images/middle.gif"); if (tutorialIcon != null) { tree.setCellRenderer(new MyRenderer(tutorialIcon)); }...class MyRenderer extends DefaultTreeCellRenderer { Icon tutorialIcon; public MyRenderer(Icon icon) { tutorialIcon = icon; } public Component getTreeCellRendererComponent( JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent( tree, value, sel, expanded, leaf, row, hasFocus); if (leaf && isTutorialBook(value)) { setIcon(tutorialIcon); setToolTipText("This book is in the Tutorial series."); } else { setToolTipText(null); //no tool tip } return this; } protected boolean isTutorialBook(Object value) { DefaultMutableTreeNode node = (DefaultMutableTreeNode)value; BookInfo nodeInfo = (BookInfo)(node.getUserObject()); String title = nodeInfo.bookName; if (title.indexOf("Tutorial") >= 0) { return true; } return false; }}
下面是结果的截图:
你可能会疑惑单元绘制器( cell renderer )是如何工作的。当一个 tree 在话每个节点的时候,不管是 Jtree 或是他的样式表现都包含了绘制节点的代码。 Tree 可以使用 cell renderer 的绘图代码代替前者去绘制节点。例如,画一个包含字符串“ The Java Programming Language ”的叶子节点, tree 会要求 cell renderer 返回一个组件,该组件能够绘制一个包含该字符串的叶子节点。如果这个 cell renderer 是一个 DefaultTreeCellRender ,它就返回一个 label ( DefaultTreeCellRender 继承于 Jlabel ),它绘制默认的叶子节点图标,紧随一段字符串。
一个 cell renderer 仅绘制而不处理事件。如果你想要对一颗 tree 增加事件处理器,你需要在树上注册监听器,如果事件紧发生在某个节点被选择时,可以选择注册在 tree 的 cell editor 上。有关 cell editors 的资料可以参考Concepts: Editors and Renderers . 这节将讨论 table cell editors 和 renderers ,他们类似于 tree cell editors 和 renderers 。
接下来的图片展示了一个叫 DynamicTreeDemo 的应用程序,它允许你从一颗可视 tree 中增加或者移除节点。你也可以编辑每个节点的文本。
这里给出了树初始化的代码:
rootNode = new DefaultMutableTreeNode("Root Node");treeModel = new DefaultTreeModel(rootNode);treeModel.addTreeModelListener(new MyTreeModelListener());tree = new JTree(treeModel);tree.setEditable(true);tree.getSelectionModel().setSelectionMode (TreeSelectionModel.SINGLE_TREE_SELECTION);tree.setShowsRootHandles(true);
通过明确的创建 tree 的模型( model ),这段代码保证 tree 的 model 是 DefaultTreeModel 的实例。这样,我们知道所有 tree model 支持的方法。例如,我们可以调用 model 的 insertNodeInto 方法,几时这个方法不是 TreeModel 接口要求的。
为使得树中节点的文本值可编辑,我们调用对 tree 调用 setEditable(true) 。当用户完成一个节点的编辑时,这个 model 产生一个 tree model 事件,它会告诉所有监听者(包括 Jtree ):树节点被改变了。注意:尽管 DefaultMutableTreeNode 拥有改变一个节点内容的方法,但是改变还是需要通过 DefaultTreeModel 上面的方法。否则, tree model 事件就不能产生,事件的监听者(例如 tree )就不能知道这些更新。
为了通知“节点改变”,我们可以实现一个 TreeModelListener 。这里有一个关于 tree model 监听器的例子,当用户为一个树节点输入一个新名字时,事件会被检测到。
class MyTreeModelListener implements TreeModelListener { public void treeNodesChanged(TreeModelEvent e) { DefaultMutableTreeNode node; node = (DefaultMutableTreeNode) (e.getTreePath().getLastPathComponent()); /* * If the event lists children, then the changed * node is the child of the node we have already * gotten. Otherwise, the changed node and the * specified node are the same. */ try { int index = e.getChildIndices()[0]; node = (DefaultMutableTreeNode) (node.getChildAt(index)); } catch (NullPointerException exc) {} System.out.println("The user has finished editing the node."); System.out.println("New value: " + node.getUserObject()); } public void treeNodesInserted(TreeModelEvent e) { } public void treeNodesRemoved(TreeModelEvent e) { } public void treeStructureChanged(TreeModelEvent e) { }}
这里是一些增加按钮事件处理器(用于增加节点)的代码:
treePanel.addObject("New Node " + newNodeSuffix++);...public DefaultMutableTreeNode addObject(Object child) { DefaultMutableTreeNode parentNode = null; TreePath parentPath = tree.getSelectionPath(); if (parentPath == null) { //There is no selection. Default to the root node. parentNode = rootNode; } else { parentNode = (DefaultMutableTreeNode) (parentPath.getLastPathComponent()); } return addObject(parentNode, child, true);}...public DefaultMutableTreeNode addObject(DefaultMutableTreeNode parent, Object child, boolean shouldBeVisible) { DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(child); ... treeModel.insertNodeInto(childNode, parent, parent.getChildCount()); //Make sure the user can see the lovely new node. if (shouldBeVisible) { tree.scrollPathToVisible(new TreePath(childNode.getPath())); } return childNode;}
用 tree model 做为 JTree 的构造函数的参数,节点的文本改变监听器是注册在 model 上,而节点增删是通过 ActionListener 监听按钮事件
这段代码创建一个节点,插入 tree model 中。如果可以的话,讲请求该节点的上层节点展开, tree 滚动,这样新节点就可视了。这段代码使用 DefaultTreeModel 类提供的 insertNodeInto 方法向 tree model 插入新节点。
如果 DefaultTreeModel 不能符合你的需求,则需要你自定义一个 data model 。你的 data model 必须实现 TreeModel 接口。 TreeModel 指定 获取树中特定节点、获取特定节的孩子数量、确定一个节点是否为叶子、通知 model 树的改变 和 增加删除 tree model 监听器的方法。
有趣的是, TreeModel 接口接受各种对象作为树节点。这就不需要通过 TreeNode 对象来表现节点,节点甚至不需要实现 TreeNode 接口。因此,如果 TreeNode 接口不适合你的 tree model, 大可自由的设计自己的节点表现形式。例如,如果一个事前存在的 阶级数据结构( hierarchical data structure ),你就不需要复制或者强制把他放进 TreeNode 模子中。你只需实现你的 tree model ,这样你就可以使用已经存在的数据结构。
下面图片展示了一个叫 GenealogyExample( 家谱例子 ) ,他展示了某一个人的子孙和祖先。
在 GenealogyModel.java
中,你可以找到这个自定义的 tree model 的实现。因为这个 model 通过一个 DefaultTreeModel 的子类实现,他必须实现 TreeModel 接口。这就需要实现获得节点信息的一系列方法,例如,哪个是根节点、某个节点的子孙是哪些节点。在 GenealogyModel 的例子中,每个节点表现为一个 Person 类型的对象,这是一个未实现 TreeNode 接口的自定义类。
一个 tree model 一定要实现一些方法,用于增删 tree model listeners (监听器),当树的数据结构或者数据被改变时,必须把 TreeModelEvents ( tree model 事件)响应到这些监听器。例如,当用户指示 GenealogyExample 从“显示子孙”改变为“显示祖先”时, tree model 实现这些改变,然后产生一个事件并通知它的监听器。
(这里涉及的四个 java 文件都挺值得读,里面的编程思想跟技巧很值得学习)
懒加载( lazy loading )是一种应用程序特征:当一个类实例的实际加载和实例化延迟到这个实例使用前才进行。
通过懒加载我们得到任何东西了吗?当然,这将肯定增加了应用程序的性能。通过懒加载,你能够在使用一个类前,利用内存资源加载和实例化它。这样避免了应用程序的初始化时占用更多的类加载跟实例化时间,加快了应用程序的初始化加载时间。
有一种办法可以懒加载一棵树的孩子:利用 TreeWillExpandListener 接口。例如,你可以声明和加载根节点,祖父双亲和双亲的显示包含在以下代码中:(树的上层为祖先)
我们声明了根节点 root ,祖父双亲和双亲如下所示:
class DemoArea extends JScrollPane implements TreeWillExpandListener { ....... ....... private TreeNode createNodes() { DefaultMutableTreeNode root; DefaultMutableTreeNode grandparent; DefaultMutableTreeNode parent; root = new DefaultMutableTreeNode("San Francisco"); grandparent = new DefaultMutableTreeNode("Potrero Hill"); root.add(grandparent); parent = new DefaultMutableTreeNode("Restaurants"); grandparent.add(parent); dummyParent = parent; return root; }
你还可以像下面代码一样,把之前声明的节点加载到树 tree 上(这里只是显示而已)
TreeNode rootNode = createNodes();tree = new JTree(rootNode); tree.addTreeExpansionListener(this);tree.addTreeWillExpandListener(this); ....... ....... setViewportView(tree);
现在,你可以你可以懒加载孩子了,无论双亲节点 Restaurant 是否可视。如上所述,我们在一个方法中声明两个孩子:
private void LoadLazyChildren(){ DefaultMutableTreeNode child; child = new DefaultMutableTreeNode("Thai Barbeque"); dummyParent.add(child); child = new DefaultMutableTreeNode("Goat Hill Pizza"); dummyParent.add(child); textArea.append(" Thai Barbeque and Goat Hill Pizza are loaded lazily"); } ....... .......public void treeWillExpand(TreeExpansionEvent e) throws ExpandVetoException { saySomething("You are about to expand node ", e); int n = JOptionPane.showOptionDialog( this, willExpandText, willExpandTitle, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, willExpandOptions, willExpandOptions[1]); LoadLazyChildren(); }
有时候,我们使用一棵 tree ,当分支变成展开或者折叠状态的时候,或许需要作出某些反映。例如,你或许需要加载或者保存数据。
有两种监听器可以负责响应展开或折叠事件: tree expansion listeners (我理解为:已展开事件监听器)和 tree-will-expand listeners. (将来可以展开事件监听器)这节讨论 tree expansion listeners 。
一个 tree expansion 监听器侦测在展开或者折叠已经发生。一般来说,你应该实现一个 tree expansion 监听器,除非你需要阻止展开或折叠。
这个例子演示了一个简单的 tree expansion 监听器。窗口底部的文字区域展示关于每次 tree expansion 事件发生的消息。这是一个简单易懂的演示。
下面的代码展示了程序如何处理 expansion 事件
来自 TreeExpandEventDemo.java
.
private void LoadLazyChildren(){ DefaultMutableTreeNode child; child = new DefaultMutableTreeNode("Thai Barbeque"); dummyParent.add(child); child = new DefaultMutableTreeNode("Goat Hill Pizza"); dummyParent.add(child); textArea.append(" Thai Barbeque and Goat Hill Pizza are loaded lazily"); } ....... .......public void treeWillExpand(TreeExpansionEvent e) throws ExpandVetoException { saySomething("You are about to expand node ", e); int n = JOptionPane.showOptionDialog( this, willExpandText, willExpandTitle, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, willExpandOptions, willExpandOptions[1]); LoadLazyChildren(); }
Tree-will-expand listener 挡住了节点的展开或折叠(就是监听器侦听在展开折叠正在发生之前)。如果仅仅要在展开和折叠发生后通知监听器,那你就应该使用 expansion listener 代替它。
这个 demo 增加了 tree-will-expand 监听器。在你每次要展开一个节点前,监听器会提示你给出确认信息,确认是否展开。
TreeExpandEventDemo2.java