Bild an Fenstergröße anpassen und skalieren

Ich hab nen Bilder Ordner mit inzwischen 3618 Bildern und es werden täglich mehr. Da ich allerdings keine Möglichkeit gefunden habe, mit Windows-Bordmitteln immer zufällige Bilder davon anzuzeigen, wollte ich das selbst Programmieren.

Das ganze funktioniert auch problemlos, entweder per Button oder per Timer automatisch. Nun sind aber viele Bilder z.B. auf 4k Auflösung vorhanden oder sind verkleinert. Die verkleinerten sind erstmal nicht das Problem, eher die zu großen. Daher möchte ich die gerne gleichmässig runterskalieren auf Fenstergröße, ohne Verzerrungen. Allerdings hab ich irgendwo nen Denkfehler und hänge auch schon paar Stunden daran… Ich zeige die Bilder als Icon eines JLabels an. Habs auch mit der Methode probiert die als Background zu benutzen, da ist es aber so gut wie immer verzerrt, das ist es also nicht. Der Teil, in dem ich die Größen abfrage und versuche zu verändern schaut so aus:

button.addActionListener((ActionEvent evt) -> {
        System.out.println(files.length);
        int r = new Random().nextInt(files.length);
        ImageIcon icon = new ImageIcon(files[r].getPath());
        Image img = icon.getImage();
        if (img.getHeight(null) > f.getHeight() && img.getWidth(null) > f.getWidth()) {
            System.out.println("img B");
            int heigth = img.getHeight(null) - f.getHeight();
            int width = img.getWidth(null) - f.getWidth();
            if (heigth > width) {
                System.out.println("Hei");
                icon.setImage(icon.getImage().getScaledInstance(img.getWidth(null) - heigth, f.getHeight() - button.getHeight(), Image.SCALE_DEFAULT));
            } else {
                System.out.println("Wid");
                icon.setImage(icon.getImage().getScaledInstance(f.getWidth(), img.getHeight(null) - width, Image.SCALE_DEFAULT));
            }
        } else if (img.getHeight(null) > f.getHeight() - button.getHeight()) {
            System.out.println("img H");
            int i = img.getHeight(null) - f.getHeight() - button.getHeight();
            int width = img.getWidth(null) - i / 2;
            System.out.println("I: " + i + ", Width: " + width);
            icon.setImage(icon.getImage().getScaledInstance(width, f.getHeight() - button.getHeight(), Image.SCALE_DEFAULT));
        } else if (img.getWidth(null) > f.getWidth()) {
            System.out.println("img W");
            int i = img.getWidth(null) - f.getWidth();
            int height = img.getHeight(null) - i;
            icon.setImage(icon.getImage().getScaledInstance(f.getWidth(), height, Image.SCALE_DEFAULT));
        }
        label.setIcon(icon);
//            ip.setImage(img);
//            ip.repaint();
            f.setTitle(files[r].getName());
//            f.pack();
        });

Evtl sieht ja wer von euch den Fehler…

Wenn das funktioniert wollte ich noch einfügen, das ich per MouseWheellistener noch per scrollen die Größe verändern kann zum zoomen, das kommt aber danach erst

Deine Berechunngen kommen mir etwas anders vor als ich das Problem einmal für mich gelöst habe.
(hab aber grad keinen Zugiff auf meinen Code von damals)

Daher kurz zusammengefasst wie ich das damals gelöst habe.

 originalW = Width of the image
 screenW = Width of the screen
 factorW = multiplication factor for width
//
 originalH = Width of the image
 screenH = Width of the screen
 factorH = multiplication factor for height
//
 factorW = computeFactor(originalW,screenW)
 factorH = computeFactor(originalH,screenH)
//
 double factor;

 if (factorW <factorH){
   factor = factorW;
 }else{
   factor = factorH;
 }
//
 int newH = originalH * factor;
 int newW = originalW * factor;

icon.getImage().getScaledInstance(newW,newH,Image.SCALE_DEFAULT)
 ...

 private double computeFactor(int original,int screen) {
 if (original <= screen) return 1.0;
 else {
    return ((double)screen)/((double)original)
 }

wenn du später noch zoom einbauen willst, kannst du einfach “factor” nach der berechnung nocheinmal gewichten und hast den Zoomeffekt.

Den Kern der Frage hat AmunRa ja schon beantwortet. Ein paar Punkte:

  • Ich finde ja, dass ein ActionListener nur eine Methode aufrufen sollte. Also, in der actionPerformed sollten nicht zig Zeilen Code stehen.

      button.addActionListener(e -> showRandomImage());
      ....
      private void showRandomImage() {
          // HIER der code hin!
      }
    
  • Das ganze scheint sich irgendwie auf einen JFrame f zu beziehen. Überleg’ oder schau’ mal, ob man diese Abhängigkeit nicht irgendwie rausbekommt oder wegabstrahieren kann. Als minimalen Schritt dahin könnte man schon

      private void showRandomImage(int maxWidth, int maxHeight) { ... }
    

    definieren, und dort dieses Gefrickel mit übergeben, was jetzt so händisch gemacht wird - also etwa

      private void showRandomImage() { 
          int maxWidth = f.getWidth() - button.getWidth();
          int maxHeight = f.getHeight() - button.getHeight();
          showRandomImage(maxWidth, maxHeight);
      }
      private void showRandomImage(int maxWidth, int maxHeight) { ... }
    

    AAABER: Diese Berechnungen sehen stark danach aus, als würdest du höchst fragwürdige Sachen machen, die eigentlich einem LayoutManager überlassen sein sollten.

  • getScaledInstance ist langsam. Teilweise wirklich absurd langsam. Dazu hatte ich mal ein bißchen was unter 1 geschrieben.

Und nebenbei… vielleicht findest du

interessant.


1:

Der relevanteste Code-Teil davon ist (leicht angepasst)

private static BufferedImage scale(BufferedImage image, double factor) {
    RenderingHints renderingHints = new RenderingHints(null);
    renderingHints.put(
        RenderingHints.KEY_INTERPOLATION, 
        RenderingHints.VALUE_INTERPOLATION_BILINEAR);        
    }
    int w = (int)(factor * image.getWidth());
    int h = (int)(factor * image.getHeight());
    return scale(image, w, h, renderingHints);
}

