java - 如何在不冻结 Java Swing 中的 GUI 的情况下与进程随机通信?
问题描述
我正在构建一个国际象棋 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
andprocess
方法。我在这个类中还有一个公共方法用于向引擎发送命令。
public void sendCommandToEngine(String command) {
try {
writer.write(command + '\n');
writer.flush();
} catch (IOException e) {
JOptionPane.showMessageDialog(null, e.getMessage());
}
}
我在方法中进行流读取,doInBackground
然后发布输出并更新 GUI process
。
当我从我的 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
方法可以调用(这是一个显示棋盘的 MainFrame 组件)。performEnginesMove
chessBoard
该performEnginesMove
函数检查移动是否有效(可能),然后在棋盘上移动(在 Move 类的帮助下)。
出于某种原因,这不起作用。
解决方案
Process
我为和类构建了一个委托,ProcessBuilder
以显示应该如何使用其余代码。我分别称这些类GameEngineProcess
和GameEngineProcessBuilder
。
GameEngineProcess
正在创建响应,这些响应很简单String
,可以直接附加到JTextArea
播放器的 GUI 中。它实际上扩展Thread
为让它异步运行。所以这个具体类的实现不是你要的,而是用来模拟Process
类的。我在这个类的响应中添加了一些延迟,以模拟引擎生成它们所需的时间。
然后是自定义类OnUserActionWorker
,它扩展SwingWorker
并异步执行您要求的操作:它接收来自引擎进程的响应并将它们转发到更新其JTextArea
. 这个类在每个引擎请求中使用一次,即我们为用户在与 GUI 交互时创建的每个请求创建并执行这个类的一个新实例。请注意,这并不意味着引擎会为每个请求关闭并重新打开。启动一次,然后GameEngineProcess
在整个游戏正常运行时间内保持运行。
我假设您有办法判断单个引擎请求是否已完成所有响应。为了简单起见,我编写的这段代码存在一条消息(类型为String
),每次都在流程流中写入该消息,以指示每个请求的响应结束。这是END_OF_MESSAGES
常数。所以这让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...
}
}
最后,我做出的另一个重要假设是,当用户与 GUI 交互时,GUI 会阻止输入(但会继续响应其他事件)。这可以防止用户同时对引擎有多个活动请求。通过阻止输入,我的意思是当您单击一个按钮时,首先所有按钮都被禁用,然后命令被发送到引擎。当对最新发出的请求的所有响应完成时,这些按钮都会重新启用。
如果您需要同时向单个引擎发出多个请求,那么您可能需要同步对某些 GUI 方法的访问,并确保每个方法OnUserActionWorker
都能将其响应与其他方法区分开来。所以那将是一个不同的故事,但如果这是你想要的,请告诉我。
要在收到响应时测试 EDT 的响应能力,例如,您可以在仍在接收(十个)响应时简单地用鼠标调整窗口大小,或者只是注意到响应JTextArea
实时打印到。
希望能帮助到你。
推荐阅读
- docker - 运行带有 ssl 支持的 Docker Image httpd
- c++ - 自定义条件范围互斥锁
- r - 如何将列表转换为数据框
- python - 如何使用python删除位图图像蒙版中的小岛补丁?
- clojure - clojure 宏中的文本替换功能,如 C 的 #define
- sockets - 如何从 Dart 中的 Future 返回套接字数据?
- unit-testing - Symfony 4. 允许获取私有服务的特殊容器
- php - php中两个数组的区别
- javascript - 如果未在承诺中验证重定向和 setState
- python - 在 PyTorch 中沿矩阵的对角线绑定所有值