¿Cómo puedo completar un ListView en JavaFX usando objetos personalizados?

CorePress2024-01-25  8

Soy un poco nuevo en Java, JavaFX y la programación en general, y tengo un problema que me está rompiendo el cerebro.

En la mayoría de los tutoriales que he buscado sobre cómo completar un ListView (usando un ObservableArrayList, más específicamente), la forma más sencilla de hacerlo es hacerlo a partir de un ObservableList de cadenas, así:

ObservableList<String> wordsList = FXCollections.observableArrayList("First word","Second word", "Third word", "Etc."); 
ListView<String> listViewOfStrings = new ListView<>(wordsList);

Pero no quiero usar cadenas. Me gustaría utilizar un objeto personalizado que hice llamado Words:

ObservableList<Word> wordsList = FXCollections.observableArrayList();
wordsList.add(new Word("First Word", "Definition of First Word");
wordsList.add(new Word("Second Word", "Definition of Second Word");
wordsList.add(new Word("Third Word", "Definition of Third Word");
ListView<Word> listViewOfWords = new ListView<>(wordsList);

Cada objeto Word solo tiene 2 propiedades: wordString (Una cadena de la palabra) y definición (Otra cadena que es la definición de la palabra). Tengo captadores y definidores para ambos.

Puedes ver hacia dónde va esto: el código se compila y funciona, pero cuando lo muestro en mi aplicación, en lugar de mostrar los títulos de cada palabraen ListView, muestra el objeto de Word como una cadena.

Mi pregunta aquí es, específicamente, ¿existe una forma sencilla de reescribir esto?

ListView<Word> listViewOfWords = new ListView<>(wordsList);

¿De tal manera que, en lugar de tomar Words directamente de la lista de palabras, acceda a la propiedad wordString en cada palabra de mi observableArrayList?

Para que quede claro, esto no es para Android y la lista de palabras se cambiará, guardará y cargará eventualmente, por lo que no puedo crear otra matriz para contener las cadenas de palabras. He investigado un poco en la web y parece haber algo llamado 'Fábricas de células', pero parece innecesariamente complicado para lo que parece ser un problema tan simple y, como dije antes, soy un poco un novato cuando se trata de programación.

¿Alguien puede ayudar? Esta es mi primera vezaquí, así que lo siento si no he incluido suficiente código o si he hecho algo mal.



------------------------------------

Enfoque de solución

Recomiendo utilizar una fábrica de células para solucionar este problema.

listViewOfWords.setCellFactory(param -> new ListCell<Word>() {
    @Override
    protected void updateItem(Word item, boolean empty) {
        super.updateItem(item, empty);

        if (empty || item == null || item.getWord() == null) {
            setText(null);
        } else {
            setText(item.getWord());
        }
    }
});

Aplicación de muestra

import javafx.application.Application;
import javafx.collections.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;

public class CellFactories extends Application {    
    @Override
    public void start(Stage stage) {
        ObservableList<Word> wordsList = FXCollections.observableArrayList();
        wordsList.add(new Word("First Word", "Definition of First Word"));
        wordsList.add(new Word("Second Word", "Definition of Second Word"));
        wordsList.add(new Word("Third Word", "Definition of Third Word"));
        ListView<Word> listViewOfWords = new ListView<>(wordsList);
        listViewOfWords.setCellFactory(param -> new ListCell<Word>() {
            @Override
            protected void updateItem(Word item, boolean empty) {
                super.updateItem(item, empty);

                if (empty || item == null || item.getWord() == null) {
                    setText(null);
                } else {
                    setText(item.getWord());
                }
            }
        });
        stage.setScene(new Scene(listViewOfWords));
        stage.show();
    }

    public static class Word {
        private final String word;
        private final String definition;

        public Word(String word, String definition) {
            this.word = word;
            this.definition = definition;
        }

        public String getWord() {
            return word;
        }

        public String getDefinition() {
            return definition;
        }
    }

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

Si la información anterior fue suficiente para comprender la implementación necesaria para su aplicación, no necesita leer el resto de esta respuesta, que proporciona implementaciones optimizadas para diferentes casos de uso.

Implementaciones personalizadas

No anule toString para fines de presentación de la interfaz de usuario

Aunque puedes anular toString en tu clase de Word paraproporcione una representación de cadena de la palabra destinada a la representación en su ListView, recomendaría proporcionar una fábrica de celdas en ListView para la extracción de los datos de la vista del objeto de palabra y su representación en su ListView. Al utilizar este enfoque, obtienes una separación de preocupaciones ya que no vinculas una vista gráfica de tu objeto de Word con su método textual toString; por lo que toString podría seguir teniendo resultados diferentes (por ejemplo, información completa sobre campos de Word con un nombre de palabra y una descripción para fines de depuración).

Personalización de celdas mediante nodos gráficos

Además, una fábrica de células es más flexible ya que puede aplicar varios nodos gráficos para crear una representación visual de sus datos más allá de una simple cadena de texto (si desea hacerlo). Cualquier nodo (incluido(paneles de diseño que comprenden múltiples nodos) se pueden agregar a una celda usando setGraphic(node), generalmente también configurando el texto como nulo con setText(null), aunque puede tener tanto un gráfico como texto si lo desea.

Como demostración de este enfoque, el ejemplo se puede actualizar para definir una celda de lista personalizada que establezca un gráfico.

Reemplace la llamada setCellFactory con:

listViewOfWords.setCellFactory(param -> new WordListCell());

Y agrega la siguiente clase:

private static class WordListCell extends ListCell<Word> {
    private final Label title = new Label();
    private final Label detail = new Label();
    private final VBox layout = new VBox(title, detail);

    public WordListCell() {
        super();
        title.setStyle("-fx-font-size: 20px;");
    }

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

        setText(null);

        if (empty || item == null || item.getWord() == null) {
            title.setText(null);
            detail.setText(null);
            setGraphic(null);
        } else {
            title.setText(item.getWord());
            detail.setText(
                    item.getDefinition() != null
                            ? item.getDefinition()
                            : "Undefined"
            );
            setGraphic(layout);
        }
    }
}

Se recomiendan objetos (registros) inmutables a menos que se requieran actualizaciones de campos dinámicos

Además, como comentario aparte, recomiendo hacer que sus objetos de Word sean objetos inmutables, eliminando sus configuradores (o usando registros en Java 16+). Si realmente necesita modificar los objetos de palabras, entonces la mejor manera de manejarlo es exponer las propiedades observables en las clases f.o los campos de objeto.

Para convertir la aplicación anterior para que utilice objetos inmutables, reemplace la definición de clase de Word con una definición de registro de Word.

public record Word(String word, String definition) {}

Entonces, cada vez que acceda a los campos de los registros, no utilice el prefijo get. Por ejemplo, en la fábrica de células, como item es un Word, use item.word() en lugar de item.getWord() para obtener la cadena de palabras de Word.

Actualizaciones dinámicas

Simplemente agregar y eliminar elementos observables subyacentes en la lista activará actualizaciones en la vista.

Si también desea que su interfaz de usuario se actualice a medida que cambian las propiedades observables de sus objetos, entonces necesita hacer que las celdas de su lista sean conscientes de los cambios en los elementos asociados, escuchando los cambios en ellos (lo cual es un poco más complejo, en este caso un extractor puede ayudar). Notaeso, la lista que contiene las palabras ya es observable y ListView se encargará de manejar los cambios en esa lista, pero si modificó la definición de la palabra, por ejemplo, dentro de un objeto de palabra mostrado, entonces su vista de lista no detectará los cambios en la definición. sin una lógica de escucha adecuada en la fábrica de células ListView (o, preferiblemente, un extractor).

Ejemplo de definición de extractor para una ObservableList de los objetos de Word. Para que esto funcione, la clase Word se ha actualizado para usar propiedades para sus campos en lugar de tipos simples.

ObservableList<Word> wordsList = FXCollections.observableArrayList(word ->
        new Observable[] {
                word.wordProperty(),
                word.definitionProperty()
        }
);

Un ejemplo completo que demuestra el uso de un extractor.

En el ejemplo, la opción "Actualizar segunda palabra" Se ha presionado el botón tres veces para actualizar la palabra. En cada pulsación, el recuento actual de veces que la palabra tiene been actualizado se añade a la palabra. Sin el extractor para notificar los cambios, la segunda palabra de la lista no se actualizaría cuando se presione el botón.

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.*;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class CellFactoriesWithExtractor extends Application {
    private int nChanges = 0;

    @Override
    public void start(Stage stage) {
        ObservableList<Word> wordsList = FXCollections.observableArrayList(word ->
                new Observable[] {
                        word.wordProperty(),
                        word.definitionProperty()
                }
        );

        wordsList.add(new Word("First Word", "Definition of First Word"));
        wordsList.add(new Word("Second Word", "Definition of Second Word"));
        wordsList.add(new Word("Third Word", "Definition of Third Word"));

        ListView<Word> listViewOfWords = new ListView<>(wordsList);
        listViewOfWords.setCellFactory(param -> new ListCell<Word>() {
            @Override
            protected void updateItem(Word item, boolean empty) {
                super.updateItem(item, empty);

                if (empty || item == null || item.getWord() == null) {
                    setText(null);
                } else {
                    setText(item.getWord());
                }
            }
        });

        Button updateWord = new Button("Update second word");
        updateWord.setOnAction(event -> {
            ++nChanges;
            Word wordToChange = wordsList.get(1);
            wordToChange.setWord(wordToChange.getWord() + " " + nChanges);
        });

        VBox layout = new VBox(10,
                updateWord,
                listViewOfWords
        );
        layout.setPadding(new Insets(10));
        VBox.setVgrow(listViewOfWords, Priority.ALWAYS);

        stage.setScene(new Scene(layout));
        stage.show();
    }

    public static class Word {
        private final StringProperty word = new SimpleStringProperty();
        private final StringProperty definition = new SimpleStringProperty();

        public Word(String word, String definition) {
            this.word.setValue(word);
            this.definition.setValue(definition);
        }

        public String getWord() {
            return word.get();
        }

        public StringProperty wordProperty() {
            return word;
        }

        public void setWord(String word) {
            this.word.set(word);
        }

        public String getDefinition() {
            return definition.get();
        }

        public StringProperty definitionProperty() {
            return definition;
        }

        public void setDefinition(String definition) {
            this.definition.set(definition);
        }
    }

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

4

¡Muchas gracias por tu respuesta tan detallada! Por ahora, intentaré anular el método toString(), pero leeré un poco más sobre Cell Factories para comprenderlos mejor y luego modificaré la aplicación para usarlos según su sugerencia. Solo modificaré la "definición" de cada palabra individual. conese botón de edición, y la vista de lista solo rastrea la 'palabra' propiedad, por lo que no necesita realizar un seguimiento de los cambios en la "definición".

-David Collins

16/04/2016 a las 14:39

@DavidCollins ¿Finalmente implementaste la solución usando Cell Factory? Te agradecería que lo publicaras

- desplegable

31 de mayo de 2020 a las 13:41

@droptop esta respuesta ya incluye código para una solución completa que utiliza una fábrica de células.

– jewelsea

1 de junio de 2020 a las 17:02

object!=null funcionó para mí, gracias.

-Noor Hossain

27 de septiembre de 2021 a las 14:42



------------------------------------

Le apostaría cinco centavos a que ListView está llamando al método toString() en Word. Si no has anuladoEn él, utiliza el método toString() predeterminado, que simplemente genera esa... información no tan útil. Anúlelo para generar una cadena con un bonito formato y ¡debería estar listo!

2

Hmmm, ¡eso tiene sentido! Lo intentaré; En este momento tengo que asistir a una clase, pero lo probaré más tarde y veré si funciona.

-David Collins

15/04/2016 a las 21:53

9

Pero no es una muy buena solución: es posible que desees que el método toString() devuelva algo diferente a lo que deseas que se muestre en la interfaz de usuario.

- James_D

16/04/2016 a las 2:10

randomThread
c - ¿Existe una versión de fgets que no termina en una nueva línea sino que lee un archivo con un número específico de caracterejava - Interrumpir escáner nextLine() (alternativas)c - Opciones de compilación del compilador Microchip MPLAB IDE y XC8diseño impulsado por dominio - Búsqueda de eventos - Reproducción de eventos¿Cómo decodifico caracteres Unicode escapados en caracteres normales?¿Cómo producir una lista de índices impares a partir de un número en Python?java - Tirar para actualizar en Webview para actualizar el contenidosql: ¿cómo puedo eliminar el signo de moneda de postgresql?javascript - ¿Cómo llamar a un php con parámetro de un