private static BufferedImage scale(
    BufferedImage image, int w, int h,
    RenderingHints renderingHints)
{
    BufferedImage scaledImage = new BufferedImage(w, h, image.getType());
    double scaleX = (double) w / image.getWidth();
    double scaleY = (double) h / image.getHeight();
    AffineTransform affineTransform = 
        AffineTransform.getScaleInstance(scaleX, scaleY);
    AffineTransformOp affineTransformOp = new AffineTransformOp(
        affineTransform, renderingHints);
    return affineTransformOp.filter(
        image, scaledImage);
}

Also ich habe beide eurer Methoden mal eingebaut in jeweils ne eigene Datei und ausprobiert. Ein Problem haben beide irgendwie:

Wenn das Fenster maximiert ist, wird das Bild bisher immer ordentlich geresized. Aber wenn ich das Fenster mit Doppelklick auf die Titelleiste verkleiner, wird es zwar auch verkleinert, wandert aber nach unten links in die Ecke… Mach ich das Fenster wieder groß und wieder klein ist es erst korrekt. Nervt etwas

Dazu ein Problem bei Marcos:
Wenn ich das Fenster per Hand verkleiner (An den Seiten einfach rumschieben) Geht teilweise ein Teil vom Bild ins “nichts” und vorallem wird das bild SEHR SEHR unscharf, tut schon fast in den Augen weh… bei AmunRa passiert das nicht.

Dann habe ich noch irgendwie das Problem, das das Bild auch wieder vergrößert werden soll. Versuche ich auch das einzubauen spielt das Bild komplett verrückt und wird nach ner Weile auch komplett unscharf, obwohl sich die Größe kaum verändert… Eigentlich so ein einfaches Projekt und so viele Probleme… Hier mal die Klasse, umgeschrieben, damit ich meine eigenen Klassen nicht mehr verwende bzw. hab den benötigten Teil mit eingebaut (der Filechooser war komplett ausgelagert und hab quasi nur 2 Zeilen in meinem Programm gehabt). Man muss nur nen Ordner mit einigen Bildern öffnen und fertig. Evtl seht ihr dann auch was ich meine. (Und ja, die Anordnung des Buttons und des Textfeldes ist nicht optimal das sie hin und herspringt - da der Fokus in der Leiste aber bleibt und Enter drücken bei mir den OK-Button auslöst, reicht es mir):

import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;

/**
 *
 * @author Andy
 */
public class BilderForum {

    public static boolean containsIgnoreCase(String str, String searchStr) {
        if (str == null || searchStr == null) {
            return false;
        }

        final int length = searchStr.length();
        if (length == 0) {
            return true;
        }

        for (int i = str.length() - length; i >= 0; i--) {
            if (str.regionMatches(true, i, searchStr, 0, length)) {
                return true;
            }
        }
        return false;
    }

    private static double computeFactor(int original, int screen, boolean windowResized) {
        if (windowResized && original < screen) {
            System.out.println("Computed: " + (2 - ((double) screen) / ((double) original)));
            return 1 + ((double) screen) / ((double) original);
        } else if (original <= screen) {
            return 1/* + ((double) screen) / ((double) original)*/;
        } else {
            return ((double) screen) / ((double) original);
        }
    }

    public static void resizeImage(ImageIcon icon, JFrame f, JLabel label, boolean windowResized) {
        int originalW, originalH, screenW, screenH;
        double factorW, factorH;

        originalW = icon.getIconWidth();
        screenW = f.getWidth();

        originalH = icon.getIconHeight();
        screenH = f.getHeight();

        factorW = computeFactor(originalW, screenW, windowResized);
        factorH = computeFactor(originalH, screenH, windowResized);

        double factor = factorW < factorH ? factorW : factorH;

        int newH = (int) (originalH * factor);
        int newW = (int) (originalW * factor);
        icon.setImage(icon.getImage().getScaledInstance(newW, newH, Image.SCALE_SMOOTH));
        label.setIcon(icon);
    }

    public static File[] getFiles(JFileChooser fc, FileFilter filter, boolean multiSelection, boolean onlyDirectories) {
        if (!onlyDirectories) {
            fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        } else {
            fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        }
        fc.setMultiSelectionEnabled(multiSelection);
        int returnVal = fc.showDialog(null, "Wählen");
        if (returnVal == JFileChooser.APPROVE_OPTION) {
            List<File> list = new ArrayList<>();
            File[] files;
            if (multiSelection) {
                files = fc.getSelectedFiles();
            } else {
                files = new File[]{fc.getSelectedFile()};
            }
            Arrays.stream(files).forEach(f -> {
                if (f.isDirectory()) {
                    addDirectory(f, list, filter);
                } else if (filter.accept(f)) {
                    list.add(f);
                } else {
                    System.out.println("Nicht akzeptiert: " + f.getName());
                }
            });
            return list.toArray(new File[list.size()]);
        }
        return new File[]{};
    }

