自动换行的draw2d标签

Draw2D里的Label不支持自动换行,虽然可以插入换行符手动换行。用TextFlow和适当的Layout可以实现文字的自动换行。以下代码由sean朋友贡献,原文链接

class LabelEx extends FlowPage {

    private TextFlow contents;

    public LabelEx() {
        this("");
    }

    public LabelEx(String text) {
        contents = new TextFlow();
        contents.setLayoutManager(new ParagraphTextLayout(contents, ParagraphTextLayout.WORD_WRAP_SOFT));
        contents.setText(text);
        add(contents);
    }

    public void setText(String text) {
        contents.setText(text);
    }

    public String getText() {
        return contents.getText();
    }
}

通过TreeColumn实现“表格树”TableTree

Eclipse 3.1里deprecate了TableTree这个控件,与之对应的jface的TableTreeViewer虽然没有deprecate,但使用它会得到很多警告。在TableTreeViewer的第一列里是不能显示图标的,因为这个位置被+/-符号占用了,而且TableTree是显示不出 Tree的层次的,也就是没有缩进。

SWT 3.1里的Tree控件新支持了列的显示,是通过TreeColumn来实现的。在jface里则没有添加新的viewer,使用原先的TreeViewer即可支持,下面是一段例子代码,注意如果在windows里运行要修改一下setInput()这条语句的参数,例如改为setInput(new File("c:\\"))。

import java.io.File;

import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TreeColumn;


public class TreeColumnTest {
    
    public void run(){
        final Display display = new Display();
        final Shell shell = new Shell(display);
        shell.setLayout(new FillLayout());
        
        final TreeViewer viewer = new TreeViewer(shell, SWT.FULL_SELECTION);
        viewer.getTree().setHeaderVisible(true);
        TreeColumn column = new TreeColumn(viewer.getTree(), SWT.LEFT);
        column.setText("Name");
        column.setWidth(200);
        column = new TreeColumn(viewer.getTree(), SWT.LEFT);
        column.setText("Size");
        column.setWidth(100);
        column = new TreeColumn(viewer.getTree(), SWT.LEFT);
        column.setText("Hidden");
        column.setWidth(100);
        viewer.setContentProvider(new MyTreeContenetProvider());
        viewer.setLabelProvider(new MyTableLableProvider());
        viewer.setInput(new File("/"));

        shell.open();
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch())
                display.sleep();
        }
        display.dispose();
    }

    public static void main(String[] args) {
        new TreeColumnTest().run();
    }
    
    class MyTreeContenetProvider implements ITreeContentProvider{

        public Object[] getChildren(Object parentElement) {
            File file=(File)parentElement;
            if(file.isDirectory())
                return file.listFiles();
            else
                return null;
        }

        public Object getParent(Object element) {
            File file=(File)element;
            return file.getParentFile();
        }

        public boolean hasChildren(Object element) {
            File file=(File)element;
            return file.isDirectory()/*&&file.list().length>0*/;
        }

        public Object[] getElements(Object inputElement) {
            File file=(File)inputElement;
            return file.isDirectory()?file.listFiles():new Object[]{file};
        }

        public void dispose() {
            
        }

        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
            
        }
        
    }
    
    class MyTableLableProvider implements ITableLabelProvider{

        public Image getColumnImage(Object element, int columnIndex) {
            return null;
        }

        public String getColumnText(Object element, int columnIndex) {
            File file=(File)element;
            switch (columnIndex) {
            case 0:
                return file.getName();
            case 1:
                return ""+file.length();
            case 2:
                return ""+file.isHidden();
            default:
                return "";
            }
        }

        public void addListener(ILabelProviderListener listener) {
            
        }

        public void dispose() {
            
        }

        public boolean isLabelProperty(Object element, String property) {
            return false;
        }

        public void removeListener(ILabelProviderListener listener) {
            
        }
    }
}

下面是运行画面:

给表格的单元格增加编辑功能(补充)

这一篇是对“给表格的单元格增加编辑功能”的补充,目的是让表格列显示Checkbox并允许单击改变选中状态,例子中的表格共有三列,其中后两列均需要显示为Checkbox。

步骤一,构造TableViewer;

final String[] columnNames = new String[] { "Project", "Must", "Must Not" };//columnNames在后面也要用到,所以专门定义为一个数组
TableColumn column = new TableColumn(tbv.getTable(), SWT.NONE);
column.setText(columnNames[0]);
tbv.setColumnProperties(columnNames);//给每个列指定一个字符串属性值
column.setWidth(200);
column = new TableColumn(tbv.getTable(), SWT.NONE);
column.setText(columnNames[1]);
column.setWidth(100);
column = new TableColumn(tbv.getTable(), SWT.NONE);
column.setText(columnNames[2]);
column.setWidth(100);
tbv.setContentProvider();
tbv.setLabelProvider();
tbv.setInput();

步骤二,定义CellEditor数组并指定给前面的TableViewer:

final CellEditor[] editors = new CellEditor[tbv.getTable().getColumnCount()];
//editors[0]保留为空,因为第一列不需要显示为Checkbox
editors[1] = new CheckboxCellEditor(tbv.getTable());
editors[2] = new CheckboxCellEditor(tbv.getTable());
tbv.setCellEditors(editors);

步骤三,定义TableViewer的CellModifier,作用是告诉表格如何改变对象的属性值,注意在modify()方法里参数element可能是org.eclipse.swt.widgets.Item类型,如果是这种情况要通过Item#getData()得到实际的对象:

tbv.setCellModifier(new ICellModifier() {
    public boolean canModify(Object element, String property) {
        return property.equals(columnNames[1]) || property.equals(columnNames[2]);
    }

    public Object getValue(Object element, String property) {
        PortfolioItem item = (PortfolioItem) element;
        if (property.equals(columnNames[1])) {
            return Boolean.valueOf(item.isMust());
        }
        if (property.equals(columnNames[2])) {
            return Boolean.valueOf(item.isMustNot());
        }
        return null;
    }

    public void modify(Object element, String property, Object value) {
        if (element instanceof Item)
            element = ((Item) element).getData();
        PortfolioItem item = (PortfolioItem) element;
        if (property.equals(columnNames[1])) {
            item.setMust(((Boolean) value).booleanValue());
        }
        if (property.equals(columnNames[2])) {
            item.setMustNot(((Boolean) value).booleanValue());
        }
    }
});

步骤四,这时单击表格可以改变选中状态了,但显示的是True/False(或其他在LableProvider里定义的内容),见图1,而非Checkbox控件选中/清空的样子。

图1 单击表格单元可改变T/F

解决的办法很简单,在LabelProvider里根据属性值True/False显示不同的图片即可,这两个图片可以在这里下载(鼠标右键另存为):

public Object getColumnImage(Object object, int columnIndex) {
    PortfolioItem item=(PortfolioItem)object;
    switch (columnIndex) {
    case 1:
        return item.isMust()?PortfolioEditPlugin.getPlugin().getImage("checked"):PortfolioEditPlugin.getPlugin().getImage("unchecked");
    case 2:
        return item.isMustNot()?PortfolioEditPlugin.getPlugin().getImage("checked"):PortfolioEditPlugin.getPlugin().getImage("unchecked");
    default:
        return null;
    }        
}

public String getColumnText(Object object, int columnIndex) {
    PortfolioItem item=(PortfolioItem)object;
    switch (columnIndex) {
    case 0:
        return item.getProject()==null?"N/A":item.getProject().getName();
//在显示为Checkbox的两列里不需要文字
//        case 1:
//            return item.isMust()?"True":"False";
//        case 2:
//            return item.isMustNot()?"True":"False";
    default:
        return "";
    }
}

最后是运行结果:

用GMF生成简化的数据库设计器

Eclipse Graphical Modeling Framework (GMF)能 够帮助我们快速构造基于EMF和GEF的图形化编辑器,实际上对于不是很复杂的应用来说,开发人员并不需要了解EMF和GEF就可以使用GMF。这篇帖子 通过从零开始生成一个数据库设计器的全过程,演示了在使用GMF创建应用程序时,构造ecore模型、构造.gmfgraph文件、构造.gmftool 文件、构造.gmfmap文件和生成编辑器的这几个步骤。

一、开发环境

由于目前gmf还没有发布正式版,所以这篇帖子使用的是相对稳定的GMF 1.0M4版本,1.0正式版将在2006年7月初发布。gmf对eclipse平台和一些插件的要求比较高,所以你可能要对你的开发环境进行一些升级更新才能感受gmf带来的方便,具体要求是这样的:Eclipse 3.2M4EMF 2.2.0M4GEF 3.2M4UML2 2.0M2;此外还要下载一个名为ANTLR的包,解压后要把antlr.jar文件放在gmf插件目录的antlr/lib下,这个依赖只是暂时的,gmf正式版发布之前会去掉它。

二、构造ecore模型

因为只是为了演示gmf,这里构造的是一个非常简化的数据库设计器。用户通过设计器可以创建表格,为每个表格增加一些列,定义这些列的属性,以及在 表格之间建立外键关系。所以在ecore模型里应该有Database、Table和Column这几个类,此外还有一个FKRelation类代表表格 之间的连接,在Database类下有一个名为fkrelations的引用用来记录一个数据库设计中所有的这些连接。

创建名为com.my.dbdesigner的Empty EMF Project项目,有多种方式可以创建ecore文件,在gmf的example里有一个例子是ecore文件的图形编辑器,如果你安装了这个例子,可 以在项目的根目录下New->Other->Examples->Ecore Diagram创建名为dbdesigner的文件,这将生成dbdesigner.ecore和dbdesigner.ecore_diagram文 件。我在使用它编辑ecore文件时遇到了一些同步的问题,所以后来还是用eclipseUML来编辑的,不过这只是一个方法问题了。总之,我们这个数据 库设计器的ecore模型如图1所示(如果嫌麻烦,可以点这里下载现成的ecore文件)。

图1 数据库设计器的简化ecore模型图

三、构造.gmfgraph文件

主菜单New->Other->Example EMF Model Creation Wizards->GMFGraph Model创建名为dbdesigner.gmfgraph的文件,向导最后一步中Model Object选择为Canvas,然后按Finish按钮。在编辑器里,把Canvas命名为DBDesignerDiagram,这将成为数据库设计器 的画布。在Canvas下New Child创建一个名为Default的Figure Gallery,Figure Gallery的作用是容纳一些可供重用的Figure。在Figure Galley下创建一个名为BasicRectangle的Rectangle节点,在这个例子里大多数图形只用矩形就够了(除了连接线)。现在,在 Canvas下创建一个名为TableNode的Node节点,它代表数据库设计器里的表格,这个节点的Figure属性选择为刚才定义的 BasicRectangle,见图2,也就是指定在将来生成的数据库设计器里,表格显示为矩形。

