【问题标题】:"Smart" autoscrolling in Java?Java中的“智能”自动滚动?
【发布时间】:2013-02-26 15:21:48
【问题描述】:

注意:我正在重新提出this 的问题,因为我需要一个答案,而那里的答案链接已断开。

我正在编写一个简单的聊天程序,作为学校项目的一部分,我希望用户能够自动跟踪消息(如果它们位于底部),而不必担心再次向上滚动如果他们正在寻找更高的东西。目前,无论如何,我的代码都只是滚动到底部。基本代码如下,我真的不知道如何将其转换为 SSCCE。如果有人可以,请随时这样做。

JTextArea jta = new JTextArea(50, 50);
JScrollPane scroll = new JScrollPane(jta);
try {
    int iter = 0;
    while (true) { //Not a for loop because it should not end after x iterations
        Thread.sleep(500);
        jta.append("Test text #" + iter);
        iter++;
        //Check if they are at the bottom and if so scroll down to the new bottom.
    }
} catch (InterruptedException ex) {
    System.out.println("Error!");
}

【问题讨论】:

  • 最好在链接断开的答案下留下评论,询问回答者可以修复链接/提供另一个示例。
  • 如果您打算稍后分享您的发现,您为什么要现在发布问题?顺便说一句,您是否尝试了解发布代码背后的想法?
  • 为了更好的帮助,请尽快发布SSCCE,简短,可运行,可编译,不是代码剪切导致锁定事件调度线程,更多在 Oracles 教程 Concurency in Swing
  • 啊,我刚刚看到您确实在原始答案下留下了评论。但我认为该答案的作者应该得到比 17 分钟多一点的时间。从那时起他/她甚至都无法保证一直在线,更不用说找到一个有解决方案的新帖子了。
  • 旁注:JTextAreaJScrollPane 的创建必须在事件调度线程 (EDT) 上进行。在 EDT 上有一个 while(true) 循环将阻止该线程,这将确保您永远看不到您附加的文本。最好为此代码使用javax.swing.Timer。也不再记录 append 是线程安全的,这使得 Swing Timer 成为更好的选择

标签: java swing concurrency jscrollpane jtextarea


【解决方案1】:

不确定这是不是最好的方法,但这里有一些代码会在用户向上滚动时停止向下滚动。当他滚动回底部时,自动滚动再次开始。

这个想法是通过比较文本区域的高度和滚动窗格的可见矩形的位置和高度来检查用户是否向上移动。如果匹配,则意味着滚动在底部并且用户想要自动滚动。在另一种情况下,我们会在每次 textarea 更改时强制可见矩形保持不变。

SSCCE:

import java.awt.BorderLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class TestScrollbars {

    protected void initUI() {
        final JFrame frame = new JFrame();
        frame.setTitle(TestScrollbars.class.getSimpleName());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final JTextArea chat = new JTextArea(10, 40);
        chat.setLineWrap(true);
        chat.setEditable(false);
        chat.setWrapStyleWord(false);
        final JScrollPane scrollPane = new JScrollPane(chat);
        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        frame.setLayout(new BorderLayout());
        frame.add(scrollPane, BorderLayout.CENTER);
        frame.pack();
        frame.setVisible(true);
        Timer t = new Timer(200, new ActionListener() {

            private int i = 1;

            @Override
            public void actionPerformed(ActionEvent e) {
                final Rectangle visibleRect = chat.getVisibleRect();
                boolean scroll = chat.getHeight() <= visibleRect.y + visibleRect.height;
                chat.append("Hello line " + i++ + "\n");
                if (!scroll) {
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            chat.scrollRectToVisible(visibleRect);
                        }
                    });
                }
            }
        });
        t.start();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new TestScrollbars().initUI();
            }
        });
    }

}

【讨论】:

  • 我实际上已经在线程中实现了这个,所以Timer 有点多余。但是,这看起来像我想要的。我马上回来拿测试结果...
  • 是的,这看起来正是我想要的!谢谢!显然,我需要对其进行一些编辑以确保它在我的程序中工作,但除此之外它是完美的。
  • @NickHartley Timer 仅用于模拟您的循环/传入数据。此外,Swing Timer 在 EDT 上执行所有操作,避免了受保护的 UI 调用的需要(尽管在这种情况下,这不是必需的,因为 JTextArea.append() 是线程安全的)。
  • 我认为这是模拟一个循环,看到它一遍又一遍地重复。但是,我不知道 Timer 是在 EDT 上运行的……将来可能会派上用场。再次感谢 SSCCE 和信息。 :)
  • 无意冒犯,但我认为将来遇到这个问题的人会希望看到一个为他们完成所有工作的预制课程并且提供解释,如由下面的 camickr 制作。我只希望我能接受两个答案,因为这个也很棒。
【解决方案2】:

编辑:

我将以下代码替换为更灵活的版本,该版本适用于 JScrollPane 中的任何组件。查看:Smart Scrolling

这里有一些可重用的代码,任何包含 JTextArea 或 JTextPane 的滚动窗格都可以使用:

import java.awt.*;
import java.awt.event.*;
import java.util.Date;
import javax.swing.*;
import javax.swing.text.*;

public class ScrollControl implements AdjustmentListener
{
    private JScrollBar scrollBar;
    private JTextComponent textComponent;
    private int previousExtent = -1;

