Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does JTable RowFilter work?

I'm try to create a Row filter for a JTable to limit the number of rows displayed in the table.

The RowFilter code is simple. It converts the model row number to the view row number (in case the table is sorted) and then checks if the view row number is less that the number of lines to be displayed in the table:

RowFilter<TableModel, Integer> filter = new RowFilter<TableModel, Integer>()
{
    @Override
    public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry)
    {
        int modelRow = entry.getIdentifier();
        int viewRow = table.convertRowIndexToView(modelRow);

        return viewRow < numberOfRows;
    }

};

The problem is that the model row number is not always converted to a reasonable view row number so too many rows are being included in the filter. To demonstrate run the code below:

1) Select "1" from the combo box and you will get output like:

Change the Filter to: 1
m0 : v0
m1 : v0
m2 : v0
m3 : v0
m4 : v0

This output is telling me that all model rows are being converted to view row 0. Since 0 is less than the filter value of 1, all rows are included in the filter (which is wrong).

So the question here is why is the convertRowIndexToView(modelRow) not working as expected?

2) Now select "2" from the combo box and you will get output like:

Change the Filter to: 2
m0 : v0
m1 : v1
m2 : v2
m3 : v3
m4 : v4

As you can see the model rows are now mapping to the proper view row, so only 2 row are included in the filter which is correct.

3) Now select "3" from the combo box and you will get output like:

Change the Filter to: 3
m0 : v0
m1 : v1
m2 : v-1
m3 : v-1
m4 : v-1

In this case the last 3 model rows are converted to -1, which I assume means the row is not currently visible in the table, which is correct. So in this case all 5 rows are again included in the filter which is incorrect since we only want the first 3.

So the question here is how to reset the filter so all model rows are mapped to the original view row?

I tried to use:

((TableRowSorter) table.getRowSorter()).setRowFilter(null);
((TableRowSorter) table.getRowSorter()).setRowFilter(filter);

to clear the filter, before resetting the filter but this gave the same results as step 1. That is, now all the model rows map to view row 0 (so all 5 rows are still displayed).