图2 TableNode节点

可以想象,现在生成的数据库设计器里已经可以在画布上创建矩形的表格了,那么怎样实现在表格里创建列呢?这稍微麻烦一些,因为表格图形并不是全部面 积都用来放置列,而要留出顶部的一行用来显示表格名称,而且这些列也不是像表格在画布上那样随意放置,而是按由上到下的顺序排放的,这就需要在表格图形里 加一个隔间(Compartment),隔间的概念可以在图3中看到,它的作用就是放置子元素,但隔间本身一般不代表模型中的某个元素。

图3 红色虚线部分所示为表格图形里的隔间

创建一个与TableNode同级的名为ColumnCompartment的Compartment,意即用来放置列的隔间,在属性视图里把它的 Figure属性设置为BasicRectangle。再创建一个名为ColumnChild的同级Child节点,它的Figure属性同样为 BasicRectangle,这个ColumnChild就是作为子元素的列,如图4所示。

图4 ColumnChild节点

如前所述,数据库设计器里允许在不同表格之间创建连接线来表示外键关系,为简单起见,我们用连接线的标签定义作为外键的列名。因为现在我们的 Figure Gallery里只有矩形,所以先要给Figure Gallery增加一个Polyline Connection,命名为BasicPolyline。然后在Canvas下创建一个名为FKConnection的Connection,它的 Figure属性选为BasicPolyline,如图5所示。

图5 FKConnection节点

四、构造.gmftool文件

主菜单New->Other->Example EMF Model Creation Wizards->GMFTool Model创建名为dbdesigner.gmftool的文件,向导最后一步中Model Object选择为Tool Registry,然后按Finish按钮。在Tool Registry下创建Palette,在Palette下创建标题为DBDesigner的Tool Group,在这个Tool Group下为Table和Column分别创建一个Creation Tool,它们将成为数据库设计器中用来创建表格和列的那的两个按钮。同样在这个Tool Group下,为连线也创建一个Creation Tool,如图6所示。

图6 ForeignKey节点

五、构造.gmfmap文件

主菜单New->Other->Example EMF Model Creation Wizards->GMFMap Model创建名为dbdesigner.gmfmap的文件,向导最后一步中Model Object选择为Mapping,然后按Finish按钮。从主菜单GMF Editor里选择“Load Resource...”命令,在对话框里按Browse Workspace按钮,选中我们的dbdesigner.ecore、dbdesigner.gmfgraph和dbdesigner.gmftool 这三个文件,见图7,再按OK关闭对话框。

图7 为定义映射载入需要的资源

在编辑器的Mapping节点下创建一个Canvas Mapping,可以看到在属性视图里它的属性被分为三类,分别对应ecore模型、工具和图形这三个方面,对于Canvas Mapping,必须设置Domain Model、Element和Diagram Canvas这三个属性,值分别为EPackage dbdesigner、EClass Database和Canvas DBDesignerDiagram,它们都是下拉选项,所以很容易确定。

刚才的设置相当于告诉了GMF我们要把Database类映射为画布,现在要告诉GMF我们还要把Table类映射为画布上的矩形,所以要创建另一 个Mapping的子节点Node Mapping,它的属性见图8,注意可能要先选择了Element属性值后Edit Feature属性才可选。

图8 为数据库表格定义Node Mapping

还要告诉GMF表格里要能创建列,因此在Node Mapping下创建Compartment Mapping和Child Node Mapping各一个,前者只要将Compartment属性选择为在.gmfgraph里定义的ColumnCompartment即可;后者的属性如 图9所示,注意Compartment Mapping的Child Nodes属性与Child Node Mapping的Compartment属性是双向的,我们只用定义其中一个方向即可,另一个方向会自动填充。

图9 为列定义Child Node Mapping

最后要处理一下连接线,方法是在Mapping下创建一个Link Mapping,它的属性比较多,见图10。

图10 为外键关系定义Link Mapping

六、生成编辑器

该做的准备工作都已就绪,现在到了激动人心的最后一个步骤了。首先是要生成基本的EMF代码,包括核心模型代码和.Edit代码,因为gmf的图形 化编辑器依赖这两个部分,而EMF传统的Editor部分则并不需要。这个步骤在EMF的帖子里已经介绍过了,这里不再重复。接下来打开 dbdesigner.gmfmap文件,在编辑器里点右键,选择“Create generator model...”命令,在对话框里接受缺省的dbdesigner.gmfgen文件名,按OK确定后就会生成一个.gmfgen文件。打开这个文件, 还是在编辑器里点右键,选择“Create diagram code”命令,这样就会生成图形化编辑器的代码,这些代码放在名为com.my.dbdesigner.gmf.editor的项目中。

