Live updating one JtextArea from another JtextArea

2019-09-07 00:23发布

问题:

I am attempting to write a program that will display two Jtextareas, both of which are editible. The goal is to when you edit textAreaRom (entering a roman numeral) the second area (textAreaArab) will display the Arabic Number equivalent. But the thing is I can not get this to happen in real-time. I have read about using DocumentListeners but, this is one of the first times I have ever done GUI programming and I am not to sure on how I would implement it. Anything helps. I am new to GUI stuff as well as StackOverflow so be nice!

Note: My conversion methods all work perfectly.

public class ArabicToRomanGUI_Hard extends JFrame
{
private static final long serialVersionUID = 1L;
private static String input = "";
private static String output = "";

//constructor to add text fields to frame
public ArabicToRomanGUI_Hard() 
{   
    //JFrame frame = new JFrame("Convert Back And Forth");
    final JTextField enterRomNumber = new JTextField(20);
    final JTextArea textAreaRom = new JTextArea(20,20);
    final JTextField enterArabNumber = new JTextField(20);
    final JTextArea textAreaArab = new JTextArea(20,20);
    setLayout(new FlowLayout());
    enterRomNumber.setText("Please enter a Roman numeral");
    enterArabNumber.setText("Please enter a Arabic Number");

    textAreaRom.setEditable(true);
    textAreaArab.setEditable(true);

    //textAreaRom.setText(enterRomNumber.getText());
    //textAreaArab.setText(enterArabNumber.getText());

    if(textAreaRom.isFocusOwner() == true)
    {

        textAreaRom.addKeyListener(new KeyAdapter()
            {
                public void keyReleased(KeyEvent e) 
                {      
                    {
                        e.getKeyChar();
                        input += e.getKeyChar();
                        ConversionLogic_Hard.ConvertFromRomanToArabic(input); //convert
                        ConversionLogic_Hard.getCheckFail(); //check if conversion is valid.

                        output = ConversionLogic_Hard.getConvertedRomanNumeral(); //get the conversion

                        while(ConversionLogic_Hard.getCheckFail() == true && textAreaArab.isFocusOwner() == false)
                        {
                            textAreaArab.setText(output);
                        }
                        textAreaArab.setText(input);
                    }
                }
            });             
        }



    getContentPane().add(enterRomNumber, BorderLayout.EAST);
    getContentPane().add(textAreaRom, BorderLayout.WEST);
}

回答1:

You actually have three problems...

  1. Updating a second text component when the first is updated, which is, relatively easy.
  2. Update the first text component when the second is updated...well, that got REAL complicated REAL fast.
  3. Filter the text as it's been entered. This is actually relatively easy by Implementing a Document Filter

Now, the real problem is #2, this is because you could end up in a nasty place real fast as the first field tries to update the second field, which triggers and event which causes the second field to update the first field which triggers and event which causes the first field to update the second field ... and you start to get the idea.

Fortunately, Swing actually won't let it get that bad, and will throw a IllegalStateException

To over come this, you need to have some kind of way to know when an update to a Document is been triggered by the slave field or when it's been triggered by something else (like setText been called, the user typing or pasting text into the field) and ignore some of those events

Now, there's probably a really cool and simply way to do this, but my brain is not in "simple" mode today, sorry.

So, I start with a custom Document, this basically has a flag which allows me to inspect its state

public class MirrorDocument extends PlainDocument {

    private boolean ignoreUpdates;

    public void setIgnoreUpdates(boolean ignoreUpdates) {
        this.ignoreUpdates = ignoreUpdates;
    }

    public boolean isIgnoreUpdates() {
        return ignoreUpdates;
    }

}

Now, I define a DocumentListener to monitor changes to a Document. This DocumentListener takes a "slave" Document which is used by this listener to update

public static class DocumentHandler implements DocumentListener {

    private MirrorDocument slaveDocument;
    private boolean ignoreUpdates = false;

    public DocumentHandler(MirrorDocument slaveDocument) {
        this.slaveDocument = slaveDocument;
    }

    @Override
    public void insertUpdate(DocumentEvent e) {
        Document doc = e.getDocument();
        if (doc instanceof MirrorDocument) {
            MirrorDocument md = (MirrorDocument) doc;
            if (!md.isIgnoreUpdates()) {
                try {
                    String text = e.getDocument().getText(e.getOffset(), e.getLength());
                    slaveDocument.setIgnoreUpdates(true);
                    slaveDocument.insertString(e.getOffset(), text, null);
                } catch (BadLocationException ex) {
                    ex.printStackTrace();
                } finally {
                    slaveDocument.setIgnoreUpdates(false);
                }
            }
        }
    }

