Eclipse RCP詳解(05):JFace和結(jié)構(gòu)化數(shù)據(jù)
Posted on 2014-03-04 20:56 京山游俠 閱讀(5280) 評論(6) 編輯 收藏 所屬分類: 擁抱Eclipse RCP上一篇
Eclipse RCP詳解(04):Eclipse RCP相關(guān)的學(xué)習(xí)資料及國內(nèi)相關(guān)圖書點(diǎn)評
前面幾篇都是使用Ubuntu系統(tǒng)及其自帶的Eclipse 3.8.1。在這一篇里,我將為大家展示Fedora 20和其自帶的Eclipse 4.3.1。同時(shí),為了方便我隨時(shí)切換操作系統(tǒng)(Ubuntu、Fedora、Win7),我在GitHub上注冊了一個(gè)賬號(hào),并把示例代碼放了上去。我切換系統(tǒng)后,只需要Fetch一下,就可以繼續(xù)開發(fā)。當(dāng)然,有興趣看我代碼的朋友,也可以clone一份到自己的機(jī)器上。地址:
https://github.com/littleloach/examples.git
關(guān)于Eclipse和Git的整合,請看這里:淺論Maven和Git的原理及展示其與Eclipse的集成
事實(shí)證明,Eclipse RCP不僅是跨平臺(tái)的,而且是跨版本的。也就是說,即使我在Fedora中使用了不一樣的Eclipse版本,我在Ubuntu中寫的代碼到Fedora中也不需要修改。(我在Win7中用的Eclipse版本和Ubuntu中的相同。)
Fedora 20和Fedora 19都是我喜歡的版本。因?yàn)樗鼈兊拈_發(fā)代號(hào)分別為Heisenberg和薛定諤的貓,不難看出,它們都和量子力學(xué)有關(guān),對我這樣的理工男吸引力那肯定是巨大的。而Heisenberg更具有傳奇性。不僅因?yàn)樗橇孔恿W(xué)的主要?jiǎng)?chuàng)始人和諾貝爾獎(jiǎng)獲得者,更因?yàn)闅v史上到現(xiàn)在都沒有定論的海森堡謎題:為什么當(dāng)初他沒有造出原子彈,是他真的搞錯(cuò)了,還是他故意搞錯(cuò)了?故事是這樣的:第二次世界大戰(zhàn)開始后,迫于納粹德國的威脅,丹麥的大物理學(xué)家玻爾離開了心愛的哥本哈根理論物理研究所,離開了朝夕相處的來自世界各地的同事,遠(yuǎn)赴美國。德國的許多科學(xué)家也紛紛背井離鄉(xiāng),堅(jiān)決不與納粹勢力妥協(xié)。然而,有一位同樣優(yōu)秀的物理學(xué)家卻留下來了,并被納粹德國委以重任,負(fù)責(zé)領(lǐng)導(dǎo)研制原子彈的技術(shù)工作,遠(yuǎn)在異鄉(xiāng)的玻爾憤怒了,他與這位過去的同事產(chǎn)生了尖銳的矛盾,并與他形成了終生未能化解的隔閡。有趣的是,這位一直未能被玻爾諒解的科學(xué)家卻在1970年獲得了“玻爾國際獎(jiǎng)?wù)?#8221;,而這一獎(jiǎng)?wù)率怯靡员碚?#8220;在原子能和平利用方面做出了巨大貢獻(xiàn)的科學(xué)家或工程師”的。歷史在此開了個(gè)巨大的玩笑,這玩笑的主人公就像他發(fā)現(xiàn)的“測不準(zhǔn)原理”一樣,一直讓人感到困惑和不解。他就是量子力學(xué)的創(chuàng)始人——海森堡。也許他當(dāng)初就是故意留下來而且故意造不出原子彈的吧。
在正式介紹JFace和結(jié)構(gòu)化數(shù)據(jù)之前,先向大家展示幾張F(tuán)edora的截圖。
第一張,F(xiàn)edora剛安裝完成后,其Gnome桌面是很丑的:

所以需要進(jìn)行一些Art Work,比如多安裝幾個(gè)wallpaper啦,多安裝幾個(gè)theme啦,然后安裝一個(gè)gnome-tweak-tool更改一下字體、字體微調(diào)和抗鋸齒啦。Fedora 19和20還有一個(gè)地方比較令人蛋疼,那就是它提供的gnome-terminal是不支持半透明背景的,如果想要半透明背景,只有自己編譯gnome-terminal。我太懶,所以只好把KDE中的konsole借來用用了。好在KDE程序在gnome中也可以正常運(yùn)行。下面是經(jīng)過我折騰后的Fedora 20:

有人說,GTK+程序的列表兩行之間的寬度大到可以塞進(jìn)一頭大象。以前在Ubuntu下用Eclipse不覺得,換到Fedora中發(fā)現(xiàn)這個(gè)寬度確實(shí)是挺大的,看著很不舒服。不過這都不是個(gè)事,換個(gè)GTK+的theme一切都搞定。下面一張是Eclipse的截圖,還是很漂亮的說:

下面開始今天的正題。
結(jié)構(gòu)化數(shù)據(jù)
結(jié)構(gòu)化數(shù)據(jù)可不是我一拍腦袋想出來的術(shù)語。平時(shí)我們在組織和保存數(shù)據(jù)的時(shí)候,離不開數(shù)據(jù)結(jié)構(gòu)。我們經(jīng)常使用的數(shù)組、鏈表或多維數(shù)組來保存數(shù)據(jù),也經(jīng)常使用數(shù)據(jù)庫的表格保存數(shù)據(jù),但是我們并不是時(shí)時(shí)刻刻想著結(jié)構(gòu)化這么一個(gè)概念。但是在Eclipse中就不一樣了,結(jié)構(gòu)化數(shù)據(jù)是它的一個(gè)很重要的理念,只要有可能,它都會(huì)把數(shù)據(jù)組織成結(jié)構(gòu)化的。比如,如果你在Eclipse中選擇了一些什么,那么這些選擇的東西就是以IStructuredSelection展示出來的。如下圖,可見Structured無處不在:

數(shù)組(包括鏈表)、樹和表格(包括多維數(shù)組)是我們常用的數(shù)據(jù)結(jié)構(gòu)。List控件、Tree控件和Table控件是我們常用的展示數(shù)據(jù)的控件。然而邏輯和形式并不總是一一對應(yīng)的。比如形式上可以用Tree控件表現(xiàn)的數(shù)據(jù),邏輯上并不一定是用樹結(jié)構(gòu)組織起來的數(shù)據(jù)。想想我們做數(shù)據(jù)庫的ORM映射,如果一個(gè)表中的一行數(shù)據(jù)和另一個(gè)表中的多行數(shù)據(jù)相關(guān)聯(lián)(一對多映射),寫成Java Entity類的時(shí)候就是一個(gè)Entity中包含有一個(gè)另一個(gè)Entity的List或Set。這兩個(gè)Entity各有各的Class Name,而不是統(tǒng)一叫TreeNode。這兩個(gè)Entity之間也不用getParent和getChildren互相訪問。但是從形式上,依然可以用一個(gè)Tree控件來展示它們,不是嗎?
JFace中的Viewer
在該系列博文的第02篇中,我展示了SWT的List控件和Tree控件。這兩個(gè)控件的使用是很簡單的,List控件只需要使用add()方法就可以在列表中添加一個(gè)字符串,Tree控件需要先構(gòu)造一個(gè)TreeItem對象,然后用setText()方法添加一個(gè)字符串。控件只能用來顯示字符串和字符串前面的一個(gè)小圖標(biāo),不能保存復(fù)雜的數(shù)據(jù)。MVC是常用的設(shè)計(jì)模式,Model用來保存數(shù)據(jù),而控件只是負(fù)責(zé)顯示,所以中間一定要有一個(gè)機(jī)制將Model中的數(shù)據(jù)轉(zhuǎn)換為能讓控件顯示的字符串和圖標(biāo),并且要能維持?jǐn)?shù)據(jù)在結(jié)構(gòu)化上的邏輯聯(lián)系。
JFace中的Viewer是對SWT中的控件的封裝,它使用的正是MVC模式。如果不了解Viewer的哲學(xué),使用Viewer就無從下手。Viewer的哲學(xué)是這樣的:Viewer對它要顯示的數(shù)據(jù)是怎么組織的沒有任何要求,可以使用它的setInput()方法將任何一個(gè)對象作為它的Model;很顯然Viewer不可能智能到顯示任何結(jié)構(gòu)的數(shù)據(jù),所以必須給它提供一個(gè)ContentProvider,ContentProvider就是負(fù)責(zé)將任意類型的Model轉(zhuǎn)化為結(jié)構(gòu)化的數(shù)據(jù);Model經(jīng)過ContentProvider轉(zhuǎn)化為結(jié)構(gòu)化數(shù)據(jù)后,Viewer再對該結(jié)構(gòu)中的每一項(xiàng)進(jìn)行顯示,前面提到過控件只能顯示文字和圖片,所以還需要一個(gè)LabelProvider來將數(shù)據(jù)轉(zhuǎn)化成要顯示的文字和圖片。
由上面的描述可以看出,要使用Viewer必須最少編寫三樣?xùn)|西:一些Model、一個(gè)ContentProvider和一個(gè)LabelProvider。當(dāng)然,Viewer還有更多的功能,比如對數(shù)據(jù)進(jìn)行排序和過濾,只需要提供相應(yīng)的SortProvider和FilterProvider即可,但這兩個(gè)Provider不是必須的。雖然要寫的類比較多,但是JFace都定義好了Interface,我們只需要實(shí)現(xiàn)即可,在IDE的幫助下,這個(gè)工作一點(diǎn)也不難。
一個(gè)簡單的示例
文字內(nèi)容寫得再多,也不如一個(gè)例子來得直接。我這里模擬HIS(Hospital Information System)系統(tǒng)中一個(gè)簡單的功能來展示Viewer的用法。在醫(yī)院里,一個(gè)住院部(Inpatient Department)可以有很多個(gè)科室(Department),比如內(nèi)科、外科、五官科等,一個(gè)科室里面可以有很多個(gè)病人(Patient)。很顯然,這些數(shù)據(jù)用一個(gè)TreeViewer顯示是再合適不過了。先看看這個(gè)示例程序界面的總體框架,如下圖:

