定制Graph显示
JGraph本身提供了一些不错的绘图效果,能够定制的功能不多。可以通过setGridColor和setGridSize修改网格,可以通过setHandleColor和SetLockedHandleColor改变选定的颜色,背景颜色可以通过setBackground进行设置。
如果你想控制图元的渲染,可以继承缺省的渲染器,重载paint函数。渲染器继承自Component,基于Cell的属性进行渲染。JGraph及其继承类并不直接进行渲染操作,二是利用Cell的渲染代码。一个新的渲染器通过重载CellView的getRendererComponent方法,或者AbstractCellView的getRenderer来关联到Cell。
下面的代码展示了如何创建新的Cell Type和Renderer。代码向Graph中添加一个椭圆形顶点。最简单的方法是继承JGraph类,因为JGraph实现了CellViewFactory接口,她负责创建视图。
当创建一个视图,如果Cell不是边和端口的实例,JGraph假设Cell是一个顶点,并且调用createVertexView方法。因此我们只需要重写该方法来实现椭圆形顶点,返回相应的视图。
// Overrides JGraph.createVertexView protected VertexView createVertexView(Object v,GraphModel model,CellMapper cm) { // Return an EllipseView for EllipseCells if (v instanceof EllipseCell) return new EllipseView(v, model, cm); // Else Call Superclass return super.createVertexView(v, model, cm); }
椭圆形顶点通过EllipseCell类来表示,它继承自DefaultGraphCell,并不提供其他附件的方法。它只是用来区别椭圆形顶点和一般的顶点。
// Define EllipseCell public class EllipseCell extends DefaultGraphCell { // Empty Constructor public EllipseCell() { this(null); } // Construct Cell for Userobject public EllipseCell(Object userObject) { super(userObject); } }
EllipseView需要定义椭圆的特殊显示。它包含一个内部类,来提供渲染所需要的代码。视图和渲染器都分别继承自VertexView和VertexRender。需要重载getPerimeteroint来返回椭圆的边界点,重载getRenderer返回正确的渲染器,重载paint进行Cell的绘制。
// Define the View for an EllipseCell public class EllipseView extends VertexView { static EllipseRenderer renderer = new EllipseRenderer(); // Constructor for Superclass public EllipseView(Object cell, GraphModel model,CellMapper cm) { super(cell, model, cm); } // Returns Perimeter Point for Ellipses public Point getPerimeterPoint(Point source, Point p) { ... } // Returns the Renderer for this View protected CellViewRenderer getRenderer() { return renderer; } // Define the Renderer for an EllipseView static class EllipseRenderer extends VertexRenderer { public void paint(Graphics g) { ... } } }
重载getRenderer,而不是getRendererComponent的原因是我们继承的AbstractCellView已经提供了一个缺省的CellViewRender。
提示框(Tooltips)可以通过重载JGraph的getToolTipText来实现,getToolTipText继承自JComponent。下面的代码当鼠标指向Cell时,用提示框来用显示Cell的Label:
// Return Cell Label as a Tooltip public String getToolTipText(MouseEvent e) { if(e != null) { // Fetch Cell under Mousepointer Object c = getFirstCellForLocation(e.getX(), e.getY()); if (c != null) // Convert Cell to String and Return return convertValueToString(c); } return null; }
Graph必须通过Swing的TooltipManager进行注册才能使用提示框。可以在启动时调用如下代码实现:
ToolTipManager.sharedInstance().registerComponent(graph)
如果一个Graph显示了非常复杂的图形结构,通过一个属性对话框来进行编辑,比直接原地编辑方便许多。为了实现对话框编辑,BasicGraphUIt的startEditing和completeEditing必须被重载。为了在Graph中使用该UI,Graph的updateUI方法也必须重载。
// Define a Graph with a Custom UI public class DialogGraph extends JGraph { // Sets the Custom UI for this graph object public void updateUI(){ // Install a new UI立即发表 setUI(new DialogUI()); invalidate(); } }
DialogUI获得了视图的编辑权力,然后放入到一个对话框中,在该对话框是一个模式对话框(会锁定父对话框)。DialogUI的详细代码到网上去找吧。
动态修改Graph
GraphView环境中包含的是CellView。GraphView允许编辑CellView,GraphModel允许插入、删除、编辑GraphCell。
JGraph区分Model和View。Model通过GraphModel定义,它包含实现GraphCell接口的类。View通过GraphView定义,它包含实现CellView接口的类。Cell和View之间的映射被CellMapper接口定义:
一个模型拥有0个或多个View,对每一个Model中的每个Cell,每个GraphView只含有一个CellView。这些对象的状态通过一个Map的键值对来表示。每个CellView将它自己的属性和对应的GraphCell的属性进行合并。属性合并时,GraphCell的属性有相对于CellView高的优先级。
Cell的状态通过它的属性表示。不论什么情况下,GraphConstant类通过两步来改变Cell的状态:
l 创建变化对象
l 在Model或View上执行变化
为了创建变化对象,调用GraphConstants类的createMap函数获得一个新的Map对象。当一个Map应用到View或Cell的状态上,它并替换已经存在的Map。新Map的项会被添加或者修改。如果需要,可以调用GraphConstants的setRemoveAttibutes和setRemoveAll方法来删除已有状态Map中的一个或者全部状态。
当CellView的状态发生改变,或者它的邻居修改了属性,CellView的update将用来更新Cell的状态。
可以将Model看做两种结构的访问点:图元结构和群组结构。图元结构是数学定义的,比如顶点和边。群组结构用来组合Cell,比如父亲和孩子。
图元结构通过getSource和getTarget定义,它们分别返回对象的源和目的端口。返回的端口是顶点的孩子,它允许点之间多个连接的存在。
群组结构通过getChild、getChildCount、getIndexOfChild和getParent定义。如果一个对象没有父亲,它就是根节点(root),可以通过getRootAt或getRootCount进行访问。
下面的方法创建一个新的DefaultGraphCell,并将它添加到Model中。同时添加两个端口到Cell上,创建一个Cell的属性Map,属性键值对可以通过GraphConstants进行类型安全的访问。
void insertVertex(Object obj, Rectangle bounds) { Map attributeMap = new Hashtable(); // Create Vertex DefaultGraphCell cell = new DefaultGraphCell(userObject); // Create Attribute Map for Cell Map map = GraphConstants.createMap(); GraphConstants.setBounds(map, bounds); // Associate Attribute Map with Cell attributeMap.put(cell, map); // Create Default Floating Port DefaultPort port = new DefaultPort("Floating"); cell.add(port); // Additional Port Bottom Right int u = GraphConstants.PERCENT; port = new DefaultPort("Bottomright"); // Create Attribute Map for Port map = GraphConstants.createMap(); GraphConstants.setOffset(map, new Point(u, u)); // Associate Attribute Map with Port attributeMap.put(port, map); cell.add(port) // Add Cell (and Children) to the Model Object[] insert = new Object[]{cell}; model.insert(insert, null, null, attributeMap); }
insertVertex的第一个参数是用户对象,它包含了关联到Cell的数据信息。用户对象可以是String,也可以是用户自定义的对象。如果是自定义对象,应该实现对象的toString方法,以便可以获得Cell显示的字符串。第二个参数辨识顶点的边界,它被存储为一个属性。
attributeMap参数并不被Model使用,它传递给View,提供创建Cell View的属性信息。insert函数的第三个参数是存储在Model的属性。
由于端口被看做是是Model中的普通孩子,GraphModel接口可以用来找到顶点的缺省端口。
Port getDefaultPort(Object vertex, GraphModel model) { // Iterate over all Children for (int i = 0; i < model.getChildCount(vertex); i++) { // Fetch the Child of Vertex at Index i Object child = model.getChild(vertex, i); // Check if Child is a Portif (child instanceof Port) // Return the Child as a Port return (Port) child; } // No Ports Found return null; }
上面的代码使用第一个端口作为缺省端口。核心API不提供这样的功能的原因在于,缺省端口往往是应用所特有的。
下面的函数创建一个新的DefaultEdge,并将其添加到Model中,同时包含特定源和目的端口。
void insertEdge(Object obj, Port source, Port target) { // Create Edge DefaultEdge edge = new DefaultEdge(userObject); // Create ConnectionSet for Insertion ConnectionSet cs = new ConnectionSet(edge, source, target); // Add Edge and Connections to the Model Object[] insert = new Object[]{edge}; model.insert(insert, cs, null, null); }
第一个参数Cell的用户对象,第二个和第三个参数是新边的源和目的端口。为了在Model中插入连接,一个ConnectionSet实例是必需的。该实例用来收集新边的源端口和目的端口,并在一个单独的事物中执行变化。
如果一个Cell从Model中删除,Model检查Cell是否拥有孩子。如果有,更新群组结构,所有的父亲和孩子不会被删除。如果一个Cell同它的孩子一起删除,它可以重新插入,而不需要孩子和ParentMap。如果只删除Cell不删除其孩子,操作的结果打散了群组结构。
如上图所示,群组A包含一个Cell B和一个群组C,群组C包含一个Cell E和一个群组D,群组D包含Cell F和Cell G。删除C和D的结果如右图所示。
下面的代码删除所有选择的Cell,包含他的所有后代:
// Get Selected Cells Object[] cells = graph.getSelectionCells(); if (cells != null) { // Remove Cells (incl. Descendants) from the Model graph.getModel().remove(graph.getDescendants(cells)); }
ConnectionSet和ParentMap用来修改Model。属性可以通过包含Cell和属性键值对的Map修改。
Edge、port和vertex分别是DefaultEdge、DefaultPort和DefaultGraphCell的实例,它们都在Model之中。下面的代码修改edge的源到port,port的父亲修改为vertex,并且vertex的用户对象是字符串“Hello World”。
// Create Connection Set ConnectionSet connectionSet = new ConnectionSet(); connectionSet.connect(edge, port, true); // Create Parent Map ParentMap parentMap = new ParentMap(); parentMap.addEntry(port, vertex); // Create Properties for VertexMap properties = GraphConstants.createMap(); GraphConstants.setValue(properties, "Hello World"); // Create Property Map Map propertyMap = new Hashtable(); propertyMap.put(a, properties); // Change the Model model.edit(connectionSet, propertyMap, parentMap, null);
edit的最后一个参数类型是UndoableEdit[]。指定的edit是事务的一部分,它会并行地修改Model和View。
每个GraphCell拥有一个或多个CellView,它包含Cell的属性。CellView的属性可能被父类GraphView的editCell修改。与前面使用的propertyMap不同,在这使用attributeMap参数来向Model中插入Cell。attributeMap参数包含一个CellView实例来作为键,属性同样表示为键值对。下面的代码修改vertex的边界颜色为黑色:
// Work on the Graph’s View GraphView v = graph.getView(); // Create Attributes for Vertex Map attributes = GraphConstants.createMap(); GraphConstants.setBorderColor(attributes, Color.black); // Get the CellView to use as Key CellView cellView = v.getMapping(vertex, false); // Create Attribute Map Map attributeMap = new Hashtable(); attributeMap.put(cellView, properties); // Change the View v.editCells(attributeMap);