2010-04-19 23 views
10

我试图模仿Adium和我见过的大多数其他聊天客户端的功能,其中滚动条在新消息进入时前进到底部,但仅限于如果你已经在那里。换句话说,如果你已经滚动了几行并正在阅读,当新消息进入时,它不会将你的位置跳到屏幕的底部;那会很烦人。但是,如果您滚动到底部,则该程序正确地假定您想要始终查看最新的消息,并相应地自动滚动。Swing:滚动到JScrollPane的底部,以当前视口位置为条件

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

在构造函数中:

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()); 
} 

现在,这个工作,但它janky并不理想有两个原因。

  1. 通过设置插入位置,用户可能在聊天区中进行的任何选择都将被删除。我可以想象,如果他试图复制/粘贴,这将是非常恼人的。
  2. 由于滚动窗格的前进发生在插入文本之后,因此滚动条位置错误时会出现一秒钟,然后视觉上跳到最后。这并不理想。

在你问,是的,我已阅读Text Area Scrolling这篇博文,但默认的滚动到底部行为是不是我想要的。

其他相关的(但在我看来,在这方面没有完全有用)问题: Setting scroll bar on a jscrollpane Making a JScrollPane automatically scroll all the way down.

在这方面的任何帮助将是非常赞赏。

编辑:

按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。

+0

你可以有检查我的答案是: http://stackoverflow.com/questions/4045722/how-to-make-jtextpane-autoscroll-only-when-scroll-bar-is - 底部和滚动 - 罗/ 23654546#23654546 – 2014-05-14 12:21:02

回答

6

看看在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 
} 
+0

我不明白你对不同事件的意思......我作为一个单独的事件滚动。 – I82Much 2010-04-19 21:42:00

+0

分离滚动和附加的目标是让GUI有机会在滚动和文本添加之间重新绘制。这允许滚动条在文本出现之前移动到底部。 – 2010-04-20 02:21:58

+0

我正在分割滚动和追加,但显然追加必须发生在滚动之前,否则当追加文本时我们不会在视图的底部。 – I82Much 2010-04-20 14:56:18

1

看看这个example

虽然,我会改变代码使用以下哪个更高效:

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

该示例再次丢弃选择。此外滚动条仍然跳动。 – I82Much 2010-04-19 21:41:15

+0

我看不到“滚动条跳跃”。如果找到选择,您可以随时更改代码以不重置插入位置。然后视口不会自动滚动,但我会认为这是你想要的行为。如果用户正在选择文本,则滚动不应该是自动的。 – camickr 2010-04-20 01:57:02

+0

@camickr您提供的链接不起作用。 – 2013-01-03 23:17:07

3

基于以前的答案,并进一步Google'ing我做了一个试验中,在线程汽车无追加字符串到一个JTextArea在JScrollPane中。我认为在这里使用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; 
    } 
} 
0
DefaultCaret caret = (DefaultCaret)textArea.getCaret(); 
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); 
1

晚了一点,但这里的东西,我发现:http://www.camick.com/java/source/SmartScroller.java

在本质上,你可以使用下面的代码:

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

而这里th Ë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; 
    } 
} 
相关问题