最近由於實驗室任務繁重,一直沒有繼續研究GEF,本來已經掌握的一些東西好象又丟掉了不少,真是無奈啊,看來還是要經常碰碰。剛剛接觸GEF的朋友大都會有這樣的印象:GEF裡概念太多,比較繞,一些能直接實現的功能非要拐幾個彎到另一個類裡做,而且很多類的名字十分相似,加上不知道他們的作用,感覺就好象一團亂麻。我覺得這種情況是由圖形化使用者介面(GUI)的複雜性所決定的,GUI看似簡單,實際上包含了相當多的邏輯,特別是GEF處理的這種圖形編輯方式,可以說是最複雜的一種。GEF裡每一個類,應該說都有它存在的理由,我們要儘可能瞭解作者的意圖,這就需要多看文檔和好的例子。
在Eclipse裡查看文檔和代碼相當便利,比如我們對某個類的用法不清楚,一般首先找它的注釋(選中類或方法按F2),其次可以查看它在其他地方用法(選中類或方法按Ctrl+Shift+G),還可以找它的原始碼(Ctrl+滑鼠左鍵或F3)來看,另外Ctrl+Shift+T可以按名稱尋找一個類等等。學GEF是少不了看代碼的,當然還需要時間和耐心。
好,閑話少說,下面進入正題。這篇文章將繼續上一篇內容,主要討論如何?DirectEdit、屬性頁面和大綱視圖,這些都是一個完整GEF應用程式需要提供的準系統。
實現DirectEdit
所謂DirectEdit(也稱In-Place-Edit),就是允許使用者在原本顯示內容的地方直接對內容進行修改,例如在Windows資源管理員裡選中一個檔案,然後按F2鍵就可以開始修改檔案名稱。實現DirectEdit的原理很直接:當使用者發出修改請求(REQ_DIRECT_EDIT)時,就在文字內容所在位置覆蓋一個文字框(也可以是下拉框,這裡我們只討論文本的情況)作為編輯器,編輯結束後,再將編輯器中的內容應用到模型裡即可。(作為類似的功能請參考:給表格的儲存格增加編輯功能)
圖1 Direct Edit
在GEF裡,這個彈出的編輯器由DirectEditManager類負責管理,在我們的NodePart類裡,通過覆蓋performRequest()方法響應使用者的DirectEdit請求,在這個方法裡一般要構造一個DirectEditManager類的執行個體(例子中的NodeDirectEditManager),並傳入必要的參數,包括接受請求的EditPart(就是自己,this)、編輯器類型(使用TextCellEditor)以及用來定位編輯器的CellEditorLocator(NodeCellEditorLocator),然後用show()方法使編輯器顯示出來,而編輯器中顯示的內容已經在構造方法裡得到。簡單看一下NodeCellEditorLocator類,它的關鍵方法在relocate()裡,當編輯器裡的內容改變時,這個方法被調用從而讓編輯器始終處於正確的座標位置。DirectEditManager有一個重要的initCellEditor()方法,它的主要作用是設定編輯器的初始值。在我們的例子裡,初始值設定為被編輯NodePart對應模型 (Node)的name屬性值;這裡還另外完成了設定編輯器字型和選中全部文字(selectAll)的功能,因為這樣更符合一般使用習慣。
在NodePart裡還要增加一個角色為DIRECT_EDIT_ROLE的EditPolicy,它應該繼承自DirectEditPolicy,有兩個方法需要實現:getDirectEditCommand()和showCurrentEditValue(),雖然還未遇到過,但前者的作用你不應該感到陌生--在編輯結束時產生一個Command對象將修改結果作用到模型;後者的目的是更新Figure中的顯示,雖然我們的編輯器覆蓋了Figure中的文本,似乎並不需要管Figure的顯示,但在編輯中時刻保持這兩個文本的一致才不會出現"蓋不住"的情況,例如當編輯器裡的文本較短時。
實現屬性頁面
在GEF裡實現屬性頁面和普通應用程式基底本一樣,例如我們希望可以通過屬性視圖(PropertyView)顯示和編輯每個節點的屬性,則可以讓Node類實現IPropertySource介面,並通過一個IPropertyDescriptor[]類型的成員變數描述要在屬性視圖裡顯示的那些屬性。有朋友問,要在屬性頁面裡增加一個屬性都該改哪些地方,主要是三個地方:首先要在你的IPropertyDescriptor[]變數裡增加對應的描述,包括屬性名稱和屬性編輯方式(比如文本或是下拉框,如果是後者還要指定選項列表),其次是getPropertyValue()和setPropertyValue()裡增加讀取屬性值和將結果寫入的代碼,這兩個方法裡一般都是像下面的結構(以前者為例):
public Object getPropertyValue(Object id) {
if (PROP_NAME.equals(id))
return getName();
if (PROP_VISIBLE.equals(id))
return isVisible() ? new Integer(0) : new Integer(1);
return null;
}
也就是根據要處理的屬性名稱做不同操作。要注意的是,下拉框類型的編輯器是以Integer類型資料代表選中項序號的,而不是int或String,例如上面的代碼根據visible屬性返回第零項或第一項,否則會出現ClassCastException。
圖2 屬性頁面
實現大綱視圖
在Eclipse裡,當編輯器(Editor)被啟用時,大綱視圖自動通過這個編輯器的getAdapter()方法尋找它提供的大綱(大綱實現IcontentOutlinePage介面)。GEF提供了ContentOutlinePage類用來實現大綱視圖,我們要做的就是實現一個它的子類,並重點實現createControl()方法。ContentOutlinePage是org.eclipse.ui.part.Page的一個子類,大綱視圖則是PageBookView的子類,在大綱視圖中有一個PageBook,包含了很多Page並可以在它們之間切換,切換的依據就是當前活動的Editor。因此,我們在createControl()方法裡要做的就是構造這個Page,簡化後的代碼如下所示:
private Control outline;
public OutlinePage() {
super(new TreeViewer());
}
public void createControl(Composite parent) {
outline = getViewer().createControl(parent);
getSelectionSynchronizer().addViewer(getViewer());
getViewer().setEditDomain(getEditDomain());
getViewer().setEditPartFactory(new TreePartFactory());
getViewer().setContents(getDiagram());
}
由於我們在構造方法裡指定了使用樹結構顯示大綱,所以createControl()裡的第一句就會使outline變數得到一個Tree(見org.eclipse.gef.ui.parts.TreeViewer的代碼),第二句把TreeViewer加到選擇同步器中,從而讓使用者不論在大綱或編輯地區裡選擇EditPart時,另一方都能自動做出同樣的選擇;最後三行的作用在以前的文章裡都有介紹,總體目的是把大綱視圖的模型與編輯地區的模型聯絡在一起,這樣,對於同一個模型我們就有了兩個視圖,體會到MVC的好處了吧。
實現大綱視圖最重要的工作基本就是這些,但還沒有完,我們要在init()方法裡綁定UNDO/REDO/DELETE等命令到Eclipse主視窗,否則當大綱視圖處於活動狀態時,主工具條上的這些命令就會變為不可用狀態;在 getControl()方法裡要返回我們的outline成員變數,也就是指定讓這個控制項出現在大綱視圖中;在dispose()方法裡應該把這個TreeViewer從選擇同步器中移除;最後,必須在PracticeEditor裡覆蓋getAdapter()方法,前面說過,這個方法是在Editor啟用時被大綱視圖調用的,所以在這裡必須把我們實現好的OutlinePage返回給大綱視圖使用,代碼如下:
public Object getAdapter(Class type) {
if (type == IContentOutlinePage.class)
return new OutlinePage();
return super.getAdapter(type);
}
這樣,樹型大綱視圖就完成了,見。很多GEF應用程式同時具有樹型和縮圖兩種大綱,實現的基本思路是一樣的,但代碼會稍微複雜一些,因為這兩種大綱一般要通過一個PageBook進行切換,縮圖一般由org.eclipse.draw2d.parts.ScrollableThumbnail負責實現,這裡暫時不講了(也許以後會詳細說),你也可以通過看logic例子的LogicEditor這個類的代碼來瞭解。
圖3 大綱視圖
P.S.寫這篇文章的時候,我對例子又做了一些修改,都是和這篇文章所說的內容相關的,所以如果你以前下載過,會發現那時的代碼與現在稍有不同(功能還是完全一樣的,下載)。另外要說一下,這個例子並不完善,比如刪除一個節點的時候,它的串連就沒同時刪除,一些鍵盤快速鍵不起作用,還存在很多被注釋掉的代碼等等。如果有興趣你可以來修改它們,也是不錯的學習途徑。