如果在执行上面步骤中出现了错误,就要检查那些模型文件是否正确,特别是.ecore文件的package中Ns Prefix和Ns URI这两个属性不应为空,如果错误信息为“java.lang.IllegalStateException: Can't find genFeature for feature 'XXX' in class XXX”则很可能是由于更改了.ecore文件后没有更新.genmodel文件。

运行这个生成的插件后,你就可以通过主菜单File->New->Example->DBDesigner Diagram创建数据库设计了,图11是它的工作界面。功能不错,但在我的机器上响应不是很快。点此下载生成后的项目打包

图11 数据库设计器的运行画面

EMF介绍系列(七、.Edit初步)

EMF除了生成模型部分的接口和实现类(不妨称作“核心模型”)以外,还生成一个名称以.Edit结尾的项目,包含一些与核心模型和编辑器关系都十 分紧密的代码。这部分代码经过了精心设计,可重用的程度是相当的高。它们不仅在EMF生成的编辑器项目里大量被用到,我们自己在扩展编辑器的时候也应该充 分利用。

在线商店的例子里,com.my.shop.edit项目里包含一个ItemProviderAdapterFactory类和一组 ItemProviderAdapter的子类,后者是和核心模型的接口一一对应的,例如核心模型的Shop、Category和Product分别对应 ShopItemProvider、CategoryItemProvider和ProductItemProvider。这篇帖子主要介绍一下这些 ItemProvider,而关于ItemProviderAdapterFactory的内容将在以后的帖子里专门介绍,其实顾名思义, ItemProviderAdapterFactory的作用主要就是生成ItemProvider。事实上在构造EMF应用程序时,我们经常要修改 ItemProvider里的代码,而ItemProviderAdapterFactory则很少改动。

图1 EMF生成的.Edit项目

注意:.Edit项目里ItemProviderAdapter的子类名称里省略了Adapter这个单词,例如 CategoryItemProvider而非CategoryItemProviderAdapter,你心里应该清楚它是一个Adapter,因为它 确实实现了Adapter接口。EMF里另外专门有一个ItemProvider类是为非Adapter类型准备的,在这篇里说的 ItemProvider不是指它,而是指XXXItemProvider,也就是ItemProviderAdapter的子类。

注意:EMF里的Adapter接口和Eclipse Runtime的IAdaptable接口虽然名称相似,但并不是同一个概念(关于IAdaptable请参见前面的翻译帖子), EMF里的Adapter等同于监听器(Listener、Observer)的作用,它监听的对象是EMF的Notifier,在一个Notifier 上可以注册多个Adapter。另一方面,ItemProviderAdapterFactory则很像IAdaptable,它们都能够起到动态转换类 型的作用,只不过前者一般只用于Notifier到Adapter的转换,后者则没有什么限制,此外转换方法的名称也不同,前者是adapt(),后者为 getAdapter()。

从图1中不难看出,ItemProvider构成了.Edit项目的主要部分,这些ItemProvider具有以下几个作用。

一、实现了JFace中ContentProvider和LabelProvider的功能

JFace查看器(Viewer)是对swt中控件的一种包装,例如TableViewer是对Table的包装,TreeViewer是对Tree的包 装,等等,通过这种方式可以将控件与显示在控件中的数据在一定程度上分离,从而方便数据显示的更新。相当多的Eclipse应用程序都是通过JFace查 看器显示数据的,与查看器关联的ContentProvider和LabelProvider分别控制查看器中显示的哪些数据以及每条数据的显示方式。

以TreeViewer的ContentProvider为例,在JFace里应该实现ITreeContentProvider接口,这个接口定 义了getParent()、hasChildren()和getChildren()这三个方法;在EMF里有 ITreeItemContentProvider接口与之对应,这个接口同样具有这三个方法,.Edit部分的每个ItemProvider都实现了这 个接口,因为EMF已经完全知道我们的模型结构,所以这三个方法在ItemProviderAdapter类里已经实现好了。不过 ITreeItemContentProvider毕竟不能直接交给JFace的TreeViewer来使用,所以EMF提供了一个 AdapterFactoryContentProvider来做适配工作,你可以在编辑器的代码里看到如何使用它。

LabelProvider也是类似的,它主要控制显示的文字和图标。EMF生成的ItemProvider缺省没有实现 ITableItemLabelProvider,所以如果要使用TableViewer,要修改代码以实现 ITableItemLabelProvider接口和额外的方法,具体请参考在线商店例子中的ProductItemProvider。从 JFace的角度来说,ItemProvider相当于集成了各种查看器的ContentProvider和LabelProvider的代码,是一个通 用的“ContentLabelProvider”。因此利用它,开发人员在改变查看器的时候只需要修改很少的代码,而不像传统方式那样每换一个查看器还 要写新的ContentProvider和LabelProvider。

二、提供了关联对象的属性表

每个ItemProvider的getPropertyDescriptors()方法返回在属性视图里显示的属性列表,列表里的每个元素是一个 ItemPropertyDescriptor对象,它决定了每个属性的标签、描述、图标以及是否可编辑。EMF为生成的代码会帮我们把模型定义里的每个 属性都显示在属性列表里,如果希望隐藏某些属性,可以通过修改这个方法移除之。

以Product为例,ProductItemProvider的getPropertyDescriptors()方法里包含这样六条语句,分别代表产品名称、价格、描述、是否有货、评价以及颜色这六个属性,如果你想让颜色属性在属性列表里消失,只要删除最后一句即可。

