在這個文章系列中,第一部分向讀者介紹了Eclipse的插件開發環境,并開發了一個簡單的插件;第二部分添加了工具欄按鈕、一個菜單項和對話框。一個小發明并不能為我們做什么事情,它僅僅將樣本信息按照某個字體格式顯示給我們,那么我們要做的是教會它去處理實際的數據。我們要通知那些插件,讓它們按照我們的需要來工作。本文討論了如何來定制一個編輯文件的向導。
一 Invokatron的故事
首先,我們來了解一下Invokatron,在前面的文章中,我們也提到了Invokatron是一個產生Java代碼的圖形化工具,用這個工具,你可以方便的采用拖放方式來創建一個Java Class。這個“dragged-in”方法被編輯好的方法調用(invoked),這就是它名字的由來。我們將采用數據驅動的方式來設計我們的系統。
在下面的章節中,我們將開發這個圖形應用接口程序。從現在開始我們需要列出插件輸入和存儲的重要數據。這就是我們常說的應用程序的模型。在接下來的文章中,我們將制作這個圖形化界面。目前我們需要考慮的僅僅是提取出哪些信息是我們的插件需要存放的重要數據。這部分通常被稱作是應用的模型
(Model)部分。以下是設計系統時需要考慮的東西:
●????????哪部分數據是需要保存的?
●????????這些數據用什么樣的存儲結構來表示他們?POJO,JavaBean,還是EJB?
●????????這些數據如何進行持久化?用數據庫中的表,XML文件,屬性文件還是一個序列化的二進制流文件?
●????????數據的讀取采用哪些途徑?使用新建文件向導,其他類型的向導,一個彈出式的對話框,一個圖形化的編輯器,文本編輯器還是使用文件屬性頁?
在我們繼續前行之前,這些問題必須解決。沒有一種答案是適用于所有項目的,這些答案是基于項目需求。我為我們的項目做了以下的定義:
●????????一個Java類包含類名,一個包,一個父類,并實現某些接口。我們先定義這些,在后面的文章中將添加一些其他的數據信息。
●????????這些數據將由一個繼承自Properties類的子類來表示。這些構成了編輯器的“文檔類”。
●????????我們將采用一類屬性文件來實現轉換。利用Properties類,這樣的操作很容易實現。
●????????采用一個新建文件向導來初始化數據,接著,我們讓用戶在屬性窗口或者文本編輯器中修改數據。這步操作將在下面的文檔中進行說明。
二 文檔類
接下來要做的是寫文檔類。創建一個名為invokatron.model的包,然后創建一個名為InvokatronDocument的類。這是我們最初的文檔類:
public class InvokatronDocument
????????extends Properties
{
????public static final String PACKAGE = "package";
????public static final String SUPERCLASS = "superclass";
????public static final String INTERFACES = "interfaces";
}
使用Properties類可以簡單的實現數據的轉換和保存操作。其中,getter和setter方法并不是必須的,如果你想添加這些方法的話,你可以添加它們。這個類并沒有寫完,在后面,我們將給它加上Eclipse需要使用的接口。對這個類而言,要取得一個屬性,只需要這樣的操作:
String package =
????document.getProperty(InvokatronDocument.PACKAGE);
三 定制一個向導
看一下我們在之前的文章中使用到的向導(如果你現在還沒有源代碼,請在這里下載)。記住,你可以通過我們添加的工具欄按鈕或者菜單項來打開這個向導。請看圖1:

