Skalierbare Icons bauen

Ich brauche für ein Programm Icons, die skalierbar sein sollen. Die Icons würde ich mit Java-Code selbst zeichnen. Hat jemand sowas schon mal gemacht?
Da einige Swing-Komponenten offenbar nichts direkt mit von javax.swing.Icon abgeleiteten Exemplaren anfangen können, habe ich zuerst daran gedacht, das Icon in ein BufferedImage zu zeichnen und dieses dann auf Swing-Komponenten abzulegen.
Gibt es vielleicht noch eine “schönere” Variante für eigene skalierbare Icons?

Bin mir nicht sicher, ob ich dein Problem ganz verstanden hab. Aber spontan würde mir dazu SVG einfallen.

Oder falls SVG nicht geht (z.b. weil du die icons nicht in dem Format hinbekommst): mach es so wie Android & Co (getreu dem ico-format): Du hinterlegst das Bild in mehreren Auflösungen und wählst dann halt die, die am besten passt.

Wenn du sie sowieso dynamisch zeichnen lässt, dann sehe ich keinen Grund das nicht parametrisiert zu machen. Im 2D-Bildbereich nennt man dass dann wohl Vektorgrafiken, im 3D Maschinenbau CAD :smiley:

Im einfachsten Fall könnte man einfach die entsprechende drawImage-Methode verwenden.

d.drawImage(img, x, y, w, h, null);

das wird aber teilweise recht matschig.

Danke für die schnellen Anworten. Also, wenn ich das mit SVG mache, muss ich mich erst in ein dazu fähiges Programm einarbeiten. Können Swing-Komponenten überhaupt mit SVG umgehen?

Was ich vergessen habe zu sagen: die Icons sollen ihre Farbe nach anklicken verändern können. In dem Beispiel mit Verwendung von Grafikdateien müsste ich also mehrere Versionen zeichnen und vorhalten. Wenn ich das per Java-Code machen würde, genügt wohl nur der Aufruf einer Methode.

Ne, Swing kann kein SVG, müsstest halt eine entsprechende Bibliothek verwenden die die Vektorgrafik wiederum in ein BufferedImage mit der entsprechenden Größe zeichnet.
Bei Android wird das mit den Icons so gemacht; aus den Vektorgrafiken werden die .ico für verschiedenen Bildschirmauflösungen generiert.

Also, es gibt die Klasse ImageIcon im JDK und die ist abgeleitet von Icon.

Dort werden auch die Methoden von Icon implementiert, die man ja überschreiben könnte.
Deshalb die Frage: Würden deine Icons als ImageIcon (egal wie geladen) auf deinen Swing-Komponenten funktionieren?

Brauchst du Icons oder Buttons?

Würde gehen, die Icons werden werden entweder in einem JLabel oder in einem JPanel abgelegt, weil um das Icon herum noch weitere Informationen dargestellt werden sollen.

Je nachdem wie sie die Farbe ändern sollen, könntest du das auch programmatisch lösen indem du einen Filter mittels AlphaComposite drüberlegst. Hier mal eine Spielerei von mir:

import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

public class ImageFilter {

    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setSize(300, 200);

        Image image = createImage();
        ImageSmartPanel imagePanel = new ImageSmartPanel(image);
        frame.add(imagePanel);

        ACCombo combo = new ACCombo();
        combo.addItemListener(e -> imagePanel.updateImage((AlphaComposite) e.getItem()));
        frame.add(combo, BorderLayout.SOUTH);

