Search code examples
javajavafxdesktop-application

Mysterious " The color components or name must be specified " error in JavaFX ListView


I tried to customize the ListView in Javafx by trying to display a colored rectangle instead of the string itself.

public class Main extends Application {
@Override
public void start(Stage primaryStage)
{
    VBox vBox = new VBox();
    ListView<String> listView = new ListView<>();
    vBox.getChildren().add(listView);
    ObservableList<String> list = FXCollections.observableArrayList("black" , "blue" , "brown" , "gold");
    listView.setItems(list);
    listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
        @Override
        public ListCell<String> call(ListView<String> stringListView) {
            return new cell();
        }
    });

    primaryStage.setScene(new Scene(vBox,400 , 400));
    primaryStage.show();
}


public class cell extends ListCell<String>
{
    Rectangle rect;

    cell() {
        super();
        this.rect = new Rectangle(20,20);
        this.rect.setFill(Color.web(getItem()));      // ERROR  ERROR  ERROR
        setGraphic(this.rect);
    }

    @Override
    protected void updateItem(String s, boolean empty) {
        super.updateItem(s, empty);
        if(empty)
            setGraphic(null);
        else 
            setGraphic(this.rect);
    }
}

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

}

Apparently the error is in the line in which I have indicated as ERROR. I manipulated the cell class a little and it worked. Below is the manipulated cell class:

public class cell extends ListCell<String>
{
    Rectangle rect;

    cell() {
        super();
        this.rect = new Rectangle(20,20);
       // this.rect.setFill(Color.web(getItem()));
        setGraphic(this.rect);
    }

    @Override
    protected void updateItem(String s, boolean empty) {
        super.updateItem(s, empty);
        if(empty)
            setGraphic(null);
        else {
            rect.setFill(Color.web(getItem()));
            setGraphic(this.rect);
        }
    }

I understand that the updateItem() is going to be called a lot of times. My first method is indeed reducing the work done by updateItem() , but for some reason its throwing error in that line. What could be the reason for the error in the previous approach


Solution

  • The item property of a ListCell initially is null. Color.web does not accept null as parameter. Furthermore you need to be able to handle the fact that the item of a ListCell can be replaced during its lifecycle and that the same item may be assigned to different cells. ListView creates only cells that are needed to fill the view and if e.g. the viewport of the scrollable area changes, different items need to be visible and the cells are reused to display the changed set of items.

    If you worry about the performance of some calculation in updateItem, you could cache the results in a map (possibly wrapping the values in SoftReferences, if you're worried about memory consumption).

    In this case this is not necessary, since:

    1. Color.web isn't expensive,
    2. Named colors like the items you use are stored in a Map anyways; only a single Color instance per distinct named color is created regardless of how often you pass the same parameter to Color.web.

    BTW: I don't recommend initializing the cell in a way that cannot be the result of updateItem calls. In your case the graphic property for empty cells is null except for the initial state. If you're worried about consistent cell sizes, it would be better to always keep the graphic and setting its visibility:

    public class cell extends ListCell<String> {
        private final Rectangle rect;
    
        cell() {
            super();
            this.rect = new Rectangle(20,20);
            setGraphic(this.rect);
        }
    
        @Override
        protected void updateItem(String s, boolean empty) {
            super.updateItem(s, empty);
            if(empty)
                rect.setVisible(false);
            else {
                rect.setFill(Color.web(getItem()));
                rect.setVisible(true);
            }
        }
    }