1

I have a JScrollPane, with a customized Scrollable view to match its width, allowing/requiring only vertical scrolling. It is populated with varying quantities of JLabels, which almost always word-wrap due to their length. I'm using a BoxLayout to ensure they are always full-width, laid out vertically, and it mostly works.

A major design requirement involves knowing precisely where each of these JLabels are, so that the scroll bar can be programmatically moved to center it. If you've used NetBeans, you should be familiar with the idea; Find in Chrome, though it doesn't jump there, also indicates where things are.

The problem I'm encountering is that, at some point, the layout just gives up. You can tell by the way that the scroll bar jumps around during scrolling that it hasn't actually calculated everything yet. Because of this, the reported getY values are useless.

I've added the following bit of debug code;

this.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {
        public void adjustmentValueChanged(AdjustmentEvent evt) {
            //getVerseBlock grabs a component from the view, with sanity checks:
            System.out.println(ChapterTab.this.getVerseBlock(255).getY());
            System.out.println(ChapterTab.this.getVerseBlock(255).getHeight());
        }
    });

As expected - but not desired - the Y position of the last (or any other) element continues to increase as the panel is scrolled, as more and more JLabels properly calculate their size. The getHeight() check indeed reveals that they are set at the height of a single line, unaware that they need to word-wrap, until they come close to being visible.

Once the entire panel has been scrolled through, it's happy... for the moment. It does not un-calculate when those JLabels go back out of view. But it also does not re-calculate when the window is resized, causing the values to once again become useless. (And, incidentally, break math; components too high above the rendered area will also report bad heights, potentially adding to far greater than the new height of their container!)

I am therefore seeking, quite simply, a way to ensure that every single component in this container will always have completely accurate and current dimension and location values. And despite that seeming to be just the way things ought to work - how unprofessional is a jittery scrollbar? - I haven't found any resources on achieving that.

EDIT: It bothers me greatly that people who don't know the answer think that's only because they can't see the code. It's okay! You can't know everything; if you haven't encountered and resolved the problem before, that doesn't make you any less of a coder, and I'm not expecting you to answer. But, I have managed to pare down the issue to a very basic single file:

package jscrolltest;


import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;

public class JScrollTest {

    public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI() {

        JFrame frame = new JFrame("JScrollTest");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().setLayout(new BorderLayout());

        ScrollableView view = new ScrollableView();
        view.setLayout(new BoxLayout(view, BoxLayout.Y_AXIS));

        JScrollPane pnlScroll = new JScrollPane(view);
        pnlScroll.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        pnlScroll.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);

        view.add(new JLabel("<html>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc convallis sapien sed tempor scelerisque."));
        view.add(new JLabel("<html>Donec ut leo nec ligula tempus eleifend. Proin in porttitor velit."));
        view.add(new JLabel("<html>Sed a facilisis orci. Nunc a diam feugiat, suscipit nunc nec, porttitor velit."));
        view.add(new JLabel("<html>Maecenas sagittis, est et bibendum luctus, tortor orci hendrerit enim, vitae rhoncus augue libero sit amet est. Phasellus a neque et magna gravida euismod dignissim ac elit."));
        view.add(new JLabel("<html>Ut neque urna, ultrices fermentum quam et, tristique tempus nulla. Curabitur non feugiat leo."));
        view.add(new JLabel("<html>Aenean eu viverra ligula, eu tempor turpis. Suspendisse eu nunc ac urna blandit egestas quis id augue. "));
        view.add(new JLabel("<html>Suspendisse a pulvinar est. Maecenas id congue neque. Donec eleifend nisi quis nisl faucibus sollicitudin."));
        view.add(new JLabel("<html>Aliquam rutrum nulla neque, sit amet sollicitudin massa ultrices quis. Interdum et malesuada fames ac ante ipsum primis in faucibus."));
        view.add(new JLabel("<html>Praesent ut auctor nisl, eget convallis neque. Quisque et vestibulum massa."));
        view.add(new JLabel("<html>Quisque consectetur ex cursus risus interdum, tristique imperdiet ante viverra. Sed et nulla eget sem dapibus fringilla."));

        frame.getContentPane().add(pnlScroll, BorderLayout.CENTER);

        pnlScroll.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {
            public void adjustmentValueChanged(AdjustmentEvent evt) {
                System.out.println(view.getComponent(9).getY());
                System.out.println(view.getComponent(9).getHeight());
            }
        });

        frame.setSize(200, 100);
        frame.setVisible(true);
    }

    public static class ScrollableView extends JPanel implements Scrollable {

        public Dimension getPreferredScrollableViewportSize() {
            return getPreferredSize();
        }

        public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 10;
        }

        public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
            return ((orientation == SwingConstants.VERTICAL) ? visibleRect.height : visibleRect.width) - 10;
        }

        public boolean getScrollableTracksViewportWidth() {
            return true;
        }

        public boolean getScrollableTracksViewportHeight() {
            return false;
        }
    }
}

