JavaFX 2.2:如何强制重新绘制/更新ListView?

9
我在JavaFX 2模态对话框窗口中有一个ListView控件。
这个ListView显示DXAlias实例,ListCell是由cell factory制造的。factory对象的主要作用是检查ListView的UserData属性数据,并将其与相应的ListCell项目进行比较。如果它们相同,则ListCell的内容将以红色呈现,否则为黑色。我这样做是为了指示ListView中哪个项目当前被选为“默认”。以下是我的ListCell factory类,以便您了解我的意思:
private class AliasListCellFactory implements 
    Callback<ListView<DXSynonym>, ListCell<DXSynonym>> {

@Override
public ListCell<DXSynonym> call(ListView<DXSynonym> p) {
    return new ListCell<DXSynonym>() {

    @Override
    protected void updateItem(DXSynonym item, boolean empty) {
        super.updateItem(item, empty);

        if (item != null) {
            DXSynonym dx = (DXSynonym) lsvAlias.getUserData();

            if (dx != null && dx == item) {
                this.setStyle("-fx-text-fill: crimson;");                    
            } else { this.setStyle("-fx-text-fill: black;"); }

            this.setText(item.getDxName());

        } else { this.setText(Census.FORMAT_TEXT_NULL); }
    }};
}

我有一个按钮处理程序叫做“handleAliasDefault()”,它通过将选定的DXAlias实例存储到ListView中来使选定项成为新的默认选项:lsvAlias.setUserData(selected DXAlias)。以下是处理程序代码:

// Handler for Button[fx:id="btnAliasDefault"] onAction
    @FXML
    void handleAliasDefault(ActionEvent event) {

        int sel = lsvAlias.getSelectionModel().getSelectedIndex();
        if (sel >= 0 && sel < lsvAlias.getItems().size()) {
            lsvAlias.setUserData(lsvAlias.getItems().get(sel));
        }
    }

因为点击“设置默认值”按钮所做的更改是更改ListView的UserData(),而没有更改支持的ObservableList,所以列表不会正确地显示新的默认值。

有没有办法强制ListView重新渲染其ListCells?虽然Android用户对此提出了无数问题,但似乎对于JavaFX来说并不是那么容易。我可能必须对后台数组进行“无意义的更改”以强制进行重绘。

我看到这个问题在JavaFX 2.1中被问到:Javafx ListView refreshing

5个回答

16

对于其他进入此页面的人:

正确的方法是使用“提取器”回调来提供您的可观察列表。这将向列表(和 ListView)发出任何属性更改的信号。

自定义显示类:

public class Custom {
    StringProperty name = new SimpleStringProperty();
    IntegerProperty id = new SimpleIntegerProperty();

    public static Callback<Custom, Observable[]> extractor() {
        return new Callback<Custom, Observable[]>() {
            @Override
            public Observable[] call(Custom param) {
                return new Observable[]{param.id, param.name};
            }
        };
    }

    @Override
    public String toString() {
        return String.format("%s: %s", name.get(), id.get());
    }
}

并且你的主代码体:

ListView<Custom> myListView;
//...init the ListView appropriately
ObservableList<Custom> items = FXCollections.observableArrayList(Custom.extractor());
myListView.setItems(items);
Custom item = new Custom();
items.add(item);
item.name.set("Mickey Mouse");
// ^ Should update your ListView!!!

见(以及类似的方法):https://docs.oracle.com/javafx/2/api/javafx/collections/FXCollections.html#observableArrayList(javafx.util.Callback)


你能解释一下在你的例子中SimpleIntegerProperty的用法吗? - serup
1
@serup 我不太确定你的问题是什么,但让我试着解释一下。在我的例子中,我从未显式设置id的值,因此它将默认为0。这将导致ListView行显示“Mickey Mouse: 0”。如果您想设置ID值,可以使用item.name.set(6),例如,您将看到ListValue正确更新。根据您的用例,在Custom类上使用非默认构造函数可能会更好。 - Andrey
@Andrey,TreeView有类似的解决方案吗? - Hosseinmp76
这个解决方案在大多数情况下应该优于@Eldelshell的解决方案。因为包含项目的“ObservableList”可以在多个“ListView”实例中使用。跟踪这些实例并在所有实例上调用“refresh”是不方便的。此外,ListView的项目可以被另一个“ObservableList”替换,因此当项目已更改但项目不再在ListView中时进行刷新有点浪费。 - Leo Mekenkamp

13

我不确定这个方法是否适用于JavaFX 2.2版本,但在JavaFX 8版本中是可行的,我花了一些时间才弄明白。你需要创建自己的 ListViewSkin 并添加一个名为refresh 的方法,代码如下:

public void refresh() {
    super.flow.recreateCells(); 
}

这将调用updateItem而无需替换整个Observable集合。

此外,要使用新的Skin,您需要实例化它并在控制器的initialize方法中设置它(如果您正在使用FXML):

MySkin<Subscription> skin = new MySkin<>(this.listView); // Injected by FXML
this.listView.setSkin(skin);
...
((MySkin) listView.getSkin()).refresh(); // This is how you use it    

在调试手风琴的行为后,我发现了这个。每次展开手风琴时,这些控件都会刷新它所包含的列表视图。


1
以下是我在一个开源项目中使用的方法示例:https://github.com/Eldelshell/JobHunter/blob/master/jobhunter/src/main/java/jobhunter/gui/UpdateableListViewSkin.java - Eldelshell

9

我不确定是否遗漏了什么,但一开始我尝试了scottb的解决方案,后来发现已经有一个实现了refresh()方法的方法,这对我很有效。

ListView<T> list;
list.refresh();

1
不确定为什么之前没有指出这一点,也许是在较新版本的JavaFX中,但它确实有效。 - Alessandro Roaro
@AlessandroRoaro 自JavaFX 8u60以来就可用。 - Salvioner
这确实可以解决问题,但你必须在适当的时候调用刷新。我觉得安德烈的提取器解决方案完美无缺。谢谢大家! - seugnimod

5

目前,我可以通过在按钮处理程序中调用以下方法forceListRefreshOn()来使ListView重新绘制并正确指示默认选择:

@FXML
void handleAliasDefault(ActionEvent event) {

    int sel = lsvAlias.getSelectionModel().getSelectedIndex();
    if (sel >= 0 && sel < lsvAlias.getItems().size()) {
        lsvAlias.setUserData(lsvAlias.getItems().get(sel));
        this.<DXSynonym>forceListRefreshOn(lsvAlias);
    }
}

这个帮助方法只是从ListView中替换ObservableList,然后再将其换回来,可能强制ListView更新其ListCells:

private <T> void forceListRefreshOn(ListView<T> lsv) {
    ObservableList<T> items = lsv.<T>getItems();
    lsv.<T>setItems(null);
    lsv.<T>setItems(items);
}

0

看起来,你不需要使用updateItem方法。你需要做的是将当前默认单元格的指示器(现在在用户数据中)存储到ObjectProperty中。并且从每个单元格添加一个监听器到这个属性上。当值改变时,重新分配样式。

但我认为,这种绑定会引发另一个问题 - 在滚动时,新的单元格将被创建,但旧的单元格不会解除绑定,这可能导致内存泄漏。所以你需要在从场景中移除单元格时添加监听器。我认为可以通过在parentProperty上添加监听器来实现,当它变为null时,解除绑定。

界面不应强制进行更新。当现有节点的属性/渲染发生改变时,它会自动更新。所以你只需要更新现有节点(单元格)的外观/属性。而且不要忘记,在滚动/重新渲染等过程中,可能会大量创建单元格。


我没有展示代码,但是ListView的数据在ObservableList中。每个数据模型项都是JavaFX属性。ListView的协议规定当数据发生变化时,视图会自动更新。它似乎没有说明当数据本身不变时,数据显示方式发生变化时应该发生什么。添加新的绑定可能提供了一个解决方案...但代价是很多非常丑陋和脆弱的代码。我只需要在默认选择更改时重新绘制ListCells。 - scottb
这是JavaFX 2的常见属性:当场景图(特定单元格)的节点更改其属性以影响视觉表示时,场景图会重新绘制那些节点(单元格),如果需要的话。因此,当您更新单元格的视觉外观时,它们将由场景图重新绘制。 - Alexander Kirov
好的,那么你可以向javafx-jira提交一个RFE =) - Alexander Kirov
我尝试了lsvAlias.requestLayout() ... 但ListView在AnchorPane中。我会尝试在那上面使用requestLayout(),谢谢你的提示。 - scottb
lsvAlias.Parent.requestLayout() 没有效果... 但是一个名为 forceListRefreshOn(lsvAlias) 的辅助方法起作用了。 - scottb
显示剩余3条评论

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