Zum Inhalt

SWT: GUI bei langen Aktionen nicht einfrieren lassen

Länger dauernde Aktionen sollten nie im GUI-Thread laufen. Dies gilt auch für SWT, der GUI-Bibliothek von IBM/Eclipse für Java. Allerdings gibt es bei SWT einige Punkte zu beachten, ohne die ein Umbau schnell sehr mühsam werden kann.

Ausgangslage

Bei den meisten Erklärungen zu SWT wird im SelectionListener eines Knopfes alle Logik eingebaut, die beim Klick darauf ausgeführt werden soll. So lange die daraus resultierenden Aktionen schnell verarbeitet werden können ist dagegen auch nichts einzuwenden.

Dauert es aber länger friert einem sehr schnell das GUI ein. Je nach PC variieren die Auswirkungen von einem flackern der Anzeige bis zu kompletten Blockieren der Anwendung. Der Code wird in so einem Fall wohl meist so aussehen:

workButtonSlow.addSelectionListener(new SelectionAdapter() {
    public void widgetSelected(SelectionEvent arg0) {
        progress.setSelection(0);
        workButtonSlow.setEnabled(false);

        // simuliert lange dauernde Aktion
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
            progress.setSelection(progress.getSelection() + 1);
        }

        workButtonSlow.setText("Thread beendet");
        workButtonSlow.setEnabled(true);
    }
});

Lösung: Threads und asyncExec

Nach einigen Anläufen bin ich bei Threads und einer Synchronisierung des GUI über asyncExec gelandet. Die lange dauernden Aktionen werden in einen eigenen Thread ausgelagert und im SelectionListener nur noch gestartet. Die Verarbeitung erfolgt so losgelöst vom GUI-Thread und behindert das Neuzeichnen der Oberfläche nicht – dies genügt damit die Anwendung viel reaktiver erscheint.

Da man nun während der Ausführung der Aktion weiterarbeiten kann, steht man unter Umständen vor neuen Problemen. Falls die gleiche Aktion nicht noch einmal parallel dazu gestartet werden darf, muss man dies nun explizit verhindern. Je nach Anwendung genügt es den entsprechenden Knopf beim Start des Arbeitsthreads zu deaktivieren und erst beim beenden wieder zu aktivieren.

Eine Implementierung mit einer eigenen Thread-Klasse kann so aussehen:

class LongRunningOperation extends Thread {
        private Display display;
        private ProgressBar progressBar;
        private Button workButton;

        /**
         * Alles übergeben was aus diesem Thread erreichbar sein soll
         */
        public LongRunningOperation(Display display, ProgressBar progressBar,
                Button workButton) {
            this.display = display;
            this.progressBar = progressBar;
            this.workButton = workButton;
        }

        /**
         * Länger laufende Methode um eine Verarbeitung zu simulieren
         */
        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println(e.getMessage());
                }
                progressBar.setSelection(progressBar.getSelection() + 1);
                // ProgressBar kann nur via asyncExec aktualisiert werden!
                display.asyncExec(new Runnable() {
                    public void run() {
                        if (progressBar.isDisposed())
                            return;

                        progressBar.setSelection(progressBar.getSelection() + 1);
                    }
                });
            }

            // Gleiches gilt für alle GUI-Elemente
            display.asyncExec(new Runnable() {
                public void run() {
                    if (workButton.isDisposed())
                        return;
                    workButton.setText("Thread beendet");
                    workButton.setEnabled(true);
                }
            });
        }
    }

Der Knopf über den die zeitintensive Aktion gestartet wird ist wie alle anderen GUI-Elemente in SWT aber nicht direkt aus einem anderen Thread heraus veränderbar. Damit der Ausführungskontext stimmt müssen alle Veränderungen dieser Elemente als Runnable der Methode asyncExec übergeben werden. Wichtig ist das man das Display-Objekt nutzt mit dem man die Shell der Anwendung initialisiert hat.

Versucht man asyncExec zu umgehen wird SWT mit dieser Exception antworten:

Exception in thread "Thread-0" org.eclipse.swt.SWTException: Invalid thread access

Sind alle Zugriffe entsprechend umgeformt, kann man im SelectionListener des Knopfes die Thread-Klasse starten:

1
2
3
4
5
6
7
8
9
workButton.addSelectionListener(new SelectionAdapter() {
    public void widgetSelected(SelectionEvent arg0) {
        progress.setSelection(0);
        workButton.setEnabled(false);
        new LongRunningOperation(s_display, progress, workButton)
                .start();
        workButton.setText("SelectionListener beendet!");
    }
});

Fazit

Mit dieser Umbauarbeit kann man auch lange laufende Aktionen ausführen ohne dass einem das GUI einfriert oder an Reaktionsfähigkeit einbüsst. Dieser Ansatz ist ein wenig aufwändig, erfüllt aber seinen Zweck. (Das ganze Beispiel ist auf Github verfügbar)

Falls es einfachere Wege gibt würde ich mich über einen Kommentar freuen.