自动换行的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();
    }
}

Draw2d里的Invalidating和Updating

本文部分内容来自Building a Database Schema Diagram Editor with GEF和GEF and Draw2d Plug-in Developer Guide,是对Draw2D里一些基本概念的说明。

LayoutManager(布局管理器)

  • 布局管理器通过Figure#setBounds()改变子图形的位置和大小。
  • 根据布局算法和子图形决定当前图形的preferredSize。
  • 布局的过程是先确定图形的大小,再计算每个子图形的新位置和大小。

Figure#invalidate()

  • 若valid属性已经是false则直接返回。
  • 如果图形拥有LayoutManager,则调用LayoutManager的invalidate()方法,在XYLayout里作用是将preferredSize重置为null值,在FlowLayout里还要把minimumSize置为null值。
  • 将图形的valid属性置为false。

Figure#revalidate()

  • 我觉得它实际代表"recursive invalidate"的意思。这个方法的功能是首先将图形自己invalidate(),然后递归的将图形的父图形invalidate(),一直到根图形为止,这个根图形会被加入到UpdateManager的一个列表中。
  • 在Figure的很多方法里,如setBorder()、setContstraint()、setLayoutManager()、add()、remove()等,会自动调用revalidate()方法。因此,大部分情况下我们不需要手动调用这个方法。

Figure#validate()

  • 若valid属性已经是true则直接返回。
  • 将图形的valid属性置为true。
  • 如果图形拥有LayoutManager,则调用LayoutManager的layout()方法。
  • 对图形的每个子图形,调用validate()方法。

Figure#repaint()

  • 在图形的UpdateManager里,将图形所处的区域标记为“脏”区域,这个区域将由UpdateManager(定期)重画。
  • 在图形的setVisible()、setOpaque()、setForegroundColor()、setBounds()、setBackgroundColor()等方法里会自动调用repaint()方法。

Figure#paint()

  • 虽然名称相似,但这个方法和repaint()关系不大。在Figure里这个方法按顺序调用paintFigure()、paintClientArea()和paintBorder()这三个方法,当实现自己的Figure时,绝大多数情况下应该只覆盖paintFigure()而不是paint()本身。

Figure#getPreferredSize()

  • 对于Label这样的图形,它的preferredSize由它所显示的文本和图标所占空间决定;如果一个图形包含子图形,则它的preferredSize要考虑子图形的排列方式,所以要由LayoutManager来决定。
  • LayoutManager的getPreferredSize()方法还有两个参数:wHint和hHint,它们分别代表图形的已知长(宽)度,如果其中一个值是大于零的,则在另一个方向上子图形将换行(列)排列,以保证长(宽)度不大于这个已知值。

基本上来说,validate是对于尺寸的调整,而repaint()是对颜色的调整。当我们把一个图形C作为子图形拖到另一个图形P里的时候(想象P为UML类图里表示类的矩形,C为表示属性或方法的矩形),因为调用了P的add()方法,所以P及P的所有“祖先”图形都将通过revalidate()被置为invalid状态。UpdateManager随后在performUpdate()里对这些图形进行validate(),在validate()的过程中,每个图形将通过自己的LayoutManager重新计算自己的尺寸。这样就实现了P随子图形的多少自动改变大小。

上面左图是在子图形上发生改变时,自动调用了Fig4的invalidate()方法,导致到根图形之间的所有图形的invalidate()方法被触发。右图则是UpdateManager对这些invalid图形进行validate(),并且是自上而下进行的(几乎可以认为validate()方法就是对layout()方法的调用)。注意到由于对Fig2进行了layout(),Fig5的尺寸也可能因此发生改变,如果发生了这种情况,则Fig5的invalidate()方法也会被调用。

[Eclipse]GEF入门系列(十一、树的一个实现)

两天前GEF发布了3.1M7版本,但使用下来发现和M6没有什么区别,是不是主要为了和Eclipse版本相配套?希望3.1正式版早日发布,应该会新增不少内容。上一篇帖子介绍了如何实现表格功能,在开发过程中,另一个经常用到的功能就是树,虽然SWT提供了标准的树控件,但使用它完成如组织结构图这样的应用还是不够直观和方便。在目前版本(3.1M7)的GEF中虽然没有直接支持树的实现,但Draw2D提供的例子程序里却有我们可以利用的代码(org.eclipse.draw2d.examples.tree.TreeExample,运行界面见下图),通过它可以节约不少工作量。

图1 Draw2D例子中的TreeExample

记得数年前曾用Swing做过一个组织结构图的编辑工具,当时的实现方式是让画布使用XYLayout,在适当的时候计算和刷新每个树节点的位置,算法的思想则是深度优先搜索,非树叶节点的位置由其子节点的数目和位置决定。我想这应该是比较直观的方法吧,但是这次看了Draw2D例子里的实现觉得也很有道理,以前没想到过。在这个例子里树节点图形称为TreeBranch,它包含一个PageNode(表现为带有折角的矩形)和一个透明容器contentsPane,(一个Layer,用来放置子节点)。在一般情况下,TreeBranch本身使用名为NormalLayout的布局管理器将PageNode放在子节点的正上方,而contentsPane则使用名为TreeLayout的布局管理器计算每个子节点应在的位置。所以我们看到的整个树实际上是由很多层子树叠加而成的,任何一个非叶节点对应的图形的尺寸都等于以它为根节点的子树所占区域的大小。

