分类:
2008-12-14 14:54:00
第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方法可由硬性生成PeopleEntity对象,改为从数据库中取出数据后生成PeopleEntity对象。
这里的List不是SWT组件的List,而是Java的集合类java.util.List。根据实际开发情况也可以用数组或Set、Map等代替List。
List是接口,而ArrayList是实际用的类。由于其后代码是基于List接口编写的,所以换用其他List接口的实现类,如Vector、LinkedList等,而不必修改其后的代码。面向接口编程,尽量让定义类型(如List)比实际类型(如ArrayList)更宽泛些,有利于以后的修改维护。
这里new ArrayList
在数据库编程中,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); |
chinaunix网友2010-06-17 17:11:50
楼主你好,感谢你的分享,我学习到了很多知识.对你提供的原代码我作了以下补充,大多数表格数据都是从数据库取出来的,我把它作了一定的补充修改. 第14章 表格(TableViewer类) TableViewer表格类是JFace组件中重要且典型的一个组件,其中涉及了JFace的众多重要概念:内容器、标签器、过滤器、排序器和修改器,这些概念对后面JFace组件特别是TreeViewer的学习非常重要。从本章也可以体会到JFace非常突出的面向对象特性。 14.1 概 述 JFace是SWT的扩展,它提供了一组功能强大的界面组件,其中包含表格、树、列表、对话框、向导对话框等,从本章之后就开始专门来介绍这些JFace组件。 图14.1 谱系图 表格是一种在软件系统很常见的数据表现形式,特别是基于数据库的应用系统,表格更是不可缺少的界面组件。SWT的表格组件(Table类)前面已经介绍过了,但在实际项目开发中一般还是用JFace的表格组件TableViewer比较多。TableViewer组件是在SWT的Table组件基础上采用MVC模式扩展而来的,但Table并