這只是一個(gè)框架,結(jié)合上面的界面,我簡要說說我要實(shí)現(xiàn)的功能:
1、最左邊的視圖使用了一個(gè)TreeViewer,用來顯示住院部的所有科室和病人;
2、工具欄有兩個(gè)按鈕,如果最左邊的視圖中被選擇的對象是科室,則帶加號(hào)的按鈕可用,其功能是為該科室增加一個(gè)病人;如果最左邊的視圖中被選擇的對象是病人,則帶減號(hào)的按鈕可用,其功能是刪除這個(gè)病人;
3、中間的視圖使用一個(gè)TableViewer控件,當(dāng)最左邊的視圖中被選擇的對象是科室時(shí),則以表格的形式顯示該科室所有病人的詳細(xì)信息,每一行代表一個(gè)病人;
4、最右邊的視圖使用一個(gè)ListViewer控件,僅僅顯示當(dāng)前選擇的內(nèi)容。
為示例程序準(zhǔn)備數(shù)據(jù)
該示例程序用到的數(shù)據(jù)是一個(gè)住院部有多個(gè)科室、一個(gè)科室有多個(gè)病人,很顯然這是一個(gè)典型的一對多的關(guān)系。所以我們的Model需要三個(gè)類:Inpatient、Department、Patient。其關(guān)系如下:

其中的Gender是一個(gè)枚舉。下面是它們的代碼:
Gender.java
Inpatient.java
Department.java
Patient.java
然后,創(chuàng)建一個(gè)PatientView類,在里面使用一個(gè)TreeViewer控件,創(chuàng)建一個(gè)Inpatient類的實(shí)例,然后添加一點(diǎn)點(diǎn)測試數(shù)據(jù),然后把Inpatient的實(shí)例作為viewer的input即可。如下圖:

當(dāng)然,如果沒有設(shè)置好ContentProvider和LabelProvider,數(shù)據(jù)是不可能正常顯示的。所以我們還需要?jiǎng)?chuàng)建一個(gè)PatientContentProvider類和PatientLabelProvider類。好在JFace中有早就定義好的接口,我們只需要從這些接口實(shí)現(xiàn)即可,如下圖:

我們先來看PatientContentProvider需要實(shí)現(xiàn)哪些方法,如下圖,可以看出IDE已經(jīng)自動(dòng)把框架搭建起來了,我們只需要填空即可(下圖中的空是我已經(jīng)填好了的):

第一個(gè)要實(shí)現(xiàn)的方法是getElements,該方法要求我們把Input的數(shù)據(jù)轉(zhuǎn)化成一個(gè)Object數(shù)組返回。在這個(gè)例子中,Input進(jìn)來的是Inpatient類的對象,所以只需要先將inputElement轉(zhuǎn)型為Inpatient,然后返回它里面的Department數(shù)組即可。第二個(gè)要實(shí)現(xiàn)的方法是hasChildren,在本例中,如果element是Department,則它就有children,如果是Patient就沒有children。最后要實(shí)現(xiàn)的一對方法就是getChildren和getParent,很顯然,如果element是Department,則它的children是它里面保存的List<Patient>,不過返回的時(shí)候要把它轉(zhuǎn)化成數(shù)組,如果element是Patient,則它的parent是它的department。So easy!不是嗎?
而PatientLabelProvider的實(shí)現(xiàn)就更簡單了,如下圖:
看圖填空,我們只需要完成getText方法和getImage方法即可。邏輯也很簡單,就是判斷element是Department還是Patient,然后分別返回相應(yīng)的字符串和圖片即可。在這里,我還故意把不同的性別顯示為不同的圖標(biāo)。有了截圖,我就不貼代碼了。
俗話說一通百通,使用不同的Model和Viewer,就給它不同的ContentProvider和LabelProvider即可。
處理Selection事件
關(guān)于數(shù)據(jù)的展示,我們輕松搞定。不過這還不算完,還有工作要做。現(xiàn)在數(shù)據(jù)顯示出來了,我們就可以用鼠標(biāo)來點(diǎn)擊,來選擇。那么我們選擇的數(shù)據(jù)怎么樣來影響我們程序的行為呢?在前面幾篇對GUI的探討中我說過,程序和用戶的交互是通過事件處理來進(jìn)行的。也就是說,當(dāng)我們選擇了Viewer中的一項(xiàng)或多項(xiàng)的時(shí)候,Viewer應(yīng)該發(fā)送一個(gè)Selection事件,我們向程序中注冊一個(gè)SelectionListener就應(yīng)該可以捕獲到這個(gè)事件。
為了讓patientViewer向程序發(fā)送Selection事件,我們需要讓它成為程序的selectionProvider。通過在PatientView中添加如下一行代碼(最后一行):