addNamePropertyDescriptor(object);
addPricePropertyDescriptor(object);
addDescriptionPropertyDescriptor(object);
addAvaiablePropertyDescriptor(object);
addScorePropertyDescriptor(object);
addBackgroundPropertyDescriptor(object);

三、生成编辑模型的各种命令

在ItemProviderAdapter基类里有很多createXXXCommand()方法,如果你用过GEF应该对这些名称不陌生,因为在 EditPolicy里也有类似的方法。我们知道,为了实现Undo/Redo功能,对模型的每个改变都应该使用Command实现,然后把 Command保存在Command栈里,每个Command对象保存Undo/Redo自己的信息。ItemProviderAdapter相当于一个 生产这些Command的工厂,用户对模型编辑的请求都将通过它转换为对应的Command,例如用户在属性视图里修改了一个属性的值,当按下回车后,会 调用该对象关联的ItemProvider类的createSetCommand()方法生成一个SetCommand对象。

注意:在createCommand()方法里会调用getChildrenFeatures()方法,而在实现ContentProvider的getChildren()时也需要这个方法,因此这个方法的返回结果同时影响ItemProvider的这两项功能。

四、将模型的改变通知到负责显示模型的视图

在一个Eclipse应用程序里经常会有很多个查看器显示模型,无论用户怎样修改模型,要让这些查看器里显示的内容总是当前的模型,最好的办法是让查看器能够响应模型的变化。ItemProvider作为监听器可以很好的完成这个任务。

模型发生改变时,与被修改的对象相关联的ItemProvider的notifyChanged()方法被调用,事件立即被通知给 ItemProviderAdapterFactory,后者是整个模型的事件处理机构,所有的ItemProvider都是通过 ItemProviderAdapterFactory创建并注册为监听器的,因此ItemProviderAdapterFactory可以把事件通过 fireNotifyChanged()通知给所有这些监听器的notifyChanged()方法去消化。图2展示了这个通知过程,此图来自 《Eclipse Modeling Framework: A Developer's Guide》第3.2.4节中的图3.10。

图2 ItemProvider的通知流程

最后,ItemProvider还有一个collectNewChildDescriptors()方法,这个方法决定了在编辑器里,模型里对应的 那个对象可以创建哪些子元素。例如在线商店模型里,Category对象的子元素是Category和Product,那么用户在编辑器里右键点击一个 Category对象选择“New Child”时,就会出现“Category”和“Product”这两个选项。有些场合我们想隐藏其中一些选项时,就可以修改这里的代码。

参考资料: Eclipse Modeling Framework A Developers Guide,第3.2节、第10.1节。

在Eclipse的About对话框上添加自己的图标

在Eclipse的About对话框上添加自己的图标的步骤如下:

1、建立一个feature;

2、在feature.xml的”Overview“属性页里设定一个“Branding Plug-in”;

3、把一个32x32的图片(名字可以任意起,例如“mylogo.gif”)放在Branding Plug-in的根目录下;

4、在Branding Plug-in的根目录下建立“about.ini”文件,内容为“featureImage=mylogo.gif”;

5、最后,输出这个feature即可。

图1 添加在About对话框里的图标

注意1:Branding Plug-in的MANIFEST.MF的Build页里一定要选择输出mylogo.gif和about.ini这两个文件

注意2:我输出feature时如果选中“Package features and plug-ins as individual JAR archives”则feature不会被安装(在About的Feature Details列表里没有出现),勾掉这个选项则正常。

参考: How Can I Give My Eclipse Blob An Icon In The Flippin' About Dialog?

下载:包含feature和branding plug-in的工程打包

将Eclipse插件转换为RCP应用程序(下)

上一篇里我们为一个普通的Eclipse插件添加了Application扩展,剩下来的工作就很简单了,甚至不需要再编写一行代码。在 Eclipse 3.1里,把具有Application的插件包装成RCP并输出的过程是通过建立产品配置文件(Product Configuration)来完成的。

在主菜单选择File->New->Other命令,在对话框里选择新建一个产品配置文件,这个文件可以建立在任何位置,为方便起见我 们就把它放在需要转换为RCP的插件的主目录下好了。产品配置文件是一个xml格式的文件,不过Eclipse 3.1提供了一个编辑器界面来编辑它的内容,所以不用像以前那样记住所有的tag了。这个编辑器分为三个页面:Overview、 Configuration和Branding。

首先在Overview页面指定产品的ID,按下“Product ID”右边的“New...”按钮,在对话框里输入插件的ID、新的产品ID以及缺省的Application的ID,见图1。关闭对话框后,选择一个要 运行的Application,并填写产品名称。下面有一个选项让你选择产品基于plug-in还是feature,feature是多个插件的集合,如 果只包含一个插件,选择基于plug-in即可;如果包含多个插件,利用feature可以让这些插件按功能分类,便于管理,建议使用基于feature 的方式,不过你要先建立feature才行。

图1 新建Product ID对话框

然后,来到Configuration页面,先把我们的插件添加到左边的插件列表里(如果前面选择了基于features方式,这里是 feature列表),再按“Add Required Plug-ins”按钮让Eclipse自动添加被依赖的其他插件。config.ini文件的作用是设置了一些变量值,RCP程序运行时会根据它们改变 一些外观或行为,例如可以在这里规定透视图切换器的停靠位置(org.eclipse.ui/DOCK_PERSPECTIVE_BAR=left)等 等;在页面的右下方可以设置一些运行参数。

图2 选择需要的插件

最后翻到Branding页面,这个页面的功能就是定制一些外观元素,例如启动时显示的splash图像,将想显示的图像以 “splash.bmp”命名保存到插件的根目录下,然后在“Splash Screen”里指定这个插件ID即可;定制窗口的图标,包括16x16和32x32两种格式,都是.gif文件;还有就是“关于...”对话框里的图片 和文字,根据自己的需要填写即可。启动器名称(Launcher Name)就是启动RCP的命令名称,在Linux里是一个脚本文件,在Windows里则是一个.exe文件。还可以定制启动器的图标,由于时间关系我 的例子里省去了这个定制项目。

需要注意一点,这些图像无论放在哪个目录里(比如icons目录)都应该确保会被输出到产品里,否则运行产品时会看不到它或看到红色的小方块,方法是在插件的build.properties编辑器里勾选需要输出的文件和目录。

上面这些都配置好以后,回到Overview页面,先点击“Launch the product”看一下产品运行后的效果,确认没有问题后,就可以点击“Eclipse Product export wizard”输出你的产品了,在弹出的对话框里填写要输出的位置,可以选择输出为目录或是打包为单个文件(.zip格式),见图3。

图3 输出产品

输出后最好再确认一下运行效果,如果和刚才有所不同,则很可能是build.properties写的有些问题,请仔细检查。

怎么样,很容易吧!

将Eclipse插件转换为RCP应用程序(上)

有不少朋友问到如何把一个已有的Eclipse插件转换为RCP应用程序,其实这个过程并不复杂,因为RCP应用也是基于插件的结构,可以说RCP 就是精简后的Eclipse平台,只是我们要对这个平台做一些定制工作。将任何一个传统的Eclipse插件项目转换到RCP可以分为两个步骤,这篇先介 绍第一个步骤:建立应用程序。

GEF入门系列(三、应用实例)里我曾做过一个精简的GEF应用程序(下载),这一篇里我就一步一步的把这个例子转换为RCP应用程序(点击下载转换后的项目打包)。应用程序(Application)是通过扩展org.eclipse.core.runtime.applications扩展点建立的,其作用 是让Eclipse知道你的RCP需要什么样的功能,比如界面上有哪些视图,菜单和工具条,应用程序窗口的初始大小等等。在plugin.xml里添加应 用程序的定义很简单,像下面这样指定一个id和一个类名就可以了。

<extension
      id="myapplication"
      point="org.eclipse.core.runtime.applications">
   <application>
      <run class="com.example.application.MyApplication"/>
   </application>
</extension>

接下来我们的主要任务是实现这个类,MyApplication必须实现 org.eclipse.core.runtime.IPlatformRunnable接口,这个接口只定义了一个run()方法,对于Eclipse Platform来说这个方法就相当于传统java程序的main()方法,是入口方法。所有RCP应用程序里这个方法的实现几乎是完全一样的,即启动 Workbench,并把一个WorkbenchAdvisor实例作为参数传给它,如下所示:

public class MyApplication implements IPlatformRunnable {

    public Object run(Object args) throws Exception {
        Display display = PlatformUI.createDisplay();
        try {
            int returnCode = PlatformUI.createAndRunWorkbench(display, new MyWorkbenchAdvisor());
            if (returnCode == PlatformUI.RETURN_RESTART) {
                return IPlatformRunnable.EXIT_RESTART;
            }
            return IPlatformRunnable.EXIT_OK;
        } finally {
            display.dispose();
        }
    }
}

所以应用程序的定制实际上是通过这个WorkbenchAdvisor实例实现的。现在我们要构造 org.eclipse.ui.application.WorkbenchAdvisor类的一个子类,也就是上面代码里出现的 MyWorkbenchAdvisor,然后覆盖它的一些方法。比较重要的是这两个方法:createWorkbenchWindowAdvisor() 返回一个WorkbenchWindowAdvisor实例,从类名不难看出它的作用是定制应用程序窗口,包括菜单和工具条,稍后将详细介绍; getInitialWindowPerspectiveId()返回一个透视图的id字符串,这个透视图定义RCP应用程序的界面布局,所以如果在原来 的插件里你没有定义透视图,现在必须要新定义一个了。

public class MyWorkbenchAdvisor extends WorkbenchAdvisor {

    private static final String PERSPECTIVE_ID = "com.example.ui.MyPerspective";

    public WorkbenchWindowAdvisor createWorkbenchWindowAdvisor(
            IWorkbenchWindowConfigurer configurer) {
        return new MyWorkbenchWindowAdvisor(configurer);
    }

    public String getInitialWindowPerspectiveId() {
        return PERSPECTIVE_ID;
    }

    public void initialize(IWorkbenchConfigurer configurer) {
        super.initialize(configurer);

        //The workaround call
        WorkbenchAdapterBuilder.registerAdapters();
    }
}

注意:因为我们这个RCP里用到了Resource视图,而这个视图依赖org.eclipse.ui.ide,所以要在上面的 initialize()方法里手动注册一下Adapter,否则Resource视图里无法显示现有项目。(Resource视图在RCP里不推荐使 用,这个调用是无奈之举,请参考这条bug报告

现在来看一下前面代码里MyWorkbenchWindowAdvisor是怎样实现的,它继承自 org.eclipse.ui.application.WorkbenchWindowAdvisor类,为了定义窗口大小和标题要覆盖 preWindowOpen()方法,可以看到我们还顺便隐藏了工具条;要定义窗口的菜单和工具条,应该覆盖 createActionBarAdvisor()方法,返回的ActionBarAdvisor实例马上会介绍到。

public class MyWorkbenchWindowAdvisor extends WorkbenchWindowAdvisor {

    public MyWorkbenchWindowAdvisor(IWorkbenchWindowConfigurer configurer) {
        super(configurer);
    }

    public ActionBarAdvisor createActionBarAdvisor(
            IActionBarConfigurer configurer) {
        return new MyActionBarAdvisor(configurer);
    }

    public void preWindowOpen() {
        IWorkbenchWindowConfigurer configurer = getWindowConfigurer();
        configurer.setInitialSize(new Point(700, 500));
        configurer.setShowCoolBar(false);
        configurer.setShowStatusLine(false);
        configurer.setTitle("My RCP Application");
    }
}

有没有注意到,我们新建(和即将新建)的几个类有这样的引用关系:MyApplication->MyWorkbenchAdvisor- >MyWorkbenchWindowAdvisor->MyActionbarAdvisor,在3.1M5以前的Eclipse RCP版本中,还没有ActionbarAdvisor这个类,大部分应用程序定制工作都是在WorkbenchWindowAdvisor这一个类中做 的,带来的问题是这个类的代码很长,可复用的程度比较低;采用现在这种方式就方便多了,比如可以定义几个ActionbarAdvisor然后在 WorkbenchWindowAdvisor中根据需要做出选择,得到的应用程序就具有不同的功能,等等。

现在就来看看MyActionbarAdvisor是怎么实现的,它继承 org.eclipse.ui.application.ActionBarAdvisor类,我们先在makeActions()里构造需要出现在菜单 或工具条上的命令,注意要调用register()方法注册这些命令,作用是在应用程序结束后释放资源,同时支持快捷键操作;然后在 fillMenuBar()方法里把这些命令加入主菜单,因为我们隐藏了工具条,所以没有覆盖fillCoolBar()方法,另外你还可以通过覆盖 fillStatusLine()定义自己的状态栏。我们的这个类实现得很简单,只是一个退出程序菜单项,你应该根据需要添加自己的命令。

public class MyActionBarAdvisor extends ActionBarAdvisor {

    private IWorkbenchAction exitAction;

    public MyActionBarAdvisor(IActionBarConfigurer configurer) {
        super(configurer);
    }

    protected void makeActions(final IWorkbenchWindow window) {
        exitAction = ActionFactory.QUIT.create(window);
        register(exitAction);
    }

    protected void fillMenuBar(IMenuManager menuBar) {
        MenuManager fileMenu = new MenuManager("&File",
                IWorkbenchActionConstants.M_FILE);
        menuBar.add(fileMenu);
        fileMenu.add(exitAction);
    }
}

现在,应用程序需要的类都写好了,让我们检查一下应用程序是否可以正常启动。在Eclipse主菜单上选择Run->Debug...命令, 在对话框左边的“Eclipse Application”组下新建一个运行项“gefpractice-rcp”,在“Program to Run”组下选择“Run an application”,然后在下拉列表里找到我们的应用程序id,要说明的是在applications扩展点里我们指定的id是 “myapplication”,而这里列出的id则添加了插件id作为前缀,变成了“GefPractice-RCP.myapplication”, 如图1所示。

图1 设置为运行应用程序

因为缺省运行会启动Eclipse的全部插件,这样在应用程序里会出现多余的菜单项和功能,所以要设置为只启动我们的这一个插件,方法是切换到 Plug-ins属性页,选择“Choose plug-ins and fragments to launch from the list”,点击右边的“Deselect All”按钮清空选择列表,勾选上我们的插件项目,再按“Add Required Plug-ins”让Eclipse自动添加它依赖的其他插件就可以了,如图2所示。

图2 只启动我们的这一个插件

现自使用这个运行配置启动我们的应用程序,会得到一个很“干净”的界面,如图3所示,如果不是那些Eclipse特有的编辑器/视图的标题栏,你能猜出它是一个Eclipse应用程序吗?作为对比,这是Eclipse插件的版本的运行截图

图3 运行中的应用程序

建立了应用程序,代码的部分就算是完成了,但要得到一个完整的可独立运行的产品这样还不够,下一个帖子里将介绍另一个步骤:将应用程序包装为产品。如果等不及可以先看Branding Your Application这篇文章,只是这篇文章写得比较早,我下个部分要写的是使用.product配置产品,可以更方便的达到相同的目的。关于建立应用程序的更多内容请参考Rich Client Tutorial,这个教程共有三个部分,我当时就是通过它学习的,后来它按照RCP API的发展又及时更新了内容,是难得的入门材料。

EMF介绍系列(六、自定义命令)

EMF生成的应用程序里,用户的发出的每一条命令都是可以撤销(Undo)的,例如修改了产品的价格,按一下撤销按钮就能恢复原来的价格,当然还可 以通过重做(Redo)再回到新的价格。为了实现这个功能,应用程序里维护了一个用于存放命令的类似栈的数据结构(CommandStack),每一条执 行过的命令都被存放在那里,需要撤销时取出最近一条命令进行撤销。这个数据结构是由EditingDomain对象负责维护的, EditingDomain相当于编辑模型时的环境。

在EMF里命令框架实际上可以分为两大部分,一部分是与模型无关的通用命令,另一部分是.Edit命令,后者是建立在前者的基础之上的。EMF对模 型的任何修改都是通过命令完成的,例如当用户在属性视图里修改一个对象的属性时,会生成一个新的SetCommand实例,然后执行它的execute ()方法,这个方法里对模型进行修改(实际上是通过doExecute()方法),成功执行完成后这个命令被放入命令栈以便撤销时使用。

通用命令可以完全脱离EMF使用,也就是说,这个命令框架可以应用到任何需要命令框架的应用程序中,包括非EMF应用程序。它位于 org.eclipse.emf.common.command包里,其中Command接口定义了什么是“命令“,一个命令具有execute()、 undo()和redo()等方法,还有canExecute()和canUndo()方法用于判断命令是否可被执行或撤销(考虑到资源消耗,有些命令可 能设计为不可撤销更合理)。另一个重要的接口是前面提到过的CommandStack,它的作用是保存所有命令,可以通过 addCommandStackListener()方法注册监听器来观察CommandStack的状态变化。CompoundCommand接口可以 把多个命令按顺序包装成一个组合命令,它具有原子性,类似数据库里事务(Transaction)的概念,只有所有命令都可执行时这个组合命令才可执行, 撤销也是如此。

EMF在.Edit框架提供了针对EMF模型编辑所需要的一些命令(位于org.eclipse.emf.edit.command包),例如 SetCommand用于修改对象的属性,CreateChildCommand的作用是创建一个子元素,还有MoveCommand、 CopyCommand和CutToClipboardCommand等等。这些命令都实现Command接口,并且大部分继承自 AbstractOverrideableCommand这个抽象类,它给我们带来的影响是在Command接口里的方法名前面都加了一个do,比如 execute()变为doExecute()、canUndo()变为doCanUndo()等等,我们在扩展这些.Edit命令时要覆盖doXXX方 法。.Edit命令是通过反射的方式来修改模型的。

EMF提供的这些命令为我们完成基本的模型编辑功能,多数情况下直接使用它们就可以了,但有时通过自定义的命令可以实现一些特别的需求。举个例子来 说,在网上商店的例子里,假设要求产品的价格只精确到小数点后两位,那么我们要在用户输入新价格以后立刻对这个数值进行四舍五入处理,这个操作就可以利用 自定义命令完成。因为利用了.Edit提供的类,所以一般我们应该扩展.Edit命令,具体来说就是SetCommand。

首先通过继承SetCommand创建我们的SetPriceCommand,在这个方法里覆盖doExecute()方法,SetCommand 里有很多可供利用的环境变量,我们要用到的是owner和value这两项,前者是要修改的对象,在这里是产品对象,后者是属性的新值,在这里也就是新价 格。所以我们的SetPriceCommand可以像下面这样写(为了使代码最简,我们直接把EObject类型转换为Product类型,这样就不需要 用反射的方式了):

public class SetPriceCommand extends SetCommand {
    public SetPriceCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Object value) {
        super(domain, owner, feature, value);
    }
    public void doExecute() {
        Product product = (Product) owner;
        double newPrice = ((Double) value).doubleValue();
        newPrice = Math.max(0, newPrice);//New price value must >= 0
        newPrice=Math.round(newPrice*100)/100d;//Max fraction digits is 2    
        product.setPrice(newPrice);
    }
}

要让这个自定义命令生效,必须在ProductItemProvider里覆盖createSetCommand()方法,因为这个方法缺省是返回SetCommand的,我们要在这里做一个判断:如果修改的是价格属性,就返回我们自定义的这个命令,如下所示:

protected Command createSetCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Object value,
        int index) {
    if (feature == ShopPackage.eINSTANCE.getProduct_Price())
        return new SetPriceCommand(domain, owner, feature, value);
    return super.createSetCommand(domain, owner, feature, value, index);
}

这样当用户在属性页里改价格属性时,就会调用我们的SetPriceCommand了。顺便说一句,在GEF里 也有类似的EditDomain和Command,只是GEF里的Command一般通过EditPolicy的createXXXCommand()方 法来创建。因为GEF和EMF的两套Command机制没有实现统一的接口,所以结合GEF和EMF的时候常会遇到一些问题,需要额外的代码帮助解决,请 参考这两处讨论(讨论1,讨论2)。

最后要说一句,CreateChildCommand有点特殊,它是.Edit命令但不继承 AbstractOverrideableCommand,而且如果想在创建子元素时自动完成一些工作,不应该通过扩展这个类完成,而应该在 XXXItemProvider的collectNewChildDescriptors()方法里处理,这个方法决定每个对象可以创建哪些子元素,你可 以修改它的代码以对新建的元素做一些处理。

参考资料: Eclipse Modeling Framework A Developers Guide,第3.3节、第14.1节。