    private static void addDirectory(File directory, List<File> list, FileFilter filter) {
        Arrays.stream(directory.listFiles()).filter(filter::accept).forEach(f -> {
            if (!f.isDirectory()) {
                list.add(f);
            } else {
                addDirectory(f, list, filter);
            }
        });
    }

    public static void main(String[] args) {
        JFileChooser fc = new JFileChooser();
        FileFilter filter = new FileNameExtensionFilter("Unterstützte Formate (.gif, .jpg, .jpeg, .png)", "gif", "jpeg", "jpg", "png");
        fc.setCurrentDirectory(new File("E:\\Lol-Bilder"));
        fc.setFileFilter(filter);
        fc.addChoosableFileFilter(filter);
        fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        File[] files = getFiles(fc, filter, true, false);
        JFrame f = new JFrame("Bilder");
        Timer timer;
        f.setLocationRelativeTo(null);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setLayout(new FlowLayout());
        f.setMinimumSize(new Dimension(800, 600));

        JButton button = new JButton("Nächstes");
        JLabel label = new JLabel();
        JTextField tf = new JTextField(8);

        button.addActionListener((ActionEvent evt) -> {
            System.out.println(files.length);
            int r = new Random().nextInt(files.length);
            while (!tf.getText().isEmpty() && !containsIgnoreCase(files[r].getName(), tf.getText())) {
                r = new Random().nextInt(files.length);
            }
            ImageIcon icon = new ImageIcon(files[r].getPath());
            resizeImage(icon, f, label, false);
            f.setTitle(files[r].getName());
        });
        f.add(button);
        f.add(tf);
        f.add(label);

        f.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                resizeImage((ImageIcon) label.getIcon(), f, label, true);
            }
        });

        f.setExtendedState(JFrame.MAXIMIZED_BOTH);
        f.pack();
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                button.doClick();
            }
        };
        timer = new Timer("Bilder");
//        timer.scheduleAtFixedRate(task, 0, 10000);
        f.setVisible(true);
        button.doClick();
    }
}

Allgemein:

icon.setImage(icon.getImage().getScaledInstance(...));

bewirkt ja: Wenn das Bild einmal verkleinert wurde, dann jede Vergrößerung dadurch erreicht, dass das verkleinerte Bild wieder groß skaliert wird. Da kommt natürlich irgendwann Pixelmatsch raus.

Den Code habe ich überflogen, und an einigen Stellen sieht das auf mehreren Ebenen nach Murx aus.

Ich denke, dass ein wichtiger Punkt tatsächlich der ist, dass das Bild nicht in einem ImageIcon gespeichert werden sollte. Das ImageIcon ist praktisch, aber für diesen Zweck eher ungeeignet. Tatsächlich bräuchtest du das Bild vermutlich gar nicht zu skalieren. Es würde reichen, es in einer eigenen component einfach nur skaliert zu zeichnen.

Ansonsten würde ich dir dringend empfehlen, den Zustand, den es dort gibt, besser zusammenzufassen. Ich denke, es könnte hilfreich sein, eine Klasse zu haben, die sich um das Filtern und das Auswählen des nächsten Bildes kümmert. Das mit dem “random” dort ist ja Unfug. Wenn man da einen Text eingibt, der in keinem Bild vorkommt, dann ist das eine Endlisschleife.

Man könnte verschiedene Lösungen in Betracht ziehen, aber ich denke, dass irgendwo sowas wie List<File> allImageFiles und List<File> imageFilesMatchingFilter vorkommen könnte, und natürlich der aktuelle index in dieser Liste. Die Zufälligkeit könnte mit Collections.shuffle erreicht werden, aber … da gibt es viele Freiheitsgrade.

Ein paar der Freiheitsgrade rausgenommen, nur für das (skalierte) Zeichnen ein bißchen rumgespielt:

package bytewelt;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
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.Arrays;
import java.util.Collections;
import java.util.List;

import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;

public class BilderForum2
{
    private static List<File> getFiles(JFileChooser fc, FileFilter filter,
        boolean multiSelection, boolean onlyDirectories)
    {
        if (!onlyDirectories)
        {
            fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        }
        else
        {
            fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        }
        fc.setMultiSelectionEnabled(multiSelection);
        int returnVal = fc.showDialog(null, "Wählen");
        if (returnVal == JFileChooser.APPROVE_OPTION)
        {
            List<File> list = new ArrayList<>();
            File[] files;
            if (multiSelection)
            {
                files = fc.getSelectedFiles();
            }
            else
            {
                files = new File[]
                { fc.getSelectedFile() };
            }
            Arrays.stream(files).forEach(f -> {
                if (f.isDirectory())
                {
                    addDirectory(f, list, filter);
                }
                else if (filter.accept(f))
                {
                    list.add(f);
                }
                else
                {
                    System.out.println("Nicht akzeptiert: " + f.getName());
                }
            });
            return list;
        }
        return new ArrayList<File>();
    }