Here is the test code:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class FilterSSCCE extends JPanel
{
    private JTable table;

    public FilterSSCCE()
    {
        setLayout( new BorderLayout() );

        JComboBox<Integer> comboBox = new JComboBox<Integer>();
        comboBox.addItem( new Integer(1) );
        comboBox.addItem( new Integer(2) );
        comboBox.addItem( new Integer(3) );
        comboBox.addItem( new Integer(4) );
        comboBox.addItem( new Integer(5) );
        comboBox.setSelectedIndex(4);

        comboBox.addActionListener( new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                //System.out.println( table.convertRowIndexToView(4) );
                Integer value = (Integer)comboBox.getSelectedItem();
                newFilter( value );
                //System.out.println( table.convertRowIndexToView(4) );
            }
        });
        add(comboBox, BorderLayout.NORTH);

        table = new JTable(5, 1);

        for (int i = 0; i < table.getRowCount(); i++)
            table.setValueAt(String.valueOf(i+1), i, 0);

        table.setAutoCreateRowSorter(true);
        JScrollPane scrollPane = new JScrollPane(table);
        add(scrollPane, BorderLayout.CENTER);
    }

    private void newFilter(int numberOfRows)
    {
        System.out.println("Change the Filter to: " + numberOfRows);

        RowFilter<TableModel, Integer> filter = new RowFilter<TableModel, Integer>()
        {
            @Override
            public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry)
            {
                int modelRow = entry.getIdentifier();
                int viewRow = table.convertRowIndexToView(modelRow);

                System.out.println("m" + modelRow + " : v" + viewRow);

                return viewRow < numberOfRows;
            }

        };

        ((TableRowSorter) table.getRowSorter()).setRowFilter(filter);
    }

    private static void createAndShowGUI()
    {
        JPanel panel = new JPanel();

        JFrame frame = new JFrame("FilterSSCCE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new FilterSSCCE());
        frame.setLocationByPlatform( true );
        frame.pack();
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
}

Any idea how to create a row filter to display the first "n" rows?

Oh yeah, one more frustrating point. If you uncomment the two System.out.. lines in the actionPeformed() method, when you select 1 from the combo box you will notice that in both cases the model index 4 is converted to view index 4 and these two outputs are sandwiched around the incorrect model to view conversions???

Edit:

Based on MadProgrammers suggestion I tried:

//((TableRowSorter) table.getRowSorter()).setRowFilter(filter);
TableRowSorter sorter = new TableRowSorter();
table.setRowSorter( sorter );
sorter.setRowFilter( filter );
sorter.sort();

Now I get nothing in the table.

like image 692
camickr Avatar asked May 26 '15 01:05

camickr


People also ask

How does a JTable work?

The JTable class is a part of Java Swing Package and is generally used to display or edit two-dimensional data that is having both rows and columns. It is similar to a spreadsheet. This arranges data in a tabular form.

What is JTable model?

The TableModel interface specifies the methods the JTable will use to interrogate a tabular data model. The JTable can be set up to display any data model which implements the TableModel interface with a couple of lines of code: TableModel myData = new MyTableModel(); JTable table = new JTable(myData);

How do I search for a JTable?

We can implement the search functionality of a JTable by input a string in the JTextField, it can search for a string available in a JTable. If the string matches it can only display the corresponding value in a JTable. We can use the DocumentListener interface of a JTextField to implement it.


2 Answers

So, after some serious testing and debugging, I copied the code for DefaultRowSorter and TableRowSorter into your FilterSSCCE and added some output monitoring the modelToView field, which is used by DefaultRowSorter#convertRowIndexToView to map between the model and view indicies...

Change the Filter to: 1
createModelToView = [0, 0, 0, 0, 0]
m0 : v0
m1 : v0
m2 : v0
m3 : v0
m4 : v0
initializeFilteredMapping.1 = [0, 1, 2, 3, 4]
initializeFilteredMapping.2 = [0, 1, 2, 3, 4]
Change the Filter to: 5
m0 : v0
m1 : v1
m2 : v2
m3 : v3
m4 : v4
initializeFilteredMapping.1 = [0, 1, 2, 3, 4]
initializeFilteredMapping.2 = [0, 1, 2, 3, 4]
Change the Filter to: 1
m0 : v0
m1 : v1
m2 : v2
m3 : v3
m4 : v4
initializeFilteredMapping.1 = [0, -1, -1, -1, -1]
initializeFilteredMapping.2 = [0, -1, -1, -1, -1]
Change the Filter to: 2
m0 : v0
m1 : v-1
m2 : v-1
m3 : v-1
m4 : v-1
initializeFilteredMapping.1 = [0, 1, 2, 3, 4]
initializeFilteredMapping.2 = [0, 1, 2, 3, 4]

The interesting part is here at end, between Change the Filter to: 1 and Change the Filter to: 2. You can see that initializeFilteredMapping has set the model indices that are out of range to -1, but when we change to Change the Filter to: 2, those same indices are still set, changing the filter has NOT reset them.

This seems to be a design choice to keep the table responsive and they probably never thought some one might try and access the view from within the filter, as you're suppose to be using the model data...

How to get around it...?

You could build a "proxy" TableModel, but that precludes the possibility that the table might be sorted.

You could write a "proxy" TableModel which "knew" about the sorted state of the JTable (probably via the RowSorter) and which could act as the filter to determine the visible row count, but this is crossing into murky water as the model is starting to venture into the world of the view...

Another choice would be to change the way that the setFilter method works and reset the modelToView and viewToModel variables, but they are private, as they should be, okay, we could use the createModelToView, createViewToModel and setModelToViewFromViewToModel methods available in the DefaultRowSorter ... but they are private to...

It would seem just about any useful method which deals with serious modification to these variables are private...story of my life...(get your torches and pitchforks, we're going on a dev-hunt)

Next choice, write it ALL yourself...what a wonderful idea, expect that goes against the basic principles of OO...

A "work around" (and I use the term very, very lightly), would be to use reflection and just call the methods we need...

public class TestRowSorter<M extends TableModel> extends TableRowSorter<M> {

    public TestRowSorter() {
    }

    public TestRowSorter(M model) {
        super(model);
    }

    public Method findMethod(String name, Class... lstTypes) {

        return findMethod(getClass(), name, lstTypes);

    }

    public Method findMethod(Class parent, String name, Class... lstTypes) {

        Method method = null;
        try {
            method = parent.getDeclaredMethod(name, lstTypes);
        } catch (NoSuchMethodException noSuchMethodException) {
            try {
                method = parent.getMethod(name, lstTypes);
            } catch (NoSuchMethodException nsm) {
                if (parent.getSuperclass() != null) {
                    method = findMethod(parent.getSuperclass(), name, lstTypes);
                }
            }
        }

        return method;

    }

    @Override
    public void setRowFilter(RowFilter<? super M, ? super Integer> filter) {

        try {
            Method method = findMethod("createModelToView", int.class);
            method.setAccessible(true);
            method.invoke(this, getModelWrapper().getRowCount());

            method = findMethod("createViewToModel", int.class);
            method.setAccessible(true);
            method.invoke(this, getModelWrapper().getRowCount());

            method = findMethod("setModelToViewFromViewToModel", boolean.class);
            method.setAccessible(true);
            method.invoke(this, true);
        } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException exp) {
            exp.printStackTrace();
        }

        super.setRowFilter(filter);
    }

}

Now, I'm pretty sure you know, as I do, this is a horrible, horrible idea which could break at any time. It's probably also very, very inefficient, as you're resetting the indexes to the bi-directional look up each time.

So, the answer, don't access the view from the filter.

Generally speaking, I tend to replace the RowSorter when ever I replace the RowFilter as it avoids these kind of issues :P

like image 60
MadProgrammer Avatar answered Oct 19 '22 00:10

MadProgrammer


Using the tips from @MadProgrammer I came up with the following solution.

Not only does the RowSorter need to be replaced, you also need to keep the sort keys so the sort() method resets the table back to its current sort state:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class FilterSSCCE extends JPanel
{
    private JTable table;

    public FilterSSCCE()
    {
        setLayout( new BorderLayout() );

        JComboBox<Integer> comboBox = new JComboBox<Integer>();
        comboBox.addItem( new Integer(1) );
        comboBox.addItem( new Integer(2) );
        comboBox.addItem( new Integer(3) );
        comboBox.addItem( new Integer(4) );
        comboBox.addItem( new Integer(5) );
        comboBox.setSelectedIndex(4);

        comboBox.addActionListener( new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                Integer value = (Integer)comboBox.getSelectedItem();
                newFilter( value );
            }
        });
        add(comboBox, BorderLayout.NORTH);

        table = new JTable(5, 1);

        for (int i = 0; i < table.getRowCount(); i++)
            table.setValueAt(String.valueOf(i+1), i, 0);

        table.setAutoCreateRowSorter(true);
        JScrollPane scrollPane = new JScrollPane(table);
        add(scrollPane, BorderLayout.CENTER);
    }

    private void newFilter(int numberOfRows)
    {
        RowFilter<TableModel, Integer> filter = new RowFilter<TableModel, Integer>()
        {
            @Override
            public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry)
            {
                int modelRow = entry.getIdentifier();
                int viewRow = table.convertRowIndexToView(modelRow);

                return viewRow < numberOfRows;
            }

        };

        TableRowSorter oldSorter = (TableRowSorter)table.getRowSorter();
        TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());
        table.setRowSorter( sorter );
        sorter.setRowFilter( filter );
        sorter.setSortKeys( oldSorter.getSortKeys() );
        sorter.sort();
    }

    private static void createAndShowGUI()
    {
        JPanel panel = new JPanel();

        JFrame frame = new JFrame("FilterSSCCE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new FilterSSCCE());
        frame.setLocationByPlatform( true );
        frame.pack();
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
}
like image 4
camickr Avatar answered Oct 19 '22 01:10

camickr