简体   繁体   中英

AWTEvent and EventQueue

I have an external device that is sending me data 1 character at a time. I'm writing this to a StyledDocument on a JTextPane. This data is sent to me on a thread that is not the AWT thread so I need to create AWTEvents and push them to the EventQueue so AWT handles the writing so that I do not get an Exception.

I have a funny issue now...

My text is printing to the document backwards.

This is obviously because I am pushing the characters to the Event queue 1 at a time as i receive them. A queue is obviously last pushed is first popped. I'm trying to thing of a way that I can fire the event before I add a new one or something similar so that I can get the events to fire in order.

http://www.kauss.org/Stephan/swing/index.html is the example I used to create the events.

private class RUMAddTextEvent extends AWTEvent {

    public static final int EVENT_ID = AWTEvent.RESERVED_ID_MAX + 1;
    private int index;
    private String str;
    private AttributeSet as;

    public RUMAddTextEvent(Component target, int index, String str, AttributeSet as) {
        super(target, EVENT_ID);
        this.index = index;
        this.str = str;
        this.as = as;
    }

    public int getIndex() {
        return index;
    }

    public String getStr() {
        return str;
    }

    public AttributeSet getAs() {
        return as;
    }
}

private class RUMRemoveTextEvent extends AWTEvent {

    public static final int EVENT_ID = AWTEvent.RESERVED_ID_MAX + 1;
    int index;
    int size;

    RUMRemoveTextEvent(Component target, int index, int size) {
        super(target, EVENT_ID);
        this.index = index;
        this.size = size;
    }

    public int getIndex() {
        return index;
    }

    public int getSize() {
        return size;
    }
}

/**
 * Prints a character at a time to the RUMComm window.
 *
 * @param c
 */
public void simpleOut(Character c) {
    cursor.x++;
    if (lines.isEmpty()) {
        this.lines.add(c.toString());
    } else {
        this.lines.add(cursor.y, this.lines.get(cursor.y).concat(c.toString()));
        this.lines.remove(cursor.y + 1);

    }

    try {
        //doc.insertString(doc.getLength(), c.toString(), as);

        eventQueue = Toolkit.getDefaultToolkit().getSystemEventQueue();
        eventQueue.postEvent(new RUMAddTextEvent(this, doc.getLength(), c.toString(), as));
        getCaret().setDot(doc.getLength());
    } catch (Exception ex) {
        //Exceptions.printStackTrace(ex);
    }
}

/**
 * Creates a new line
 */
public void newLine() {
    cursor.y++;
    cursor.x = 0;
    this.lines.add("");

    //doc.insertString(doc.getLength(), "\n", null);
    eventQueue = Toolkit.getDefaultToolkit().getSystemEventQueue();
    eventQueue.postEvent(new RUMAddTextEvent(this, doc.getLength(), "\n", null));
    getCaret().setDot(doc.getLength());

}

/**
 * Backspace implementation.
 *
 */
public void deleteLast() {
    int endPos = doc.getLength();
    //doc.remove(endPos - 1, 1);
    eventQueue = Toolkit.getDefaultToolkit().getSystemEventQueue();
    eventQueue.postEvent(new RUMRemoveTextEvent(this, endPos - 1, 1));
    cursor.x--;

}

 @Override
protected void processEvent(AWTEvent awte) {
    //super.processEvent(awte);
    if (awte instanceof RUMAddTextEvent) {
        RUMAddTextEvent ev = (RUMAddTextEvent) awte;
        try {
            doc.insertString(ev.getIndex(), ev.getStr(), ev.getAs());
        } catch (BadLocationException ex) {
            Exceptions.printStackTrace(ex);
        }
    } else if (awte instanceof RUMRemoveTextEvent) {
        RUMRemoveTextEvent ev = (RUMRemoveTextEvent) awte;
        try {
            doc.remove(ev.getIndex(), ev.getSize());
        } catch (BadLocationException ex) {
            Exceptions.printStackTrace(ex);
        }

    } else {
        super.processEvent(awte);
    }
}

I can only encourage people who try to take care of the Swing threading rules. However, despite your efforts to create events and push them onto the EventQueue you still access the Swing component on the background Thread with the calls for the length of the document and the altering of the caret position.