    private static void addDirectory(File directory, List<File> list,
        FileFilter filter)
    {
        Arrays.stream(directory.listFiles()).filter(filter::accept)
            .forEach(f -> {
                if (!f.isDirectory())
                {
                    list.add(f);
                }
                else
                {
                    addDirectory(f, list, filter);
                }
            });
    }

    public static void main(String[] args)
    {

        SwingUtilities.invokeLater(() -> createAndShowGui());

    }

    private static List<File> selectFiles()
    {
        JFileChooser fc = new JFileChooser();
        FileFilter filter = new FileNameExtensionFilter(
            "Unterstützte Formate (.gif, .jpg, .jpeg, .png)", "gif", "jpeg",
            "jpg", "png");
        fc.setCurrentDirectory(new File("E:\\Lol-Bilder"));
        fc.setFileFilter(filter);
        fc.addChoosableFileFilter(filter);
        fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        List<File> files = getFiles(fc, filter, true, false);
        return files;
    }

    private static class ImageComponent extends JPanel
    {
        private BufferedImage image;

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

        @Override
        protected void paintComponent(Graphics g)
        {
            super.paintComponent(g);
            if (image != null)
            {
                int iw = image.getWidth();
                int ih = image.getHeight();
                double factorW = (double) getWidth() / iw;
                double factorH = (double) getHeight() / ih;
                double factor = Math.min(factorW, factorH);
                int w = (int) (factor * iw);
                int h = (int) (factor * ih);
                int x = (getWidth() - w) / 2;
                int y = (getHeight() - h) / 2;
                g.drawImage(image, x, y, w, h, null);
            }
        }
    }

    private static void createAndShowGui()
    {
        List<File> files = selectFiles();
        Collections.shuffle(files);

        JFrame f = new JFrame("Bilder");
        f.setLocationRelativeTo(null);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new BorderLayout());
        f.setMinimumSize(new Dimension(800, 600));

        JButton nextButton = new JButton("Nächstes");
        JLabel label = new JLabel();
        JTextField filterTextField = new JTextField(8);

        JPanel controlPanel = new JPanel();
        controlPanel.add(nextButton);
        controlPanel.add(filterTextField);
        controlPanel.add(label);
        f.getContentPane().add(controlPanel, BorderLayout.NORTH);

        ImageComponent imageComponent = new ImageComponent();
        f.getContentPane().add(imageComponent, BorderLayout.CENTER);

        nextButton.addActionListener(new ActionListener()
        {
            private int index = 0;

            @Override
            public void actionPerformed(ActionEvent e)
            {
                File file = files.get(index % files.size());
                BufferedImage image = read(file);
                imageComponent.setImage(image);
                index++;
            }
        });

        File file = files.get(0);
        BufferedImage image = read(file);
        imageComponent.setImage(image);

        f.setExtendedState(JFrame.MAXIMIZED_BOTH);
        f.pack();
        f.setVisible(true);
    }

    private static BufferedImage read(File file)
    {
        try
        {
            return ImageIO.read(file);
        }
        catch (IOException e)
        {
            System.out.println("Could not read " + file);
            return null;
        }
    }
}

Also ich habs ausprobiert und das Filter mit dem Textfeld wieder eingefügt, ist im Prinzip schon das was ich mir vorgestellt habe - nur hab ich jetzt das Problem, das animierte GIFs nicht mehr funktionieren. Das Skalieren funktioniert problemlos, nur die animierten Gifs sind jetzt ein Standbild des ersten Frames. Das liegt denke ich an dem BufferedImage…

Edit: Google bestätigt mich - ImageIcons können animierte Gifs anzeigen, BufferedImage ist dafür jedoch nicht gemacht. Müsste also quasi doch wieder zurück auf ImageIcon umsteigen

Edit²: Dazu kommt der GifImageReader bei bestimmten Gifs zu einer AIOOBE

    java.lang.ArrayIndexOutOfBoundsException: 4096
at com.sun.imageio.plugins.gif.GIFImageReader.read(GIFImageReader.java:984)

Ich hab nachgeschaut und wie ich die Implementierung verstehe passiert das, wenn die GIF’s eine zu große Größe erreichen. Das GIF das bei mir den Fehler auslöst ist 6,5MB groß.

Edit³: java - ArrayIndexOutOfBoundsException: 4096 while reading gif file - Stack Overflow
Bekannter allgemeiner Bug bei ImageIO… ImageIO wird mir immer unsympathischer

Edit4: Habs umgeschrieben auf ImageIcon, funktioniert jetzt auch. mit dem Skalieren, nur das Problem ist, das Bild wird nur animiert, wenn ich das Fenster permanent mit der Maus bewege - heißt wenn repaint ausgeführt wird. Ansonsten bleibt das auch Still - wird aber mitskaliert. Das 6,5MB große GIf wird auch problemlos so geladen.

Jetzt die Frage, wie löse ich das. Wenn ich es wieder als Icon hinzufüge, ist es wieder animiert, wird aber nicht skaliert. Mache ich es auf Marcos Weiße, wird es skaliert, aber nur beim wiederholten ausführen von repaint animiert :dizzy_face: Entweder ich kriegs heute noch hin oder ich probiers morgen mal…

So ist Marcos Klasse umgeschrieben, das das Textfeld wieder ne Funktion zum Filtern hat (Brauch ich einfach gif eingeben und sehe nur die gifs bzw den Namen des großen gifs um zu schauen ob die Exception kommt) und BufferedImage komplett rausfliegt und nurnoch ImageIcon zum Zuge kommt.

Edit5: Nochmal umgeschrieben - diesmal wird in ImageLabel noch setIcon ausgeführt.

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.accessibility.AccessibleContext;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;

public class BilderForum2Icon {

    private static List<File> getFiles(JFileChooser fc, FileFilter filter,
            boolean multiSelection, boolean onlyDirectories) {
        if (!onlyDirectories) {
            fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        } else {
            fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        }
        fc.setMultiSelectionEnabled(multiSelection);
        int returnVal = fc.showDialog(null, "Wählen");
        if (returnVal == JFileChooser.APPROVE_OPTION) {
            List<File> list = new ArrayList<>();
            File[] files;
            if (multiSelection) {
                files = fc.getSelectedFiles();
            } else {
                files = new File[]{fc.getSelectedFile()};
            }
            Arrays.stream(files).forEach(f -> {
                if (f.isDirectory()) {
                    addDirectory(f, list, filter);
                } else if (filter.accept(f)) {
                    list.add(f);
                } else {
                    System.out.println("Nicht akzeptiert: " + f.getName());
                }
            });
            return list;
        }
        return new ArrayList<>();
    }

    private static void addDirectory(File directory, List<File> list,
            FileFilter filter) {
        Arrays.stream(directory.listFiles()).filter(filter::accept)
                .forEach(f -> {
                    if (!f.isDirectory()) {
                        list.add(f);
                    } else {
                        addDirectory(f, list, filter);
                    }
                });
    }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(() -> createAndShowGui());

    }

    private static List<File> selectFiles() {
        JFileChooser fc = new JFileChooser();
        FileFilter filter = new FileNameExtensionFilter(
                "Unterstützte Formate (.gif, .jpg, .jpeg, .png)", "gif", "jpeg",
                "jpg", "png");
        fc.setCurrentDirectory(new File("E:\\Lol-Bilder"));
        fc.setFileFilter(filter);
        fc.addChoosableFileFilter(filter);
        fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        List<File> files = getFiles(fc, filter, true, false);
        return files;
    }

    private static class ImageLabel extends JLabel {

        private ImageIcon image;

        public void setImage(ImageIcon image) {
            this.image = image;
            super.setIcon(image);
            repaint();
        }

        public ImageIcon getImage() {
            return image;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (getIcon() != null) {
                int iw = image.getIconWidth();
                int ih = image.getIconHeight();
                double factorW = (double) getWidth() / iw;
                double factorH = (double) getHeight() / ih;
                double factor = Math.min(factorW, factorH);
                int w = (int) (factor * iw);
                int h = (int) (factor * ih);
                int x = (getWidth() - w) / 2;
                int y = (getHeight() - h) / 2;
                g.drawImage(image.getImage(), x, y, w, h, null);
            }
        }
    }

    private static class ImageComponent extends JPanel {

        private ImageIcon image;

        public void setImage(ImageIcon image) {
            this.image = image;
            repaint();
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (image != null) {
                int iw = image.getIconWidth();
                int ih = image.getIconHeight();
                double factorW = (double) getWidth() / iw;
                double factorH = (double) getHeight() / ih;
                double factor = Math.min(factorW, factorH);
                int w = (int) (factor * iw);
                int h = (int) (factor * ih);
                int x = (getWidth() - w) / 2;
                int y = (getHeight() - h) / 2;
                g.drawImage(image.getImage(), x, y, w, h, null);
            }
        }
    }

    private static void createAndShowGui() {
        List<File> files = selectFiles();
        Collections.shuffle(files);

        JFrame f = new JFrame("Bilder");
        f.setLocationRelativeTo(null);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new BorderLayout());
        f.setMinimumSize(new Dimension(800, 600));

        JButton nextButton = new JButton("Nächstes");
        ImageLabel label = new ImageLabel();
        JTextField filterTextField = new JTextField(8);

        JPanel controlPanel = new JPanel();
        controlPanel.add(nextButton);
        controlPanel.add(filterTextField);
        f.getContentPane().add(controlPanel, BorderLayout.NORTH);

        f.getContentPane().add(label, BorderLayout.CENTER);

        nextButton.addActionListener(new ActionListener() {
            private int index;
            private final Random r = new Random();
            private int maxBilder = 0;

            @Override
            public void actionPerformed(ActionEvent e) {
                index = r.nextInt(files.size());
                File file = files.get(index);
                while (!filterTextField.getText().isEmpty() && !containsIgnoreCase(file.getName(), filterTextField.getText()) && maxBilder < 10_000_000) {
                    index = r.nextInt(files.size());
                    System.out.println("Index: " + index);
                    file = files.get(index);
                    maxBilder++;
                }
                ImageIcon image = read(file);
                label.setImage(image);
                f.setTitle(file.getName());
                index = r.nextInt(files.size());
            }
        });

        f.setExtendedState(JFrame.MAXIMIZED_BOTH);
        f.pack();
        f.setVisible(true);

        nextButton.doClick();
    }

    private static ImageIcon read(File file) {
        System.out.println("Read: " + file.getName());
        return new ImageIcon(file.getPath());
    }

    public static boolean containsIgnoreCase(String str, String searchStr) {
        if (str == null || searchStr == null) {
            return false;
        }

        final int length = searchStr.length();
        if (length == 0) {
            return true;
        }

        for (int i = str.length() - length; i >= 0; i--) {
            if (str.regionMatches(true, i, searchStr, 0, length)) {
                return true;
            }
        }
        return false;
    }
}

JETZT wird es korrekt skaliert UND ist animiert - jedoch sieht man links in Originalgröße das Bild eben erneut . halt das mit setIcon hinzugefügte Icon. Ich hab allerdings nicht rausfinden können, was JLabel bei seinem Icon anders macht, das es repaint die ganze Zeit aufruft und die Gifs ordnungsgemäß animiert. paintcomponent muss ja die ganze Zeit ausgeführt werden das unser selbst programmiertes ImageLabel so ja auch auf einmal animiert wird…

Edit6: Lösung gefunden - aber sehr unschön und vorallem sehr unperformant…

Einfach das unten hinzufügen:

TimerTask task = new TimerTask() {
        @Override
        public void run() {
            label.repaint();
        }
    };
    Timer timer = new Timer("Bilder");
    timer.scheduleAtFixedRate(task, 0, 100);

Allerdings… Selbst mit 100ms geht meine CPU Auslastung auf 35% von dem Programm und wirklich flüssig sind viele da auch nicht mehr

Ehrlich gesagt habe ich mit animierten GIFs noch nicht viel gemacht (und der Bug is neu für mich, und interessant, und enttäuschend). Aber ganz grob wird, soweit ich weiß (!) die Animation eines GIFs mit einem ImageObserver (Java Platform SE 8 ) beobachtet - der triggert dann, grob vereinfacht gesagt, das repaint() (für konkretere Informationen wie er das macht, müßte ich aber auch erst in den ImageIcon-Code gucken).

Ansonsten:

private ImageIcon image;

aaaaaaaaaahhhhh :confounded:

Besser:

private ImageIcon imageIcon;

(wenn eine Variable ein Image ist, kann sie image heißen. Wenn sie ein ImageIcon ist, sollte sie imageIcon heißen. Das kann in Spezialfällen schwer durchzuziehen sein, aber du würdest ja auch nicht

private String integer;
private Window string;
private Person animal;

schreiben…)

Ja würde ich allerdings nicht, da war es nur der Fall, das ich einfach BufferedImage in ImageIcon geändert habe bei dir und den Rest so gelassen :sweat_smile:

Aber ich hatte mit Bildern noch nicht wirklich viel zu tun, daher auch noch keine Ahnung von den Observern. Wenn das allerdings der schlüssel ist, schau ich mir das mal an.

Edit: Son Müll. Wenn ich schlicht den JLabel mit this als ImageObserver übergebe ist das Problem, das imageUpdate von Component überschrieben ist und nur funktioniert, wenn ein Icon gesetzt wurde… und zwar das gleiche wie das das angezeigt wurde…

Edit²: Die Lösung ist so einfach… Ich lass mein ImageComponent einfach direkt von JComponent anstatt von JLabel erben und es klappt jetzt. Die Gifs werden korrekt animiert, mit der Größe mitskaliert und es braucht auch nicht 50% CPU-Leistung. Die „Finale“ Klasse schaut so aus:

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;

public class BilderForum2Icon {

    private static List<File> getFiles(JFileChooser fc, FileFilter filter, boolean multiSelection, boolean onlyDirectories) {
        if (!onlyDirectories) {
            fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        } else {
            fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        }
        fc.setMultiSelectionEnabled(multiSelection);
        int returnVal = fc.showDialog(null, "Wählen");
        if (returnVal == JFileChooser.APPROVE_OPTION) {
            List<File> list = new ArrayList<>();
            File[] files;
            if (multiSelection) {
                files = fc.getSelectedFiles();
            } else {
                files = new File[]{fc.getSelectedFile()};
            }
            Arrays.stream(files).forEach(f -> {
                if (f.isDirectory()) {
                    addDirectory(f, list, filter);
                } else if (filter.accept(f)) {
                    list.add(f);
                } else {
                    System.out.println("Nicht akzeptiert: " + f.getName());
                }
            });
            return list;
        }
        System.exit(1);
        return new ArrayList<>();
    }

    private static void addDirectory(File directory, List<File> list, FileFilter filter) {
        Arrays.stream(directory.listFiles()).filter(filter::accept).forEach(f -> {
            if (!f.isDirectory()) {
                list.add(f);
            } else {
                addDirectory(f, list, filter);
            }
        });
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }

    private static List<File> selectFiles() {
        JFileChooser fc = new JFileChooser();
        FileFilter filter = new FileNameExtensionFilter(
                "Unterstützte Formate (.gif, .jpg, .jpeg, .png)",
                "gif", "jpeg", "jpg", "png");
        fc.setCurrentDirectory(new File("E:\\Lol-Bilder"));
        fc.setFileFilter(filter);
        fc.addChoosableFileFilter(filter);
        List<File> files = getFiles(fc, filter, true, false);
        return files;
    }

    private static class ImageComponent extends JComponent {

        private ImageIcon imageIcon;

        public void setImage(ImageIcon icon) {
            this.imageIcon = icon;
            repaint();
        }

        public ImageIcon getImage() {
            return imageIcon;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (imageIcon != null) {
                int iw = imageIcon.getIconWidth();
                int ih = imageIcon.getIconHeight();
                double factorW = (double) getWidth() / iw;
                double factorH = (double) getHeight() / ih;
                double factor = Math.min(factorW, factorH);
                int w = (int) (factor * iw);
                int h = (int) (factor * ih);
                int x = (getWidth() - w) / 2;
                int y = (getHeight() - h) / 2;
                g.drawImage(imageIcon.getImage(), x, y, w, h, this);
            }
        }
    }

