8

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.

Robin
  • 36,233
  • 5
  • 47
  • 99
  • please whats output by using [Rectangle from JVievport and together from JTables row](http://stackoverflow.com/a/7052751/714968), because ignores paints cache, and whats return value from Rectangle.contains(), because this could be painting artefact, as aside the divider should be wrapped into invokeLater in all cases (animation, the same as required for animation with/in JTree) – mKorbel Aug 05 '15 at 14:12

2 Answers2

3

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...

Marco13
  • 53,703
  • 9
  • 80
  • 159
  • (1+) good observation. I had actually tried setting the height of the rectangle to the height of the viewport, which fixed the problem when the scroll pane was hidden, but of course it forced the selected row to always go to the top when the scroll pane was visible, At least I know why it worked when the scroll pane was hidden, because the height of the rectangle matched the height of the viewport. – camickr Aug 06 '15 at 16:48
  • It was accepted, but still wonder whether this always works as desired, even when the animation is involved.... – Marco13 Aug 06 '15 at 17:44
2

Not exactly sure what the scrollRectToVisible() is doing.

You might be able to use the JViewport.setViewPosition(...) method.

Rectangle r = fTable.getCellRect(aRowIndex, 0, true);
Point p = new Point(r.x, r.y);
fScrollPane.getViewport().setViewPosition( p );

In this case the selected row will always be shown at the top of the viewport (if possible). So the viewport will always scroll unless the selected row is current at the top. Using this approach if the first row is at the top of the viewport and you select the 10th row the viewport will scroll to display the 10th row at the top.

However, this behaviour is slightly different than using the scrollRectToVisible() method. When using the scrollRectToVisible() method the viewport when only scrolled when the rectangle is not in the visible part of the viewport. Using this approach if the first row is at the top of the viewport and you select the 10th row the viewport will NOT scroll since the 10th row is already visible in the viewport.

Don't know if this change in functionality is acceptable or not.

Note if you don't want to viewport to automatically scroll when you select a row you could try something like:

JViewport viewport = fScrollPane.getViewport();
Rectangle viewRect = viewport.getViewRect();
Rectangle r = fTable.getCellRect(aRowIndex, 0, true);
Point p = new Point(r.x, r.y);

if (! viewRect.contains(p))
    viewport.setViewPosition( p );
camickr
  • 321,443
  • 19
  • 166
  • 288