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

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

這兩個視圖中一個是List控件,一個是Tree控件,使用AbsoluteLayout。界面很簡單,根本不需要動用WindowBuilder這樣的神器,直接寫代碼即可,如下:


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


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

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

然后,添加一個ShowBundlesHandler類,繼承自org.eclipse.core.commands.AbstractHandler,代碼很簡單,如下:

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

再來實現另外一個菜單項的功能。先添加一個ShowExtensionsHandler類,仍然繼承自org.eclipse.core.commands.AbstractHandler。代碼如下:

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

更坑爹的是,Ubuntu中的RCP程序經常不顯示菜單欄,只有顯示Splash Screen的程序才能正常顯示菜單欄。如下兩圖,都是用Eclipse自帶的向導生成的程序,Ubuntu中沒有菜單欄,Win7中有:

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

下一篇:
Eclipse RCP詳解(03):SWT的相關概念以及一個連連看游戲的實現