SwingUtilites: как возвращать значения из другого потока в Java?
Я пытаюсь сделать приложение на Java. Чтобы сделать Swing работать правильно, я сделал это:
public static void main(String[] array){
String outerInput;
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run() {
// I want this string input.
String input = JOptionPane.showInputDialog(
null,"Stop ?", JOptionPane.QUESTION_MESSAGE);
});
// How can I get this input value in String outerInput?
}
Как мне получить эту входную строку в моем основном теле?
7 ответов
как мне получить эту входную строку в моем основном теле?
ты бы не стал. Идея, что ваш "main" вызовет диалоговое окно Swing, а затем сделает что-то с результатами, противоречит всей идее графического пользовательского интерфейса.
в GUI вы разрабатываете свою программу для работы с серией событий, инициированных пользователем. Эти события могут быть полностью асинхронными, например нажатия клавиш, выбор и выбор меню вашего обычного текстового процессора. Или они могут быть написаны по сценарию, например, в формате "Вопрос-ответ" мастера."
предполагая, что вы хотите сделать что-то вроде последнего, вы бы реализовать его, используя следующую последовательность:
- пользователь инициирует некоторое действие, возможно, выбрав пункт меню. Это превращается в вызов
ActionListener
, который решает, что ему нужно больше ввода от пользователя. - на
ActionListener
, который выполняется в потоке отправки событий, разрешено делать все, что он хочет для пользовательского интерфейса, например, отображение диалога. Этот диалог может быть модальным или немодальным; в одном случае вывод доступен исходному слушателю, в другом вам нужно написать новый слушатель для принятия последующих действий. - как только у вас будет достаточно информации, вы можете вызвать фоновую операцию. Обычно для обслуживания этих запросов используется пул потоков. Вы не попытаетесь выполнить запрос в "основном" потоке; фактически, для всех намерений основной поток больше не работает.
- когда ваша операция завершится, она будет возвращать данные в поток отправки событий с помощью
SwingUtilities.invokeLater()
. В то время как вы могли бы использоватьinvokeAndWait()
чтобы отправить результаты, чтобы качаться в середине фоновой операции, это редко хорошая идея. Вместо этого создайте последовательность операций, предпочтительно ту, которая легко отменяется пользователем.
"стандартный" способ инициировать операции в фоновом потоке-через SwingWorker. Существуют альтернативы; например, вы можете использовать BlockingQueue
для отправки операций в один длительный фоновый поток и использования invokeLater()
для возврата результатов.
независимо от того, есть одно правило, что вы не хотите сломать: никогда, никогда не выполняйте операцию блокировки в потоке отправки событий. Если вы это сделаете, то ваше приложение сломанные.
можно использовать AtomicReference<String>
для передачи значений между потоками в потокобезопасным способом.
Как отметил Hemal, вам понадобится некоторая синхронизация между двумя потоками, чтобы убедиться, что она уже выполнена. Например, можно использовать CountDownLatch или SwingUtilities.invokeAndWait (убедитесь, что вы не вызываете его из Swing thread!)
Update: вот полный пример использования метод atomicreference и CountDownLatch за
public class Main {
public static void main(String[] args) throws InterruptedException {
final AtomicReference<String> result = new AtomicReference<String>();
final CountDownLatch latch = new CountDownLatch(1);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
String input = JOptionPane.showInputDialog(null, "Stop?", "Stop?", JOptionPane.QUESTION_MESSAGE);
result.set(input);
// Signal main thread that we're done and result is set.
// Note that this doesn't block. We never call blocking methods
// from Swing Thread!
latch.countDown();
}
});
// Here we need to wait until result is set. For demonstration purposes,
// we use latch in this code. Using SwingUtilities.invokeAndWait() would
// be slightly better in this case.
latch.await();
System.out.println(result.get());
}
}
Читайте также ответ об общем дизайне приложений GUI (и Swing).
прямо сейчас у вас есть два потока: основной поток и EDT (поток отправки событий). Я полагаю, вы знаете, что SwingUtilities.invokeLater(runnable)
выполняется задача на EDT.
чтобы обмениваться данными между потоками, вам просто нужна переменная, которая находится в области обоих потоков. Самый простой способ сделать это-объявить volatile
элемент данных или AtomicReference
в классе, содержащем метод main.
для того, чтобы убедиться, что вы прочитать значение после это возвращенный JOptionPane
, самое простое, что вы можете сделать здесь, это изменить вызов invokeLater на invokeAndWait
звонок. Это заставит ваш основной поток прекратить выполнение до тех пор, пока то, что вы положили на EDT, не завершится.
Ex:
public class MyClass {
private static volatile String mySharedData;
public static void main(String[] args) throws InterruptedException {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
mySharedData = JOptionPane.showInputDialog(null, "Stop ?", JOptionPane.QUESTION_MESSAGE);
}
});
// main thread is blocked, waiting for the runnable to complete.
System.out.println(mySharedData);
}
}
если ваш основной поток выполняет какую-то задачу, которая не должна быть остановлена, пока присутствует панель опций, то в основном потоке вы можете периодически проверять (т. е. во внешней части цикла, в котором выполняется ваша задача), или не mySharedData
была установлена. Если ваша задача не выполняет цикл и вместо этого выполняет ввод-вывод или ожидание, вы можете использовать нить.прервать и mySharedData
в обработчиках InterruptedExecption.
Я предлагаю использовать для этого шаблон observer/observable, возможно, с помощью PropertyChangeListener. Тогда ваше приложение Swing сможет уведомить всех слушателей, если критическая переменная(ы) состояние изменится.
например:
import java.awt.*;
import java.beans.*;
import javax.swing.*;
import javax.swing.event.*;
public class ListenToSwing {
public static final String STATE = "state";
private static final int STATE_MAX = 10;
private static final int STATE_MIN = -10;
private JPanel mainPanel = new JPanel();
private int state = 0;
private JSlider slider = new JSlider(STATE_MIN, STATE_MAX, 0);
public ListenToSwing() {
mainPanel.add(slider);
slider.setPaintLabels(true);
slider.setPaintTicks(true);
slider.setMajorTickSpacing(5);
slider.setMinorTickSpacing(1);
slider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
setState(slider.getValue());
}
});
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
mainPanel.addPropertyChangeListener(listener);
}
public Component getMainPanel() {
return mainPanel;
}
public void setState(int state) {
if (state > STATE_MAX || state < STATE_MIN) {
throw new IllegalArgumentException("state: " + state);
}
int oldState = this.state;
this.state = state;
mainPanel.firePropertyChange(STATE, oldState, this.state);
}
public int getState() {
return state;
}
public static void main(String[] args) {
final ListenToSwing listenToSwing = new ListenToSwing();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFrame frame = new JFrame("ListenToSwing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(listenToSwing.getMainPanel());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
listenToSwing.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals(ListenToSwing.STATE)) {
System.out.println("New state: " + listenToSwing.getState());
}
}
});
}
}
вы можете использовать AtomicReference и invokeAndWait.
public static void main(String[] array){
AtomicReference<String> outerInput = new AtomicReference<String>();
SwingUtilities.invokeAndWait(new Runnable(){
@Override
public void run() {
String input = JOptionPane.showInputDialog(
null,"Stop ?", JOptionPane.QUESTION_MESSAGE);
outerInput.set(input);
});
outerInput.get(); //Here input is returned.
}
вы можете тривиально выставить его внешнему классу, объявив String[]
в котором runnable устанавливает значение. Но обратите внимание, что вам понадобится некоторый механизм синхронизации, чтобы узнать, был ли он назначен Runnable
.
следующий код будет делать то, что вы хотите. Я делал нечто похожее только я запускаю JFileChooser
вместо диалогового окна ввода. Я нашел это более удобным, чем жесткое кодирование группы путей в моем приложении или принятие аргумента командной строки, по крайней мере, для целей тестирования. Я хотел бы добавить, что можно изменить prompt()
метод, чтобы возвратить FutureTask
экземпляр для дополнительной гибкости.
public class Question {
public static void main(String[] args) {
Question question = new Question();
String message = "Stop?";
System.out.println(message);
// blocks until input dialog returns
String answer = question.ask(message);
System.out.println(answer);
}
public Question() {
}
public String ask(String message) {
try {
return new Prompt(message).prompt();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return null;
}
private class Prompt implements Callable<String> {
private final String message;
public Prompt(String message) {
this.message = message;
}
/**
* This will be called from the Event Dispatch Thread a.k.a. the Swing
* Thread.
*/
@Override
public String call() throws Exception {
return JOptionPane.showInputDialog(message);
}
public String prompt() throws InterruptedException, ExecutionException {
FutureTask<String> task = new FutureTask<>(this);
SwingUtilities.invokeLater(task);
return task.get();
}
}
}