3

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!

jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • 1
    Rename `getPhoto` and `setPhoto` to `getPicture` and `setPicture`, respectively. Or use `new PropertyValueFactory<>("photo");`. That _should_ get your code working. – Slaw Jun 30 '22 at 20:29
  • 3
    However, [I would recommend _not_ using `PropertyValueFactory`](https://stackoverflow.com/questions/72437983/why-should-i-avoid-using-propertyvaluefactory-in-javafx). And you might want to also [check out the first half of this tutorial](https://docs.oracle.com/javase/8/javase-clienttechnologies.htm) to understand how to use JavaFX properties. On a design level, your model class should not have an `ImageView`. That's a view class and as such belongs in the view. You could have an `Image` in the model or, probably better, simply the URL/path to the image in the model. – Slaw Jun 30 '22 at 20:33
  • 2
    I would also recommend not extending a class unless you're adding behavior in the subclass. Your `ParticipantsScene` is only _configuring_ a `Scene`, not adding behavior. That can be done in a dedicated "view class" or even just a method (e.g., in the `Application#start(Stage)` method override for relatively simply applications). – Slaw Jun 30 '22 at 20:39
  • @Slaw thank you very much all of your comments. I will revise my code and consider them. For the first thing you said about changing photo to picture, error disappeared but image still not shown. Do you have any idea why? – B. Selin Zaza Jun 30 '22 at 20:53
  • 1
    I don't see you setting an `Image` in any of the `ImageView`s in the code you've provided. This leads me to assume the `ImageView` is being displayed in the table cell (pretty sure the default `cellFactory` will simply set the cell's `graphic` property if the item is a `Node`), but that you haven't given it an `Image` to render. – Slaw Jun 30 '22 at 20:58
  • 1
    @Slaw Thank you very much!!! I changed the way how I read the picture and now it appears in the cell. I was really stuck for a long time! really appreciating your help :) – B. Selin Zaza Jun 30 '22 at 21:07

1 Answers1

8

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

import javafx.application.Application;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.util.*;

public class StudentTableViewer extends Application {
    public record Student(String last, String first, String avatar) {}

    @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 AvatarCell()
        );
        avatarColumn.setPrefWidth(70);

        //noinspection unchecked
        table.getColumns().addAll(lastColumn, firstColumn, avatarColumn);

        return table;
    }

    public static class AvatarCell extends TableCell<Student, 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);
            }
        }
    }

    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")
        );
    }
}

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);
    }

    private static 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;
        }
    }
}
jewelsea
  • 150,031
  • 14
  • 366
  • 406