2D-Grafik Slideshow (DoubleBuffering und Image transformieren/bewegen)

Tank_AUT

Mitglied
Hallo liebes Forum!

Ich habe folgendes Problem: Ich will eine ganz stinknormale Foto-Slideshow in Java programmieren. Dazu habe ich mir eine Klasse gebastelt, die JPanel extended und verschiedene Methoden implementiert. Unter anderem das Setzen des Hintergrundbildes und das Sliden von einem Bild aufs nächste (Das alte Bild wandert auf der einen Panelseite heraus, während das andere sich auf der anderen Seite hineinschiebt).
Nun zu den Problemen:

1. Beim Sliden kommt es bei der Anzeige immer wieder zu Rucklern und "lag", so das das Endergebnis als Animation unzufriedenstellend ist. Ich habe mich zwar mit Doppelpufferung beschäftigt und dazu Tests durchgeführt (bigPaint(); ist einer dieser Tests), jedoch habe ich das Prinzip entweder nicht verstanden, oder es ist nicht für mein Problem geeignet. Die Ruckler hat es jedenfalls nicht beseitigt.

2. Der Slide an sich wird bis jetzt in Absolutwerten und durchgeführt (soll heißen, das bild wird um jeweils einen Pixel verschoben, die geschwindigkeit wird über ein Thread.sleep(sleeptime); geregelt. Auch diese vorläfige Lösung ist mmn äußerst unbefriedigend, da bei aufziehen des Fensters (Vollbild) die Geschwindigkeit gleich bleibt -> zum Füße einschlafen langsam. Auch ist die Maximalgeschwindigkeit begrenzt, da man nicht schneller als 1Pixel pro "Zykluszeit?" ->(wenn Thread.sleep(0); also die schnellstmögliche Durchlaufzeit) fahren kann.

Ich bitte hier nicht um eine "Komplettlösung" aber ich würde mich sehr über Verbesserungsvorschläge bezüglich der Probleme und natürlich auch anderer Fehler/Unrenheiten im Code freuen.

Nun, hier der Code:

Java:
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package fba_bilderkennung.GUI_Slideshow.guihelper;

import com.sun.org.apache.xml.internal.dtm.ref.DTMDefaultBaseIterators;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author ****
 */
public class ImagePanel extends javax.swing.JPanel implements slideObserver {

    private BufferedImage img;
    private static boolean inProgress = false;

    /**
     * Creates new form ImagePanel
     */
    public ImagePanel() {
        initComponents();
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);


    }

    private void bigPaint(Graphics g) {
        g.drawImage(img, 0, 0, getWidth(), getHeight(), null);
    }
    private final GraphicsConfiguration gfxConf = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
    private BufferedImage offImg;

    @Override
    protected void paintComponent(Graphics g) {
        if (offImg == null || offImg.getWidth() != getWidth() || offImg.getHeight() != getHeight()) {
            offImg = gfxConf.createCompatibleImage(getWidth(), getHeight());
            bigPaint(offImg.createGraphics());
        }

        g.drawImage(offImg, 0, 0, this);
        bigPaint( g );
    }


    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        setLayout(new java.awt.GridLayout(1, 0));
    }// </editor-fold>                        
    // Variables declaration - do not modify                     
    // End of variables declaration                   

    public void setImage(BufferedImage img) {
        this.img = img;
        repaint();
    }

    public void slideLeft(BufferedImage imgOld, BufferedImage imgNew, Graphics g) {
        super.paint(g);
        SlideThread slide = new SlideThread("slider", imgOld, imgNew, g, this, "l");
        slide.setDaemon(true);
        slide.start();

    }

    public void slideRight(BufferedImage imgOld, BufferedImage imgNew, Graphics g) {
        super.paint(g);
        SlideThread slide = new SlideThread("slider", imgOld, imgNew, g, this, "r");
        slide.setDaemon(true);
        slide.start();
    }

    @Override
    public void update(BufferedImage img, boolean inProgress) {
//        System.out.println("UPDATE");
        ImagePanel.inProgress = inProgress;
        setImage(img);
    }

    private void setInProgress(boolean inProgress) {
        ImagePanel.inProgress = inProgress;
    }

    private static class SlideThread extends Thread {

        private final BufferedImage imgOld;
        private final BufferedImage imgNew;
        private final Graphics g;
        private final ImagePanel p;
        private final String direction;

        public SlideThread(String name, BufferedImage imgOld, BufferedImage imgNew, Graphics g, ImagePanel p, String direction) {
            super(name);
            this.imgOld = imgOld;
            this.imgNew = imgNew;
            this.g = g;
            this.p = p;
            this.direction = direction;
        }

        @Override
        public void run() {
            while (ImagePanel.inProgress) {
                //System.out.println("waiting: "+direction);
                
            }
            ImagePanel.inProgress = true;
            int slidepix = 0;
            if (direction.equalsIgnoreCase("l")) {
                while (slidepix <= p.getWidth()) {
                    BufferedImage combined = new BufferedImage(p.getWidth(), p.getHeight(), BufferedImage.TYPE_INT_ARGB);
                    Graphics2D cg = (Graphics2D) combined.getGraphics();

                    cg.drawImage(imgOld, 0 - slidepix, 0, p.getWidth(), p.getHeight(), null);
                    cg.drawImage(imgNew, p.getWidth() - slidepix, 0, p.getWidth(), p.getHeight(), null);

                    p.update(combined, true);
                    try {
                        Thread.sleep(((int) ((Math.pow((slidepix - (p.getWidth() / 2)), 1.1)) + (p.getWidth() / 2))) + 2); //Versuch die animation "smooth" zu machen
                    } catch (InterruptedException ex) {
                        //Logger.getLogger(ImagePanel.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    slidepix++;
                    System.out.println("Sliding: " + slidepix + "px");
                }
            }

            if (direction.equalsIgnoreCase("r")) {
                while (slidepix >= -p.getWidth()) {
                    BufferedImage combined = new BufferedImage(p.getWidth(), p.getHeight(), BufferedImage.TYPE_INT_ARGB);
                    Graphics2D cg = (Graphics2D) combined.getGraphics();

                    cg.drawImage(imgOld, 0 - slidepix, 0, p.getWidth(), p.getHeight(), null);
                    cg.drawImage(imgNew, -p.getWidth() - slidepix, 0, p.getWidth(), p.getHeight(), null);

                    p.update(combined, true);
                    try {
                        double progress = (slidepix + .0) / (p.getWidth() + .0);

                        System.out.println("" + progress);

                        int sleepingtime = (int) (Math.pow(Math.abs(progress - 0.5), 1.01));
                        System.out.println("Time: " + sleepingtime + "ms");
                        Thread.sleep(sleepingtime);
                    } catch (InterruptedException ex) {
                        //Logger.getLogger(ImagePanel.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    slidepix--;
                    System.out.println("Sliding: " + slidepix + "px");
                }
            }


            p.update(imgNew, false);


        }
    }
}

(Anmerkung: Der Code ist aufgrund einiger Tests und ungereimtheiten leider nicht was an strukturiert oder Aufgeräumt nennen kann. Ich entschuldge mich für die Unannehmlichkeiten.)
(Anmerkung2: Die Veriegelung der Threads untereinander um nicht mehrere Slides gleichzeitig zu zeichen - Was natürlich in ein heilloses durcheinander enden würde - ist noch nicht funktionstüchtig, jedoch nicht essentiell für meine eigentlichen Probleme. Dieses Problem kann ich selbst lösen.)

Konkret will ich also das ruckeln wegbringen und den Slide an sich in einer angemessenen Geschwindigkeit und eventuell "smooth" (langsam anfahren, schneller werden, wieder langsam abbremsen) machen.


Danke im Voraus für die Hilfe. :toll:
 
:shock: Da ist ziemlich viel :autsch: dabei, so beim ersten Drüberschauen. (Der Thread passt wohl nur bedingt, da es hier nur um Bilder geht)

Sowas wie
while (ImagePanel.inProgress) {}
ist "busy waiting" und i.a. Unfug, und kann ziemlich unschöne Konsequenzen haben.

Viel kritischer dürfte hier aber sein, dass ja bei jedem Durchlauf ein neues Bild erzeugt wird. Man kann die Bilder ja einfach an der passenden Position malen...

Hab' mal schnell was hingestümpert - ist nur sehr rudimentär, behandelt z.B. Klicks während der Animation nicht richtig und so, aber ein Ansatz, vielleicht
Java:
import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;


public class ImageSlidePanel extends JPanel
{
	public static void main(String[] args)
	{
		SwingUtilities.invokeLater(new Runnable()
		{
			@Override
			public void run()
			{
				try
				{
					createAndShowGUI();
				}
				catch (IOException e)
				{
					e.printStackTrace();
				}
			}
		});
	}
	
	private static void createAndShowGUI() throws IOException
	{
		JFrame f = new JFrame();
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.getContentPane().setLayout(new BorderLayout());

		List<BufferedImage> images = new ArrayList<BufferedImage>();
		images.add(ImageIO.read(new File("image00.png")));
		images.add(ImageIO.read(new File("image01.png")));
		images.add(ImageIO.read(new File("image02.png")));
		
		final ImageSlidePanel imageSlidePanel = new ImageSlidePanel(images);
		f.getContentPane().add(imageSlidePanel, BorderLayout.CENTER);
		
		JPanel panel = new JPanel(new GridLayout(1,2));
		JButton leftButton = new JButton("<-");
		leftButton.addActionListener(new ActionListener()
		{
			
			@Override
			public void actionPerformed(ActionEvent e)
			{
				imageSlidePanel.moveLeft();
			}
		});
		panel.add(leftButton);
		JButton rightButton = new JButton("->");
		rightButton.addActionListener(new ActionListener()
		{
			
			@Override
			public void actionPerformed(ActionEvent e)
			{
				imageSlidePanel.moveRight();
			}
		});
		panel.add(rightButton);
		
		f.getContentPane().add(panel, BorderLayout.SOUTH);
		f.setSize(1000,1000);
		f.setVisible(true);
	}
	
	
	private List<BufferedImage> images;
	private int currentIndex = 0;
	private int currentOffset = 0;
	private int targetIndex = 0;
	private Object monitor = new Object();
	
	public ImageSlidePanel(List<BufferedImage> images)
	{
		if (images.size() == 0)
		{
			throw new IllegalArgumentException("At least one image required");
		}
		this.images = new ArrayList<BufferedImage>(images);
		
		Thread thread = new Thread(new Runnable()
		{
			@Override
			public void run()
			{
				handleAnimation();
			}
		});
		thread.setDaemon(true);
		thread.start();
	}
	
	@Override
	protected void paintComponent(Graphics g)
	{
		super.paintComponent(g);
		
		int index = currentIndex-1;
		BufferedImage image = getImage(index);
		int x = -image.getWidth()+currentOffset;
		while (x < getWidth())
		{
			g.drawImage(image, x, 0, this);
			x += image.getWidth();
			index++;
			image = getImage(index);
		}
	}
	
	private int getIndex(int index)
	{
		int result = index%images.size();
		if (result < 0)
		{
			result += images.size();
		}
		return result;
	}
	
	
	private BufferedImage getImage(int index)
	{
		return images.get(getIndex(index));
	}
	
	
	private void handleAnimation()
	{
		while (true)
		{
			//System.out.println("Current "+currentIndex);
			//System.out.println("Target "+targetIndex);
			
			while (currentIndex == targetIndex)
			{
				synchronized (monitor)
				{
					try
					{
						monitor.wait();
					}
					catch (InterruptedException e)
					{
						Thread.currentThread().interrupt();
						return;
					}
				}
			}
			
			//System.out.println("Current "+currentIndex);
			//System.out.println("Target "+targetIndex);

			if (targetIndex > currentIndex)
			{
				BufferedImage image = getImage(currentIndex);
				int targetOffset = -image.getWidth();
				while (currentOffset > targetOffset)
				{
					currentOffset -= 10;
					currentOffset = Math.max(currentOffset, targetOffset);
					repaint();
					try
					{
						Thread.sleep(10);
					}
					catch (InterruptedException e)
					{
						Thread.currentThread().interrupt();
						return;
					}
				}
				currentIndex = targetIndex;
				currentOffset = 0;
				repaint();
			}
			if (targetIndex < currentIndex)
			{
				BufferedImage image = getImage(targetIndex);
				int targetOffset = image.getWidth();
				while (currentOffset < targetOffset)
				{
					currentOffset += 10;
					currentOffset = Math.min(currentOffset, targetOffset);
					repaint();
					try
					{
						Thread.sleep(10);
					}
					catch (InterruptedException e)
					{
						Thread.currentThread().interrupt();
						return;
					}
				}
				currentIndex = targetIndex;
				currentOffset = 0;
				repaint();
			}
			
			
			
		}
	}
	
	
	public void moveLeft()
	{
		targetIndex -= 1;
		synchronized (monitor)
		{
			monitor.notifyAll();
		}
	}
	public void moveRight()
	{
		targetIndex += 1;
		synchronized (monitor)
		{
			monitor.notifyAll();
		}
	}
	
	
}
 
Mir gings eher um das Konzept: Ein Bild wird nicht Pixel für Pixel verschoben, sondern man legt eine Aktualisierungsrate fest und in dieser wird die Position des zu bewegenden Bildes neu berechnet und gezeichnet. Das heißt, z.B ich will 30 Aktualisierungen pro Sekunde -> 1 Sekunde /30=33 Millisekunden Pause zwischen zwei Berechnungen.
 
Zuletzt bearbeitet:
Also erst mal sehr großen Dank and Marco13:toll:

Mir ist klar, das ich sehr viele Fehler mache. Dies ruht vor allem daher, das ich mich 1. Mit speziell diesem Thema hier (überschneidende Threads, sycronisierung und ähnliches) hier zum ersten mal befasse; 2. Ich nie "richtiges" Software-Design gelehrt bekommen habe (habe also keine Ahnung was "schön" programmiert ist, bzw. welche Alternativen es zu gewisse Stümperhaftigkeiten die ich begehe gibt.)

Dein Programm hat mir schon sehr weitergeholfen und ich habe mir ein paar Teile daraus adaptiert, sofern es dir nichts ausmacht. Jedoch verstehe ich einige wenige Sequenzen nicht vollständig un bitte daher um eine etwas genauere Erklärung :bahnhof:.

In diesem Teil hier:
Java:
@Override
    protected void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        
        int index = currentIndex-1;
        BufferedImage image = getImage(index);
        int x = -image.getWidth()+currentOffset;
        while (x < getWidth())
        {
            g.drawImage(image, x, 0, this);
            x += image.getWidth();
            index++;
            image = getImage(index);
        }
    }

