Sung in Blog

           一些技术文章 & 一些生活杂碎
前段时间,我买了个很小的、口袋式的电子通讯簿。我一直随身带着它,直到有一天,它坏了。我联系了卖家,对方告诉我,他们没有办法维修,但是可以给我换个新的,就在这个时候,我意识到了数据的重要性。和存储的信息相比,这个小发明简直一文不值。

在这个文章系列中,第一部分向读者介绍了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:
image
图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;
    }


在构造函数中,我们打开了过程监听器并为向导设置了图片。你可以通过右键下载这个新图标:

image

将这个图标保存在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就是结果:

image
图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来适应开发的需求……
posted on 2005-10-16 13:00 Sung 阅读(1717) 评论(0)  编辑  收藏 所属分类: Eclipse

只有注册用户登录后才能发表评论。


网站导航: