GUI-Probleme mit Threads

Ich habe ein Statusfenster programmiert, in dem der aktuelle Arbeitsvorgang anzeigt werden soll. Dazu gibt es eine JProgressBar und eine JTextArea. Nur werden die leider erst angezeigt wenn bereits alle Arbeitsschritte im Hintergrund beendet wurden, dazwischen bleibt die GUI einfach grau, und der Rest der Programmoberfläche ist ebenfalls nicht ansprechbar.

Mein Problem ist dass ich bisher noch nicht die geringste Ahnung von Thread-Programmierung habe. Mir ist nur bekannt dass ein sogen. EventDispatcher-Thread für das Zeichnen der GUI-Komponenten zuständig ist.
Da ich das Programm für einen kommerziellen Zweck programmiere, möchte ich hier nicht den ganzen Programmcode posten. Die ProgressWindow-Klasse sieht so aus:


import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

public class ProgressWindow extends JFrame {
	protected String title;
	protected int worksteps;
	
	// GUI-Components
	protected Point winCoords;
	protected JProgressBar progressBar;
	protected JTextArea progressField;
	protected JButton close;
	
	public ProgressWindow( String title, int worksteps, Point winCoords ) {
		this.title = title;
		this.worksteps = worksteps;
		this.winCoords = winCoords;
		this.init();
	}
	
