调整大小时,JScrollPane 动态 RowHeader 不同步

JScrollPane dynamic RowHeader out of sync when resizing

我想实现一个 TextArea,它带有一个侧边栏,显示换行文本每行中最左边字符的索引。这是通过 JScrollPane 实现的,使用 RowHeader 显示数字 JTextPaneViewport 显示序列 JTextArea

除水平调整大小时 RowHeaderViewport 不同步外,一切正常。在下面的 SSCCE 中,它也发生在启动时。

图片说明:JTextArea的光标在第一个位置,而JTextPane的下端显示。

我知道它与 JTextPane 动态更改其内容有关,因为它适用于静态内容。

是否可以防止这种奇怪的行为?

package main;

import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Collections;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.Utilities;

public class SequenceAreaExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setSize(150, 200);

        SequenceArea sequenceArea = new SequenceArea();
        frame.add(sequenceArea);

        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        
        frame.setVisible(true);
        sequenceArea.setText(String.join("", Collections.nCopies(200, "a")));
    }
}

class SequenceArea extends JScrollPane implements ComponentListener {
    private static final long serialVersionUID = 1L;

    private JTextArea text;
    private JTextPane numbers;

    // FIXME text and numbers are out of sync when resizing horizontally
    SequenceArea() {
        text = new JTextArea();
        text.setLineWrap(true);
        text.setEnabled(true);
        text.addComponentListener(this);

        numbers = new JTextPane();
        numbers.setEnabled(false);

        setViewportView(text);
        setRowHeaderView(numbers);
    }

    void updateNumbers() {
        StringBuilder newNumbers = new StringBuilder();

        int length = text.getText().length();
        int index = 0;

        while (index < length) {
            try {
                int start = Utilities.getRowStart(text, index);
                int end = Utilities.getRowEnd(text, index);

                newNumbers.append(start);
                newNumbers.append('\n');

                index = end + 1;
            } catch (BadLocationException e) {
                break;
            }
        }

        numbers.setText(newNumbers.toString());
    }

    void setText(String t) {
        text.setText(t);
    }

    @Override
    public void componentResized(ComponentEvent e) {
        updateNumbers();
    }

    @Override
    public void componentMoved(ComponentEvent e) {}

    @Override
    public void componentShown(ComponentEvent e) {}

    @Override
    public void componentHidden(ComponentEvent e) {}
}

根据我的评论,我尝试构建一个替代解决方案,其中 header 行是视口的一部分,到目前为止它似乎工作正常。 (顺便说一句,在一开始就添加 SSCCE 的荣誉)

JPanel panel = new ScrollablePanel();        
panel.setLayout(new GridBagLayout());

//can be reused as constraints are only read when adding components
GridBagConstraints constraints = new GridBagConstraints();

//components should take all vertical space if possible.
constraints.weighty = 1.0;

//components should expand even if it doesn't need more space
constraints.fill = GridBagConstraints.BOTH;

//add the numbers component 
panel.add(numbers, constraints );

//add specific constraints for the text component        
//text takes as much of the width as it can get
constraints.weightx = 1.0;

//numbers component seems to have some insets so add 2px at the top to get better alignment - could be done differently as well
constraints.insets = new Insets(2, 0, 0, 0);
panel.add(text, constraints);
    
setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

    
setViewportView(panel);

为了使其正常工作,您还需要使用一个能够识别滚动并能够根据视口调整其尺寸的面板:

class ScrollablePanel extends JPanel implements Scrollable {
    public Dimension getPreferredScrollableViewportSize() {
        //the panel prefers to take as much height as possible
        return new Dimension(getPreferredSize().width, Integer.MAX_VALUE);
    }

    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
        return 1;
    }

    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
        return 1;
    }

    public boolean getScrollableTracksViewportWidth() {
        return true;
    }

    public boolean getScrollableTracksViewportHeight() {
        return true;
    }       
}