有了發(fā)送Selection事件的源,接著應(yīng)該有接受Selection事件的Listener。先從最簡單的功能做起。最右邊的視圖只是用來顯示所選擇的對象,所以從它先開始。最右邊的視圖類是SelectionView,里面用了一個(gè)ListViewer,在這里,我們先讓SelectionView實(shí)現(xiàn)ISelectionListener接口,然后把它注冊到Workbench中。如下圖,關(guān)鍵代碼我把它標(biāo)出來了:

selectionChanged()方法將在有目標(biāo)被選中的時(shí)候調(diào)用,該方法的實(shí)現(xiàn)也很簡單,就是把selection作為ListViewer的input,讓它顯示即可。而要使用ListViewer,我們又得為它提供一個(gè)ContentProvider和一個(gè)LabelProvider。下面是ContentProvider和LabelProvider的實(shí)現(xiàn),也很簡單,這次ContentProvider實(shí)現(xiàn)了IStructuredContentProvider接口,而且從代碼中可以看出,selection是一個(gè)IStructuredSelection,哪怕我們只選中一項(xiàng),它也是Structured的。


做好之后,運(yùn)行程序是這樣的效果:
然后再來實(shí)現(xiàn)中間那個(gè)視圖DepartmentDetailView的功能。在這個(gè)視圖中使用了一個(gè)TableViewer。使用TableViewer比使用ListViewer和TreeViewer要稍微復(fù)雜一點(diǎn),因?yàn)樾枰覀冏约禾砑颖砀竦牧校⒃O(shè)置表格的屬性。和SelectionView一樣,DepartmentDetailView實(shí)現(xiàn)ISelectionListener,并把自己注冊到Workbench中。代碼如下:

TableViewer依然需要一個(gè)ContentProvider和一個(gè)LabelProvider,不過沒有ITableContentProvider,因?yàn)門ableViewer是把每一行當(dāng)成一個(gè)element,所以它只需要和ListViewer一樣的ContentProvider即可。但是TableViewer需要的LabelProvider不一樣,要實(shí)現(xiàn)ITableLabelProvider,在這里,TableViewer才處理不同的列。如下圖:


完成后,運(yùn)行程序是這樣的效果:

根據(jù)用戶的選擇決定工具欄按鈕是否可用
終于進(jìn)入今天的最后一個(gè)議題了。 在前面的幾篇中,我陸續(xù)用過主菜單、視圖工具欄,今天用到了主工具欄。菜單和工具欄是GUI程序產(chǎn)生命令的主要方式,還有就是快捷鍵和彈出菜單。在Eclipse中,它們的本質(zhì)都一樣,都是通過關(guān)聯(lián)到一個(gè)command和一個(gè)handler來實(shí)現(xiàn)其功能,通過一個(gè)menuContribution來指定它們的位置。在今天的示例中,我把menuContribution的locationURI設(shè)置為toolbar:org.eclipse.ui.main.toolbar,所以這兩個(gè)按鈕就出現(xiàn)在了主工具欄中。如下圖:

在今天的示例中加入工具欄按鈕可不是為了將工具欄。今天的主題是結(jié)構(gòu)化數(shù)據(jù)。StructuredSelection也是結(jié)構(gòu)化的數(shù)據(jù)。所以在這個(gè)示例中加入工具欄是為了展示用戶選擇的數(shù)據(jù)會(huì)對工具欄按鈕的行為產(chǎn)生影響。從功能上講,我們希望當(dāng)用戶選擇一個(gè)科室的時(shí)候,添加病人的按鈕可用,而選擇多個(gè)科室或選擇一個(gè)或多個(gè)病人的時(shí)候,添加病人工具欄不可用,希望當(dāng)用戶選擇一個(gè)病人的時(shí)候,刪除病人按鈕可用,而當(dāng)選擇科室或多個(gè)病人的時(shí)候,刪除按鈕不可用。
這樣的需求在GUI編程中可以算是無處不在。Eclipse RCP是如何處理這個(gè)問題的呢?答案就是Workbench Core Expressions。下圖是Eclipse幫助文檔中關(guān)于Workbench Core Expressions的頁面:

然后,我們只需要對工具欄的按鈕添加如下的Workbench Core Expressions即可:

收工,今天就寫到這里。這篇文章已經(jīng)夠長了,截圖也有20多張了,所以實(shí)現(xiàn)那兩個(gè)按鈕的Handler我就不寫了。理解了結(jié)構(gòu)化數(shù)據(jù)和理解了IStructuredSelection后,理解Workbench Core Expression就很容易了,更詳細(xì)的內(nèi)容大家自己看幫助文檔吧。
寫到這里,Eclipse RCP的大部分UI元素如窗口、菜單、工具欄、視圖等等什么的都提到了,還有一個(gè)很重要的大件的UI building block就是編輯器了。等下次有時(shí)間,我再來詳細(xì)討論編輯器的編寫。
Eclipse RCP詳解(04):Eclipse RCP相關(guān)的學(xué)習(xí)資料及國內(nèi)相關(guān)圖書點(diǎn)評
前面幾篇都是使用Ubuntu系統(tǒng)及其自帶的Eclipse 3.8.1。在這一篇里,我將為大家展示Fedora 20和其自帶的Eclipse 4.3.1。同時(shí),為了方便我隨時(shí)切換操作系統(tǒng)(Ubuntu、Fedora、Win7),我在GitHub上注冊了一個(gè)賬號(hào),并把示例代碼放了上去。我切換系統(tǒng)后,只需要Fetch一下,就可以繼續(xù)開發(fā)。當(dāng)然,有興趣看我代碼的朋友,也可以clone一份到自己的機(jī)器上。地址:
https://github.com/littleloach/examples.git
關(guān)于Eclipse和Git的整合,請看這里:淺論Maven和Git的原理及展示其與Eclipse的集成
事實(shí)證明,Eclipse RCP不僅是跨平臺(tái)的,而且是跨版本的。也就是說,即使我在Fedora中使用了不一樣的Eclipse版本,我在Ubuntu中寫的代碼到Fedora中也不需要修改。(我在Win7中用的Eclipse版本和Ubuntu中的相同。)
Fedora 20和Fedora 19都是我喜歡的版本。因?yàn)樗鼈兊拈_發(fā)代號(hào)分別為Heisenberg和薛定諤的貓,不難看出,它們都和量子力學(xué)有關(guān),對我這樣的理工男吸引力那肯定是巨大的。而Heisenberg更具有傳奇性。不僅因?yàn)樗橇孔恿W(xué)的主要?jiǎng)?chuàng)始人和諾貝爾獎(jiǎng)獲得者,更因?yàn)闅v史上到現(xiàn)在都沒有定論的海森堡謎題:為什么當(dāng)初他沒有造出原子彈,是他真的搞錯(cuò)了,還是他故意搞錯(cuò)了?故事是這樣的:第二次世界大戰(zhàn)開始后,迫于納粹德國的威脅,丹麥的大物理學(xué)家玻爾離開了心愛的哥本哈根理論物理研究所,離開了朝夕相處的來自世界各地的同事,遠(yuǎn)赴美國。德國的許多科學(xué)家也紛紛背井離鄉(xiāng),堅(jiān)決不與納粹勢力妥協(xié)。然而,有一位同樣優(yōu)秀的物理學(xué)家卻留下來了,并被納粹德國委以重任,負(fù)責(zé)領(lǐng)導(dǎo)研制原子彈的技術(shù)工作,遠(yuǎn)在異鄉(xiāng)的玻爾憤怒了,他與這位過去的同事產(chǎn)生了尖銳的矛盾,并與他形成了終生未能化解的隔閡。有趣的是,這位一直未能被玻爾諒解的科學(xué)家卻在1970年獲得了“玻爾國際獎(jiǎng)?wù)?#8221;,而這一獎(jiǎng)?wù)率怯靡员碚?#8220;在原子能和平利用方面做出了巨大貢獻(xiàn)的科學(xué)家或工程師”的。歷史在此開了個(gè)巨大的玩笑,這玩笑的主人公就像他發(fā)現(xiàn)的“測不準(zhǔn)原理”一樣,一直讓人感到困惑和不解。他就是量子力學(xué)的創(chuàng)始人——海森堡。也許他當(dāng)初就是故意留下來而且故意造不出原子彈的吧。
在正式介紹JFace和結(jié)構(gòu)化數(shù)據(jù)之前,先向大家展示幾張F(tuán)edora的截圖。
第一張,F(xiàn)edora剛安裝完成后,其Gnome桌面是很丑的:
所以需要進(jìn)行一些Art Work,比如多安裝幾個(gè)wallpaper啦,多安裝幾個(gè)theme啦,然后安裝一個(gè)gnome-tweak-tool更改一下字體、字體微調(diào)和抗鋸齒啦。Fedora 19和20還有一個(gè)地方比較令人蛋疼,那就是它提供的gnome-terminal是不支持半透明背景的,如果想要半透明背景,只有自己編譯gnome-terminal。我太懶,所以只好把KDE中的konsole借來用用了。好在KDE程序在gnome中也可以正常運(yùn)行。下面是經(jīng)過我折騰后的Fedora 20:
有人說,GTK+程序的列表兩行之間的寬度大到可以塞進(jìn)一頭大象。以前在Ubuntu下用Eclipse不覺得,換到Fedora中發(fā)現(xiàn)這個(gè)寬度確實(shí)是挺大的,看著很不舒服。不過這都不是個(gè)事,換個(gè)GTK+的theme一切都搞定。下面一張是Eclipse的截圖,還是很漂亮的說:
下面開始今天的正題。
結(jié)構(gòu)化數(shù)據(jù)
結(jié)構(gòu)化數(shù)據(jù)可不是我一拍腦袋想出來的術(shù)語。平時(shí)我們在組織和保存數(shù)據(jù)的時(shí)候,離不開數(shù)據(jù)結(jié)構(gòu)。我們經(jīng)常使用的數(shù)組、鏈表或多維數(shù)組來保存數(shù)據(jù),也經(jīng)常使用數(shù)據(jù)庫的表格保存數(shù)據(jù),但是我們并不是時(shí)時(shí)刻刻想著結(jié)構(gòu)化這么一個(gè)概念。但是在Eclipse中就不一樣了,結(jié)構(gòu)化數(shù)據(jù)是它的一個(gè)很重要的理念,只要有可能,它都會(huì)把數(shù)據(jù)組織成結(jié)構(gòu)化的。比如,如果你在Eclipse中選擇了一些什么,那么這些選擇的東西就是以IStructuredSelection展示出來的。如下圖,可見Structured無處不在:
數(shù)組(包括鏈表)、樹和表格(包括多維數(shù)組)是我們常用的數(shù)據(jù)結(jié)構(gòu)。List控件、Tree控件和Table控件是我們常用的展示數(shù)據(jù)的控件。然而邏輯和形式并不總是一一對應(yīng)的。比如形式上可以用Tree控件表現(xiàn)的數(shù)據(jù),邏輯上并不一定是用樹結(jié)構(gòu)組織起來的數(shù)據(jù)。想想我們做數(shù)據(jù)庫的ORM映射,如果一個(gè)表中的一行數(shù)據(jù)和另一個(gè)表中的多行數(shù)據(jù)相關(guān)聯(lián)(一對多映射),寫成Java Entity類的時(shí)候就是一個(gè)Entity中包含有一個(gè)另一個(gè)Entity的List或Set。這兩個(gè)Entity各有各的Class Name,而不是統(tǒng)一叫TreeNode。這兩個(gè)Entity之間也不用getParent和getChildren互相訪問。但是從形式上,依然可以用一個(gè)Tree控件來展示它們,不是嗎?
JFace中的Viewer
在該系列博文的第02篇中,我展示了SWT的List控件和Tree控件。這兩個(gè)控件的使用是很簡單的,List控件只需要使用add()方法就可以在列表中添加一個(gè)字符串,Tree控件需要先構(gòu)造一個(gè)TreeItem對象,然后用setText()方法添加一個(gè)字符串。控件只能用來顯示字符串和字符串前面的一個(gè)小圖標(biāo),不能保存復(fù)雜的數(shù)據(jù)。MVC是常用的設(shè)計(jì)模式,Model用來保存數(shù)據(jù),而控件只是負(fù)責(zé)顯示,所以中間一定要有一個(gè)機(jī)制將Model中的數(shù)據(jù)轉(zhuǎn)換為能讓控件顯示的字符串和圖標(biāo),并且要能維持?jǐn)?shù)據(jù)在結(jié)構(gòu)化上的邏輯聯(lián)系。
JFace中的Viewer是對SWT中的控件的封裝,它使用的正是MVC模式。如果不了解Viewer的哲學(xué),使用Viewer就無從下手。Viewer的哲學(xué)是這樣的:Viewer對它要顯示的數(shù)據(jù)是怎么組織的沒有任何要求,可以使用它的setInput()方法將任何一個(gè)對象作為它的Model;很顯然Viewer不可能智能到顯示任何結(jié)構(gòu)的數(shù)據(jù),所以必須給它提供一個(gè)ContentProvider,ContentProvider就是負(fù)責(zé)將任意類型的Model轉(zhuǎn)化為結(jié)構(gòu)化的數(shù)據(jù);Model經(jīng)過ContentProvider轉(zhuǎn)化為結(jié)構(gòu)化數(shù)據(jù)后,Viewer再對該結(jié)構(gòu)中的每一項(xiàng)進(jìn)行顯示,前面提到過控件只能顯示文字和圖片,所以還需要一個(gè)LabelProvider來將數(shù)據(jù)轉(zhuǎn)化成要顯示的文字和圖片。
由上面的描述可以看出,要使用Viewer必須最少編寫三樣?xùn)|西:一些Model、一個(gè)ContentProvider和一個(gè)LabelProvider。當(dāng)然,Viewer還有更多的功能,比如對數(shù)據(jù)進(jìn)行排序和過濾,只需要提供相應(yīng)的SortProvider和FilterProvider即可,但這兩個(gè)Provider不是必須的。雖然要寫的類比較多,但是JFace都定義好了Interface,我們只需要實(shí)現(xiàn)即可,在IDE的幫助下,這個(gè)工作一點(diǎn)也不難。
一個(gè)簡單的示例
文字內(nèi)容寫得再多,也不如一個(gè)例子來得直接。我這里模擬HIS(Hospital Information System)系統(tǒng)中一個(gè)簡單的功能來展示Viewer的用法。在醫(yī)院里,一個(gè)住院部(Inpatient Department)可以有很多個(gè)科室(Department),比如內(nèi)科、外科、五官科等,一個(gè)科室里面可以有很多個(gè)病人(Patient)。很顯然,這些數(shù)據(jù)用一個(gè)TreeViewer顯示是再合適不過了。先看看這個(gè)示例程序界面的總體框架,如下圖:
這只是一個(gè)框架,結(jié)合上面的界面,我簡要說說我要實(shí)現(xiàn)的功能:
1、最左邊的視圖使用了一個(gè)TreeViewer,用來顯示住院部的所有科室和病人;
2、工具欄有兩個(gè)按鈕,如果最左邊的視圖中被選擇的對象是科室,則帶加號(hào)的按鈕可用,其功能是為該科室增加一個(gè)病人;如果最左邊的視圖中被選擇的對象是病人,則帶減號(hào)的按鈕可用,其功能是刪除這個(gè)病人;
3、中間的視圖使用一個(gè)TableViewer控件,當(dāng)最左邊的視圖中被選擇的對象是科室時(shí),則以表格的形式顯示該科室所有病人的詳細(xì)信息,每一行代表一個(gè)病人;
4、最右邊的視圖使用一個(gè)ListViewer控件,僅僅顯示當(dāng)前選擇的內(nèi)容。
為示例程序準(zhǔn)備數(shù)據(jù)
該示例程序用到的數(shù)據(jù)是一個(gè)住院部有多個(gè)科室、一個(gè)科室有多個(gè)病人,很顯然這是一個(gè)典型的一對多的關(guān)系。所以我們的Model需要三個(gè)類:Inpatient、Department、Patient。其關(guān)系如下:
其中的Gender是一個(gè)枚舉。下面是它們的代碼:




然后,創(chuàng)建一個(gè)PatientView類,在里面使用一個(gè)TreeViewer控件,創(chuàng)建一個(gè)Inpatient類的實(shí)例,然后添加一點(diǎn)點(diǎn)測試數(shù)據(jù),然后把Inpatient的實(shí)例作為viewer的input即可。如下圖:
當(dāng)然,如果沒有設(shè)置好ContentProvider和LabelProvider,數(shù)據(jù)是不可能正常顯示的。所以我們還需要?jiǎng)?chuàng)建一個(gè)PatientContentProvider類和PatientLabelProvider類。好在JFace中有早就定義好的接口,我們只需要從這些接口實(shí)現(xiàn)即可,如下圖:
我們先來看PatientContentProvider需要實(shí)現(xiàn)哪些方法,如下圖,可以看出IDE已經(jīng)自動(dòng)把框架搭建起來了,我們只需要填空即可(下圖中的空是我已經(jīng)填好了的):
第一個(gè)要實(shí)現(xiàn)的方法是getElements,該方法要求我們把Input的數(shù)據(jù)轉(zhuǎn)化成一個(gè)Object數(shù)組返回。在這個(gè)例子中,Input進(jìn)來的是Inpatient類的對象,所以只需要先將inputElement轉(zhuǎn)型為Inpatient,然后返回它里面的Department數(shù)組即可。第二個(gè)要實(shí)現(xiàn)的方法是hasChildren,在本例中,如果element是Department,則它就有children,如果是Patient就沒有children。最后要實(shí)現(xiàn)的一對方法就是getChildren和getParent,很顯然,如果element是Department,則它的children是它里面保存的List<Patient>,不過返回的時(shí)候要把它轉(zhuǎn)化成數(shù)組,如果element是Patient,則它的parent是它的department。So easy!不是嗎?
而PatientLabelProvider的實(shí)現(xiàn)就更簡單了,如下圖:
看圖填空,我們只需要完成getText方法和getImage方法即可。邏輯也很簡單,就是判斷element是Department還是Patient,然后分別返回相應(yīng)的字符串和圖片即可。在這里,我還故意把不同的性別顯示為不同的圖標(biāo)。有了截圖,我就不貼代碼了。
俗話說一通百通,使用不同的Model和Viewer,就給它不同的ContentProvider和LabelProvider即可。
處理Selection事件
關(guān)于數(shù)據(jù)的展示,我們輕松搞定。不過這還不算完,還有工作要做。現(xiàn)在數(shù)據(jù)顯示出來了,我們就可以用鼠標(biāo)來點(diǎn)擊,來選擇。那么我們選擇的數(shù)據(jù)怎么樣來影響我們程序的行為呢?在前面幾篇對GUI的探討中我說過,程序和用戶的交互是通過事件處理來進(jìn)行的。也就是說,當(dāng)我們選擇了Viewer中的一項(xiàng)或多項(xiàng)的時(shí)候,Viewer應(yīng)該發(fā)送一個(gè)Selection事件,我們向程序中注冊一個(gè)SelectionListener就應(yīng)該可以捕獲到這個(gè)事件。
為了讓patientViewer向程序發(fā)送Selection事件,我們需要讓它成為程序的selectionProvider。通過在PatientView中添加如下一行代碼(最后一行):
有了發(fā)送Selection事件的源,接著應(yīng)該有接受Selection事件的Listener。先從最簡單的功能做起。最右邊的視圖只是用來顯示所選擇的對象,所以從它先開始。最右邊的視圖類是SelectionView,里面用了一個(gè)ListViewer,在這里,我們先讓SelectionView實(shí)現(xiàn)ISelectionListener接口,然后把它注冊到Workbench中。如下圖,關(guān)鍵代碼我把它標(biāo)出來了:
selectionChanged()方法將在有目標(biāo)被選中的時(shí)候調(diào)用,該方法的實(shí)現(xiàn)也很簡單,就是把selection作為ListViewer的input,讓它顯示即可。而要使用ListViewer,我們又得為它提供一個(gè)ContentProvider和一個(gè)LabelProvider。下面是ContentProvider和LabelProvider的實(shí)現(xiàn),也很簡單,這次ContentProvider實(shí)現(xiàn)了IStructuredContentProvider接口,而且從代碼中可以看出,selection是一個(gè)IStructuredSelection,哪怕我們只選中一項(xiàng),它也是Structured的。
做好之后,運(yùn)行程序是這樣的效果:
然后再來實(shí)現(xiàn)中間那個(gè)視圖DepartmentDetailView的功能。在這個(gè)視圖中使用了一個(gè)TableViewer。使用TableViewer比使用ListViewer和TreeViewer要稍微復(fù)雜一點(diǎn),因?yàn)樾枰覀冏约禾砑颖砀竦牧校⒃O(shè)置表格的屬性。和SelectionView一樣,DepartmentDetailView實(shí)現(xiàn)ISelectionListener,并把自己注冊到Workbench中。代碼如下:
TableViewer依然需要一個(gè)ContentProvider和一個(gè)LabelProvider,不過沒有ITableContentProvider,因?yàn)門ableViewer是把每一行當(dāng)成一個(gè)element,所以它只需要和ListViewer一樣的ContentProvider即可。但是TableViewer需要的LabelProvider不一樣,要實(shí)現(xiàn)ITableLabelProvider,在這里,TableViewer才處理不同的列。如下圖:
完成后,運(yùn)行程序是這樣的效果:
根據(jù)用戶的選擇決定工具欄按鈕是否可用
終于進(jìn)入今天的最后一個(gè)議題了。 在前面的幾篇中,我陸續(xù)用過主菜單、視圖工具欄,今天用到了主工具欄。菜單和工具欄是GUI程序產(chǎn)生命令的主要方式,還有就是快捷鍵和彈出菜單。在Eclipse中,它們的本質(zhì)都一樣,都是通過關(guān)聯(lián)到一個(gè)command和一個(gè)handler來實(shí)現(xiàn)其功能,通過一個(gè)menuContribution來指定它們的位置。在今天的示例中,我把menuContribution的locationURI設(shè)置為toolbar:org.eclipse.ui.main.toolbar,所以這兩個(gè)按鈕就出現(xiàn)在了主工具欄中。如下圖:
在今天的示例中加入工具欄按鈕可不是為了將工具欄。今天的主題是結(jié)構(gòu)化數(shù)據(jù)。StructuredSelection也是結(jié)構(gòu)化的數(shù)據(jù)。所以在這個(gè)示例中加入工具欄是為了展示用戶選擇的數(shù)據(jù)會(huì)對工具欄按鈕的行為產(chǎn)生影響。從功能上講,我們希望當(dāng)用戶選擇一個(gè)科室的時(shí)候,添加病人的按鈕可用,而選擇多個(gè)科室或選擇一個(gè)或多個(gè)病人的時(shí)候,添加病人工具欄不可用,希望當(dāng)用戶選擇一個(gè)病人的時(shí)候,刪除病人按鈕可用,而當(dāng)選擇科室或多個(gè)病人的時(shí)候,刪除按鈕不可用。
這樣的需求在GUI編程中可以算是無處不在。Eclipse RCP是如何處理這個(gè)問題的呢?答案就是Workbench Core Expressions。下圖是Eclipse幫助文檔中關(guān)于Workbench Core Expressions的頁面:
然后,我們只需要對工具欄的按鈕添加如下的Workbench Core Expressions即可:
收工,今天就寫到這里。這篇文章已經(jīng)夠長了,截圖也有20多張了,所以實(shí)現(xiàn)那兩個(gè)按鈕的Handler我就不寫了。理解了結(jié)構(gòu)化數(shù)據(jù)和理解了IStructuredSelection后,理解Workbench Core Expression就很容易了,更詳細(xì)的內(nèi)容大家自己看幫助文檔吧。
寫到這里,Eclipse RCP的大部分UI元素如窗口、菜單、工具欄、視圖等等什么的都提到了,還有一個(gè)很重要的大件的UI building block就是編輯器了。等下次有時(shí)間,我再來詳細(xì)討論編輯器的編寫。