关于Swing中“虚拟树”(自定义TreeModel)的问题

时间:2009-08-03 15:14:22

标签: java swing treemodel

好吧,我真的找不到Swing中使用超过基本功能的自定义TreeModel的一个不错的例子,所以我自己编写(代码如下),所以我可以提出有关它的问题,而不是关于更复杂的应用程序,当我理解如何编写它时,我真正想写的那个。对于这里存在多个相关问题这一事实表示道歉,很难在真空中提出这些问题而没有一个例子可以参考,我认为最好在一个帖子中提出一个例子,而不是分开我的的问题。我真正的应用程序不是无限的,只是大(状态存储在数据库中)所以自定义TreeModel似乎是合适的。

package com.example.test.gui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Map;
import javax.swing.AbstractCellEditor;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
/*
 * GUI rendering of the ancestry of hailstone numbers 
 * (see http://mathworld.wolfram.com/CollatzProblem.html)
 * 
 * This is an infinite tree model.
 * 
 * each node in the tree is a Long number
 * each node has 1 or 2 children:
 *   all nodes N have a child 2N
 *   any node N = 3k+1, where k > 0, has a second child k
 *   
 * checkboxes are present just to see custom rendering
 *   - nodes N where N is divisible by 7 are editable, the rest are not
 *   - editable nodes override their default state (stored in a hashmap)
 *   - default state of a node N is checked if N is divisible by 5,
 *     unchecked otherwise  
 */
class HailstoneTreeModel implements TreeModel {
    final private Map<Long,Boolean> modifiedCheckState = new HashMap<Long,Boolean>();

    @Override public Object getChild(Object parent, int index) {
        if (!(parent instanceof Long))
            return null;
        if (index < 0 || index > 1)
            return null;
        final long l = ((Long)parent).longValue();
        if (index == 0)
        {
            return (l*2);
        }
        else if ((l > 1) && (l-1)%3 == 0)
        {
            return (l-1)/3;
        }
        else
            return null;
    }

    @Override public int getChildCount(Object parent) {
        if (!(parent instanceof Long))
            return 0;
        final long l = ((Long)parent).longValue();
        if ((l > 1) && (l-1) % 3 == 0)
            return 2;
        return 1;
    }

    @Override public int getIndexOfChild(Object parent, Object child) {
        if (parent instanceof Long && child instanceof Long)
        {
            final long p = ((Long)parent).longValue();
            final long c = ((Long)child).longValue();
            if (p*2 == c)
                return 0;
            if (p == 3*c+1)
                return 1;
        }
        return -1;
    }

    @Override public Object getRoot() {
        return 1L;
    }

    @Override public boolean isLeaf(Object arg0) {
        return false;
    }

    @Override
    public void addTreeModelListener(TreeModelListener arg0) {
        // TODO Auto-generated method stub      
    }

    @Override
    public void removeTreeModelListener(TreeModelListener arg0) {
        // TODO Auto-generated method stub      
    }

    @Override
    public void valueForPathChanged(TreePath arg0, Object arg1) {
        // !!! what is typically done here and when does this get called?
    }

    public boolean isEditable(TreePath path) {
        if (path != null) {
            Object node = path.getLastPathComponent();
            // only the nodes divisible by 7 are editable
            if (node instanceof Long)
            {
                return ((Long)node) % 7 == 0;
            }
        }
        return false;
    }

    private void _setState(Long value, boolean selected)
    {
        this.modifiedCheckState.put(value, selected);
        System.out.println(value+" -> "+selected);      
    }
    public void setState(Object value, boolean selected) {
        if (value instanceof Long)
        {
            _setState((Long)value, selected);
        }       
    }
    private boolean _getState(Long value)
    {
        Boolean b = this.modifiedCheckState.get(value);
        if (b != null)
        {
            return b.booleanValue();                
        }
        return (value.longValue() % 5 == 0);
    }
    public boolean getState(Object value)
    {
        if (value instanceof Long)
        {
            return _getState((Long) value);
        }           
        return false;       
    }

    public void toggleState(Object value) {
        if (value instanceof Long)
        {
            _setState((Long)value, !_getState((Long)value));
        }       
    }   
}

// adapted from http://www.java2s.com/Code/Java/Swing-JFC/CheckBoxNodeTreeSample.htm
class CheckBoxNodeRenderer implements TreeCellRenderer {
    final private JCheckBox nodeRenderer = new JCheckBox();
    final private HailstoneTreeModel model;
    private Long currentValue = null; // value currently being displayed/edited

    final private Color selectionBorderColor, selectionForeground, selectionBackground,
    textForeground, textBackground;

    protected JCheckBox getNodeRenderer() {
        return this.nodeRenderer;
    }

    public CheckBoxNodeRenderer(HailstoneTreeModel model) {
        this.model=model;

        Font fontValue;
        fontValue = UIManager.getFont("Tree.font");
        if (fontValue != null) {
            this.nodeRenderer.setFont(fontValue);
        }
        Boolean booleanValue = (Boolean) UIManager
        .get("Tree.drawsFocusBorderAroundIcon");
        this.nodeRenderer.setFocusPainted((booleanValue != null)
                && (booleanValue.booleanValue()));

        this.selectionBorderColor = UIManager.getColor("Tree.selectionBorderColor");
        this.selectionForeground = UIManager.getColor("Tree.selectionForeground");
        this.selectionBackground = UIManager.getColor("Tree.selectionBackground");
        this.textForeground = UIManager.getColor("Tree.textForeground");
        this.textBackground = UIManager.getColor("Tree.textBackground");
    }

    public Component getTreeCellRendererComponent(JTree tree, Object value,
            boolean selected, boolean expanded, boolean leaf, int row,
            boolean hasFocus) {

        Component returnValue = this.nodeRenderer;
        String stringValue = tree.convertValueToText(value, selected,
                expanded, leaf, row, false);
        this.nodeRenderer.setText(stringValue);
        this.nodeRenderer.setSelected(false);       
        this.nodeRenderer.setEnabled(tree.isEnabled());

        if (selected) {
            this.nodeRenderer.setForeground(this.selectionForeground);
            this.nodeRenderer.setBackground(this.selectionBackground);
        } else {
            this.nodeRenderer.setForeground(this.textForeground);
            this.nodeRenderer.setBackground(this.textBackground);
        }

        if (value instanceof Long)
        {
            this.currentValue = (Long) value;
        }
        this.nodeRenderer.setSelected(this.model.getState(value));
        returnValue = this.nodeRenderer;
        return returnValue;
    }
    public Long getCurrentValue() { return this.currentValue; }
}


class CheckBoxNodeEditor extends AbstractCellEditor implements TreeCellEditor {

    final CheckBoxNodeRenderer renderer;
    final HailstoneTreeModel model;

    public CheckBoxNodeEditor(HailstoneTreeModel model) {
        this.model = model;
        this.renderer = new CheckBoxNodeRenderer(model);
        ItemListener itemListener = new ItemListener() {
            public void itemStateChanged(ItemEvent itemEvent) {
                Object cb = itemEvent.getItem();
                if (cb instanceof JCheckBox && itemEvent.getStateChange() == ItemEvent.SELECTED)
                {
                    Long v = CheckBoxNodeEditor.this.renderer.getCurrentValue(); 
                    CheckBoxNodeEditor.this.model.toggleState(v);
                }
                // !!! the following 3 lines are important because... ?
                if (stopCellEditing()) {
                    fireEditingStopped();
                }
            }
        };
        this.renderer.getNodeRenderer().addItemListener(itemListener);
    }

    public Object getCellEditorValue() {
        JCheckBox checkbox = this.renderer.getNodeRenderer();
        return checkbox;
    }

    @Override public boolean isCellEditable(EventObject event) {
        boolean returnValue = false;
        Object source = event.getSource();
        if (event instanceof MouseEvent && source instanceof JTree) {
            MouseEvent mouseEvent = (MouseEvent) event;         
            TreePath path = ((JTree)source).getPathForLocation(mouseEvent.getX(),
                    mouseEvent.getY());
            returnValue = this.model.isEditable(path);
        }
        return returnValue;
    }

    public Component getTreeCellEditorComponent(JTree tree, final Object value,
            boolean selected, boolean expanded, boolean leaf, int row) {

        Component editor = this.renderer.getTreeCellRendererComponent(tree, value,
                true, expanded, leaf, row, true);
        return editor;
    }
}

public class VirtualTree1 {
    public static void main(String[] args) {
        HailstoneTreeModel model = new HailstoneTreeModel();

        // Create a JTree and tell it to display our model
        JTree tree = new JTree(model);
        tree.setCellRenderer(new CheckBoxNodeRenderer(model));
        tree.setCellEditor(new CheckBoxNodeEditor(model));
        tree.setEditable(true);

        // The JTree can get big, so allow it to scroll
        JScrollPane scrollpane = new JScrollPane(tree);

        // Display it all in a window and make the window appear
        JFrame frame = new JFrame("Hailstone Tree Demo");
        frame.getContentPane().add(scrollpane, "Center");
        frame.setSize(400,600);
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }   
}

