【问题标题】:Java Swing exception when notifying from an event callback从事件回调通知时的 Java Swing 异常
【发布时间】:2015-02-09 17:19:26
【问题描述】:

所以我有一个类似于控制台的 GUI。我希望用户在 JTextField 中输入文本并按 Enter。当用户按下回车键时,我使用键绑定进行回调。

现在我想创建一个名为 waitForInput() 的方法,它等待用户输入并返回它。我正在尝试的是下面。但是在回调函数中调用 notify() 会导致 java.lang.IllegalMonitorStateException。

public class MainWindow{
    private JFrame mainWindow;
    private JTextArea textEntry;
    private String inputStringMonitor = ""; // lock/user input value
    private Boolean stringReady = false;    //flag for wait while loop


    public MainWindow(){
        mainWindow = new JFrame("console");
        textEntry = new JTextArea();

        // set up key bindings
        InputAction = new UserInputAction();
        textEntry.getInputMap().put( KeyStroke.getKeyStroke( "ENTER" ),"EnterAction" );
        textEntry.getActionMap().put( "EnterAction", InputAction);

        //configure window
        mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        mainWindow.setMinimumSize(new Dimension(800,675));

        mainWindow.getContentPane().add(textEntry);
        mainWindow.pack();
        mainWindow.setVisible(true);
    }



    // callback action when user presses enter
    public class UserInputAction extends AbstractAction
    {
        public void actionPerformed( ActionEvent bp )
        {
            System.out.println( "enter pressed" );
            textEntry.setText("> ");
            textEntry.setCaretPosition(2);

            synchronized(inputStringMonitor){
                stringReady = true;
                inputStringMonitor = textEntry.getText();
                inputStringMonitor.notify();  //causes exception 
            }
        }

    }


    public String waitForInput() throws InterruptedException {
        String retval = "";

        synchronized(inputStringMonitor){
            stringReady = false;
            System.out.println("waiting");
            while(!stringReady){
                inputStringMonitor.wait();
            }
            retval = inputStringMonitor;
        }

        return retval;
    } 
}

【问题讨论】:

  • 这都是错误的。您通过在 actionPerformed(....) 方法中使用 synchronized 块并重新初始化同步 synchronized(inputStringMonitor){... inputStringMonitor = textEntry.getText(); ...} 的对象来阻止 EDT
  • 好的。那么有没有办法从 EDT 通知呢?我想做同样的事情,但分支一个新线程来做它,所以它不会阻塞?并使用专用锁修复重新初始化部分?
  • 你可以使用新线程来做等待和通知,你可以创建一个SwingWorker或者你可以使用invokeLater(..)方法。正如你所说,使用专用对象进行同步。
  • 字符串不是用作同步监视器的好对象,因为它可以被实习和共享。 new Object() 足以创建监视器。如果需要可序列化,new StringBuilder() 是另一种选择。

标签: java swing callback monitor


【解决方案1】:

认为我知道你想要做什么,如果是这样,我觉得我有一个更好的解决方案。如果我错了,请纠正我,但我认为您想创建一个其他程序可以使用的 GUI 文本输入窗口,并在输入文本时通知其他程序。如果是这样,那么更好的解决方案是使用已经存在于 Swing GUI 组件中的工具——PropertyChangeSupport。如果您想监听字符串状态的变化,则将字符串设置为“绑定”属性,通过触发 Swing 固有属性更改方法通知 GUI 其状态是否发生变化。这样,外部类可以注册为侦听器并收到此更改的通知。

例如,下面的类扩展了 JPanel,部分原因是这会给类一个 SwingPropertyChangeSupport 对象以及添加/删除 PropertyChangeListener 方法,但是如果您不想扩展 Swing 组件,您可以轻松滚动自己的通过添加您自己的 SwingPropertyChangeSupport 对象以及向您的类添加/删除 PropertyChangeListener 方法。

import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import javax.swing.*;

@SuppressWarnings("serial")
public class CallBackGui extends JPanel {
   // public constant for the propertyName
   public static final String TEXT_ENTRY = "text entry";

   private static final int ROWS = 20;
   private static final int COLUMNS = 40;
   private static final String CARET_MARKER = "> ";
   private JTextArea textEntryArea = new JTextArea(ROWS, COLUMNS);
   private String enteredText = ""; // "bound" property

   public CallBackGui() {
      textEntryArea.setText(CARET_MARKER);
      textEntryArea.setWrapStyleWord(true);
      textEntryArea.setLineWrap(true);
      JScrollPane scrollPane = new JScrollPane(textEntryArea);
      scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
      add(scrollPane);

      int condition = WHEN_FOCUSED;
      InputMap inputMap = textEntryArea.getInputMap(condition);
      ActionMap actionMap = textEntryArea.getActionMap();
      KeyStroke enterKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
      inputMap.put(enterKeyStroke, TEXT_ENTRY);
      actionMap.put(TEXT_ENTRY, new TextEntryAction());
   }

