目录
高级Swing和图形化编程(一)
1、表格
JTable构件用于显示二维对象表格。
1.1、一个简单表格
JTable并不存储它自己的数据,而是从一个表格模型中获取数据。JTable类有一个构造器,能够将二维对象数组包装进一个默认的模型。
表格中的数据是以Object值的二维数组的形式存储的,该表格直接调用每个对象上的toString方法来显示它们。
public class PlanetTableFrame extends JFrame {
//列名
private String[] columnNames = {"Planet", "Radius", "Moons", "Gaseous", "Color"};
//数据
private Object[][] cells = {
{"Mercury", 2440.0, 0, false, Color.YELLOW},
{"Venus", 6052.0, 0, false, Color.YELLOW},
{"Earth", 6378.0, 1, false, Color.BLUE},
{"Mars", 3397.0, 2, false, Color.RED},
{"Jupiter", 71492.0, 16, true, Color.ORANGE},
{"Saturn", 60268.0, 18, true, Color.ORANGE},
{"Uranus", 25559.0, 17, true, Color.BLUE},
{"Neptune", 24766.0, 8, true, Color.BLUE},
{"Pluto", 1137.0, 1, false, Color.BLACK}
};
public PlanetTableFrame() {
JTable table = new JTable(cells, columnNames);
//点击列头,自动排序
table.setAutoCreateRowSorter(true);
//将表格包装到一个JScrollPane中,用来添加滚动条,在滚动表格时,列表头并不会滑到视图的外面
add(new JScrollPane(table), BorderLayout.CENTER);
JButton printButton = new JButton("Print");
printButton.addActionListener(event -> {
try {
table.print();
} catch (PrinterException e) {
e.printStackTrace();
}
});
JPanel buttonPanel = new JPanel();
buttonPanel.add(printButton);
add(buttonPanel, BorderLayout.SOUTH);
pack();
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
PlanetTableFrame frame = new PlanetTableFrame();
frame.setTitle("TableTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
javax.swing.JTable:
- JTable(Object[][] entries, Object[] columnNames):用默认的表格模型构建一个表格
- void print():显示打印对话框,并打印该表格
- boolean getAutoCreateRowSorter()
- void setAutoCreateRowSorter(boolean newValue):获取或设置autoCreateRowSorter属性,默认值为false。如果进行了设置,只要模型发生变化,就会自动设置一个默认的行排序器。
- boolean getFillsViewportHeight()
- void setFillsViewportHeight(boolean newValue):获取或设置fillsViewportHeight属性,默认值为false。如果进行了设置,该表格就总是会填充其外围的视图。
1.2、表格模型
在上一个示例中,表格数据是存储在一个二维数组中的。不过,通常不应该在自己的代码中使用这种策略。如果你发现自己在将数据装入一个数组中,然后作为一个表格显示出来,那么就应该考虑实现自己的表格模型了。
表格模型实现起来特别简单,因为可以充分利用AbstractTableModel类,它实现了大部分必需的方法。你仅仅需要提供下面三个方法便可:
- public int getRowCount():返回行数
- public int getColumnCount():返回列数
- public Object getValueAt(int row, int column):返回数据
实现getValueAt方法有多种途径。例如,如果想显示包含数据库查询结果的RowSet的内容,只需提供下面的方法:
public Object getValueAt(int r, int c) {
try {
rowSet.absolute(r + 1);
return rowSet.getObject(c + 1);
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
如果不提供列名,那么AbstractTableModel的getColumnName
方法会将列命名为A、B、C等。如果要改变列名,请覆盖getColumnName
方法。通常需要覆盖默认的行为。
public class InvestmentTableFrame extends JFrame {
public InvestmentTableFrame() {
InvestmentTableModel model = new InvestmentTableModel(30, 5, 10);
JTable table = new JTable(model);
add(new JScrollPane(table));
pack();
}
public static void main(String[] args) {
JFrame frame = new InvestmentTableFrame();
frame.setTitle("InvestmentTable");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
class InvestmentTableModel extends AbstractTableModel {
private static double INITIAL_BALANCE = 100000.0;
private int years;
private int minRate;
private int maxRate;
public InvestmentTableModel(int years, int minRate, int maxRate) {
this.years = years;
this.minRate = minRate;
this.maxRate = maxRate;
}
@Override
public int getRowCount() {
return years;
}
@Override
public int getColumnCount() {
return maxRate - minRate + 1;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
double rate = (columnIndex + minRate) / 100.0;
int nperiods = rowIndex;
double futureBalance = INITIAL_BALANCE * Math.pow(1 + rate, nperiods);
return String.format("%.2f", futureBalance);
}
@Override
public String getColumnName(int column) {
return (column + minRate) + "%";
}
}
javax.swing.table.TableModel:
- int getRowCount()
- int getColumnCount():获取表模型中的行和列的数量
- Object getValueAt(int row, int column):获取在给定的行和列所确定的位置的值
- void setValueAt(Object newValue, int row, int column):设置在给定的行和列所确定的位置的值
- boolean isCellEditable(int row, int column):如果在给定的行和列所确定的位置的值是可编辑的,则返回true
- String getColumnName(int column):获取列的名字
1.3、对行和列的操作
1)、各种列类
表格模型定义了如下方法,可以返回一个列类型:
Class<?> getColumnClass(int columnIndex)
JTable类会选取合适的绘制器,默认的绘制器如下:
- Boolean:复选框
- Icon:图像
- Object:字符串
2)、访问表格列
JTable类将有关表格列的信息存放在类型为TableColumn
的对象中,由一个TableColumnModel
对象负责管理这些列。如果不想动态地插入或删除,那么最好不要过多的使用表格列模型。列模型最常见的用法是直接获取一个TableColumn对象:
int columnIndex = ...;
TableColumn column = table.getColumnModel().getColumn(columnIndex);
3)、改变列的大小
TableColumn类可以控制更改列的大小的行为。使用下面这些方法,可以设置首选的、最小的以及最大的宽度:
void setPreferredWidth(int width)
void setMinWidth(int width)
void setMaxWidth(int width)
使用如下方法,可以控制是否允许用户改变列的大小:
void setResizable(boolean resizable)
可以使用如下方法在程序中改变列的大小:
void setWidth(int width)
调整一个列的大小时,默认情况下表格的总体大小会保持不变。当然,更改过大小的列的宽度的增加值或减小值会分摊到其他列上。
默认方式是更改那些在被改变了大小的列右边
的所有列的大小。这是一种很好的默认方式,因为这样使得用户可以通过将所有列从左到右移动,将它们调整为自己所期望的宽度。
使用如下方法,可以设置变更列大小的模式:
void setAutoResizeMode(int mode):
AUTO_RESIZE_OFF
:不更改其他列的大小,而是更改整个表格的宽度AUTO_RESIZE_NEXT_COLUMN
:只更改下一列的大小AUTO_RESIZE_SUBSEQUENT_COLUMNS
:均匀地更改后续列的大小,这是默认行为AUTO_RESIZE_LAST_COLUMN
:只更改最后一列的大小AUTO_RESIZE_ALL_COLUMNS
:更改表格中的所有列的大小,这并不是一种很明智的选择,因为这阻碍了用户只对数列而不是整个表进行调整以达到自己期望大小的行为
4)、改变行的大小
行的高度是直接由JTable类管理的。如果单元格比默认值高,那么可以像下面这样设置行的高度:
table.setRowHeight(height);
默认情况下,表格中的所有行都具有相同的高度,可以用下面的调用来为每一行单独设置高度:
table.setRowHeight(row,height);
实际的行高度等于用这些方法设置的行高度减去行边距,其中行边距的默认值是1个像素,但是可以通过下面的调用来修改它:
table.setRowMargin(margin);
5)、选择行、列和单元格
利用不同的选择模式,用户可以分别选择表格中的行、列或者单个的单元格。默认情况下,使能的是行选择,点击一个单元格的内部就可以选择整行。调用如下方法可以禁用行选择。
table.setRowSelectionAllowed (false);
当行选择功能可用时,可以控制用户是否可以选择单一行、连续几行或者任意几行。此时,需要获取选择模式,然后调用它的setSelectionMode方法:
table.getSelectionModel().setSelectionMode(mode);
mode取值如下:
- ListSelectionModel.SINGLE_SELECTION
- ListSelectionModel.SINGLE_INTERVAL_SELECTION
- ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
默认情况下,列选择是禁用的。不过可以调用下面这个方法启用列选择:
table.setColumnSelectionAllowed(true);
同时启用行选择和列选择等价于启用单元格选择,这样用户就可以选择一定范围内的单元格。也可以使用下面的调用完成这项设置:
table.setCellSelectionEnabled(true);
可以通过调用getSelectedRows
方法和getSelectedColumns
方法来查看选中了哪些行及哪些列。这两个方法都返回一个由被选定项的索引构成的t[]
数组。注意,这些索引值是表格视图中的索引值,而不是底层表格模型中的索引值。尝试着选择一些行和列,然后将列拖拽到不同的位置,并通过点击列头来对这些行进行排序。使用Print Selection菜单项来查看它会报告哪些行和列被选中。
如果要将表格索引值转译为表格模型索引值,可以使用JTable的ConvertRowIndexToModel
和
convertColumnIndexToModel
方法。
6)、对行排序
JTable中添加行排序机制是很容易的,只需调用setAutoCreateRowSorter
方法。但是,要对排序行为进行细粒度的控制,就必须向JTable中安装一个TableRowSorter<M>
对象,并对其进行定制化。类型参数M表示表格模型,它必须是TableModel接口的子类型:
var sorter = new TableRowSorter<TableModel>(model);
table.setRowSorter(sorter);
某些列是不可以排序的,可以通过下面的调用来关闭排序机制:
sorter.setSortable(IMAGE_COLUMN, false);
默认情况下,如果不指定列的比较器,那么排列顺序就是按照下面的原则确定的:
- 如果列所属的类是String,就使用
Collator.getInstance()
方法返回的默认比较器。它按照适用于当前locale的方式对字符串排序。 - 如果列所属的类型实现了Comparable,则使用它的compareTo方法。
- 如果已经为排序器设置过TableStringConverter,就用默认比较器对转换器的toString方法返回的字符串进行排序。如果要使用该方法,可以像下面这样定义转换器:
sorter.setstringConverter(new TableStringConverter()
{
public String toString(TableModel model,int row,int column)
{
Object value model.getvalueAt(row,column);
convert value to a string and return it
}
};
- 否则,在单元格的值上调用toString方法,然后用默认比较器对它们进行比较。
7)、过滤行
除了可以对行排序之外,TableRowSorter还可以有选择性地隐藏行,这种处理称为过滤(filtering)。要想激活过滤机制,需要设置RowFilter。
sorter.setRowFilter(RowFilter.numberFilter(ComparisonType.NOT EQUAL,0,MOONS COLUMN));
这里我们使用了预定义的过滤器,即数字过滤器。要构建数字过滤器,需要提供:
- 比较类型(
EQUAL
、NOT_EQUAL
、AFTER
和BEFORE
之一) - Number的某个子类的一个对象(例如Integer和Double),只有与给定的Number对象属于相同的类的对象才在考虑的范围内
- 0或多列的索引值,如果不提供任何索引值,那么所有的列都被搜索
静态的RowFilter.dateFilter
方法以相同的方式构建了日期过滤器,这里需要提供Date对象而不是Number对象。
最后,静态的RowFilter.regexFilter
方法构建的过滤器可以查找匹配某个正则表达式的字符串。例如:
sorter.setRowFilter(RowFilter.regexFilter(".*[s]$",PLANET COLUMN));
还可以用andFilter
、orFilter
和notFilter
方法来组合过滤器,例如:
sorter.setRowFilter(RowFilter.andFilter(List.of
RowFilter.regexFilter(".*[s]$",PLANET COLUMN),
RowFilter.numberFilter(ComparisonType.NOT EQUAL,0,MOONS COLUMN))));
要实现自己的过滤器,需要提供RowFilter的一个子类,并实现include方法来表示哪些行应该显示。这很容易实现,但是RowFilter类卓越的普适性令它有点可怕。
RowF1lter<M,I>
类有两个类型参数:模型的类型和行标识符的类型。在处理表格时,模型总是TableModel的某个子类型,而标识符类型总是Integer。(在将来的某个时刻,其他构件可能也会支持行过滤机制。例如,要过滤JTree中的行,就可能可以使用RowFilter <TreeModel, TreePath>
了。)
行过滤器必须实现下面的方法:
public boolean include(RowFilter.Entry<?extends M,extends I>entry)
RowFilter.Entry类提供了获取模型、行标识符和给定索引处的值等内容的方法,因此,按照行标识符和行的内容都可以进行过滤。
8)、隐藏和显示列
JTable类的removeColumn
方法可以将一列从表格视图中移除。该列的数据实际上并没有从模型中移除,它们只是在视图中被隐藏了起来。removeColumn方法接受一个TableColumn参数,如果你有的是一个列号(比如来自getSelectedColumns的调用结果),那就需要向表格模型请求实际的列对象:
TableColumnModel columnModel = table.getColumnModel();
TableColumn column = columnModel.getColumn(i)
table.removeColumn(column);
如果你记得住该列,那么将来就可以再把它添加回去:
table.addColumn(column);
该方法将该列添加到表格的最后面。如果想让它出现在表格中的其他任何地方,那么可以调用moveColumn方法。
通过添加一个新的TableColumn对象,还可以添加一个对应于表格模型中的一个列索引的新列:
table.addColumn(new TableColumn(modelColumnIndex));
可以让多个表格列展示模型中的同一列。
9)、示例
public class PlanetTableFrame2 extends JFrame {
private static final int DEFAULT_WIDTH = 600;
private static final int DEFAULT_HEIGHT = 500;
public static final int COLOR_COLUMN = 4;
public static final int IMAGE_COLUMN = 5;
private JTable table;
private HashSet<Integer> removedRowIndices;
private ArrayList<TableColumn> removedColumns;
private JCheckBoxMenuItem rowsItem;
private JCheckBoxMenuItem columnsItem;
private JCheckBoxMenuItem cellsItem;
private String[] columnNames = {"Planet", "Radius", "Moons", "Gaseous", "Color", "Image"};
private Object[][] cells = {
{"Mercury", 2440.0, 0, false, Color.YELLOW, new ImageIcon("icon.png")},
{"Venus", 6052.0, 0, false, Color.YELLOW, new ImageIcon("icon.png")},
{"Earth", 6378.0, 1, false, Color.BLUE, new ImageIcon("icon.png")},
{"Mars", 3397.0, 2, false, Color.RED, new ImageIcon("icon.png")},
{"Jupiter", 71492.0, 16, true, Color.ORANGE, new ImageIcon("icon.png")},
{"Saturn", 60268.0, 18, true, Color.ORANGE, new ImageIcon("icon.png")},
{"Uranus", 25559.0, 17, true, Color.BLUE, new ImageIcon("icon.png")},
{"Neptune", 24766.0, 8, true, Color.BLUE, new ImageIcon("icon.png")},
{"Pluto", 1137.0, 1, false, Color.BLACK, new ImageIcon("icon.png")}
};
public PlanetTableFrame2() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
DefaultTableModel model = new DefaultTableModel(cells, columnNames) {
@Override
public Class<?> getColumnClass(int columnIndex) {
return cells[0][columnIndex].getClass();
}
};
table = new JTable(model);
table.setRowHeight(100);
table.getColumnModel().getColumn(COLOR_COLUMN).setMinWidth(250);
table.getColumnModel().getColumn(IMAGE_COLUMN).setMinWidth(100);
TableRowSorter<TableModel> sorter = new TableRowSorter<>(model);
table.setRowSorter(sorter);
sorter.setComparator(COLOR_COLUMN,
Comparator.comparing(Color::getBlue)
.thenComparing(Color::getGreen)
.thenComparing(Color::getRed));
sorter.setSortable(IMAGE_COLUMN, false);
add(new JScrollPane(table), BorderLayout.CENTER);
removedRowIndices = new HashSet<>();
removedColumns = new ArrayList<>();
RowFilter<TableModel, Integer> filter = new RowFilter<TableModel, Integer>() {
@Override
public boolean include(Entry<? extends TableModel, ? extends Integer> entry) {
return !removedRowIndices.contains(entry.getIdentifier());
}
};
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu selectionMenu = new JMenu("Selection");
menuBar.add(selectionMenu);
rowsItem = new JCheckBoxMenuItem("Rows");
columnsItem = new JCheckBoxMenuItem("Columns");
cellsItem = new JCheckBoxMenuItem("Cells");
rowsItem.setSelected(table.getRowSelectionAllowed());
columnsItem.setSelected(table.getColumnSelectionAllowed());
cellsItem.setSelected(table.getCellSelectionEnabled());
rowsItem.addActionListener(event -> {
table.clearSelection();
table.setRowSelectionAllowed(rowsItem.isSelected());
updateCheckboxMenuItems();
});
selectionMenu.add(columnsItem);
cellsItem.addActionListener(event -> {
table.clearSelection();
table.setCellSelectionEnabled(cellsItem.isSelected());
updateCheckboxMenuItems();
});
selectionMenu.add(cellsItem);
JMenu tableMenu = new JMenu("Edit");
menuBar.add(tableMenu);
JMenuItem hideColumnsItem = new JMenuItem("Hide Columns");
hideColumnsItem.addActionListener(event -> {
int[] selected = table.getSelectedColumns();
TableColumnModel columnModel = table.getColumnModel();
for (int i = selected.length - 1; i >= 0; i--) {
TableColumn column = columnModel.getColumn(selected[i]);
table.removeColumn(column);
removedColumns.add(column);
}
});
tableMenu.add(hideColumnsItem);
JMenuItem showColumnsItem = new JMenuItem("Show Columns");
showColumnsItem.addActionListener(event -> {
for (TableColumn tc : removedColumns) {
table.addColumn(tc);
}
removedColumns.clear();
});
tableMenu.add(showColumnsItem);
JMenuItem hideRowsItem = new JMenuItem("Hide Rows");
hideColumnsItem.addActionListener(event -> {
int[] selected = table.getSelectedRows();
for (int i : selected) {
removedRowIndices.add(table.convertColumnIndexToModel(i));
}
sorter.setRowFilter(filter);
});
tableMenu.add(hideRowsItem);
JMenuItem printSelectionItem = new JMenuItem("Print Selection");
printSelectionItem.addActionListener(event -> {
int[] selected = table.getSelectedRows();
System.out.println("Selected rows:" + Arrays.toString(selected));
selected = table.getSelectedColumns();
System.out.println("Selected columns:" + Arrays.toString(selected));
});
tableMenu.add(printSelectionItem);
}
private void updateCheckboxMenuItems() {
rowsItem.setSelected(table.getRowSelectionAllowed());
columnsItem.setSelected(table.getColumnSelectionAllowed());
cellsItem.setSelected(table.getCellSelectionEnabled());
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
PlanetTableFrame2 frame = new PlanetTableFrame2();
frame.setTitle("TableTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
10)、API
javax.swing.table.TableModel:
- Class getColumnClass(int columnIndex):获取该列中的值的类。该信息用于排序或绘制
javax.swing.JTable:
- TableColumnModel getColumnModel():获取描述表格列布局安排的列模式
- void setAutoResizeMode(int mode):设置自动更改表格列大小的模式
- int getRowHeight()
- void setRowMargin(int margin):获取和设置相邻行中单元格之间的间隔大小
- int getRowHeight()
- void setRowHeight(int height):获取和设置表格中所有行的默认高度
- int getRowHeight(int row)
- void setRowHeight(int row, int height):获取和设置表格中给定行的高度
- ListSelectionModel getSelectionModel():返回列表的选择模式。你需要该模式以便在行、列以及单元格之间进行选择
- boolean getRowSelectionAllowed()
- void setRowSelectionAllowed(boolean b):获取和设置rowSelectionAllowed属性。如果为true,那么当用户点击单元格的时候,可以选定行
- boolean getColumnSelectionAllowed()*
- void setColumnSelectionAllowed(boolean b):获取和设置columnSelectionAllowed属性。如果为true,那么当用户点击单元格的时候,可以选定列
- boolean getCellSelectionEnabled():如果既允许选定行又允许选定列,则返回true
- void setCellSelectionEnabled(boolean b):同时将rowSelectionAllowed和columnSelectionAllowed设置为b
- void addColumn(TableColumn column):向表格视图中添加一列作为最后一列
- void moveColumn(int from, int to):移动表格from索引位置中的列,使它的索引编程to。该操作仅仅影响到视图
- void removeColumn(TableColumn column):将给定的列从视图中移除
- int convertRowIndexToModel(int index)
- int convertColumnIndexToModel(int index):返回具有给定索引的行或列的模型索引,这个值与行被排序和过滤,以及列被移动和移除时的索引不同
- void setRowSorter(RowSorter< ? extents TableModel> sorter):设置排序器
javax.swing.table.TableColumnModel:
- TableColumn getColumn(int index):获取表格的列对象,用于描述给定索引的列
javax.swing.table.TableColumn:
- TableColumn(int modelColumnIndex):构建一个表格列,用以显示给定索引位置上的模型列
- void setPreferredWidth(int width)
- void setMinWidth(int width)
- void setMaxWidth(int width):将表格的首选宽度、最小宽度以及最大宽度设置为width
- void setWidth(int width):设置该列的实际宽度为width
- void setResizable(boolean b):如果b为true,那么该列可以更改大小
javax.swing.ListSelectionModel:
- void setSelectionMode(int mode):
SINGLE_SELECTION
SINGLE_INTERVAL_SELECTION
MULTIPLE_INTERVAL_SELECTION
之一。
javax.swing.DefaultRowSorter<M, I>:
- void setComparator(int column, Comparator<?> comparator):设置用于给定列的比较器
- void setSortable(int column, boolean enabled):使对给定列的排序可用或禁用
- void setRowFilter(RowFilter<? super M, ? super I> filter):设置行过滤器
javax.swing.table.TableRowSorter<M extends TableModel>:
- abstract String toString(TableModel model, int row, int column):将给定位置的模型值转换为字符串
javax.swing.RowFilter<M, I>:
- boolean include(RowFilter.Entry<? extends M, ? extends I> entry):指定要保留的行
- static <M, I> RowFilter<M, I> numberFilter(RowFilter.ComparisonType type, Number number, int… indices)
- static <M, I> RowFilter<M, I> dateFilter(RowFilter.ComparisonType type, Date date, int… indices):返回一个过滤器,它包含的行是那些与给定的数字或日期进行给定比较后匹配的行。比较类型是EQUAL、NOT_EQUAL、AFTER或BEFORE之一。如果给定了列模型索引,则只搜索这些列。否则,将搜索所有列。对于数字过滤器,单元格的值所属的类必须与给定数字的类匹配。
- static <M, I> RowFilter<M, I> regexFilter(String regex, int… indices):返回一个过滤器,它包含的行含有与给定的正则表达式匹配的字符串。如果给定了列模型索引,则只搜索这些列。否则,将搜索所有列。注意,RowFilter.Entry的getStringValue方法返回的字符串是匹配的。
- static <M, I> RowFilter<M, I> andFilter(Iterable<? extends RowFilter<? super M, ? super I>> filters)
- static <M, I> RowFilter<M, I> orFilter(Iterable<? extends RowFIlter<? super M, ? super I>> filters):返回一个过滤器,它包含的项是那些包含在所有的过滤器或至少包含在一个过滤器中的项
- **static <M, I> RowFilter<M, I> notFilter(RowFilter<M, I> filter):返回一个过滤器,它包含的项是那些不包含在给定过滤器中的项
javax.swing.RowFilter.Entry<M, I>:
- I getIdentifier():返回这个行的标识符。
- M getModel():返回这个行的模型。
- Object getValue(int index):返回在这个行的给定索引处存储的值。
- int getValueCount():返回在这个行中存储的值的数量。
- String getStringValue():返回在这个行的给定索引处存储的值转换成的字符串。由TableRowSorter产生的项的getStringValue方法会调用排序器的字符串转换器。
1.4、单元格的绘制和编辑
1)、绘制单元格
表格的单元格绘制器与你在前面看到的列表单元格绘制器类似。它们都实现了TableCellRenderer接口,并只有一个方法:
Component getTableCellRendererComponent(JTable table,Object value,boolean isSelected,boolean hasFocus,int row,int column)
该方法在表格需要绘制单元格的时候被调用。它会返回一个构件,接着该构件的pait方法会被调用,以填充单元格区域。
2)、绘制表头
为了在表头中显示图标,需要设置表头值。
moonColumn.setHeaderValue(new ImageIcon("Moons.gif"))
然而,表头还未智能到可以为表头值选择一个合适的绘制器,因此,绘制器需要手工安装。例如,要在列头显示图像图标,可以调用:
moonColumn.setHeaderRenderer(table.getDefaultRenderer(ImageIcon.class));
3)、单元格编辑
为了使单元格可编辑,表格模型必须通过定义isCellEditable
方法来指明哪些单元格是可编辑的。最常见的情况是,你可能想使某几列可编辑。
在这个示例程序中,我们允许对表格中的四列进行编辑:
public boolean isCellEditable(int r,int c)
{
return c == PLANET_COLUMN || c == MOONS_COLUMN || c == GASEOUS_COLUMN || c ==COLOR_COLUMN;
}
DefaultCellEditor可以用JTextField
、JCheckBox
或者JComboBox
来构造。JTable类会自动为Boolean类型的单元格安装一个复选框编辑器,并为所有可编辑但未提供它们自己的绘制器的单元格安装一个文本编辑器。文本框可以让用户去编辑那些对表格模型getValueAt方法的返回值执行toString操作而产生的字符串。
一旦编辑完成,通过调用编辑器的getCellEditorValue
方法就可以读取编辑过的值。该方法应该返回一个正确类型的值(也就是模型的getColumnType方法返回的类型)。
4)、定制编辑器
为了创建一个定制的单元格编辑器,需要实现TableCellEditor
接口。这个接口有点拖沓冗长,从Java SE1.3开始,提供了AbstractCellEditor
类,用于负责事件处理的细节。
public class TableCellRenderFrame extends JFrame {
private static final int DEFAULT_WIDTH = 600;
private static final int DEFAULT_HEIGHT = 400;
public TableCellRenderFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
PlanetTableModel model = new PlanetTableModel();
JTable table = new JTable(model);
table.setRowSelectionAllowed(false);
table.setDefaultRenderer(Color.class, new ColorTableCellRenderer());
table.setDefaultEditor(Color.class, new ColorTableCellEditor());
JComboBox moonCombo = new JComboBox<Integer>();
for (int i = 0; i <= 20; i++) {
moonCombo.addItem(i);
}
TableColumnModel columnModel = table.getColumnModel();
TableColumn moonColumn = columnModel.getColumn(PlanetTableModel.MOONS_COLUMN);
moonColumn.setCellEditor(new DefaultCellEditor(moonCombo));
moonColumn.setHeaderRenderer(table.getDefaultRenderer(ImageIcon.class));
moonColumn.setHeaderValue(new ImageIcon("icon.png"));
table.setRowHeight(100);
add(new JScrollPane(table), BorderLayout.CENTER);
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new TableCellRenderFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setTitle("CellRender");
frame.setVisible(true);
});
}
}
public class PlanetTableModel extends AbstractTableModel {
public static final int PLANET_COLUMN = 0;
public static final int MOONS_COLUMN = 2;
public static final int GASEOUS_COLUMN = 3;
public static final int COLOR_COLUMN = 4;
private String[] columnNames = {"Planet", "Radius", "Moons", "Gaseous", "Color", "Image"};
private Object[][] cells = {
{"Mercury", 2440.0, 0, false, Color.YELLOW, new ImageIcon("icon.png")},
{"Venus", 6052.0, 0, false, Color.YELLOW, new ImageIcon("icon.png")},
{"Earth", 6378.0, 1, false, Color.BLUE, new ImageIcon("icon.png")},
{"Mars", 3397.0, 2, false, Color.RED, new ImageIcon("icon.png")},
{"Jupiter", 71492.0, 16, true, Color.ORANGE, new ImageIcon("icon.png")},
{"Saturn", 60268.0, 18, true, Color.ORANGE, new ImageIcon("icon.png")},
{"Uranus", 25559.0, 17, true, Color.BLUE, new ImageIcon("icon.png")},
{"Neptune", 24766.0, 8, true, Color.BLUE, new ImageIcon("icon.png")},
{"Pluto", 1137.0, 1, false, Color.BLACK, new ImageIcon("icon.png")}
};
@Override
public String getColumnName(int column) {
return columnNames[column];
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return cells[0][columnIndex].getClass();
}
@Override
public int getRowCount() {
return cells.length;
}
@Override
public int getColumnCount() {
return cells[0].length;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
return cells[rowIndex][columnIndex];
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
cells[rowIndex][columnIndex] = aValue;
}
@Override
public boolean isCellEditable(int r, int c) {
return c == PLANET_COLUMN || c == MOONS_COLUMN || c == GASEOUS_COLUMN || c == COLOR_COLUMN;
}
}
public class ColorTableCellRenderer extends JPanel implements TableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
setBackground((Color)value);
if (hasFocus) {
setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
} else {
setBorder(null);
}
return this;
}
}
public class ColorTableCellEditor extends AbstractCellEditor implements TableCellEditor {
private JColorChooser colorChooser;
private JDialog colorDialog;
private JPanel panel;
public ColorTableCellEditor() {
panel = new JPanel();
colorChooser = new JColorChooser();
colorDialog = JColorChooser.createDialog(null, "Planet Color", false, colorChooser,
EventHandler.create(ActionListener.class, this, "stopCellEditing"),
EventHandler.create(ActionListener.class, this, "cancelCellEditing"));
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
colorChooser.setColor((Color)value);
return panel;
}
@Override
public boolean shouldSelectCell(EventObject anEvent) {
colorDialog.setVisible(true);
return true;
}
@Override
public void cancelCellEditing() {
colorDialog.setVisible(false);
super.cancelCellEditing();
}
@Override
public boolean stopCellEditing() {
colorDialog.setVisible(false);
super.stopCellEditing();
return true;
}
@Override
public Object getCellEditorValue() {
return colorChooser.getColor();
}
}
5)、API
javax.swing.JTable:
- TableCellRenderer getDefaultRenderer(Class<?> type):获取给定类型的默认绘制器
- TableCellEditor getDefaultEditor(Class<?> type):获取给定类型的默认编辑器
javax.swing.table.TableCellRenderer:
- Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column):返回一个构件,它的paint方法将被调用以便绘制一个单元格
- table:该表格包含要绘制的单元格
- value:要绘制的单元格
- selected:如果该单元格当前已被选中,则为true
- hasFocus:如果该单元格当前具有焦点,则为true
- row, column:单元格的行及列
javax.swing.table.TableColumn:
- void setCellEditor(TableCellEditor editor)
- void setCellRenderer(TableCellRenderer renderer):为该列中的所有单元格设置单元格编辑器或绘制器
- void setHeaderRenderer(TableCellRenderer renderer):为该列中的所有表头单元格设置单元格绘制器
- void setHeaderValue(Object value):为该列中的表头设置用于显示的值
javax.swing.DefaultCellEditor:
- DefaultCellEditor(JComboBox comboBox):构建一个单元格编辑器,并以一个组合框的形式显示出来,用于选择单元格的值。
javax.swing.table.TableCellEditor:
- Component getTableCellEditorComponent(JTable table, Object value, boolean selected, int row, int column):返回一个构件,它的paint方法用于绘制表格的单元格
javax.swing.CellEditor:
- boolean isCellEditable(EventObject event):如果该事件能够启动对该单元格的编辑过程,那么返回true
- boolean shouldSelectCell(EventObject anEvent):启动编辑过程。如果被编辑的单元格应该被选中,则返回tue。通常情况下,你希望返回的是tue,不过,如果你不希望在编辑过程中改变单元格被选中的情况,那么你可以返回false
- void cancelCellEditing():取消编辑过程,可以放弃已进行了部分编辑对的操作
- boolean stopCellEditing():出于使用编辑结果的目的,停止编辑过程。如果被编辑的值对读取来说处于适合的状态,则返回true
- Object getCellEditorValue():返回编辑结果
- void addCellEditorListener(CellEditorListener l)
- void removeCellEditorListener(CellEditorListener l):添加或移除必需的单元格编辑器的监听器
2、树
JTree类(以及它的辅助类)负责布局树状结构,按照用户请求展开或折叠树的节点。
一棵树由一些节点(node)组成。每个节点要么是叶节点(leaf)要么是有孩子节点(child node)的节点。除了根节点(root node),每一个节点都有一个唯一的父节,点(parent node)。一棵树只有一个根节点。有时,你可能有一个树的集合,其中每棵树都有自己的根节点。这样的集合称作森林(forest)。
2.1、简单的树
如同大多数Swig构件一样,只要提供一个数据模型,构件就可以将它显示出来。为了构建JTree,需要在构造器中提供这样一个树模型:
TreeModel model = ...;
JTree tree = new JTree(model);
可以通过创建一个实现了TreeModel
接口的类来构建自己的树模型。Swing类库提供了DefaultTreeModel
模型。
为了构建一个默认的树模型,必须提供一个根节点。
TreeNode root =...;
var model = new DefaultTreeModel(root);
TreeNode是另外一个接口。可以将任何实现了这个接口的类的对象组装到默认的树模型中。这里,我们使用的是Swing提供的具体节点类,叫做DefaultMutableTreeNode
。这个类实现了MutableTreeNode
接口,该接口是TreeNode
的一个子接口。
任何一个默认的可变树节点都存放着一个对象,即用户对象(user object)。树会为所有的节点绘制这些用户对象。除非指定一个绘制器,否则树将直接显示执行完toString方法之后的结果字符串。
可以在构造器中设定用户对象,也可以稍后在setUserO bject方法中设定用户对象:
var node = new DefaultMutableTreeNode("Texas");
...
node.setUserobject("California");
接下来,可以建立节点之间的父/子关系。从根节点开始,使用add方法来添加子节点:
DefaultMutableTreeNode root = new DefaultMutableTreeNode("World");
DefaultMutableTreeNode country = new DefaultMutableTreeNode("USA");
root.add(country);
按照这种方式将所有的节点链接起来。然后用根节点构建一个DefaultTreeModel.。最后,用这个树模型构建一
个JTree
var treeModel = new DefaultTreeModel(root);
var tree = new JTree(treeModel);
或者,使用快捷方式,直接将根节点传递给JTree构造器。那么这棵树就会自动构建一个默认的树模型:
var tree = new JTree(root);
1)、示例
public class SimpleTreeFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
public SimpleTreeFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
DefaultMutableTreeNode root = new DefaultMutableTreeNode("World");
DefaultMutableTreeNode country = new DefaultMutableTreeNode("USA");
root.add(country);
DefaultMutableTreeNode state = new DefaultMutableTreeNode("California");
country.add(state);
DefaultMutableTreeNode city = new DefaultMutableTreeNode("San Jose");
state.add(city);
city = new DefaultMutableTreeNode("Cupertino");
state.add(city);
state = new DefaultMutableTreeNode("Michigan");
country.add(state);
city = new DefaultMutableTreeNode("Ann Arbor");
state.add(city);
country = new DefaultMutableTreeNode("Germany");
root.add(country);
state = new DefaultMutableTreeNode("Schleswig-Holstein");
country.add(state);
city = new DefaultMutableTreeNode("Kiel");
state.add(city);
JTree tree = new JTree(root);
add(new JScrollPane(tree));
}
public static void main(String[] args) {
JFrame frame = new SimpleTreeFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setTitle("SimpleTree");
frame.setVisible(true);
}
}
可以使用下面这句神奇的代码取消父子节点之间的连接线:
tree.putclientProperty("JTree.lineStyle","None");
相反地,如果要确保显示这些线条,则可以使用:
tree.putClientProperty("JTree.lineStyle","Angled");
可以使用下面的代码将根节点隐藏起来:
tree.setRootVidible(false);
在显示这棵树的时候,每个节点都绘有一个图标。实际上一共有三种图标:叶节点图标
、展开的非叶节点图标
以及闭合的非叶节点图标
。为了简化起见,我们将后面两种图标称
为文件夹图标。
节点绘制器必须知道每个节点要使用什么样的图标。默认情况下,这个决策过程是这样的:如果某个节点的isLeaf
方法返回的是true,那么就使用叶节点图标,否则,使用文件夹图标。
如果某个节点没有任何儿子节点,那么DefaultMutableTreeNode类的isLeaf方法将返回tue。因此,具有儿子节点的节点使用文件夹图标,没有儿子节点的节点使用叶节点图标。
JTree类无法知道哪些节点是叶节点,它要询问树模型。如果一个没有任何子节点的节点不应该自动地被设置为概念上的叶节点,那么可以让树模型对这些叶节点使用一个不同的标准,即可以查询其“允许有子节点”的节点属性。
对于那些不应该有子节点的节点,调用
node.setAllowsChildren(false);
然后,告诉树模型去查询“允许有子节点”的属性值以确定一个节点是否应该显示成叶子图标。可以使用DefaultTreeModel类中的方法setAsksAllowsChildren设定此动作:
model.setAsksAllowsChildren(true);
有了这个判定规则,允许有子节点的节点就可以获得文件夹图标,而不允许有子节点的节点将获得叶子图标。
另外,如果是通过提供根节点来构建一棵树的,那么请在构造器中直接提供“询问允许有子节点”属性值的设置。
var tree new JTree(root,true);
2)、编辑树和树的路径
TreePath类管理着一个Object(不是TreeNode!)引用序列。有很多JTree的方法都可以返回TreePath对象。当拥有一个树路径时,通常只需要知道其终端节点,该节点可以通过getLastPathComponent方法得到。例如,如果要查找一棵树中当前选定的节点,可以使用JTree类中的getSelectionPath方法。它将返回一个TreePath对象,根据这个对象就可以检索实际节点。
TreePath selectionPath = tree.getSelectionPath();
var selectedNode (DefaultMutableTreeNode)selectionPath.getLastPathComponent();
实际上,由于这种特定查询经常被使用到,因此还提供了一个更方便的方法,它能够立即给出选定的节点
var selectedNode = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();
该方法之所以没有被称为getSelectedNode,是因为这棵树并不了解它包含的节点,它的树模型只处理对象的路径。
一旦选定了某个节点,那么就可以对它进行编辑了。不过,不能直接向树节点添加子节点:
selectedNode.add(newNode);/No!
如果改变了节点的结构,那么改变的只是树模型,而相关的视图却没有被通知到。可以自己发送一个通知消息,但是如果使用DefaultTreeModel类的insertNodeInto方法,那么该模型类会全权负责这件事情。例如,下面的调用可以将一个新节点作为选定节点的最后子节点添加到树中,并通知树的视图。
model.insertNodeInto(newNode,selectedNode,selectedNode.getChildCount());
类似的调用removeNodeFromParent可以移除一个节点并通知树的视图:
model.removeNodeFromParent(selectedNode);
如果想保持节点结构,但是要改变用户对象,那么可以调用下面这个方法:
model.nodeChanged(changedNode);
自动通知是使用DefaultTreeModel的主要优势。如果你提供自己的树模型,那么必须自己动手实现这种自动通知。
当视图接收到节点结构被改变的通知时,它会更新显示树的视图,但是不会自动展开某个节点以展现新添加的子节点。特别是在我们上面那个示例程序中,如果用户将一个新节点添加到其子节点正处于折叠状态的节点上,那么这个新添加的节点就被悄无声息地添加到了一个处于折叠状态的子树中,这就没有给用户提供任何反馈信息以告诉用户已经执行了该命令。在这种情况下,你可能需要特别费劲地展开所有的父节点,以便让新添加的节点成为可视节点。可以使用类JTree中的方法makeVisible实现这个目的。makeVisible方法将接受一个树路径作为参数,该树路径指向应该变为可视的节点。
因此,需要构建一个从根节点到新添加节点的树路径。为了获得一个这样的树路径,首先要调用DefaultTreeModel类中的getPathToRoot方法,它返回一个包含了某一节点到根节点之间所有节点的数组TreeNode[]。可以将这个数组传递给一个TreePath构造器。
public class TreeEditFrame extends JFrame {
private static final int DEFAULT_WIDTH = 400;
private static final int DEFAULT_HEIGHT = 200;
private DefaultTreeModel model;
private JTree tree;
public TreeEditFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
TreeNode root = makeSampleTree();
model = new DefaultTreeModel(root);
tree = new JTree(model);
tree.setEditable(true);
JScrollPane scrollPane = new JScrollPane(tree);
add(scrollPane, BorderLayout.CENTER);
makeButtons();
}
public TreeNode makeSampleTree() {
DefaultMutableTreeNode root = new DefaultMutableTreeNode("World");
DefaultMutableTreeNode country = new DefaultMutableTreeNode("USA");
root.add(country);
DefaultMutableTreeNode state = new DefaultMutableTreeNode("California");
country.add(state);
DefaultMutableTreeNode city = new DefaultMutableTreeNode("San Jose");
state.add(city);
city = new DefaultMutableTreeNode("San Diego");
state.add(city);
state = new DefaultMutableTreeNode("Michigan");
country.add(state);
city = new DefaultMutableTreeNode("Ann Arbor");
state.add(city);
country = new DefaultMutableTreeNode("Germany");
root.add(country);
state = new DefaultMutableTreeNode("Schleswig-Holstein");
country.add(state);
city = new DefaultMutableTreeNode("Kiel");
state.add(city);
return root;
}
public void makeButtons() {
JPanel panel = new JPanel();
JButton addSiblingButton = new JButton("Add Sibling");
addSiblingButton.addActionListener(event -> {
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();
if (selectedNode == null) {
return;
}
DefaultMutableTreeNode parent = (DefaultMutableTreeNode)selectedNode.getParent();
if (parent == null) {
return;
}
DefaultMutableTreeNode newNode = new DefaultMutableTreeNode("New");
int selectIndex = parent.getIndex(selectedNode);
model.insertNodeInto(newNode, parent, selectIndex + 1);
TreeNode[] nodes = model.getPathToRoot(newNode);
TreePath path = new TreePath(nodes);
tree.scrollPathToVisible(path);
});
panel.add(addSiblingButton);
JButton addChildButton = new JButton("Add Child");
addChildButton.addActionListener(event -> {
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();
if (selectedNode == null) {
return;
}
DefaultMutableTreeNode newNode = new DefaultMutableTreeNode("New");
model.insertNodeInto(newNode, selectedNode, selectedNode.getChildCount());
TreeNode[] nodes = model.getPathToRoot(newNode);
TreePath path = new TreePath(nodes);
tree.scrollPathToVisible(path);
});
panel.add(addChildButton);
JButton deleteButton = new JButton("Delete");
deleteButton.addActionListener(event -> {
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();
if (selectedNode != null && selectedNode.getParent() != null) {
model.removeNodeFromParent(selectedNode);
}
});
panel.add(deleteButton);
add(panel, BorderLayout.SOUTH);
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new TreeEditFrame();
frame.setTitle("TreeEdit");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
3)、API
javax.swing.JTree:
- JTree(TreeModel model):根据一个树模型构造一棵树
- JTree(TreeNode root)
- JTree(TreeNode root, boolean asksAllowChildren):使用默认的树模型构造一棵树,显示根节点和它的子节点
- void setShowsRootHandles(boolean b):如果b为true,则根节点具有折叠或展开它的子节点的把手图标
- void setRootVisible(boolean b):如果b为true,则显示根节点,否则隐藏根节点
- TreePath getSelectionPath():获取到当前选定节点的路径,如果选定多个节点,则获取到第一个选定节点的路径。如果没有选定任何节点,则返回ulL。
- Object getLastSelectedPathComponent():获取表示当前选定节点的节点对象,如果选定多个节点,则获取第一个选定的节点。如果没有选定任何节点,则返回null。
- void makeVisible(TreePath path):展开该路径中的所有节点
- void scrollPathToVisible(TreePath path):展开该路径中的所有节点,如果这棵树是置于滚动面板中的,则滚动以确保该路径中的最后一个节点是可见的。
javax.swing.tree.TreePath:
- Object getLastPathComponent():获取该路径中最后一个节点,也就该路径代表的节点对象
javax.swing.tree.TreeNode:
- boolean isLeaf():如果该节点是一个概念上的叶节点,则返回true
- boolean getAllowsChildren():如果该节点可以拥有子节点,则返回true
- TreeNode getParent():返回该节点的父节点
- TreeNode getChildAt(int index):查找给定索引号上的子节点。该索引号必须在0和getChildCount()-1之间
- int getChildCount():返回该节点的子节点个数
- Enumeration children():返回一个枚举对象,可以迭代遍历该节点的所有子节点
javax.swing.tree.MutableTreeNode:
- void setUserObject(Object userObject):设置树节点用于绘制的用户对象
javax.swing.tree.TreeModel:
- boolean isLeaf(Object node):如果该节点应该以叶节点的形式显示,则返回true
javax.swing.tree.DefaultTreeModel:
- void setAsksAllowsChildren(boolean b):如果b为true,那么当节点的getAllowsChildren方法返回false时,这些节点显示为叶节点。否则,当节点的isLeaf方法返回true时,它们显示为叶节点。
- void insertNodeInto(MutableTreeNode new Child, MutableTreeNode parent, int index):将newChild作为parent的新子节点添加到给定的索引位置上,并通知树模型的监听器
- void removeNodeFromParent(MutableTreeNode node):将节点node从该模型中删除,并通知树模型的监听器
- void nodeChanged(TreeNode node):通知树模型的监听器,节点node发生了改变
- void nodesChanged(TreeNode parent, int[] changedChildIndexes):通知树模型的监听器:节点parent所有在给定索引位置上的子节点发生了改变
- void reload():将所有节点重新载入到树模型中。这是一项动作剧烈的操作,只有当由于一些外部作用,导致树的节点完全改变时,才应该使用该方法。
javax.swing.tree.DefaaultMutableTreeNode:
- DefaultMutableTreeNode(Object userObject):用给定的用户对象构建一个可变树节点
- void add(MutableTreeNode child):将一个节点添加为该节点最后一个子节点
- void setAllowsChildren(boolean b):如果b为true,则可以向该节点添加子节点
javax.swing.JComponent:
- void putClientProperty(Object key, Object value):将一个键/值对添加到一个小表格中,每一个构件都管理着这样的一个小表格。这是一种“紧急逃生”机制,很多Swing构件用它来存放与外观相关的属性。
2.2、节点枚举
有时为了查找树中一个节点,必须从根节点开始,遍历所有子节点直到找到相匹配的节点。DefaultMutableTreeNode类有几个很方便的方法用于迭代遍历所有节点。
breadthFirstEnumeration
方法和depthFirstEnumeration
方法分别使用广度优先或深度优先的遍历方式,返回枚举对象,它们的nextElement
方法能够访问当前节点的所有子节点。下图显示了对示例树进行遍历的情况,节点标签则指示遍历节点时的先后次序。
按照广度优先的方式进行枚举是最容易可视化的。树是以层的形式遍历的,首先访问根节点,然后是它的所有子节点,接着是它的孙子节点,依此类推。
为了可视化深度优先的枚举,让我们想象一只老鼠陷入一个树状陷阱的情形。它沿着第一条路径迅速爬行,直到到达一个叶节点位置。然后,原路返回并转人下一条路径,依此类推。
计算机科学家也将其称为后序遍历(postorder traversal),因为整个查找过程是先访问到子节点,然后才访问到父节点。postOrderTraversal
方法是depthFirstTraversal
的同义语。为了完整性,还存在一个preorderTraversal
方法,它也是一种深度优先搜索方法,但是它首先枚举父节点,然后是子节点。下面是一种典型的使用模式:
Enumeration breadthFirst = node.breadthFirstEnumeration();
while (breadthFirst.hasMoreElements()) {
do something with breadthFirst.nextElement();
}
最后,还有一个相关方法pathFromAncestorEnumeration
,用于查找一条从祖先节点到给定节点之间的路径,然后枚举出该路径中的所有节点。整个过程并不需要大量的处理操作,只需要不断调用getParent直到发现祖先节点,然后将该路径倒置过来存放即可。
2.3、绘制节点
在应用中可能会经常需要改变树构件绘制节点的方式,最常见的改变当然是为节点和叶节点选取不同的图标,其他一些改变可能涉及节点标签的字体或节点上的图像绘制等方面。
所有这些改变都可以通过向树中安装一个新的树单元格绘制器来实现。在默认情况下,JTree类使用DefaultTreeCellRenderer对象来绘制每个节点。DefaultTreeCellRenderer类继承自JLabel类,该标签包含节点图标和节点标签。
可以通过以下三种方式定制显示外观:
- 可以使用DefaultTreeCellRenderer改变图标、字体以及背景颜色。这些设置适用于树中所有节点。
- 可以安装一个继承了DefaultTreeCellRenderer类的绘制器,用于改变每个节点的图标、字体以及背景颜色。
- 可以安装一个实现了TreeCellRenderer接口的绘制器,为每个节点绘制自定义的图像。
最简单的定制方法是构建一个DefaultTreeCellRenderer对象,改变图标,然后将它安装到树中:
var renderer = new DefaultTreeCellRenderer();
renderer.setLeafIcon(new ImageIcon("blue-ball.gif"));//used for leaf nodes
renderer.setClosedIcon(new ImageIcon("red-ball.gif"));/used for collapsed nodes
renderer.setopenIcon(new ImageIcon("yellow-ball.gif"));//used for expanded nodes
tree.setCellRenderer(renderer);
javax.swing.tree.DefaultMutableTreeNode:
- Enumeration breadthFirstEnumeration()
- Enumeration depthFirstEnumeration()
- Enumeration preOrderEnumeration()
- Enumeration postOrderEnumeration():返回枚举对象,用于按照某种特定顺序访问树模型中的所有节点。在广度优先遍历中,先访问离根节点更近的子节点,再访问那些离根节点远的节点。在深度优先遍历中,先访问一个节点的所有子节点,然后再访问它的兄弟节点。post0 rderEnumeration方法与depthFirstEnumeration基本上相似。除了先访问父节点,后访问子节点之外,先序遍历和后序遍历基本上一样。
javax.swing.tree.TreeCellRenderer:
- Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus):返回一个paint方法被调用的构件,以便绘制树的一个单元格
javax.swing.tree.DefaultTreeCellRenderer:
- void setLeafIcon(Icon icon)
- void setOpenIcon(Icon icon)
- void setClosedIcon(Icon icon):设置叶节点、展开节点以及折叠节点的显示图标
2.4、监听树事件
树选择监听器必须实现TreeSelection-Listener接口,这是一个只有下面这个单一方法的接口:
void valueChanged(TreeSelectionEvent event)
每当用户选定或者撤销选定树节点的时候,这个方法就会被调用。可以按照下面这种通常方式向树中添加监听器:
tree.addTreeSelectionListener(listener);
可以设定是否允许用户选定一个单一的节点、连续区间内的节点或者一个任意的、可能不连续的节点集。JTree类使用TreeSelectionModel来管理节点的选择。必须检索整个模型,以便将选择状态设置为SINGLE_TREE_SELECTION
、CONTIGUOUS_TREE SELECTION
或DISCONTIGUOUS_TREE_SELECTION
三种状态之一。(在默认情况下是非连续的选择模式。)
例如,在我们的类浏览器中,我们希望只允许选择单个类:
int mode = TreeSelectionModel.SINGLE TREE SELECTION;
tree.getSelectionModel().setSelectionMode(mode)
除了设置选择模式之外,并不需要担心树的选择模型。
要找出当前的选项集,可以用getSelectionPaths方法来查询树:
TreePath[]selectedPaths tree.getSelectionPaths();
如果想限制用户只能做单项选择,那么可以使用便捷的getSelectionPath方法,它将返回第一个被选择的路径,或者是null(如果没有任何路径被选)。
public class ClassNameTreeCellRenderer extends DefaultTreeCellRenderer {
private Font plainFont = null;
private Font italicFont = null;
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
Class<?> c = (Class<?>) node.getUserObject();
if (plainFont == null) {
plainFont = getFont();
if (plainFont != null) {
italicFont = plainFont.deriveFont(Font.ITALIC);
}
}
if ((c.getModifiers() & Modifier.ABSTRACT) == 0) {
setFont(plainFont);
} else {
setFont(italicFont);
}
return this;
}
}
public class ClassTreeFrame extends JFrame {
private static final int DEFAULT_WIDTH = 400;
private static final int DEFAULT_HEIGHT = 300;
private DefaultMutableTreeNode root;
private DefaultTreeModel model;
private JTree tree;
private JTextField textField;
private JTextArea textArea;
public ClassTreeFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
root = new DefaultMutableTreeNode(Object.class);
model = new DefaultTreeModel(root);
tree = new JTree(model);
addClass(getClass());
ClassNameTreeCellRenderer renderer = new ClassNameTreeCellRenderer();
renderer.setClosedIcon(new ImageIcon("icon.png"));
renderer.setOpenIcon(new ImageIcon("icon.png"));
renderer.setLeafIcon(new ImageIcon("icon.png"));
tree.setCellRenderer(renderer);
tree.addTreeSelectionListener(event -> {
TreePath path = tree.getSelectionPath();
if (path == null) {
return;
}
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) path.getLastPathComponent();
Class<?> c = (Class<?>) selectedNode.getUserObject();
String description = getFieldDescription(c);
textArea.setText(description);
});
int mode = TreeSelectionModel.SINGLE_TREE_SELECTION;
tree.getSelectionModel().setSelectionMode(mode);
textArea = new JTextArea();
JPanel panel = new JPanel();
panel.setLayout(new GridLayout(1, 2));
panel.add(new JScrollPane(tree));
panel.add(new JScrollPane(textArea));
add(panel, BorderLayout.CENTER);
addTextField();
}
public void addTextField() {
JPanel panel = new JPanel();
ActionListener addListener = event -> {
try {
String text = textField.getText();
addClass(Class.forName(text));
textField.setText("");
} catch (ClassNotFoundException e) {
JOptionPane.showMessageDialog(null, "Class not fount");
}
};
textField = new JTextField(20);
textField.addActionListener(addListener);
panel.add(textField);
JButton addButton = new JButton("Add");
addButton.addActionListener(addListener);
panel.add(addButton);
add(panel, BorderLayout.SOUTH);
}
public DefaultMutableTreeNode findUserObject(Object obj) {
Enumeration<TreeNode> e = root.breadthFirstEnumeration();
while (e.hasMoreElements()) {
DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) e.nextElement();
if (treeNode.getUserObject().equals(obj)) {
return treeNode;
}
}
return null;
}
public DefaultMutableTreeNode addClass(Class<?> c) {
if (c.isInterface() || c.isPrimitive()) {
return null;
}
DefaultMutableTreeNode node = findUserObject(c);
if (node != null) {
return node;
}
Class<?> s = c.getSuperclass();
DefaultMutableTreeNode parent;
if (s == null) {
parent = root;
} else {
parent = addClass(s);
}
DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(c);
model.insertNodeInto(newNode, parent, parent.getChildCount());
TreePath path = new TreePath(model.getPathToRoot(newNode));
tree.makeVisible(path);
return newNode;
}
public static String getFieldDescription(Class<?> c) {
StringBuilder r = new StringBuilder();
Field[] fields = c.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field f = fields[i];
if ((f.getModifiers() & Modifier.STATIC) != 0) {
r.append("static ");
}
r.append(f.getType().getName());
r.append(" ");
r.append(f.getName());
r.append("\n");
}
return r.toString();
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new ClassTreeFrame();
frame.setTitle("ClassTree");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
javax.swing.JTree:
- TreePath getSelectionPath()
- TreePath[] getSelectionPaths():返回第一个选定的路径,或者一个包含所有选定节点的数组。如果没有选定任何路径,这两个方法返回为null。
javax.swing.event.TreeSelectionListener:
- void valueChanged(TreeSelectionEvent event):每当选定节点或撤销选定的时候,该方法就被调用
javax.swing.event.TreeSelectionEvent:
- TreePath getPath()
- TreePath[] getPaths():获取在该选择事件中已经发生更改的第一个路径或所有路径。如果你想知道当前的选择路径,而不是选择路径的更改情况,那么应该调用JTree.getSelectionPaths。
2.5、定制树模型
public class ObjectInspectorFrame extends JFrame {
private JTree tree;
private static final int DEFAULT_WIDTH = 400;
private static final int DEFAULT_HEIGHT = 300;
public ObjectInspectorFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
Variable v = new Variable(getClass(), "this", this);
ObjectTreeModel model = new ObjectTreeModel();
model.setRoot(v);
tree = new JTree(model);
add(new JScrollPane(tree), BorderLayout.CENTER);
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new ObjectInspectorFrame();
frame.setTitle("ObjectInspector");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
public class ObjectTreeModel implements TreeModel {
private Variable root;
private EventListenerList listenerList = new EventListenerList();
public ObjectTreeModel() {
root = null;
}
public void setRoot(Variable v) {
Variable oldRoot = v;
root = v;
fireTreeStructureChanded(oldRoot);
}
@Override
public Object getRoot() {
return root;
}
@Override
public Object getChild(Object parent, int index) {
ArrayList<Field> fields = ((Variable)parent).getFields();
Field f = (Field)fields.get(index);
Object parentValue = ((Variable)parent).getValue();
try {
return new Variable(f.getType(), f.getName(), f.get(parentValue));
} catch (IllegalAccessException e) {
return null;
}
}
@Override
public int getChildCount(Object parent) {
return ((Variable)parent).getFields().size();
}
@Override
public boolean isLeaf(Object node) {
return getChildCount(node) == 0;
}
@Override
public void valueForPathChanged(TreePath path, Object newValue) {
}
@Override
public int getIndexOfChild(Object parent, Object child) {
int n = getChildCount(parent);
for (int i = 0; i < n; i++) {
if (getChild(parent, i).equals(child)) {
return i;
}
}
return -1;
}
@Override
public void addTreeModelListener(TreeModelListener l) {
listenerList.add(TreeModelListener.class, l);
}
@Override
public void removeTreeModelListener(TreeModelListener l) {
listenerList.remove(TreeModelListener.class, l);
}
protected void fireTreeStructureChanded(Object oldRoot) {
TreeModelEvent event = new TreeModelEvent(this, new Object[]{oldRoot});
for (TreeModelListener l : listenerList.getListeners(TreeModelListener.class)) {
l.treeStructureChanged(event);
}
}
}
public class Variable {
private Class<?> type;
private String name;
private Object value;
private ArrayList<Field> fields;
public Variable(Class<?> aType, String aName, Object aValue) {
this.type = aType;
this.name = aName;
this.value = aValue;
fields = new ArrayList<>();
if (!type.isPrimitive() && !type.isArray() && !type.equals(String.class) && value != null) {
for (Class<?> c = value.getClass(); c != null; c = c.getSuperclass()) {
Field[] fs = c.getDeclaredFields();
AccessibleObject.setAccessible(fs, true);
for (Field f : fs) {
if ((f.getModifiers() & Modifier.STATIC) == 0) {
fields.add(f);
}
}
}
}
}
public Object getValue() {
return value;
}
public ArrayList<Field> getFields() {
return fields;
}
public String toString() {
String r = type + " " + name;
if (type.isPrimitive()) {
r += "=" + value;
} else if (type.equals(String.class)) {
r += "=" + value;
} else if (value == null) {
r += "=null";
}
return r;
}
}
javax.swing.tree.TreeModel:
- Object getRoot():返回根节点
- int getChildCount(Object parent):获取parent节点的子节点个数
- Object getChild(Object parent, int index):获取给定索引位置上parent节点的子节点
- int getIndexOfChild(Object parent, Object child):获取parent节点的子节点child的索引位置。如果在树模型中child节点不是parent的一个子节点,则返回-1。
- boolean isLeaf(Object node):如果节点node从概念上将是一个叶节点,则返回true
- void addTreeModelListener(TreeModelListener l)
- void removeTreeModelListener(TreeModelListener l):当模型中的信息发生变化时,告知添加和移除监听器
- void valueForPathChanged(TreePath path, Object newValue):当一个单元格编辑器修改了节点值的时候,该方法被调用
javax.swing.event.TreeModelListener:
- void treeNodesChanged(TreeModelEvent e)
- void treeNodesInserted(TreeModelEvent e)
- void treeNodesRemoved(TreeModelEvent e)
- void treeStructureChanged(TreeModelEvent e):如果树被修改过,树模型将调用该方法
javax.swing.event.TreeModelEvent:
- TreeModelEvent(Object eventSource, TreePath node):构建一个树模型事件