Swing:滚动到JScrollPane的底部,以当前视口位置为条件

我试图模仿Adium和我见过的大多数其他聊天客户端的function,其中当新消息进入时滚动条会向前移动,但前提是你已经在那里。 换句话说,如果你向上滚动几行并正在阅读,当有新消息进入时它不会将你的位置跳到屏幕的底部; 这会很烦人。 但是,如果您滚动到底部,程序会正确地假设您希望始终查看最新的消息,因此会相应地自动滚动。

我有一段时间试图模仿这个; 该平台似乎不惜一切代价来对抗这种行为。 我能做的最好的事情如下:

在构造函数中:

JTextArea chatArea = new JTextArea(); JScrollPane chatAreaScrollPane = new JScrollPane(chatArea); // We will manually handle advancing chat window DefaultCaret caret = (DefaultCaret) chatArea.getCaret(); caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); 

在处理新文本的方法中:

 boolean atBottom = isViewAtBottom(); // Append the text using styles etc to the chatArea if (atBottom) { scrollViewportToBottom(); } public boolean isAtBottom() { // Is the last line of text the last line of text visible? Adjustable sb = chatAreaScrollPane.getVerticalScrollBar(); int val = sb.getValue(); int lowest = val + sb.getVisibleAmount(); int maxVal = sb.getMaximum(); boolean atBottom = maxVal == lowest; return atBottom; } private void scrollToBottom() { chatArea.setCaretPosition(chatArea.getDocument().getLength()); } 

现在,这有效,但由于两个原因,它很笨拙而且不理想。

  1. 通过设置插入符号位置,可以删除用户在聊天区域中可能具有的任何选择。 我可以想象,如果他试图复制/粘贴,这将是非常恼人的。
  2. 由于滚动窗格的前进发生在插入文本之后,因此滚动条处于错误位置时存在分秒,然后它在视觉上向末尾跳跃。 这不太理想。

在你问之前,是的,我已经阅读了关于文本区域滚动的这篇博文 ,但默认滚动到底部行为不是我想要的。

其他相关(但在我看来,在这方面并不完全有用)问题: 在jscrollpane上设置滚动条 使JScrollPane自动向下滚动。

在这方面的任何帮助将非常感谢。

编辑:

根据Devon_C_Miller的建议,我有一种改进的滚动方式,解决问题#1。

 private void scrollToBottom() { javax.swing.SwingUtilities.invokeLater(new Runnable() { public void run() { try { int endPosition = chatArea.getDocument().getLength(); Rectangle bottom = chatArea.modelToView(endPosition); chatArea.scrollRectToVisible(bottom); } catch (BadLocationException e) { System.err.println("Could not scroll to " + e); } } }); } 

我还有问题#2。

看一下scrollRectToVisible(Rectangle r)和modelToView(int pos)

这应该可以让你找到你想要的东西,而不会打扰用户的选择。

