上一篇:
Eclipse RCP詳解(01):Hello World以及Eclipse RCP和OSGi的簡(jiǎn)單展示 先來談?wù)勎覍?duì)GUI編程的體會(huì):
GUI編程的本質(zhì)和難題:
GUI編程一直都不簡(jiǎn)單,不管在什么平臺(tái)、用什么語言。而且項(xiàng)目越大就越難駕馭。如果追本溯源,會(huì)發(fā)現(xiàn)GUI編程的本質(zhì)就是窗口和消息循環(huán)。(本人比較了解Windows平臺(tái)和MFC框架,卻從未在X Window中使用任何庫編寫GUI程序,故以下探討皆來源于Windows系統(tǒng)的理論,如有不當(dāng),歡迎指正。)GUI不同于CLI,程序運(yùn)行時(shí)沒有固定的流程,程序界面必須能隨時(shí)響應(yīng)用戶的任何操作,當(dāng)然,也要響應(yīng)操作系統(tǒng)或其它程序?qū)Ρ境绦虻牟僮鳎虼顺绦蚪Y(jié)構(gòu)中必然存在一個(gè)循環(huán),在該循環(huán)中不停地獲取消息和響應(yīng)消息。在Windows系統(tǒng)中,一切界面元素,不管是對(duì)話框、文本框,還是菜單欄、工具欄按鈕,其本質(zhì)都是窗口。如果使用EnumWindow這個(gè)API枚舉一下操作系統(tǒng)中的窗口,會(huì)發(fā)現(xiàn)系統(tǒng)中的窗口數(shù)量遠(yuǎn)遠(yuǎn)多于我們見到的窗口數(shù)量。每一個(gè)窗口都能接受發(fā)送給它的消息,每一個(gè)類型的窗口都有一個(gè)消息處理函數(shù)用來處理發(fā)送給它的消息。窗口之間通過互相發(fā)送消息而通訊。
以上所描述的GUI系統(tǒng)機(jī)制看似脈絡(luò)清晰、簡(jiǎn)單自然,但是卻沒有多少人愿意用這些反撲歸真的底層API去編寫自己的GUI程序。因?yàn)槠渲刑N(yùn)含著兩個(gè)難題:1、直接使用底層API非常麻煩,必須經(jīng)過適當(dāng)?shù)姆庋b;2、程序越大,則窗口對(duì)象越多,它們之間的通訊越復(fù)雜,如果不經(jīng)過精心組織,則非常難以管理。
為了解決以上難題,不同平臺(tái)、不同編程語言及不同的程序庫都會(huì)對(duì)底層API進(jìn)行封裝。這些封裝基本上都會(huì)從兩個(gè)方面著手:1、將不同的窗口封裝為不同的控件,大的如Frame、Dialog,小的如Button、Label,并且都提供方便的方法來設(shè)置這些控件的外觀;2、提供某種事件機(jī)制響應(yīng)用戶對(duì)GUI的操作。
有人可能會(huì)說Java中的Swing沒有使用底層API,它所有的控件都是自己畫出來的。但是Java對(duì)GUI的封裝依然離不開我上面說的兩點(diǎn)。
不同編程語言特性帶來的難度:
由于編程語言自身的特性,在使用封裝好的GUI組件時(shí)仍然有不同的難易性。最簡(jiǎn)單的應(yīng)該算是VB之類,拖拖拉拉就可以創(chuàng)建一個(gè)GUI界面,要處理哪個(gè)控件的事件就設(shè)置哪個(gè)控件的事件處理函數(shù),其次是JavaScript,先使用HTML定義好界面,然后定義各個(gè)控件的事件處理函數(shù)即可。不過上面所說的簡(jiǎn)單也只能算是入門簡(jiǎn)單,如果要寫大規(guī)模的程序,它們?nèi)匀槐苊獠涣舜罅康目丶o法合理組織管理這樣的難題。
最難的應(yīng)該算是C++,因?yàn)樗畹讓印FC對(duì)各種控件都做了不錯(cuò)的封裝,也提供了SDI、MDI和基于對(duì)話框的程序這樣的框架,大大簡(jiǎn)化了使用C++開發(fā)GUI程序的難度,但它依然還是最難的。在MFC中是通過消息映射來處理事件的,如果要處理某個(gè)窗口的事件,必須得創(chuàng)建自己的窗口類,并定義大量的事件處理函數(shù)。對(duì)于大規(guī)模的程序,仍然有大量對(duì)象不好合理組織的難題。
用Java語言編寫GUI程序介于以上兩種之間。Java語言的面向?qū)ο蠓庋b做得非常好,GUI編程理解起來不難。其流程也是先使用諸如JFrame、JButton、JText之類的控件構(gòu)建一個(gè)GUI界面,然后再處理各個(gè)控件的事件。但是也正是因?yàn)镴ava語言在面向?qū)ο筇匦苑矫娴倪^分完美,給GUI編程帶來的額外的復(fù)雜性。其表現(xiàn)體現(xiàn)在兩個(gè)方面:1、在Java中一切皆是對(duì)象,所以在設(shè)置一個(gè)控件的事件處理器時(shí),傳遞給它的必須也是一個(gè)對(duì)象,而前面提到的幾種編程語言都可以直接設(shè)置事件處理器為一個(gè)函數(shù),可以把函數(shù)或函數(shù)指針當(dāng)參數(shù)傳遞。所以在使用Java編寫GUI程序時(shí)往往需要編寫大量的EventHandler類,哪怕編寫這些類只是為了定義一個(gè)函數(shù),然后實(shí)例化出一個(gè)對(duì)象。當(dāng)然,隨著Java語言的不斷改進(jìn),我們可以通過內(nèi)部類、匿名類、Lambda表達(dá)式來減少編寫EventHandler類的工作,在Java8中,甚至可以直接傳遞方法引用。個(gè)人認(rèn)為,以上語言特性在提供了方便的同時(shí),有點(diǎn)破壞Java面向?qū)ο缶幊痰耐昝佬浴?、對(duì)象可見性的問題,不像VB、JavaScript,定義一個(gè)全局變量后則全世界都可以訪問到,在Java中則不行,一個(gè)對(duì)象要訪問另一個(gè)對(duì)象學(xué)問可就深了,特別是為了解決上一個(gè)問題而出現(xiàn)的內(nèi)部類、匿名類,它們想要訪問外部類外面的對(duì)象容易嗎?Java中的閉包就是為了解決這樣的問題,但我認(rèn)為這不是完美的解決方案。
設(shè)計(jì)模式在GUI編程中的應(yīng)用:
在前面的探討中,我反復(fù)提到一旦程序規(guī)模過大,則大量的對(duì)象難以管理,這些對(duì)象之間的相互通訊更是錯(cuò)綜復(fù)雜。這個(gè)時(shí)候就必須靠設(shè)計(jì)模式來解決問題了。首先,幾乎所有的GUI程序都是按照樹形結(jié)構(gòu)組織起來的,父控件包含子控件,子控件再包含更多的子控件,這樣一級(jí)一級(jí)的包含下去。最后只需要把頂層的控件暴露出來,就可以順著路徑找到其它的控件。其次,MVC模式在GUI編程中廣泛使用。比如MFC中的SDI和MDI,都有一個(gè)Document對(duì)象,修改Document對(duì)象的狀態(tài)后,再刷新視圖的顯示。最后,還有眾多的諸如工廠模式、單例模式、監(jiān)聽器模式、過濾器模式等等在GUI編程中廣泛使用。
編寫GUI程序不能從零開始,要適當(dāng)使用框架: 這一點(diǎn)應(yīng)該不需要仔細(xì)論證。這也是我們使用Eclipse RCP編程的初衷。
Eclipse的Runtime和UI: 我們可以認(rèn)為Eclipse RCP是一個(gè)很適合我們使用的編寫GUI程序的起點(diǎn)。在本篇博文中,我正是要對(duì)Eclipse的Runtime和UI進(jìn)行探討。因?yàn)椴豢赡芤幌伦訉懙煤苌钊耄栽跇?biāo)題中稱為“初探”。Eclipse的Runtime提供了很多功能,比如對(duì)配置首選項(xiàng)的支持、對(duì)ContentType的支持、對(duì)安全存儲(chǔ)的支持、對(duì)結(jié)構(gòu)化文本的支持、對(duì)網(wǎng)絡(luò)和代理的支持、甚至還有一個(gè)多線程框架(貌似已經(jīng)deprecated),這些功能我統(tǒng)統(tǒng)不感冒。(當(dāng)然,如果程序的功能正好需要這些底層支持,用一下也無妨。)Eclipse的Runtime最引人注目的當(dāng)然是它的插件機(jī)制了。插件機(jī)制可以帶來這樣的優(yōu)點(diǎn):開發(fā)項(xiàng)目時(shí),我們可以先編寫一個(gè)核心的程序?qū)崿F(xiàn)最基本的功能,然后把程序需要實(shí)現(xiàn)的其它功能進(jìn)行劃分,每一個(gè)功能單獨(dú)做成一個(gè)Plugin插入到核心中即可。不難看出,使用這樣的結(jié)構(gòu)的程序添加和刪除功能都非常方便,可以大大降低項(xiàng)目失敗的風(fēng)險(xiǎn)。Eclipse的UI更是提供了獨(dú)具一格的界面風(fēng)格和程序框架,使用Eclipse RCP,我們當(dāng)然不需要從頭開始實(shí)現(xiàn)一個(gè)窗口,只需要添添補(bǔ)補(bǔ),就可以讓程序擁有完美的菜單、工具欄、視圖、編輯器等一系列的界面元素。
要使用Eclipse的Runtime和UI提供的功能,我們需要熟悉兩個(gè)工具類,它們是:
org.eclipse.core.runtime.Platform
org.eclipse.ui.PlatformUI
我們先來熟悉一下Eclipse的UI是怎么組織的,以及其中的一些基本概念。在上一篇中,我展示了一個(gè)HelloWorld程序,通過該程序,應(yīng)該不難了解什么是視圖。除了視圖之外,在HelloWorld程序中還有一個(gè)編輯器區(qū)域,但是我們沒有實(shí)現(xiàn)任何編輯器。視圖和編輯器放在一個(gè)稱為Workbench Page的容器中,一個(gè)Workbench Page可以容納多個(gè)編輯器和視圖。Workbench Page又是放在一個(gè)稱為Workbench Window的容器中。Workbench Window則是我們程序的主窗口,Workbench Window中還可以包含菜單欄、工具欄及狀態(tài)欄。不同于其它框架開發(fā)出的程序,Eclipse還有一個(gè)比較有特色的UI特性,那就是每一個(gè)視圖都可以有自己的菜單欄和工具欄。經(jīng)常使用Eclipse的開發(fā)者對(duì)以上概念應(yīng)該不陌生。Eclipse各個(gè)組件的層次關(guān)系如下圖:

Workbench Window其實(shí)還有上一級(jí),那就是Workbench。理論上講,一個(gè)Workbench可以有多個(gè)Workbench Window,但是同一時(shí)刻只能有一個(gè)Workbench window是Active的,也只有一個(gè)是可見的。事實(shí)上,我們見到的程序一般都只有一個(gè)Workbench Window。同樣,一個(gè)Workbench Window可以有多個(gè)Workbench Page,但同一時(shí)刻,也只有一個(gè)是Active的和可見的。以上情況不同于視圖,因?yàn)槎鄠€(gè)視圖可以同時(shí)顯示。我們把一組視圖和編輯器的初始布局稱為透視圖(Perspective)。Workbench Window可以切換Page,每一個(gè)Page初始顯示時(shí)都是以透視圖為模板來組織視圖和編輯器。
當(dāng)我們需要使用Workbench、Workbench Window、Workbench Page的功能時(shí),可以使用如下接口:
org.eclipse.ui.IWorkbench
org.eclipse.ui.IWorkbenchWindow
org.eclipse.ui.IWorkbenchPage
以上接口不需要我們自己去實(shí)現(xiàn),這些接口指向的對(duì)象也不需要我們?nèi)?shí)例化,只需要調(diào)用PlatformUI.getWorkbench()、PlatfromUI.getWorkbench().getActiveWorkbenchWindow()以及PlatfromUI.getWorkbench().getActiveWorkbenchWindow().getActivePage()即可分別得到以上對(duì)象。(當(dāng)然,也可以通過Id獲得不是Active的Window和Page。)
除了窗口、視圖和編輯器,Eclipse UI中的另外一部分就是菜單和工具欄了。在SWT中,菜單欄、菜單項(xiàng)、工具欄、工具欄按鈕屬于最基本的控件。直接使用這些控件是可行的,但是肯定很麻煩,所以JFace對(duì)它們做了一次封裝。封裝后就成了Action。在我五年前寫的文章中,菜單和工具欄就是使用Action實(shí)現(xiàn)的。但是Action還是沒有把菜單、工具欄的外觀和行為徹底分開,所以Eclipse中又出現(xiàn)了command和handler機(jī)制。
在Helloworld程序中,大家已經(jīng)看到了HelloWorldView是怎么通過定義org.eclipse.ui.views擴(kuò)展添加到程序界面中的。菜單和工具欄也可以通過定義相應(yīng)的擴(kuò)展而增加到Workbench Window中。在我五年前寫的文章中我們使用了org.eclipse.ui.actionSets擴(kuò)展。今天,我們不使用Action,而是使用全新的Command機(jī)制。
不同于Action,Commands通過三步有效的達(dá)到界面表現(xiàn)和內(nèi)部實(shí)現(xiàn)的分離:首先,通過 org.eclipse.ui.commands 擴(kuò)展點(diǎn)創(chuàng)建命令和類別(Category),并且可以把某些命令放在一個(gè)類別(Category)中;然后,通過 org.eclipse.ui.menus 指定命令出現(xiàn)在界面的哪個(gè)區(qū)域(視圖菜單/主菜單/上下文菜單);最后通過 org.eclipse.ui.handlers 指定命令的實(shí)現(xiàn)。因此,Eclipse 推薦使用Commands 來創(chuàng)建菜單。在定義org.eclipse.ui.menus擴(kuò)展時(shí),需要使用到locationURI來指定菜單出現(xiàn)的位置,locationURI的格式不難,大家可以看Eclipse的參考文檔。
下面開始看實(shí)例。為了展示Eclipse的底層功能,我設(shè)計(jì)了這樣一個(gè)例子:在程序中有兩個(gè)視圖和兩個(gè)菜單項(xiàng),其中一個(gè)視圖中是一個(gè)SWT的List控件,當(dāng)點(diǎn)擊菜單項(xiàng)“Show Bundles”時(shí),在這個(gè)List中顯示所有的Bundle,另一個(gè)視圖中是一個(gè)SWT的Tree控件,當(dāng)點(diǎn)擊菜單項(xiàng)“Show Extensions”時(shí),在這個(gè)Tree中顯示所有的擴(kuò)展點(diǎn)和擴(kuò)展。
第一步,先實(shí)現(xiàn)兩個(gè)視圖的界面,如下圖:

