[Swing][Wątki] Problem z tworzeniem okna z paskiem postępu.

0

Witam.
Chyba się nie polubię z programowaniem wątków w Javie... :-/
Sytuacja wygląda tak: Mam metodę, która wykonuje nieco czasochłonne działania więc chcę zaopatrzyć program w proste okno z paskiem postępu i możliwością przerwania tych operacji. Po zakończonym procesie chcę zwrócić rezultat wykonania tej czasochłonnej metody. Mam więc taki kod:

public void createThreads()
    {
        Thread gui = new Thread(new GUI());
        gui.start();

        test = new Thread(new TestAvailability());
        test.start();
    }
    public synchronized void checkAvailabilityRun ()
    {
        /*------ Czasochłonne czynności- ;)------*/
        notifyAll();
    }


    class TestAvailability implements Runnable
    {
        public void run()
        {
            checkAvailabilityRun();
        }
    }

    class GUI implements Runnable
    {
        public void run()
        {
            window = new CheckAvailabilityWindow();
            window.createAndShowCheckAvailabilityWindow();
            progressBar = window.getProgressBar();
        }
    }

    public synchronized boolean checkAvailability ()
    {
        try
        {
            wait();
        }
        catch (InterruptedException ex)
        {
            System.out.println("Bla");
        }

        return availability;
    }

Najpierw uruchamiana jest metoda createThreads(), która tworzy dwa wątki - pierwszy, zajmujący się stworzeniem okna z paskiem postępu oraz drugi, który jest odpowiedzialny za uruchomienie czasochłonnej metody. Później uruchamiana jest metoda checkAvailability(), która czeka, aż główny wątek zakończy swoją pracę i zwraca jej rezultat (w polu availability). Czekanie zrealizowane jest za pomocą wait()/notifyAll() i działa dobrze (sprawdzane debuggerem, pole availability jest faktycznie zwracane dopiero po zakończeniu pracy żądanego wątku). Wszystko działa fajnie, tyle, że okno z paskiem postępu nie jest prawidłowo odrysowywane.