   public String getEnteredText() {
      return enteredText;
   }

   // or can make this private if you wish it to not be changed by outside forces
   public void setEnteredText(String enteredText) {
      String oldValue = this.enteredText;
      String newValue = enteredText;
      this.enteredText = enteredText; // change our bound property here

      // notify listeners here
      firePropertyChange(TEXT_ENTRY, oldValue, newValue);
   }

   // used by Key Bindings
   private class TextEntryAction extends AbstractAction {
      @Override
      public void actionPerformed(ActionEvent e) {
         // call method to set bound poperty
         setEnteredText(textEntryArea.getText().substring(CARET_MARKER.length()));
         textEntryArea.setText(CARET_MARKER);
      }
   }
}

然后,任何引用了显示的 CallBackGui 的外部类都可以在此对象上注册一个属性更改侦听器并获得通知。一个非常(过于)简单的例子:

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class TestCallBackGui {
   private static void createAndShowGui() {
      CallBackGui callBackGui = new CallBackGui();

      JFrame frame = new JFrame("CallBackGui");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(callBackGui);
      frame.pack();
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);

      // add our PropertyChangeListener
      callBackGui.addPropertyChangeListener(CallBackGui.TEXT_ENTRY, new PropertyChangeListener() {

         @Override
         public void propertyChange(PropertyChangeEvent evt) {
            System.out.println("Text Entered:");

            // result held by newValue 
            System.out.println(evt.getNewValue());
            // or can call callBackGui.getEnteredText()
         }
      });
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}

好处——避免所有低级等待/通知/同步代码,尤其是在 Swing 事件线程上使用这种类型的代码,而是使用更安全的高级构造。此外,由于 Swing 组件实际上使用了一个 SwingPropertyChangeSupport 对象,所有的回调都将在 Swing 事件线程上进行,如果监听程序也是一个 Swing GUI,这一点很重要。

【讨论】:

  • 您对我正在尝试做的事情非常正确。基本上,重点是我正在编写的程序驱动 GUI 比 GUI 驱动它更多。所以我想基本上在需要时从 GUI 请求信息,然后继续我的业务。如果我正确理解您的解决方案,那么它与我的非常相似,除了输入键的回调由外部构造拥有。但是,如果我想等待该回调,我仍然处于需要从回调中调用 notify() 的相同情况。
  • @PartyBuddha:请澄清以上内容。您所说的“等待回调是什么意思?PCListener 就是为了这个 - 通知使用代码回调已处理数据。请提供更具体的描述您的更大程序设计和您当前的问题。跨度>
  • 我希望我的主程序从 GUI 请求信息,然后停止执行,直到用户提供该信息。我最初想做的方式是让 GUI 提供一个请求输入的功能,然后阻塞直到提供该输入。这个想法是它通过请求信息(通过在 GUI 中显示一条消息)然后调用 wait() 来做到这一点。用户提供信息,然后按下回车,触发回调,调用 notify() 唤醒等待线程。您的解决方案将该回调的所有权授予主线程,但我仍然必须调用 wait()。
  • @PartyBuddha:不,我仍然认为不需要wait(),但让我们确定一下。如果您对自己感到好奇,请尝试创建一个功能最小的示例程序,类似于我在上面发布的内容,我们所有人都可以编译和运行而无需更改,不需要外部图像或数据库,除非图像是免费的可在线获取并通过 URL 获取,并将其作为对底部问题的编辑发布,mcve。如果你这样做,那么我可以操纵你的代码,看看我是否可以让它在没有等待的情况下运行。谢谢!
【解决方案2】:

好的。多亏了 Titus,这是一个解决方案

1) 在回调中使用 synchronized() 会阻塞 EDT,所以这很糟糕。而是使用 invokeLater() 异步通知。

// thread to branch off in order to notify
Runnable doNotify = new Runnable() {
    public void run() {
        synchronized(inputStringMonitor){
            userString = textEntry.getText();
            inputStringMonitor.notify();
        }
    }
};

// callback function
public void actionPerformed( ActionEvent bp )
{
    System.out.println( "enter pressed" );
    textEntry.setText("> ");
    textEntry.setCaretPosition(2);

    SwingUtilities.invokeLater(doNotify);
}

2) 分配给 inputStringMonitor 会重新初始化锁并将事情搞砸。而是使用专用锁和单独的字符串来存储实际数据。

【讨论】:

  • 这是一个危险的解决方案,因为您认为在后台线程中调用的代码实际上正在排队到 Swing 事件线程 - 与您尝试做的完全相反。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-25
  • 2013-08-07
  • 1970-01-01
  • 1970-01-01
  • 2013-05-03
相关资源
最近更新 更多