這兩個(gè)視圖中一個(gè)是List控件,一個(gè)是Tree控件,使用AbsoluteLayout。界面很簡(jiǎn)單,根本不需要?jiǎng)佑肳indowBuilder這樣的神器,直接寫代碼即可,如下:


這個(gè)程序的界面和HelloWorld相比有兩處不同,一是窗口變大了,二是沒有顯示編輯器區(qū)域。是通過修改ApplicationWorkbenchWindowAdvisor.java和Perspective.jave文件實(shí)現(xiàn)的,如下圖:


怎么在plugin.xml中通過Extension來顯示這兩個(gè)視圖,我就不截圖了。下面來看怎么定義菜單。下面是定義完Commands、Bindings和Menus的界面
:
可以看到,locationURI使用的是menu:org.eclipse.ui.main.menu?after=additions,就是把菜單添加到主窗口的菜單欄中。
下面是定義完菜單程序的運(yùn)行效果,因?yàn)闆]有實(shí)現(xiàn)Handler,所以菜單項(xiàng)顯示為灰色:

下面,來實(shí)現(xiàn)Handler。首先實(shí)現(xiàn)顯示Bundles的功能。在上一篇中我講過,Bundles是OSGi的概念,如果想知道我們的程序有哪些Bundles,可以通過Activator的start方法的context參數(shù)得到。所以,我們先改寫Activator類,在里面添加一個(gè)static的List<String>,在start方法中獲取所有的bundles,并將它們的SymbolicName保存到List<String>中,如下圖:

然后,添加一個(gè)ShowBundlesHandler類,繼承自org.eclipse.core.commands.AbstractHandler,代碼很簡(jiǎn)單,如下:

在plugin.xml中定義org.eclipse.ui.handlers擴(kuò)展,如下圖:

再來實(shí)現(xiàn)另外一個(gè)菜單項(xiàng)的功能。先添加一個(gè)ShowExtensionsHandler類,仍然繼承自org.eclipse.core.commands.AbstractHandler。代碼如下:

以上代碼展示了怎么使用Platform類來獲得所有的擴(kuò)展點(diǎn)和擴(kuò)展。同時(shí)也展示了SWT的Tree控件使用起來也很簡(jiǎn)單。
下面是程序的運(yùn)行效果:
補(bǔ)充內(nèi)容: Eclipse在Ubuntu下的表現(xiàn)并不完美,甚至有些Bug讓我走了不少彎路。
Eclipse在Ubuntu中的菜單項(xiàng)是不能顯示圖標(biāo)的,如下兩圖,左邊是Ubuntu中Eclipse的菜單,沒有圖標(biāo),右邊是Win7中的菜單,有圖標(biāo):(同樣的Eclipse 3.8.1版)

更坑爹的是,Ubuntu中的RCP程序經(jīng)常不顯示菜單欄,只有顯示Splash Screen的程序才能正常顯示菜單欄。如下兩圖,都是用Eclipse自帶的向?qū)傻某绦颍琔buntu中沒有菜單欄,Win7中有:

所以,我上面的示例程序并不是從Hello RCP模板擴(kuò)展而來的,而是從RCP Mail Template創(chuàng)建,然后刪掉不要的功能,再然后擴(kuò)展而來的。如下圖:

下一篇:
Eclipse RCP詳解(03):SWT的相關(guān)概念以及一個(gè)連連看游戲的實(shí)現(xiàn)