B. Selin Zaza
B. Selin Zaza

Reputation: 91

Adding an image into a JavaFX TableView column

I am new to Java and OOP and got stuck in adding image to tableview column. Code seems to work, I can see the name of the student correct but images are not shown in the column. I am getting this error and could not understand how to make it work:

javafx.scene.control.cell.PropertyValueFactory getCellDataReflectively
WARNING: Can not retrieve property 'picture' in PropertyValueFactory: javafx.scene.control.cell.PropertyValueFactory@5b0da50f with provided class type: class model.StudentModel
java.lang.IllegalStateException: Cannot read from unreadable property picture

StudentModel:

package model;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.image.ImageView;

import java.util.ArrayList;
import java.util.List;

public class StudentModel {

    private ImageView picture;
    private String name;
    private SubjectModel major;
    private SubjectModel minor;
    private String accountPassword;
    public String getAccountPassword()
    {
        return accountPassword;
    }
    public List<LectureModel> lectureModelList = new ArrayList<>();

    public StudentModel(String name, SubjectModel major, SubjectModel minor, ImageView picture, String accountPassword)
    {
        this.name = name;
        this.major = major;
        this.minor = minor;
        this.picture = picture;
        this.accountPassword = accountPassword;
    }
    public String getName()
    {
        return name;
    }

    public ObservableList<LectureModel> myObservableLectures(){
        ObservableList<LectureModel> observableList = FXCollections.observableArrayList(lectureModelList);
        return observableList;
    }


    public ImageView getPhoto(){
        return picture;
    }


    public void setPhoto(ImageView photo)
    {
        this.picture =  photo;

    }
}

And Participants Scene which I have the tableview:

public class ParticipantsScene extends Scene {

    private final StudentController studentController;
    private final ClientApplication clientApplication;
    private final TableView<StudentModel> allParticipantsTable;
    private final ObservableList<StudentModel> enrolledStudents;
    private LectureModel lecture;

    public ParticipantsScene(StudentController studentController, ClientApplication application, LectureModel lecture) {
        super(new VBox(), 800 ,500);
        this.clientApplication = application;
        this.studentController = studentController;
        this.lecture = lecture;
        enrolledStudents=lecture.observeAllParticipants();


        TableColumn<StudentModel, String > nameCol = new TableColumn<>("Name");
        nameCol.setMinWidth(200);
        nameCol.setCellValueFactory(new PropertyValueFactory<>("name"));

        TableColumn<StudentModel, ImageView> picCol = new TableColumn<>("Images");
        picCol.setPrefWidth(200);
        picCol.setCellValueFactory(new PropertyValueFactory<>("picture"));

        allParticipantsTable = new TableView<>();
        allParticipantsTable.getColumns().addAll(nameCol,picCol);
        allParticipantsTable.setItems(enrolledStudents);

        VBox vBox = new VBox(10, allParticipantsTable, createButtonBox());
        vBox.setAlignment(Pos.CENTER);
        setRoot(vBox);

    }
    private HBox createButtonBox() {
        var backButton = new Button("Back");
        backButton.setOnAction(event -> clientApplication.showAllLecturesScene());

        var buttonBox = new HBox(10, backButton);
        buttonBox.setAlignment(Pos.CENTER);
        return buttonBox;
    }
}

Also adding Lectures model in case it may helpful:

public class LectureModel {

    private String lectureName;
    private String lectureHall;
    private String subjectName;
    private SubjectModel subject;
    private TimeSlot timeSlot;
    //private Button actionButton1;
    //private Button actionButton2;

    private List<StudentModel> enrolledStudents = new ArrayList<>();
    private String name;



    public LectureModel(String lectureName, String lectureHall, SubjectModel subject, TimeSlot timeSlot){
        this.lectureName = lectureName;
        this.lectureHall = lectureHall;
        this.subject = subject;
        this.timeSlot = timeSlot;
        this.subjectName = this.subject.getSubjectName();
    }

    public String getLectureName()
    {
        return lectureName;
    }
    public String getLectureHall()
    {
        return lectureHall;
    }
    public SubjectModel getSubject()
    {
        return subject;
    }
    public String getSubjectName()
    {
        return subjectName;
    }
    public List<StudentModel> getEnrolledStudents()
    {
        return enrolledStudents;
    }

    public ObservableList<StudentModel> observeAllParticipants() {
        ObservableList<StudentModel> observableList = FXCollections.observableArrayList(getEnrolledStudents());
        return observableList;
    }
    public TimeSlot getTimeSlot() {
        return timeSlot;
    }

    public void addStudent(StudentModel studentModel){ enrolledStudents.add(studentModel);}
    public void removeStudent(StudentModel studentModel)
    {
        enrolledStudents.remove(studentModel);
    };

Appreciate any kind of helps, Thanks!

Upvotes: 3

Views: 1110

Answers (1)

jewelsea
jewelsea

Reputation: 159576

You have misnamed the property name used in the PropertyValueFactory.

In general, don't use PropertyValueFactories, instead use a lambda:

Also, as a general principle, place data in the model, not nodes. For example, instead of an ImageView, store either an Image or a URL to the image in the model. Then use nodes only in the views of the model. For example, to display an image in a table cell, use a cell factory.

An LRU cache can be used for the images if needed (it may not be needed).

Often the images displayed in a table might be smaller than the full-size image, i.e. like a thumbnail. For efficiency, you might want to load images in the background using a sizing image constructor.

If you need help placing and locating your image resources, see:

Example code

The example in this answer uses some of the principles from the answer text:

  • Uses a Lambda instead of PropertyValue.
  • The model for list items is represented as a record using immutable data.
    • Replace the record with a standard class if you want read/write access to data.
  • An Image URL is stored as a String in the model rather than as an ImageView node.
  • A cell factory is used to provide an ImageView node to view the image.
  • Images are loaded in the background and resized to thumbnail size on loading.
    • You can skip the thumbnail sizing and use full-size images if your app requires that.
    • You can load in the foreground if you want the UI to wait until the images are loaded before displaying (not recommended, but for small local images you won't see any difference).
  • Images are loaded in an LRU cache.
    • If you don't have a lot of images (e.g. thousands), you could instead store the Image (not the ImageView) directly in the model and use that, removing the LRU cache from the solution.

Though I didn't test it, this solution should scale fine to a table with thousands of rows, each with different images.

The images used in this answer are provided here:

table

ImageCell.java

import javafx.scene.control.TableCell;
import javafx.scene.image.ImageView;

public class ImageCell<S> extends TableCell<S, String> {
    private final ImageView imageView = new ImageView();
    private final ImageCache imageCache = ImageCache.getInstance();

    @Override
    protected void updateItem(String url, boolean empty) {
        super.updateItem(url, empty);

        if (url == null || empty || imageCache.getThumbnail(url) == null) {
            imageView.setImage(null);
            setGraphic(null);
        } else {
            imageView.setImage(imageCache.getThumbnail(url));
            setGraphic(imageView);
        }
    }
}

ImageCache.java

import javafx.scene.image.Image;

import java.util.Map;
import java.util.Objects;

public class ImageCache {
    private static final int IMAGE_CACHE_SIZE = 10;
    private static final int THUMBNAIL_SIZE = 64;

    private static final ImageCache instance = new ImageCache();

    public static ImageCache getInstance() {
        return instance;
    }

    private final Map<String, Image> imageCache = new LruCache<>(
            IMAGE_CACHE_SIZE
    );

    private final Map<String, Image> thumbnailCache = new LruCache<>(
            IMAGE_CACHE_SIZE
    );

    public Image get(String url) {
        if (!imageCache.containsKey(url)) {
            imageCache.put(
                    url,
                    new Image(
                            Objects.requireNonNull(
                                    ImageCache.class.getResource(
                                            url
                                    )
                            ).toExternalForm(),
                            true
                    )
            );
        }

        return imageCache.get(url);
    }

    public Image getThumbnail(String url) {
        if (!thumbnailCache.containsKey(url)) {
            thumbnailCache.put(
                    url,
                    new Image(
                            Objects.requireNonNull(
                                    ImageCache.class.getResource(
                                            url
                                    )
                            ).toExternalForm(),
                            THUMBNAIL_SIZE,
                            THUMBNAIL_SIZE,
                            true,
                            true,
                            true
                    )
            );
        }

        return thumbnailCache.get(url);
    }
}

LruCache.java

import java.util.LinkedHashMap;
import java.util.Map;

public final class LruCache<A, B> extends LinkedHashMap<A, B> {
    private final int maxEntries;

    public LruCache(final int maxEntries) {
        super(maxEntries + 1, 1.0f, true);
        this.maxEntries = maxEntries;
    }

    @Override
    protected boolean removeEldestEntry(final Map.Entry<A, B> eldest) {
        return super.size() > maxEntries;
    }
}

Student.java

public record Student(
        String last,
        String first,
        String avatar
) {}

StudentTableApp.java

import javafx.application.Application;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class StudentTableApp extends Application {
    @Override
    public void start(Stage stage) {
        TableView<Student> table = createTable();
        populateTable(table);

        VBox layout = new VBox(
                10,
                table
        );
        layout.setPadding(new Insets(10));
        layout.setPrefSize(340, 360);

        layout.setStyle("-fx-font-size:20px; -fx-base: antiquewhite");

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

    private TableView<Student> createTable() {
        TableView<Student> table = new TableView<>();

        TableColumn<Student, String> lastColumn = new TableColumn<>("Last");
        lastColumn.setCellValueFactory(
                p -> new ReadOnlyStringWrapper(
                        p.getValue().last()
                ).getReadOnlyProperty()
        );

        TableColumn<Student, String> firstColumn = new TableColumn<>("First");
        firstColumn.setCellValueFactory(
                p -> new ReadOnlyStringWrapper(
                        p.getValue().first()
                ).getReadOnlyProperty()
        );

        TableColumn<Student, String> avatarColumn = new TableColumn<>("Avatar");
        avatarColumn.setCellValueFactory(
                p -> new ReadOnlyStringWrapper(
                        p.getValue().avatar()
                ).getReadOnlyProperty()
        );
        avatarColumn.setCellFactory(
                p -> new ImageCell<>()
        );
        avatarColumn.setPrefWidth(70);

        table.getColumns().addAll(
                List.of(
                        lastColumn, firstColumn, avatarColumn
                )
        );

        return table;
    }

    private void populateTable(TableView<Student> table) {
        table.getItems().addAll(
                new Student("Dragon", "Smaug", "Dragon-icon.png"),
                new Student("Snake-eyes", "Shifty", "Medusa-icon.png"),
                new Student("Wood", "Solid", "Treant-icon.png"),
                new Student("Rainbow", "Magical", "Unicorn-icon.png")
        );
    }
}

Upvotes: 8

Related Questions