Swing Lauftext Performance Probleme, in größerer Anwendung

sMau90

Aktives Mitglied
Hey,
Ich habe eine Klasse geschrieben welche einen Lauftext in Java realisiert, sie erweitert JComponent und das zeichnen wird in der paintComponent Methode realisiert. Ich benutze einen SwingTimer für das Aufrufen von repaint() in einem vorgegebenen Intervall um so die Geschwindigkeit des Textes zu beinflussen. Das Programm um das es geht ist schon recht groß und es kommt zu folgendem Problem:
Wenn ich z.B. nun dass "Einstellungs-Fenster" meines Programms öffne, stockt der Text für einen winzigen Moment, es ist nicht sonderlich viel, aber sichtbar. Das Ganze tritt mehr oder weniger stark bei noch anderen "größeren" Aktionen auf. Ich habe schon versucht durch Optimierungen, wie alle größeren Aufgaben in SwingWorker Klassen auszulagern, oder überall dort wo es möglich ist durch die Beachtung vom Clip des GraphicsContext, die Probleme in den Griff zu bekommen. Es ist dadurch auch bedeutend besser geworden, aber leider gibt es bei bestimmten Aktionen immer noch leichte, aber leider sichtbare Ruckler.
Da Swing mit dem EDT ja ein Single-Threaded-Model umsetzt, hab ich leider nun keine Idee mehr was ich da noch machen kann.
Ich hoffe jemand von euch hat hier ein paar Tips oder Tricks wie ich das ganze besser in den Griff bekomme.
Hier mal der Code der entsprechenden Komponente.


Java:
public class TickerComponent extends JComponent {
	
	private String tickerString;
	private int posOfString1;
	private int posOfString2;
	
	private int toDrawStringWidth;
	
	private int tickerSpeed;
	
	private String toDraw1;
	private String toDraw2;
	
	private Timer liveTickerTimer;

	public TickerComponent() {
		super();
		posOfString1 = getWidth();
		posOfString2 = getWidth();
		
		try {
			tickerSpeed = Integer.parseInt(PreferencesHandler.getInstance().getProperties().getProperty(Constants.PROP_TICKER_SPEED));
		} catch (NumberFormatException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
        startLiveTicker();

		
	}
	
	/**
	 * creates a new Swing Timer with the given update interval and starts the timer.
	 * The timer refreshes the ticker component every x milliseconds.
	 */
	public void startLiveTicker() {

		liveTickerTimer = new Timer(tickerSpeed, new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				repaint(0, 0, getWidth(), getHeight()); 
			}
		});
    	liveTickerTimer.start();
    	
	}
    
	/**
	 * stops the timer
	 */
    public void stopLiveTicker() {
    	liveTickerTimer.stop();
    }

    
	/**
	 * Set the ticker string to draw on the component.
	 * A GUI update is invoked automatically after setting the new string.
	 * @param tickerString
	 */
	public void setTickerString(String tickerString) {
		this.tickerString = tickerString;
		if(tickerString != null && !tickerString.equals("")) {
			generateDrawingStrings();
		} else if(tickerString != null) {
			toDraw1 = " ";
			toDraw2 = " ";
			stopLiveTicker();
		} else {
			stopLiveTicker();
		}
	}
	
	/**
	 * needs to be invoked after resizing the tickerComponent, e.g. after entering the fullscreen
	 */
	public void refreshStringGeneration() {
		generateDrawingStrings();
	}
	
	/**
	 * generates the two strings needed for a gapless ticker run.
	 */
	private void generateDrawingStrings() {

		int tickerStringWidth = getFontMetrics(getFont()).stringWidth(tickerString);
		
		toDraw1 = tickerString;
		toDrawStringWidth = tickerStringWidth;
		
		while(toDrawStringWidth < getWidth()) {
			toDrawStringWidth = toDrawStringWidth + tickerStringWidth;
			toDraw1 = toDraw1 + tickerString;
		}
		
		toDraw2 = toDraw1;
		posOfString2 = posOfString1 + toDrawStringWidth;
		
	}

	@Override
	protected void paintComponent(Graphics g) {

		if (toDraw1 != null && toDraw2 != null && !toDraw1.equals("") && !toDraw2.equals("")) {
			posOfString1--;
			posOfString2--;
			//if last char of the string is not visible anymore
			if(posOfString1 + toDrawStringWidth  < 0 ) {
				posOfString1 = posOfString2 + toDrawStringWidth;
			}
			//if last char of the string is not visible anymore
			if(posOfString2 + toDrawStringWidth < 0 ) {
				posOfString2 = posOfString1 + toDrawStringWidth;
			}
			Graphics2D tmpG2D = (Graphics2D) g.create();
	        tmpG2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
	        tmpG2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
			
	        tmpG2D.drawString(toDraw1, posOfString1, 55);
	        tmpG2D.drawString(toDraw2, posOfString2, 55);
	        tmpG2D.dispose();
		} else {
			stopLiveTicker();
		}
	}

}

Vielen Dank schonmal.
Grüße,
sMau
 
Zuletzt bearbeitet:
Alles, was mit GUI zu tun hat, sollte im EDT geschehen (invokeLater-Methode aufrufen): A Java SwingUtilities invokeLater example | SwingUtilities invokeLater and the EventDispatchThread (EDT) | devdaily.com (kurze Zusammenfassung)

Inwieweit dies bei dir der Fall ist, kann ich auf die Schnelle nicht nachvollziehen

Und, inwiefern SwingWorker evtl. schneller sein könnte, auch nicht. Aber in der API werden zwei Beispiele genannt, die vielleicht aufschlussreich sind: SwingWorker (Java Platform SE 6)
 
Zuletzt bearbeitet von einem Moderator:
Mal unabhängig von dem Problem würde ich den
Code:
tickerSpeed
fest einstellen, so dass mit 20 - 30 fps gezeichnet wird und stattdessen die Schrittweite anpassen, um die Laufgeschwindigkeit zu variieren.
Ausserdem gehört die Postionsberechnung aus der paintComponent raus, da diese 1. in regelmäßigen Intervallen (gleichmäßige Geschwindigkeit) neu berechnet werden soll und nicht bei jedem neuzeichnen der Komponente und 2. in der paintComponent grundsätzlich nur das nötigste berechnet werden soll, um den EDT möglichst wenig zu blockieren.
Evtl. für das sogar schon zu einer Verbesserung des Problems.
 
Wow, cool vielen Dank @Michael. Genau das was du gesagt hast hat geholfen. Ich hab nun die Berechnung der String Positionen in einen java.util.Timer ausgelagert, welcher dann über invokeLater auf dem EDT nur das repaint() aufruft. In der paintComponent wird nun wirklich nur noch gezeichnet und siehe da die Ruckler sind weg.
Hier falls es jemand interessiert die fertige Lösung:

Java:
public class TickerComponent extends JComponent {

	private static final long serialVersionUID = 4472552567124740434L;
	
	private String tickerString;
	private int posOfString1;
	private int posOfString2;
	
	private int toDrawStringWidth;
	
	private int tickerSpeed;
	
	private String toDraw1;
	private String toDraw2;
	
	private Timer liveTickerTimer;

	
	class TickerTimerTask extends TimerTask {
		public void run() {
			runLiveTicker();
		}
	}
	
