Swing: прокрутить до нижней части JScrollPane, в зависимости от текущего местоположения области просмотра - PullRequest
10 голосов
/ 19 апреля 2010

Я пытаюсь имитировать функциональность 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. Поскольку продвижение панели прокрутки происходит после вставки текста, есть доля секунды, когда полоса прокрутки находится в неправильном положении, а затем она визуально скачет к концу. Это не идеально.

Прежде чем вы спросите, да, я прочитал этот пост в блоге на Прокрутка текстовой области , но прокрутка по умолчанию не соответствует желаемому.

Другие связанные (но, на мой взгляд, не совсем полезные) вопросы: Настройка полосы прокрутки на панели jscroll Автоматическая прокрутка JScrollPane до упора.

Любая помощь в этом отношении будет очень признательна.

Edit:

Согласно совету 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.

Ответы [ 7 ]

6 голосов
/ 20 апреля 2010

Посмотрите на 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
}
3 голосов
/ 14 декабря 2011

Основываясь на предыдущих ответах и ​​дальнейших действиях Google, я провел тест, в котором поток постоянно добавляет строки в JTextArea в JScrollPane. Я думаю, что здесь важно использовать invokeLater, поскольку JScrollBar необходимо обновить до нового максимального значения, прежде чем мы перейдем к этому значению. Используя invokeLater, AWT Thread позаботится об этом.

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;
    }
}
1 голос
/ 27 октября 2015

Немного поздно, но вот что я нашел: 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;
    }
}
1 голос
/ 19 марта 2012

Последние несколько дней я пробовал разные подходы к этой проблеме. Лучшее решение, которое я нашел, - это заменить менеджер макета окна просмотра JScrollPane и реализовать там все желаемое поведение прокрутки. Прыгающая ручка полосы прокрутки исчезла, и она сверкнула быстро - настолько быстро, что это создало еще одно условие гонки, с которым мне пришлось обходиться. Подробности здесь: http://www.java -forums.org / awt-swing / 56808-scroll-bar-knob-jumpy-when-component-growth.html

1 голос
/ 20 апреля 2010

Проверьте это пример .

Хотя я бы изменил код, чтобы использовать следующее, что более эффективно:

caret.setDot(textArea.getDocument().getLength());
0 голосов
/ 28 февраля 2013
DefaultCaret caret = (DefaultCaret)textArea.getCaret();  
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); 
0 голосов
/ 28 ноября 2012
public static void scrollToBottom(JComponent component) 
{
    Rectangle visibleRect = component.getVisibleRect();
    visibleRect.y = component.getHeight() - visibleRect.height;
    component.scrollRectToVisible(visibleRect);
}

Вы можете найти полную информацию здесь: Как сделать JTextPane автопрокруткой только тогда, когда полоса прокрутки находится внизу и блокировка прокрутки отключена?

...