Wygląda tak: http://img444.imageshack.us/img444/9232/oknomp3.jpg
Jest przezroczyste i bez zawartości. Zawartość (JProgressBar i JButton) wyświetlane są dopiero po zakończeniu całego procesu (po zwróceniu pola availability przez metodę checkAvailability(). A to zdecydowanie nie jest tym, co chciałbym osiągnąć.

Problemem jest być może fakt, że synchronizuję metodę checkAvailability() ale inaczej nie mógłbym użyć w niej wait(). Jest jakiś sposób, żeby temu zaradzić?

Będę wdzięczny za wszelkie wskazówki

1

Najpierw musisz zaakceptować kilka zasad, które są niezbędne, żeby posługiwać się Swingiem i wątkami

  1. Swing nie jest wielowątkowy. NIGDY więc nie uruchamiaj wywołań metod klas Swing (lub tych, które po nich dziedziczą - np. Twoich klas) z innego wątku niż wątek Swinga.
  2. NIE MOŻESZ też na wątku Swinga organizować żadnych pętli oczekujących na wykonanie zadania w innym wątku ponieważ zwyczajnie zatrzymujesz obsługę zdarzeń, czyli pośrednio to co widzisz na ekranie.
  3. Metody wywoływane przez Swing (lub te, które zostały z nich wywołane) służą wyłącznie do "poruszania" kontrolek lub zmiany stanu Twojego programu. Po wykonaniu tych działań należy natychmiast z nich uciekać. Zmianą stanu może być również utworzenie innych wątków - tym razem będących "Twoją własnością" (i tam możesz grzebać do woli).

Niemal każde działanie w programie pochodzi od wywołania zdarzenia przez wątek Swinga. Rozpoczynając czasochłonne rzeczy na swoim stworzonym wątku - po zakończeniu ich musisz stamtąd poinformować resztę programu (przypomnę, że wciąż "poruszaną" przez Swing) o tym, że coś się zakończyło lub zmieniło. Inaczej mówiąc musisz z czasochłonnego wątku wypuścić komunikat.
Możesz to zrobić na dwa sposoby pierwszy i najprostszy, to po prostu wywołanie jakiejś synchronizowanej metody w Twoim kodzie, a w drugim wytworzyć komunikat wypuszczany wprost do kolejki zdarzeń.

W obu wypadkach nie "wrócisz" do miejsca z którego utworzyłeś swój czasochłonny wątek. Tak się nie pisze programów sterowanych zdarzeniami bo jest to nieefektywne (chociaż jest to do wykonania - np. za pomocą Thread.join()).

To co jest Twoim rozwiązaniem nazywa się SwingWorker.
Jak łatwo się przekonasz zadanie nie jest takie banalne jak się pierwotnie wydaje - o ile robi się do porządnie. Na poniższym kodzie, to co Ciebie najbardziej może interesować, to metoda wykonajZadanieWyzwalacza(). W niej jest cała logika wykonywaniem zadania i przekazywaniem informacji do paska postępu. Reszta to tylko kod wywoływany przez Swing pokazujący jak powinna ona współdziałać z resztą programu okienkowego

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.concurrent.ExecutionException;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingWorker;

public class JakiśPanel extends JPanel
{
	private JButton wyzwalacz = new JButton("Podaj wynik");
	private OdbierakZdarzeń odbierakZdarzeń = new OdbierakZdarzeń();

	public class TypWyniku { /*...*/ }
	private TypWyniku wynikObliczeń;
	private SwingWorker<TypWyniku, Integer> praca;

	public JakiśPanel()
	{
		wyzwalacz.addActionListener(odbierakZdarzeń);
		//...
	}
	//inne elementy i zadania panelu
	//...

	private class OdbierakZdarzeń
		implements ActionListener /*...*/
	{
		//wywoływana w wyniku naciśnięcia buttona "wyzwalacz"
		public void actionPerformed( ActionEvent e )
		{
			if(e.getSource().equals(wyzwalacz))
				JakiśPanel.this.wykonajZadanieWyzwalacza();
			//obsługa innych klawiszy
			//...
		}
		//obsługa innych zdarzeń
		//...
	}
	
	//@S1 - Swing, ale niekoniecznie musi być wywołane z wątku Swing
	private void wykonajZadanieWyzwalacza()
	{	
		wyzwalacz.setEnabled(false); //@X1 - blokujemy wyzwalacz wywołań
		if(praca != null && !praca.isDone())
			//blokujemy kolejne wywołanie, które było "już w drodze" przed @X1
			return;
		
		final int MIN = 0, MAX = 100; //musi być final dla klasy wewnęt.
		final JProgressBar pb = new JProgressBar(MIN, MAX); //j.w.
		add(pb); //dodaje pasek postępu do panelu

		//niekoniecznie musi być klasą wewnętrzną i anonimową
		praca  =
			new SwingWorker<TypWyniku, Integer>() //zadanie w klasie anonim.
			{
				//@R3 - wątek roboczy. BARDZO CZASOCHŁONNE ZADANIE...
				@Override public TypWyniku doInBackground() 
				{	
					TypWyniku wynik = null; //początkowo nie ma wyniku
					int i = MIN;
					//...
						publish(new Integer(i)); //informacja o postępie
					//...
					publish(MAX); //warto zaznaczyć wykonanie 100%
					return wynik;
				}

				//zmiana paska postępu
				@Override protected void process(List<Integer> postępy) //@S4
				{	//na liście postępy jest 1 lub więcej odebranych wartości 
					//wysłanych przez publish()
					for(Integer i: postępy)
						//ustawianie kolejnych "złapanych" wartości postępu
						pb.setValue(i); 
				}

				//zakończono pracę
				@Override public void done() //@S5 - Swing
				{
					JakiśPanel.this.remove(pb);
					try { wynikObliczeń = this.get(); }
					catch (InterruptedException olany)
					{
						System.err.println("Przerwano obliczanie wyniku.");
					}
					catch (ExecutionException wyrąbka)
					{
						Throwable powód = wyrąbka.getCause();
						String przyczyna = (powód != null)
							? powód.getMessage(): wyrąbka.getMessage();
						System.err.println("Nie udało się uzyskać wyniku"
							+ " z powodu: " + przyczyna);
					}
					//odblokowujemy możliwość ponownego wywołania
					JakiśPanel.this.wyzwalacz.setEnabled(false);
				}
			};
		praca.execute(); //@S2 - wątek Swing, zainicjowanie wątku roboczego
		//...
		//pozostałe zadania wynikające z odpalenia akcji
		//...
	}
	//...
	//reszta klasy panelu
	//...
}

Jeżeli nie potrzeba blokować wielokrotnych naciśnięć wyzwalacza przez użytkownika - czyli wtedy gdy chcemy generować czasochłonne zadania w metodzie wykonajZadanieWyzwalacza() zanim zakończą się poprzednie wykonania, to nie trzeba tej klasy w ogóle ani przypisywać zmiennej, ani robić z niej klasy jawnej. Wtedy wywołanie wyglądałoby tak:

//...
SwingWorker<...,...>
{
//...
}.execute();
//...

Zauważ, że klasa wywiedziona ze SwingWorkera jest normalną klasą, którą można rozszerzać o własne metody i pola w celu wykonania czasochłonnego zadania oraz wykorzystać na mnóstwo sposobów. Trzeba tylko pamiętać, że jedyna metoda, która jest wykonywana na osobnym wątku, który nie ma ograniczeń Swinga, to doInBackground(). Warto też pamiętać, że samo miejsce wywołania SwingWorker.execute() nie musi być wcale wątkiem Swing. Mozna ją wywołać z wątku uruchamiającego statyczną main() oraz z wątku wywołania zupełnie innej metody doInBackground(). Ba, jest możliwe nawet rekurencyjne wywoływanie tej samej klasy SwingWorkera (ale nie tego samego obiektu!).
Jedynym ograniczeniem SwingWorkera jest właśnie zakaz wywoływania metody execute na obiektach, które już raz uruchomiono tą metodą. Aby móc wykonać "rekurencję" trzeba utworzyć nowy obiekt SwingWorker.

import javax.swing.SwingWorker;

//uruchamia łańcuch wątków dopóki nie zakończy się pierwszy wątek kaskada
public class Robotnik
{
	private Praca kaskada;
	
	public Robotnik()
	{
		kaskada = new Praca();
		//...
	}
	
	//Void, Void oznacza, że nie ma wyniku z get() i nie ma sensu dziedziczyć
	//metody process(List<Void) postępy)
	public class Praca extends SwingWorker<Void, Void>
	{
		@Override public Void doInBackground() 
		{	
			//...
				if(!kaskada.isDone())
					new Praca().execute(); //wywołanie *niby* rekurencyjne
			//...
			return null; //konieczne gdy typ wyniku to Void
		}

		@Override public void done() //@S5 - Swing
		{
			//podjęcie tylko jakiejś akcji informującej o zakończeniu pracy
			//...
		}
	}
	//...
}

Mając klasę SwingWorker niemal nie trzeba bawić się własnymi wątkami odpalanymi przez Thread lub jakiec inne fabryki wątków. SwingWorker implementuje też interfejs Runnable, więc można tę klasę wrzucać do wszystkiego co ten interfejs obsługuje (np. kolejki, fabryki wykonawcze itp.).

1 użytkowników online, w tym zalogowanych: 0, gości: 1