    private static void createAndShowGui() {
        List<File> files = selectFiles();
        Collections.shuffle(files);

        JFrame f = new JFrame("Bilder");
        f.setLocationRelativeTo(null);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new BorderLayout());
        f.setMinimumSize(new Dimension(800, 600));

        JButton nextButton = new JButton("Nächstes");
        ImageComponent label = new ImageComponent();
        JTextField filterTextField = new JTextField("gif", 8);

        JPanel controlPanel = new JPanel();
        controlPanel.add(nextButton);
        controlPanel.add(filterTextField);
        f.getContentPane().add(controlPanel, BorderLayout.NORTH);

        f.getContentPane().add(label, BorderLayout.CENTER);

        nextButton.addActionListener(new ActionListener() {
            private int index;
            private final Random r = new Random();
            private int maxBilder = 0;

            @Override
            public void actionPerformed(ActionEvent e) {
                index = r.nextInt(files.size());
                File file = files.get(index);
                while (!filterTextField.getText().isEmpty() && !containsIgnoreCase(file.getName(), filterTextField.getText()) && maxBilder < files.size() * 10) {
                    index = r.nextInt(files.size());
                    file = files.get(index);
                    maxBilder++;
                }
                maxBilder = 0;
                ImageIcon image = read(file);
                label.setImage(image);
                f.setTitle(file.getName());
                index = r.nextInt(files.size());
            }
        });

        f.setExtendedState(JFrame.MAXIMIZED_BOTH);
        f.pack();
        f.setVisible(true);

        nextButton.doClick();
    }

    private static ImageIcon read(File file) {
        System.out.println("Read: " + file.getName());
        return new ImageIcon(file.getPath());
    }

    public static boolean containsIgnoreCase(String str, String searchStr) {
        if (str == null || searchStr == null) {
            return false;
        }

        final int length = searchStr.length();
        if (length == 0) {
            return true;
        }

        for (int i = str.length() - length; i >= 0; i--) {
            if (str.regionMatches(true, i, searchStr, 0, length)) {
                return true;
            }
        }
        return false;
    }
}

Bzw wer Will, hier noch mit dem Timer und nem nativen Global KeyHook, durch den man überall mit Tastendruck den Button drücken kann (Einfach gewünschten KeyCode reinschreiben)

import com.youtube.mrschesam.musikplayer.helper.NativeKeyAdapter;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;
import org.jnativehook.GlobalScreen;
import org.jnativehook.NativeHookException;
import org.jnativehook.SwingDispatchService;
import org.jnativehook.keyboard.NativeKeyEvent;

public class Bilder {

    private static List<File> getFiles(JFileChooser fc, FileFilter filter, boolean multiSelection, boolean onlyDirectories) {
        if (!onlyDirectories) {
            fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        } else {
            fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        }
        fc.setMultiSelectionEnabled(multiSelection);
        int returnVal = fc.showDialog(null, "Wählen");
        if (returnVal == JFileChooser.APPROVE_OPTION) {
            List<File> list = new ArrayList<>();
            File[] files;
            if (multiSelection) {
                files = fc.getSelectedFiles();
            } else {
                files = new File[]{fc.getSelectedFile()};
            }
            Arrays.stream(files).forEach(f -> {
                if (f.isDirectory()) {
                    addDirectory(f, list, filter);
                } else if (filter.accept(f)) {
                    list.add(f);
                } else {
                    System.out.println("Nicht akzeptiert: " + f.getName());
                }
            });
            return list;
        }
        System.exit(1);
        return new ArrayList<>();
    }

    private static void addDirectory(File directory, List<File> list, FileFilter filter) {
        Arrays.stream(directory.listFiles()).filter(filter::accept).forEach(f -> {
            if (!f.isDirectory()) {
                list.add(f);
            } else {
                addDirectory(f, list, filter);
            }
        });
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }

    private static List<File> selectFiles() {
        JFileChooser fc = new JFileChooser();
        FileFilter filter = new FileNameExtensionFilter(
                "Unterstützte Formate (.gif, .jpg, .jpeg, .png)",
                "gif", "jpeg", "jpg", "png");
        fc.setCurrentDirectory(new File("E:\\Lol-Bilder"));
        fc.setFileFilter(filter);
        fc.addChoosableFileFilter(filter);
        List<File> files = getFiles(fc, filter, true, false);
        return files;
    }

    private static class ImageComponent extends JComponent {

        private ImageIcon imageIcon;

        public void setImage(ImageIcon icon) {
            this.imageIcon = icon;
            repaint();
        }

        public ImageIcon getImage() {
            return imageIcon;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (imageIcon != null) {
                int iw = imageIcon.getIconWidth();
                int ih = imageIcon.getIconHeight();
                double factorW = (double) getWidth() / iw;
                double factorH = (double) getHeight() / ih;
                double factor = Math.min(factorW, factorH);
                int w = (int) (factor * iw);
                int h = (int) (factor * ih);
                int x = (getWidth() - w) / 2;
                int y = (getHeight() - h) / 2;
                g.drawImage(imageIcon.getImage(), x, y, w, h, this);
            }
        }
    }

