第14章 表格(TableViewer类)
TableViewer表格类是JFace组件中重要且典型的一个组件,其中涉及了JFace的众多重要概念:内容器、标签器、过滤器、排序器和修改器,这些概念对后面JFace组件特别是TreeViewer的学习非常重要。从本章也可以体会到JFace非常突出的面向对象特性。
14.1 概 述
JFace是SWT的扩展,它提供了一组功能强大的界面组件,其中包含表格、树、列表、对话框、向导对话框等,从本章之后就开始专门来介绍这些JFace组件。
表格是一种在软件系统很常见的数据表现形式,特别是基于数据库的应用系统,表格更是不可缺少的界面组件。SWT的表格组件(Table类)前面已经介绍过了,但在实际项目开发中一般还是用JFace的表格组件TableViewer比较多。TableViewer组件是在SWT的Table组件基础上采用MVC模式扩展而来的,但Table并非TableViewer的父类,从图14.1两个类的谱系图就可以看出这两个类不属于同一族系。
从下面的TableViewer类源代码可以看到,TableViewer把Table作为一个实例变量,从而实现了对Table功能的扩展。
public class TableViewer extends StructuredViewer {
private TableViewerImpl tableViewerImpl;
private Table table; //把Table类作为一个实例变量
private TableEditor tableEditor;
……
}
|
本章就如何使用表格组件TableViewer类来展开讲解,并通过一步步地创建一个完整的表格应用实例来串起表格的知识点,实例的最后界面如图14.2所示。
|
图14.1 谱系图 |
|
图14.2 本章实例的最后界面 |
14.2 创建表格并显示数据
作为起步,本节将演示如何创建一个TableViewer对象,如何用TableViewer来显示数据记录,实例运行效果如图14.3所示。
|
图14.3 TableViewer效果图 |
14.2.1 实例的数据模型介绍
本实例用TableViewer来显示一个数据表中的3条记录,每一条记录对应某一个人的基本资料,记录有5个字段:ID号(数值型)、姓名(字符型)、性别(布尔型)、年龄(数值型)和记录建立时间(日期型)。
如何在程序中体现和操作这些数据记录呢?在过去,像ASP、PHP这类面向过程的编程模式,人们习惯了这样操作数据:从数据库中读取数据,并不对数据做任何封装,直接将数据一条条地显示在表格中。
现在用Java这种面向对象的编程语言,应该用更规范的方式来操作数据:将数据库中的记录看作一个数据对象,用一个类来表示它,数据表的字段写成类的实例变量,这样的类在Java中叫做实体类(或称数据类)。EJB和Hibernate的数据操作方式都是这样的。
数据库与表格显示之间加上了实体类,如此一来,以前的“数据表→表格显示”方式就分成了两个步骤“数据库→实体类→表格显示”。有些习惯了以前编程方式的人也许会觉得多了一个步骤太麻烦,但其实这种方式很有好处:
表格显示的代码不再和数据库表相关。例如,将数据库由Oracle移植到MySQL时就不需要更改“数据库→实体类”这个环节的代码。
零散的字段变量统一在一个类中,程序代码结构更紧凑、清晰,有利于今后代码的维护。不要小看维护问题,很多系统做好后不敢再改,害怕改动后会牵涉到其他模块,其中原因之一就是代码结构太乱、编程不规范所致。
将数据封装在一个实体类中,在数据传递时方便许多,可以将实体类作为一个参数在方法与方法之间来回传递。
14.2.2 创建数据表的实体类
下面依照表中的字段来创建一个相应的实体类,类名为PeopleEntity,代码如下所示。
//------------- 文件名:PeopleEntity.java --------------
// 本类包含5个不同数据类型的变量,分别对应数据库表中的5个字段。变量为private型,即只能
// 由类的内部代码访问,外界只能通过这些变量相应的Setter/Getter方法来访问它们
public class PeopleEntity {
private Long id; //唯一识别码,在数据库里常为自动递增的ID列
private String name; //姓名
private boolean sex; //性别 true男,flase女
private int age; //年龄
private Date createDate; //记录的建立日期。Date类型是java.util.Date,而不是java.sql.Date
//以下代码为字段各自的Setter/Getter方法。参考第3.5.2节,这些方法在Eclipse可自动生成
public Long getId() { return id;}
public void setId(Long long1) {id = long1;}
public String getName() {return name;}
public void setName(String string) {name = string;}
public boolean isSex() { return sex;}
public void setSex(boolean sex) { this.sex = sex; }
public int getAge() {return age;}
public void setAge(int i) {age = i;}
public Date getCreateDate() {return createDate;}
public void setCreateDate(Date date) {createDate = date;}
}
|
14.2.3 数据的生成
由于数据操作是分两步走:“数据库→实体类→表格显示”,实体类隔离了代码对数据库的依赖,所以“数据库→实体类”这一步就不再讲解,这部分的代码与JFace组件的使用无关紧要,也不会影响表格组件的讲解。关于TableViewer和数据库结合使用方面的内容,在后面“插件项目实战”中会有详细示例。
那么如何生成实体类的对象呢?因为数据记录和实体对象相对应,新创建的实体对象就相当于一个空记录,可以用其set方法一个个地将值设入实体对象中,这样就能得到带有数据的实体对象了。
为了今后便于扩展,将创建实体对象的方法集中在一个类中,这种专门负责创建对象的类又叫对象工厂。此类的代码如下:
//-----------文件名:PeopleFactory.java ----------------
//创建PeopleEntity对象的工厂,创建3个PeopleEntry对象,并装入List集合返回
public class PeopleFactory {
public static List getPeoples() { // 工厂的静态方法
List list = new ArrayList();
{ // 第1个实体类对象
PeopleEntity o = new PeopleEntity();
o.setId(new Long(1));// id字段的类型被定义成了Long,所以要转化一下
o.setName("陈刚");
o.setSex(true);
o.setAge(28);
o.setCreateDate(new Date()); // 当前日期
list.add(o);
}
{ // 第2个实体类对象
PeopleEntity o = new PeopleEntity();
o.setId(2L); // 利用JDK5.0的自动装箱功能,省了long到Long对象的转化
o.setName("周阅");
o.setSex(false);
o.setAge(18);
o.setCreateDate(new Date());
list.add(o);
}
{ // 第3个实体类对象
PeopleEntity o = new PeopleEntity();
o.setId(3L);
o.setName("陈常恩");
o.setSex(true);
o.setAge(27);
o.setCreateDate(new Date());
list.add(o);
}
return list;
}
}
|
程序说明:
在实际应用中,getPeoples方法可由硬性生成PeopleEntity对象,改为从数据库中取出数据后生成PeopleEntity对象。
这里的List不是SWT组件的List,而是Java的集合类java.util.List。根据实际开发情况也可以用数组或Set、Map等代替List。
List是接口,而ArrayList是实际用的类。由于其后代码是基于List接口编写的,所以换用其他List接口的实现类,如Vector、LinkedList等,而不必修改其后的代码。面向接口编程,尽量让定义类型(如List)比实际类型(如ArrayList)更宽泛些,有利于以后的修改维护。
这里new ArrayList()使用了JDK5.0的泛型功能,关于泛型可参阅www.chengang.com.cn上的Java类文章。
在数据库编程中,Java集合类起着重要作用。一定要很熟悉各集合类在特性上的差别,这样才能根据实际开发情况作出适当的选择(集合类的详细资料可查阅Java基础书籍)。
14.2.4 在表格中显示数据
在得到由List装载的包含数据信息的实体类对象后,接下来就是使用TableViewer来显示这些数据,实现过程一般要经过如下步骤:
第一步:创建一个TableViewer对象,并在构造函数中用式样设置好表格的外观,这与其他SWT组件的用法一样。
第二步:通过表格内含的Table对象设置布局方式,一般都使用TableViewer的专用布局管理器TableLayout。该布局方式将用来管理表格内的其他组件(如TableColumn表格列)。
第三步:用TableColumn类创建表格列。
第四步:设置内容器和标签器。内容器和标签器是JFace组件中的重要概念,它们分别是IStructuredContentProvider、ITableLabelProvider两个接口的实现类,它们的作用就是定义好数据应该如何在TableViewer中显示。
第五步:用TableViewer的setInput方法将数据输入到表格。就像人的嘴巴,setInput就是TableViewer的嘴巴。
图14.4是TableViewer整个数据流程的示意图。
|
图14.4 TableViewer数据流程示意图 |
程序代码如下(内容器和标签器写成两个单独的类):
//-------------文件名:TableViewer1.java-------------------
shell.setLayout(new FillLayout());
// 第一步:创建一个TableViewer对象。式样:MULTI可多选、H_SCROLL有水平滚动条、V_SCROLL
// 有垂直滚动条、BORDER有边框、FULL_SELECTION整行选择
TableViewer tv=new TableViewer(shell, SWT.MULTI |SWT.BORDER |SWT.FULL_SELECTION);
// 第二步:通过表格内含的Table对象设置布局方式
Table table = tv.getTable();
table.setHeaderVisible(true); // 显示表头
table.setLinesVisible(true); // 显示表格线
TableLayout layout = new TableLayout(); // 专用于表格的布局
table.setLayout(layout);
// 第三步:用TableColumn类创建表格列
layout.addColumnData(new ColumnWeightData(13));// ID列宽13像素
new TableColumn(table, SWT.NONE).setText("ID号");
layout.addColumnData(new ColumnWeightData(40));
new TableColumn(table, SWT.NONE).setText("姓名");
layout.addColumnData(new ColumnWeightData(20));
new TableColumn(table, SWT.NONE).setText("性别");
layout.addColumnData(new ColumnWeightData(20));
new TableColumn(table, SWT.NONE).setText("年龄");
layout.addColumnData(new ColumnWeightData(60));
new TableColumn(table, SWT.NONE).setText("记录建立时间");
// 第四步:设置内容器和标签器
tv.setContentProvider(new TableViewerContentProvider());
tv.setLabelProvider(new TableViewerLabelProvider());
// 第五步:用TableViewer的setInput方法将数据输入到表格
Object data = PeopleFactory.getPeoples();
tv.setInput(data);
|
//-------------文件名:TableViewerContentProvider.java-------------------
//内容器。由此类对输入到表格的数据进行筛选和转化。此类要实现接口的3种方法,其中
//getElements是主要方法,另外两个方法很少用到,空实现就行了
public class TableViewerContentProvider implements IStructuredContentProvider {
// 对输入到表格的数据集合进行筛选和转化。输入的数据集全部要转化成数组,每一个数组元素
//就是一个实体类对象,也就是表格中的一条记录
public Object[] getElements(Object element) {
// 参数element就是通过setInput(Object input)输入的对象input
// 本例中输入给setInput是List集合
if (element instanceof List)// 加一个List类型判断
return ((List) element).toArray(); // 将数据集List转化为数组
else
return new Object[0]; // 如非List类型则返回一个空数组
}
// 当TableViewer对象被关闭时触发执行此方法
public void dispose() {}
// 当TableViewer再次调用setInput()时触发执行此方法
public void inputChanged(Viewer v, Object oldInput, Object newInput) {}
}
|
//-------------文件名:TableViewerLabelProvider.java-------------------
//标签器。如果说内容器是对输入表格的数据集作处理,那么标签器则是对数据集中的单个实体对象
//进行处理和转化,由标签器来决定实体对象中的字段显示在表格的哪一列中
public class TableViewerLabelProvider implements ITableLabelProvider {
//创建几个图像
private Image[] images = new Image[] {
new Image(null, "icons/refresh.gif"),
new Image(null, "icons/star.jpg"),
new Image(null, "icons/moon.jpg") };
// 由此方法决定数据记录在表格的每一列显示什么文字。 element参数是一个实体类对象
// col是当前要设置的列的列号,0是第一列
public String getColumnText(Object element, int col) {
PeopleEntity o = (PeopleEntity) element; // 类型转换
if (col == 0)// 第一列要显示什么数据
return o.getId().toString();
if (col == 1)
return o.getName();
if (col == 2)
return o.isSex() ? "男" : "女";
if (col == 3)
return String.valueOf(o.getAge()); // 将int型转为String型
if (col == 4)
return o.getCreateDate().toString();
return null; // 方法可以返回空值
}
// getColumnText方法用于显示文字,本方法用于显示图片
public Image getColumnImage(Object element, int col) {
PeopleEntity o = (PeopleEntity) element;
// 只让“陈刚”这条记录显示图片
if (o.getName().equals("陈刚")||o.getName().equals("周阅")) {
if (col == 0)// 第一列要显示的图片
return images[0];
if (col == 2)//根据性别显示不同的图标
return o.isSex() ? images[1] : images[2];
}
return null; // 方法可以返回空值
}
// 当TableViewer对象被关闭时触发执行此方法
public void dispose() {
//别忘了SWT组件的原则:自己创建,自释放
for (Image image : images) {
image.dispose();
}
}
// -------------以下方法很少使用,先不用管,让它们空实现-----------------
public boolean isLabelProperty(Object element, String property) {return false;}
public void addListener(ILabelProviderListener listener) {}
public void removeListener(ILabelProviderListener listener) {}
}
|
程序说明:TableViewer的setInput方法的参数类型是Object,所以它可以接受任何类型的参数,因此在内容器中要将参数转换过来,如(List) element。但如果setInput不是List类型的参数,程序就会出错,所以最好用element instanceof List来作一下类型判断会比较稳妥,在SWT/JFace编程中很多BUG都出在这种地方。当然,本例的setInput参数定的就是List类型,不用instanceof判断直接类型转换也没什么问题。
14.3 响应鼠标双击事件
如何让TableViewer的每一行响应鼠标的双击或单击事件呢?又如何取得被选择中的记录数据呢?本节将解决这个问题。本节实例的效果如图14.5所示,双击表格中的某条记录时弹出一个提示框,框中的文字信息显示该记录的人名。
|
图14.5 鼠标响应的效果图 |
本节实例在14.2节的代码基础上修改完成(完整代码见配书光盘的TableViewer2.java文件),具体如下。
在tv.setInput(data)一句之后,添加一个自定义方法addListener(tv),在此方法中给TableViewer添加监听器。addListener方法的代码如下:
private void addListener(TableViewer tv) {
// 鼠标双击事件监听
tv.addDoubleClickListener(new IDoubleClickListener() {
public void doubleClick(DoubleClickEvent event) {
//得到表格的选择对象,里面封装了表格中被选择的记录信息
IStructuredSelection selection = (IStructuredSelection) event.getSelection();
// 得到所选择的第一条实体对象(表格可以有多选),并进行类型转换
PeopleEntity o = (PeopleEntity) selection.getFirstElement();
// 弹出一个提示框
MessageDialog.openInformation(null, "提示", o.getName());
}
});
// 选择事件(单击)监听
tv.addSelectionChangedListener(new ISelectionChangedListener() {
public void selectionChanged(SelectionChangedEvent event) {
// 事件处理代码……(略)
}
});
}
|
14.4 给表格加上右键菜单(Action类、ActionGroup类、MenuManager类)
本节来给表格加上如图14.6所示的右键菜单。本节实例在前两节的代码基础上修改完成(完整代码见配书光盘的TableViewer3.java文件)。
|
图14.6 右键菜单的效果图 |
14.4.1 Action、ActionGroup、MenuManager介绍
SWT中菜单是Menu类,本书在前面章节中已经介绍过Menu类的使用,在第11章中菜单项用MenuItem类来实现。但在实际项目中,同一种功能会有多种表现形式,例如Eclipse中的“新建”功能,它会分别出现在主菜单、主工具栏、右键菜单里。如果都是用MenuItem来实现,就需要写3份类似的代码,以后也要维护3份代码。当然也可以将事件处理写成外部类来共享代码,但名称、图像以及一些其他的信息写成外部类来共享则不太方便。
JFace包中已经对以上问题提供了解决方案,JFace提供了一个Action类,它将名称、图像、动作处理程序等集成在其中,这样就可以共享这些Action来形成菜单项、工具栏按钮等。
当然在底层最后用于Menu的还是MenuItem对象,将Action转化成MenuItem是由MenuManager(菜单管理器)来完成的。MenuManager简化了菜单的创建,一旦生成了MenuManager对象,就可以通用于菜单栏、弹出菜单、工具栏下拉菜单。
另外,Action写成一个个的类会很零乱,JFace又提供了一个ActionGroup类用于统一管理Action,然后让外界程序通过ActionGroup来访问Action。当然,并非一定要使用ActionGroup类来管理Action,只是用它会更好。
14.4.2 创建Action和ActionGroup
以下代码演示了如何创建Action、ActionGroup,以及如何使用MenuManager。
//--------文件名:MyActionGroup.java----------
public class MyActionGroup extends ActionGroup {
private TableViewer tv;
// 在Action要使用到TableViewer对象,所以通过构造函数把它传进来
public MyActionGroup(TableViewer tv) {
this.tv = tv;
}
// 生成菜单Menu,并将两个Action传入
public void fillContextMenu(IMenuManager mgr) {
// 加入两个Action对象到菜单管理器
MenuManager menuManager = (MenuManager) mgr;
menuManager.add(new OpenAction());
menuManager.add(new RefreshAction());
// 生成Menu并挂在表格table上。menu和table两个对象互为对方的参数
Table table = tv.getTable();
Menu menu = menuManager.createContextMenu(table);
table.setMenu(menu);
}
// “打开”的Action类
private class OpenAction extends Action {
public OpenAction() {setText("打开");}
public void run() {// 继承自Action的方法,动作代码写在此方法中
IStructuredSelection selection = (IStructuredSelection) tv.getSelection();
PeopleEntity o = (PeopleEntity) selection.getFirstElement();
if (o == null)
MessageDialog.openInformation(null, null, "请先选择记录");
else
MessageDialog.openInformation(null, null, o.getName());
}
}
// “刷新”的Action类
private class RefreshAction extends Action {
public RefreshAction() {setText("刷新");}
public void run() {
tv.refresh(); //表格的刷新方法,界面会重新读取数据并显示
}
}
}
|
14.4.3 在主程序中使用ActionGroup、MenuManager
MyActionGroup类封装了Action以及Action和菜单Menu之间的交互代码。最后,只需将以下代码加入到shell.open()语句之前即可。
//生成一个ActionGroup对象,并调用fillContextMenu方法将按钮注入到菜单对象中
MyActionGroup actionGroup = new MyActionGroup(tv);
actionGroup.fillContextMenu(new MenuManager());
|
程序说明:图14.7说明了在程序中是如何创建右键菜单的,在主程序生成一个MenuManager对象传给ActionGroup对象,然后再通过ActionGroup内部的createContextMenu方法生成一个菜单对象Menu,最后用Menu的add()方法将Action加入。
|
图14.7 程序分析图 |
14.5 表格的排序(ViewerSorter类)
本节实例将实现表格的单击表头排序功能(以ID、姓名两字段的排序为例),本节实例在前面几节的代码基础上修改完成(完整代码见配书光盘的TableViewer4.java文件)。
14.5.1 编写排序器ViewerSorter
TableViewer是根据排序器ViewerSorter中的设置来进行排序的,所以编写ViewerSorter类是排序的关键。编写排序器的代码如下:
//------文件名:MySorter.java-----------
public class MySorter extends ViewerSorter {
// 每列对应一个不同的常量,正数表示要升序、相反数表示要降序
private static final int ID = 1;
private static final int NAME = 2;
//给外界使用排序器对象
public static final MySorter ID_ASC=new MySorter(ID);
public static final MySorter ID_DESC=new MySorter(-ID);
public static final MySorter NAME_ASC=new MySorter(NAME);
public static final MySorter NAME_DESC=new MySorter(-NAME);
// 当前所要排序的列,取自上面的ID、NAME两值或其相反数
private int sortType;
// 构造函数用private,表示不能在外部创建MySorter对象
private MySorter(int sortType) {
this.sortType = sortType;
}
// 最关键的比较方法compare,改写自ViewerSorter。方法返回值是一个int值:正数则
//obj1移到obj2之前;零则obj1和obj2的位置不动;负数则obj1移到obj2之后
public int compare(Viewer viewer, Object obj1, Object obj2) {
// 传入两条记录(实体类),然后依列给出它们的先后顺序
PeopleEntity o1 = (PeopleEntity) obj1;
PeopleEntity o2 = (PeopleEntity) obj2;
switch (sortType) {
case ID: {
Long l1 = o1.getId();
Long l2 = o2.getId();
// Long的compareTo方法返回值有三个可能值1,0,-1:
//如l1>l2则返回1;如l1=l2则返回0;如l1<l2则返回-1
return l1.compareTo(l2);
}
case -ID: {
Long l1 = o1.getId();
Long l2 = o2.getId();
return l2.compareTo(l1);
}
case NAME: {
String s1 = o1.getName();
String s2 = o2.getName();
return s1.compareTo(s2);
}
case -NAME: {
String s1 = o1.getName();
String s2 = o2.getName();
return s2.compareTo(s1);
}
}
return 0;
}
}
|
程序说明:排序器的代码虽多,但要点就两个。
q 要用MySorter类生成4个不同的排序对象:ID列的升序、ID列的降序、姓名列的升序、姓名列的降序,那么MySorter首先就要解决如何生成这4个不同的排序对象。方法就是让不同的列对应不同的int值,而int值的正负数对应升、降序,然后根据MySorter类构造函数传入的int值就可以判断生成不同的排序器对象。另外,由于MySorter是无状态类,所以多个表格可以安全地共享MySorter所提供的4个排序器对象。
排序的算法由MySorter类的compare方法负责,它实际调用的是JDK中同类型对象之间进行比较的compareTo方法,TableViewer则根据MySorter返回的int值来进行记录的排序
14.5.2 为表格列添加事件监听器
表格列是TableColumn对象,把原来新增ID列和姓名列的4句代码修改如下:
// layout.addColumnData(new ColumnWeightData(13));// ID列宽13像素
// new TableColumn(table, SWT.NONE).setText("ID号");
// layout.addColumnData(new ColumnWeightData(40));
// new TableColumn(table, SWT.NONE).setText("姓名");
layout.addColumnData(new ColumnWeightData(13));
TableColumn col1 = new TableColumn(table, SWT.NONE);
col1.setText("ID号");
col1.addSelectionListener(new SelectionAdapter() {
boolean asc = true; // 记录上一次的排序方式,默认为升序
public void widgetSelected(SelectionEvent e) {
// asc=true则ID的升序排序器,否则用降序
tv.setSorter(asc ? MySorter.ID_ASC : MySorter.ID_DESC);
asc = !asc;// 得到下一次排序方式
}
});
layout.addColumnData(new ColumnWeightData(40));
TableColumn col2 = new TableColumn(table, SWT.NONE);
col2.setText("姓名");
col2.addSelectionListener(new SelectionAdapter() {
boolean asc = true;
public void widgetSelected(SelectionEvent e) {
tv.setSorter(asc ? MySorter.NAME_ASC : MySorter.NAME_DESC);
asc = !asc;
}
});
|
14.6 给表格加上工具栏(ToolBarManager类)
如图14.8所示,本节将给表格加上一个工具栏。本节实例在前几节的代码基础上修改完成(完整代码见配书光盘的TableViewer5.java文件)。
|
图14.8 工具栏效果图 |
和14.4节给表格加上右键菜单的方法相似,也是用ActionGroup、Action类。不同的是,菜单用MenuManager,这里的工具栏则用ToolBarManager。此实例分成如下几步完成。
14.6.1 创建Action类并填充进工具栏
将图14.8中的按钮都写成一个个的Action类,关于Action类的写法在14.4节已经讲过,只需依样扩充MyActionGroup类中的Action的个数即可,而刷新按钮则和刷新菜单共用RefreshAction。MyActionGroup的代码修改示意如下:
//------文件名:MyActionGroup.java-----------
……(省略了原来的老代码)
private class AddAction extends Action {
public AddAction() {
setHoverImageDescriptor(getImageDesc("project.gif"));// 正常状态下的图标
setText("增加");
}
public void run() {
PeopleEntity o = createPeople();// 创建一个新实体对象
tv.add(o);// 增加到表格界面中
List list = (List) tv.getInput();
list.add(o); // 增加到数据模型的List容器中
// ....向数据库增加记录(略)
}
private PeopleEntity createPeople() {// 创建一个新实体对象
PeopleEntity o = new PeopleEntity();
o.setId(5L);
o.setName("新人");
o.setSex(true);
o.setAge(15);
o.setCreateDate(new Date());
return o;
}
}
private class RemoveAction extends Action {
public RemoveAction() {
setHoverImageDescriptor(getImageDesc("remove.gif"));// 正常状态下的图标
// 按钮无效状态下的图标。不设也可以,当按钮失效时会自动使正常图片变灰
setDisabledImageDescriptor(getImageDesc("disremove.gif"));
setText("删除");
}
// 这里演示了如何从表格中删除所选的多个记录
public void run() {
IStructuredSelection s = (IStructuredSelection) tv.getSelection();// 得到选择的对象集
if (s.isEmpty()) {// 判断是否有选择
MessageDialog.openInformation(null, "提示", "请先选择");
return;
}
for (Iterator it = s.iterator(); it.hasNext();) {
PeopleEntity o = (PeopleEntity) it.next();
tv.remove(o);// 从表格界面上删除
List list = (List) tv.getInput();
list.remove(o); // 从数据模型的List容器中删除
// ....,从数据库中删除记录(略)
}
}
}
// 自定义方法。生成Action对象,并通过工具栏管理器ToolBarManager填充进工具栏
public void fillActionToolBars(ToolBarManager actionBarManager) {
// 创建Action对象,一个按钮对应一个个的Action
Action refreshAction = new RefreshAction();
Action addAction = new AddAction();
Action removeAction = new RemoveAction();
// 将按钮通过工具栏管理器ToolBarManager填充进工具栏,如果用add(action)
// 也是可以的,只不过只有文字没有图像。要显示图像需要将Action包装成
// ActionContributionItem,在这里我们将包装的处理过程写成了一个方法
actionBarManager.add(createContributionItem(refreshAction));
actionBarManager.add(createContributionItem(addAction));
actionBarManager.add(createContributionItem(removeAction));
actionBarManager.update(true);// 更新工具栏,否则工具栏不显示任何按钮
}
// 将Action包装成ActionContributionItem类的方法。实际上Action加入到
// ToolBarManager或MenuManager里时,也做了ActionContributionItem的包装,
// 大家看它ToolBarManager的add(IAction)的源代码即知
private IContributionItem createContributionItem(IAction action) {
ActionContributionItem aci = new ActionContributionItem(action);
aci.setMode(ActionContributionItem.MODE_FORCE_TEXT);// 显示图像+文字
return aci;
}
// 得到一个图像的ImageDescriptor对象
private ImageDescriptor getImageDesc(String fileName) {
try {
URL url = new URL("file:icons/" + fileName);
return ImageDescriptor.createFromURL(url);
} catch (MalformedURLException e) {
e.printStackTrace();
}
return null;
}
|
程序说明:在表格中,界面中显示的数据和setInput(Object input)传入的input对象是分离的。也就是说如果input对象中的记录数据发生改变,要调用表格的tv.refresh()或tv.update(Object element, String[] properties)才能在界面上也显示新的数据。refresh、update两个更新界面的方法中:前者是全面更新;后者是只更新某一条记录(element)在界面上的显示,后者的第二个参数甚至还可以指定更新哪几个字段的界面显示,显然后者更新效率要高些。
对于新增记录和删除记录则TableViewer有add和remove方法可用,不过由于前面所说的界面数据和input数据分离,在tv.add、tv.remove之后,勿忘input.add、input.remove。
14.6.2 用ViewForm做布局调整
在上一步创建好ActionGroup中的Action后,接下来就是要在界面中加上工具栏。先要将布局用ViewForm类来调整一下,ViewForm也是继承自Composite的一个容器。原先表格是建立在Shell之上的,现在要在Shell上再插入一个ViewForm容器,以它为基座将工具栏和表格创建于其中,如图14.9所示。
将原主程序中的open()方法修改如下,其他代码不变:
shell.setLayout(new FillLayout());
ViewForm viewForm = new ViewForm(shell, SWT.NONE); //布局基座ViewForm
viewForm.setLayout(new FillLayout());
final TableViewer tv = new TableViewer(viewForm, SW… //父容器由shell改为viewForm
//……和上一节相同的代码(省略)
//创建工具栏
ToolBar toolBar = new ToolBar(viewForm, SWT.FLAT); // 创建一个ToolBar容器
ToolBarManager toolBarManager = new ToolBarManager(toolBar); // 创建一个toolBar的管理器
actionGroup.fillActionToolBars(toolBarManager); //将Action通过toolBarManager注入ToolBar中
// 设置表格和工具栏在布局中的位置
viewForm.setContent(tv.getControl()); // 主体:表格
viewForm.setTopLeft(toolBar); // 顶端边缘:工具栏
shell.open();
|
|
图14.9 布局示意图 |
14.7 带复选框的表格(CheckboxTableViewer类)
带复选框的表格如图14.10所示,它具有如下功能:
单击“全选”按钮时,将表格中的所有记录选中(选中复选框)。
单击“全不选”按钮时,取消所有选择(取消选中复选框)。
单击“删除”按钮时,将所有选中复选框的记录删除。
|
图14.10 带复选框的表格 |
本节实例在前几节的代码基础上修改完成(完整代码见配书光盘的TableViewer6.java文件)。要完成此实例需要如下几个步骤。
14.7.1 使用表格的复选框式样
(1)在创建TableViewer对象时多加一个SWT.CHECK式样,表格变为复选框式。复选框式的表格要取得选中的记录,还需要增加一个CheckboxTableViewer对象来辅助表格的使用,因为仅TableViewer对象无法取得选中的记录。
final TableViewer tv = new TableViewer(viewForm, SWT.CHECK | SWT.MULTI | SWT.……
final CheckboxTableViewer ctv = new CheckboxTableViewer(tv.getTable());
|
(2)修改创建MyActionGroup的语句,将CheckboxTableViewer的ctv对象作构造函数的第二个参数传入,因为MyActionGroup中的Action需要用到此对象。
MyActionGroup actionGroup = new MyActionGroup(tv, ctv);
|
这一步完成后,因为还没有对MyActionGroup类作相应改动,Eclipse会显示错误。下面开始修改MyActionGroup类。
14.7.2 修改MyActionGroup类
在原有MyActionGroup类的代码中作如下几处修改:
新增一个用于接受CheckboxTableViewer对象的构造函数。
增加“全选”和“全不选”两个Action类,并相应修改fillActionToolBars方法。
修改“删除”的RemoveAction,改由CheckboxTableViewer来取得选中的记录。因为前几节的程序也用到RemoveAction,为了兼容,所以RemoveAction原有的处理代码还不能废弃掉。可以加一个表格是否为复选框式样的判断,以决定使用哪种删除处理代码。
具体代码如下所示:(和原代码相同部分用省略号代替)
public class MyActionGroup extends ActionGroup {
private TableViewer tv;
private CheckboxTableViewer ctv; //新增的语句
public MyActionGroup(TableViewer v) {
this.tv = v;
}
//新增一个构造函数
public MyActionGroup(TableViewer v, CheckboxTableViewer ctv) {
this.tv = v;
this.ctv = ctv;
}
……
//修改原来的“删除”Action的run方法
private class RemoveAction extends Action {
……
public void run() {
if (ctv != null) {
Object[ ] checkObj = ctv.getCheckedElements(); // 取得打勾的记录
if (checkObj.length == 0) {// 判断是否有勾选复选框
MessageDialog.openInformation(null, "提示", "请先勾选记录");
return;
}
for (int i = 0; i < checkObj.length; i++) {
PeopleEntity o = (PeopleEntity) checkObj[i];
ctv.remove(o);// 从表格界面上删除
List list = (List) tv.getInput();
list.remove(o);// 从数据模型的List容器中删除
// ....,从数据库中删除记录(略)
}
} else {
IStructuredSelection s = (IStructuredSelection) tv.getSelection();
if (s.isEmpty()) {// 判断是否有选择
MessageDialog.openInformation(null, "提示", "请先选择");
return;
}
for (Iterator it = s.iterator(); it.hasNext();) {
PeopleEntity o = (PeopleEntity) it.next();
tv.remove(o);// 从表格界面上删除
List list = (List) tv.getInput();
list.remove(o); // 从数据模型的List容器中删除
// ....,从数据库中删除记录(略)
}
}
}
}
……
//新增的“全选”Action
private class SelectAllAction extends Action {
public SelectAllAction() {
setHoverImageDescriptor(getImageDesc("selectall.gif"));
setText("全选");
}
public void run() {
if (ctv != null) ctv.setAllChecked(true); //将所有复选框打勾
}
}
//新增的“全不选”Action
private class DeselectAction extends Action {
public DeselectAction() {
setHoverImageDescriptor(getImageDesc("deselect.gif"));
setText("全不选");
}
public void run() {
if (ctv != null) ctv.setAllChecked(false); //取消所有复选框打勾
}
}
//修改此方法将“全选”、“全不选”加入
public void fillActionToolBars(ToolBarManager actionBarManager) {
……
Action selAllAction = new SelectAllAction();
Action deselAction = new DeselectAction();
……
actionBarManager.add(createContributionItem(selAllAction));
actionBarManager.add(createContributionItem(deselAction));
actionBarManager.update(true); // 更新工具栏,否则工具栏不显示任何按钮
}
}
|
14.8 让表格可直接编辑(CellEditor类、ICellModifier接口)
前面仅仅是显示表格数据,本节谈谈如何修改数据。本节实例效果如图14.11所示。本节实例在前几节的代码基础上修改完成(完整代码见配书光盘的TableViewer7.java文件)。
|
图14.11 CellEditor效果图 |
14.8.1 设置编辑组件CellEditor
首先在TableViewer主程序前部的变量定义区中创建一个静态公用的字符串数组,它们就是修改“姓名”列时出现在下拉框中的值。
public static String[] NAMES = { "老张", "小红", "陈刚", "周阅", "陈常恩" };
|
接着给表格列添加编辑组件CellEditor,在tv.setInput(data)语句之后,加入如下程序块:
// 定义每一列的别名
tv.setColumnProperties(new String[] { "id", "name", "sex", "age", "createdate" });
// 设置每一列的单元格编辑组件CellEditor
CellEditor[] cellEditor = new CellEditor[5];
cellEditor[0] = null;
cellEditor[1] = new ComboBoxCellEditor(tv.getTable(), NAMES, SWT.READ_ONLY);
cellEditor[2] = new CheckboxCellEditor(tv.getTable());
cellEditor[3] = new TextCellEditor(tv.getTable());
cellEditor[4] = null;
tv.setCellEditors(cellEditor);
tv.setCellModifier(new MyCellModifier(tv)); // 设置表格的修改器MyCellModifier
Text text = (Text) cellEditor[3].getControl();// 设置第4列只能输入数值
text.addVerifyListener(new VerifyListener() { // 以下代码说明参阅第8.4节“文本框”,完全一样
public void verifyText(VerifyEvent e) {
String inStr = e.text;
if (inStr.length() > 0) {
e.doit = NumberUtils.isDigits(inStr);
}
}
});
|
程序说明:表格设置的列别名在修改器MyCellModifier类中要用到。和设置列别名一样,设置列的CellEditor编辑组件也是用数组的方式,其数组序号和列序号一一对应。
14.8.2 创建修改器ICellModifier
修改器MyCellModifier是最重要的一个类,也是最复杂的一个类,编程时一不小心就容易出BUG。其代码如下所示:
//------------- 文件名:MyCellModifier.java --------------
public class MyCellModifier implements ICellModifier {
private TableViewer tv;
public MyCellModifier(TableViewer tv) {
this.tv = tv;
}
// 判断是否可以修改某条记录的某一字段。这里返回true表示都可以修改
// 参数element是表格记录对象,也就是PeopleEntity对象
// 参数property是列别名。该值不会有CellEditor为null的列,也就是说它不可能为id,createdate
public boolean canModify(Object element, String property) {
return true;
}
// 此方法决定当单击单元格出现CellEditor时应该显示什么值。参数说明参考canModify方法
// 每种CellEditor要求返回的数据类型都是各不相同的,类型不对应就会出错
public Object getValue(Object element, String property) {
PeopleEntity o = (PeopleEntity) element;
if (property.equals("name")) {
// ComboBoxCellEditor要求返回姓名在下拉框中的索引值
return getNameIndex(o.getName());
} else if (property.equals("sex")) {
// CheckboxCellEditor要求返回当前值对应的布尔值
return o.isSex();
} else if (property.equals("age")) {
// TextCellEditor要求返回当前值对应的字符串
return String.valueOf(o.getAge());
}
throw new RuntimeException("错误的列别名:" + property);
}
private int getNameIndex(String name) {
for (int i = 0; i < TableViewer7.NAMES.length; i++) {
if (TableViewer7.NAMES[i].equals(name))
return i;
}
return -1;
}
// 从CellEditor取值得此单元格的值
// 参数element是表格行对象TableItem,其getData()方法可取得PeopleEntity
// 参数property是列别名
// 参数value是修改后的新值。每种CellEditor的value的数据类型各不相同
public void modify(Object element, String property, Object value) {
TableItem item = (TableItem) element;
PeopleEntity o = (PeopleEntity) item.getData();
// 根据新的修改值更新PeopleEntity对象的数据
if (property.equals("name")) {
// ComboBoxCellEditor的value是其索引值
Integer comboIndex = (Integer) value;
String newName = TableViewer7.NAMES[comboIndex];
o.setName(newName);
} else if (property.equals("sex")) {
// CheckboxCellEditor的value是布尔值
Boolean newValue = (Boolean) value;
o.setSex(newValue);
} else if (property.equals("age")) {
// TextCellEditor的value就是文本框里的字符
String newValue = (String) value;
if (newValue.equals(""))// 如果不修改
return;
int newAge = Integer.parseInt(newValue);
o.setAge(newAge);
} else {
throw new RuntimeException("错误的列别名:" + property);
}
// 更新对象在表格上的界面显示。也可以用tv.refresh()全面更新界面,但太浪费效率
tv.update(o, null);
}
}
|
程序说明:
当单击一个可修改表格列时,首先执行canModify方法来决定是否编辑这条记录,如果它返回true才会接着去执行getValue方法,并通过getValue方法决定编辑组件的显示值。接着用户在表格上显示的编辑组件里进行值的修改,修改完成后,将修改值传入到modify方法,在此方法中自己编程把新值更新到表格显示。
在感观上单元格编辑组件似乎是表格的一部分,但实际上它是作为单独组件叠加在表格上的,加上编辑组件种类复杂,所以才要MyCellModifier这样的类来作为编辑组件和表格组件的中间人,进行数据处理和传递。
14.9 其他使用技巧
14.9.1 表格记录的过滤
建立一个继承自ViewerFilter的类,称之为过滤器类。下面的实例建立了一个过滤器,此过滤器的作用是在表格中只显示姓名叫“陈刚”的记录。
//------------- 文件名:MyFilter.java --------------
public class MyFilter extends ViewerFilter {
// 参数viewer在本例中就是TableViewer对象
// 参数parentElement 在本例中是一个包含全部记录的Object数组
// 参数element 当前传入的记录,需要判断是否过滤它
// 返回值=false则此记录(element)不显示。true为显示
public boolean select(Viewer viewer, Object parentElement, Object element) {
PeopleEntity o = (PeopleEntity) element;
return o.getName().equals("陈刚");
}
}
|
表格使用过滤器的语句如下所示,也可以把它写入某Action的run方法中:
tv.addFilter(new MyFilter()); //tv是TableViewer对象
|
14.9.2 控制表格的当前选择行
可以将以下语句写在某个事件代码中,例如写在Action的run方法中。
(1)向下移动,到底后又回到第一行。
Table table = tv.getTable();
int i = table.getSelectionIndex(); //取得当前所选行的序号,如没有则返回-1
table.setSelection(i + 1); //当前选择行移下一行
|
(2)向上移动,到第一行后又回到最末尾一行。
Table table = tv.getTable();
int i = table.getSelectionIndex();
if (i > 0) //是否超过第一行
table.setSelection(i - 1); //向上移
else {
int count = table.getItemCount(); //总的行数
table.setSelection(count - 1);
}
|
14.9.3 给表格的单元格设置背景色
如下语句将使第1行第2列的单元格背景色变为红色(要加在tv.setInput()方法后面)。
Table table = tv.getTable(); //tv是一个TableViewer对象
TableItem item = table.getItem(0); //得到第1行
Color color =Display.getDefault().getSystemColor(SWT.COLOR_RED);//红色
item.setBackground(1, color); //设置此行的第2列为红色
table.redraw(); //重画界面
|
14.9.4 加快TableItem和记录之间的查找速度
用以下语句可以在TableViewer内部为数据记录和TableItem之间的映射创建一个哈希表,这样可以加快TableItem和记录之间的查找速度,这条语句必须加在setInput方法之前。
tv.setUseHashlookup(true);
|