Extending a text component in order to set text on it looks like overkill to me, and certainly not the best way to approach this problem. Personally I would let the background Thread fill a buffer and flush that buffer to the text document once in a while (eg at each new line, twice a second using a Timer, each time I reach 1000 characters, ... ). For the update, I would simply use SwingUtilities.invokeLater .

Some example code to illustrate this, retrieved from an old project of mine. It won't compile but it illustrates my point. The class appends String s to an ISwingLogger which should be accessed on the EDT. Note the usage of the javax.swing.Timer to have periodical updates on the EDT.

import javax.swing.Timer;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class SwingOutputStream extends ByteArrayOutputStream{
  private final ISwingLogger fSwingLogger;

  private Timer fTimer;
  private final StringBuilder fBuffer = new StringBuilder( 1000 );

  public SwingOutputStream( ISwingLogger aSwingLogger ) {
    fSwingLogger = aSwingLogger;

    fTimer = new Timer( 200, new ActionListener() {
      public void actionPerformed( ActionEvent aActionEvent ) {
        flushBuffer();
      }
    } );
    fTimer.setRepeats( false );
  }

  @Override
  public void flush() throws IOException {
    synchronized( fBuffer ){
      fBuffer.append( toString( "UTF-8") );
    }
    if ( fTimer.isRunning() ){
      fTimer.restart();
    } else {
      fTimer.start();
    }

    super.flush();
    reset();
  }

  private void flushBuffer(){
    synchronized ( fBuffer ){
      final String output = fBuffer.toString();
      fSwingLogger.appendString( output );
      fBuffer.setLength( 0 );
    }
  }
}

Your actual issue isn't that the events are processed out of order, it's that you're creating the events with soon-to-be-out-of-date information:

new RUMAddTextEvent(this, doc.getLength(), ...

When the event is submitted, the doc may have length 0, but that might not be true by the time the event is processed. You could resolve that by having an AppendTextEvent instead of specifying the index, though if you have position-based insertions, too, you'll have to account for those.

An alternative option for the whole lot is to use an invalidate model:

// in data thread
void receiveData(String newData) {
    updateModelText(newData);
    invalidateUI();
    invokeLater(validateUI);
}

// called in UI thread
void validateUI() {
    if (!valid) {
        ui.text = model.text;
    }
    valid = true;
}

IMHO, you've taken something which should have been simple and made it more complicated then it needs to be (I'm a real master of this by the way ;))

Without knowing you exact problem, it's difficult to be 100%, but this small example demonstrates how I might approach the same problem using SwingUtilities.invokeLater/invokeAndWait

public class EventTest {

    public static void main(String[] args) {

        JFrame frame = new JFrame();
        frame.setLayout(new BorderLayout());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 400);
        JTextArea area = new JTextArea();
        frame.add(new JScrollPane(area));
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);

        Thread thread = new Thread(new Dispatcher(area));
        thread.setDaemon(true);
        thread.start();

    }

    public static class Dispatcher implements Runnable {

        private String message = "Hello from the other side\n\nCan you read this\n\nAll done now";
        private JTextArea area;

        public Dispatcher(JTextArea area) {
            this.area = area;
        }

        @Override
        public void run() {

            int index = 0;
            while (index < message.length()) {

                try {
                    Thread.sleep(250);
                } catch (InterruptedException ex) {
                }

                send(message.charAt(index));
                index++;

            }

        }

        public void send(final char text) {

            System.out.println("Hello from out side the EDT - " + EventQueue.isDispatchThread());

            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("Hello from the EDT - " + EventQueue.isDispatchThread());
                        area.append(new String(new char[]{text}));
                    }
                });
            } catch (Exception exp) {
                exp.printStackTrace();
            }

        }

    }

}

In you case, I would probably make a series of runnables that were capable of handling each update you want to make.

For example, I might create a InsertRunnable and DeleteRunnable (from you sample code) that could, insert a string at the current location or remove a string/characters from the current caret location.

To these I would pass the required information and let them take care of the rest.

...as i receive them. A queue is obviously last pushed is first popped. I'm trying to...

You have some conceptual mistake here. A queue is FIFO , means first pushed in is first popped out. A stack, will be LIFO , means last pushed in is first popped out. I believe this is where your problem truly was.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM