以前我有一个微型的便携式电子地址薄。我一直认为它很不错,直到有一天它停止运行了。销售该产品的人员无法找回我的联系地址名册,却提议更换一台。这时候我才知道数据的重要性。这个闪亮的小发明与存储在它里面的数据相比根本就不值一提。
在这个序列文章的第一部分中,我介绍了Eclipse插件的开发环境,并开发了一个简单的插件。在第二部分,我添加了工具条按钮、菜单项和对话框。它实际上没有实现任何具体功能。它简单地用某种字体显示了示例文本内容。现在我们要让它能够管理实际的数据。我们将修改这个插件,让它实现我们所需要的功能。本文讨论的是编辑器文档,并演示了如何定制一个向导。
Invokatron的历史
首先,我们详细说明一下Invokatron本身。在前面的文章中我们讨论过,Invokatron是一个生成Java代码的的图形工具。你可以简单地通过拖放操作建立类的方法。拖入的方法被编辑的方法(也就是插件)"调用"。我们将让数据来驱动应用程序的设计。在后面一篇文章中,我们将开发这个GUI。现在我们需要做的是,找到插件将输入和存储的重要数据。它通常被称为应用程序的模型(model)。在设计这个系统的时候,我们需要考虑下面一些内容:
· 哪些细节数据需要保存?
· 这些数据在内存中用什么来表现?POJO、JavaBean还是EJB?
· 这些数据的存储格式是怎样的?数据库表、XML文件、属性文件还是串行二进制文件?
· 输入数据的方式有哪几种?用"新建文件"向导还是在文档属性页面上使用弹出对话框、用编辑器绘制、在文本编辑器中输入的其它向导?
在我们继续工作之前必须回答这些问题。不可能有适合所有项目的答案;它完全依赖于你的需求。在我们的例子中,我做出了一些随意的、可能有问题的决定,如下所示:
· 一个Java类,它包含类名、程序包、超类(superclass)和实现接口。我们以它为基础,在后面的文章中添加更多数据。
· 我将把数据表现为扩展Properties类的类。它建立了编辑器的"文档类"。
· 我将使用的格式是属性文件,很容易使用Properties类来分析它。
· 在"新建文件"向导中,我将先寻找数据,接着让用户改变属性窗口或文本编辑器中的数据。这个步骤将在下一篇文章中完成。
Document(文档)类
下一步是编写文档类。建立一个新程序包(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"; } |
String package =document.getProperty(InvokatronDocument.PACKAGE); |
定制向导
请看一看前面的文章中所出现的向导。你应该记得,我们可以通过点击(我们自己添加的)工具条按钮或者菜单项来访问它。图1是它的界面:
它只有一个页面,右上角没有图片。我们想输入更多的信息,并提供一个很好的图片。换句话说,我们希望定制这个向导。
我们来分析一下这个向导。请打开InvokatronWizard.java文件。请注意这个类是如何扩展Wizard并实现INewWizard接口的。你应该理解它里面的很多方法。为了定制向导,我们简单地调用或重载其中的某些方法。下面是一些重要的方法:
生命周期方法
我们应该重载这些方法,把初始化和析构(destruction)代码插入向导中:
· Constructor(构造函数):向导实例化的时候、在Eclipse给它传递信息之前调用。向导的一般初始化实现。通常你希望调用"美化方法"(后面有描述)并设置对话框的默认值。
· init(IWorkbench workbench, IStructuredSelection editorSelection): Eclipse调用它为向导提供工作台的信息。请重载它,保存IWorkbench和对象的句柄供以后使用。如果它是一个编辑器向导而不是新向导,我们最好把当前的编辑器选项作为第二个参数。
· dispose():Eclipse调用它执行清理工作。重载它来清除向导使用的资源。
· finalize():清除代码,可能使用dispose()代替。
美化方法
这些方法都是用于装饰向导窗体的。
· setWindowTitle(String title):设置窗体的标题行字符串。
· setDefaultPageImageDescriptor(ImageDescriptor image):用于提供显示在向导的所有页面右上方的图片。
· setTitleBarColor(RGB color):指定标题栏用什么颜色。
按钮方法
这些方法控制着向导按钮的实用性和行为。
· boolean canFinish():重载它用于指定Finish(完成)按钮是否激活(根据向导的状态)。
· boolean performFinish():重载它来实现向导的根本的业务逻辑。如果向导没有完成(错误的条件),就返回false。
· boolean performCancel():重载它,在用户点击Cancel(取消)按钮的时候进行清除操作。如果向导不能终止,则返回false。
· boolean isHelpAvailable():重载它用于指定Help(帮助)按钮是否可视。
· boolean needsPreviousAndNextButtons():重载它来指定Previous(前一步)和Next(后一步)按钮是否可视。
· boolean needsProgressMonitor():重载它来指定进度条部件是否可视。当点击Finish按钮调用performFinish()方法的时候,它就会出现。
页面方法
这些方法控制着页面的外观。
· addPages():向导显示的时候调用。重载它给向导插入新页面。
· createPageControls(Composite pageContainer):Eclipse调用它来实例化所有的向导页面(用前面的addPages()方法已经添加的页面)。重载它给向导添加持续可视的窗体小部件(除页面之外的部件)。
· IWizardPage getStartingPage():重载它来检测哪个页面是向导的第一个页面。
· IWizardPage getNextPage(IWizardPage nextPage):在默认情况下,点击Next按钮将进入addPages()所提供的数组中的下一个页面。你可能希望根据用户选择进入不同的页面。重载它来计算后一个页面。
· IWizardPage getPreviousPage(IWizardPage previousPage):与getNextPage()类似,用于计算前一个页面。
· int getPageCount():检索addPages()添加的页面的数量。在典型情况下,你不必重载它,除非你希望显示页面的数量和形式。
其它有用的方法
这些都是有用的辅助方法:
· setDialogSettings(IDialogSettings settings):你可以载入对话框的状态,并通过在init()中调用这个方法来设置这些值。在典型情况下,这些设置可以作为向导字段的默认值。请查看DialogSettings类了解更详细的信息。
· IDialogSettings getDialogSettings():当我们需要数据的时候,就调用这个方法来检索它。在performFinish()的对话框的末尾,你再次可以把数据保存到文件中。
· IWizardContainer getContainer():对于检索Shell、运行的后台线程、刷新窗口等非常有用。
向导页面方法
你已经看到了,向导是由一个或多个页面组成的。这些页面扩展了WizardPage类,并实现了IWizardPage接口。为了定制单独的页面,你必须了解很多方法。下面是一些重要的方法:
· Constructor:用于实例化页面。
· 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); } |
public boolean performFinish() { //首先把所有的页面数据保存在变量中 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()); //现在调用完成(finish)方法 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 { // 建立一个示例文件 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()); } //确保项目已经刷新了,该文件在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); } |
private void throwCoreException(String message) throws CoreException { IStatus status =new Status(IStatus.ERROR,"Invokatron",IStatus.OK,message,null); throw new 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处理页面特定的错误信息的显示。如果没有错误信息,就意味着页面完成了;因此,"下一步"按钮就可以使用了。还有数据字段内容的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); } |
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); } } |
验证类
验证可以在插件的用户输入数据的任何部分中进行。因此,把验证代码放入可重复使用的类中是有意义的,这样就不用把它复制到多个位置。下面是一个验证类的例子。
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; } } |