As you can see, there's nothing inherently special; literally the bare minimum required to make a frame, drop a JScrollPane in, make it so the view fits the width, and make some JLabels arranged vertically via BoxLayout. Precisely as described, no special tricks.

trashgod
  • 203,806
  • 29
  • 246
  • 1,045
DigitalMan
  • 2,440
  • 5
  • 26
  • 32
  • Maybe [how-to-scroll-to-a-specific-location-in-a-jscrollpane](http://stackoverflow.com/questions/12102620/how-to-scroll-to-a-specific-location-in-a-jscrollpane) is what you want? – tomse Apr 01 '15 at 20:41
  • Scrolling to a specific location is done and working. It's getting that "specific location" that is causing the problem. – DigitalMan Apr 01 '15 at 20:49
  • 2
    `Once the entire panel has been scrolled through, it's happy...` - makes no sense. Scrolling has no effect on sizes. `JLabels, which almost always word-wrap` - labels don't word wrap, or are you using HTML? `... a way to ensure that every single component in this container will always have completely accurate and current dimension and location values.` Components should have current dimensions once the component has been added to the panel and you have used revalidate() on the panel to invoke the layout manager. Post a [SSCCE](http://sscce.org/) that demonstrates your problem. – camickr Apr 01 '15 at 21:05
  • @camickr A JLabel will word-wrap fine, if it has a parent of limited width, as I stated. In this case, the off-screen JLabels are either not given a width, or not given the paint() call that actually causes the word-wrap to occur (a known flaw in their design; they will not run those calculations until painted, hence why the numbers *are* correct after the labels have been shown once). In this case a "SSCCE" is about 6 full files, so for now I'll wait for someone more familiar with it to answer. – DigitalMan Apr 01 '15 at 21:15
  • `"In this case a "SSCCE" is about 6 full files..."` -- then it may be time to refactor your code to try to reduce its cyclomatic complexity, and thus allow for smaller contained testable units. Otherwise you might be waiting a long time since @camickr knows a heck of a lot (and a lot more than me). – Hovercraft Full Of Eels Apr 01 '15 at 21:18
  • Given that the question should not require looking at any of my code at all, since it is simply not that kind of question ("here's what I need to do", not "what's wrong here/please fix this"), I'm not sure the folks here could handle it if they weren't provided every single custom class I'm using. – DigitalMan Apr 01 '15 at 21:29
  • 2
    @DigitalMan You obviously have not read what an SSCCE is. The folks here are actually very likely to help if you provide the correct input. Your description is very vague and unless we can reproduce the problem (the whole idea of the SSCCE), it's very hard to help you fix the problem. – Guillaume Polet Apr 01 '15 at 21:52
  • 2
    Amen @GuillaumePolet. I'm going to say that it's likely near impossible to guess what could be wrong or how to fix it without an SSCCE. Yes, creating one would require a lot of labor, but it would be effort well spent. – Hovercraft Full Of Eels Apr 01 '15 at 22:01
  • 2
    @HovercraftFullOfEels The things with an SSCCE is that in 50% of the cases, when writing it, you actually find the solution on your own. So the SSCCE is the easiest and fastest solution. At the end, you either found the solution or you can give it as correct input to SO folks. And espcially Swing SO folks have a very deep knowledge of Swing.; – Guillaume Polet Apr 01 '15 at 22:07
  • `they will not run those calculations until painted,` - I added a ComponentListener to the panel and forced a paint() of each component on the panel and it seems to work fine the best I can tell. – camickr Apr 02 '15 at 02:33
  • @camickr Would you mind demonstrating exactly how? I've tried implementing that in the full program a few times, but to no avail. – DigitalMan Apr 02 '15 at 03:11
  • So now you need some code? Now you know how we feel. `...Precisely as described,` Not really. You are using HTML in the labels which was not described. You implemented Scrollable, which means you are NOT using default functionality, so you could have made a mistake and implemented a method incorrectly and caused the problem. This is extremely common! No matter how good we are we all make mistakes and this is ok, but we like to see the `SSCCE` so we can rule out the obvious. Also, even if we don't know the answer the SSCCE gives us a starting point to do our own problem solving. – camickr Apr 03 '15 at 00:33
  • `I've tried implementing that in the full program` - why would you do that in the full program? The point of the SSCCE is to simplify the problem and find a solution for the SSCCE first then apply the knowledge to the real code. Saying it doesn't work gives us no information. Again we don't know how you implemented the logic. Show us the code. Maybe the problem is the implementation, or maybe the problem is the approach. Until we see the code we can't say! – camickr Apr 03 '15 at 00:37
  • @camickr I implemented it as part of my previous troubleshooting. I've been at this for well over a month now; SO is my absolute, positive, dead-last resort, when the only hope is to find someone else who's resolved the exact same bizarre issue. I did not, however, explicitly call `paint()`, since I've been told that's a bad idea. I will give that a try. – DigitalMan Apr 03 '15 at 02:59

1 Answers1

1

In this case, the off-screen JLabels are either not given a width, or not given the paint() call that actually causes the word-wrap to occur (a known flaw in their design; they will not run those calculations until painted,

You say this is a design problem so the only solution I can come up with would be a hack at best. Seems to work for your SSCCE:

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;

public class JScrollTest {

    public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI() {

        JFrame frame = new JFrame("JScrollTest");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().setLayout(new BorderLayout());

        final ScrollableView view = new ScrollableView();
        view.setLayout(new BoxLayout(view, BoxLayout.Y_AXIS));

        JScrollPane pnlScroll = new JScrollPane(view);
        pnlScroll.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        pnlScroll.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);

        view.add(new JLabel("<html>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc convallis sapien sed tempor scelerisque."));
        view.add(new JLabel("<html>Donec ut leo nec ligula tempus eleifend. Proin in porttitor velit."));
        view.add(new JLabel("<html>Sed a facilisis orci. Nunc a diam feugiat, suscipit nunc nec, porttitor velit."));
        view.add(new JLabel("<html>Maecenas sagittis, est et bibendum luctus, tortor orci hendrerit enim, vitae rhoncus augue libero sit amet est. Phasellus a neque et magna gravida euismod dignissim ac elit."));
        view.add(new JLabel("<html>Ut neque urna, ultrices fermentum quam et, tristique tempus nulla. Curabitur non feugiat leo."));
        view.add(new JLabel("<html>Aenean eu viverra ligula, eu tempor turpis. Suspendisse eu nunc ac urna blandit egestas quis id augue. "));
        view.add(new JLabel("<html>Suspendisse a pulvinar est. Maecenas id congue neque. Donec eleifend nisi quis nisl faucibus sollicitudin."));
        view.add(new JLabel("<html>Sed a facilisis orci. Nunc a diam feugiat, suscipit nunc nec, porttitor velit."));
        view.add(new JLabel("<html>Maecenas sagittis, est et bibendum luctus, tortor orci hendrerit enim, vitae rhoncus augue libero sit amet est. Phasellus a neque et magna gravida euismod dignissim ac elit."));
        view.add(new JLabel("<html>Ut neque urna, ultrices fermentum quam et, tristique tempus nulla. Curabitur non feugiat leo."));
        view.add(new JLabel("<html>Aenean eu viverra ligula, eu tempor turpis. Suspendisse eu nunc ac urna blandit egestas quis id augue. "));
        view.add(new JLabel("<html>Suspendisse a pulvinar est. Maecenas id congue neque. Donec eleifend nisi quis nisl faucibus sollicitudin."));
        view.add(new JLabel("<html>Aliquam rutrum nulla neque, sit amet sollicitudin massa ultrices quis. Interdum et malesuada fames ac ante ipsum primis in faucibus."));
        view.add(new JLabel("<html>Praesent ut auctor nisl, eget convallis neque. Quisque et vestibulum massa."));
        view.add(new JLabel("<html>123Quisque consectetur ex cursus risus interdum, tristique imperdiet ante viverra. Sed et nulla eget sem dapibus fringilla."));

        frame.getContentPane().add(pnlScroll, BorderLayout.CENTER);

        pnlScroll.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {
            public void adjustmentValueChanged(AdjustmentEvent evt) {
                System.out.println(view.getComponent(14).getY());
                System.out.println(view.getComponent(14).getHeight());
            }
        });

        frame.setSize(300, 200);
        frame.setVisible(true);
    }

    public static class ScrollableView extends JPanel implements Scrollable, ComponentListener {

        public ScrollableView()
        {
            addComponentListener( this );
        }

        public Dimension getPreferredScrollableViewportSize() {
            return getPreferredSize();
        }

        public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 10;
        }

        public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
            return ((orientation == SwingConstants.VERTICAL) ? visibleRect.height : visibleRect.width) - 10;
        }

        public boolean getScrollableTracksViewportWidth() {
            return true;
        }

        public boolean getScrollableTracksViewportHeight() {
            return false;
        }

        //  Implement ComponentListener

        public void componentResized(ComponentEvent e)
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    int width = getParent().getSize().width;

                    if (width == 0) return;

                    BufferedImage bi = new BufferedImage(width, 600, BufferedImage.TYPE_INT_RGB);
                    Graphics g = bi.getGraphics();

                    for (Component c: getComponents())
                    {
                        c.paint(g);
                    }

                    revalidate();
                    repaint();
                }
            });
        }

        public void componentHidden(ComponentEvent e) {}

        public void componentMoved(ComponentEvent e) {}

        public void componentShown(ComponentEvent e) {}

    }
}

Don't know if the invokeLater(), revalidate() and repaint() are needed, I just through them in. Also, I just picked a random height for the BufferedImage. For all I know it may even work with a height of 1.

camickr
  • 321,443
  • 19
  • 166
  • 288
  • After a quick bit if Science, I've made several discoveries, at least regarding this in the SSCCE. No need for `invokeLater()` (tested to already be in EDT), or `revalidate()`, or `repaint()`... or even a width. A 1x1 BufferedImage is fine. See, this is why Swing gives me problems. – DigitalMan Apr 03 '15 at 03:08