Search code examples
listviewjavafxcombobox

Java FX ComboBox with coloured shapes


I wish to add two or more circles as options to a combo box.

Circle c1 = new Circle(10, 8, 5);
Circle c2 = new Circle(24, 8, 5);

The circles should be coloured individually using either CSS styling

c1.setStyle("-fx-fill: red;");
c2.setStyle("-fx-fill: green;");

or directly in Java syntax

c1.setFill(Color.RED);
c2.setFill(Color.GREEN);

My problem is that the setGraphic() method takes as parameter a single Node object and to form this Node object one needs a boolean operation such as Shape.union(c1, c2). But this eliminates the styling applied previously so instead of a red and a green Circle I get both of them black.

enter image description here

Below is the minimum example. All code can go in a single .java file

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;
import javafx.util.Callback;

public class Test extends Application  {

   public static ObservableList<String> colours = FXCollections.observableArrayList("1", "2");

   @Override
   public void start(Stage primaryStage) throws Exception {

       ComboBox comboBox = new ComboBox();
       comboBox.setItems(colours);
       comboBox.setCellFactory(new gui.combos.ColourCellFactory());
       comboBox.setButtonCell(new gui.combos.ColourCell());
       HBox hbox = new HBox(comboBox);
       Scene scene = new Scene(hbox, 200, 120);
       primaryStage.setScene(scene);
       primaryStage.show();
   }

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

class ColourCell extends ListCell<String> {
   @Override
   public void updateItem(String item, boolean empty) {
       super.updateItem(item, empty);
       if (empty) {
           setText(null);
           setGraphic(null);
       }
       else {
           setText(item);
           Shape shapes = this.getShapes(item);
           setGraphic(shapes);
       }
   }

    public Shape getShapes(String string)  {

        Shape shapes = null;
        switch (string) {
            case "1":
                Circle c = new Circle(10, 8,5); c.setStyle("-fx-fill: red;");
                shapes = c;
                break;
            case "2":
                Circle c1 = new Circle(10, 8,5); c1.setFill(Color.RED); 
                Circle c2 = new Circle(24, 8,5); c2.setFill(Color.GREEN);
                shapes = Shape.union(c1, c2);
                break;
            default:
                shapes = null;
        }
        return shapes;
    }
}

class ColourCellFactory implements Callback<ListView<String>, ListCell<String>> {
   @Override
   public ListCell<String> call(ListView<String> listview) {
       return new gui.combos.ColourCell();
   }
}

Any idea how to overcome this inconvenience would be greatly appreciated. Thank you.


Solution

  • A union of shapes will create a single shape. The elements which comprise the union shape cannot be styled independently.

    As James_D suggests in comments, don't union the shapes, instead use a parent container such as Group or HBox.

    The example below demonstrates using an HBox for the graphic which allows the graphic content to be managed by the layout pane rather than requiring a manual layout.

    image

    import javafx.application.Application;
    import javafx.collections.*;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.layout.HBox;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.*;
    import javafx.stage.Stage;
    
    import java.util.List;
    
    public class ShapeCombo extends Application {
    
        public static ObservableList<String> colours = FXCollections.observableArrayList(
                "1", "2"
        );
    
        @Override
        public void start(Stage stage) throws Exception {
            ComboBox<String> comboBox = new ComboBox<>(colours);
            comboBox.setCellFactory(listView -> new ColourCell());
            comboBox.setButtonCell(new ColourCell());
    
            Scene scene = new Scene(new HBox(comboBox), 200, 120);
            stage.setScene(scene);
            stage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    
    class ColourCell extends ListCell<String> {
        private HBox graphicContent = new HBox(6);
    
        public ColourCell() {
            graphicContent.setPadding(new Insets(2, 3, 2, 3));
        }
    
        @Override
        public void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
    
            if (item == null || empty) {
                setText(null);
                setGraphic(null);
            } else {
                setText(item);
                graphicContent.getChildren().setAll(
                        getShapes(item)
                );
                setGraphic(graphicContent);
            }
        }
    
        public List<Shape> getShapes(String string) {
            return switch (string) {
                case "1" -> List.of(
                        new Circle(5, Color.RED)
                );
                case "2" -> List.of(
                        new Circle(5, Color.RED),
                        new Circle(5, Color.GREEN)
                );
                default -> List.of();
            };
        }
    }