从这个例子里我们还看到,用户可以选择使用横向或纵向组织树(见图2),可以压缩各节点之间的空隙,每个节点可以横向或纵向排列子节点,还可以展开或收起子节点,等等,这为我们实现一个方便好用的树编辑器提供了良好的基础(视图部分的工作大大简化了)。

图2 纵向组织的树

这里要插一句,Draw2D例子中提供的这些类的具体内容我没有仔细研究,相当于把它们当作Draw2D API的一部分来用了(包括TreeRoot、TreeBranch、TreeLayout、BranchLayout、NormalLayout、HangingLayout、PageNode等几个类,把代码拷到你的项目中即可使用),因为按照GEF 3.1的计划表,它们很有可能以某种形式出现在正式版的GEF 3.1里。下面介绍一下我是如何把它们转换为GEF应用程序的视图部分从而实现树编辑器的。

首先从模型部分开始。因为树是由一个个节点构成的,所以模型中最主要的就是节点类(我称为TreeNode),它和例子里的TreeBranch图形相对应,它应该至少包含nodes(子节点列表)和text(显示文本)这两个属性;例子里有一个TreeRoot是TreeBranch的子类,用来表示根节点,在TreeRoot里多了一些属性,如horizontal、majorSpacing等等用来控制整个树的外观,所以模型里也应该有一个继承TreeNode的子类,而实际上这个类就应该是编辑器的contents,它对应的图形TreeRoot也就是一般GEF应用程序里的画布,这个地方要想想清楚。同时,虽然看起来节点间有线连接,但这里我们并不需要Connection对象,这些线是由布局管理器绘制的,毕竟我们并不需要手动改变线的走向。所以,模型部分就是这么简单,当然别忘了要实现通知机制,下面看看都有哪些EditPart。

与模型相对应,我们有TreeNodePart和TreeRootPart,后者和前者之间也是继承关系。在getContentPane()方法里,要返回TreeBranch图形所包含的contentsPane部分;在getModelChildren()方法里,要返回TreeNode的nodes属性;在createFigure()方法里,TreeNodePart应返回TreeBranch实例,而TreeRootPart要覆盖这个方法,返回TreeRoot实例;另外要注意在refreshVisuals()方法里,要把模型的当前属性正确反映到图形中,例如TreeNode里有反映节点当前是否展开的布尔变量expanded,则refreshVisuals()方法里一定要把这个属性的当前值赋给图形才可以。以下是TreeNodePart的部分代码:

public IFigure getContentPane() {
    return ((TreeBranch) getFigure()).getContentsPane();
}

protected List getModelChildren() {
    return ((TreeNode) getModel()).getNodes();
}

protected IFigure createFigure() {
    return new TreeBranch();
}

protected void createEditPolicies() {
    installEditPolicy(EditPolicy.COMPONENT_ROLE, new TreeNodeEditPolicy());
    installEditPolicy(EditPolicy.LAYOUT_ROLE, new TreeNodeLayoutEditPolicy());
    installEditPolicy(EditPolicy.SELECTION_FEEDBACK_ROLE, new ContainerHighlightEditPolicy());
}

上面代码中用到了几个EditPolicy,这里说一下它们各自的用途。实际上,从Role中已经可以看出来,TreeNodeEditPolicy是用来负责节点的删除,没有什么特别;TreeNodeLayoutEditPolicy则复杂一些,我把它实现为ConstrainedLayoutEditPolicy的一个子类,并实现createAddCommand()和getCreateCommand()方法,分别返回改变节点的父节点和创建新节点的命令,另外我让createChildEditPolicy()方法返回NonResizableEditPolicy的实例,并覆盖其createSelectionHandles()方法如下,以便在用户选中一个节点时用一个控制点表示选中状态,不用缺省边框的原因是,边框会将整个子树包住,不够美观,并且在多选的时候界面比较混乱。

protected List createSelectionHandles() {
    List list=new ArrayList();
    list.add(new ResizeHandle((GraphicalEditPart)getHost(), PositionConstants.NORTH));
    return list;
}

选中节点的效果如下图,我根据需要改变了树节点的显示(修改PageNode类):

图3 同时选中三个节点(Node2、Node3和Node8)

最后一个ContainerHighlightEditPolicy的唯一作用是当用户拖动节点到另一个节点区域中时,加亮显示后者,方便用户做出是否应该放开鼠标的选择。它是GraphicalEditPolicy的子类,部分代码如下,如果你看过Logic例子的话,应该不难发现这个类就是我从那里拿过来然后修改一下得到的。

protected void showHighlight() {
    ((TreeBranch) getContainerFigure()).setSelected(true);
}

public void eraseTargetFeedback(Request request) {
    ((TreeBranch) getContainerFigure()).setSelected(false);
}

好了,现在树编辑器应该已经能够工作了。为了让用户使用更方便,你可以实现展开/收起子节点、横向/纵向排列子节点等等功能,在视图部分Draw2D的例子代码已经内置了这些功能,你要做的就是给模型增加适当的属性。我这里的一个截图如下所示,其中Node1是收起状态,Node6纵向排列子节点(以节省横向空间)。

图4 树编辑器的运行界面

这个编辑器我花一天时间就完成了,但如果不是利用Draw2D的例子,相信至少要四至六天,而且缺陷会比较多,功能上也不会这么完善。我感觉在GEF中遇到没有实现过的功能前最好先找一找有没有可以利用的资源,比如GEF提供的几个例子就很好,当然首先要理解它们才谈得上利用。