如何在 JTextArea 中用四个空格替换制表符?

How can I replace a tab with four spaces in JTextArea?

我在 JFrame 中使用 JTextArea。我希望制表键插入四个空格而不是制表符。

方法 setTabSize 不起作用,因为它在文本区域的内容中放置了一个制表符 ('\t')。

如何让 JTextArea 在我按下 Tab 键时插入四个空格而不是一个制表符?这样,getText() 方法将为每个制表符 return 缩进四个空格。

我会避免使用 KeyListeners(作为 JTextComponents 的一般规则)甚至键绑定,因为虽然键绑定适用于键盘输入,但它不适用于 copy-and-paste。

在我看来,最好的方法是使用在 JTextArea 的文档(顺便说一下,它是一个 PlainDocument)上设置的 DocumentFilter。这样,即使您将文本复制并粘贴到带有制表符的 JTextAreas 中,所有制表符也会在插入时自动转换为 4 个空格。

例如:

import javax.swing.*;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
import javax.swing.text.PlainDocument;

public class TestTextArea {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JTextArea textArea = new JTextArea(20, 50);
            JScrollPane scrollPane = new JScrollPane(textArea);

            int spaceCount = 4;
            ((PlainDocument) textArea.getDocument()).setDocumentFilter(new ChangeTabToSpacesFilter(spaceCount));

            JFrame frame = new JFrame("GUI");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(scrollPane);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
    
    private static class ChangeTabToSpacesFilter extends DocumentFilter {
        private int spaceCount;
        private String spaces = "";
        
        public ChangeTabToSpacesFilter(int spaceCount) {
            this.spaceCount = spaceCount;
            for (int i = 0; i < spaceCount; i++) {
                spaces += " ";
            }
        }

        @Override
        public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr)
                throws BadLocationException {
            string = string.replace("\t", spaces);
            super.insertString(fb, offset, string, attr);
        }
        
        @Override
        public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
            super.remove(fb, offset, length);
        }
        
        @Override
        public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
                throws BadLocationException {
            text = text.replace("\t", spaces);
            super.replace(fb, offset, length, text, attrs);
        }
        
    }

}

所以现在,即使我将其中包含制表符的文档复制并粘贴到 JTextArea 中,所有制表符都会自动替换为 spaceCount 个空格。

这是“我想知道是否……”的时刻之一。

就我个人而言,我会尝试更直接地从源头上解决问题。这意味着以某种方式“捕获”Tab 事件并“替换”它的功能。

所以,我开始修改 http://www.java2s.com/Tutorial/Java/0260__Swing-Event/ListingtheKeyBindingsinaComponent.htm,它可以列出组件的键绑定,为此,我发现 JTextArea 使用 insert-tab 作为动作映射键。

然后我创建了自己的 Action 旨在在当前插入符位置插入空格,例如……

public class Main {

    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
//            insert-tab
            JTextArea ta = new JTextArea(10, 20);
            add(new JScrollPane(ta));

            ActionMap am = ta.getActionMap();
            am.put("insert-tab", new SpacesTabAction());
        }

        public class SpacesTabAction extends AbstractAction {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (!(e.getSource() instanceof JTextArea)) {
                    return;
                }
                JTextArea textArea = (JTextArea) e.getSource();
                int caretPosition = textArea.getCaretPosition();
                Document document = textArea.getDocument();
                try {
                    document.insertString(caretPosition, "  ", null);
                } catch (BadLocationException ex) {
                    ex.printStackTrace();
                }
            }

        }

    }
}

可接受的限制

这不会涵盖将文本粘贴到组件中,否则 DocumentFilter 会涵盖这些内容,但是,我想考虑 DocumentFilter 可能无法使用的场景(例如因为已经安装了一个)

更多调查

The method setTabSize does not work, as it puts a tab ('\t') in the contents of the text area.

这是那些“空格 vs 制表符”的争论之一吗:P

我做了一些摆弄,发现与 setTabSize 的大部分不一致都是因为没有使用固定宽度的字体,例如…

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

public class Main {

    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new BorderLayout());
            Font font = Font.decode("Courier New").deriveFont(12);
            JTextArea ta = new JTextArea(10, 40);
            ta.setFont(font);
            ta.setText("---|----|----|----|----|----|----|----|\n\tHello");
            ta.setTabSize(0);
            add(new JScrollPane(ta));

            DefaultComboBoxModel<Integer> model = new DefaultComboBoxModel<>(new Integer[] {0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24});
            JComboBox<Integer> tabSizes = new JComboBox<>(model);
            add(tabSizes, BorderLayout.NORTH);
            tabSizes.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    Integer tabSize = (Integer)tabSizes.getSelectedItem();
                    ta.setTabSize(tabSize);
                }
            });
        }

    }
}

当我将字体设置为“Courier New”时,我能够获得与设置的标签大小对齐的一致缩进

我显然遗漏了一些东西...

The tab is four spaces. In this "111\t" string, the tab is expands to 1 space; "22\t" — to 2 spaces; "3\t" — to 3 spaces; finally, "\t" expands to four spaces.

是的,这不是预期的行为吗?!

JTextArea, 4tabSize, 等宽字体

Sublime 文本编辑器,tabSize of 4,单间距字体

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Font;
import java.util.StringJoiner;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Main {

    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            StringJoiner textJoiner = new StringJoiner("\n");
            for (int row = 0; row < 10; row++) {
                StringJoiner rowJoiner = new StringJoiner("\t");
                for (int col = 0; col < 10; col++) {
                    rowJoiner.add(Integer.toString(col).repeat(row));
                }
                textJoiner.add(rowJoiner.toString());
            }

            setLayout(new BorderLayout());
            JTextArea ta = new JTextArea(10, 40);
            ta.setFont(new Font("Monospaced", Font.PLAIN, 13));
            ta.setText(textJoiner.toString());
            add(new JScrollPane(ta));
        }

        protected String replicate(char value, int times) {
            return new String(new char[times]).replace('[=12=]', value);
        }
    }

}

我认为您考虑的是“制表位”,而不是“制表符大小”for example,在这种情况下,即使 DocumentFilter 也不会达到您的预期.

I assume the use of monospace font is implied in the question

不要“假设”我们什么都知道。在我的实验中,字体不是等宽的。这是提供“预期”和“实际”结果以及 minimal reproducible example 的地方,因为它消除了歧义并且不会浪费每个人的时间(尤其是你的)

It is how it works in any text editor.

我显然在使用不同的文本编辑器