对于滚动条,尝试强制滚动并在不同事件上发生追加。 例如:

 if (atBottom) { // append new line & do scroll SwingUtilities.invokerLater(new Runnable(){ public void run() { // append text }}); } else { // append newline // append text } 

基于之前的答案和进一步的Google,我做了一个测试,其中一个Thread继续将字符串附加到JScrollPane中的JTextArea。 我认为在这里使用invokeLater很重要,因为在滚动到该值之前,需要使用新的最大值更新JScrollBar。 使用invokeLater,AWT线程将负责这一点。

 private class AppendThread extends Thread { public void run() { while (true) { String s = "random number = " + Math.random() + "\n"; boolean scrollDown = textAreaBottomIsVisible(); textArea.append(s); if (scrollDown) { javax.swing.SwingUtilities.invokeLater(new Runnable() { public void run() { JScrollBar bar = scrollPane.getVerticalScrollBar(); bar.setValue(bar.getMaximum()); } } ); } try { Thread.sleep(1000); } catch (Exception e) { System.err.println("exception = " + e); } } } private boolean textAreaBottomIsVisible() { Adjustable sb = scrollPane.getVerticalScrollBar(); int val = sb.getValue(); int lowest = val + sb.getVisibleAmount(); int maxVal = sb.getMaximum(); boolean atBottom = maxVal == lowest; return atBottom; } } 

看看这个例子 。

虽然,我会改变代码使用以下更有效的代码:

 caret.setDot(textArea.getDocument().getLength()); 

我花了最近几天尝试不同的方法来解决这个问题。 我发现的最佳解决方案是替换JScrollPane视口的布局管理器,并在那里实现所有需要的滚动行为。 跳跃的滚动条旋钮消失了,而且速度非常快 – 它创造了另一种竞争条件我不得不解决。 详细信息如下: http : //www.java-forums.org/awt-swing/56808-scroll-bar-knob-jumpy-when-component-growing.html

有点晚了,但我发现这里是: http : //www.camick.com/java/source/SmartScroller.java

实质上,您可以使用以下代码:

 JScrollPane scrollPane = new JScrollPane(myOversizedComponent); // Injects smartscroll behaviour to your scrollpane new SmartScroller(scrollPane); 

这里是SmartScroller类:

 import java.awt.Component; import java.awt.event.*; import javax.swing.*; import javax.swing.text.*; /** * The SmartScroller will attempt to keep the viewport positioned based on * the users interaction with the scrollbar. The normal behaviour is to keep * the viewport positioned to see new data as it is dynamically added. * * Assuming vertical scrolling and data is added to the bottom: * * - when the viewport is at the bottom and new data is added, * then automatically scroll the viewport to the bottom * - when the viewport is not at the bottom and new data is added, * then do nothing with the viewport * * Assuming vertical scrolling and data is added to the top: * * - when the viewport is at the top and new data is added, * then do nothing with the viewport * - when the viewport is not at the top and new data is added, then adjust * the viewport to the relative position it was at before the data was added * * Similiar logic would apply for horizontal scrolling. */ public class SmartScroller implements AdjustmentListener { public final static int HORIZONTAL = 0; public final static int VERTICAL = 1; public final static int START = 0; public final static int END = 1; private int viewportPosition; private JScrollBar scrollBar; private boolean adjustScrollBar = true; private int previousValue = -1; private int previousMaximum = -1; /** * Convenience constructor. * Scroll direction is VERTICAL and viewport position is at the END. * * @param scrollPane the scroll pane to monitor */ public SmartScroller(JScrollPane scrollPane) { this(scrollPane, VERTICAL, END); } /** * Convenience constructor. * Scroll direction is VERTICAL. * * @param scrollPane the scroll pane to monitor * @param viewportPosition valid values are START and END */ public SmartScroller(JScrollPane scrollPane, int viewportPosition) { this(scrollPane, VERTICAL, viewportPosition); } /** * Specify how the SmartScroller will function. * * @param scrollPane the scroll pane to monitor * @param scrollDirection indicates which JScrollBar to monitor. * Valid values are HORIZONTAL and VERTICAL. * @param viewportPosition indicates where the viewport will normally be * positioned as data is added. * Valid values are START and END */ public SmartScroller(JScrollPane scrollPane, int scrollDirection, int viewportPosition) { if (scrollDirection != HORIZONTAL && scrollDirection != VERTICAL) throw new IllegalArgumentException("invalid scroll direction specified"); if (viewportPosition != START && viewportPosition != END) throw new IllegalArgumentException("invalid viewport position specified"); this.viewportPosition = viewportPosition; if (scrollDirection == HORIZONTAL) scrollBar = scrollPane.getHorizontalScrollBar(); else scrollBar = scrollPane.getVerticalScrollBar(); scrollBar.addAdjustmentListener( this ); // Turn off automatic scrolling for text components Component view = scrollPane.getViewport().getView(); if (view instanceof JTextComponent) { JTextComponent textComponent = (JTextComponent)view; DefaultCaret caret = (DefaultCaret)textComponent.getCaret(); caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); } } @Override public void adjustmentValueChanged(final AdjustmentEvent e) { SwingUtilities.invokeLater(new Runnable() { public void run() { checkScrollBar(e); } }); } /* * Analyze every adjustment event to determine when the viewport * needs to be repositioned. */ private void checkScrollBar(AdjustmentEvent e) { // The scroll bar listModel contains information needed to determine // whether the viewport should be repositioned or not. JScrollBar scrollBar = (JScrollBar)e.getSource(); BoundedRangeModel listModel = scrollBar.getModel(); int value = listModel.getValue(); int extent = listModel.getExtent(); int maximum = listModel.getMaximum(); boolean valueChanged = previousValue != value; boolean maximumChanged = previousMaximum != maximum; // Check if the user has manually repositioned the scrollbar if (valueChanged && !maximumChanged) { if (viewportPosition == START) adjustScrollBar = value != 0; else adjustScrollBar = value + extent >= maximum; } // Reset the "value" so we can reposition the viewport and // distinguish between a user scroll and a program scroll. // (ie. valueChanged will be false on a program scroll) if (adjustScrollBar && viewportPosition == END) { // Scroll the viewport to the end. scrollBar.removeAdjustmentListener( this ); value = maximum - extent; scrollBar.setValue( value ); scrollBar.addAdjustmentListener( this ); } if (adjustScrollBar && viewportPosition == START) { // Keep the viewport at the same relative viewportPosition scrollBar.removeAdjustmentListener( this ); value = value + maximum - previousMaximum; scrollBar.setValue( value ); scrollBar.addAdjustmentListener( this ); } previousValue = value; previousMaximum = maximum; } } 
 public static void scrollToBottom(JComponent component) { Rectangle visibleRect = component.getVisibleRect(); visibleRect.y = component.getHeight() - visibleRect.height; component.scrollRectToVisible(visibleRect); } 

您可以在此处找到完整的详细信息: 如何仅在滚动条位于底部且滚动锁定关闭时才使JTextPane自动滚动?

 DefaultCaret caret = (DefaultCaret)textArea.getCaret(); caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);