JavaFX TreeView 多个对象类型?(以及更多)

9

我现在有如下的对象数据结构:

物品

  • 字符串 名称
  • 信息的ArrayList

角色

  • 字符串 名称
  • 物品 的集合

账户

  • 字符串 名称
  • 最多包含 8 个 角色 的集合

我想要制作一个类似以下的树形视图:

Root(invisible)
======Jake(Account)
============JakesChar(Character)
==================Amazing Sword(Item)
==================Broken Bow(Item)
==================Junk Metal(Item)
======Mark(Account)
============myChar(Character)
==================Godly Axe(Item)
======FreshAcc(Account)
======MarksAltAcc(Account)
============IllLvlThisIPromise(Character)
======Jeffrey(Account)
============Jeff(Character)
==================Super Gun(Item)
==================Better Super Gun(Item)
==================Super Gun Scope(Item)

我编造了所有这些名称和内容,很明显真实的实现会更加复杂。那么如何实现呢?TreeItem要求每个子项与其父项是相同类型。
我唯一的解决方案是执行以下步骤:
public class ObjectPointer
{
    Object pointer;
    String name;
}

我的TreeView将是ObjectPointer类型的,在每一行上,我将把ObjectPointer转换为AccountCharacterItem。这样做很糟糕,但是我认为它会起作用。

子问题:

  • 如何使TreeItem检测到setOnMouseHover事件?

  • 如何使TreeItem不使用其类型的toString方法,而是使用自定义的方式展示它们需要的String属性?

  • 如何使TreeItem在GUI中显示有颜色的文本,而不是纯文本?

谢谢!


主要问题很难处理。你的其他问题(你真的应该将不同的问题分开发布,而不是将多个问题捆绑成一个)都可以通过“在TreeView上使用cellFactory”来回答。如果我以后有时间,我会看看能否回答主要问题。 - James_D
你可以考虑创建一个接口(比如Displayable),并让所有的类,如Character、Item、Account等都实现它。这样,你就可以创建一个TreeView<Displayable>,但是这取决于你的应用程序的复杂性,可能会变得很丑陋。看到你想要显示的架构,另一种解决方案可能是拥有一个XML(或JSON或其他)转换器来转换你的模型类。然后,你只需要解析XML数据来将其显示给用户。在我看来,这似乎是分离模型和显示逻辑的好方法。 - pwillemet
无论你做什么,某些时候它都会变得很丑陋,因为TreeView是同质的。让所有对象实现一个接口可以有所帮助。 - James_D
所以实际上,我撤回关于丑陋的部分。由于模型中的所有类都非常相似,因此将(通用)超类分解并将其用作树的类型(基本上是@Kwoinkwoin建议的内容)使得这非常好。根据您想要做什么,您可能需要在单元格实现中进行一些类型测试。 - James_D
1个回答

23

如果您从泛化的角度看待模型,所有类都具有一定的相似性,这些相似性可以提取出来形成一个超类:

package model;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public abstract class GameObject<T extends GameObject<?>> {


    public GameObject(String name) {
        setName(name);
    }

    private final StringProperty name = new SimpleStringProperty();

    public final StringProperty nameProperty() {
        return this.name;
    }


    public final String getName() {
        return this.nameProperty().get();
    }


    public final void setName(final String name) {
        this.nameProperty().set(name);
    }

    private final ObservableList<T> items = FXCollections.observableArrayList();

    public ObservableList<T> getItems() {
        return items ;
    }

    public abstract void createAndAddChild(String name);
}

这里的类型参数 T 表示“子”对象的类型。因此,您的 Account 类(其子类型为 GameCharacter - 顺便说一下,不要将类命名为 java.lang 中的任何内容...)如下所示:
package model;

public class Account extends GameObject<GameCharacter> {

    public Account(String name) {
        super(name);
    }

    @Override
    public void createAndAddChild(String name) {
        getItems().add(new GameCharacter(name));
    }

}

同样的,整个层级结构都是这样定义的。我会定义一个Information类(即使它只有一个名称)来适应整个结构,因此:

package model;

public class Item extends GameObject<Information> {

    public Item(String name) {
        super(name);
    }

    @Override
    public void createAndAddChild(String name) {
        getItems().add(new Information(name));
    }

}

由于Information没有子元素,因此它的子元素列表为空:

package model;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class Information extends GameObject<GameObject<?>> {

    public Information(String name) {
        super(name);
    }

    @Override
    public ObservableList<GameObject<?>> getItems() {
        return FXCollections.emptyObservableList();
    }

    @Override
    public void createAndAddChild(String name) {
        throw new IllegalStateException("Information has no child items");
    }

}

现在您树中的每个项目都是一个GameObject<?>,因此您可以基本构建一个TreeView<GameObject<?>>。棘手的部分是,您的树项需要反映模型中已构建的结构。由于您有可观察列表,因此可以使用列表上的侦听器来实现这一点。
您可以在树上使用单元格工厂自定义显示TreeItem的单元格外观。如果您希望每种类型的项目具有不同的外观,则建议在外部CSS类中定义样式,并在对应于该项目类型的单元格上设置CSS PseudoClass。如果使用某些命名约定(我认为伪类名称是类名的小写版本),那么这可能非常流畅。以下是一个相当简单的示例:
package ui;

import static java.util.stream.Collectors.toList;

import java.util.Arrays;
import java.util.List;

import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item; 

public class Tree {

    private final TreeView<GameObject<?>> treeView ;

    private final List<Class<? extends GameObject<?>>> itemTypes = Arrays.asList(
         Account.class, GameCharacter.class, Item.class, Information.class
    );

    public Tree(ObservableList<Account> accounts) {
        treeView = new TreeView<>();

        GameObject<?> root = new GameObject<Account>("") {

            @Override
            public ObservableList<Account> getItems() {
                return accounts ;
            }

            @Override
            public void createAndAddChild(String name) {
                getItems().add(new Account(name));
            }

        };

        TreeItem<GameObject<?>> treeRoot = createItem(root);

        treeView.setRoot(treeRoot);
        treeView.setShowRoot(false);

        treeView.setCellFactory(tv -> {

            TreeCell<GameObject<?>> cell = new TreeCell<GameObject<?>>() {

                @Override
                protected void updateItem(GameObject<?> item, boolean empty) {
                    super.updateItem(item, empty);
                    textProperty().unbind();
                    if (empty) {
                        setText(null);
                        itemTypes.stream().map(Tree.this::asPseudoClass)
                            .forEach(pc -> pseudoClassStateChanged(pc, false));
                    } else {
                        textProperty().bind(item.nameProperty());
                        PseudoClass itemPC = asPseudoClass(item.getClass());
                        itemTypes.stream().map(Tree.this::asPseudoClass)
                            .forEach(pc -> pseudoClassStateChanged(pc, itemPC.equals(pc)));
                    }
                }
            };

            cell.hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
                if (isNowHovered && (! cell.isEmpty())) {
                    System.out.println("Mouse hover on "+cell.getItem().getName());
                }
            });

            return cell ;
        }
    }

    public TreeView<GameObject<?>> getTreeView() {
        return treeView ;
    }

    private TreeItem<GameObject<?>> createItem(GameObject<?> object) {

        // create tree item with children from game object's list:

        TreeItem<GameObject<?>> item = new TreeItem<>(object);
        item.setExpanded(true);
        item.getChildren().addAll(object.getItems().stream().map(this::createItem).collect(toList()));

        // update tree item's children list if game object's list changes:

        object.getItems().addListener((Change<? extends GameObject<?>> c) -> {
            while (c.next()) {
                if (c.wasAdded()) {
                    item.getChildren().addAll(c.getAddedSubList().stream().map(this::createItem).collect(toList()));
                }
                if (c.wasRemoved()) {
                    item.getChildren().removeIf(treeItem -> c.getRemoved().contains(treeItem.getValue()));
                }
            }
        });

        return item ;
    }

    private PseudoClass asPseudoClass(Class<?> clz) {
        return PseudoClass.getPseudoClass(clz.getSimpleName().toLowerCase());
    }

}

快速测试,这个测试可以工作,但请注意您可能需要测试更多的功能:

package application;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item;
import ui.Tree;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        Tree tree = new Tree(createAccounts());
        TreeView<GameObject<?>> treeView = tree.getTreeView();

        TextField addField = new TextField();
        Button addButton = new Button("Add");
        EventHandler<ActionEvent> addHandler = e -> {
            TreeItem<GameObject<?>> selected = treeView
                .getSelectionModel()
                .getSelectedItem();
            if (selected != null) {
                selected.getValue().createAndAddChild(addField.getText());
                addField.clear();
            }
        };
        addField.setOnAction(addHandler);
        addButton.setOnAction(addHandler);
        addButton.disableProperty().bind(Bindings.createBooleanBinding(() -> {
            TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
            return selected == null || selected.getValue() instanceof Information ;
        }, treeView.getSelectionModel().selectedItemProperty()));

        Button deleteButton = new Button("Delete");
        deleteButton.setOnAction(e -> {
            TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
            TreeItem<GameObject<?>> parent = selected.getParent() ;
            parent.getValue().getItems().remove(selected.getValue());
        });
        deleteButton.disableProperty().bind(treeView.getSelectionModel().selectedItemProperty().isNull());

        HBox controls = new HBox(5, addField, addButton, deleteButton);
        controls.setPadding(new Insets(5));
        controls.setAlignment(Pos.CENTER);

        BorderPane root = new BorderPane(treeView);
        root.setBottom(controls);

        Scene scene = new Scene(root, 600, 600);
        scene.getStylesheets().add(getClass().getResource("/ui/style/style.css").toExternalForm());
        primaryStage.setScene(scene);

        primaryStage.show();
    }   

    public static void main(String[] args) {
        launch(args);
    }

    private ObservableList<Account> createAccounts() {
        Account jake = new Account("Jake");
        Account mark = new Account("Mark");
        Account freshAcc = new Account("Fresh Account");
        Account marksAltAcc = new Account("Mark's alternative account");
        Account jeffrey = new Account("Jeffrey");

        GameCharacter jakesChar = new GameCharacter("Jakes character");
        Item amazingSword = new Item("Amazing Sword");
        Item brokenBow = new Item("Broken Bow");
        Item junkMetal = new Item("Junk Metal");

        GameCharacter myChar = new GameCharacter("Me");
        Item godlyAxe = new Item("Godly Axe");

        GameCharacter level = new GameCharacter("I'll level this I promise");

        GameCharacter jeff = new GameCharacter("Jeff");
        Item superGun = new Item("Super Gun");
        Item superGunScope = new Item("Super Gun Scope");

        jake.getItems().add(jakesChar);
        mark.getItems().add(myChar);
        marksAltAcc.getItems().add(level);
        jeffrey.getItems().add(jeff);

        jakesChar.getItems().addAll(amazingSword, brokenBow, junkMetal);
        myChar.getItems().add(godlyAxe);
        jeff.getItems().addAll(superGun, superGunScope);

        return FXCollections.observableArrayList(jake, mark, freshAcc, marksAltAcc, jeffrey);

    }

}

作为示例,附上CSS:

.tree-cell, .tree-cell:hover:empty {
    -fx-background-color: -fx-background ;
    -fx-background: -fx-control-inner-background ;
}

.tree-cell:hover {
    -fx-background-color: crimson, -fx-background ;
    -fx-background-insets: 0, 1;
}

.tree-cell:account {
    -fx-background: lightsalmon ;
}
.tree-cell:gamecharacter {
    -fx-background: bisque ;
}
.tree-cell:item {
    -fx-background: antiquewhite ;
}

.tree-cell:selected {
    -fx-background: crimson ;
}

我必须说,你在这里发帖所付出的努力真的值得称赞。感谢你提供如此详细的信息。有一件事让我有点紧张,那就是仅为了 TreeView 的目的而更改我的项目如何查看 AccountGameCharacterItem 类。做一个 TreeView<Object> myTree = new TreeView<>(); 然后在需要时将其转换为 AccountGameCharacterItem,这样做是否不好?使用只有一个字段的 Interface 也是一个有趣的想法。我只是有点害怕使用继承的想法,因为我已经深入到我的项目中去了。 - Hatefiend
1
你不愿使用继承是有道理的。测试类型和强制转换也有点像代码异味...不幸的是,TreeView 整个过程中只使用了一个类型,这实际上迫使你在这两个恶魔之间做出选择。看一下我在 github 上放置的稍微通用一些的版本:那里的 ModelTree 不会对继承层次结构做任何假设。你可以基本上将类型测试封装在传递到 ModelTree 中的函数中。 - James_D
2
嗯,你对“同质”的理解有点偏差。GridPane是同质的:每个子元素都具有相同的类型:Node。当然,你可以传入任何你喜欢的节点(里氏替换原则),但如果你想迭代getChildren()列表并针对已传入的节点类型执行特定操作,那么你基本上会陷入同样的境地。可能可以使用T getValue()ObservableList<C> getChildren()定义TreeItem<T,C>,但这将变得非常复杂,并且可能会阻止经验不足的程序员使用它。 - James_D
在您创建的 TreeCell 上定义 setOnHover(或者,如果可能的话,使用 CSS 进行悬停效果)。添加示例。 - James_D
嗯,我还是有点困惑TreeCell的事情。现在我正在尝试一些想法,首先是Object。我有@Override public TreeCell<Object> call(TreeView<Object> arg0)。现在我需要return一个TreeCell<Object>。这就是让我感到困惑的地方。我需要知道这个TreeCell里面有什么。如果用户在一个Item上面,那么我希望我的TreeCell有一个setOnMouseHover效果。我还需要知道它正在处理什么类型的Item。如果我有TreeItem,那么我可以使用.getValue()并且很好,但是我只有一个TreeView对象。 - Hatefiend
显示剩余14条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接