javafx - 如何创建复杂对象的 ListView 并允许编辑对象上的字段?

标签 javafx

我想要一个 Person 对象的 JavaFX ListView。我希望列表仅显示名称并允许编辑名称。在提交对名称的编辑后,它还应该保留每个对象中的其他字段。您将如何在 JavaFX 中惯用地做到这一点?

我有以下代码,它有效,但它有点不稳定,因为它有一个 StringConverter 可以将一种方式从 Person 转换为人名的字符串,然后不进行反向转换,而是依赖于列表单元格 commitEdit 方法取一串名字并将其设置在适当的人身上。

这是代码:

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.StringConverter;

public class Main extends Application {

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

    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("My Custom List View");
        ObservableList<Person> people = FXCollections.observableArrayList(
            new Person("John Doe", "123 New York"),
            new Person("Jane Doe", "456 San Francisco")
        );
        ListView<Person> listView = new ListView();
        listView.setCellFactory(new CustomCellFactory());
        listView.setEditable(true);
        listView.setItems(people);
        Scene scene = new Scene(listView,400,300);
        stage.setScene(scene);
        stage.show();
    }

    public static class CustomCellFactory implements Callback<ListView<Person>,ListCell<Person>> {
        @Override
        public ListCell<Person> call(ListView param) {
            TextFieldListCell<Person> cell = new TextFieldListCell() {
                @Override
                public void updateItem(Object item, boolean empty) {
                    super.updateItem(item, empty);
                    if (!empty && item != null) {
                        System.out.println("updating item: "+item.toString());
                        setText(((Person) item).getName());
                    } else {
                        setText(null);
                    }
                }
                @Override
                public void commitEdit(Object newName) {
                    ((Person)getItem()).setName((String)newName);
                    super.commitEdit(getItem());
                }
            };
            cell.setConverter(new StringConverter() {
                @Override
                public String toString(Object person) {
                    return ((Person)person).getName();
                }
                @Override
                public Object fromString(String string) {
                    return string;
                }
            });
            return cell;
        }
    }

    public static class Person {
        private String name;
        private String address;
        public Person(String name, String address) {
            this.name = name;
            this.address = address;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getAddress() {
            return address;
        }
        public void setAddress(String address) {
            this.address = address;
        }
        public String toString() {
            return name+" at "+address;
        }
    }
}

最佳答案

TextFieldListCell 只是 ListCell 的一种便利实现,它为列表单元格提供了最常见的编辑形式(即,如果列表中的项目是 String s,或者可以轻松转换为字符串的对象)。您经常会发现您需要更具体的编辑(例如,您通常希望使用 TextFormatter 过滤编辑文本字段中允许的文本),在这种情况下,您只需自己实现 ListCell。我认为这是一种情况,总的来说,从头开始实现 ListCell 更有意义。

似乎您可以强制 TextFieldListCell 用于此用例,使用:

    listView.setCellFactory(lv -> {
        TextFieldListCell<Person> cell = new TextFieldListCell<Person>();
        cell.setConverter(new StringConverter<Person>() {
            @Override
            public String toString(Person person) {
                return person.getName();
            }
            @Override
            public Person fromString(String string) {
                Person person = cell.getItem();
                person.setName(string);
                return person ;
            }
        });
        return cell;
    });

(请注意,在您的代码中,您的 updateItem() 方法等效于 TextFieldListCell 中已经实现的方法,因此它是多余的,并且 commitEdit(...) 中的额外功能现在位于(类型安全)StringConverter 中,因此不再需要子类。)

这只是感觉有点脆弱,因为它依赖于从文本字段提交新值的特定实现及其与字符串转换器的交互,但它在测试中似乎工作正常。

但是,我对此的偏好是直接自己实现 ListCell,因为它使您可以完全控制文本字段和编辑过程之间的交互。这非常简单:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        ListView<Person> listView = new ListView<>();
        ObservableList<Person> people = FXCollections.observableArrayList(
            new Person("John Doe", "123 New York"),
            new Person("Jane Doe", "456 San Francisco")
        );
        listView.setEditable(true);
        listView.setItems(people);

        listView.setCellFactory(lv -> new ListCell<Person>() {
            private TextField textField = new TextField() ;

            {
                textField.setOnAction(e -> {
                    commitEdit(getItem());
                });
                textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
                    if (e.getCode() == KeyCode.ESCAPE) {
                        cancelEdit();
                    }
                });
            }

            @Override
            protected void updateItem(Person person, boolean empty) {
                super.updateItem(person, empty);
                if (empty) {
                    setText(null);
                    setGraphic(null);
                } else if (isEditing()) {
                    textField.setText(person.getName());
                    setText(null);
                    setGraphic(textField);
                } else {
                    setText(person.getName());
                    setGraphic(null);
                }
            }

            @Override
            public void startEdit() {
                super.startEdit();
                textField.setText(getItem().getName());
                setText(null);
                setGraphic(textField);
                textField.selectAll();
                textField.requestFocus();
            }

            @Override
            public void cancelEdit() {
                super.cancelEdit();
                setText(getItem().getName());
                setGraphic(null);
            }

            @Override
            public void commitEdit(Person person) {
                super.commitEdit(person);
                person.setName(textField.getText());
                setText(textField.getText());
                setGraphic(null);
            }
        });

        // for debugging:
        listView.setOnMouseClicked(e -> {
            if (e.getClickCount() == 2) {
                listView.getItems().forEach(p -> System.out.println(p.getName()));
            }
        });

        Scene scene = new Scene(listView,400,300);
        primaryStage.setScene(scene);
        primaryStage.show();

    }

    public static class Person {
        private String name;
        private String address;
        public Person(String name, String address) {
            this.name = name;
            this.address = address;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getAddress() {
            return address;
        }
        public void setAddress(String address) {
            this.address = address;
        }
        public String toString() {
            return name+" at "+address;
        }
    }

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

如果您可能经常需要这种功能,您可以轻松创建一个可重用的类:
import java.util.function.BiFunction;
import java.util.function.Function;

import javafx.scene.control.ListCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

public class EditingListCell<T> extends ListCell<T> {
    private final TextField textField ;
    private final Function<T, String> propertyAccessor ;

    public EditingListCell(Function<T, String> propertyAccessor, BiFunction<String, T, T> updater) {
        this.propertyAccessor = propertyAccessor ;
        this.textField = new TextField();

        textField.setOnAction(e -> {
            T newItem = updater.apply(textField.getText(), getItem());
            commitEdit(newItem);
        });
        textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
            if (e.getCode() == KeyCode.ESCAPE) {
                cancelEdit();
            }
        });
    }

    @Override
    protected void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);
        if (empty || item == null) {
            setText(null);
            setGraphic(null);
        } else if (isEditing()) {
            textField.setText(propertyAccessor.apply(item));
            setText(null);
            setGraphic(textField);
        } else {
            setText(propertyAccessor.apply(item));
            setGraphic(null);
        }
    }

    @Override
    public void startEdit() {
        super.startEdit();
        textField.setText(propertyAccessor.apply(getItem()));
        setText(null);
        setGraphic(textField);       
        textField.selectAll();
        textField.requestFocus();
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setText(propertyAccessor.apply(getItem()));
        setGraphic(null);
    }

    @Override
    public void commitEdit(T item) {
        super.commitEdit(item);
        getListView().getItems().set(getIndex(), item);
        setText(propertyAccessor.apply(getItem()));
        setGraphic(null);        
    }
}

然后你只需要
listView.setCellFactory(lv -> new EditingListCell<>(
        Person::getName,
        (text, person) -> {
            person.setName(text);
            return person ;
        })
);

关于javafx - 如何创建复杂对象的 ListView 并允许编辑对象上的字段?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35963888/

相关文章:

按下删除键后,Javafx TextField 插入符号的位置不正确

JavaFX Raspberry Pi UI 未显示

java - 舞台任务栏中的低质量图标。 JavaFX

JavaFX - 如何将事件句柄分配给标签数组?

java - 无法在 AnchorPane 内调整 AnchorPane 的大小

JavaFX:如何最好地将标签放置在形状的中心?

java - Javafx在特定条件下每10毫秒触发一个事件

mysql - 从 TableView CheckBox JavaFX 将数据插入数据库

kotlin - Unresolved reference :Github上针对Kotlin&Gradle的javafx

java - 运行 JavaFX Gradle 应用程序时出现 NoClassDefFoundError StringUtils 或 Gson