Ich versuche es zu interpretieren: Du zeichnest offensichtlich das Image hinaus. Was mich iritiert ist die While-Schleife außerhalb des Threads. Auch, was es mit der inkrementierung des Indexes auf sich hat bleibt mir schleierhaft. Ich bitte dich, das du mir versuchst es zu erklären, da ich recht neu in Java bin und mir hier einfach nicht der Knopf aufgehen will.

Wieder mal Danke im voraus!
 
Hmja, es gibt ta tausende Möglichkeiten (und nebenbei: Das gepostete ist, wie auch deutlich gesagt, noch weit weg von dem, was ich als "gut" bezeichnen würde - statt dess "monitor"s würde man eher Locks & Conditions nehmen, und ... auf Buttonklicks während der Animation "vernünftig" zu reagieren wäre schon etwas frickeliger, aber ja das mindeste, was man bei so einem Programm erwarten würde...)

Aber zum Code: Es war nicht klar, wie das ganze aussehen sollte. Insbesondere wenn man ein Bild hat, das NICHT ganz genau so groß ist, wie das Fenster (bzw. schon unterschiedlich große Bilder). Es gibt den "currentIndex", der beschreibt, welches Bild aus der Liste man sich gerade ansieht. (Der currentIndex kann auch negativ oder sehr groß werden, und wird immer auf die Listenindizes umgerechnet). In der paintComponent wird jetzt im Prinzip so getan, als wären die Bilder eine "unendlich lange Liste" von Bildern, von denen man in jedem Moment einen Ausschnitt sieht. Der currentIndex besagt dann nur, auf welches Bild der Ausschnitt gerade ausrerichtet ist. Aber rechts davon kommen weitere Bilder (wenn der Bildschirm breit genug ist) - oder links ein weiteres Bild (je nach Scrollrichtung). In der paintComponent wird also direkt LINKS neben dem aktuellen Bild angefangen zu zeichnen - und zwar alle Bilder die dann eben so kommen, direkt nebeneinander - bis der Bildschirm voll ist.
Java:
        int index = currentIndex-1;
        BufferedImage image = getImage(index);  // Fange mit dem Bild links vom aktuellen an
        int x = -image.getWidth()+currentOffset; // Berechne, wo es gezeichnet werden soll
        //                                                       (ggf. ragt es ja nur ein kleines Stück ins Fenster rein)
        while (x < getWidth()) // Solange der Bildschirm nicht voll ist
        {
            g.drawImage(image, x, 0, this); // Male das aktuelle Bild
            x += image.getWidth(); // Bereite dich darauf vor, das nächste Bild rechts daneben zu malen
            index++;
            image = getImage(index); // Hole dir das nächste Bild
        }
 
Vielen vielen Dank!

Jetz hab ich's auch verstanden.
Das mit den Mausklicks ist eigentlich für meinen anwendungsfall vollkommen egal, da der input zum hin- und hersliden über ein selbstprogrammiertes Schnittstellen-Objekt daherkommt. Dieses Objekt beut eine TCP/IP verbindung zu einem anderen Programm auf welches dann die Kommandos gibt. So gesehen ist die Slideshow nur als purerOutput zu sehen.

Nochmals danke und ich kann dieses Thema hiermit für erledigt erklären! 😀
 

Zurück
Oben