智能 JScrollPane 跟随输出文本

Smart JScrollPane follows output text

我有一个智能 JScrollPane 但与其他所有解决方案不同,我想知道如果滚动窗格不在底部,则视口是否可以跟随(向上滚动)查看的组件 (JTextArea) 并且如果文本缓冲区已满。包括功能齐全的代码片段。谢谢

public class ScrollingJTextAreaExample extends JFrame {
    // Worker thread to help periodically append example messages to JTextArea
    Timer timer = new Timer();
    // Merely informative counter, will be displayed with the example messages
    int messageCounter = 0;
    // GUI components
    JScrollPane jScrollPane;
    MyJTextArea jTextArea;

    public ScrollingJTextAreaExample() {
        initComponents(); // Boiler plate GUI construction and layout

        // Configure JTextArea to not update the cursor position after
        // inserting or appending text to the JTextArea. This disables the
        // JTextArea's usual behavior of scrolling automatically whenever
        // inserting or appending text into the JTextArea: we want scrolling
        // to only occur at our discretion, not blindly. NOTE that this
        // breaks normal typing into the JTextArea. This approach assumes
        // that all updates to the ScrollingJTextArea are programmatic.
        DefaultCaret caret = (DefaultCaret) jTextArea.getCaret();
        caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);

        // Schedule a task to periodically append example messages to jTextArea
        timer.schedule(new TextGeneratorTask(), 250, 250);

        // This DocumentListener takes care of re-scrolling when appropriate
        Document document = jTextArea.getDocument();
        document.addDocumentListener(new ScrollingDocumentListener());
    }

    // Boring, vanilla GUI construction and layout code
    private void initComponents() {
        jScrollPane = new javax.swing.JScrollPane();
        jTextArea = new MyJTextArea();
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        jScrollPane.setViewportView(jTextArea);
        getContentPane().add(jScrollPane, java.awt.BorderLayout.CENTER);
        setSize(320, 240);
        setLocationRelativeTo(null);
    }

    // ScrollingDocumentListener takes care of re-scrolling when appropriate
    class ScrollingDocumentListener implements DocumentListener {
        public void changedUpdate(DocumentEvent e) {
            maybeScrollToBottom();
        }

        public void insertUpdate(DocumentEvent e) {
            maybeScrollToBottom();
        }

        public void removeUpdate(DocumentEvent e) {
            maybeScrollToBottom();
        }

        private void maybeScrollToBottom() {
            JScrollBar scrollBar = jScrollPane.getVerticalScrollBar();
            boolean scrollBarAtBottom = isScrollBarFullyExtended(scrollBar);
            boolean scrollLock = Toolkit.getDefaultToolkit()
                    .getLockingKeyState(KeyEvent.VK_SCROLL_LOCK);
            if (scrollBarAtBottom && !scrollLock) {
                // Push the call to "scrollToBottom" back TWO PLACES on the
                // AWT-EDT queue so that it runs *after* Swing has had an
                // opportunity to "react" to the appending of new text:
                // this ensures that we "scrollToBottom" only after a new
                // bottom has been recalculated during the natural
                // revalidation of the GUI that occurs after having
                // appending new text to the JTextArea.
                EventQueue.invokeLater(new Runnable() {
                    public void run() {
                        EventQueue.invokeLater(new Runnable() {
                            public void run() {
                                scrollToBottom(jTextArea);
                            }
                        });
                    }
                });
            }
        }
    }

    class TextGeneratorTask extends TimerTask {
        public void run() {
            EventQueue.invokeLater(new Runnable() {
                public void run() {
                    String message = (++messageCounter)
                            + " Lorem ipsum dolor sit amet, consectetur adipisicing elit. \n";
                    jTextArea.appendText(message);
                }
            });
        }
    }

    public static boolean isScrollBarFullyExtended(JScrollBar vScrollBar) {
        BoundedRangeModel model = vScrollBar.getModel();
        return (model.getExtent() + model.getValue()) == model.getMaximum();
    }

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

    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new ScrollingJTextAreaExample().setVisible(true);
            }
        });
    }


    public class MyJTextArea extends JTextArea {
        DefaultCaret caret;
        static final int BUFFERSIZE = 100;

        public MyJTextArea() {
            setBorder(BorderFactory.createEtchedBorder());      
            setBackground(Color.black);
            setForeground(Color.white);
            setCaretColor(getForeground());
            setFont(new Font("Lucida Console", Font.PLAIN, 12));
            setLineWrap(false);            
            setEditable(false);
        }

        public void appendText(String text) {
            // When the line count in the text window > size of the buffer, 
            // replace the first line in buffer to give way to the last line, 
            // all lines in buffer shift one line up
            if (getLineCount() > BUFFERSIZE) {
                try {
                    replaceRange(null, getLineStartOffset(0), getLineEndOffset(0));
                } catch (Exception e) {
                    append("An exception occured in replacing the topmost entry in the buffer");
                    e.printStackTrace();
                    return;
                }
            }
            append(text);            
        }
    }
}

在您的示例中,您有一个 100 行的缓冲区。假设当您打开滚动锁定时,视口中的第一行位于第 40 行。

如果我理解您的要求,您希望第 40 行在您开始从文档中删除行后留在视口的顶部。因此,随着行被删除,滚动条将移动到滚动窗格的顶部。当第 40 行最终被删除时,滚动条将全部位于滚动窗格的顶部。

如果是这样,下面的(粗略的)代码可以给你一个想法:

public void removeUpdate(DocumentEvent e) {
    //maybeScrollToBottom();
    int lineHeight = jTextArea.getPreferredSize().height / jTextArea.getLineCount();
    JViewport viewport = jScrollPane.getViewport();
    Point position = viewport.getViewPosition();
    position.y -= lineHeight;

    if (position.y > 0 )
        viewport.setViewPosition(position);
}

我怀疑这段代码是否完全按照您想要的方式工作,因为一旦缓冲区已满,您就不能将滚动条向下拖动到底部以使视口随着行的添加而不断滚动。但它可能会给你一些想法。