sobota, 28 maja 2011

Java - Po prostu wątki

Często pisząc aplikację natrafiamy na sytuację w której chcemy wykonać dwie (albo i więcej) rzeczy jednocześnie. Na przykład, chcemy jednocześnie rysować JPanel, obliczać w czasie rzeczywistym nowe dane do wyświetlenia i dodatkowo obsługiwać zdarzenia użytkownika.

Dzisiaj napiszemy prostą aplikacje z zastosowaniem wielowątkowości.

Na początek przygotujmy okno z panelem i przyciskiem. Panel będzie wyświetlał koło wielkości 10x10px o środku w miejscu wskazywanym przez zmienną location. Po naciśnięciu przycisku koło będzie zmieniało kolor.

/* Main.java */

public class Main
{
    public static void main(String[] args)
    {
        Frame f = new Frame("Java - Multithreading");
        
        f.budujOkno();
    }
}

/* Frame.java */

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.border.LineBorder;

public class Frame extends JFrame
{
    JButton button;
    Panel p;
    
    public final static int X = 400;
    public final static int Y = 400;
    
    public Frame(String title)
    {
        super(title);
    }
    
    public void budujOkno()
    {
        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent e)
            {
                System.exit(0);
            }
        });
        
        setSize(435,487);
        setLayout(null);
        setVisible(true);
        
        button = new JButton("Przycisk");
        button.setLocation(100,420);
        button.setSize(200,20);
        button.setVisible(true);
        button.addActionListener(new ActionListener()
                {
                    public void actionPerformed(ActionEvent e)
                    {
                        p.changeColor();
                    }
                });
        add(button);
        
        p = new Panel();
        p.setLocation(10,10);
        p.setSize(X,Y);
        p.setVisible(true);
        p.setBorder(new LineBorder(Color.BLACK));
        p.setBackground(Color.white);
        add(p);
    }
}

/* Panel.java */

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JPanel;

public class Panel extends JPanel
{
    public int[] location = null;
    
    private int color = 0;
    private Color[] colors = {Color.red, Color.yellow, Color.blue, Color.green};
    
    public Panel()
    {
        super(null);
    }
    
    @Override
    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        
        Graphics2D g2d = (Graphics2D)g;
        
        if(location != null)
        {
            g2d.setColor(colors[color]);
            g2d.fillOval(location[0]-5, location[1]-5, 10, 10);
            
            g2d.setColor(Color.black);
            g2d.drawOval(location[0]-5, location[1]-5, 10, 10);
        }
    }
    
    public void changeColor()
    {
        if(color+1 < colors.length)
            color++;
        else
            color = 0;
    }
    
    public void setLoc(int x, int y)
    {
        if(location == null)
            location = new int[2];
        
        location[0] = x;
        location[1] = y;
    }
}
Wszystko ok, program się uruchamia, ale jest coś nie tak. Koło się nie wyświetla bo zmienna location jest null'em. Trzeba obliczyć pozycję koła. Do tego celu użyjemy pierwszego dziś wątku. Tworzymy klasę anonimową implementującą interface Runnable. Chcemy aby obliczenia wykonywały się do zamknięcia programu więc w metodzie run znajdzie się nieskończona pętla. Do klasy Frame dodajemy import klasy Random potrzebnej do wylosowania pozycji początkowej, zmienną dla wątku obliczającego, przerwanie wątku w przypadku zamknięcia aplikacji oraz utworzenie nowego wątku.
/* Frame.java */
import java.util.Random;
...
    Thread obliczenia;
...
            if(obliczenia != null)
                obliczenia.interrupt();
...
        obliczenia = new Thread(new Runnable()
        {
            public void run()
            {
                int kX = 1;
                int kY = 1;
                
                int x = (new Random().nextInt((Frame.X - 10) / 10) + 10) * 10;
                int y = (new Random().nextInt((Frame.Y - 10) / 10) + 10) * 10;

                try
                {
                    while (true)
                    {
                        if (kX * 10 + x >= Frame.X)
                        {
                            x = Frame.X;
                            kX *= -1;
                        }
                        else if(kX*10 + x <= 0)
                        {
                            x = 0;
                            kX *= -1;
                        }
                        else
                        {
                            x = kX * 10 + x;
                        }

                        if (kY * 10 + y >= Frame.Y)
                        {
                            y = Frame.Y;
                            kY *= -1;
                        }
                        else if(kY*10 + y <= 0)
                        {
                            y = 0;
                            kY *= -1;
                        }
                        else
                        {
                            y = kY * 10 + y;
                        }

                        p.setLoc(x, y);

                        Thread.sleep(10);
                    }
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        });
        obliczenia.start();
...
W tej chwili pozycja jest już obliczana 100 razy na sekundę, natomiast nadal się nie wyświetla. Z tego powodu potrzebny jest wątek odświeżający panel, powiedzmy, 50 razy na sekundę. Ponownie wykorzystamy do tego celu klasę anonimową. Oto ostateczna wygląd klasy Frame:
/* Frame.java */

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Random;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.border.LineBorder;

public class Frame extends JFrame
{
    JButton button;
    Panel p;
    
    public final static int X = 400;
    public final static int Y = 400;
    
    Thread obliczenia;
    Thread obraz;
    
    public Frame(String title)
    {
        super(title);
    }
    
    public void budujOkno()
    {
        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent e)
            {
                if(obliczenia != null)
                    obliczenia.interrupt();
                
                if(obraz != null)
                    obraz.interrupt();
                
                System.exit(0);
            }
        });
        
        setSize(435,487);
        setLayout(null);
        setVisible(true);
        
        button = new JButton("Przycisk");
        button.setLocation(100,420);
        button.setSize(200,20);
        button.setVisible(true);
        button.addActionListener(new ActionListener()
                {
                    public void actionPerformed(ActionEvent e)
                    {
                        p.changeColor();
                    }
                });
        add(button);
        
        p = new Panel();
        p.setLocation(10,10);
        p.setSize(X,Y);
        p.setVisible(true);
        p.setBorder(new LineBorder(Color.BLACK));
        p.setBackground(Color.white);
        add(p);
        
        obliczenia = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                int kX = 1;
                int kY = 1;
                
                int x = (new Random().nextInt((Frame.X - 10) / 10) + 10) * 10;
                int y = (new Random().nextInt((Frame.Y - 10) / 10) + 10) * 10;

                try
                {
                    while (true)
                    {
                        if (kX * 10 + x >= Frame.X)
                        {
                            x = Frame.X;
                            kX *= -1;
                        }
                        else if(kX*10 + x <= 0)
                        {
                            x = 0;
                            kX *= -1;
                        }
                        else
                        {
                            x = kX * 10 + x;
                        }

                        if (kY * 10 + y >= Frame.Y)
                        {
                            y = Frame.Y;
                            kY *= -1;
                        }
                        else if(kY*10 + y <= 0)
                        {
                            y = 0;
                            kY *= -1;
                        }
                        else
                        {
                            y = kY * 10 + y;
                        }

                        p.setLoc(x, y);

                        Thread.sleep(10);
                    }
                }
                catch (InterruptedException e)
                {
                    //
                }
            }
        });
        obliczenia.start();
        
        obraz = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                try
                {
                    while(true)
                    {
                        p.repaint();
                        Thread.sleep(20);
                    }
                }
                catch(InterruptedException e)
                {
                    //
                }
            }
        });
        obraz.start();
    }
}
A oto efekt końcowy: