Filtering JComboBox

2020-07-30 03:30发布

问题:

At beginning I will say that I don't have in mind auto-complete combobox, but rather having a "setFilter(Set)" method in my combobox, so it displays what is in the set.

I was unable to achieve that effect, trying different approaches and I think it's the view responsibility to filter what it displays, so I should not extend ComboBoxModel.

This is what I have so far (main includes the case which doesn't work):

import java.awt.*;
import java.util.Set;

import javax.swing.*;


public class FilteredComboBox extends JComboBox {
    private ComboBoxModel entireModel;
    private final DefaultComboBoxModel filteredModel = new DefaultComboBoxModel();

    private Set objectsToShow;

    public FilteredComboBox(ComboBoxModel model) {
        super(model);
        this.entireModel = model;
    }

    public void setFilter(Set objectsToShow) {
        if (objectsToShow != null) {
            this.objectsToShow = objectsToShow;
            filterModel();
        } else {
            removeFilter();
        }
    }

    public void removeFilter() {
        objectsToShow = null;
        filteredModel.removeAllElements();
        super.setModel(entireModel);
    }

    private void filterModel() {
        filteredModel.removeAllElements();
        for (int i = 0; i < entireModel.getSize(); ++i) {
            Object element = entireModel.getElementAt(i);
            addToFilteredModelIfShouldBeDisplayed(element);
        }

        super.setModel(filteredModel);
    }

    private void addToFilteredModelIfShouldBeDisplayed(Object element) {
        if (objectsToShow.contains(element)) {
            filteredModel.addElement(element);
        }
    }

    @Override
    public void setModel(ComboBoxModel model) {
        entireModel = model;
        super.setModel(entireModel);
        if (objectsToShow != null) {
            filterModel();
        }
    }

    public static void main(String[] args) {
        JFrame f = new JFrame();
        f.setLayout(new BoxLayout(f.getContentPane(), BoxLayout.X_AXIS));
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        DefaultComboBoxModel model = new DefaultComboBoxModel();

        FilteredComboBox cb = new FilteredComboBox(model);
        cb.setPrototypeDisplayValue("XXXXXXXXXXXX");

        f.add(cb);
        f.pack();

        Set objectsToShow = new HashSet();
        objectsToShow.add("1");
        objectsToShow.add("3");
        objectsToShow.add("4");

        cb.setFilter(objectsToShow); // if you set that filter after addElements it will work
        model.addElement("1");
        model.addElement("2");
        model.addElement("3");
        model.addElement("4");
        model.addElement("5");

        f.setVisible(true);
    }
}

回答1:

"I think it's the view responsibility to filter what it displays" - I'd argue that, the view displays what it's told, the model drives what it can show, but that's me...

This is an idea I wrote way back in Java 1.3 (with generic updates) which basically wraps a proxy ComboBoxModel around another ComboBoxModel. The proxy (or FilterableComboBoxModel) then makes decisions about which elements from the original model match a filter and updates it's indices.

Basically, all it does is generates an index map between itself and the original model, so it's not copying anything or generating new references to the original data.

The filtering is controlled via a "filterable" interface which simply passes the element to be checked and expects a boolean result in response. This makes the API highly flexible as filtering can be done any way you want without the need to change the FilterableComboBoxModel in any way. It also means you can change the filter been used by simply applying a new one...

If, like I usually do, you want to pass some value to the filter, you will need to inform the model that the filter has changed, via the updateFilter method...yeah, I know, a ChangeListener would probably be a better idea, but I was trying to keep it simple ;)

For flexibility (and to maintain the current inheritance model), the core API is based on a ListModel, which means, you can also use the same concept with a JList

FilterableListModel

import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractListModel;
import javax.swing.ListModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;

public class FilterableListModel<E> extends AbstractListModel<E> implements ListDataListener {

    private ListModel<E> peer;
    private List<Integer> lstFilteredIndicies;
    private IFilterable filter;

    public FilterableListModel() {
        lstFilteredIndicies = new ArrayList<Integer>(25);
    }

    public FilterableListModel(ListModel<E> model) {
        this();
        setModel(model);
    }

    public FilterableListModel(ListModel<E> model, IFilterable filter) {
        this();

        setModel(model);
        setFilter(filter);
    }

    public void setModel(ListModel<E> parent) {
        if (peer == null || !peer.equals(parent)) {
            if (peer != null) {
                fireIntervalRemoved(this, 0, peer.getSize() - 1);
                peer.removeListDataListener(this);
            }

            peer = parent;
            lstFilteredIndicies.clear();
            if (peer != null) {
                peer.addListDataListener(this);
            }
            filterModel(true);
        }
    }

    public ListModel<E> getModel() {
        return peer;
    }

