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

发布于 2024-08-29 14:37:15 字数 2404 浏览 6 评论 0原文

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

我曾花了一段时间试图模仿这一点。该平台似乎不惜一切代价打击这种行为。我能做的最好的事情如下:

在构造函数中:

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。

I am attempting to mimic the functionality of Adium and most other chat clients I've seen, wherein the scrollbars advance to the bottom when new messages come in, but only if you're already there. In other words, if you've scrolled a few lines up and are reading, when a new message comes in it won't jump your position to the bottom of the screen; that would be annoying. But if you're scrolled to the bottom, the program rightly assumes that you want to see the most recent messages at all times, and so auto-scrolls accordingly.

I have had a bear of a time trying to mimic this; the platform seems to fight this behavior at all costs. The best I can do is as follows:

In constructor:

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

In method that handles new text coming in:

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

Now, this works, but it's janky and not ideal for two reasons.

  1. By setting the caret position, whatever selection the user may have in the chat area is erased. I can imagine this would be very irritating if he's attempting to copy/paste.
  2. Since the advancement of the scroll pane occurs after the text is inserted, there is a split second where the scrollbar is in the wrong position, and then it visually jumps towards the end. This is not ideal.

Before you ask, yes I've read this blog post on Text Area Scrolling, but the default scroll to bottom behavior is not what I want.

Other related (but to my mind, not completely helpful in this regard) questions:
Setting scroll bar on a jscrollpane
Making a JScrollPane automatically scroll all the way down.

Any help in this regard would be very much appreciated.

Edit:

As per Devon_C_Miller's advice, I have an improved way of scrolling to the bottom, solving issue #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);
           }
       }
    });
}

I still have problem #2.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

∝单色的世界 2024-09-05 14:37:15

看一下 scrollRectToVisible(矩形 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
}

Take a look at the scrollRectToVisible(Rectangle r) and modelToView(int pos)

That should get you what you're looking for without disturbing the user's selection.

As for the scrollbar, try forcing the scroll and the append into occur on different events. For example:

if (atBottom) {
    // append new line & do scroll
    SwingUtilities.invokerLater(new Runnable(){
        public void run() {
            // append text
        }});
} else {
    // append newline
    // append text
}
栩栩如生 2024-09-05 14:37:15

根据之前的答案和进一步的 Google 搜索,我做了一个测试,其中线程连续将字符串附加到 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;
    }
}

Based on previous answers and further Google'ing I made a test in which a Thread continously appends Strings to a JTextArea in a JScrollPane. I think it is important to use invokeLater here, since the JScrollBar needs to be updated with its new maximum value before we scroll to that value. Using invokeLater, the AWT Thread will take care of this.

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;
    }
}
囚我心虐我身 2024-09-05 14:37:15

有点晚了,但我发现了一些东西: 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;
    }
}

A little late but here is something i found: http://www.camick.com/java/source/SmartScroller.java

In essence you can use following code:

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

And here the SmartScroller class:

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;
    }
}
怎会甘心 2024-09-05 14:37:15

查看此示例

尽管如此,我会更改代码以使用以下更有效的代码:

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

Check out this example.

Although, I would change the code to use the following which is more efficient:

caret.setDot(textArea.getDocument().getLength());
回忆那么伤 2024-09-05 14:37:15

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

I've spent the last few days trying different approaches to this exact problem. The best solution I've found is to replace the layout manager of JScrollPane's viewport and implement all the desired scrolling behavior there. The jumpy scrollbar knob is gone, and it's blazing fast -- so fast that it created another race condition I had to work around. Details are here: http://www.java-forums.org/awt-swing/56808-scroll-bar-knob-jumpy-when-component-growing.html

娇俏 2024-09-05 14:37:15
public static void scrollToBottom(JComponent component) 
{
    Rectangle visibleRect = component.getVisibleRect();
    visibleRect.y = component.getHeight() - visibleRect.height;
    component.scrollRectToVisible(visibleRect);
}

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

public static void scrollToBottom(JComponent component) 
{
    Rectangle visibleRect = component.getVisibleRect();
    visibleRect.y = component.getHeight() - visibleRect.height;
    component.scrollRectToVisible(visibleRect);
}

You can find the full details here: How to make JTextPane autoscroll only when scroll bar is at bottom and scroll lock is off?

庆幸我还是我 2024-09-05 14:37:15
DefaultCaret caret = (DefaultCaret)textArea.getCaret();  
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); 
DefaultCaret caret = (DefaultCaret)textArea.getCaret();  
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); 
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文