вопросы по "виртуальным деревьям" (custom TreeModel) в Swing - PullRequest
1 голос
/ 03 августа 2009

ОК, я не смог найти достойного примера для пользовательской TreeModel в Swing, которая использует больше, чем базовые функции, поэтому я написал свою собственную (код следует ниже), чтобы я мог задавать вопросы, а не о более сложное приложение, которое я действительно хочу написать, когда понимаю, как его написать. Извиняюсь за то, что здесь есть несколько связанных вопросов, трудно задать эти вопросы в вакууме без примера, и я полагаю, что лучше привести пример один раз в одном посте, чем выделять мои вопросы. Мое реальное приложение не бесконечно, просто велико (состояние хранится в базе данных), поэтому подходящая 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, отображающая бесконечное дерево, что невозможно в «обычном» дереве, где все узлы дерева должны фактически существовать в памяти, но возможно с использованием пользовательских TreeModels, поскольку отображаются только те части, которые создаются / создаются те, на которые пользователь нажимает, что по своей сути конечно. : -)

У меня есть несколько разных вопросов. Принятый ответ на этот пост будет дан за лучший ответ на мой вопрос № 4.

1) Слушатели TreeModel - правильно ли я полагаю, что это для классов, которые хотят получать события для обновлений от моей TreeModel? (пишу ли я их или кто-то другой) Каковы типичные варианты использования?

2) TreeModel.valueForPathChanged() - когда это будет вызвано, и что я обычно буду делать с этим?

3) (это связано с TreeCellEditor / TreeCellRenderer) - строки в адаптированном мною примере имели следующий вызов:

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

для чего это нужно?

4) Что касается организации классов - есть ли лучший способ структурировать подобные вещи? Я полагаю, у меня должно быть разделение между TreeModel (M = Model в MVC) и TreeCellEditor / TreeCellRenderer (V = view или C = controller, я не уверен), но они смутно должны знать друг о друге, и Я не был уверен, что должно содержать ссылку на что. Прямо сейчас у меня TreeModel в качестве независимого объекта, а редактор / рендерер имеет ссылку на TreeModel, поэтому он может запрашивать / изменять модель по мере необходимости. Также мне интересно, может быть, пользовательские TreeCellEditor и TreeCellRenderer действительно должны быть одним классом, реализующим оба интерфейса. Мой itemStateChanged() метод в CheckBoxNodeEditor кажется немного странным ... Я получаю событие прослушивания элемента, когда флажок нажимается, и затем я вроде бы предполагаю, что это событие происходит от средства визуализации, и переключаю соответствующее значение, так как я могу ' Кажется, не выясняется, как определить, установлен ли флажок в данный момент или снят, флажок в этом объекте флажка, по-видимому, заключается в том, была ли нажата или отпущена мышь, а не в состоянии флажка.

Вероятно, существует более одного способа реструктуризации, так что это кажется более подходящим, более модульным подходом, но сейчас я не уверен, как это сделать, поэтому любые предложения приветствуются.

5) Направленные ациклические графы (DAG), отображаемые в виде древовидной иерархии. Если вы запустите приложение, разверните узлы 1,2,4,8,16,32,64,128, и вы увидите секунду: msgstr "который на самом деле является тем же узлом, что и первый" 1 ", так как значения узлов в этом примере приложения являются просто Long объектами. Если вы увеличите эту вторую «1» до 1,2,4,8,16,32,64,128, вы увидите два «21» узла. Узел «21» может быть отмечен / снят при необходимости. Но в идеальном мире обе «21» обновят свое проверенное состояние, когда я нажму на одну из них. Есть ли способ сделать это автоматически? Или мне нужно отслеживать все множественные пути для одного узла, которые в данный момент отображаются одновременно? (альтернативно, все существующие пути для одного узла - возможно в конечной группе обеспечения доступности баз данных, невозможно в бесконечной группе обеспечения доступности баз данных) Это проблема только для групп обеспечения доступности баз данных, где существует несколько путей для достижения одного и того же узла ... Мне нужно разобраться с этим в моем приложении.

1 Ответ

1 голос
/ 03 августа 2009

1

Да.
JTree (или TreeUI) устанавливает прослушиватели TreeModel, которые запускают повторные макеты JTree при изменении данных. Иногда просто обновляя отдельные узлы (когда значение узла изменилось), иногда выполняю повторную компоновку всего дерева.

2

см. Ниже

3

Я полагаю, что лучшее название stopCellEditing - mustStopCellEditing. Например, если пользователь нажал escape. Почему это не связано напрямую, я не знаю.

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

Этот последний шаг - то, где вам нужно (в вашей древовидной модели):

а. обновите свою базу данных б. fireTreeNodesChanged на всех ваших узлах дерева, которые являются псевдонимами этих данных.

Мое решение.

Я бы разработал TreeNode (или sublcass DefaultMutableTreeNode), который содержит ключ, описывающий, какие данные он представляет. Убедитесь, что у него нет детей изначально. Пусть модель установит TreeWillExpandListener на дерево, и когда узел расширится, загрузите его потомки. Это позволяет выполнять отложенную загрузку дочерних элементов, и вам потребуется только столько узлов дерева в памяти, сколько видимых узлов дерева. В качестве бонуса, в циклическом графе у вас есть ациклическое дерево, поскольку каждый повторяющийся узел является уникальным псевдонимом для одной и той же точки на графике. Поскольку дочерние элементы загружаются (и, возможно, выгружаются) вашим TreeWillExpandListener, вы можете пройти по дереву в поисках эквивалентных псевдонимов или зарегистрировать каждый узел с помощью своего рода ключа => карты списка узлов.

...