ОК, я не смог найти достойного примера для пользовательской 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» обновят свое проверенное состояние, когда я нажму на одну из них. Есть ли способ сделать это автоматически? Или мне нужно отслеживать все множественные пути для одного узла, которые в данный момент отображаются одновременно? (альтернативно, все существующие пути для одного узла - возможно в конечной группе обеспечения доступности баз данных, невозможно в бесконечной группе обеспечения доступности баз данных) Это проблема только для групп обеспечения доступности баз данных, где существует несколько путей для достижения одного и того же узла ... Мне нужно разобраться с этим в моем приложении.