圖1 以前的向導
這個向導只有一頁,在右上角也沒有圖片。我們想添加更多的信息并且有個漂亮的圖片。換句話說,我們想定制這個向導。
首先讓我們來剖析一下向導。打開InvokatronWizard.java文件,注意這個類繼承了Wizard并且實現了InewWizard接口。這里有很多你必須知道的方法。定制我們的向導,只需要簡單的調用或者重寫這些方法。這里有一些重要介紹:
3.1 關于生命期的方法
我們需要重寫這些方法來加入關于那些初始化和銷毀定制的向導代碼。
●????????構造器。這個方法將在向導進行實例化的時候、并且Eclispe向它傳送信息之前調用。是向導的常規信息初始化的實現。通常情況下你會調用“美化方法”(見下面)來給對話框做默認設置。
●????????init(IWorkbench workbench, IStructuredSelection editorSelection):這個方法由Eclipse調用,它給向導提供一些工作取信息。重寫這個方法來處理Iworkbench和之后將使用的對象。如果這是個編輯向導而不是新建向導,我們需要將當前編輯器的選擇項也作為一個參數。
●????????dispose():由Eclipse調用來處理回收。重寫這個方法來回收向導所占用的資源。
●????????finalize():對于回收處理,建議采用dispose()方法調用。
3.2 美化方法
以下的方法將裝飾這個向導窗口:
●????????setWindowTitle(String title):調用這個方法來取得標題欄字符。
●????????setDefaultPageImageDescriptor(ImageDescriptor image):調用這個方法來取得顯示在所有向導頁右上角的圖片。
●????????setTitleBarColor(RGB color):調用這個方法來來定義標題欄的顏色。
3.3 按鈕的常用方法
下面的方法控制向導中的按鈕的可用性和它的操作。
●????????boolean canFinish():重寫這個方法,根據向導的狀態來指明Finish按鈕是否可用。
●????????boolean performFinish():重寫這個方法,實現這個向導最根本的業務邏輯。如果向導不能完成(產生錯誤)則返回false。
●????????boolean performCancel():重寫這個方法,當用戶單擊Cancel按鈕的時候做清除操作。如果這個向導不能取消,返回false。
●????????boolean isHelpAvailable():重寫這個方法,定義Help按鈕是否可見。
●????????boolean needsPreviousAndNextButtons():重寫這個方法,定義Previous和Next按鈕是否可見。
●????????boolean needsProgressMonitor():重寫這個方法,指明過程監聽器控件是否可見。它將在單擊“Finish”按鈕后調用performFinish()方法的時候出現。
3.4 頁面的常用方法
下面的方法將控制頁面的出現:
●????????addPages():當向導框出現的時候調用。重寫這個方法給向導添加新頁面。
●????????createPageControls(Composite pageContainer):Eclipse調用這個方法將向導中的所有頁面(由上面的addPages()方法添加的)實例化。重寫這個方法,添加向導中一直可見的控件(不僅僅是頁面)。
●????????IWizardPage getStartingPage():重寫這個方法來定義向導的第一個頁面。
●????????IWizardPage getNextPage(IWizardPage nextPage):在默認情況下,單擊Next按鈕將得到addPages()方法提供的頁面數組中的下一頁。你可能需要根據用戶的選擇而轉向不同的頁面。重寫這個方法來得到下個頁面。
●????????IWizardPage getPreviousPage(IWizardPage previousPage):與getNextPage()方法類似,用來計算得到前一個頁面。
●????????int getPageCount():返回使用addPages()方法添加的頁面個數。你不需要重寫這個方法,除非你想顯示和頁面實際數量不一致的數量。
3.5 其他有用的方法
這里有很多有用的輔助性方法:
●????????setDialogSettings(IDialogSettings settings):你可以加載對話框的設置并調用init()方法來發布其中的數據。典型的,向導中的問題設置是默認的,在這里(DialogSettings)察看更多的信息。
●????????IDialogSettings getDialogSettings():使用這個方法可以找回需要的數據。在performFinish()方法的最后,你可以將數據再次存入文件。
●????????IWizardContainer getContainer():可以得到Shell對象,運行后臺線程,刷新窗口,等等。
3.6 向導頁方法
我們已經看到,一個向導是一個或者多個頁面的組合。這些頁面擴展了WizardPage類并實現了IwizardPage接口。為了定制一個個性化的向導,還有很多方法是必須掌握的。這里介紹一些比較重要的方法:
●????????構造器:初始化頁面
●????????dispose():重寫這個方法,實現清除代碼。
●????????createControl(Composite parent):重寫這個方法,為這個頁面添加控制器。
●????????IWizard getWizard():用來得到向導對象。在調用getDialogSettings()方法的時候有用。
●????????setTitle(String title):為向導的標題區提供顯示的文字。
●????????setDescription(String description):提供顯在在標題文字下的信息。
●????????setImageDescriptor(ImageDescriptor image):得到一個圖片,用來取代西安在在向導右上角的默認圖片。
●????????setMessage(String message):在描述信息的下方顯示的文本信息,一般用來提示或者警告用戶。
●????????setErrorMessage(String error):在描述信息的下面高亮度顯示一個字符串。它表示向導在處理完錯誤信息之前不能做下一步的操作。
●????????setPageComplete(boolean complete):如果輸入參數是true,那么Next按鈕可以使用。
●????????performHelp():重寫這個方法,提供內容敏感的幫助。這個將在Help按鈕被按下的時候被向導調用。
四 為向導編碼
這些方法使得我們開發一個可擴展的向導成為可能。我們現在來修改這個在前面文章中創建的Invokatron向導,給他添加一個頁面來請求原始的文檔數據,并添加一個圖片。下面的代碼中粗體部分是新增加的:
public class InvokatronWizard extends Wizard
????????implements INewWizard {
????private InvokatronWizardPage page;
????private InvokatronWizardPage2 page2;
????private ISelection selection;
????public InvokatronWizard() {
????????super();
????????setNeedsProgressMonitor(true);
????????ImageDescriptor image =
????????????AbstractUIPlugin.
????????????????imageDescriptorFromPlugin("Invokatron",
?????????????????? "icons/InvokatronIcon32.GIF");
????????setDefaultPageImageDescriptor(image);
????}
????public void init(IWorkbench workbench,
????????????IStructuredSelection selection) {
????????this.selection = selection;
????}
在構造函數中,我們打開了過程監聽器并為向導設置了圖片。你可以通過右鍵下載這個新圖標:

將這個圖標保存在Invokatron/icons文件夾下。為了促進圖片的加載,我們使用了AbstractUIPlugin.imageDescriptorFromPlugin()方法。
注意:你必須知道,盡管這個向導是InewWizard類型的向導,但并不是所有的向導是用來產生新文檔的。要想知道怎么去顯示一個“獨立”的向導,請參看文章最后的資源部分。
接下來是addPages()方法:
????public void addPages() {
????????page=new InvokatronWizardPage(selection);
????????addPage(page);
????????page2 = new InvokatronWizardPage2(
????????????selection);
????????addPage(page2);
????}
在這個方法中,我們添加了一個名為InvokatronWizardPage2的新頁面,我們待會就會寫這個頁面。接下來是用戶按下向導中的Finish按鈕后會調用的方法:
????
public boolean performFinish() {
????????//First save all the page data as variables.
????????final String containerName =
????????????page.getContainerName();
????????final String fileName =
????????????page.getFileName();
????????final InvokatronDocument properties =
????????????new InvokatronDocument();
????????properties.setProperty(
????????????InvokatronDocument.PACKAGE,
????????????page2.getPackage());
????????properties.setProperty(
????????????InvokatronDocument.SUPERCLASS,
????????????page2.getSuperclass());
????????properties.setProperty(
????????????InvokatronDocument.INTERFACES,
????????????page2.getInterfaces());
????????//Now invoke the finish method.
????????IRunnableWithProgress op =
????????????new IRunnableWithProgress() {
????????????public void run(
????????????????????IProgressMonitor monitor)
????????????????????throws InvocationTargetException {
????????????????try {
????????????????????doFinish(
????????????????????????containerName,
????????????????????????fileName,
????????????????????????properties,
????????????????????????monitor);
????????????????} catch (CoreException e) {
????????????????????throw new InvocationTargetException(e);
????????????????} finally {
????????????????????monitor.done();
????????????????}
????????????}
????????};
????????try {
????????????getContainer().run(true, false, op);
????????} catch (InterruptedException e) {
????????????return false;
????????} catch (InvocationTargetException e) {
????????????Throwable realException =
????????????????e.getTargetException();
????????????MessageDialog.openError(
????????????????getShell(),
????????????????"Error",
????????????????realException.getMessage());
????????????return false;
????????}
????????return true;
????}
現在我們需要作一些數據保存的工作。這個工作將被向導的容器(Eclipse工作區)執行,所以必須實現IrunnableWithProgress接口,這個接口只包含一個run()方法。IprogressMonitor允許輸出任務的過程信息。這是我們接下來將會看到的。真正的數據保存操作在輔助性方法中,doFinish():
????
private void doFinish(
????????String containerName,
????????String fileName,
????????Properties properties,
????????IProgressMonitor monitor)
????????throws CoreException {
????????// create a sample file
????????monitor.beginTask("Creating " + fileName, 2);
????????IWorkspaceRoot root = ResourcesPlugin.
????????????getWorkspace().getRoot();
????????IResource resource = root.findMember(
????????????new Path(containerName));
????????if (!resource.exists() ||
????????????!(resource instanceof IContainer)) {
????????????throwCoreException("Container \"" +
????????????????containerName +
????????????????"\" does not exist.");
????????}
????????IContainer container =
????????????(IContainer)resource;
????????final IFile iFile = container.getFile(
????????????new Path(fileName));
????????final File file =
????????????iFile.getLocation().toFile();
????????try {
????????????OutputStream os =
????????????????new FileOutputStream(file, false);
????????????properties.store(os, null);
????????????os.close();
????????} catch (IOException e) {
????????????e.printStackTrace();
????????????throwCoreException(
????????????????"Error writing to file " +
????????????????file.toString());
????????}
????????//Make sure the project is refreshed
????????//as the file was created outside the
????????//Eclipse API.
????????container.refreshLocal(
????????????IResource.DEPTH_INFINITE, monitor);
????????monitor.worked(1);
????????monitor.setTaskName(
????????????"Opening file for editing...");
????????getShell().getDisplay().asyncExec(
????????????new Runnable() {
????????????public void run() {
????????????????IWorkbenchPage page =
????????????????????PlatformUI.getWorkbench().
????????????????????????getActiveWorkbenchWindow().
????????????????????????getActivePage();
????????????????try {
????????????????????IDE.openEditor(
????????????????????????page,
????????????????????????iFile,
????????????????????????true);
????????????????} catch (PartInitException e) {
????????????????}
????????????}
????????});
????????monitor.worked(1);
????}
這里,我們做了很多工作:
●????????我們得到了想保存的這個文件的路徑(作為Eclipse的IFile類)
●????????我們也得到了與之等價的File
●????????我們將屬性保存到了相應的路徑下
●????????接著,我們請求Eclipse工作臺刷新項目,這樣,這個新的文件就顯示出來了。
●????????最后,我們制定為將來制定了一個工作計劃。這些工作包括在編輯器中打開一個新的文件。
●????????我們傳遞一個參數,調用IprogressMonitor對象的方法,使用戶能接收到整個工作過程的信息。
最后一個方法是當保存文件失敗的情況下,在向導中顯示錯誤信息的輔助性方法:
????
private void throwCoreException(
????????????String message) throws CoreException {
????????IStatus status =
????????????new Status(
????????????????IStatus.ERROR,
????????????????"Invokatron",
????????????????IStatus.OK,
????????????????message,
????????????????null);
????????throw new CoreException(status);
????}
}
一個CoreException被向導捕獲,接著,它所包含的Status對象將信息呈現給用戶。這時,向導并沒有關閉。
五 為新建向導頁編程
接著,我們來寫InvokatronWizardPage2。這個類是一個新添加的文件:
public class InvokatronWizardPage2 extends WizardPage {
????private Text packageText;
????private Text superclassText;
????private Text interfacesText;
????private ISelection selection;
????public InvokatronWizardPage2(ISelection selection) {
????????super("wizardPage2");
????????setTitle("Invokatron Wizard");
????????setDescription("This wizard creates a new"+
????????????" file with *.invokatron extension.");
????????this.selection = selection;
????}
????private void updateStatus(String message) {
????????setErrorMessage(message);
????????setPageComplete(message == null);
????}
????public String getPackage() {
????????return packageText.getText();
????}
????public String getSuperclass() {
????????return superclassText.getText();
????}
????public String getInterfaces() {
????????return interfacesText.getText();
????}
上面的構造函數設置了頁面的標題(它將高亮顯示在標題欄下)。我們也有一些輔助性方法。updateStatus將管理顯示出的這個特定頁面的錯誤信息。如果沒有錯誤信息,,那就說明頁面完成了;這時,Next按鈕將變成可用的狀態。這里對數據屬性內容設置了getter方法。接下來是createControl()方法,它將創建頁面上所有可見的組建。
????
public void createControl(Composite parent) {
????????Composite controls =
????????????new Composite(parent, SWT.NULL);
????????GridLayout layout = new GridLayout();
????????controls.setLayout(layout);
????????layout.numColumns = 3;
????????layout.verticalSpacing = 9;
????????Label label =
????????????new Label(controls, SWT.NULL);
????????label.setText("&Package:");
????????packageText = new Text(
????????????controls,
????????????SWT.BORDER | SWT.SINGLE);
????????GridData gd = new GridData(
????????????GridData.FILL_HORIZONTAL);
????????packageText.setLayoutData(gd);
????????packageText.addModifyListener(
????????????new ModifyListener() {
????????????????public void modifyText(
????????????????????????ModifyEvent e) {
????????????????????dialogChanged();
????????????????}
????????});
????????label = new Label(controls, SWT.NULL);
????????label.setText("Blank = default package");
????????label = new Label(controls, SWT.NULL);
????????label.setText("&Superclass:");
????????superclassText = new Text(
????????????controls,
????????????SWT.BORDER | SWT.SINGLE);
????????gd = new GridData(
????????????GridData.FILL_HORIZONTAL);
????????superclassText.setLayoutData(gd);
????????superclassText.addModifyListener(
????????????new ModifyListener() {
????????????????public void modifyText(
????????????????????????ModifyEvent e) {
????????????????????dialogChanged();
????????????}
????????});
????????label = new Label(controls, SWT.NULL);
????????label.setText("Blank = Object");
????????label = new Label(controls, SWT.NULL);
????????label.setText("&Interfaces:");
????????interfacesText = new Text(
????????????controls,
????????????SWT.BORDER | SWT.SINGLE);
????????gd = new GridData(
????????????GridData.FILL_HORIZONTAL);
????????interfacesText.setLayoutData(gd);
????????interfacesText.addModifyListener(
????????????new ModifyListener() {
????????????????public void modifyText(
????????????????????????ModifyEvent e) {
????????????????????dialogChanged();
????????????}
????????});
????????label = new Label(controls, SWT.NULL);
????????label.setText("Separated by ','");
????????dialogChanged();
????????setControl(controls);
????}
你需要了解SWT來書寫這些代碼。如果你對SWT不了解,在文章的最后有一些鏈接,這些鏈接可以告訴你學習SWT的地方。基本上這個方法創建了標簽、輸入框并對他們進行了布局。每次輸入框的改變,它的數據也通過dialogChanged()方法來進行修改:
????
private void dialogChanged() {
????????String aPackage = getPackage();
????????String aSuperclass = getSuperclass();
????????String interfaces = getInterfaces();
????????String status = new PackageValidator().isValid(aPackage);
????????if(status != null) {
????????????updateStatus(status);
????????????return;
????????}
????????status = new SuperclassValidator().isValid(aSuperclass);
????????if(status != null) {
????????????updateStatus(status);
????????????return;
????????}
????????status = new InterfacesValidator().isValid(interfaces);
????????if(status != null) {
????????????updateStatus(status);
????????????return;
????????}
????????updateStatus(null);
????}
}
這些工作利用了3個工具類來完成:PackageValidator,SuperclassValidator和InterfacesValidator。我們將在后面來完成這三個類。
5.1 驗證類
在用戶輸入數據后,驗證工作可以在插件的任何部分完成。所以,將驗證代碼放在一個可重用的類中是有意義的,這樣比將驗證代碼四處拷貝要好得多。下面是一個驗證類的例子:
public class InterfacesValidator implements ICellEditorValidator
{
????public String isValid(Object value)
????{
????????if( !( value instanceof String) )
????????????return null;
????????String interfaces = ((String)value).trim();
????????if( interfaces.equals(""))
????????????return null;
????????String[] interfaceArray = interfaces.split(",");
????????for (int i = 0; i < interfaceArray.length; i++)
????????{
????????????IStatus status = JavaConventions
????????????????????.validateJavaTypeName(interfaceArray[i]);
????????????if (status.getCode() != IStatus.OK)
????????????????return "Validation of interface " + interfaceArray[i]
????????????????????????+ ": " + status.getMessage();
????????}
????????return null;
????}
}
其他的驗證類跟這個十分相似——參見文檔最后的源代碼。
另一個Eclipse工廠中的漂亮的類是JavaConventions,它將為我們驗證數據。它包含了很多驗證方法,例如:
●????????validateJavaTypeName()用來檢查類和接口的名稱
●????????validatePackageName()用來檢查包名
●????????validateFieldName()用來檢查數據成員的名稱
●????????validateMethodName()用來檢查方法名稱
●????????validateIdentifierName()用來檢查變量名稱
我們目前不需要使用ICellEditorValidator接口,但是在下一篇文章中,我們將使用到它。
六 結果
到這里,我們已經完成了一個有圖片并由第二頁的向導,它將初始化Invokatron文檔。圖2就是結果:

圖2 自定義的向導
七 耀眼的小發明
正如我們所看到的,很多應用都是數據驅動的。表現方式也很重要。一個糟糕的小發明是賣不出去的,但是一個耀眼的小發明卻可以賣出去。但是數據,才是我們這些程序員的根源。
在這篇文章中,我們首先決定了那些數據是需要處理的。接著,我們通過定制的向導可視化地捕獲了這些數據。下篇文章將在表現方式上進行進一步闡述,這將包括一個定制的編輯器和一個屬性頁。
八 資源
●????????PluginTest3.zip 示例代碼:[下載文件]
●????????“Eclipse Platform Online Help: Wizards”
●????????“Creating JFace Wizards”闡述了如何創建獨立的向導
●????????讀這些文檔來學習SWT。一定要閱讀“SWT: The Standard Widget Toolkit”的第一和第二部分,還有“Understanding Layouts in SWT”。
九 作者和譯者簡介
作者:Emmanuel Proulx 是一位J2EE和EJB方面的資深專家,也是一位WebLogic Server 7.0鑒定工程師。他主攻電信和網絡開發領域。
譯者:hopeshared 是一位在讀MSE,目前從事Eclipse插件研究與開發工作。
十 相關文檔
Eclipse Plugins Exposed, Part 2: Simple GUI Elements
Eclipse由大量的插件組成,但是你不能隨意的編寫代碼然后簡單的與之合并起來。在Emmanuel Proulx這個關于Eclipse的文章系列的第二部分中,他通過創建一個工具欄按鈕、菜單項、對話框等介紹了Eclipse的“擴展點”……
Eclipse Plugins Exposed, Part 1: A First Glimpse
很多開發者僅僅將Eclipse作為一個集成開發環境來使用,從來不去它強大的可擴展性。就像Emmanuel Proulx在這個系列文章的開始所展示的,Eclipse的插件系統提供給你一個可定制的工作平臺,這樣,你可以定制Eclipse來適應開發的需求……