查看显示内容的第一条评论(上面应该可见)。它是一个显示无限树的自定义TreeModel,这是“常规”树中无法实现的,树中的所有节点都需要实际存在于内存中,但是可以使用自定义TreeModel,因为显示/实例化的唯一部分是用户点击的内容,这本质上是有限的。 : - )

我有一些杂项问题。 我会在第4条问题的最佳答案中给出这篇文章的接受答案。

1)TreeModel监听器 - 假设这是针对想要从TreeModel接收更新事件的类,我是否正确? (无论我是写它们还是其他人都这样做)什么是典型的用例?

2)TreeModel.valueForPathChanged() - 什么时候被调用,我通常会怎么做呢?

3)(这与TreeCellEditor / TreeCellRenderer有关) - 我改编的示例中的行有以下调用:

if (stopCellEditing()) {
    fireEditingStopped();
}

这是为了什么?

4)就班级组织而言 - 是否有更好的方法来构建这类事物?我想我应该在TreeModel(M =模型在MVC中)和TreeCellEditor / TreeCellRenderer(V =视图,或C =控制器,我不确定)之间分离,但他们模糊地需要了解彼此,并且我不确定哪个应该包含哪个引用。现在我将TreeModel作为独立对象,编辑器/渲染器具有对TreeModel的引用,因此它可以根据需要查询/变更模型。此外,我想知道自定义TreeCellEditor和TreeCellRenderer是否真的应该是一个实现两个接口的类。 itemStateChanged()中的CheckBoxNodeEditor方法似乎有点奇怪...当单击复选框时我得到一个项侦听器事件,然后我假设此事件来自渲染器,并切换适当的值因为我似乎无法弄清楚如何确定当前是否选中了复选框,所以此复选框对象中的选择似乎是鼠标是否已被点击或放开,而不是复选框状态。

可能有不止一种方法可以对其进行重组,因此它似乎是一种更好,更模块化的方法,但是现在我不知道如何做到这一点,所以任何建议都值得赞赏。

5)定向非循环图(DAG)显示为树形层次结构 - 如果运行应用程序,然后展开节点1,2,4,8,16,32,64,128,您将看到第二个“1 “这与第一个”1“实际上是相同的节点,因为此示例应用程序中的节点值只是Long个对象。如果将第二个“1”展开为1,2,4,8,16,32,64,128,您现在将看到两个“21”节点。必要时可以检查/取消选中“21”节点。但是在一个理想的世界里,当我点击其中一个时,两个“21”都会更新它们的检查状态。 有没有办法自动执行此操作?或者我是否必须跟踪当前同时显示的单个节点的所有多个路径? (或者,存在单个节点的所有路径 - 可能在有限DAG中,在无限DAG中不可能)这只是DAG的问题,其中有多条路径到达同一节点...我必须在我的申请中处理这个问题。

1 个答案:

答案 0 :(得分:1)

1

是。
JTree(或TreeUI)安装TreeModel侦听器,在数据更改时触发JTree的重新布局。有时只更新单个节点(当节点的值发生变化时),有时会执行整个树的重新布局。

2

见下文

3

我相信stopCellEditing更好的名字是shouldStopCellEditing。例如,如果用户点击了逃脱。为什么它没有直接约束我不知道。

4

IIRC,TreeCellEditor和TreeCellRenderer做需要了解该模型。他们获取了在getTreeCellXXX方法中显示为值对象所需的数据。

大致它的工作原理如下:

对于树中的每个节点:

  1. 获取树的TreeCellRenderer并调用getTreeCellRendererComponent(this,currNode,...)
  2. 使用SwingUtilities paintComponent方法在JTree上绘制上述组件
  3. 如果您正在编辑节点,请使用TreeCellEditor。
  4. 编辑完成后,从编辑器中获取新值。这可能是任何对象。
  5. 模型已更新(getModel()。valueForPathChanged(path_to_changed_node,newvalue))
  6. 最后一步是你需要的地方(在你的树模型中):

    一个。更新您的数据库  湾fireTreeNodesChanged在所有树节点上,这些节点是此数据的别名。

    我的解决方案。

    我会设计一个TreeNode(或sublcass DefaultMutableTreeNode),它包含一个描述它所代表的数据的键。确保最初没有孩子。让模型在树上安装TreeWillExpandListener,当节点WILL展开时,此时加载它的子节点。这允许延迟加载子项,并且在内存中只需要与树上的可见节点一样多的树节点。作为奖励,在循环图中,您有一个循环树,因为每个重复节点都是图中同一点的唯一别名。由于TreeWillExpandListener会加载(也可能是卸载)子项,因此您可以遍历树以查找等效别名,或者使用某种key =&gt;节点列表映射每个节点。