如何支持缩放JScrollPane中显示的视图以避免将JScrollBars显示到最小尺寸?

我想要实现的行为是默认情况下以1:1的比例显示视图。 如果其容器变大(由用户动态调整父JFrame的大小),则应缩放视图以适应更大的区域; 如果缩小,视图应缩放以适应较小的区域 – 达到极限。 当小于最小尺寸时,滚动条应与视口一起出现,以支持在最小比例显示的视图周围导航。

我现在有一个运行不良的实现,使用JScrollPane和ComponentListener来确定何时发生resize。 基于新大小,设置新的比例因子以绘制视图以适合(最小比例),设置视图的preferredSize并调用revalidate()。 问题是这会导致“抖动”显示; 当调整超过应用新比例的点以避免显示滚动条时,滚动条会出现,然后消失。 我相信这是因为我的方法是react native的,而不是预测性的。 也就是说,LayoutManager仅查看视图的preferredSize并基于此执行布局。 我正在侦听JScrollPane的大小调整,当它只是太小时才更改视图的preferredSize,然后通过调用revalidate()使容器再次布局。 至少这是我理解它的方式。

我一直在审查JScrollPane,JViewport及其相应的LayoutManagers的源代码。 在这一点上,我正在考虑将一个或多个子类化为更好(预测,导致更平滑的大小调整)实现。这似乎是其他人必须实现的行为。是否有另一种方法来使用现有的Container / LayoutManagers /方法来执行这没有子类化并冒着跨越不同LnF或平台的意外行为的风险?

编辑:我已经攻击了ScrollPaneLayout的原型子类。 它在添加滚动条之前检查视图的最小大小。 这是有效的(因为滚动条在视口小于视图的最小大小之前不会出现,而不是在视口小于视图的首选大小时显示滚动条的原始行为)但是当显示滚动条时,视图会认为它仍然是首选尺寸,而不是最小尺寸。 我也可能不得不破解ViewportLayout类,但这很快就会成为我怀疑会强大的东西。

编辑:

我回到基础并再次尝试了这些建议,并取得了成功。 我尝试重载ScrollPaneLayout和ViewportLayout的默认行为,以及它们之间的交互不是正确的方向。 在我第一次尝试之后,我确信在LayoutManagers完成他们的工作之后,没有办法避免我的“反应”方法的闪烁和不稳定性来修复不正确的大小。 幸运的是,有一种方法可以在没有子类化LayoutManager的情况下完成这项工作 – 如上所述,它是通过实现Scrollable接口(我之前已经正确完成,但没有进行其他更改以使其工作)。 诀窍的确是实现getScrollableTracksViewport(),使其根据视口大小返回true / false。 除了我正在做的其他计算之外,我还没有做的是更新视图的preferredSize。 这是至关重要的一步。 (请注意,我还依赖于侦听ComponentListener.componentResized()通知来触发正确设置返回值所需的计算。function代码如下所示,即可获得帮助。

 @SuppressWarnings("serial") class ScalingScreenPanel extends JScrollPane { private static final Dimension PREFERRED_SIZE = new Dimension(800,200); private static ScalingScreen sScreen; public ScalingScreenPanel() { setPreferredSize(PREFERRED_SIZE); getViewport().addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent event) { sScreen.calculateSizes(event.getComponent().getSize()); } }); setViewportView(sScreen=new ScalingScreen()); } public void setShow(Show pShow) { sScreen.setShow(pShow); } } // class PreviewScreenPanel @SuppressWarnings("serial") public abstract class ScalingScreen extends JPanel implements Scrollable { private static final Dimension PREFERRED_SIZE = new Dimension(1000,100); private static final JLabel EMPTY_PANEL = new JLabel("Empty",SwingConstants.CENTER); private static final int DEFAULT_SIZE = 7; private static final int MINIMUM_SIZE = 5; private static final int SPACING = 3; private static final int MINIMUM_PITCH = MINIMUM_SIZE+SPACING; // Do not modify directly private static final int DEFAULT_PITCH = DEFAULT_SIZE+SPACING; // Do not modify directly protected int cSize; protected int cPitch; protected Dimension cOrigin; private Dimension cMinimumScreenSize; private Dimension cDefaultScreenSize; protected Dimension cCurrentScreenSize; private boolean cEnableVerticalScrollbar = false, cEnableHorizontalScrollbar = false; protected Dimension cGridSize = null; ScalingScreen() { cOrigin = new Dimension(0,0); add(EMPTY_PANEL); } public void setShow(Show pShow) { remove(EMPTY_PANEL); cGridSize = new Dimension(pShow.dimension()); cMinimumScreenSize = new Dimension(cGridSize.width*MINIMUM_PITCH+SPACING,cGridSize.height*MINIMUM_PITCH+SPACING); cDefaultScreenSize = new Dimension(cGridSize.width*DEFAULT_PITCH+SPACING,cGridSize.height*DEFAULT_PITCH+SPACING); setMinimumSize(cMinimumScreenSize); setPreferredSize(cDefaultScreenSize); calculateSizes(getSize()); repaint(); } public void calculateSizes(Dimension pViewportSize) { if (cGridSize==null) return; cPitch = Math.max(MINIMUM_PITCH,Math.min((pViewportSize.width-SPACING)/cGridSize.width,(pViewportSize.height-SPACING)/cGridSize.height)); cSize = cPitch - SPACING; cOrigin = new Dimension((pViewportSize.width-(cPitch*cGridSize.width))/2,(pViewportSize.height-(cPitch*cGridSize.height))/2); cCurrentScreenSize = new Dimension(Math.max(pViewportSize.width,cMinimumScreenSize.width),Math.max(pViewportSize.height,cMinimumScreenSize.height)); Dimension preferredSize = new Dimension(); if (pViewportSize.width<cMinimumScreenSize.width) { cOrigin.width = 0; cEnableHorizontalScrollbar = true; preferredSize.width = cMinimumScreenSize.width; } else { cOrigin.width = (pViewportSize.width-(cPitch*cGridSize.width))/2; cEnableHorizontalScrollbar = false; preferredSize.width = cDefaultScreenSize.width; } if (pViewportSize.height<cMinimumScreenSize.height) { cOrigin.height = 0; cEnableVerticalScrollbar = true; preferredSize.height = cMinimumScreenSize.height; } else { cOrigin.height = (pViewportSize.height-(cPitch*cGridSize.height))/2; cEnableVerticalScrollbar = false; preferredSize.height = cDefaultScreenSize.height; } setPreferredSize(preferredSize); repaint(); } // Methods to implement abstract Scrollable interface @Override public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } @Override public boolean getScrollableTracksViewportHeight() { return !cEnableVerticalScrollbar; } @Override public boolean getScrollableTracksViewportWidth() { return !cEnableHorizontalScrollbar; } @Override public int getScrollableBlockIncrement(Rectangle pVisibleRect, int pOrientation, int pDirection) { switch (pOrientation) { case SwingConstants.VERTICAL: return pVisibleRect.height/2; case SwingConstants.HORIZONTAL: return pVisibleRect.width/2; default: return 0; } } @Override public int getScrollableUnitIncrement(Rectangle pVisibleRect, int pOrientation, int pDirection) { switch (pOrientation) { case SwingConstants.VERTICAL: return 1; case SwingConstants.HORIZONTAL: return 1; default: return 0; } } @Override public void paintComponent(Graphcs g) { // custom drawing stuff } } // class ScalingScreen 

基本方法是让您的自定义组件实现Scrollable并根据需要编写getTracksViewportWidth / -Height。

编辑

实现它们以返回true(从而禁用滚动条),直到达到最小刻度,然后返回false?

确切地说,这就是想法 – 但没有像我预期的那样有效:在达到最小值的“转折点”时,即使使用自调整的getPrefScrollable(这似乎没有任何影响,它在开始时调用一次)

编辑2

在OP的帮助下:

更新视图的preferredSize

终于得到了它:(我的初步尝试将它颠倒了;-)将prefScrollable保持为“real”pref并让pref返回minimum或pref,具体取决于滚动条是否应该可见。

 public static class JImagePanel extends JPanel implements Scrollable { private BufferedImage image; public JImagePanel(BufferedImage image) { this.image = image; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); BufferedImage scaled = //new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); GraphicsUtilities.createCompatibleImage(getWidth(), getHeight()); Graphics2D g2 = scaled.createGraphics(); g2.drawImage(image, 0, 0, getWidth(), getHeight(), null); g.drawImage(scaled, 0, 0, this); g2.dispose(); } /** * This method is used for laying out this container * inside the Viewport: let it return the "real" pref * or min, depending on whether or not the scrollbars * are showing. */ @Override public Dimension getPreferredSize() { Dimension size = getImageSize(); if (!getScrollableTracksViewportWidth()) { size.width = getMinimumSize().width; } if (!getScrollableTracksViewportHeight()) { size.height = getMinimumSize().height; } return size; } @Override public Dimension getMinimumSize() { Dimension min = getImageSize(); min.height /= 2; min.width /= 2; return min; } /** * This is used for laying out the scrollPane. Keep * it fixed to "real" pref size. */ @Override public Dimension getPreferredScrollableViewportSize() { return getImageSize(); } /** * The unscaled image size (aka: "real" pref) */ protected Dimension getImageSize() { return new Dimension(image.getWidth(), image.getHeight()); } @Override public boolean getScrollableTracksViewportWidth() { return getParent() instanceof JViewport && getParent().getWidth() >= getMinimumSize().width; } @Override public boolean getScrollableTracksViewportWidth() { return getParent() instanceof JViewport && getParent().getWidth() >= getMinimumSize().width; } @Override public boolean getScrollableTracksViewportHeight() { return getParent() instanceof JViewport && getParent().getHeight() >= getMinimumSize().height; } @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { return 10; } @Override public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { return 100; } }