    @Override
    public int getSize() {
        IFilterable filter = getFilter();
        return filter == null ? getModel() == null ? 0 : getModel().getSize() : lstFilteredIndicies.size();
    }

    @Override
    public E getElementAt(int index) {
        IFilterable filter = getFilter();
        ListModel<E> model = getModel();

        E value = null;
        if (filter == null) {
            if (model != null) {
                value = model.getElementAt(index);
            }
        } else {
            int filterIndex = lstFilteredIndicies.get(index);
            value = model.getElementAt(filterIndex);
        }
        return value;
    }

    public int indexOf(Object value) {
        int index = -1;
        for (int loop = 0; loop < getSize(); loop++) {
            Object at = getElementAt(loop);
            if (at == value) {
                index = loop;
                break;
            }
        }
        return index;
    }

    @Override
    public void intervalAdded(ListDataEvent e) {
        IFilterable filter = getFilter();
        ListModel model = getModel();

        if (model != null) {
            if (filter != null) {
                int startIndex = Math.min(e.getIndex0(), e.getIndex1());
                int endIndex = Math.max(e.getIndex0(), e.getIndex1());
                for (int index = startIndex; index <= endIndex; index++) {
                    Object value = model.getElementAt(index);
                    if (filter.include(value)) {
                        lstFilteredIndicies.add(index);
                        int modelIndex = lstFilteredIndicies.indexOf(index);
                        fireIntervalAdded(this, modelIndex, modelIndex);
                    }
                }
            } else {
                fireIntervalAdded(this, e.getIndex0(), e.getIndex1());
            }
        }
    }

    @Override
    public void intervalRemoved(ListDataEvent e) {
        IFilterable filter = getFilter();
        ListModel model = getModel();

        if (model != null) {
            if (filter != null) {
                int oldRange = lstFilteredIndicies.size();
                filterModel(false);
                fireIntervalRemoved(this, 0, oldRange);
                if (lstFilteredIndicies.size() > 0) {
                    fireIntervalAdded(this, 0, lstFilteredIndicies.size());
                }
            } else {
                fireIntervalRemoved(this, e.getIndex0(), e.getIndex1());
            }
        }
    }

    @Override
    public void contentsChanged(ListDataEvent e) {
        filterModel(true);
    }

    public void setFilter(IFilterable<E> value) {
        if (filter == null || !filter.equals(value)) {
            filter = value;
            if (getModel() != null) {
                if (getModel().getSize() > 0) {
                    fireIntervalRemoved(this, 0, getModel().getSize() - 1);
                }
            }
            filterModel(true);
        }
    }

    public IFilterable<E> getFilter() {
        return filter;
    }

    protected void filterModel(boolean fireEvent) {
        if (getSize() > 0 && fireEvent) {
            fireIntervalRemoved(this, 0, getSize() - 1);
        }
        lstFilteredIndicies.clear();

        IFilterable<E> filter = getFilter();
        ListModel<E> model = getModel();
        if (filter != null && model != null) {
            for (int index = 0; index < model.getSize(); index++) {
                E value = model.getElementAt(index);
                if (filter.include(value)) {
                    lstFilteredIndicies.add(index);
                    if (fireEvent) {
                        fireIntervalAdded(this, getSize() - 1, getSize() - 1);
                    }
                }
            }
        }
    }

    public void updateFilter() {
        IFilterable filter = getFilter();
        ListModel model = getModel();

        if (filter != null && model != null) {
            for (int index = 0; index < model.getSize(); index++) {
                Object value = model.getElementAt(index);
                if (filter.include(value)) {
                    if (!lstFilteredIndicies.contains(index)) {
                        lstFilteredIndicies.add(index);
                        fireIntervalAdded(this, getSize() - 1, getSize() - 1);
                    }
                } else if (lstFilteredIndicies.contains(index)) {
                    int oldIndex = lstFilteredIndicies.indexOf(index);
                    lstFilteredIndicies.remove(oldIndex);
                    fireIntervalRemoved(this, oldIndex, oldIndex);
                }
            }
        }
    }
}

Filterable

public interface IFilterable<O> {

    public boolean include(O value);

}

FilterableComboBoxModel

import javax.swing.ComboBoxModel;

public class FilterableComboBoxModel<E> extends FilterableListModel<E> implements ComboBoxModel<E> {

    private FilterableComboBoxModel(ComboBoxModel<E> model) {
        super(model);
    }

    public ComboBoxModel<E> getComboBoxModel() {
        return (ComboBoxModel) getModel();
    }

    @Override
    public void setSelectedItem(Object anItem) {
        getComboBoxModel().setSelectedItem(anItem);
    }

    @Override
    public Object getSelectedItem() {
        return getComboBoxModel().getSelectedItem();
    }

}

It should be noted that it might actually be possible to use a RowFilter instead, but I've never really had the time to look at it (since I already had a working API)