2.4 改造EnumGeneratorNewWizardPage类
这个类有很多地方不能满足我们的要求,下面一一地进行修改。
2.4.1 修改构造函数
代码2-2是修改以后的EnumGeneratorNewWizardPage类的构造函数。
【代码2-2】修改以后的构造函数:
public EnumGeneratorNewWizardPage(ISelection selection)
{
super("wizardPage");
setTitle("Multi-page Editor File");
setDescription("This wizard creates a new file with *.java
extension that can be opened by a multi-page editor.");
this.selection = selection;
}
在构造函数中首先调用父类的构造函数,然后调用setTitle为向导页设定标题,调用setDescription方法为页设置描述信息,最后把代表用户选择项的ISelection赋值给selection私有变量。
向导的标题、向导页的标题、向导页的描述是不同的,如图2.10所示。

图2.10 向导不同的提示显示
“向导的标题”是一个向导窗口的标题,这个标题在这个向导的生命周期内是不变的;“向导页的标题”是向导中每一页的标题,它标明了此页的作用,“向导页的描述”是对此页功能的进一步解释。
2.4.2 修改createControl方法
createControl方法是Override父类的一个方法,框架会调用此方法绘制向导页界面。
【代码2-3】修改后的createControl方法:
public void createControl(Composite parent)
{
Composite container = new Composite(parent, SWT.NULL);
GridLayout layout = new GridLayout();
container.setLayout(layout);
layout.numColumns = 3;
layout.verticalSpacing = 9;
Label label = new Label(container, SWT.NULL);
label.setText("&Container:");
containerText = new Text(container, SWT.BORDER | SWT.SINGLE);
GridData gd = new GridData(GridData.FILL_HORIZONTAL);
containerText.setLayoutData(gd);
containerText.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e)
{
dialogChanged();
}
});
Button button = new Button(container, SWT.PUSH);
button.setText("Browse...");
button.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e)
{
handleBrowse();
}
});
label = new Label(container, SWT.NULL);
label.setText("&File name:");
fileText = new Text(container, SWT.BORDER | SWT.SINGLE);
gd = new GridData(GridData.FILL_HORIZONTAL);
fileText.setLayoutData(gd);
fileText.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e)
{
dialogChanged();
}
});
initialize();
dialogChanged();
setControl(container);
}
我们不能直接在createControl传递过来的parent上绘制界面,因为这个parent代表所有向导页面(包括向导选择页面)的父Composite,如果直接在这个parent上绘制界面会导致界面显示混乱,所以要调用“Composite container = new Composite(parent, SWT.NULL);”创建一个新的Composite。
接下来为这个container设定布局管理器,在插件项目中采用GridLayout是比较方便的。GridLayout把界面分成网格,控件都放在这些网格中,与Swing的GridLayout不同的地方就是它不仅可以为每一列设定不同的宽度,而且还可以让一个控件占据多个网格。由于插件项目中的界面大部分都是规矩的表单式排列,因此采用GridLayout布局管理器是比较好的。因此这里采用GridLayout作为布局管理器,并设定其为3列。
创建标记控件、按钮控件、文本框控件等,并为它们设定合适的GridData。需要注意的是每个控件的GridData不能与其他控件共享,也就是说,即使两个控件使用的GridData全部一样,也要为它们各创建一个GridData。
在创建控件的时候,给containerText控件和fileText都增加了ModifyListener,当控件内容改变的时候,就会去调用dialogChanged方法。我们给【浏览】按钮增加了监听器,这样当按钮被按下的时候handleBrowse方法就会被调用以弹出容器选择对话框。
为了减少复杂性,这里指定fileText为不可手工编辑。接着调用initialize方法进行一些初始化操作,然后调用dialogChanged方法,最后调用setControl方法将container设定为本向导页的页面。
2.4.3 修改initialize方法
initialize方法被createControl方法调用来初始化控件,为了适应需求,我们进行如下的修改。
【代码2-4】修改后的initialize方法:
private void initialize()
{
if (selection != null && selection.isEmpty() == false
&& selection instanceof IStructuredSelection)
{
IStructuredSelection ssel = (IStructuredSelection) selection;
if (ssel.size() > 1)
return;
Object obj = ssel.getFirstElement();
if (obj instanceof IResource)
{
IContainer container;
if (obj instanceof IContainer)
container = (IContainer) obj;
else
container = ((IResource) obj).getParent();
containerText.setText(container.getFullPath().toString());
}
}
fileText.setText("*.java");
}
这段代码用来把打开向导界面之前在【包资源管理器】视图中选中的对象的容器作为【容器】控件的初始值,并为fileText控件赋初值。
基础知识:
(1) IResource、IContainer、IContainer、IFolder、IProject、IWorkspaceRoot、IFile
图2.11是这些接口的类型层次图(在IResource上右击,在弹出的快捷菜单中选择【打开类型层次结构】命令)。