        SwingUtilities.invokeLater(() -> {
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }

    private static Image createImage() {
        int imageSize = 64;
        BufferedImage image = new BufferedImage(imageSize, imageSize, BufferedImage.TYPE_INT_ARGB);

        Graphics2D graphics = image.createGraphics();

        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        graphics.setPaint(new GradientPaint(0, 0, Color.BLUE, imageSize, imageSize, Color.CYAN));
        graphics.fillRoundRect(5, 5, imageSize-5, imageSize-5, 20, 20);

        graphics.dispose();

        return image;
    }
}

class ACCombo extends JComboBox<AlphaComposite> {
    ACCombo() {
        Field[] alphaComposites = Arrays.stream(AlphaComposite.class.getFields())
                .filter(field -> Modifier.isStatic(field.getModifiers()))
                .filter(field -> field.getType() == AlphaComposite.class)
                .toArray(Field[]::new);

        AlphaComposite[] acs = new AlphaComposite[alphaComposites.length];
        Map<Integer, String> nameMap = new HashMap<>();

        for (int i = 0; i < alphaComposites.length; i++) {
            Field f = alphaComposites**;

            try {
                acs** = (AlphaComposite) f.get(null);
                nameMap.put(acs**.hashCode(), f.getName());
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        setModel(new DefaultComboBoxModel<>(acs));
        setRenderer(new DefaultListCellRenderer() {
            @Override
            public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,
                    boolean cellHasFocus) {
                Component listCellRendererComponent = super
                        .getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);

                if(value != null) {
                    setText(nameMap.get(value.hashCode()));
                }

                return listCellRendererComponent;
            }
        });
    }
}

class ImageSmartPanel extends JComponent {
    private final Image image;
    private Image imageHover;

    private boolean hover;

    ImageSmartPanel(Image image) {
        this.image = image;

        imageHover = hoverImage(AlphaComposite.DstIn);

        registerListener();

        Dimension prefSize = new Dimension(
            image.getWidth(null),
            image.getHeight(null)
        );
        setPreferredSize(prefSize);
    }

    void updateImage(AlphaComposite composite) {
        imageHover = hoverImage(composite);

        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        g.drawImage(hover ? imageHover : image, 0, 0, null);
    }

    private Image hoverImage(AlphaComposite composite) {
        BufferedImage img = new BufferedImage(
                image.getWidth(null),
                image.getHeight(null),
                BufferedImage.TYPE_INT_ARGB
        );

        Graphics2D g = img.createGraphics();

        g.setComposite(AlphaComposite.Src);
        g.drawImage(image, 0, 0, null);

        g.setComposite(composite);
        g.setColor(new Color(200, 200, 200, 100));
        g.fillRect(0, 0, img.getWidth(), img.getHeight());

        g.dispose();

        return img;
    }

    private void registerListener() {
        addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                boolean repaint = !hover;

                hover = true;

                if(repaint) {
                    repaint();
                }
            }

            @Override
            public void mouseExited(MouseEvent e) {
                boolean repaint = hover;

                hover = false;

                if(repaint) {
                    repaint();
                }
            }
        });
    }
}

image
Und mit Hover:
image

So ganz hab’ ich die Frage wohl auch nicht kapiert.

SVG anzuzeigen ist recht trivial, wenn man Apache Batik verwendet. Da kann man

JSVGCanvas canvas = new JSVGCanvas();
canvas.setURI("example.svg");

machen und hat einen Canvas mit einem SVG drauf, aber es könnten Teufelchen im Detail stecken: Man muss ggf. noch die Skalierung anpassen und die Default-Mausinteraktion für den JSVGCanvas deaktivieren (zoomen, verschieben etc).

Allgemein ist Apache Batik aber schon ein ziemlicher Brocken, wenn es nur im ein paar SVGs geht. (Im Ernst: IIRC hat das einige Dependencies, die mindesten mehrere MB haben, und in der letzten Version ist IIRC eine der Dependencies >20 MB groß geworden… bin nicht mehr ganz sicher, aber … da sollte man mal ein Auge drauf haben).

Das “Einfärben” könnte man vielleicht mit einem AlphaComposite machen, wie Tomate_Salat schon gesagt hat. Ich hatte da mal in https://stackoverflow.com/a/21385150/3182664 was gepostet. Aber das kann natürlich nur einen “Farbstich” über das Bild legen. Wenn nur ein “Element” des Icons seine Farbe ändern soll, geht das erstmal nicht direkt.

