【问题标题】:JScrollPane Not Wide Enough When Vertical Scrollbar Appears出现垂直滚动条时 JScrollPane 不够宽
【发布时间】:2012-07-20 04:46:37
【问题描述】:

我在同一个窗口中有两个 JScrollPanes。左边的那个足够大,可以显示包含面板的内容。右边那个不够大,无法显示内容,所以需要创建一个垂直滚动条。

但是如你所见,问题是当垂直滚动条出现时,滚动条出现在JScrollPane的里面。它覆盖了里面包含的内容,因此需要一个水平滚动条来显示所有内容。我想把它修好。

我意识到我可以一直打开垂直滚动条,但出于审美原因,我只希望它在必要时出现,而不需要出现水平滚动窗格。

编辑:我的启动代码非常简单:

JScrollPane groupPanelScroller = new JScrollPane(groupPanel);
this.add(groupPanelScroller, "align center");

我正在使用 MigLayout (MigLayout.com),但无论我使用什么布局管理器,这个问题似乎都会出现。此外,如果我缩小窗口以使左侧面板不再大到足以显示所有内容,则会发生与右侧面板相同的行为。

【问题讨论】:

  • 我尝试调用 setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)),但它只是在内容周围做了一个非常大的边框,并没有阻止水平滚动条的出现.
  • 你必须知道的一件事是当垂直滚动条出现时你希望它如何表现。要么内容需要收缩(然后在内容的 LayoutManager 上工作),要么容器需要增长(然后在 Container 的 LayoutManager 上工作。
  • 我想我希望容器能够增长。

标签: java swing jscrollpane


【解决方案1】:

首先:never-ever tweak the sizing hints 在组件级别。尤其是当您拥有强大的 LayoutManager(例如 MigLayout)时,情况并非如此,它支持在管理器级别进行调整。

在代码中,调整首选项的大小:

// calculate some width to add to pref, f.i. to take the scrollbar width into account
final JScrollPane pane = new JScrollPane(comp);
int prefBarWidth = pane.getVerticalScrollBar().getPreferredSize().width;
// **do not**  
comp.setPreferredSize(new Dimension(comp.getPreferredSize().width + prefBarWidth, ...);
// **do**
String pref = "(pref+" + prefBarWidth + "px)"; 
content.add(pane, "width " + pref);

也就是说:基本上,您在 ScrollPaneLayout 中遇到了一个(有争议的)错误。虽然看起来考虑了滚动条的宽度,但实际上并非在所有情况下都如此。来自preferredLayoutSize的相关sn-p

// filling the sizes used for calculating the pref
Dimension extentSize = null;
Dimension viewSize = null;
Component view = null;

if (viewport != null) {
    extentSize = viewport.getPreferredSize();
    view = viewport.getView();
    if (view != null) {
        viewSize = view.getPreferredSize();
    } else {
        viewSize = new Dimension(0, 0);
    }
}

....

// the part trying to take the scrollbar width into account

if ((vsb != null) && (vsbPolicy != VERTICAL_SCROLLBAR_NEVER)) {
    if (vsbPolicy == VERTICAL_SCROLLBAR_ALWAYS) {
        prefWidth += vsb.getPreferredSize().width;
    }
    else if ((viewSize != null) && (extentSize != null)) {
        boolean canScroll = true;
        if (view instanceof Scrollable) {
            canScroll = !((Scrollable)view).getScrollableTracksViewportHeight();
        }
        if (canScroll && 
            // following condition is the **culprit** 
            (viewSize.height > extentSize.height)) {
            prefWidth += vsb.getPreferredSize().width;
        }
    }
}

这是罪魁祸首,因为

  • 它将视图首选项与视口首选项进行比较
  • 它们大部分时间都是一样的

结果就是您所看到的:滚动条与视图重叠(在切断一些宽度的意义上)。

一个 hack 是一个自定义 ScrollPaneLayout,如果视图的高度小于 实际 视口高度,它会添加滚动条宽度,这是一个粗略的例子(注意:不是生产质量) 一起玩

public static class MyScrollPaneLayout extends ScrollPaneLayout {

    @Override
    public Dimension preferredLayoutSize(Container parent) {
        Dimension dim =  super.preferredLayoutSize(parent);
        JScrollPane pane = (JScrollPane) parent;
        Component comp = pane.getViewport().getView();
        Dimension viewPref = comp.getPreferredSize();
        Dimension port = pane.getViewport().getExtentSize();
        // **Edit 2** changed condition to <= to prevent jumping
        if (port.height < viewPref.height) {
            dim.width += pane.getVerticalScrollBar().getPreferredSize().width;
        }
        return dim;
    }

}

编辑

hmm ...看到跳跃(在显示与不显示垂直滚动条之间,如评论中所述):当我用另一个滚动窗格替换示例中的文本字段时,然后调整“接近”其首选项宽度的大小显示问题。所以hack不够好,可能是询问视口范围的时间不正确(在布局过程的中间)。目前不知道如何做得更好。

编辑 2

试探性跟踪:当逐像素改变宽度时,感觉就像一次性错误。将条件从 &lt; 更改为 &lt;= 似乎可以解决跳跃问题 - 以始终添加滚动条宽度为代价。所以总的来说,这导致第一步的尾随插图更宽;-) 同时认为 scollLlayout 的整个逻辑需要改进......

总结您的选择:

  • 调整 (MigLayout) 组件约束中的首选项宽度。这是最简单的,缺点是在滚动条不显示的情况下额外的尾随空格
  • 修复scrollPaneLayout。需要一些努力和测试(参见核心 ScrollPaneLayout 的代码需要做什么),优点是没有滚动条的一致填充
  • 不是一个选项在组件上手动设置首选项宽度

下面是代码示例:

// adjust the pref width in the component constraint
MigLayout layout = new MigLayout("wrap 2", "[][]");
final JComponent comp = new JPanel(layout);
for (int i = 0; i < 10; i++) {
    comp.add(new JLabel("some item: "));
    comp.add(new JTextField(i + 5));
}

MigLayout outer = new MigLayout("wrap 2", 
        "[][grow, fill]");
JComponent content = new JPanel(outer);
final JScrollPane pane = new JScrollPane(comp);
int prefBarWidth = pane.getVerticalScrollBar().getPreferredSize().width;
String pref = "(pref+" + prefBarWidth + "px)";
content.add(pane, "width " + pref);
content.add(new JTextField("some dummy") );
Action action = new AbstractAction("add row") {

    @Override
    public void actionPerformed(ActionEvent e) {
        int count = (comp.getComponentCount() +1)/ 2;
        comp.add(new JLabel("some Item: "));
        comp.add(new JTextField(count + 5));
        pane.getParent().revalidate();
    }
};
frame.add(new JButton(action), BorderLayout.SOUTH);
frame.add(content);
frame.pack();
frame.setSize(frame.getWidth()*2, frame.getHeight());
frame.setVisible(true);

// use a custom ScrollPaneLayout
MigLayout layout = new MigLayout("wrap 2", "[][]");
final JComponent comp = new JPanel(layout);
for (int i = 0; i < 10; i++) {
    comp.add(new JLabel("some item: "));
    comp.add(new JTextField(i + 5));
}

MigLayout outer = new MigLayout("wrap 2", 
        "[][grow, fill]");
JComponent content = new JPanel(outer);
final JScrollPane pane = new JScrollPane(comp);
pane.setLayout(new MyScrollPaneLayout());
content.add(pane);
content.add(new JTextField("some dummy") );
Action action = new AbstractAction("add row") {

    @Override
    public void actionPerformed(ActionEvent e) {
        int count = (comp.getComponentCount() +1)/ 2;
        comp.add(new JLabel("some Item: "));
        comp.add(new JTextField(count + 5));
        pane.getParent().revalidate();
    }
};
frame.add(new JButton(action), BorderLayout.SOUTH);
frame.add(content);
frame.pack();
frame.setSize(frame.getWidth()*2, frame.getHeight());
frame.setVisible(true);

【讨论】:

  • 感谢您提供非常详细的解决方案。除了必须禁用水平滚动条之外,我没有看到使用 insets 或 prefs 来创建足够的空间来容纳重叠滚动条之间的任何区别,所以这可能只是个人喜好问题。我喜欢在 JScrollPane 中修复这个“错误”的想法,所以我一定会尝试使用它。再次感谢您的详细回复!
  • 刚刚尝试使用您的代码创建我自己的 JScrollPane,并使用重写的 ScrollPaneLayout preferredLayoutSize 方法。它在某些时候有效。当我尝试调整窗口大小时,任何没有可见 VerticalScrollBar 的 JScrollPanes 都会在我们正在寻找的正确大小和错误大小之间快速交替。我可以再玩一些,但如果你有任何想法,我很乐意听到。
  • 经过一些调试,我发现 pane.getViewport().getExtentSize() 的结果变化非常频繁,这似乎导致了我描述的行为。研究如何解决这个问题。
【解决方案2】:

我想通了。问题是 JScrollPane 符合内容的大小,并在滚动条变得可见时与其重叠。所以我决定让内容更宽,以便在滚动条变得可见时使其适应滚动条。这也可以通过将 insets 设置为 Scrollbar 的宽度来完成,这可能是更正统的方式。

int scrollBarWidth = new JScrollPane().getVerticalScrollBar().getPreferredSize();
groupPanel.setLayout(new MigLayout("insets 1 " + scrollBarWidth + " 1 " + scrollBarWidth*1.5); //Adjust multipliers and widths to suit
//...
//Add everything in
//...
JScrollPane groupPanelScroller = new JScrollPane(groupPanel);
groupPanelScroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
this.add(groupPanelScroller, "align center");

编辑:上面的答案可能是更好的答案,但这是我对好奇的原始答案,它实际上调整了面板的大小。

JScrollPane groupPanelScroller = new JScrollPane(groupPanel);
groupPanel.setPreferredSize(new Dimension(groupPanel.getPreferredSize().width + groupPanelScroller.getVerticalScrollBar().getVisibleAmount()*2, 
        groupPanel.getPreferredSize().height));
groupPanelScroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
this.add(groupPanelScroller, "align center");

那么它是做什么的,它使内容的大小相同,加上滚动条宽度的两倍。由于所有内容都居中,因此看起来都一样,然后如果滚动条出现,则一侧的一些额外空间会被填充。

【讨论】:

  • +1 将您的解决方案发送到这里的好主意,请您接受您的帖子(2 天???)
  • 并且不 - visibleAmount 不是您要调整宽度的属性.. 并且不:如果首选项宽度由任何一个调整,则无需禁用水平滚动条的显示我建议的选项
  • 啊,我看到垂直滚动条上的 getPreferredSize() 可能是比 getVisibleAmount() 更好的获取宽度的方法。我已经改变了。没错,这种方法确实会导致禁用水平滚动条,这可能并不适合所有人。
【解决方案3】:

我也遇到了同样的问题,经过数小时的努力寻找原因后来到这里。我最终实现了自己的 ViewportLayout,也想分享该解决方案。

根本问题是,ScrollPaneLayout 不知道一个维度(在您的情况下是视图的高度)将被限制为最大值,因此它无法预先确定是否需要滚动条。因此它不能调整另一个灵活的尺寸(在你的情况下是宽度)。

自定义 ViewportLayout 可以考虑到这一点,向其父级询问允许的高度,从而允许 ScrollPaneLayout 相应地调整首选宽度:

public class ConstrainedViewPortLayout extends ViewportLayout {

    @Override
    public Dimension preferredLayoutSize(Container parent) {

        Dimension preferredViewSize = super.preferredLayoutSize(parent);

        Container viewportContainer = parent.getParent();
        if (viewportContainer != null) {
            Dimension parentSize = viewportContainer.getSize();
            preferredViewSize.height = parentSize.height;
        }

        return preferredViewSize;
    }
}

ViewportLayout 是这样设置的:

scrollPane.getViewport().setLayout(new ConstrainedViewPortLayout());

【讨论】:

    【解决方案4】:

    这个错误已经存在好几年了,但仍未修复,所以我将添加我的贡献。

    您还可以通过在 JScrollPane 的视图组件上实现 javax.swing.Scrollable 来解决该错误。 ScrollPaneLayout 检查此接口的方法以确定是否首先显示水平滚动条,然后再返回其错误的计算。

    您可以实现 Scrollable 接口,以便视图组件与实现 Scrollable 之前完全相同,除了用于确定是否显示水平滚动条的方法。

    一个具体的例子来证明这一点,我没有单独测试过,而是从我们的代码库中提取的。变化:

    JPanel panel = new JPanel();
    JScrollPane pane = new JScrollPane(panel);
    

    到

    class ScrollableJPanel extends JPanel implements Scrollable {
        @Override public Dimension getPreferredScrollableViewportSize() {
            return getPreferredSize();
        }
    
        @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
            JScrollPane scrollPane = (JScrollPane)getParent();
            return orientation == SwingConstants.VERTICAL ? scrollPane.getVerticalScrollBar().getUnitIncrement() : scrollPane.getHorizontalScrollBar().getUnitIncrement();
        }
    
        @Override public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
            JScrollPane scrollPane = (JScrollPane)getParent();
            return orientation == SwingConstants.VERTICAL ? scrollPane.getVerticalScrollBar().getBlockIncrement() : scrollPane.getHorizontalScrollBar().getBlockIncrement();
        }
    
        @Override public boolean getScrollableTracksViewportHeight() {
            return false;
        }
    
        @Override public boolean getScrollableTracksViewportWidth() {
            Dimension sz = getSize();
            Dimension vsz = getViewportSize();
            return sz.width - 1 <= vsz.width; // This is the bodge
        }
    }
    
    JPanel panel = new ScrollableJPanel();
    JScrollPane pane = new JScrollPane();
    

    在针对经过修改以打印大量调试输出的 Java 1.8 rt.jar 测试我们的应用程序时,我们发现该值仅超出了一个像素 - 这就是我们在上述 bodge 中使用的常量。

    顺便提一下,这已提交给 Oracle https://bugs.openjdk.java.net/browse/JDK-8042020,但它被标记为“不会修复”。

    【讨论】:

      【解决方案5】:
      • 这张图片谈论的是GridLayout,两个部分的大小相同

      • 使用正确的LayoutManager,接受不同的PreferredSize、BoxLayout或GridBagLayout

      【讨论】:

      • 我实际上在使用 MigLayout (miglayout.com)。对不起,应该先提一下。但是,无论我使用什么布局,它似乎都会发生。
      • 对不起,我不完全确定你在问什么。您是在问我是否已将这个问题发布到 SSCCE?那是什么?
      • 啊,好吧。我不久前编辑了原始帖子以包含代码。尽管如此,无论我是否使用 MigLayout,都会发生这种行为。如果我调整窗口大小,使其太小而无法在左侧面板中显示内容,则会出现与右侧面板相同的行为。
      • -1 表示功能较弱 - 与 Mig 相比 - LayoutManagers 不能解决问题。实际上,行为不端的不是任何应用程序代码管理器,而是 ScrollPaneLayout ;-)
      【解决方案6】:

      好的,我遇到了完全相同的问题:

      • 将(子)面板动态添加到(主)面板中,该面板被放入 jscrollpane(我想创建一个垂直的面板列表)
      • jscrollpane 的垂直滚动条与我的内容重叠 (即我的子面板)。

      我尝试了很多在互联网(和 stackoverflow)上找到的解决方法,但没有真正的解决方案!

      这是我的理解,以及我的工作解决方案:

      • scrollpanelayout(jscrollpane 使用的布局管理器)中似乎存在错误,无法正确计算其中的组件大小。

      • 如果你放mainpanel.setPreferedSize(new Dimension(1, 1)),就不会再有重叠的bug,但是垂直滚动条(如果放为VERTICAL_SCROLLBAR_​​ALWAYS的事件)不会滚动!这是因为滚动窗格认为您的内容是 1 像素高度。

      • 所以只需遍历您的子面板并计算适当的高度:

      //into your constructor or somewhere else
      JPanel mainpanel = new JPanel();
      mainpanel.setLayout(new BoxLayout(mainpanel, BoxLayout.Y_AXIS));
      JScrollPane scrollPane = new JScrollPane(mainpanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
      
      
      //into another function to create dynamic content
      int height = 0;
      for (int i = 0; i < nb_subpanel_to_create; ++i) {
          JPanel subpanel = create_your_sub_panel();
          height += subpanel.getPreferedSize().height;
          mainpanel.add(subpanel);
      }
      mainpanel.setPreferedSize(new Dimension(1, height);
      

      奖励:

      如果您希望“列表”中的内容堆栈到顶部:

      List<JComponent> cmps = new LinkedList<JComponent>();
      int height = 0;
      for (int i = 0; i < nb_subpanel_to_create; ++i) {
          JPanel subpanel = create_your_sub_panel();
          height += subpanel.getPreferedSize().height;
          cmps.add(subpanel);
      }
      mainpanel.add(stackNorth(subpanel));
      mainpanel.setPreferedSize(new Dimension(1, height);
      

      还有stackNorth函数:

      public static JPanel stackNorth(List<JComponent> components) {
           JPanel last = new JPanel(new BorderLayout());
           JPanel first = last;
           for (JComponent jComponent : components) {
                last.add(jComponent, BorderLayout.NORTH);
                JPanel tmp = new JPanel(new BorderLayout());
                last.add(tmp, BorderLayout.CENTER);
                last = tmp;
           }
           return first;
      } 
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-03-22
        • 2012-04-08
        • 2012-11-08
        • 2016-09-03
        相关资源
        最近更新 更多