    @Override
    public void removeUpdate(DocumentEvent e) {
        Document doc = e.getDocument();
        if (doc instanceof MirrorDocument) {
            MirrorDocument md = (MirrorDocument) doc;
            if (!md.isIgnoreUpdates()) {
                try {
                    slaveDocument.setIgnoreUpdates(true);
                    slaveDocument.remove(e.getOffset(), e.getLength());
                } catch (BadLocationException ex) {
                    ex.printStackTrace();
                } finally {
                    slaveDocument.setIgnoreUpdates(false);
                }
            }
        }
    }

    @Override
    public void changedUpdate(DocumentEvent e) {
    }

}

Now, the really tripy part here is around the ignoreUpdates flag. Basically, what this does is when a event occurs, first we check the ignoreUpdates flag of the Document which triggered the event, if it's false, we proceeded to set the ignoreUpdates flag of the slaveDocument to true, which prevents it's DocumentListener from processing any new events and then update the slaveDocument

Okay, this might seem a bit weird, and I'm sorry, but trust me, this will make sense...(one day ... and when it does, you can explain it back to me)

So, next, we need to create everything and glue it altogether...

    JTextArea left = new JTextArea(10, 20);
    JTextArea right = new JTextArea(10, 20);

    MirrorDocument leftDoc = new MirrorDocument();
    MirrorDocument rightDoc = new MirrorDocument();

    left.setDocument(leftDoc);
    right.setDocument(rightDoc);

    leftDoc.addDocumentListener(new DocumentHandler(rightDoc));
    rightDoc.addDocumentListener(new DocumentHandler(leftDoc));

So we create two JTextAreas, left and right. We create two MirrorDocuments, one for each JTextArea, we then create two DocumentHandlers, one for each JTextArea and provide the opposite Document as the slave (rightDoc for left and leftDoc for right), this allows for the event cross over and updating to occur

This allows us to set up something like...

import java.awt.EventQueue;
import java.awt.GridLayout;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;

public class MirrorTextAreas {

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

    public MirrorTextAreas() {
        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() {
            JTextArea left = new JTextArea(10, 20);
            JTextArea right = new JTextArea(10, 20);
            setLayout(new GridLayout(1, 2));

            add(new JScrollPane(left));
            add(new JScrollPane(right));

            MirrorDocument leftDoc = new MirrorDocument();
            MirrorDocument rightDoc = new MirrorDocument();

            left.setDocument(leftDoc);
            right.setDocument(rightDoc);

            leftDoc.addDocumentListener(new DocumentHandler(rightDoc));
            rightDoc.addDocumentListener(new DocumentHandler(leftDoc));
        }

    }

    public class MirrorDocument extends PlainDocument {

        private boolean ignoreUpdates;

        public void setIgnoreUpdates(boolean ignoreUpdates) {
            this.ignoreUpdates = ignoreUpdates;
        }

        public boolean isIgnoreUpdates() {
            return ignoreUpdates;
        }

    }

    public static class DocumentHandler implements DocumentListener {

        private MirrorDocument slaveDocument;
        private boolean ignoreUpdates = false;

        public DocumentHandler(MirrorDocument slaveDocument) {
            this.slaveDocument = slaveDocument;
        }

        @Override
        public void insertUpdate(DocumentEvent e) {
            Document doc = e.getDocument();
            if (doc instanceof MirrorDocument) {
                MirrorDocument md = (MirrorDocument) doc;
                if (!md.isIgnoreUpdates()) {
                    try {
                        String text = e.getDocument().getText(e.getOffset(), e.getLength());
                        slaveDocument.setIgnoreUpdates(true);
                        slaveDocument.insertString(e.getOffset(), text, null);
                    } catch (BadLocationException ex) {
                        ex.printStackTrace();
                    } finally {
                        slaveDocument.setIgnoreUpdates(false);
                    }
                }
            }
        }

        @Override
        public void removeUpdate(DocumentEvent e) {
            Document doc = e.getDocument();
            if (doc instanceof MirrorDocument) {
                MirrorDocument md = (MirrorDocument) doc;
                if (!md.isIgnoreUpdates()) {
                    try {
                        slaveDocument.setIgnoreUpdates(true);
                        slaveDocument.remove(e.getOffset(), e.getLength());
                    } catch (BadLocationException ex) {
                        ex.printStackTrace();
                    } finally {
                        slaveDocument.setIgnoreUpdates(false);
                    }
                }
            }
        }

        @Override
        public void changedUpdate(DocumentEvent e) {
        }

    }

}

Okay, so that's about half of what we need, next we need to be able to filter the content been entered into one of the fields so we can change, this can be accomplished by Implementing a Document Filter