	public void addFinishedWorkstep( final String message ) {
		//Wert der Progressbar erhoehen
		progressBar.setValue( progressBar.getValue()+1 );
		progressBar.setString( (progressBar.getValue()*100/progressBar.getMaximum()) + " %" );
				
		// Statusausgabe
		String oldText = progressField.getText();
		if ( oldText.isEmpty() )
			progressField.setText( message );
		else
			progressField.setText( oldText + '
' + message );
	}
	
	public void finishProgress() {
		if ( this.progressBar.getValue() < this.progressBar.getMaximum() ) {
			if ( this.progressBar.getValue() == 0 )
				this.progressField.setText( this.progressField.getText() + "Nothing to do ..." );
			this.progressBar.setValue( this.progressBar.getMaximum() );
			this.progressBar.setString( "100 %" );
		}
		
		this.progressField.setText( this.progressField.getText() + "
Finished!" );
		this.unlockCloseButton();
	}
	
	protected void unlockCloseButton() {
		this.close.setEnabled( true );
	}
	
	protected void init() {
		this.setTitle( this.title );
		this.setSize( 600, 370 );
		this.setLocation( this.winCoords );
		this.setResizable( false );
		this.setDefaultCloseOperation( JFrame.DO_NOTHING_ON_CLOSE );
		this.setLayout( null );
		
		this.progressBar = new JProgressBar( JProgressBar.HORIZONTAL, 0, this.worksteps );
		this.progressBar.setStringPainted( true );
		this.progressBar.setString( "0 %" );
		this.progressBar.setBounds( 10, 10, this.getWidth()-25, 25 );
		
		this.progressField = new JTextArea();
		this.progressField.setEditable( false );
		this.progressField.setWrapStyleWord( false );
		this.progressField.setBackground( Color.WHITE );
		
		JScrollPane progressPane = new JScrollPane( this.progressField );
		progressPane.setBounds( 10, this.progressBar.getX()+this.progressBar.getHeight()+10, this.getWidth()-25, 250 );
		
		this.close = new JButton( "Abschließen" );
		this.close.addActionListener( new ActionListener() {
			public void actionPerformed( ActionEvent ae ) {
				setVisible( false );
				dispose();
			}
		});
		this.close.setEnabled( false );
		this.close.setSize( 250, 25 );
		this.close.setLocation( (this.getWidth()-this.close.getWidth())/2, progressPane.getX()+progressPane.getHeight()+50 );
		
		this.add( this.progressBar );
		this.add( progressPane );
		this.add( this.close );
		
		this.setVisible( true );
	}
}```

Bis 25. Februar sollt ich zumindestens eine vorläufige Version des Programms präsentieren. Ich bin für jede Hilfe dankbar! :)

**EDIT:**
Ich habs jetzt mal mit der Observer-Klasse versucht, funktioniert aber leider auch nicht :(
```package gcomponents;

import java.awt.*;
import java.awt.event.*;
import java.util.Observable;
import java.util.Observer;

import javax.swing.*;

public class ProgressWindow extends JFrame implements Observer {
	protected String title;
	protected int worksteps;
	
	// GUI-Components
	protected Point winCoords;
	protected JProgressBar progressBar;
	protected JTextArea progressField;
	protected JButton close;
	
	public ProgressWindow( String title, int worksteps, Point winCoords ) {
		this.title = title;
		this.worksteps = worksteps;
		this.winCoords = winCoords;
		this.init();
	}
	
	public void addFinishedWorkstep( String message ) {
		//Wert der Progressbar erhoehen
		progressBar.setValue( progressBar.getValue()+1 );
		progressBar.setString( (progressBar.getValue()*100/progressBar.getMaximum()) + " %" );
				
		// Statusausgabe
		String oldText = progressField.getText();
		if ( oldText.isEmpty() )
			progressField.setText( message );
		else
			progressField.setText( oldText + '
' + message );
	}
	
	public void finishProgress() {
		if ( this.progressBar.getValue() < this.progressBar.getMaximum() ) {
			if ( this.progressBar.getValue() == 0 )
				this.progressField.setText( this.progressField.getText() + "Nothing to do ..." );
			this.progressBar.setValue( this.progressBar.getMaximum() );
			this.progressBar.setString( "100 %" );
		}
		
		this.progressField.setText( this.progressField.getText() + "
Finished!" );
		this.unlockCloseButton();
	}
	
	protected void unlockCloseButton() {
		this.close.setEnabled( true );
	}
	
	protected void init() {
		this.setTitle( this.title );
		this.setSize( 600, 370 );
		this.setLocation( this.winCoords );
		this.setResizable( false );
		this.setDefaultCloseOperation( JFrame.DO_NOTHING_ON_CLOSE );
		this.setLayout( null );
		
		this.progressBar = new JProgressBar( JProgressBar.HORIZONTAL, 0, this.worksteps );
		this.progressBar.setStringPainted( true );
		this.progressBar.setString( "0 %" );
		this.progressBar.setBounds( 10, 10, this.getWidth()-25, 25 );
		
		this.progressField = new JTextArea();
		this.progressField.setEditable( false );
		this.progressField.setWrapStyleWord( false );
		this.progressField.setBackground( Color.WHITE );
		
		JScrollPane progressPane = new JScrollPane( this.progressField );
		progressPane.setBounds( 10, this.progressBar.getX()+this.progressBar.getHeight()+10, this.getWidth()-25, 250 );
		
		this.close = new JButton( "Abschließen" );
		this.close.addActionListener( new ActionListener() {
			public void actionPerformed( ActionEvent ae ) {
				setVisible( false );
				dispose();
			}
		});
		this.close.setEnabled( false );
		this.close.setSize( 250, 25 );
		this.close.setLocation( (this.getWidth()-this.close.getWidth())/2, progressPane.getX()+progressPane.getHeight()+50 );
		
		this.add( this.progressBar );
		this.add( progressPane );
		this.add( this.close );
		
		this.setVisible( true );
	}

	public void update( Observable obs, Object o ) {
		//System.out.println( o );
		this.addFinishedWorkstep( (String) o );
		this.repaint();
	}
}```

Aufruf aus der "Arbeiterklasse":
```protected void setProgressMessage( final String message ) {
		if ( this.progressWindow != null ) {
		    /*SwingUtilities.invokeLater( new Runnable(){
		        public void run() {
		        	progressWindow.addFinishedWorkstep( message );
		        }
		    } );*/
			
		    this.setChanged();
			this.notifyObservers( message );
		} else {
			System.out.println( message );
		}
	}```

Naja also ich seh in der ProgressWindow Klasse jetzt so direkt keinen Fehler. Ich hab auch keine Ahnung was ne ProgressBar ist und wie sie funktioniert, aber wirklich schwierig sollte es ja eigentlich nicht sein. Die Frage ist halt wo du diese Progessbar “hochzählst” sag ich jetzt mal. Der Code dazu fehlt leider.

Der Fehler, dass man erst ganz am Schluss was sieht, ist typisch für ne fehlerhafte Thread Programmierung. Du kannst jedenfalls nicht durch ne Art Schleife einfach nur diese addFinishedWorkstep Methode aufrufen. Dazu brauch man dann einen Thread das stimmt ja (das Fenster muss ja ständig neu gezeichnet werden, und das wird im Thread über den Aufruf von repaint() gelöst).

Wenn du ein kleines kompilierbares Beispiel zur Verfügung stellen kannst, kann ich dir sicher noch besser Helfen. Ansonsten kannst du mal versuchen das Fenster zu verschieben (über den Bildschirmrand hinaus und wieder hinein). Damit zwingst du Java das Fenster neu zu zeichnen, also die paint Methode aufzurufen. Wenn du jetzt ne Veränderung siehst kannst du dir ziemlich sicher sein, dass dein Thread fehlerhaft implementiert ist. Vielleicht hilft auch schon ein einfaches minimieren/ maximieren

Hier die CreateThumbnail.java (Anmerkung: In scanDirectory() wird setProgressMessage() aufgerufen!)


import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;

import javax.imageio.ImageIO;
 
public class CreateThumbnail extends Thread {
	protected ProgressWindow progressWindow;
	
	protected File quellverzeichnis;
	protected File zielverzeichnis;
	protected int width;
	protected int height;
	protected boolean override;
	protected boolean brand;
	
	public CreateThumbnail( File source, File goal, int width, int height, boolean override, boolean brand, ProgressWindow progressWindow ) { ... }
	
	public static boolean isImage( File image ) { ... }
	
	public static boolean isImage( String image ) { ... }
	
	protected boolean addBrandToImage( BufferedImage orig, BufferedImage logo, String suffix, File target ) { ... }
	
	protected Dimension getProportionalDimension( int width, int height, int newWidth, int newHeight ) { ... }
	
	public void scaleImage( File img, File zielpfad, int width, int height, boolean brand ) { ... }
	
	public boolean copyTo( File file1, File file2 ) { ... }
	
	protected void setProgressMessage( String message ) {
		if ( this.progressWindow != null ) {
		   this.progressWindow.addFinishedWorkstep( message );
		} else {
			System.out.println( message );
		}
	}
	
	public void scanDirectory() { ... }
	
	public void run() {
		this.scanDirectory();
	}
}```

ProgressWindow.java
```import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

public class ProgressWindow extends JFrame {
	protected String title;
	protected int worksteps;
	
	// GUI-Components
	protected Point winCoords;
	protected JProgressBar progressBar;
	protected JTextArea progressField;
	protected JButton close;
	
	public ProgressWindow( String title, int worksteps, Point winCoords ) {
		this.title = title;
		this.worksteps = worksteps;
		this.winCoords = winCoords;
		this.init();
	}
	
	public void addFinishedWorkstep( String message ) {
		// Wert der Progressbar erhoehen
		progressBar.setValue( progressBar.getValue()+1 );
		progressBar.setString( (progressBar.getValue()*100/progressBar.getMaximum()) + " %" );
				
		// Statusausgabe
		String oldText = progressField.getText();
		if ( oldText.isEmpty() )
			progressField.setText( message );
		else
			progressField.setText( oldText + '
' + message );
		
		this.repaint();
	}
	
	public void finishProgress() {
		if ( this.progressBar.getValue() < this.progressBar.getMaximum() ) {
			if ( this.progressBar.getValue() == 0 )
				this.progressField.setText( this.progressField.getText() + "Nothing to do ..." );
			this.progressBar.setValue( this.progressBar.getMaximum() );
			this.progressBar.setString( "100 %" );
		}
		
		this.progressField.setText( this.progressField.getText() + "
Finished!" );
		this.unlockCloseButton();
	}
	
	protected void unlockCloseButton() {
		this.close.setEnabled( true );
	}
	
	protected void init() {
		this.setTitle( this.title );
		this.setSize( 600, 370 );
		this.setLocation( this.winCoords );
		this.setResizable( false );
		this.setDefaultCloseOperation( JFrame.DO_NOTHING_ON_CLOSE );
		this.setLayout( null );
		
		this.progressBar = new JProgressBar( JProgressBar.HORIZONTAL, 0, this.worksteps );
		this.progressBar.setStringPainted( true );
		this.progressBar.setString( "0 %" );
		this.progressBar.setBounds( 10, 10, this.getWidth()-25, 25 );
		
		this.progressField = new JTextArea();
		this.progressField.setEditable( false );
		this.progressField.setWrapStyleWord( false );
		this.progressField.setBackground( Color.WHITE );
		
		JScrollPane progressPane = new JScrollPane( this.progressField );
		progressPane.setBounds( 10, this.progressBar.getX()+this.progressBar.getHeight()+10, this.getWidth()-25, 250 );
		
		this.close = new JButton( "Abschließen" );
		this.close.addActionListener( new ActionListener() {
			public void actionPerformed( ActionEvent ae ) {
				setVisible( false );
				dispose();
			}
		});
		this.close.setEnabled( false );
		this.close.setSize( 250, 25 );
		this.close.setLocation( (this.getWidth()-this.close.getWidth())/2, progressPane.getX()+progressPane.getHeight()+50 );
		
		this.add( this.progressBar );
		this.add( progressPane );
		this.add( this.close );
		
		this.setVisible( true );
	}
}```

Aufruf der beiden Klassen: ( this.progressWindow ist eine Instanz von ProgressWindow )
```// Scannt Quellverzeichnis nach Unterverzeichnissen (Fotos zum Drucken)
		for ( File tmp : this.sourcePath.listFiles() ) {
			if ( tmp.isDirectory() ) {
				// Erstellt aus den Bildern im Unterordner die Mail-Fotos
				CreateThumbnail mail = new CreateThumbnail(
					tmp,
					this.targetPathOriginals,
					900,
					900,
					false,
					true,
					this.progressWindow
				);
				mail.start();
			}
		}
		
		// Erstellen der Thumbnails
		CreateThumbnail vorschau = new CreateThumbnail(
			this.targetPathOriginals,
			this.targetPathThumbnails,
			150,
			150,
			false,
			false,
			this.progressWindow
		);
		vorschau.start();
		
		// Beendet ProgressWindow ordnungsgemaess
		this.progressWindow.finishProgress();```

Ich hab's zwar jetzt so geschafft, dass die GUI synchron geschalten wird, allerdings ist bei 50% des Arbeitsvorganges sense! Der Thread vorschau wird nicht mehr ausgeführt. Außerdem wird **this.progressWindow.finishProgress();** merkwürdigerweise zuallererst ausgeführt! So ist die ProgressBar schon auf 100% obwohl noch gar nicht der erste Arbeitsschritt getan wurde.

ich erkenne das bei dir gerade nicht richtig, aber du musst deinen Dialog den du anzeigen willst (der grau bleibt) in dem Thread erzeugen der nicht voll ausgelastet ist.

wenn du willst kann ich dir so einen Dialog auch geben, ich hab sowas schon einmal gebaut

ThreadDemo.jar (Quellcode im jar)

Jo in der Run Methode oder beim Ablauf des Threads muss die ProgressBar halt verändert werden. Leider sieht man die wichtigen Stellen wieder nicht :wink:

public void scanDirectory() { … }

public void run() {
    this.scanDirectory();
}

Ein kleines kompilierbares Beispiel würde bei der Menge an Code wirklich helfen. Es geht ja nur um die prinzipielle Funktionsweise

Noch ein paar Bemerkungen zu meinem obigen Beispiel:
Es geht ja hier um sehr viele Threads, die gleichzeitig laufen.
Sammele sie also in einer Liste, die du dann in einem extra Thread durchläufst
um mit Thread#join auf das Ende jedes einzelnen Threads zu warten (in finishProgress).
Die Aktualisierung der GUI sollte immer auf dem EDT erfolgen (–>SwingUtilities.invokeLater).
Anstatt “setText” nimmt man besser “append” um die JTextArea zu erweitern.
“append” ist “thread safe” und kann aus dem Workerthread direkt aufgerufen werden.
Statt Nullayout benutze lieber LayoutManager!
Auch ist es besser, dein Fenster nicht absolut zu positionieren, sondern relativ zum Owner:
setLocationRelativeTo(owner);

Ich habe das Problem jetzt gelöst, indem ich nur jeweils die Arbeiterklassen von java.lang.Thread abgeleitet habe.

@Andre Uhres
Ja, beim ProgressWindow einen Layoutmanager zu verwenden kam mir auch in den Sinn. Das mit append() ist ein guter Tipp, werde ich gleich einbauen!
Die ThreadDemo werde ich mir auf jeden mal durchsehen, meine Umsetzung ist sicherlich noch verbesserungswürdig.