Mehr Infos wären natürlich wie immer hilfreich: Um wie viele Icons geht es? Wie kompliziert wäre es, den Inhalt mit graphics.drawWhatever selbst zu malen? Wie viele Elemente in den Icons sollen unterschiedliche Farben haben können? WIe stark soll skaliert werden (Faktor 2 oder Faktor 20)? Soll die komplette Grafik oder nur die Formen skaliert werden? (D.h. soll beim Skalieren um Faktor 4 eine Linie, die vorher 1 Pixel dick war, dann 4 Pixel dick sein? - Siehe screenshot in https://github.com/javagl/Viewer/tree/master/viewer-core )

Ein grober Ansatz, bei sowas Allgemeingültigkeit reinzubringen, kann sein, dass man sein Zeug ins (0,0)-(1,1) Einheitsquadrat malt, aber überall eine affine Transform davorklemmt:

void paintIcon(Graphics2D g, AffineTransform at, Color colorA, Color colorB) {
    // Die Koordinaten beziehen sich alle auf das Einheitsquadrat
    g.setColor(colorA);
    g.fill(at.createTranformedShape(new Rectangle2D.Double(0.25,0.25,0.5,0.5)));
    g.setColor(colorB);
    g.draw(at.createTranformedShape(new Line2D.Double(0.25,0.25,0.75,0.75)));
}

Dann kann man machen, dass das Icon dann eine gewünschte Pixelgröße einnimmt:

// Male das Icon 16x16 pixel groß
AffineTransform at = AffineTransform.getScaleInstance(16,16); 
drawIcon(g, at, Color.RED, Color.GREEN);

Ist aber nur einer von vielen Ansätzen.

Danke für deine Antwort und natürlich auch die Antworten der Andren.

Ich wollte es eigentlich relativ einfach machen, ganz ohne externe Bibliotheken. Die Icons sind zweifarbig. Eine Hintergrundfarbe und die Farbe für das enthaltene Symbol. Das Symbol ist eigentlich nur einfachste Grafik. Linien, Buchstaben oder Pfeile, die allesamt mit Grafik-Methoden gezeichnet werden können.

Insgesamt werden es wahrscheinlich 6 Icons.
Die Größe der Icons soll zwischen 16 und 64 Pixel variieren.

Sind die Grafiken vorgegeben? Vielleicht wäre bei solchen einfachen Symbolen auch eine Icon-Font wie z.B. fontawesome eine Möglichkeit. Sowas mit Swing zu verwenden scheint nicht so kompliziert, als Inspiration könnte das hier dienen.

Nun, dann klingt es eigentlich, als könnte man das recht einfach machen (insbesondere ohne irgendeine Riesen-Library): Das Icon könnte das zu zeichnende Objekt als Shape enthalten, und eine Farbe dazu. In der paint-Methode des Icons wird dann das Shape auf die passende Größe sakliert gezeichnet. Schnell getestet:

package bytewelt;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;

import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;

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

    private static void createAndShowGui()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new BorderLayout());
        
        JPanel panel = new JPanel(new FlowLayout());

        CustomIcon customIcon = new CustomIcon(createArrowShape(), Color.RED);
        panel.add(new JLabel(customIcon));
        
        f.getContentPane().add(panel, BorderLayout.CENTER);
        
        JPanel controlPanel = new JPanel(new FlowLayout());
        JSlider slider = new JSlider(16, 64, 16);
        slider.addChangeListener(e -> 
        {
            int value = slider.getValue();
            customIcon.setIconWidth(value);
            customIcon.setIconHeight(value);
            panel.revalidate();
        });
        controlPanel.add(slider);
        
        f.getContentPane().add(controlPanel, BorderLayout.SOUTH);
        
        f.setSize(500, 300);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
    

    private static Shape createArrowShape()
    {
        Path2D path = new Path2D.Double();
        path.moveTo(0.0, 0.25);
        path.lineTo(0.5, 0.25);
        path.lineTo(0.5, 0.0);
        path.lineTo(1.0, 0.5);
        path.lineTo(0.5, 1.0);
        path.lineTo(0.5, 0.75);
        path.lineTo(0.0, 0.75);
        path.closePath();
        return path;
    }
    
    static class CustomIcon implements Icon
    {
        private Color backgroundColor;
        private int iconWidth;
        private int iconHeight;

        private final Shape shape;
        private final Color color;
        
        CustomIcon(Shape shape, Color color)
        {
            this.shape = shape;
            this.color = color;
            this.backgroundColor = Color.GREEN;
            this.iconWidth = 16;
            this.iconHeight = 16;
        }
        
        void setIconWidth(int iconWidth)
        {
            this.iconWidth = iconWidth;
        }
        
        void setIconHeight(int iconHeight)
        {
            this.iconHeight = iconHeight;
        }
        
        @Override
        public void paintIcon(Component c, Graphics gr, int x, int y)
        {
            Graphics2D g = (Graphics2D)gr;
            g.setColor(backgroundColor);
            g.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING, 
                RenderingHints.VALUE_ANTIALIAS_ON);
            g.fillRect(x, y, getIconWidth(), getIconHeight());
            AffineTransform at = new AffineTransform();
            at.translate(x, y);
            at.scale(getIconWidth(), getIconHeight());
            
            Shape transformedShape = at.createTransformedShape(shape);
            g.setColor(color);
            g.fill(transformedShape);
            g.setColor(Color.BLACK);
            g.draw(transformedShape);
        }

        @Override
        public int getIconWidth()
        {
            return iconWidth;
        }

        @Override
        public int getIconHeight()
        {
            return iconHeight;
        }
        
    }
    
}

