JTable#scrollRectToVisible 结合 JSplitPlane 显示错误的行

JTable#scrollRectToVisible in combination with JSplitPlane shows the wrong row

当我调用 JTable#scrollRectToVisible 时,在某些情况下,我要显示的行隐藏在 header 下方。

这个问题的其余部分只有在使用以下代码时才有意义。这是一个非常简单的程序,我用它来说明问题。它显示了一个 UI,其中包含一个 JSplitPane,上半部分带有一些控制按钮,下半部分包含一个 JTable,包裹在一个 JScrollPane 中(请参阅底部的屏幕截图这个 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);
      }
    });
  }
}

不需要的行为

想要的行为

重复上述步骤,但确保选中 "Make table invisible before adjusting divider" 复选框。这将在隐藏底部面板之前在 JTable 周围的 JScrollPane 上调用 setVisible(false)

通过这样做,在最后一步中,第 10 行将显示为最顶行,这正是我想要的。 我只是不想让滚动窗格不可见:在我的实际应用程序中,分隔线以动画方式调整,因此您希望在 table 期间保持可见动画。

截图

不需要:执行上述步骤后第 10 行不可见

需要:执行上述步骤后第 10 行可见

环境

我认为这无关紧要,但以防万一:我在 Linux 系统上使用 JDK7。

不太确定 scrollRectToVisible() 在做什么。

您或许可以使用 JViewport.setViewPosition(...) 方法。

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

在这种情况下,selected 行将始终显示在视口的顶部(如果可能)。因此视口将始终滚动,除非 selected 行当前位于顶部。如果第一行位于视口的顶部并且您 select 第 10 行,视口将滚动以在顶部显示第 10 行,则使用此方法。

但是,此行为与使用 scrollRectToVisible() 方法略有不同。当使用 scrollRectToVisible() 方法时,仅当矩形不在视口的可见部分时才滚动视口。如果第一行位于视口的顶部并且您 select 第 10 行,则使用此方法视口将不会滚动,因为第 10 行已经在视口中可见。

不知道这种功能上的变化是否可以接受。

请注意,如果您不想让视口在 select 行时自动滚动,您可以尝试类似的操作:

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

这似乎是由 JViewport 处理 scrollRectToVisible 调用的方式引起的,因为它的大小小于所需的矩形。它在 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.

我没有仔细阅读完整的代码,也没有做所有的数学计算并检查所有的案例。所以警告:以下解释包含完全相同的挥手。但是在这个特殊情况下这对我意味着什么的简单描述:

当底部隐藏时(通过相应设置分隔符位置),则 JScrollPane 及其 JViewport 的高度为 0。现在,当请求 scrollRectToVisible使用高度为 20 的矩形(例如,对于一个 table 行),然后它会注意到这不适合。根据 JViewport 的当前视图位置,这可能会导致视口滚动,以便此矩形的 底部 可见。

(您可以观察到这一点:手动拖动分隔线位置 ,这样 table 行的大约 一半 可见。单击 "Select row 45" 按钮时,行的 上半部分 将可见。单击 "Select row 10" 按钮时,行的 下半部分将可见 一半的行将可见)

这里似乎对我有用的一个实用解决方案是确保它始终滚动,以便矩形的 top 可见(即使矩形不可见)完全适合视口!)。像这样:

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

但我不能保证当 动画 开始播放时,这会对您产生预期的效果...