[英]How to randomly communicate with a process without freezing the GUI in Java Swing?
我正在构建一个国际象棋 GUI 应用程序,其工作是显示棋盘和棋子并防止输入非法移动。
它还应该具有涉及与国际象棋引擎(例如 Stockfish)通信的功能。 这就是我现在正在努力解决的问题。 国际象棋引擎是一个使用 ProcessBuilder 访问的 exe 文件:
Process chessEngineProcess = new ProcessBuilder(chessEngineUrl).start();
InputStream processInputStream = chessEngineProcess.getInputStream();
OutputStream processOutputStream = chessEngineProcess.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(processOutputStream));
BufferedReader reader = new BufferedReader(new InputStreamReader(processInputStream));
我想将字符串(UCI 协议中的命令)发送到它通过连续输出文本几秒钟或更长时间来响应的引擎。 这会挂断 GUI。 我需要根据引擎的输出更新 GUI 中的 textArea(实时)。 这不会是一种一次性的操作。 我想在发生某些 GUI 事件(例如用户移动)时随机执行此操作(发送命令并实时更新 GUI)。
我知道我需要在另一个线程中进行流读取,并且我知道 SwingWorker,但我根本无法让它正常工作。
我尝试过的:由于流读取是一个阻塞操作(我们一直在等待引擎的输出),因此流读取线程永远不会终止。
考虑到这一点,我尝试创建一个扩展SwingWorker<Void, String>
的类,并设置和包含chessEngineProcess
(以及它的流读取器和写入器)作为私有成员变量。 我实现了doInBackground
和process
方法。 我在这个类中还有一个公共方法,用于向引擎发送命令。
public void sendCommandToEngine(String command) {
try {
writer.write(command + '\n');
writer.flush();
} catch (IOException e) {
JOptionPane.showMessageDialog(null, e.getMessage());
}
}
我在doInBackground
进行流读取,然后在process
方法中发布输出并更新 GUI。
当我从 GUI 类(例如从事件侦听器)向引擎发送命令时,这会导致非常奇怪的行为。 显示的输出(有时是部分,有时是完全?)错误,并且经常抛出异常。
我不知所措,非常绝望,所以请帮助! 这是一个非常重要的项目。 随意提出您认为可行的任何解决方案!
编辑:我得到一个空指针异常,并带有以下堆栈跟踪:
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
at Moves.Move.isMovePossible(Move.java:84)
at Moves.Move.executeMove(Move.java:68)
at gui.ChessBoard.performEngineMove(ChessBoard.java:328)
at gui.MainFrame.receiveEnginesBestMove(MainFrame.java:180)
at gui.EngineWorker.process(EngineWorker.java:91)
at javax.swing.SwingWorker$3.run(SwingWorker.java:414)
at sun.swing.AccumulativeRunnable.run(AccumulativeRunnable.java:112)
at javax.swing.SwingWorker$DoSubmitAccumulativeRunnable.run(SwingWorker.java:832)
at sun.swing.AccumulativeRunnable.run(AccumulativeRunnable.java:112)
at javax.swing.SwingWorker$DoSubmitAccumulativeRunnable.actionPerformed(SwingWorker.java:842)
at javax.swing.Timer.fireActionPerformed(Timer.java:313)
at javax.swing.Timer$DoPostEvent.run(Timer.java:245)
at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:756)
at java.awt.EventQueue.access$500(EventQueue.java:97)
at java.awt.EventQueue$3.run(EventQueue.java:709)
at java.awt.EventQueue$3.run(EventQueue.java:703)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:80)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:726)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)
一些细节:基本上我有一个“MainFrame”类,它是一个包含我所有 GUI 元素的 JFrame。 这是我向组件添加事件侦听器的地方。 在某些事件侦听器中,我调用sendCommandToEngine
。 这将在引擎开始发送响应时启动被阻止的doInBackground
。
如果process
方法检测到引擎输出了“最佳移动”,则它可以在chessBoard
(它是一个显示棋盘的 MainFrame 组件)上调用performEnginesMove
。
performEnginesMove
函数检查移动是否有效(可能),然后在棋盘上移动(借助 Move 类)。
出于某种原因,这行不通。
我为Process
和ProcessBuilder
类构建了一个委托,以展示应如何使用其余代码。 我分别称这些类为GameEngineProcess
和GameEngineProcessBuilder
。
GameEngineProcess
正在创建响应,这些响应是直接附加到玩家 GUI 的JTextArea
中的简单String
。 它实际上扩展了Thread
以使其异步运行。 所以这个具体类的实现不是你要的,而是用来模拟Process
类的。 我在这个类的响应中添加了一些延迟来模拟引擎生成它们所需的时间。
然后是自定义类OnUserActionWorker
,它扩展了SwingWorker
并异步执行您的要求:它接收来自引擎进程的响应并将它们转发到更新其JTextArea
的 GUI。 这个类在每个引擎请求中使用一次,即我们为用户在与 GUI 交互时创建的每个请求创建并执行这个类的新实例。 请注意,这并不意味着引擎会针对每个请求关闭并重新打开。 GameEngineProcess
启动一次,然后在整个游戏正常运行时间内保持运行。
我假设您有办法判断单个引擎请求是否已完成所有响应。 为了在我编写的这段代码中简单起见,存在一条消息(类型为String
),每次在流程流中写入以指示每个请求的响应结束。 这是END_OF_MESSAGES
常量。 因此,这让OnUserActionWorker
知道何时终止接收响应,以便稍后为每个新请求创建它的下一个实例。
最后是 GUI,它是一个JFrame
由一个JTextArea
和一个按钮网格组成,玩家可以与之交互并根据按下的按钮向引擎发送请求命令。 我再次使用String
作为命令,但我猜在这种情况下这可能也是您需要的。
遵循以下代码:
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.List;
import java.util.Objects;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingWorker;
public class Main {
//Just a simple 'flag' to indicate end of responses per engine request:
private static final String END_OF_MESSAGES = "\u0000\u0000\u0000\u0000";
//A class simulating the 'ProcessBuilder' class:
private static class GameEngineProcessBuilder {
private String executionCommand;
public GameEngineProcessBuilder(final String executionCommand) {
this.executionCommand = executionCommand;
}
public GameEngineProcessBuilder command(final String executionCommand) {
this.executionCommand = executionCommand;
return this;
}
public GameEngineProcess start() throws IOException {
final GameEngineProcess gep = new GameEngineProcess(executionCommand);
gep.setDaemon(true);
gep.start();
return gep;
}
}
//A class simulating the 'Process' class:
private static class GameEngineProcess extends Thread {
private final String executionCommand; //Actually not used.
private final PipedInputStream stdin, clientStdin;
private final PipedOutputStream stdout, clientStdout;
public GameEngineProcess(final String executionCommand) throws IOException {
this.executionCommand = Objects.toString(executionCommand); //Assuming nulls allowed.
//Client side streams:
clientStdout = new PipedOutputStream();
clientStdin = new PipedInputStream();
//Remote streams (of the engine):
stdin = new PipedInputStream(clientStdout);
stdout = new PipedOutputStream(clientStdin);
}
public OutputStream getOutputStream() {
return clientStdout;
}
public InputStream getInputStream() {
return clientStdin;
}
@Override
public void run() {
try {
final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(stdout));
final BufferedReader br = new BufferedReader(new InputStreamReader(stdin));
String line = br.readLine();
while (line != null) {
for (int i = 0; i < 10; ++i) { //Simulate many responses per request.
Thread.sleep(333); //Simulate a delay in the responses.
bw.write(line + " (" + i + ')'); //Echo the line with the index.
bw.newLine();
bw.flush();
}
bw.write(END_OF_MESSAGES); //Indicate termination of this particular request.
bw.newLine();
bw.flush();
line = br.readLine();
}
System.out.println("Process gracefull shutdown.");
}
catch (final InterruptedException | IOException x) {
System.err.println("Process termination with error: " + x);
}
}
}
//This is the SwingWorker that handles the responses from the engine and updates the GUI.
private static class OnUserActionWorker extends SwingWorker<Void, String> {
private final GameFrame gui;
private final String commandToEngine;
private OnUserActionWorker(final GameFrame gui,
final String commandToEngine) {
this.gui = Objects.requireNonNull(gui);
this.commandToEngine = Objects.toString(commandToEngine); //Assuming nulls allowed.
}
//Not on the EDT...
@Override
protected Void doInBackground() throws Exception {
final BufferedWriter bw = gui.getEngineProcessWriter();
final BufferedReader br = gui.getEngineProcessReader();
//Send request:
bw.write(commandToEngine);
bw.newLine();
bw.flush();
//Receive responses:
String line = br.readLine();
while (line != null && !line.equals(END_OF_MESSAGES)) {
publish(line); //Use 'publish' to forward the text to the 'process' method.
line = br.readLine();
}
return null;
}
//On the EDT...
@Override
protected void done() {
gui.responseDone(); //Indicate end of responses at the GUI level.
}
//On the EDT...
@Override
protected void process(final List<String> chunks) {
chunks.forEach(chunk -> gui.responsePart(chunk)); //Sets the text of the the text area of the GUI.
}
}
//The main frame of the GUI of the user/player:
private static class GameFrame extends JFrame implements Runnable {
private final JButton[][] grid;
private final JTextArea output;
private BufferedReader procReader;
private BufferedWriter procWriter;
public GameFrame(final int rows,
final int cols) {
super("Chess with remote engine");
output = new JTextArea(rows, cols);
output.setEditable(false);
output.setFont(new Font(Font.MONOSPACED, Font.ITALIC, output.getFont().getSize()));
final JPanel gridPanel = new JPanel(new GridLayout(0, cols));
grid = new JButton[rows][cols];
for (int row = 0; row < rows; ++row)
for (int col = 0; col < cols; ++col) {
final JButton b = new JButton(String.format("Chessman %02d,%02d", row, col));
b.setPreferredSize(new Dimension(b.getPreferredSize().width, 50));
b.addActionListener(e -> sendCommandToEngine("Click \"" + b.getText() + "\"!"));
gridPanel.add(b);
grid[row][col] = b;
}
final JScrollPane outputScroll = new JScrollPane(output);
outputScroll.setPreferredSize(gridPanel.getPreferredSize());
final JPanel contents = new JPanel(new BorderLayout());
contents.add(gridPanel, BorderLayout.LINE_START);
contents.add(outputScroll, BorderLayout.CENTER);
super.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
super.getContentPane().add(contents);
super.pack();
}
//Utility method to enable/disable all the buttons of the grid at once:
private void gridSetEnabled(final boolean enabled) {
for (final JButton[] row: grid)
for (final JButton b: row)
b.setEnabled(enabled);
}
//This is the method that sends the next request to the engine:
private void sendCommandToEngine(final String commandToEngine) {
gridSetEnabled(false);
output.setText("> Command accepted.");
new OnUserActionWorker(this, commandToEngine).execute();
}
public BufferedReader getEngineProcessReader() {
return procReader;
}
public BufferedWriter getEngineProcessWriter() {
return procWriter;
}
//Called by 'SwingWorker.process':
public void responsePart(final String msg) {
output.append("\n" + msg);
}
//Called by 'SwingWorker.done':
public void responseDone() {
output.append("\n> Response finished.");
gridSetEnabled(true);
}
@Override
public void run() {
try {
//Here you build and start the process:
final GameEngineProcess proc = new GameEngineProcessBuilder("stockfish").start();
//Here you obtain the I/O streams:
procWriter = new BufferedWriter(new OutputStreamWriter(proc.getOutputStream()));
procReader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
//Finally show the GUI:
setLocationRelativeTo(null);
setVisible(true);
}
catch (final IOException iox) {
JOptionPane.showMessageDialog(null, iox.toString());
}
}
}
public static void main(final String[] args) {
new GameFrame(3, 3).run(); //The main thread starts the game, which shows the GUI...
}
}
最后,我做出的另一个重要假设是,当用户与 GUI 交互时,GUI 会阻止输入(但会继续响应其他事件)。 这可以防止用户同时向引擎发出多个活动请求。 通过阻止输入,我只是意味着当您单击一个按钮时,首先禁用所有按钮,然后将命令发送到引擎。 当最近发出的请求的所有响应完成时,所有按钮都将重新启用。
如果您需要同时向单个引擎发出多个请求,那么您可能需要同步某些 GUI 方法的访问,并确保每个OnUserActionWorker
可以将其响应与其他响应区分开来。 所以那将是一个不同的故事,但如果这是你想要的,请告诉我。
要在接收响应时测试 EDT 的响应能力,您可以例如在仍然接收(十个)响应时简单地用鼠标调整窗口大小,或者只是注意响应实时打印到JTextArea
中。
希望能帮助到你。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.