    public ScrollControl(JScrollPane scrollPane)
    {
        Component view = scrollPane.getViewport().getView();

        if (! (view instanceof JTextComponent))
            throw new IllegalArgumentException("Scrollpane must contain a JTextComponent");

        textComponent = (JTextComponent)view;

        scrollBar = scrollPane.getVerticalScrollBar();
        scrollBar.addAdjustmentListener( this );
    }

    @Override
    public void adjustmentValueChanged(final AdjustmentEvent e)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            public void run()
            {
                checkScrollBar(e);
            }
        });
    }

    private void checkScrollBar(AdjustmentEvent e)
    {
        //  The scroll bar model contains information needed to determine the
        //  caret update policy.

        JScrollBar scrollBar = (JScrollBar)e.getSource();
        BoundedRangeModel model = scrollBar.getModel();
        int value = model.getValue();
        int extent = model.getExtent();
        int maximum = model.getMaximum();
        DefaultCaret caret = (DefaultCaret)textComponent.getCaret();

        //  When the size of the viewport changes there is no need to change the
        //  caret update policy.

        if (previousExtent != extent)
        {
            //  When the height of a scrollpane is decreased the scrollbar is
            //  moved up from the bottom for some reason. Reposition the
            //  scrollbar at the bottom

            if (extent < previousExtent
            &&  caret.getUpdatePolicy() == DefaultCaret.UPDATE_WHEN_ON_EDT)
            {
                scrollBar.setValue( maximum );
            }

            previousExtent = extent;
            return;
        }

        //  Text components will not scroll to the bottom of a scroll pane when
        //  a bottom inset is used. Therefore the location of the scrollbar,
        //  the height of the viewport, and the bottom inset value must be
        //  considered when determining if the scrollbar is at the bottom.

        int bottom = textComponent.getInsets().bottom;

        if (value + extent + bottom < maximum)
        {
            if (caret.getUpdatePolicy() != DefaultCaret.NEVER_UPDATE)
                caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
        }
        else
        {
            if (caret.getUpdatePolicy() != DefaultCaret.UPDATE_WHEN_ON_EDT)
            {
                caret.setDot(textComponent.getDocument().getLength());
                caret.setUpdatePolicy(DefaultCaret.UPDATE_WHEN_ON_EDT);
            }
        }
    }

    private static void createAndShowUI()
    {
        JPanel center = new JPanel( new GridLayout(1, 2) );
        String text = "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n";

        final JTextArea textArea = new JTextArea();
        textArea.setText( text );
        textArea.setEditable( false );
        center.add( createScrollPane( textArea ) );
        System.out.println(textArea.getInsets());

        final JTextPane textPane = new JTextPane();
        textPane.setText( text );
        textPane.setEditable( false );
        center.add( createScrollPane( textPane )  );
        textPane.setMargin( new Insets(5, 3, 7, 3) );
        System.out.println(textPane.getInsets());

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(center, BorderLayout.CENTER);
        frame.setSize(500, 200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);

        Timer timer = new Timer(2000, new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                try
                {
                    Date now = new Date();
                    textArea.getDocument().insertString(textArea.getDocument().getLength(), "\n" + now.toString(), null);
                    textPane.getDocument().insertString(textPane.getDocument().getLength(), "\n" + now.toString(), null);
                }
                catch (BadLocationException e1) {}
            }
        });
        timer.start();
    }

    private static JComponent createScrollPane(JComponent component)
    {
        JScrollPane scrollPane = new JScrollPane(component);
        new ScrollControl( scrollPane );

        return scrollPane;
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowUI();
            }
        });
    }
}

编辑:

更新了代码,使其在滚动窗格的高度降低时也能正常工作。由于某种原因,默认行为是将滚动条向上移动一行,这意味着添加新文本时滚动条将不再保留在底部。

【讨论】:

  • 做这个很棒,但我将如何使用它?我会只创建这个类的一个对象,而该类会做其他所有事情吗?还是我必须做其他事情才能让它工作?
  • 不过,这似乎是我的问题的另一个很好的解决方案,如果我能弄清楚,我可能会接受这个。可惜我不能同时接受...
  • 创建 JTextArea 并将其添加到 JScrollPane 后,只需一行代码即可使用它:new ScrollControl( scrollPane ); 所有 Timer 代码只是为了证明该课程有效。在您的应用程序中,您只需像当前一样将文本添加到文本区域,滚动将自动发生。
  • 另外,您可能想在星期一回来查看。我计划发布该类的更通用版本,该版本应该适用于任何滚动窗格。那就是滚动窗格可以包含 JTextArea、JTextPane、JList、JTable...
  • 哇。太棒了,创建一个只需要一行代码的类,并且几乎可以处理所有可以滚动的东西......顺便说一句,你可以使用“`”标记来显示代码,就像它在帮助中所说的那样.它只是更容易分辨它是代码,而不是强调的普通语句。
【解决方案3】:

旧的 Apple MacOS 方式是有可能拥有(几个)自动拆分窗格,这些窗格可以从下方向上撕裂,然后再次掉落。

他们会在同一个 StyledDocument 上创建多个视图(文本框),其中只有底部的视图会自动滚动。

【讨论】:

  • 我应该为所有平台制作这个,而不仅仅是任何特定平台,这就是我选择使用 Java 的原因。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多