2

I've got a Swing UI where one part consist of a BufferedImage on which I need to regularly modify pixels.

It seems common to create a JLabel and to call the setIcon method on that JLabel by passing it:

new ImageIcon(bufferedImage)

For example that is how the BufferedImage is shown on screen in the accepted answer here by a user with 20K+ rep:

A simple way to setting a bufferedImage into a single colored pixel without placing a image into it?

So I'm doing the same: a JLabel which has an Icon set to an ImageIcon which contains the BufferedImage and my question is related to multi-threading and Swing repaints: how am I supposed to modify pixels inside the BufferedImage so that the changes are guaranteed to appear to the user?

I take it that if I modify the BufferedImage from a thread which is not the EDT thread and if no synchronization / lock / memory barrier is used, then nothing guarantees that the changes are going to be visible.

Can I simply modify the pixels directly on the EDT?

And once I've modified the pixels, should I call the JPanel's repaint method?

Would that guaranteed that my changes would always be visible? (by "visible" here I both literally mean "visible" as in visible on screen and as in "visible to the EDT").

I'd rather keep this simple and I do not want to use non-Swing APIs nor 3D APIs etc.

Community
  • 1
  • 1
Cedric Martin
  • 5,945
  • 4
  • 34
  • 66
  • Why the downvote and the close vote without comment? This is not right. Really not right. The problem is a very real problem: if you don't do it right your UI isn't displaying things correctly. Somebody please upvote this to counter the downvote. – Cedric Martin Mar 26 '13 at 01:48
  • You could use a backing buffer/copy of the image, modify it in a background thread and then resync it back to the label within the EDT – MadProgrammer Mar 26 '13 at 01:58
  • @MadProgrammer: when you say "resync", what do you mean by that? Should I remove the Icon and then re-attach a new Icon to the JLabel? – Cedric Martin Mar 26 '13 at 02:09
  • No. In Swing all interactions with the UI must be made from within the context of the Event Dispatching Thread, what I mean by re-sync, is basically to move off your current thread and place the call within the EDT. `SwingWorker` and `SwingUtilities.invokeLater` are both examples of this – MadProgrammer Mar 26 '13 at 02:36

2 Answers2

2

If I understand the question properly, this example takes a BufferedImage and replaces all the pixels in that image with a red pixel.

This is achieved through the use of a SwingWorker. Basically, this makes a copy of the original image and walks through the pixel data, updating each pixel. It then re-syncs that image with the UI by making a copy of it.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.util.List;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class PixelMe {

    public static void main(String[] args) {
        new PixelMe();
    }

    public PixelMe() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException ex) {
                } catch (InstantiationException ex) {
                } catch (IllegalAccessException ex) {
                } catch (UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame("Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }

        });
    }

    public BufferedImage createImage() {

        BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, 100, 100);
        g.dispose();

        return image;

    }

    public class TestPane extends JPanel {

        private JLabel label;
        private BufferedImage master;

        public TestPane() {
            setLayout(new BorderLayout());
            label = new JLabel(new ImageIcon(createImage()));
            add(label);

            JButton update = new JButton("Update");
            add(update, BorderLayout.SOUTH);
            update.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    BufferedImage image = (BufferedImage) ((ImageIcon)label.getIcon()).getImage();
                    new UpdateWorker(image, label).execute();
                }

            });
        }

    }

    public class UpdateWorker extends SwingWorker<BufferedImage, BufferedImage> {

        private BufferedImage copy;
        private JLabel target;

        public UpdateWorker(BufferedImage master, JLabel target) {
            this.target = target;
            copy = makeCopy(master);
        }

        public BufferedImage makeCopy(BufferedImage master) {
            BufferedImage image = new BufferedImage(master.getWidth(), master.getHeight(), master.getType());
            Graphics2D g = image.createGraphics();
            g.drawImage(master, 0, 0, null);
            g.dispose();
            return image;
        }

        @Override
        protected void process(List<BufferedImage> chunks) {
            target.setIcon(new ImageIcon(chunks.get(chunks.size() - 1)));
        }

        @Override
        protected BufferedImage doInBackground() throws Exception {
            int pixel = Color.RED.getRGB();
            for (int row = 0; row < copy.getHeight(); row++) {
                for (int col = 0; col < copy.getWidth(); col++) {
                    copy.setRGB(col, row, pixel);
                    publish(makeCopy(copy));
                }
            }
            return null;
        }
    }
}

It should be noted that this is a VERY expensive example, as a new BufferedImage is created for each change of pixel. You could establish a pool of images and use those instead, seen as were really only interested in the last image (within the process method) or reduce the number of updates, but it's just a proof of concept.

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • thanks a lot for helping me with this. Yes indeed that seems very expensive. Basically what I don't understand (and hence this question on SO) is if can or not directly mess with the pixel of the unique BufferedImage, without always creating new ones or without using some double-buffering or whatnots. Basically I don't "get" what Swing is doing and what I should do : ( But +1 because you're really helping me here! – Cedric Martin Mar 26 '13 at 03:41
  • I would say no. Mostly because you don't control the repaint process, this could mean that part or the whole image could be repainted while you are updating it – MadProgrammer Mar 26 '13 at 04:45
2

Can I simply modify the pixels directly on the EDT?

No. Several alternatives are mentioned here, but the main example treats the offscreen buffer as a model that evolves over time in a separate thread. A javax.swing.Timer periodically notifies the listening view that an update is available, synchronizing access to the shared data. Because any pixel in the model may change, the entire view is redrawn at each tick.

If the updated pixels are geometrically localized, you can try a variation of drawImage() that updates just a portion of the screen; but try to avoid inadvertent scaling.

trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • thanks a lot for that link. It's a subject which seems simple but which is actually quite complex. Great link, I'll be analyzing this in great details! – Cedric Martin Mar 26 '13 at 15:00