gpt4 book ai didi

java - 等待加载完成时无法更新树单元格/项目图形

转载 作者:行者123 更新时间:2023-11-30 06:15:52 30 4
gpt4 key购买 nike

最初的情况是这样的:

initial situation

现在,这棵树可能很大(例如,我有一棵有 2200 万个节点的树)。这里发生的是我使用 jooq/h2 后端来存储所有节点并执行查询以查找子节点。

这意味着,在上图中,节点被标记为可扩展,但其子节点尚未填充。它是按需完成的。扩展后我得到这个:

after expansion

我的问题是,当然,扩展可能需要时间。我想做的是为TreeItem的图形添加视觉线索以表明它正在加载...

而我做不到。

好的,首先,架构的总体 View :

  • 简而言之,它是“具有被动观点的 MVP”;
  • JavaFX 称之为“ Controller ”的东西,在我的代码中由 *Display 实现classes 是被动 View ,其唯一作用是捕获 UI 事件并将它们转发到 *Presenter ;这些类是“特定于 GUI 实现的”;
  • *Presenter通过订购 *View 对这些事件使用react类更新 *Display ;
  • 当任务需要一定的时间才能完成,GUI 无法保持交互时,BackgroundTaskRunner*Presenter 使用到:
  • 指示*View修改 UI 以确认任务(在 GUI 线程上);
  • 执行任务(在后台线程上);
  • 指示*View在任务完成时修改 UI(在 GUI 线程上);
  • 如果任务失败,指示*View相应地修改 UI(在 GUI 线程上)。

  • 使用 JavaFX:
  • 用户界面是 *Display类(class);它由 FXML 文件定义并从中加载;
  • *View 的(实现)类对 *Display 中定义的所有 GUI 元素具有可见性。类(class)。
  • *View类实际上是一个接口(interface);这使我能够制作该程序的 webapp 版本(计划中)。

    现在,这段代码的上下文......
    *Presenter , *View*Display所有这些都与上图中可见的“解析树”选项卡有关。

    鉴于上述架构,问题在于 *View 的实现。类,并使用 *Display类(class)。
    *Display类有 init()如果需要,初始化所有相关 JavaFX 组件的方法。在这种情况下, TreeView , 称为 parseTree , 被初始化为:
    @Override
    public void init()
    {
    parseTree.setCellFactory(param -> new ParseTreeNodeCell(this));
    }
    ParseTreeNodeCell被定义为:
    public final class ParseTreeNodeCell
    extends TreeCell<ParseTreeNode>
    {
    // FAILED attempt at showing a progress indicator...
    private final ProgressIndicator indicator = new ProgressIndicator();
    private final Text text = new Text();
    private final HBox hBox = new HBox(text, indicator);

    public ParseTreeNodeCell(final TreeTabDisplay display)
    {
    // FIXME: not sure about the following line...
    indicator.setMaxHeight(heightProperty().doubleValue());
    // ... But this I want: by default, not visible
    indicator.setVisible(false);

    // The whole tree is readonly
    setEditable(false);

    // Some non relevant code snipped away
    }

    public void showIndicator()
    {
    indicator.setVisible(true);
    }

    public void hideIndicator()
    {
    indicator.setVisible(false);
    }

    // What to do when a TreeItem is actually attached...
    @Override
    protected void updateItem(final ParseTreeNode item, final boolean empty)
    {
    super.updateItem(item, empty);
    if (empty) {
    setGraphic(null);
    return;
    }
    final String msg = String.format("%s (%s)",
    item.getRuleInfo().getName(),
    item.isSuccess() ? "SUCCESS" : "FAILURE");
    text.setText(msg);
    setGraphic(hBox);
    // HACK. PUKE. UGLY.
    ((ParseTreeItem) getTreeItem()).setCell(this);
    }
    }
    ParseTreeItem这是:
    public final class ParseTreeItem
    extends TreeItem<ParseTreeNode>
    {
    private final boolean leaf;

    private ParseTreeNodeCell cell;

    public ParseTreeItem(final TreeTabDisplay display,
    final ParseTreeNode value)
    {
    super(value);
    leaf = !value.hasChildren();

    // If the item is expanded, we load children.
    // If it is collapsed, we unload them.
    expandedProperty().addListener(new ChangeListener<Boolean>()
    {
    @Override
    public void changed(
    final ObservableValue<? extends Boolean> observable,
    final Boolean oldValue, final Boolean newValue)
    {
    if (oldValue == newValue)
    return;
    if (!newValue) {
    getChildren().clear();
    return;
    }
    display.needChildren(ParseTreeItem.this);
    }
    });
    }

    @Override
    public boolean isLeaf()
    {
    return leaf;
    }

    public void setCell(final ParseTreeNodeCell cell)
    {
    this.cell = cell;
    }

    public void showIndicator()
    {
    cell.showIndicator();
    }

    public void hideIndicator()
    {
    cell.hideIndicator();
    }
    }

    现在,总是在 *Display类, needChildren()方法定义如下:
    ParseTreeItem currentItem;

    // ...

    public void needChildren(final ParseTreeItem parseTreeItem)
    {
    // Keep a reference to the current item so that the *View can act on it
    currentItem = parseTreeItem;
    presenter.needChildren(currentItem.getValue());
    }

    演示者这样做:
    public void needChildren(final ParseTreeNode value)
    {
    taskRunner.computeOrFail(
    view::waitForChildren, () -> {
    // FOR TESTING
    TimeUnit.SECONDS.sleep(1L);
    return getNodeChildren(value.getId());
    },
    view::setTreeChildren,
    throwable -> mainView.showError("Tree expand error",
    "Unable to extend parse tree node", throwable)
    );
    }

    (见 here ;对于 taskRunner )
    view中的对应方法上面的成员(JavaFX 实现)这样做:
    @Override
    public void waitForChildren()
    {
    // Supposedly shows the indicator in the TreeItemGraphic...
    // Except that it does not.
    display.currentItem.showIndicator();
    }

    @Override
    public void setTreeChildren(final List<ParseTreeNode> children)
    {
    final List<ParseTreeItem> items = children.stream()
    .map(node -> new ParseTreeItem(display, node))
    .collect(Collectors.toList());
    // This works fine
    display.currentItem.getChildren().setAll(items);
    // But this does not...
    display.currentItem.hideIndicator();
    }

    尽管我在 TreeItem 上定义了方法显示进度指示器,它根本不显示:/

    事实上,我的问题是双重的,都与 ParseTreeItem有关。 :
  • ParseTreeNodeCell ,我需要转换为 ParseTreeItem设置单元格;
  • 即使我这样做了,它根本不起作用,我根本看不到指标出现。

  • 不仅如此,出于某种原因,我还需要检查(在 ParseTreeNodeCell 中)我是否真的有一个值,否则我会得到一个 NPE。而且我找不到从树项中获取匹配单元格的方法...

    所以,总而言之,我做的很多事情都很糟糕,没有一件是正确的。

    我如何设法获得 TreeItem 的图形在那种情况下改变,只要加载仍在进行中?

    编辑

    在@James_D 编写的代码的启发下找到了解决方案;请参阅我自己的答案,了解我是如何做到的。

    最佳答案

    首先,我承认我没有仔细阅读您的所有代码。

    我认为这里的方法是使用 TreeItem子类,它公开了一个描述子级“加载状态”的可观察属性。然后让树单元观察当前树项的加载状态,并相应地显示一个进度条。

    这是一个SSCCE:

    ( 更新 : 显然,如果我只观察 treeItem 而不是 item ,树无法从空单元格中删除披露图形...通过使用 itemProperty 管理文本来修复.)

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;

    import javafx.application.Application;
    import javafx.beans.property.ObjectProperty;
    import javafx.beans.property.SimpleObjectProperty;
    import javafx.beans.value.ChangeListener;
    import javafx.collections.ObservableList;
    import javafx.concurrent.Task;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.ProgressBar;
    import javafx.scene.control.TreeCell;
    import javafx.scene.control.TreeItem;
    import javafx.scene.control.TreeView;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;



    public class LazyTreeCellLoadingExample extends Application {

    // Executor for background tasks:
    private static final ExecutorService exec = Executors.newCachedThreadPool(r -> {
    Thread t = new Thread(r);
    t.setDaemon(true);
    return t ;
    });

    @Override
    public void start(Stage primaryStage) {
    TreeView<Long> tree = new TreeView<>();
    tree.setRoot(new LazyTreeItem(1L));

    // cell factory that displays progress bar when item is loading children:
    tree.setCellFactory(tv -> {

    // the cell:
    TreeCell<Long> cell = new TreeCell<>();

    // progress bar to display when needed:
    ProgressBar progressBar = new ProgressBar();

    // listener to observe *current* tree item's child loading status:
    ChangeListener<LazyTreeItem.ChildrenLoadedStatus> listener = (obs, oldStatus, newStatus) -> {
    if (newStatus == LazyTreeItem.ChildrenLoadedStatus.LOADING) {
    cell.setGraphic(progressBar);
    } else {
    cell.setGraphic(null);
    }
    };

    // listener for tree item property
    // ensures that listener above is attached to current tree item:
    cell.treeItemProperty().addListener((obs, oldItem, newItem) -> {

    // if we were displaying an item, (and no longer are...),
    // stop observing its child loading status:
    if (oldItem != null) {
    ((LazyTreeItem) oldItem).childrenLoadedStatusProperty().removeListener(listener);
    }

    // if there is a new item the cell is displaying:
    if (newItem != null) {

    // update graphic to display progress bar if needed:
    LazyTreeItem lazyTreeItem = (LazyTreeItem) newItem;
    if (lazyTreeItem.getChildrenLoadedStatus() == LazyTreeItem.ChildrenLoadedStatus.LOADING) {
    cell.setGraphic(progressBar);
    } else {
    cell.setGraphic(null);
    }

    // observe loaded status of current item in case it changes
    // while we are still displaying this item:
    lazyTreeItem.childrenLoadedStatusProperty().addListener(listener);
    }
    });

    // change text if item changes:
    cell.itemProperty().addListener((obs, oldItem, newItem) -> {
    if (newItem == null) {
    cell.setText(null);
    cell.setGraphic(null);
    } else {
    cell.setText(newItem.toString());
    }
    });

    return cell ;
    });

    Button debugButton = new Button("Debug");
    debugButton.setOnAction(evt -> {
    dumpData(tree.getRoot(), 0);
    });

    primaryStage.setScene(new Scene(new BorderPane(tree, null, null, debugButton, null), 400, 250));
    primaryStage.show();
    }

    private void dumpData(TreeItem<Long> node, int depth) {
    for (int i=0; i<depth; i++) System.out.print(" ");
    System.out.println(node.getValue());
    for (TreeItem<Long> child : node.getChildren()) dumpData(child, depth+1);
    }

    // TreeItem subclass that lazily loads children in background
    // Exposes an observable property specifying current load status of children
    public static class LazyTreeItem extends TreeItem<Long> {

    // possible load statuses:
    enum ChildrenLoadedStatus { NOT_LOADED, LOADING, LOADED }

    // observable property for current load status:
    private final ObjectProperty<ChildrenLoadedStatus> childrenLoadedStatus = new SimpleObjectProperty<>(ChildrenLoadedStatus.NOT_LOADED);

    public LazyTreeItem(Long value) {
    super(value);
    }

    // getChildren() method loads children lazily:
    @Override
    public ObservableList<TreeItem<Long>> getChildren() {
    if (getChildrenLoadedStatus() == ChildrenLoadedStatus.NOT_LOADED) {
    lazilyLoadChildren();
    }
    return super.getChildren() ;
    }

    // load child nodes in background, updating status accordingly:
    private void lazilyLoadChildren() {

    // change current status to "loading":
    setChildrenLoadedStatus(ChildrenLoadedStatus.LOADING);
    long value = getValue();

    // background task to load children:
    Task<List<LazyTreeItem>> loadTask = new Task<List<LazyTreeItem>>() {

    @Override
    protected List<LazyTreeItem> call() throws Exception {
    List<LazyTreeItem> children = new ArrayList<>();
    for (int i=0; i<10; i++) {
    children.add(new LazyTreeItem(10*value + i));
    }

    // for testing (loading is so lazy it falls asleep)
    Thread.sleep(3000);
    return children;
    }

    };

    // when loading is complete:
    // 1. set actual child nodes to loaded nodes
    // 2. update status to "loaded"
    loadTask.setOnSucceeded(event -> {
    super.getChildren().setAll(loadTask.getValue());
    setChildrenLoadedStatus(ChildrenLoadedStatus.LOADED);
    });

    // execute task in backgroun
    exec.submit(loadTask);
    }

    // is leaf is true only if we *know* there are no children
    // i.e. we've done the loading and still found nothing
    @Override
    public boolean isLeaf() {
    return getChildrenLoadedStatus() == ChildrenLoadedStatus.LOADED && super.getChildren().size()==0 ;
    }

    // normal property accessor methods:

    public final ObjectProperty<ChildrenLoadedStatus> childrenLoadedStatusProperty() {
    return this.childrenLoadedStatus;
    }

    public final LazyTreeCellLoadingExample.LazyTreeItem.ChildrenLoadedStatus getChildrenLoadedStatus() {
    return this.childrenLoadedStatusProperty().get();
    }

    public final void setChildrenLoadedStatus(
    final LazyTreeCellLoadingExample.LazyTreeItem.ChildrenLoadedStatus childrenLoadedStatus) {
    this.childrenLoadedStatusProperty().set(childrenLoadedStatus);
    }


    }

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

    更新

    经过一番讨论,我想出了第二个解决方案。这在管理进度条的方式上与之前的解决方案基本相似:有一个 TreeItem暴露 BooleanProperty 的子类当且仅当项目当前正在加载其子项时,这是正确的。 TreeCellTreeItem 上观察此属性它当前正在显示 - 注意用 treeItemProperty 注册一个监听器以便 loadingProperty 的监听器始终与当前项目注册。

    不同之处在于子项的加载方式,以及 - 在此解决方案的情况下 - 卸载方式。在之前的解决方案中,子节点在第一次请求时被加载,然后被保留。在此方案中,子节点在节点展开时加载,然后在节点折叠时移除。这是通过 expandedProperty 上的简单监听器处理的。 .

    从用户的角度来看,第一个解决方案的行为稍微像预期的那样,因为如果您折叠作为子树头部的节点,然后再次展开它,则保留子树的展开状态。在第二种解决方案中,折叠节点具有折叠所有后代节点的效果(因为它们实际上已被删除)。

    第二种解决方案对内存使用更稳健。除了一些极端用例之外,这实际上不太可能成为问题。 TreeItem对象是纯粹的模型——即它们只存储数据,没有 UI。因此,它们每个使用的内存可能不超过几百字节。为了消耗大量内存,用户必须浏览数十万个节点,这可能需要几天时间。 (也就是说,我将这个输入谷歌浏览器,我想我已经运行了一个多月,每天至少有 8-10 小时的活跃使用,所以这样的用例当然不是不可能的。)

    这是第二个解决方案。注意事项:我没有做出任何努力来处理节点的快速展开和折叠(在数据仍在加载时折叠)。 TreeItem子类应该真正跟踪任何当前的 Task (或使用 Service )并调用 cancel()如果任务正在运行并且用户折叠节点。我不想为了演示基本思想而过度混淆逻辑。
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;

    import javafx.application.Application;
    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.concurrent.Task;
    import javafx.scene.Scene;
    import javafx.scene.control.ProgressBar;
    import javafx.scene.control.TreeCell;
    import javafx.scene.control.TreeItem;
    import javafx.scene.control.TreeView;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;

    public class LazyTreeCellLoadingExample2 extends Application {

    private static final ExecutorService EXEC = Executors.newCachedThreadPool((Runnable r) -> {
    Thread t = new Thread(r);
    t.setDaemon(true);
    return t ;
    });

    @Override
    public void start(Stage primaryStage) {
    TreeView<Integer> tree = new TreeView<>();
    tree.setRoot(new LazyTreeItem(1));

    tree.setCellFactory(tv -> createTreeCell()) ;

    primaryStage.setScene(new Scene(new BorderPane(tree), 450, 600));
    primaryStage.show();
    }

    private TreeCell<Integer> createTreeCell() {

    ProgressBar progressBar = new ProgressBar();
    TreeCell<Integer> cell = new TreeCell<>();

    ChangeListener<Boolean> loadingChangeListener =
    (ObservableValue<? extends Boolean> obs, Boolean wasLoading, Boolean isNowLoading) -> {
    if (isNowLoading) {
    cell.setGraphic(progressBar);
    } else {
    cell.setGraphic(null);
    }
    };

    cell.treeItemProperty().addListener(
    (ObservableValue<? extends TreeItem<Integer>> obs,
    TreeItem<Integer> oldItem,
    TreeItem<Integer> newItem) -> {

    if (oldItem != null) {
    LazyTreeItem oldLazyTreeItem = (LazyTreeItem) oldItem ;
    oldLazyTreeItem.loadingProperty().removeListener(loadingChangeListener);
    }

    if (newItem != null) {
    LazyTreeItem newLazyTreeItem = (LazyTreeItem) newItem ;
    newLazyTreeItem.loadingProperty().addListener(loadingChangeListener);

    if (newLazyTreeItem.isLoading()) {
    cell.setGraphic(progressBar);
    } else {
    cell.setGraphic(null);
    }
    }
    });

    cell.itemProperty().addListener(
    (ObservableValue<? extends Integer> obs, Integer oldItem, Integer newItem) -> {
    if (newItem == null) {
    cell.setText(null);
    cell.setGraphic(null);
    } else {
    cell.setText(newItem.toString());
    }
    });

    return cell ;
    }

    public static class LazyTreeItem extends TreeItem<Integer> {

    private final BooleanProperty loading = new SimpleBooleanProperty(false);

    private boolean leaf = false ;

    public final BooleanProperty loadingProperty() {
    return this.loading;
    }

    public final boolean isLoading() {
    return this.loadingProperty().get();
    }

    public final void setLoading(final boolean loading) {
    this.loadingProperty().set(loading);
    }


    public LazyTreeItem(Integer value) {
    super(value);

    expandedProperty().addListener((ObservableValue<? extends Boolean>obs, Boolean wasExpanded, Boolean isNowExpanded) -> {
    if (isNowExpanded) {
    loadChildrenLazily();
    } else {
    clearChildren();
    }
    });
    }

    @Override
    public boolean isLeaf() {
    return leaf ;
    }

    private void loadChildrenLazily() {

    setLoading(true);

    int value = getValue();
    Task<List<TreeItem<Integer>>> loadTask = new Task<List<TreeItem<Integer>>>() {

    @Override
    protected List<TreeItem<Integer>> call() throws Exception {
    List<TreeItem<Integer>> children = new ArrayList<>();
    for (int i=0; i<10; i++) {
    children.add(new LazyTreeItem(value * 10 + i));
    }
    Thread.sleep(3000);
    return children ;
    }

    };

    loadTask.setOnSucceeded(event -> {
    List<TreeItem<Integer>> children = loadTask.getValue();
    leaf = children.size() == 0 ;
    getChildren().setAll(children);
    setLoading(false);
    });

    EXEC.submit(loadTask);
    }

    private void clearChildren() {
    getChildren().clear();
    }
    }

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

    单元格中的“双重监听器”是因为我们确实需要观察“属性的属性”。具体来说,该单元格具有 treeItem属性,并且树项具有 loadingProperty .我们真正感兴趣的是属于当前树项的加载属性。当然,这可以通过两种方式进行更改:单元格中的树项更改,或者树项中的加载属性更改。 EasyBind框架包括专门用于观察“属性的属性”的 API。如果你使用 EasyBind,你可以替换(大约 30 行)代码
    ChangeListener<Boolean> loadingChangeListener = ... ;

    cell.treeItemProperty().addListener(...);


        ObservableValue<Boolean> loading = EasyBind.select(cell.treeItemProperty())
    // ugly cast still here:
    .selectObject(treeItem -> ((LazyTreeItem)treeItem).loadingProperty());

    loading.addListener((obs, wasLoading, isNowLoading) -> {
    if (isNowLoading != null && isNowLoading.booleanValue()) {
    cell.setGraphic(progressBar);
    } else {
    cell.setGraphic(null);
    }
    });

    关于java - 等待加载完成时无法更新树单元格/项目图形,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28304745/

    30 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com