Home > Back-end >  Updating JPanel based on results from slow server (Using threads to not block GUI)
Updating JPanel based on results from slow server (Using threads to not block GUI)

Time:07-12

So I have been looking to update one of my panels in a my client code with data that comes from a server in Indonesia. The delay is rather long (2-8) sec and Im noticing that my UI is freezing during the time it takes for the response to return from the server.

The response will be used to draw some points on a map (not yet implemented). I have been looking all over the net to find out how to do it and I have come across:

InvokeLater. SwingWroker. ScheduledThreadPoolExecutor. Making the JPanel a runnable to run in its own thread(seems like best option). http://www.java2s.com/Tutorial/Java/0160__Thread/CreateathreadtoupdateSwing.htm

But tbh most of the data i find is out dated (more than 5 years old).

Here is the JPanel class i want to update based on a server query:

public class MapPanel extends JPanel implements Pointable, Runnable {

    private static final long serialVersionUID = 1L;
    private List<Shape> shapes = new LinkedList<>();
    private State mapPanelState;
    public Shape selected;
    private BufferedImage image;


    public MapPanel() {
        Commander.getInstance().addShapeContainer(this);
        mapPanelState = NoState.getInstance();
        MouseHandler mouseHandler = new MouseHandler(this);
        KeyListener keyListener = new KeyListener();

        readImage();

        this.addMouseListener(mouseHandler);
        this.addMouseMotionListener(mouseHandler);
        this.addKeyListener(keyListener);
        this.setBackground(Color.white);
        this.setFocusable(true);
        this.requestFocusInWindow();
    }

    private void readImage(){
        try {
            image = ImageIO.read(new File("/MapCoordProject/earthmap1.jpg"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void setState(State state) {
        mapPanelState = state;
    }

    public List<Shape> getShapes() { return shapes; }

    public void setShapes(List<Shape> shapes) {
        this.shapes = shapes;
    }

    public Shape getLastShape(){ return shapes.get(shapes.size()-1); }

    
    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        g.drawImage(image, 0, 0, null);

        for (Shape shape : shapes)
            shape.draw((Graphics2D) g);
    }

    public void select(Point point) {
        for (Shape shape : shapes) {
            if (shape.intersects(point)) {
                selected = shape;
            }
        }
    }

    @Override
    public Dimension getPreferredSize() {
        if (image == null) {
            return super.getPreferredSize();
        } else {
            int w = image.getWidth();
            int h = image.getHeight();
            return new Dimension(w, h);
        }
    }



    public void pointerDown(Point point)  {
        mapPanelState.pointerDown(point, this);
    }

    public void pointerUp(Point point) {
        mapPanelState.pointerUp(point, this);
        selected = null;
    }

    public void pointerMoved(Point point, boolean pointerDown)  {
        mapPanelState.pointerMoved(point, pointerDown, this);
    }

    @Override
    public void run() {
        
    }
}

I want a method that updates the "Shapes" array in a separate thread to stop everything from freezing.

Any suggestions?

CodePudding user response:

Making your JPanel implement Runnable is not the best solution. There is no reason to expose a run() method to other classes.

Instead, create a private void method that takes no arguments. A method reference that refers to that method can act as a Runnable, since it will have the same arguments and return type. You can then pass it to a Thread constructor.

public MapPanel() {
    // ...

    readImage();
    new Thread(this::readShapes, "Reading shapes").start();

    // ...
}

private void readShapes() {
    try {
        List<Shape> newShapes = new ArrayList<>();

        URL server = new URL("https://example.com/indonesia/data");
        try (InputStream dataSource = server.openStream()) {
            while ( /* ... */ ) {
                Shape shape = /* ... */;
                newShapes.add(shape);
            }
        }

        EventQueue.invokeLater(() -> setShapes(newShapes));
    } catch (IOException e) {
        e.printStackTrace();
        EventQueue.invokeLater(() -> {
            JOptionPane.showMessageDialog(getTopLevelContainer(),
                "Unable to retrieve data:\n"   e, "Load Error",
                JOptionPane.ERROR_MESSAGE);
        });
    }
}

Notice that calls to methods involving Swing objects are always wrapped in a call to EventQueue.invokeLater, to make sure they run on the correct thread.

It is possible to improve this by creating a progress dialog that shows while the data is being loaded, but that would make this answer much longer and would require more knowledge about the Indonesian API you’re calling.

  • Related