在JavaSwing中,如何在不冻结GUI的情况下与进程进行随机通信?
我正在构建一个国际象棋GUI应用程序,其任务是显示棋盘和棋子,防止非法移动被输入 它还应该具有与国际象棋引擎(如stockfish)通信的功能。这就是我现在正在努力解决的问题。chess引擎是一个exe文件,可使用ProcessBuilder访问:在JavaSwing中,如何在不冻结GUI的情况下与进程进行随机通信?,java,multithreading,swing,processbuilder,swingworker,Java,Multithreading,Swing,Processbuilder,Swingworker,我正在构建一个国际象棋GUI应用程序,其任务是显示棋盘和棋子,防止非法移动被输入 它还应该具有与国际象棋引擎(如stockfish)通信的功能。这就是我现在正在努力解决的问题。chess引擎是一个exe文件,可使用ProcessBuilder访问: Process chessEngineProcess = new ProcessBuilder(chessEngineUrl).start(); InputStream processInputStream = chessEngineProcess
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
,并将chessEngineProcess
(以及它的流读取器和写入器)设置为私有成员变量。我实现了doInBackground
和过程
方法。我在这个类中还有一个公共方法,用于向引擎发送命令
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
如果过程
方法检测到引擎输出了“最佳移动”,则可以在棋盘
(显示棋盘的大型机组件)上调用性能引擎移动
performEnginesMove
功能检查移动是否有效(可能),然后在板上进行移动(在move类的帮助下)
由于某些原因,这不起作用。我为
进程
和ProcessBuilder
类构建了一个委托,以显示其余代码应如何使用。我分别将这些类称为GameEngineProcess
和GameEngineProcessBuilder
GameEngineProcess
正在创建响应,这些响应是简单的String
s,可以直接附加到玩家GUI的JTextArea
。它实际上扩展了线程
,使其异步运行。因此,这个特定类的实现不是您想要的,而是用于模拟过程类。我在这个类的响应中添加了一些延迟,以模拟引擎生成它们所需的时间
然后是自定义类OnUserActionWorker
,它扩展了SwingWorker
,并异步执行您的请求:它接收来自引擎进程的响应,并将它们转发给GUI,GUI更新其JTextArea
。该类在每个引擎请求中使用一次,即我们为用户在与GUI交互时创建的每个请求创建并执行该类的新实例。请注意,这并不意味着针对每个请求关闭和重新打开引擎。GameEngineProcess
启动一次,然后在整个游戏正常运行时间内保持运行
我假设您有一种方法来判断单个引擎请求是否已完成所有响应。为了简单起见,我编写的代码中存在一条消息(类型为String
),每次在流程流中写入该消息,以指示每个请求的响应结束。这是消息的END\u
常量。因此,这让OnUserActionWorker
知道何时终止接收响应,因此稍后将为每个新请求创建下一个实例
最后是GUI,它是一个JFrame
,由JTextArea
和一个按钮网格组成,玩家可以与这些按钮交互,并根据按下的按钮向引擎发送请求命令。我再次使用String
s作为命令,但我猜在这种情况下,这可能也是您需要的
遵循代码:
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...
}
}
导入java.awt.BorderLayout;
导入java.awt.Dimension;
导入java.awt.Font;
导入java.awt.GridLayout;
导入java.io.BufferedReader;
导入java.io.BufferedWriter;
导入java.io.IOException;
导入java.io.InputStream;
导入java.io.InputStreamReader;
导入java.io.OutputStream;
导入java.io.OutputStreamWriter;
导入java.io.PipedInputStream;
导入java.io.PipedOutputStream;
导入java.util.List;
导入java.util.Objects;
导入javax.swing.JButton;
导入javax.swing.JFrame;
导入javax.swing.JOptionPane;
导入javax.swing.JPanel;
导入javax.swing.JScrollPane;
导入javax.swing.JTextArea;
导入javax.swing.SwingWorker;
公共班机{
//只是一个简单的“标志”来表示响应的结束