图2.11 Eclipse的资源类继承图
这个结构图就代表了Eclipse中的所有资源。其中IFolder代表文件夹,IProject代表项目,IWorkspaceRoot代表工作空间根目录。因为这3个资源都可以包含下级资源,所以为它们抽象一个公共接口IContainer出来。与IContainer同级的IFile代表文件。为IFile和IContainer再抽取一个公共接口IResource出来。
(2) IStructuredSelection
被选择对象用ISelection来表示,但是ISelection表达的内容太少,因此继承一个子接口IStructuredSelection出来,通过这个接口可以得到被选择的对象,而且支持多选。
在initialize方法中首先判断selection是不是不为空、是不是实现了IStructuredSelection接口。请注意,如果在资源视图的某个节点上能通过右键菜单弹出这个向导的话那么selection一定实现了IStructuredSelection接口。如果选择了一个对象并且被选择的对象实现了IResource接口的话,则继续判断。
如果被选择的对象实现了IContainer接口(即被选择的对象是文件夹、项目或者工作空间根目录)的话,就调用这个IContainer的getFullPath方法把容器的路径填充到“容器”控件中。如果被选择对象没有实现IContainer接口,也就是文件的话,那么其父容器一定实现了IContainer接口,所以这里把其父容器的FullPath填充到“容器”控件中。
这段代码需要改造,因为它的实现是把选择对象的容器路径赋值给控件,而我们的要求是把被选择包的包名赋值给控件。
我们需要判断的是被选择的对象是不是Java包。在JDT中用IPackageFragment类代表的包,它定义在org.eclipse.jdt.core下。在文件的import部分加入“import org.eclipse.jdt.core. IPackageFragment;”,我们会发现系统报错,说找不到IPackageFragment接口。原来IPackageFragment所在的包没有被放到项目的类路径中。那么如何把包放到类路径中呢?您也许会说,找到相应的jar包,然后加入到项目的构建路径中不就可以了吗?
这是初学插件开发的人员常犯的错,他们经常会问这样的问题:我做的插件在Eclipse中开发的时候没有编辑错误,可是我为什么就运行不了呢?
这是因为Eclipse插件的类引用机制比较特别。要弄明白这个问题,首先要弄清楚“插件依赖”的概念。
一个插件不依赖于其他插件是几乎不可能的事情,只有“站在巨人的肩膀上”才能更快更好地做出一个有实用价值的插件。我们写程序时依赖的一些组件通常都是以jar包的形式提供的,比如XML解析用的dom4j、日志工具log4j等,使用这些包的时候只要将jar包加入构建路径就可以了。Eclipse插件则不同,Eclipse插件是有生命周期的,也就是说Eclipse插件的jar包(Eclipse插件并不一定以jar包形式发布,这里这样说是为了方便描述)是“活的”,而普通jar包是“死的”。我们在开发插件的时候,只要指定此插件依赖的插件的标识id即可,无需知道此插件对应的jar包,Eclipse会自动加载此插件对应的jar包。在插件工程中,有个名字为“插件依赖项”的库列表,如图2.12所示。

图2.12 插件依赖项
这个列表是只读的,无法向其中加入内容,此库列表中的jar包是由PDE读取插件项目的依赖项以后根据依赖项的标识自动加载的,只是起到了方便开发的作用,在实际运行的时候并不一定会引用这些列表中所列的这些包,而且此工程拿到其他版本的Eclipse中打开的时候,这个列表中的内容也会随着变化。
那么如何在开发环境中添加依赖项呢?
双击plugin.xml打开插件配置编辑器,切换到【依赖性】页,单击【添加】按钮,如图2.13所示。

图2.13 添加依赖项
在弹出的【选择插件】窗口中可以直接输入插件的标识id,也可以在下方的插件列表中选择。那么如何知道我们依赖的插件的标识id呢?第一种方式就是老老实实地去查Eclipse帮助文档,另一种方式就是猜测。Eclipse中标准插件的命名是很有规律的,每个不同的插件都放在不同的包中,此插件也以此包作为标识id,这样就可以避免冲突。所以当我们要引用一个类的时候,只要尝试着将类的包路径输入【选择插件】文本框中,然后一级一级地排除包,直到有和下方的插件列表符合的为止。比如接口IPackageFragment在包org.eclipse.jdt.core下,我们在【依赖性】选项卡中单击【添加】按钮,在【选择插件】文本框中输入“org.eclipse.jdt.core”就立即可以看到有两个相符项了,如图2.14所示。

图2.14 选择依赖插件
选择第一项,单击【确定】按钮,将此插件引入。再来查看“插件依赖项”,可以看到“org.eclipse.jdt.core_***.jar”已经被引入构建路径了,如图2.15所示。

图2.15 动态生成的依赖Jar包
编写如下代码:
if (obj instanceof IPackageFragment)
{
IPackageFragment pckFragment = (IPackageFragment)obj;
containerText.setText(pckFragment.getElementName());
}
运行插件项目,在一个包上右击,运行此向导,可以发现选中的包名被自动填到了【包】文本框中,如图2.16所示。
![]()
图2.16 选择被创建文件所在的包
但是如果我们在一个Java文件夹上右击,运行向导的时候,Java文件所在包的包名就不会自动填充到文本框中了。可以断定Java文件不是IPackageFragment类型了,可是到底是什么类型呢?查帮助文档?到网上搜索?到BBS中发帖提问?当然不用。这些都来得太慢了,程序员最不缺乏的就是探索精神。
以调试方式启动Eclipse插件工程,在“if (obj instanceof IPackageFragment)”一句处设置断点,在被调试的Eclipse中的工程下选择一个Java文件,右击,选择“枚举创建向导”,单击【下一步】按钮,此时程序就会在刚才添加的断点处暂挂。选择变量“obj”,右击,在弹出的快捷菜单中选择【检查】命令,此时就会显示出此变量的类型等信息,如图2.17所示。
从图中可以看出obj的类型是CompilationUnit。
根据实验结果来完善代码,添加如下代码:
else if(obj instanceof CompilationUnit)
{
CompilationUnit cu =(CompilationUnit)obj;
containerText.setText(cu.getParent().getElementName());
}

图2.17 调试视图中查看变量类型
这里这样写是没有错误的,但是在Eclipse的插件开发中要尽量基于接口编程,CompilationUnit是实现了ICompilationUnit接口的实现类,因此我们要修改代码如下:
else if(obj instanceof ICompilationUnit)
{
ICompilationUnit cu =(ICompilationUnit)obj;
//cu的父容器一定是一个包,所以直接通过cu.getParent()得到Java
//文件所在的包
containerText.setText(cu.getParent().getElementName());
}
ICompilationUnit在两个地方都有定义,分别是“org.eclipse.jdt.core”包下和“org.eclipse. jdt.internal.compiler.env”包下的,要记住这里使用的是第一个。
2.4.4 修改handleBrowse方法
当用户单击【浏览】按钮的时候,handleBrowse方法会被调用以弹出一个容器选择对话框,并把用户选择的值填充到文本框中。这是不符合我们的需求的,我们的需求是弹出一个包选择对话框,并把用户选择的值填充到文本框中。
问题的焦点就集中到了如何弹出一个包选择对话框了。如果要自己实现的话需要处理的问题就太多了,好在JDT为我们提供了一个工具类org.eclipse.jdt.ui.JavaUI,这个工具类中有一个静态方法createPackageDialog,调用这个方法就可以创建一个包选择对话框。通过查询帮助文档得知JavaUI定义在插件“org.eclipse.jdt.ui”中,因此在使用这个类之前,首先要将“org.eclipse.jdt.ui”加入插件的依赖项。
createPackageDialog方法有4个重载方法,因为我们要创建一个可以选择工程中所有包的包选择对话框,我们最终敲定使用:
public static SelectionDialog createPackageDialog(Shell parent, IJavaProject project, int style)
我们可以把当前向导页的Shell传递给第1个参数,第3个参数传递一个风格就可以。难点在第2个参数,因为它要我们传递要对哪个Java项目创建对话框,这个IJavaProject接口就代表了一个Java项目。
我们现在运行的类是在一个界面中,因此取得外界信息的唯一方式就是构造函数传递过来的ISelection。我们在一个Java项目中启动“枚举创建向导”之前,一般都会选中项目中的某些元素,比如包、Java文件、项目等,我们只能尝试通过它们去突破了。打开熟悉的ICompilationUnit接口,来看看它有哪些方法。
在ICompilationUnit接口源码中,按Ctrl+O快捷键打开此类的类型成员,如图2.18所示。