	public TickerComponent() {
		super();
		posOfString1 = getWidth();
		posOfString2 = getWidth();
		
		try {
			tickerSpeed = Integer.parseInt(PreferencesHandler.getInstance().getProperties().getProperty(Constants.PROP_TICKER_SPEED));
		} catch (NumberFormatException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
        startLiveTicker();

		
	}
	
	private void runLiveTicker() {
		if (toDraw1 != null && toDraw2 != null && !toDraw1.equals("") && !toDraw2.equals("")) {
			posOfString1--;
			posOfString2--;
			
			//if last char of the string is not visible anymore
			if(posOfString1 + toDrawStringWidth  < 0 ) {
				posOfString1 = posOfString2 + toDrawStringWidth;
			}
			//if last char of the string is not visible anymore
			if(posOfString2 + toDrawStringWidth < 0 ) {
				posOfString2 = posOfString1 + toDrawStringWidth;
			}
			SwingUtilities.invokeLater(new Runnable() {
				
				@Override
				public void run() {
					repaint(0, 0, getWidth(), getHeight());
				}
			});
			
		} else {
			stopLiveTicker();
		}
		
	}
	
	/**
	 * creates a new Swing Timer with the given update interval and starts the timer.
	 * The timer refreshes the ticker component every x milliseconds.
	 */
	public void startLiveTicker() {
		liveTickerTimer = new Timer();
		liveTickerTimer.schedule(new TickerTimerTask(), 0, tickerSpeed);
    	
	}
    
	/**
	 * stops the timer
	 */
    public void stopLiveTicker() {
    	
    	liveTickerTimer.cancel();
    	liveTickerTimer.purge();
    }
    
	/**
	 * Set the ticker string to draw on the component.
	 * A GUI update is invoked automatically after setting the new string.
	 * @param tickerString
	 */
	public void setTickerString(String tickerString) {
		this.tickerString = tickerString;
		if(tickerString != null && !tickerString.equals("")) {
			generateDrawingStrings();
		} else if(tickerString != null) {
			toDraw1 = " ";
			toDraw2 = " ";
			stopLiveTicker();
		} else {
			stopLiveTicker();
		}
	}
	
	/**
	 * needs to be invoked after resizing the tickerComponent, e.g. after entering the fullscreen
	 */
	public void refreshStringGeneration() {
		generateDrawingStrings();
	}
	
	/**
	 * generates the two strings needed for a gapless ticker run.
	 */
	private void generateDrawingStrings() {

		int tickerStringWidth = getFontMetrics(getFont()).stringWidth(tickerString);
		
		toDraw1 = tickerString;
		toDrawStringWidth = tickerStringWidth;
		
		while(toDrawStringWidth < getWidth()) {
			toDrawStringWidth = toDrawStringWidth + tickerStringWidth;
			toDraw1 = toDraw1 + tickerString;
		}
		
		toDraw2 = toDraw1;
		posOfString2 = posOfString1 + toDrawStringWidth;
		
	}

	@Override
	protected void paintComponent(Graphics g) {
		Graphics2D tmpG2D = (Graphics2D) g.create();
        tmpG2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
        tmpG2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		
        tmpG2D.drawString(toDraw1, posOfString1, 55);
        tmpG2D.drawString(toDraw2, posOfString2, 55);
        tmpG2D.dispose();
	}

}


Hab aber grade noch eine Frage zu dem was du gesagt hast, bzgl des fixieren des Updateintervalls. Ich hab da auch schon dran gedacht, so dass ich ne feste Framerate kriege, was mich an dieser Lösung stört ist die schlechte Skalierung der Geschwindigkeit, also ich kann ja immer nur entweder 1px pro Zyklus, oder 2px oder nur jeden 2. Zyklus 1px, wobei ich da die Geschwindigkeit ja jeweils halbiere/verdopple in diesem Beispiel. Gibts da ne bessere Variante, bei der ich feinere Einstellungsmöglichkeiten erreiche aber trotzdem eine feste Framerate vorgebe?

Grüße,
sMau
 
Zuletzt bearbeitet:
Das geht so: Du verwendest die Formel v=s/t (Oder Geschwindigkeit = Weg/Zeit). -> s=v*t

Also statt posOfString dauernd um 1 zu erhöhen zählst du die Ticks und machst
Java:
posOfString = (v * ticks) % breite;
 
Also statt posOfString dauernd um 1 zu erhöhen zählst du die Ticks und machst
Java:
posOfString = (v * ticks) % breite;
Vorsicht! Wenn immer nur [c]ticks++[/c] gerechnet wird, wird ticks irgendwann negativ (auch wenn dies bei int sehr lange dauern dürfte, und, wenn dies bei long passieren sollte, mindestens alle jetzt lebenden Menschen, wenn nicht sogar unser Sonnensystem, gestorben sein werden 😀).

Also sollte möglichst früh ein geeignetes [c]ticks %= maxTicks[/c] oder Ähnliches stehen.

Ark
 

Zurück
Oben