JavaFX - ComboBox con cuadro de búsqueda

Un ComboBox es un control JavaFX que muestra una lista desplegable de elementos, el usuario puede seleccionar el elemento deseado, cuando la lista es extensa el usuario puede tener problemas con la búsqueda, por lo que intentaremos crear un ComboBox JavaFX con un cuadro de búsqueda.  

control javafx SearchComboBox

Para lograr este objetivo extenderemos la clase javafx.scene.control.ComboBox para agregar una lista filtrada según en texto contenido en el javafx.scene.control.TextField, también es necesario redefinir el Skin, para un ComboBox la clase ComboBoxListViewSkin define la apariencia y comportamiento del control, extenderemos esta clase para agregar el cuadro de búsqueda y el filtro.

public class SearchComboBoxSkin<T> extends ComboBoxListViewSkin<T> {

    private final TextField searchBox;
    private final ListView<T> itemView;

    private boolean clickSelection = false;

    public SearchComboBoxSkin(SearchComboBox<T> comboBox) {
        super(comboBox);

        searchBox = new TextField();
        searchBox.setPromptText("Search Box");
        searchBox.textProperty().addListener((p, o, text) -> handleTextChange(text));

        itemView = new ListView<>();
        itemView.setItems(comboBox.getFilterList());

        // administrar la seleccion de un nuevo item
        itemView.getSelectionModel().selectedItemProperty().addListener((p, o, item) -> {
            if (item != null) {
                comboBox.getSelectionModel().select(item);
                
                // ocultar popup cuando el item fue seleccionado mediante un click
                if (clickSelection) {
                    comboBox.hide();
                }
            }
        });

        // ocultar popup al usar las teclas determindas ENTER, ESC, SPACE
        itemView.setOnKeyPressed(t -> {
            if (t.getCode() == KeyCode.ENTER || t.getCode() == KeyCode.SPACE || t.getCode() == KeyCode.ESCAPE) {
                comboBox.hide();
            }
        });

        // cambia el foco del TextField al ListView usando las teclas ENTER y ESC
        searchBox.setOnKeyPressed(t -> {
            if (t.getCode() == KeyCode.ENTER || t.getCode() == KeyCode.ESCAPE) {
                itemView.requestFocus();
            }
        });

        // se ha hecho click sobre el ListView
        itemView.addEventFilter(MouseEvent.ANY, me
                -> clickSelection = me.getEventType().equals(MouseEvent.MOUSE_PRESSED));
    }

    @Override
    protected PopupControl getPopup() {

        // redefinir el combobox popup
        super.getPopup().setSkin(new Skin<Skinnable>() {

            @Override
            public Skinnable getSkinnable() {
                return null;
            }

            @Override
            public Node getNode() {
                return createPopupContent();
            }

            @Override
            public void dispose() {
                
            }
        });

        return super.getPopup();
    }

    private void handleTextChange(String text) {
        SearchComboBox<T> scb = ((SearchComboBox) getSkinnable());
        scb.setPredicateFilter(item -> text.isEmpty() ? true : scb.getFilter().test(item, text));
    }

    private Node createPopupContent() {

        VBox box = new VBox(searchBox, itemView);
        box.setSpacing(2.0);
        box.setPadding(new Insets(2.0));
        box.getStyleClass().add("combo-box-popup");
        box.setMaxWidth(getSkinnable().getWidth());

        return box;
    }

    @Override
    protected void handleControlPropertyChanged(String p) {
        super.handleControlPropertyChanged(p);

        if ("SHOWING".equals(p)) {

            SearchComboBox<T> scb = ((SearchComboBox<T>) getSkinnable());
            if (scb.isShowing()) {
                searchBox.clear();

                itemView.getSelectionModel().select(scb.getValue());
                itemView.requestFocus();
            }
        }
    }

}

Sobre escribimos el método protected PopupControl getPopup() este es el encargado de crear el Popup del ComboBox, el Popup es la ventana que se muestra al hacer clic sobre el control, esta ventana le permite al usuario seleccionar un elemento de la lista disponible.