    private static void createAndShowGui() {
        List<File> files = selectFiles();
        Collections.shuffle(files);

        JFrame f = new JFrame("Bilder");
        f.setLocationRelativeTo(null);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new BorderLayout());
        f.setMinimumSize(new Dimension(800, 600));

        JButton nextButton = new JButton("Nächstes");
        ImageComponent label = new ImageComponent();
        JTextField filterTextField = new JTextField("gif", 8);

        JPanel controlPanel = new JPanel();
        controlPanel.add(nextButton);
        controlPanel.add(filterTextField);
        f.getContentPane().add(controlPanel, BorderLayout.NORTH);

        f.getContentPane().add(label, BorderLayout.CENTER);

        nextButton.addActionListener(new ActionListener() {
            private int index;
            private final Random r = new Random();
            private int maxBilder = 0;

            @Override
            public void actionPerformed(ActionEvent e) {
                index = r.nextInt(files.size());
                File file = files.get(index);
                while (!filterTextField.getText().isEmpty() && !containsIgnoreCase(file.getName(), filterTextField.getText()) && maxBilder < files.size() * 10) {
                    index = r.nextInt(files.size());
                    file = files.get(index);
                    maxBilder++;
                }
                maxBilder = 0;
                ImageIcon image = read(file);
                label.setImage(image);
                f.setTitle(file.getName());
                index = r.nextInt(files.size());
            }
        });

        GlobalScreen.setEventDispatcher(new SwingDispatchService());
        f.addWindowListener(new WindowAdapter() {
            @Override
            @SuppressWarnings("CallToPrintStackTrace")
            public void windowOpened(WindowEvent e) {
                // Initialize native hook.
                try {
                    GlobalScreen.registerNativeHook();
                } catch (NativeHookException ex) {
                    System.err.println("There was a problem registering the native hook.");
                    System.err.println(ex.getMessage());
                    ex.printStackTrace();

                    System.exit(1);
                }

                GlobalScreen.addNativeKeyListener(new NativeKeyAdapter() {
                    @Override
                    public void nativeKeyReleased(NativeKeyEvent e) {
                        switch (e.getKeyCode()) {
                            case NativeKeyEvent.VC_F12:
                                nextButton.doClick();
                                break;
                            default:
                                break;
                        }
                    }
                });
            }

            @Override
            @SuppressWarnings("CallToPrintStackTrace")
            public void windowClosed(WindowEvent e) {
                try {
                    //Clean up the native hook.
                    GlobalScreen.unregisterNativeHook();
                } catch (NativeHookException ex) {
                    ex.printStackTrace();
                }
                System.runFinalization();
                System.exit(0);
            }
        });
        Logger.getLogger("org.jnativehook").setLevel(Level.WARNING);

        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                nextButton.doClick();
            }
        };
        Timer timer = new Timer("Bilder");
//        timer.scheduleAtFixedRate(task, 0, 5000);

        f.setExtendedState(JFrame.MAXIMIZED_BOTH);
        f.pack();
        f.setVisible(true);

        nextButton.doClick();
    }

    private static ImageIcon read(File file) {
        System.out.println("Read: " + file.getName());
        return new ImageIcon(file.getPath());
    }

    public static boolean containsIgnoreCase(String str, String searchStr) {
        if (str == null || searchStr == null) {
            return false;
        }

        final int length = searchStr.length();
        if (length == 0) {
            return true;
        }

        for (int i = str.length() - length; i >= 0; i--) {
            if (str.regionMatches(true, i, searchStr, 0, length)) {
                return true;
            }
        }
        return false;
    }
}

Die werde ich auch weiterhin benutzen und evtl erweitern wenn ich Lust zu habe. Die Library für den NativeHook ist JNativeHook

Okay Update… Sobald das Programm länger wie ne Minute läuft steigt der CPU-Auslastung wieder auf 65% an… Warum?

Edit: Problemursache erkannt: Das Programm beginnt mit 1% Auslastung. für jedes weitere geladene animierte GIF Steigt die Auslastung an, nach 13 Bildern ist er bereits bei 30%, werden dann normale Bilder ausgewählt, bleibt die Auslastung auf dem Prozentwert, bis wieder ein GIF geladen wird, dann steigt sie wieder. Also bei GIFs steigt sie und bei normalen bleibt sie auf den vorherigen wert.

Das lässt in mir die Vermutung aufsteigen, das die GIfs die ganze Zeit im Hintergrund geladen bleiben und Ressourcen beanspruchen?

Die beschriebenen Symptome legen die Vermutung nahe, die du selbst schon geäußert hast. Direkt mit dem Finger draufzeigen, woran das liegt, kann ich gerade nicht. GANZ wild geraten: Schreib’ mal in die setImage-Methode

public void setImage(ImageIcon icon) {
    if (this.imageIcon != null) this.imageIcon.getImage().flush(); // Das hier
    ...
}

rein, aber das ist ganz wild spekuliert - ggf. probier’ ich das ganze morgen mal aus, man sollte ja rausfinden können, wo da die Zeit verbrannt wird.

Also ich hab es eingefügt und mal schnell mehrere gifs durchgeklickt - Beim schnellen durchklicken steigt sie kurz auf 10-20% an, sinkt dann aber ganz schnell wieder auf 1%. Das sollte es schon gelöst haben. Zur Not, sollte ich morgen es trotzdem wieder feststellen, sag ich wieder Bescheid bzw probier direkt wieder rum.