1

I am having some difficulties using swing workers, timers, and I am actually a little confused. As far as my understanding goes, I have to put on a timer to set-up recurring tasks that have to be called by the EDT.

I'm trying to make a program that shows graphically a sorting alghoritm (like this : https://www.youtube.com/watch?v=kPRA0W1kECg )

I just don't understand why the GUI won't refresh. I am quite sure the repaint method is being called since I put a sysout showing me the ordered values and it seems to work , but the GUI just... doesn't change.

Here's my code:

public class MainWindow {
    private JFrame frame;
    JPanel panel;
    public final static int JFRAME_WIDTH = 800;
    public final static int JFRAME_HEIGHT = 600;
    public final static int NELEM = 40;

    ArrayList<Double> numbers;
    ArrayList<myRectangle> drawables = new ArrayList<myRectangle>();
    Lock lock = new ReentrantLock();
    Condition waitme = lock.newCondition();


    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    MainWindow window = new MainWindow();
                    window.frame.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public MainWindow() {
        initialize();
    }

    private void initialize() {
        frame = new JFrame();
        frame.setBounds(100, 100, JFRAME_WIDTH + 20, JFRAME_HEIGHT + 40);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        panel = new myPanel();
        frame.getContentPane().add(panel, BorderLayout.CENTER);

        Timer timer = new Timer(500, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent arg0) {
                lock.lock();
                try{
                    //Updating the gui
                    panel.repaint();
                    panel.revalidate();
                    //Giving the OK to the sorting alghoritm to proceed.
                    waitme.signal();
                }catch(Exception e){
                    e.printStackTrace();
                }finally{
                    lock.unlock();
                }
            }
        });
        timer.start();

        SwingWorker<Integer, String> sw = new SwingWorker<Integer, String>(){

            @Override
            protected Integer doInBackground() throws Exception {
                mapAndCreate();
                bubbleSort();
                return null;
            }

        };
        sw.execute();
    }

    private void bubbleSort() throws InterruptedException{      
        for(int i=0; i < NELEM; i++){
            for(int j=1; j < (NELEM-i); j++){
                if(drawables.get(j-1).wid > drawables.get(j).wid){
                    //swap the elements!
                    myRectangle temp = drawables.get(j-1);
                    drawables.set(j-1, drawables.get(j));
                    drawables.set(j, temp);
                    lock.lock();
                    try{
                        //Wait for the GUI to update.
                        waitme.await();
                    }catch(Exception e){
                        e.printStackTrace();
                    }finally{
                        lock.unlock();
                    }
                }
            }
        }

    }

    /***
     * Function that maps values from 0 to 1 into the rectangle width.
     */
    private void mapAndCreate() {
        double max = 0;
         numbers = new ArrayList<Double>(NELEM);
        //Finding maximum.
        for(int i = 0; i < NELEM; i++){
            Double currElem = Math.random();
            if(currElem > max) max = currElem;
            numbers.add(currElem);
        }
        //Mapping process
        int offset = 0;
        for(int j = 0; j < NELEM; j++){
            Integer mapped = (int) (( JFRAME_WIDTH * numbers.get(j) ) / max);
            myRectangle rect = new myRectangle(offset , mapped);
            drawables.add(rect);
            offset += JFRAME_HEIGHT / NELEM;
        }
    }

    private class myRectangle{

        int myy , wid , colorR,colorG,colorB;

        public myRectangle(int y , int wid){
            this.myy = y;
            this.wid = wid;
            Random r = new Random();
            colorR = r.nextInt(255);
            colorG = r.nextInt(255);
            colorB = r.nextInt(255);

        }
    }

    private class myPanel extends JPanel{

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            for(myRectangle rectan : drawables){
                Graphics2D graphics2D = (Graphics2D)g;
                System.out.println(rectan.wid);
                Rectangle2D.Double rect = new Rectangle2D.Double(0,rectan.myy,rectan.wid,JFRAME_HEIGHT / NELEM);
                graphics2D.setColor(new Color(rectan.colorR,rectan.colorG,rectan.colorB));
                graphics2D.fill(rect);
            }
            System.out.println("====================================================================================================");
        }
    }


}
arocketman
  • 1,134
  • 12
  • 21
  • There's a non-zero chance that `lock.lock();` call is blocking the Event Dispatch Thread causing your GUI be unable to repaint itself. Hard to be sure withouth a [MCVE](http://stackoverflow.com/help/mcve) though – dic19 Sep 19 '14 at 12:02
  • I honestly don't know how to make that code shorter without removing essential details. That's all there is to it tho, there are no other files. – arocketman Sep 19 '14 at 12:08

1 Answers1

4

Most OSs (or rather the UI frameworks which they use) don't support concurrent access. Simply put, you can't render two strings of text at the same time.

That's why Swing runs all rendering operations in the UI thread. Calling rendering functions (like paint()) outside of the UI thread can cause all kinds of problems. So when you do it, Swing will just remember "I should repaint" and return (instead of doing any actual work). That way, Swing protects you but most people would prefer to get an error with a useful message.

A timer always also means that there is a thread somewhere which executes when the timer runs out. This is not the UI thread of Swing. So any paing operations there must be wrapped with EventQueue.invokeLater() or similar.

Another common bug is to hog the UI thread (so no rendering happens because you do complex calculations there). That's what the SwingWorker is for. Again, in most methods of the SwingWorker, calling methods which would render something is forbidden (-> use invokeLater()).

So my guess is that the UI thread waits for the lock and the lock simply isn't unlocked early or often enough. See this demo how to do a simple animation in Swing.

public class TimerBasedAnimation extends JPanel implements ActionListener {
  public void paint(Graphics g) {
      // setup
      // do some first-run init stuff
      // calculate the next frame
      // render frame
  }

  public void actionPerformed(ActionEvent e) {
    repaint();
  }

  public static void main(String[] args) {
    JFrame frame = new JFrame("TimerBasedAnimation");
    frame.add(new TimerBasedAnimation());
    ...
  }
}

As you can see in the code doesn't lock. Instead, you just send "render now" events from actionPerformed to Swing. Some time later, Swing will call paint(). There is no telling (and no way to make sure or force Swing) when this will happen.

So good animation code will take the current time, calculate the animation state at that time and then render it. So it doesn't blindly step through N phases in M seconds. Instead, it adjusts for every frame to create the illusion that the animation is smooth when it really isn't.

Related:

Community
  • 1
  • 1
Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • Thank you, you were very exhaustive with your answer. I'll get back to the code, remove the locks and adapt it to the examples you provided. Have a nice day ! – arocketman Sep 19 '14 at 12:37
  • @Aaron Digulla I'm disagee, caused by code line (loop) - `Graphics2D graphics2D = (Graphics2D)g;`, logics is correct to 1. `fill array` outside of painting, before, 2. then call for `repaint()`, 3. reset painting - `super.paintComponent(g);`, 4. `setBackground in paintComponen`t, 5. then finally (loop inside array of prepared Ojbects, coordinates, etc) paint in `paintComponent`, – mKorbel Sep 19 '14 at 13:20
  • @mKorbel: I'm not sure which "code line (loop)" you're talking about. My main point is that you can't be sure that the UI will have been updated when `repaint()` returns. – Aaron Digulla Sep 19 '14 at 13:43
  • @Aaron Digulla 1. Graphics2D graphics2D = (Graphics2D)g; creates shapshot, in loop can be (Graphics2D from Graphics2D to) derived to nothing, 2. agree with repaint – mKorbel Sep 19 '14 at 13:53