前面加一點自己關於TableViewer的筆記
設置table只顯示有數據的部分 放置table過大 出現過多的空行 可以通過設置table的layout。
第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 instanceofList來作一下類型判斷會比較穩妥,在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(Objectinput)傳入的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);