Results 1 to 8 of 8
  1. #1
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Making my JComponent scrollable - the finer points

    I posted the other day about how to make my JComponent scrollable. I've got it working pretty well, but there's one thing I can't figure out.

    I want to be able to test and update the JScrollPane viewport position when my component's preferred size changes. First, if it's scrolled to the bottom and more content is added to the bottom, then I want to reposition the viewport to the new bottom. Second, if the content it's displaying scrolls off the top, I want to reposition the viewport to the new top.

    I know about JComponent.getVisibleRect() and JComponent.scrollRectToVisible(). My problem is where to call these.

    My JComponent is called Display, and the data it displays is behind an interface called Buffer. Display overrides getPreferredSize() to return a value derived from the extents of the Buffer. The Display also registers itself as a BufferObserver. When the Buffer extents change, it calls Display.extentsChanged() which in turn calls revalidate().

    I think maybe I should call getVisibleRect/scrollRectToVisible in Display.extentsChanged(), but I'm not sure because I don't fully understand what revalidate() does. I understand that it causes the EDT to reexamine the preferred size of the component... but I'm not sure if I can reliably call scrollRectToVisible() with an argument based on the new preferred size immediately after calling revalidate(), when the EDT may not have done its work.
    Get in the habit of using standard Java naming conventions!

  2. #2
    DarrylBurke's Avatar
    DarrylBurke is offline Forum Police
    Join Date
    Sep 2008
    Location
    Madgaon, Goa, India
    Posts
    11,457
    Rep Power
    20

    Default Re: Making my JComponent scrollable - the finer points

    I'm not sure if I can reliably call scrollRectToVisible() with an argument based on the new preferred size immediately after calling revalidate(), when the EDT may not have done its work.
    Good thinking. Going that route would probably lead to intermittent, hard-to-debug misbehavior arising out of concurrency issues.

    Cache the getVisibleRect() before changing the preferredSize/revalidating, compute the new visibleRect based on the delta of the extents -- not on the current state of the scrollable component -- and after revalidating, wrap the call to setVisibleRect(...) in a SwingUtilities#invokeLater(...) and you're set.

    db
    If you're forever cleaning cobwebs, it's time to get rid of the spiders.

  3. #3
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: Making my JComponent scrollable - the finer points

    Hmmm...

    This still leads to a race condition:

    Viewport is at bottom
    Extents change
    -- Cache getVisibleRect()
    -- Update preferred size and revalidate
    -- Compute new visibleRect based on extents delta
    -- Queue scrollRectToVisible with computed visibleRect
    Extents change
    -- Cache getVisibleRect()
    -- Update preferred size and revalidate
    -- Compute new visibleRect based on extents delta <- computes wrong value because it should be combined with the delta from the previous extents change
    -- Queue scrollRectToVisible with computed visibleRect
    EDT updates visible area
    Viewport is no longer at bottom

    It looks like I'm going to have to cache the preferred visibleRect as a member variable. But that creates another problem: updating the preferred visibleRect when the scroll bar is moved by the user, but <i>not</i> when the EDT calls scrollRectToVisible.

    Maybe I'm going about this entirely the wrong way...
    Get in the habit of using standard Java naming conventions!

  4. #4
    DarrylBurke's Avatar
    DarrylBurke is offline Forum Police
    Join Date
    Sep 2008
    Location
    Madgaon, Goa, India
    Posts
    11,457
    Rep Power
    20

    Default Re: Making my JComponent scrollable - the finer points

    Ah ok, I see where that can blow up. Is it possible to condense the problem in the form of an SSCCE?

    db
    If you're forever cleaning cobwebs, it's time to get rid of the spiders.

  5. #5
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: Making my JComponent scrollable - the finer points

    Here's a SSCCE that demonstrates a growing component, without any attempt to make it "stick" to the bottom. I have a couple of ideas for how to do that. I'll let you know how it goes.

    The 'grow' method is analogous to the 'extentsChanged' method in my real code.

    Java Code:
    import java.awt.Dimension;
    import java.awt.Rectangle;
    
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JScrollPane;
    import javax.swing.Scrollable;
    import javax.swing.SwingUtilities;
    
    /**
     * A SSCCE demonstrating a growing scrollable component.  It starts off at 500
     * by 300 pixels, with a viewport of the same size.  At intervals of a few
     * seconds, a non-Swing thread causes it to rapidly increase in height by 100
     * pixels in increments of 5 pixels.  This is meant to simulate the bursty
     * addition of new lines to the end of a scrolling terminal component.
     *
     * @author Kevin Krumwiede (kjkrum@gmail.com)
     */
    public class GrowingScrollableComponent extends JComponent implements Scrollable {
    	private static final long serialVersionUID = 1L;
    	
    	protected static final Dimension initialSize = new Dimension(500, 300);
    	protected final Dimension preferredSize;
    	
    	public GrowingScrollableComponent() {
    		preferredSize = new Dimension(initialSize);
    	}
    	
    	@Override
    	public boolean isPreferredSizeSet() { return true; }
    	
    	@Override
    	synchronized public Dimension getPreferredSize() {
    		return new Dimension(preferredSize);
    	}
    	
    	synchronized public void grow(int pixels) {
    		preferredSize.height += pixels;
    		revalidate();
    	}
    
    	@Override
    	public Dimension getPreferredScrollableViewportSize() {
    		return new Dimension(initialSize);
    	}
    
    	@Override
    	public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
    		return 10;
    	}
    	
    	@Override
    	public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
    		return 100;
    	}
    
    	@Override
    	public boolean getScrollableTracksViewportHeight() {
    		return false;
    	}
    
    	@Override
    	public boolean getScrollableTracksViewportWidth() {
    		return false;
    	}
    
    	
    	public static void main(String[] args) {
    		SwingUtilities.invokeLater(new Runnable() {
    			@Override
    			public void run() {
    				JFrame frame = new JFrame("Growing Scrollable Component");
    				final GrowingScrollableComponent comp = new GrowingScrollableComponent();
    				JScrollPane scrollPane = new JScrollPane(comp);
    				scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
    				frame.getContentPane().add(scrollPane);
    				frame.pack();
    				frame.setResizable(false);
    				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    				frame.setVisible(true);
    				
    				// launch the thread that makes it grow
    				new Thread(new Runnable() {
    					@Override
    					public void run() {
    						while(true) {
    							try {
    								Thread.sleep(5000);
    							} catch (InterruptedException e) {
    								return;
    							}
    							for(int i = 0; i < 20; ++i) {
    								comp.grow(5);
    							}
    						}
    					}
    				}).start();
    			}
    		});
    	}
    }
    Get in the habit of using standard Java naming conventions!

  6. #6
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: Making my JComponent scrollable - the finer points

    Think I got it!

    The stuff in grow() makes it stick to the bottom if it's at the bottom when it grows. The stuff in paintComponent() makes it not stick to the bottom if the user drags the scrollbar knob away from the bottom. Drag it back, and it sticks again.

    I tried doing the latter in scrollRectToVisible() instead of paintComponent(), but it isn't called when the user drags the knob.

    Java Code:
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Rectangle;
    
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JScrollPane;
    import javax.swing.Scrollable;
    import javax.swing.SwingUtilities;
    
    /**
     * A SSCCE demonstrating a growing scrollable component.  It starts off at 500
     * by 300 pixels, with a viewport of the same size.  At intervals of a few
     * seconds, a non-Swing thread causes it to rapidly increase in height by 100
     * pixels in increments of 5 pixels.  This is meant to simulate the bursty
     * addition of new lines to the end of a scrolling terminal component.
     *
     * @author Kevin Krumwiede (kjkrum@gmail.com)
     */
    public class GrowingScrollableComponent extends JComponent implements Scrollable {
    	private static final long serialVersionUID = 1L;
    	
    	protected static final Dimension initialSize = new Dimension(500, 300);
    	protected final Dimension preferredSize;
    	protected final Rectangle preferredVisibleRect;
    	
    	public GrowingScrollableComponent() {
    		preferredSize = new Dimension(initialSize);
    		preferredVisibleRect = new Rectangle(0, 0, preferredSize.width, preferredSize.height);
    	}
    	
    	@Override
    	public boolean isPreferredSizeSet() { return true; }
    	
    	@Override
    	synchronized public Dimension getPreferredSize() {
    		return new Dimension(preferredSize);
    	}
    	
    	synchronized public void grow(int pixels) {
    		boolean atBottom = (preferredVisibleRect.y + preferredVisibleRect.height == preferredSize.height);
    		
    		preferredSize.height += pixels;
    		revalidate();
    		
    		if(atBottom) {
    			SwingUtilities.invokeLater(new Runnable() {
    				@Override
    				public void run() {
    					synchronized(GrowingScrollableComponent.this) {
    						preferredVisibleRect.y = preferredSize.height - preferredVisibleRect.height;
    					}
    					scrollRectToVisible(preferredVisibleRect);					
    				}
    			});
    		}
    	}
    
    	@Override
    	protected void paintComponent(Graphics g) {
    		super.paintComponent(g);
    		synchronized(this) {
    			preferredVisibleRect.setBounds(getVisibleRect());
    		}
    	}
    
    	@Override
    	public Dimension getPreferredScrollableViewportSize() {
    		return new Dimension(initialSize);
    	}
    
    	@Override
    	public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
    		return 10;
    	}
    	
    	@Override
    	public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
    		return 100;
    	}
    
    	@Override
    	public boolean getScrollableTracksViewportHeight() {
    		return false;
    	}
    
    	@Override
    	public boolean getScrollableTracksViewportWidth() {
    		return false;
    	}
    
    	
    	public static void main(String[] args) {
    		SwingUtilities.invokeLater(new Runnable() {
    			@Override
    			public void run() {
    				JFrame frame = new JFrame("Growing Scrollable Component");
    				final GrowingScrollableComponent comp = new GrowingScrollableComponent();
    				JScrollPane scrollPane = new JScrollPane(comp);
    				scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
    				frame.getContentPane().add(scrollPane);
    				frame.pack();
    				frame.setResizable(false);
    				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    				frame.setVisible(true);
    				
    				// launch the thread that makes it grow
    				new Thread(new Runnable() {
    					@Override
    					public void run() {
    						while(true) {
    							try {
    								Thread.sleep(5000);
    							} catch (InterruptedException e) {
    								return;
    							}
    							for(int i = 0; i < 20; ++i) {
    								comp.grow(5);
    							}
    						}
    					}
    				}).start();
    			}
    		});
    	}
    }
    Get in the habit of using standard Java naming conventions!

  7. #7
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: Making my JComponent scrollable - the finer points

    I found it necessary to call repaint() in the Runnable passed to SwingUtilities.invokeLater() in grow()/extentsChanged(). My real code looks like this:

    Java Code:
    	@Override
    	synchronized public void extentsChanged() {
    		//System.out.println("buffer extents changed: " + buffer.getExtents());
    		
    		// FIXME: handle case of visible stuff scrolling out
    		
    		boolean atBottom = (preferredVisibleRect.y + preferredVisibleRect.height == preferredSize.height);
    		
    		updatePreferredSize();
    		revalidate();
    		
    		if(atBottom) {
    			SwingUtilities.invokeLater(new Runnable() {
    				@Override
    				public void run() {
    					synchronized(Display.this) {
    						preferredVisibleRect.y = preferredSize.height - preferredVisibleRect.height;
    						scrollRectToVisible(preferredVisibleRect);
    						repaint();
    					}
    				}
    			});
    		}
    	}
    Get in the habit of using standard Java naming conventions!

  8. #8
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: Making my JComponent scrollable - the finer points

    It's finished and behaving in all the ways I envisioned.

    One detail I didn't mention is that my actual component doesn't grow infinitely. It's backed by a circular buffer. So once it reaches its maximum size, content begins scrolling off the top. I've made it so when the buffer is full and the viewport is positioned on data in the middle, the viewport adjusts itself to keep displaying the same data until it scrolls off the top.

    Everything seems to work well, except that whenever the component grows rapidly, the scroll bar knob is jumpy. I'm going to start another thread about that.

    Java Code:
    	@Override
    	synchronized public void extentsChanged() {
    		// compute deltas
    		Rectangle newExtents = buffer.getExtents();
    		int bufferYDelta = newExtents.y - previousExtents.y;
    		previousExtents.setBounds(newExtents);
    		boolean atBottom = (preferredVisibleRect.y + preferredVisibleRect.height == preferredSize.height);
    		int oldHeight = preferredSize.height;
    		preferredSize.setSize(computeComponentSize(newExtents));
    		int componentHDelta = preferredSize.height - oldHeight;
    
    		// FIXME: scroll bar knob is jumpy when buffer extents are growing
    		if(atBottom) {
    			if(componentHDelta != 0) {
    				SwingUtilities.invokeLater(new Runnable() {
    					@Override
    					public void run() {
    						synchronized(Display.this) {
    							revalidate();
    							preferredVisibleRect.y = preferredSize.height - preferredVisibleRect.height;
    							scrollRectToVisible(preferredVisibleRect);
    							repaint();
    						}
    					}
    				});
    			}
    			else repaint();
    		}
    		else if(bufferYDelta != 0) {
    			preferredVisibleRect.y -= bufferYDelta * glyphSize.height;
    			if(preferredVisibleRect.y < 0) preferredVisibleRect.y = 0;
    			SwingUtilities.invokeLater(new Runnable() {
    				@Override
    				public void run() {
    					synchronized(Display.this) {
    						revalidate();
    						scrollRectToVisible(preferredVisibleRect);
    						repaint();
    					}
    				}
    			});
    		}
    	}
    Get in the habit of using standard Java naming conventions!

Similar Threads

  1. Making my JComponent scrollable
    By kjkrum in forum AWT / Swing
    Replies: 4
    Last Post: 03-11-2012, 09:08 PM
  2. Replies: 1
    Last Post: 08-11-2011, 11:46 AM
  3. Replies: 2
    Last Post: 10-01-2010, 08:18 PM
  4. scrollable applets
    By kaemonsaionji in forum New To Java
    Replies: 2
    Last Post: 02-25-2009, 09:47 AM
  5. Replies: 2
    Last Post: 08-24-2008, 01:30 AM

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •