JavaFX 2 – aplikacja wielowątkowa

post_img

Kontynuując nasz wątek blogowy o JavaFX 2.0, chciałbym dziś zaprezentować kilka możliwości JavaFX. Głównym tematem jest oczywiście wielowątkowość, ale znajdzie się też miejsce na wiązanie danych pomiędzy warstwą danych a warstwą prezentacji, definiowanie akcji oraz pracę z biblioteką JavaFX z wykorzystaniem środowiska Maven.

Temat wielowątkowości, szczególnie w kontekście aplikacji typu „gruby klient”, nigdy nie był łatwy. Zapewnienie odpowiedniej synchronizacji, komunikacji oraz braku zakleszczeń może niejednego programistę przyprawić o zawrót głowy. Śmiało można śmiało powiedzieć, że JavaFX wprowadza nową jakość w tworzeniu tego typu aplikacji. Udogodnienia dostępne w wersji 2.0 dają wiele możliwości. W efekcie kod aplikacji oraz czas wykonania są znacznie krótsze w porównaniu do tworzenia bliźniaczej aplikacji w bibliotece SWING.

W JavaFX klasy i interfejsy związane z obsługą wielowątkowości znajdują się w pakiecie javafx.concurrent. Interfejs Worker jest implementowany przez pozostałe dwie klasy: Task i Service. Obiekt typu Worker wykonuje prace w jednym lub kilku równoległych wątkach. Stan jest w pełni obserwowalny i dostępny dla głównego wątku interfejsu GUI. Nie da się natomiast bezpośrednio modyfikować stanu interfejsu GUI. Próba taka kończy się wyjątkiem. Abstrakcyjna klasa Task służy do enkapsulacji pracy, jaka ma być wykonywana w danym wątku. Implementująca klasa musi wypełnić metodę call, która będzie wykonywana podczas uruchomienia wątku. Obiektu tej klasy nie można ponownie uruchomić. Ostatnią klasą jest Service, która – dla odmiany – da się użyć ponownie poprzez restart lub reset.

Z kodu tych wątków nie można bezpośrednio modyfikować stanu GUI. Wszystkie operacje komunikacji wykonuje za nas silnik JavaFX. Wystarczy powiązać (ang. bind) odpowiedni parametr z elementem graficznym. Domyślnie interfejs Worker oferuje nam całą listę parametrów związanych ze stanem wątku. Niektóre z nich to:

  • ReadOnlyStringProperty messageProperty()
  • ReadOnlyDoubleProperty progressProperty()
  • ReadOnlyBooleanProperty runningProperty()
  • ReadOnlyObjectProperty stateProperty()
  • ReadOnlyStringProperty titleProperty()
  • ReadOnlyDoubleProperty totalWorkProperty()

Można też oczywiście dodawać własne parametry. Przykładowo, aby powiązać parametr „message” z elementem wizualnym typu TextField, należy wykonać polecenie:

1
task.messageProperty().bind(textField.textProperty());

A następnie w kodzie wątku wywołać polecenie:

1
updateMessage("tekst do prezentacji");

Demonstracyjna aplikacja będzie uruchamiała wątki typu Task i Service. Każdy z nich będzie informował interfejs o postępie wykonania swojego zadania. Każdy wiersz w tabeli to jeden wątek. Pierwsza kolumna w tabeli będzie zawierała nazwę wątku nadaną przez maszynę wirtualną Javy. Druga kolumna będzie zawierała element typu ProgressBar, który będzie na bieżąco pokazywał stan zaawansowania wykonania danego wątku. W naszym przypadku wątek nie będzie nic robił oprócz ustawienia stanu postępu i przejścia w stan uśpienia. W docelowej aplikacji wątek taki może wykonywać dowolne zadanie, nie blokując jednocześnie GUI. Aplikację utworzyłem w edytorze IDE Eclipse, wykorzystująć środowisko Maven.

Przygotowanie środowiska i utworzenie nowego projektu

  1. Utworzenie projektu

    1
    mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=pl.atena.javafxblog -DartifactId=JavaFXBlog
  2. Dodanie plików konfiguracyjnych wymaganych przez Eclipse. Wchodzimy do katalogu JavaFXBlog i wykonujemy polecenie:

    1
    mvn eclipse:eclipse
  3. Modyfikacja pliku konfiguracyjnego pom.xml. W sekcji <dependencies> dodajemy zależność do biblioteki JavaFX2:

    1
    2
    3
    4
    5
    6
    7
            <dependency>
                <groupId>javafx</groupId>
                <artifactId>javafxrt</artifactId>
                <version>2.0.2</version>
                <scope>system</scope>
                <systemPath>C:/Program Files/Oracle/JavaFX 2.0 SDK/rt/lib/jfxrt.jar</systemPath>
            </dependency>

    Ścieżka do biblioteki jfxrt.jar w elemencie <systemPath> powinna odpowiadać lokalizacji tego pliku na naszym komputerze.

  4. Import projektu do środowiska Eclipse

    W aplikacji Eclipse wybieramy opcje File->Import…->Existing Projects into Workspace i wskazujemy katalog z projektem.

Kod aplikacji

