SwingWorker w akcji;)
Postanowiłem oderwać się od swojej piekielnej potyczki z wyrażeniami regularnymi i napisać coś w końcu zgodnego z tytułem blogu. Temat na początek łatwy, ale od czegoś trzeba zacząć, a dokładnie wykonywanie długotrwałych procesów programu za pomocą klasy SwingWorker
Postanowiłem o tym napisać bo sam dosyć często lekceważyłem tę kwestię i jakoś nie przeszkadzało mi kilku-sekundowe przywieszenie się UI podczas np. łączenia z bazą danych w prostych programikach na zaliczenia. W końcu stało się to bardzo wkurzające i jest to po prostu duży przypał jeśli widzi się takie działanie programu. Drugim powodem było to, że nie tak wiele osób umie prawidłowo korzystać ze “świątyni wszechwiedzy” Google, widziałem nawet jak nie którzy sami próbowali implementować “ciężkie” zadania zrzucając je na własne wątki, czyli prostować poziomicę.
Co do tych ciężkich procesów, to ogólna zasada mówi że każde zadanie ktorego czas trwania moze spowodowac zauwazalne ‘zamrozenie’ interfejsu uzytkownika, nie powinno być wykonywane w wątku operującym GUI. Do takich długotrwałych procesów zalicza się m.in:
- Łączenie z bazą danych oraz operacje na bazie danych
- Wczytywanie dużych plików np. (jpg-ów) (ogólnie operacji IO)
- Długotrwałych obliczeń (np.operacji morphingu)
- Odświeżanie dużych tabel
W Java wątek odpowiadający za obsługę GUI zwie się Event Dispatch Thread (jakby ktoś nie wiedział;)), czyli po prostu EDT. Jak sama nazwa mówi zajmuję się on obsługą kolejki zdarzeń i informowaniem o nich obiektów nasłuchujących (czyt. z reguły ActionListener-ów), dodatkowo zarządza rozłożeniem komponentów, ich wyświetleniem, zmianą właściwości komponentów (np. dezaktywacja przycisku) i obsługą zadań. Zadaniami tymi powinny być tylko i wyłącznie krótkotrwałe procesy. Niektórzy myślą że to…:
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run() {
JFrame f = new JFrame();
f.setSize(200,200);
f.setVisible(true);
}
});
}
}
lub
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable(){
@Override
public void run() {
JFrame f = new JFrame();
f.setSize(200,200);
f.setVisible(true);
}
});
}
}
…załatwia sprawę, ale to dopiero początek, każda nie banalna aplikacja powinna odpalać GUI w EDT za pomocą powyższych technik (pierwsza jest ciut mniej pracochłonna, ponieważ wystarczy zaimportować tylko pakiet javax.swing.*;), ale jak zwał tak zwał wszystko to jeden ciul ważne że mamy prawidłowo zainicjalizowane GUI i jak na razie wszystko jest wporządku .
Teraz potrzebujemy obsługi długotrwałych zadań, która nasza spłodzona w pocie czoła aplikacja ma wykonywać.
Pierwszy krok…
…a jakże ulubiona dokumentacja
Rozkminimy sobie co ten wspaniały produkt programistów Sun-a ma nam do zaoferowania.
Pierwsze co rzuca się w oczy to deklaracja klasy:
public abstract class SwingWorker<T,V> extends Object implements RunnableFuture
Jak widać SwingWorker to klasa abstrakcyjna, czyli możemy utworzyć obiekt tylko i wyłącznie klasy po niej dziedziczącej. Drugą rzeczą rzucającą się w oczy są parametry T i V, tak SwingWorker czerpie pełnymi z garściami z dobrodziejstw typów uogólnionych dodanych w Java 1.5. Implementowany interfejs jest połączeniem interfejsów Runnable i Future (z niego właśnie pochodzi metoda get() zwracająca rezulat obliczeń).
Parametr T – określa typ zwracany przez metody doInBackground() i get() o których później. Jest to po prostu rezultat naszego zadania np. obrazek.
Parametr V – określa typ danych pośrednich, które może produkować zadanie, przykładowo linie tekstu z wczytywanego pliku. Dane te można wyłuskać za pomocą metody process(List dane), a które przekazuje do niej metoda publish(V... dane), która powinna być używana w implementacji metody doInBackground() .
Uwaga!
Aby metody zwracały typ void (a dokładniej żadnego konkretnego typu), bo taką możliwość naturalnie też mamy, w deklaracji klasy należy podstawić parametr Void.
Użyteczne metody klasy SwingWorker:
abstract protected doInBackground()Zaimplementowanie tej metody najbardziej nas interesuje, ba, nawet musimy to zrobić ponieważ jest to metoda abstrakcyjna. To w niej powinniśmy dostarczyć zadanie do wykonania. Jeśli przesłonimy także metodę
process(List dane), możemy przy pomocypublish(V... dane)przekazywać doprocess(List dane)cząstkowe wyniki działania zadania.-
protected process(List dane)Dzięki tej metodzie możemy operować na pośrednich danych zwróconych przez
publish(V... dane). W tej metodzie możemy bezpiecznie operować na komponentach graficznych, np. dodawać linijki tekstu to JTextArea, ponieważ działa ona asynchronicznie w EDT. protected void publish(V... chunks)Metoda ta przesyła szczątkowe dane metodzie
protected process(List dane), nie ma potrzeby jej przesłaniania, ale można to zrobić w przypadku gdy np. chcemy wykonać jakieś extra operację na dostarczanych danych.protected void done()Metoda wywoływana po zakończeniu zadania, wykonywana w EDT, można w niej przeprowadzić sprzątania i zaprezentować w GUI główny rezultat wykonywanego zadania, czyli pobrać rezultat działania
doInBackground()przy pomocy metodyget().
To nie wszystkie ciekawe rzeczy związane ze SwingWorker-em, polecam zaglądnięcie do dokumentacji, jest to bardzo dobrze udokumentowana klasa.
A teraz kodzik prostego przykładu, pełny program można pobrać stąd.
button.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
SwingWorker<Icon, Void> worker = new SwingWorker<Icon, Void>(){
@Override
protected Icon doInBackground() throws Exception {
progressBar.setIndeterminate(true);
return loadImage(textField.getText());
}
@Override
protected void done() {
try {
imageLabel.setIcon(get());
progressBar.setIndeterminate(false);
} catch (Exception ex) {
ex.printStackTrace();
}
}
};
worker.execute();
}
});
Dla ułatwienia jak widać posłużyłem się klasą anonimową, tutaj ktoś może się zastanowić dlaczego za każdym razem tworzę nowy obiekt, a to dlatego, że nie ma możliwości powtórnego wykorzystania obiektu tego typu.
I na tym kończę ten arcik, mam nadzieję że nie popełniłem żadnych byków i polecam wszystkim nie stosującym do tej pory, specjalnie przeznaczonego do wykonywania długich zadań SwingWorker-a.
Podobne zachowanie mamy chocby w przypadku uslug sieciowych – tworzenie instancji ServerChannel po to, aby nie blokowac dzialania aplikacji.