图2.18 ICompilationUnit的成员
保持这个提示框不关闭,再次按Ctrl+O快捷键打开此类的所有继承的成员,如图2.19所示。

图2.19 ICompilationUnit以及父接口的成员
在这个列表中发现了我们要寻找的目标:getJavaProject,单击此方法就会转到此方法的声明处,原来这个方法定义在IJavaElement接口中。IJavaElement是Java工程中所有Java特有元素(包、源文件、Java工程等)的基础接口。选择IJavaElement,右击,在弹出的快捷菜单中选择【打开类型层次结构】命令,如图2.20所示显示出了IJavaElement的类体系。

图2.20 IJavaElement的类继承图
根据名字可以看出:IClassFile表示.class文件,IJavaProject代表Java工程等,这些就代表了所有Java项目中能选择的元素。
经过一番简单分析,得到当前Java项目的方法完成。
【代码2-5】得到当前Java项目:
private IJavaProject getCurrentJavaProject()
{
if (selection != null && selection.isEmpty() == false
&& selection instanceof IStructuredSelection)
{
IStructuredSelection ssel = (IStructuredSelection) selection;
Object obj = ssel.getFirstElement();
if(obj instanceof IJavaElement)
{
return ((IJavaElement)obj).getJavaProject();
}
}
return null;
}
其他的功能都好实现,我们可以直接参考已完成的handleBrowse方法。
【代码2-6】handleBrowse方法:
private void handleBrowse()
{
IJavaProject JavaProject = getCurrentJavaProject();
if(JavaProject==null)
{
MessageDialog.openWarning(getShell(), "error",
"请在Java项目内运行此向导!");
}
SelectionDialog dialog = null;
try
{
dialog = JavaUI.createPackageDialog(getShell(),JavaProject,
IJavaElementSearchConstants.CONSIDER_REQUIRED_PROJECTS);
} catch (JavaModelException e1)
{
MessageDialog.openWarning(getShell(), "error", e1.getMessage());
e1.printStackTrace();
}
if (dialog.open() != Window.OK)
{
return;
}
IPackageFragment pck = (IPackageFragment) dialog.getResult()[0];
if (pck != null)
{
containerText.setText(pck.getElementName());
}
}
2.4.5 修改dialogChanged方法
当两个文本框中的内容发生变化的时候就调用dialogChanged方法,在这个方法中校验界面控件状态是否合法,并给出提示信息。
【代码2-7】dialogChanged方法:
private void dialogChanged()
{
String pckName = packageText.getText();
String fileName = fileText.getText();
if (pckName==null||pckName.length() == 0)
{
updateStatus("请指定包");
return;
}
if (fileName==null||fileName.length() == 0)
{
updateStatus("请输入文件名");
return;
}
if (fileName.replace('\\', '/').indexOf('/', 1) > 0)
{
updateStatus("文件名不合法");
return;
}
int dotLoc = fileName.lastIndexOf('.');
if (dotLoc != -1)
{
String ext = fileName.substring(dotLoc + 1);
if (ext.equalsIgnoreCase("Java") == false)
{
updateStatus("文件扩展名必须是 \"java\"");
return;
}
}
updateStatus(null);
}
由于包文本框是不可手工编辑的,所以此处忽略了对包文本框的合法性校验。判断文件名是否以 .java结尾时,最好使用正则表达式,不过此处为了方便我们就沿用了自动生成的代码。
2.4.6 分析updateStatus方法
【代码2-8】updateStatus方法:
private void updateStatus(String message)
{
setErrorMessage(message);
setPageComplete(message == null);
}
这个方法非常简单,就是把传过来的校验信息显示出来,并决定【下一步】按钮是否可用。调用setErrorMessage方法设置向导页的错误信息,而setPageComplete方法用来设置【下一步】按钮是否可用。
2.4.7 取得界面控件值的方法
【代码2-9】取得界面控件值:
public String getPackageName()
{
return packageText.getText();
}
public String getFileName()
{
return fileText.getText();
}