Aaaaber: Vermutlich kommt jetzt dazu: „Ja, das Icon in Rot, aber da soll noch was anderes, Schwarzes dazu“ (ich hatte eben schon eine Variante mit List<Shape> und List<Color>, aber solche Erweiterungen muss derjenige sich überlegen, der die Anforderungen genauer kennt).

Das ganze hat natürlich caveats: Je nach Inhalt und Skalierungsfaktor sehen die Icons ggf. nicht so „schön“ aus (d.h. etwas pixelig). „Schöne“ Icons der Größe 16x16 zu erstellen ist eine Kunst für sich…

Die Option, die Icons in 16x16, 32x32 und 64x64 als Images vorzuhalten und immer das passende auszuwählen sollte man IMHO auch im Auge behalten.

(BTW: Wild geratene Spekulation: Ihr habt da eine Swing-Anwendung, und neuerdings hat dort irgendein Anwender einen 4k-Monitor, und der hat sich beschwert, dass die Icons zu klein sind, richtig? Ich war jedenfalls etwas frustriert, als ich in einer Anwendung mit Schriftgröße 9 möglichst viele Infos auf den Bildschirm pressen wollte, um dann auf einem Laptop mit ~12-Zoll 4k (!)-Displaystatt des kompakten Textes nurnoch einen unlesbaren Fliegenschiß zu sehen :confused: )

1 „Gefällt mir“

Hallo Marco, vielen Dank für deine Gedanken und Anregung. Deine Lösung ist gaaaanz nahe dran, an dem, was ich mir im Kopf zurechtgelegt habe. Nur eleganter, wie ich das gemacht hätte…
Mit Path2D habe ich mir auch meine Pfeile gebaut. Bis jetzt habe ich allerdings alles in ein BufferedImage gezeichnet, weil ich wie oben bereits gesagt, die Objekte, die Icon implementieren, nicht an Komponenten übergeben konnte. Grafiken aber schon.
Gibt es einen bedeutenden Vorteil bei der Nutzung des Icon-Interfaces, der sich mir jetzt nicht nicht erschließt?

Nein, der Grund ist, dass es mehrere Exemplare der Icons im Programm geben soll, die sich aber von den Dimensionen her unterscheiden, je nach dem, welche Komponente es einbettet, bzw. Es soll bspw. in einer Tabelle dargestellt werden, außerdem aber in einem Eigenschaften-Dialog.

Nette Idee, darüber habe ich anfangs auch nachgedacht. Aber genau weil die Icons vorgegeben sind, geht das nicht.

Ah OK, da war wohl nicht ganz klar, was damit gemeint war. Es geht also um „irgendwas“, was direkt ein Image bzw. BufferedImage braucht? Ich könnte/konnte mir nicht ganz vorstellen, was das sein sollte - doch vermutlich keine der vorgegebenen Swing Components?! Der Unterschied ist wohl nicht so dramatisch: Was auch immer in der paint-Methode gemalt wird, kann natürlich auch in ein Image gemalt werden. (Das Image wird zwar meistens mit graphics.drawImage gemalt wo stattdessen auch paintMyIcon(graphics) stehen könnte, aber das gibt sich nicht viel…)

braucht; nicht unbedingt. Wichtig ist, dass ein Symbolbild dargestellt wird, von dem der Benutzer ableiten kann, um welche Bedienkomponente es sich handelt. Daher gibt es nicht viele Symbole und diese sind recht einfach und einprägsam gestaltet.

Doch, alles Swing-Components. JTable, JTree, JPanel, JLabel. Aber nicht alle davon können mit javax.swing.Icon umgehen.
Wie gesagt, möglichst einfach.