Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JTable#scrollRectToVisible in combination with JSplitPlane shows the wrong row

When I call JTable#scrollRectToVisible, the row I want to show is hidden underneath the header in certain situations.

The rest of this question only makes sense when using the following code. This is a very simply program which I use to illustrate the problem. It shows a UI containing a JSplitPane with in the upper part some control buttons, and the lower part contains a JTable wrapped in a JScrollPane (see screenshots at the bottom of this post).

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;

import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;

public class DividerTest {

  private final JSplitPane fSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
  private final JTable fTable;
  private final JScrollPane fScrollPane;

  private boolean fHideTable = false;

  public DividerTest() {
    fTable = new JTable( createTableModel(50));
    fScrollPane = new JScrollPane(fTable);
    fSplitPane.setBottomComponent(fScrollPane);
    fSplitPane.setTopComponent(createControlsPanel());
    fSplitPane.setDividerLocation(0.5);
  }

  private JPanel createControlsPanel(){
    JPanel result = new JPanel();
    result.setLayout(new BoxLayout(result, BoxLayout.PAGE_AXIS));

    final JCheckBox checkBox = new JCheckBox("Make table invisible before adjusting divider");
    checkBox.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        fHideTable = checkBox.isSelected();
      }
    });
    result.add(checkBox);

    JButton upperRow = new JButton("Select row 10");
    upperRow.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        selectRowInTableAndScroll(10);
      }
    });
    result.add(upperRow);

    JButton lowerRow = new JButton("Select row 45");
    lowerRow.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        selectRowInTableAndScroll(45);
      }
    });
    result.add(lowerRow);

    JButton hideBottom = new JButton("Hide bottom");
    hideBottom.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        if (fHideTable) {
          fScrollPane.setVisible(false);
        }
        fSplitPane.setDividerLocation(1.0);
      }
    });
    result.add(hideBottom);

    JButton showBottom = new JButton("Show bottom");
    showBottom.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        fScrollPane.setVisible(true);
        fSplitPane.setDividerLocation(0.5);
      }
    });
    result.add(showBottom);

    return result;
  }

  private void selectRowInTableAndScroll( int aRowIndex ){
    fTable.clearSelection();
    fTable.getSelectionModel().addSelectionInterval(aRowIndex, aRowIndex);
    fTable.scrollRectToVisible(fTable.getCellRect(aRowIndex, 0, true));
  }

  public JComponent getUI(){
    return fSplitPane;
  }

  private TableModel createTableModel(int aNumberOfRows){
    Object[][] data = new Object[aNumberOfRows][1];
    for( int i = 0; i < aNumberOfRows; i++ ){
      data[i] = new String[]{"Row" + i};
    }
    return new DefaultTableModel(data, new String[]{"Column"});
  }

  public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
      @Override
      public void run() {
        JFrame frame = new JFrame("Test frame");

        frame.getContentPane().add(new DividerTest().getUI());
        frame.pack();
        frame.setVisible(true);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
      }
    });
  }
}

Unwanted behavior

  • Run the above code
  • Press the "Select row 10": row 10 is selected and visible
  • Press the "Select row 45": row 45 is selected and visible
  • Click the "Hide bottom" button. This will adjust the divider of the JSplitPane so that only the upper panel is visible
  • Click the "Select row 10" button. You see of course nothing because the table is not yet visible
  • Click the "Show bottom" button. The divider is adjusted, but row 10 is hidden underneath the header. I expected it to be visible without needing to scroll.

Wanted behavior

Repeat the steps from above, but make sure the "Make table invisible before adjusting divider" checkbox is selected. This will call setVisible(false) on the JScrollPane around the JTable before hiding the bottom panel.

By doing this, in the last step row 10 will be visible as the top most row, which is what I want. I just do not want to turn the scrollpane invisible: in my real application, the divider is adjusted in an animated way and as such you want to keep the table visible during the animation.

Screenshots

Unwanted: row 10 is invisible after performing the aforementioned steps

Unwanted behavior screenshot

Wanted: row 10 is visible after performing the aforementioned steps

Wanted behavior screenshot

Environment

I do not think it will matter, but just in case: I am using JDK7 on a Linux system.

like image 362
Robin Avatar asked Aug 05 '15 13:08

Robin


People also ask

What is JTable used for?

The JTable is used to display and edit regular two-dimensional tables of cells. See How to Use Tables in The Java Tutorial for task-oriented documentation and examples of using JTable .

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);

What is JTable in Netbeans?

The JTable class is used to display data in tabular form. It is composed of rows and columns.


1 Answers

This seems to be caused by the way how the JViewport handles the scrollRectToVisible calls for the cases that its size is smaller than the desired rectangle. It contains a (somewhat fuzzy, but probably related) comment in the JavaDocs:

Note that this method will not scroll outside of the valid viewport; for example, if contentRect is larger than the viewport, scrolling will be confined to the viewport's bounds.

I did not go though the complete code and do all the maths and check all the cases. So a warning: The following explainations contain quite same hand-waving. But a simplified description of what this means for me in this particular case:

When the bottom part is hidden (by setting the divider location accordingly), then this height of the JScrollPane and its JViewport is 0. Now, when requesting to scrollRectToVisible with a rectangle that has a height of 20 (for one table row, as an example), then it will notice that this does not fit. Depending on the current view position of the JViewport, this may cause to viewport to be scrolled so that the bottom of this rectangle is visible.

(You can observe this: Drag the divider location manually, so that approximately half of one table row is visible. When clicking the "Select row 45" button, the upper half of the row will be visible. When clicking the "Select row 10" button, then the lower half of the row will be visible)

One pragmatic solution here that seemed to work for me was to make sure that it will always scroll so that the top of the rectangle is visible (even when the rectangle does not at all fit into the viewport!). Like this:

private void selectRowInTableAndScroll(int aRowIndex)
{
    fTable.clearSelection();
    fTable.getSelectionModel().addSelectionInterval(aRowIndex, aRowIndex);

    Rectangle r = fTable.getCellRect(aRowIndex, 0, true);
    r.height = Math.min(fScrollPane.getHeight(), r.height);
    fTable.scrollRectToVisible(r);
}

But I can't promise that this will have the desired effect for you, when an animation comes into play...

like image 81
Marco13 Avatar answered Oct 19 '22 06:10

Marco13