Hacemos la modificaciones necesarias para mostrar un cuadro de texto y una nueva lista que contendrá solamente los elementos que coinciden con el filtro de búsqueda.

public class SearchComboBoxSkin<T> extends ComboBoxListViewSkin<T> {

    @Override
    protected PopupControl getPopup() {

        // redefinir el combobox popup
        super.getPopup().setSkin(new Skin<Skinnable>() {

            @Override
            public Skinnable getSkinnable() {
                return null;
            }

            @Override
            public Node getNode() {
                return createPopupContent();
            }

            @Override
            public void dispose() {
                
            }
        });

        return super.getPopup();
    }

    private Node createPopupContent() {

        VBox box = new VBox(searchBox, itemView);
        box.setSpacing(2.0);
        box.setPadding(new Insets(2.0));
        box.getStyleClass().add("combo-box-popup");
        box.setMaxWidth(getSkinnable().getWidth());

        return box;
    }

}

Cuando es usuario escriba un texto en el cuadro de búsqueda, debemos cambiar el filtro para mostrar solo aquellos elementos que coincidan con el mismo, agregamos un listener a la propiedad textProperty para reaccionar a la introducción de un nuevo texto, cuando ello ocurre cambiamos el filtro.

public class SearchComboBoxSkin<T> extends ComboBoxListViewSkin<T> {

    public SearchComboBoxSkin(SearchComboBox<T> comboBox) {
        super(comboBox);

        // cuadro de busqueda
        searchBox = new TextField();
        searchBox.setPromptText("Search Box");
        searchBox.textProperty().addListener((p, o, text) -> handleTextChange(text));

        // ListView que muestra los elementos filtrados
        itemView = new ListView<>();
        itemView.setItems(comboBox.getFilterList());
    }

    private void handleTextChange(String text) {
        SearchComboBox<T> scb = ((SearchComboBox) getSkinnable());
        scb.setPredicateFilter(item -> text.isEmpty() ? true : scb.getFilter().test(item, text));
    }
}

Extendemos la clase ComboBox<T> para agregar un FilteredList<T> (lista filtrado de elementos de tipo T) y un BiPredicate<T, String> que nos permite definir el criterio de búsqueda, donde T es el elemento de la lista y String el texto del cuadro de búsqueda.

public class SearchComboBox<T> extends ComboBox<T> {

    private FilteredList<T> filterList;
    private BiPredicate<T, String> filter;

    public SearchComboBox() {
        this(FXCollections.observableArrayList());
    }
    
    public SearchComboBox(ObservableList<T> items) {

        this.filterList = new FilteredList<>(items);
        this.filter = (i, s) -> true;

        super.setItems(items);
        super.itemsProperty().addListener((p, o, n) -> {
            this.filterList = new FilteredList<>(n);
        });
    }

    @Override
    protected Skin<?> createDefaultSkin() {
        return new SearchComboBoxSkin<>(this);
    }

    public void setFilter(BiPredicate<T, String> filter) {
        this.filter = filter;
    }

    public BiPredicate<T, String> getFilter() {
        return filter;
    }

    public void setPredicateFilter(Predicate<T> predicate) {
        filterList.setPredicate(predicate);
    }

    public FilteredList<T> getFilterList() {
        return filterList;
    }
}

Utilizamos nuestro control de la siguiente manera, creamos la lista de elementos, para este ejemplo usamos un conjunto de cadenas de texto, pueden ser elementos de cualquier tipo, establecemos la lista usando el método setItems(items), y finalmente indicamos el filtro, usaremos un filtro sencillo, todos los elementos que contengan el texto escrito serán mostrados, aplicamos el filtro usando setFilter(filter).

SearchComboBox<String> cbx = new SearchComboBox<>();
cbx.setItems(items);
cbx.setFilter((item, text) -> item.contains(text));
cbx.setPrefWidth(250.0);
cbx.getSelectionModel().select(5);

Con ello logramos lo siguiente:

combobox busqueda javafx

GitHub: ComboBox con Cuadro de Búsqueda

Comentarios

Temas relacionados

Entradas populares de este blog

tkinter Grid

tkinter Canvas

Histogramas OpenCV Python

Python Binance API