Aplikacja składa się z dwóch elementów: tabeli i przycisku. Przykładowe ułożenie elementów może być wykonane w następujący sposób:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    public void start(Stage primaryStage) throws Exception {
        tableData = new TableView<Worker<Void>>();
        runTaskButton = new Button("Nowy wątek typu Task");
        runServiceButton = new Button("Nowy wątek typu Service");
       
        BorderPane rootGroup = new BorderPane();
        FlowPane paneFlow = new FlowPane();
        paneFlow.getChildren().addAll(runTaskButton, runServiceButton);
        paneFlow.setPadding(new Insets(10, 10, 10, 10));
        paneFlow.setHgap(10);
        paneFlow.setVgap(10);
        paneFlow.setAlignment(Pos.CENTER);
       
        rootGroup.setCenter(tableData);
        rootGroup.setBottom(paneFlow);

        Scene scene = new Scene(rootGroup, 800, 600);
        scene.setFill(Color.OLDLACE);
       
        primaryStage.setTitle("JavaFX Blog");
        primaryStage.setScene(scene);
       
       
        configTable();
        attachActions();
        bindData();
       
        primaryStage.show();
    }

Powiązanie tabeli z listą utworzonych wątków. Ważne jest, aby kolekcja przechowująca listę wątków była typu „ObservableList”

1
2
3
    private void bindData() {
        tableData.setItems(listThreads.getThreadList());
    }

gdzie obiekt threadList został utworzony w następujący sposób:

1
private final ObservableList<ThreadTask> threadList = FXCollections.observableArrayList();

W tabeli znajdują się dwie kolumny. Pierwsza będzie wyświetlać nazwę wątku, druga postęp wykonania zadania. Konfiguracja tabeli z dwiema kolumnami:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    private void configTable() {
        tableData.setEditable(false);
        TableColumn nameCol = new TableColumn("Nazwa wątku");
        nameCol.setCellValueFactory(
                new PropertyValueFactory<ThreadTask,Integer>("name") //1
            );
        nameCol.setPrefWidth(150);        
       
        TableColumn progressCol = new TableColumn("Postęp");
        progressCol.setCellValueFactory( new Callback<CellDataFeatures<ThreadTask, ProgressBar>, ObservableValue<ProgressBar>>() {
            @Override
            public ObservableValue<ProgressBar> call(CellDataFeatures<ThreadTask, ProgressBar> arg0) {
                ThreadTask task = arg0.getValue();
                ProgressBar bar = new ProgressBar();
                bar.setPrefWidth(630);
                bar.progressProperty().bind(task.progressProperty()); //2
                return new SimpleObjectProperty<ProgressBar>(bar);
            }
        });
        progressCol.setPrefWidth(630);
        tableData.getColumns().addAll(nameCol, progressCol);
    }

Dane w komórkach tabeli powiązane są z polami w klasie typu ThreadTask. W pierwszej kolumnie definiujemy odczytywanie danych z funkcji getName() — linia nr 1. Druga kolumna progressCol jest trochę bardziej skomplikowana. Umieszczony został w niej element typu ProgressBar. Powinien on wyświetlać aktualny stan wykonania zadania. Powiązanie to jest wykonane w linii nr 2.

Teraz najważniejszy element demonstracyjnej aplikacji: uruchomienie nowego wątku po wciśnięciu przycisku „Nowy wątek typu Task” lub „Nowy wątek typu Service”. Kod akcji uruchamiany po wciśnięciu przycisku:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    private void attachActions() {
        runTaskButton.setOnAction(new EventHandler<ActionEvent>() {
            public void handle(ActionEvent t) {
                ThreadTask task = new ThreadTask(); //1
                new Thread(task).start(); //2
                listThreads.getThreadList().add(task);
            }
        });
        runServiceButton.setOnAction(new EventHandler<ActionEvent>() {
            public void handle(ActionEvent t) {
                Service service = new ServiceTask(); //3
                service.setExecutor(executor); //4
                service.start(); //5
                listThreads.getThreadList().add(service); //6
            }
        });
    }

W linii nr 1 tworzymy nowy obiekt typu ThreadTask (kod poniżej). Implementuje on interfejs „Runnable” a więc można go uruchomić jak zwykły wątek javowy – linia nr 2. Obiekt klasy ServiceTask jest uruchamiany poprzez wywołanie metody „start()”, linia nr 5. Politykę zarządzania wątkami ustawia się metodą „setExecutor”. W linii nr 6 dodajemy wątek do listy wyświetlanej przez obiekt Table. Kod klasy ThreadTask:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ThreadTask extends Task{
    private static int MAX = 100; // maksymalna wartość postępu
    private static int SLEEP_TIME = 200; // czas wstrzymania wątku w ms
    private String threadName;    // nazwa wątku
   
    /**
     * @return Nazwa wątku
     */

    public String getName() {
        return threadName;
    }
   
    /**
     * Wykonanie zadania
     * @see javafx.concurrent.Task#call()
     */

    @Override
    protected Object call() throws Exception {
        threadName = Thread.currentThread().getName();
       
        for (int i=1; i<=MAX; i++) {  // pętla od 1 do 100
            updateProgress(i, MAX);   // aktualizacja postępu pracy wątku
            Thread.sleep(SLEEP_TIME); // wstrzymanie czasu pracy wątku                    
        }      
        return null;
    }
}

Aktualizacja postępu wykonania jest ustawiana za pomocą metody updateProgress(int,int). Pierwszy argument to stan pracy, drugi wartość maksymalna.

Kod klasy ThreadService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThreadService extends Service<Void>{
   
    private ThreadTask task;
   
    public String getName() {
        return "[Service] " + task.getThreadName();
    }
   
    @Override
    protected Task createTask() {
        task = new ThreadTask();
        return task;
    }

}

Podsumowanie

Jak widać, programowanie wielowątkowe w JavaFX 2 jest dość łatwe. Po poznaniu mechanizmu tworzenia i uruchamiania wątków oraz komunikacji wątków z interfejsem javafx mamy pewność, że pisane przez nas programy będą stabilne i wydajne.


Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

2 komentarzy do “JavaFX 2 – aplikacja wielowątkowa