1

OK、基本的な機能以上のものを使用する Swing のカスタム TreeModel の適切な例を実際に見つけることができなかったので、より複雑なアプリケーションではなく、それについて質問できるように独自に作成しました (コードは以下に続きます)。書き方を理解したら、本当に書きたいものです。ここに複数の関連する質問があることをお詫びします。参照する例がない状態でこれらの Q を単独で尋ねるのは難しいです。質問。私の実際のアプリケーションは無限ではなく、ただ大きい (状態がデータベースに格納されている) だけなので、カスタム 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 が、両方のインターフェースを実装する 1 つのクラスである必要があるのではないかと考えています。私のitemStateChanged()方法CheckBoxNodeEditorちょっと奇妙に思えます...チェックボックスがクリックされたときにアイテムリスナーイベントを取得します。次に、このイベントがレンダラーからのものであると想定し、適切な値を切り替えます。チェックボックスは現在チェックされているかチェックされていないため、このチェックボックスオブジェクトの選択は、チェックボックスの状態ではなく、マウスがクリックされたか離されたかのようです。

これを再構築する方法はおそらく複数あるため、より優れたモジュール化されたアプローチのように思えますが、現時点ではこれを行う方法がわからないため、提案をいただければ幸いです。

5) ツリー階層として表示される有向非巡回グラフ (DAG) -- アプリケーションを実行し、ノード 1、2、4、8、16、32、64、128 を展開すると、2 番目の「1」が表示されます。この例のアプリケーションのノード値は単なるLongオブジェクトであるため、実際には最初の「1」と同じノードです。この 2 番目の "1" を 1,2,4,8,16,32,64,128 に拡張すると、2 つの "21" ノードが表示されます。「21」ノードは、必要に応じてチェック/チェックを外すことができます。しかし、理想的な世界では、「21」のいずれかをクリックすると、両方の「21」のチェック状態が更新されます。これを自動的に行う方法はありますか?または、現在同時に表示されている 1 つのノードの複数のパスをすべて追跡する必要がありますか? (または、存在する単一ノードのすべてのパス -- 有限 DAG では可能ですが、無限 DAG では不可能です) これは、同じノードに到達する複数のパスがある DAG のみの問題です...私のアプリケーションでこれを処理します。

4

1 に答える 1

1

1

はい。
JTree (または TreeUI) は、データが変更されたときに JTree の再レイアウトをトリガーする TreeModel リスナーをインストールします。(ノードの値が変更されたときに) 単一のノードを更新するだけの場合もあれば、ツリー全体の再レイアウトを実行する場合もあります。

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 ))

その最後のステップは、(ツリーモデルで)必要な場所です。

a. データベースを更新します b. このデータのエイリアスであるすべてのツリー ノードで fireTreeNodesChanged。

私の解決策。

それが表すデータを説明するキーを保持する TreeNode (またはサブクラスの DefaultMutableTreeNode) を設計します。最初に子がないことを確認してください。モデルに TreeWillExpandListener をツリーにインストールさせ、ノードが展開するときに、この時点でその子をロードします。これにより、子の遅延ロードが可能になり、ツリーの可視ノードと同じ数のツリー ノードのみがメモリ内に必要になります。おまけとして、巡回グラフでは、繰り返される各ノードがグラフ内の同じスポットの一意のエイリアスであるため、非巡回ツリーがあります。子は TreeWillExpandListener によってロードされる (場合によってはアンロードされる) ため、ツリーを走査して同等のエイリアスを探したり、各ノードをある種のキー => ノード マップのリストに登録したりできます。

于 2009-08-03T16:23:58.673 に答える