如何在不冻结Java Swing中GUI的情况下与进程进行随机通信?

问题描述 投票:2回答:1

我正在构建一个国际象棋GUI应用程序,其作用是显示棋盘和棋子,并防止输入非法举动。

它还应具有涉及与国际象棋引擎(例如鳕鱼)通信的功能。这就是我现在正在努力的目标。国际象棋引擎是一个exe文件,可以使用ProcessBuilder访问:

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(及其流读取器和写入器)作为私有成员变量的类。我实现了doInBackgroundprocess方法。我在此类中也有一个用于向引擎发送命令的公共方法。

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方法可以在检测到引擎输出了“最佳动作”时,在performEnginesMove(这是显示棋盘的MainFrame组件)上调用chessBoard

performEnginesMove函数检查移动是否有效(可能),然后在板上进行移动(借助于Move类)。

由于某种原因,这行不通。

java multithreading swing processbuilder swingworker
1个回答
0
投票

我为ProcessProcessBuilder类构建了一个委托,以显示应如何使用其余代码。我分别将这些类称为GameEngineProcessGameEngineProcessBuilder

GameEngineProcess正在创建响应,这些响应是简单的String,可以直接附加到播放器GUI的JTextArea中。实际上,它扩展了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会阻止input(但会继续响应其他事件)。这样可以防止用户同时对引擎有多个活动请求。通过blocking input我的意思是简单地说,当您单击按钮时,首先禁用所有按钮,然后将命令发送到引擎。当对最新发出的请求的所有响应都完成时,按钮将全部重新启用。

如果您需要同时对一个引擎有多个请求,那么您可能需要同步某些GUI方法的访问,并确保每个OnUserActionWorker都可以将其响应与其他。所以那将是一个不同的故事,但是让我知道这是否是您想要的。

要在接收响应时测试EDT的响应性,您可以例如在仍接收(十个)响应时用鼠标简单调整窗口的大小,或者只是注意将响应打印到JTextArea中实时。

希望有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.