viele JPanels auf JLayeredPane -> to SVG?

blz

Bekanntes Mitglied
Hi,
ich habe ein Programm zum Zeichnen von Diagrammen geschrieben.
Ein JLayeredPane enthält eine beliebige Anzahl transparenter JPanels, die geometrische Figuren und Textelemente enthalten.
Nun möchte ich die Gesamtgrafik - also den Inhalt des JLayeredPane, den Inhalt aller JPanels - gerne als SVG abspeichern.
Vermutlich ist Batik als mächtigste SVG-Bibliothek dafür die sinnvollste Wahl.
Ich finde aber nicht so recht heraus, ob es eine elegante Möglichkeit gibt, mein Vorhaben umzusetzen:
  • ich möchte die Grafik sowohl im Programm darstellen als auch als SVG abspeichern können
  • Bisher kann ich das nur realisieren, indem ich in der paintComponent-Methode meines JPanels parallel sowohl auf das Graphics-Objekt zeichne als auch auf ein SVGGraphics-Objekt, dass ich von jedem JPanel verwalten lasse. Ich führe also jeden Zeichenbefehl doppelt aus, also zB g.drawLine(...) und g2d.drawLine(...)
  • Dann habe ich zwar lauter SVG-Codes für die einzelnen JPanels, aber ich habe noch nicht das Gesamtdiagramm.
    Muss ich mir das dann wirklich einzeln selbst berechnen, also die Position der JPanels innerhalb des JLayeredPanes abfragen, die enthaltenen Elemente wie Rechtecke oder Linien um diese Werte verschieben und die Positionen in dem SVG-Code aktualisieren und alle Elemente dann abschließend händisch in ein einziges, neues SVG-Dokument kopieren?
    Oder gibt es eine bessere Möglichkeit?
 
Du hast recht, da hatte ich einen Denkfehler, danke für den Hinweis.
Ich kann ja in der paint-Methode einfach zu Beginn auf ein SVGGraphics-Objekt casten, bleibt ja das gleiche Objekt.
(Das muss ich dann im Prinzip zuallererst machen, noch vor dem Aufruf von super.paintComponent(g), oder?)

Bleibt mein anderes Problem, wie ich die Inhalte der einzelnen Panels zu einem einzigen SVG-Dokument zusammenbekomme.
Hast du da auch eine Idee?
 
... und noch eine Frage: Ich habe den Cast auf das SVGGraphicsObjekt nun zu Beginn der Methode ergänzt.
Allerdings bekomme ich nun folgende Exception:
Java:
Exception in thread "AWT-EventQueue-0" java.lang.ClassCastException: class sun.java2d.SunGraphics2D cannot be cast to class org.apache.batik.svggen.SVGGraphics2D (sun.java2d.SunGraphics2D is in module java.desktop of loader 'bootstrap'; org.apache.batik.svggen.SVGGraphics2D is in unnamed module of loader 'app')
Ich verstehe, dass es ein Konfigurationsproblem meines Projekts ist: Der Cast ist nicht möglich, weil die eine Klasse im Modul des Classloaders bootstrap ist, die andere in einem unbenannten Modul eines anderen Classloaders.
Was aber genau heißt das?
 
Vielleicht noch zum Verständnis: SVGGraphics2D ist (indirekt) von Graphics2D abgeleitet und Graphics2D ist wiederum von Graphics abgeleitet. D. h. jedes SVGGraphics2D-Objekt ist zugleich auch ein Graphics2D-Objekt und jedes Graphics2D-Objekt ist auch ein Graphics-Objekt. Daher kannst Du ein SVGGraphics2D-Objekt überall dort als Parameter übergeben, wo ein Graphics2D- oder Graphics-Objekt erwartet wird.
 
Ich verstehe die Einwände nicht. Es ist doch üblich, beim Überschreiben einer paint-Methode, wenn man ein Graphics2D-Objekt verwenden will, etwa zu schreiben:
Java:
public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;      
        g2.draw(new ...);
        ...
}
Dann arbeite ich ja immer noch auf demselben Graphicsobjekt, betrachte es aber als Objekt der spezielleren Subklasse und kann daher die entsprechenden Methoden von G2D verwenden.
Analog hätte ich eben, statt Graphics2D nun SVGGraphics2d verwendet.

Zweitens verstehe ich zwar prinzipiell natürlich, dass ich eine solche Methode mit einem OBjekt einer Unterklasse aufrufen kann, aber speziell bei paint verstehe ich es nicht, denn ist es hier nicht gerade der Clou, dass die paint Methode immer automatisch aufgerufen wird, wenn sich irgendetwas verändert, und man sie - dachte ich - oft gar nicht händisch aufruft.
 
Wie Du richtig schreibst, wird paint() normalerweise vom Framework aufgerufen, nämlich dann, wenn eine Komponente am Bildschirm gezeichnet werden soll. Dabei wird eine Instanz von Graphics2D (bzw. eines Subtyps davon) an paint übergeben, weswegen der Downcast von Graphics nach Graphics2D möglich ist. Aus diesem Grund findet man in paint() normalerweise auch nur Methoden, die in Graphics bzw. Graphics2D existieren.

Solltest Du tatsächlich Methoden von SVGGraphics2D benötigen, die in Graphics2D nicht existieren, dann musst Du casten, Dir gleichzeitig aber bewusst sein, dass die Methode normalerweise mit einer Instanz eines anderen Typs aufgerufen wird. D. h. Du musst prüfen, ob Du gerade eine SVGGraphics2D-Instanz vor Dir hast oder nicht und kannst auf dieser dann spezielle Methoden aufrufen:

Java:
if (g instanceof SVGGraphics2D) {
    SVGGraphics2D svg = (SVGGraphics2D) g;
    svg.dieMethodeDieNurInSVGGraphics2DExistiert();
}
 
Das Problem ist doch, dass Du in der Methode ein Argument vom Typ Graphics bekommst.

Graphics2D erbt von Graphics. Wenn die übergebene Instanz vom Typ Graphics2D ist, dann kannst Du so ein Cast machen.

Du bekommst eine Instanz der Klasse sun.java2d.SunGraphics2D - diese erbt von Graphics2D und damit auch von Graphics. Diese Instanz konnte also als Paramter übergeben werden und daher kannst Du da auch diesen Cast machen.

SVGGraphics2D erbt auch von Grapics2D und damit auch von Graphics. Eine Instanz dieser Klasse kann also auch übergeben werden.

Aber ein SunGraphics2D ist kein SVGGraphics2D - daher ist so ein Cast unmöglich.

Nehmen wir einfach ein anschauliches Beispiel:
  • Graphics --> Nahrung
  • Graphics2D --> Obst
  • SunGraphics2D --> Apfel
  • SVGGraphics2D --> Orange

Du hast also nun etwas, dazu brauchst Du Nahrung. Du bist Dir aber sicher, dass die Nahrung, die Du bekommst, immer Obst sein wird. Also kannst Du hin gehen und sagen: Ich behandel die Nahrung als Obst. (Sollte Dir dann jemand ein Brot geben, das auch Nahrung ist, dann wirst Du aber auf die Nase fallen! - Also ohne Überprüfung ist das schon ein Risiko!)

Und nun ist der Fall, dass Du halt einen Apfel übergeben bekommst und Du sagst nun: Nimm den Apfel als Orange. Das geht aber nicht. Ein Apfel ist keine Orange!

Und auch von dem Sinn her ist es unsinnig. Was ist denn Dein Problem?
Die Controls haben eine Automatik, die den Inhalt malt. Du hast also eine Malautomatik. Dieser Automatik wird in der Regel ein Blatt Papier (Entspricht Darstellung auf Monitor) gegeben. Dann wird das Papier bemalt und das war es. Nun willst Du es aber nicht auf einem Papier haben sondern auf einem Pappkarton. Der kann genau so bemalt werden wie das Papier.
==> Alles, was Du machen musst, ist hier der Malautomatik statt einem Papier den Karton zu geben. Das ist, was der Code, den Du in dem Link findest, auch macht.

Was nicht gehen kann, ist etwas wie "Mache das Papier zu Pappe" oder so.

Das einfach einmal als ein paar Versuche, es bildlich darzustellen.
 
ok: mir war nicht so recht klar, dass "intern" an paint ja ein Graphics2D-Objekt übergeben wird; aber logisch, nur dann ist der Cast ja überhaupt möglich und klar, daher funktioniert der von mir gepostete Code nicht. Die anschaulichen Erklärungen haben das auch noch einmal klarer gemacht.

Aber das heißt ja dann, es bleibt letztlich nur die Möglichkeit, die Methode paintComponent immer aktiv aufzurufen?

Wenn ich bisher in meinem Zeichenfenster (dem JLayeredPane) ein neues JPanel hinzugefügt habe, sah das so aus:
Java:
    public void addNewCard() {
        CardPanel cp = new CardPanel(...);
        this.add(cp);             
        // children des LayeredPanes zu einem repaint veranlassen, aber nicht das Pane selbst
        this.revalidate();     
    }

Heißt das, dass ich in jedem JPanel nun ein SVGGraphics2D Objekt verwalten muss und in der Zeichenfläche nun vor dem Aufruf von revalidate() für alle in der Zeichenfläche enthaltenen JPanels einen Aufruf wie folgt brauche:
Code:
for (panel: jPanelList)
{
    panel.paintComponent(panel.getG2DGraphics());
}
 
Heißt das, dass ich in jedem JPanel nun ein SVGGraphics2D Objekt verwalten muss und in der Zeichenfläche nun vor dem Aufruf von revalidate() für alle in der Zeichenfläche enthaltenen JPanels einen Aufruf wie folgt brauche:
Nein. An dem Fenster / den Komponenten musst Du nichts ändern. Diese Elemente haben bereits eine Logik, welche den Inhalt mit Hilfe eines Graphics Objekts malt. Daran musst Du also nichts ändern.

So wie das Programm dieses paint immer aufruft mit einem Graphics Objekt, welches dazu dient, die Fenster auf dem Monitor darzustellen musst du dies nur nutzen um paint mit einem SVGGraphics2D Objekt aufzurufen.

Schauen wir uns doch einmal den schon einmal gebrachten Link https://xmlgraphics.apache.org/batik/using/svg-generator.html gemeinsam an:

In dem Code der Klasse TestSVGGen sieht man eine paint Methode - Das ist jetzt zwar kein Window aber die Methode entspricht der Window.paint Methode.

In der main Methode findet man dann etwas Code, um zu einem SVGGraphics2D Objekt zu kommen. Im Anschluss wird paint damit aufgerufen (Und da wir noch keine Instanz von der Klasse hatten, wird erst noch eine erzeugt).

Wenn Du nun also ein JFrame (erbt von Window) hast, dann kannst Du genau das Gleiche machen:
  • Erst die SVGGraphics2D Instanz erzeugen
  • Dann die paint Methode aufrufen.

Wenn Du nicht das ganzer Fenster sondern statt dessen nur eine Komponente haben willst, dann geht dies auch mit der Komponente: JComponent.paint.

Sprich: Den Code, den Du da von Hand schreiben willst, gibt es bereits. Wobei Deine Schleife so noch falsch ist. Du malst alle Komponenten übereinander - also sozusagen an Position 0,0 und ohne Größenbeschränkung. Du müsstest aber jede Komponente an seiner Position malen. Das würde z.B. mit Hilfe von Graphics.create gehen. In der Schleife erzeugst Du vor jeder Komponente eine neue Graphics instanz, welches an den x/y Koordinaten anfängt und auch nur die Größe der Komponente hat.
Und dann gibt es ggf. noch paar weitere Dinge, die man bedenken könnte, so dass es zu einem Code wie in
kommt. (Die Children werden in paintChildren gemalt.)
 
Verstehe ich richtig:
Ich verwalte einen einzigen svg2dGrahpics-Kontext im LayeredPane.
Ich erzeuge in der Schleife für jedes Panel jeweils eine Kopie des Graphics-Kontexts, allerdings mit den spezifischen Koordinaten.
Vom Gedankengang scheint mir das ähnlich zu sein zu dem was ich vorhatte: Erzeugen und Verwalten eines svg-Graphics-Kontexts im jeweiligen Panel. (Wie ich das mit der Position dann innerhalb des Panels hinbekomme, hatte ich allerdings noch nicht genau überlegt.)

Das bedeutet doch dann: Bei jeder Änderung im Diagramm werden für alle Elemente neue Graphics-Kontexte erzeugt?
Und: Wenn ich dann lauter eigenständige Graphics-Kontexte habe, bekomme ich sie dann in das Ausgabe Dokument, indem ich sie dann in einer Schleife alle einzeln hineinzeichne?
 
Also ich habe bisher verstanden: In einem Window oder JComponent hast Du das, was du in einer SVG Datei speichern können willst.

In dem Window oder JComponent musst Du nichts machen! Du musst auch keine Child Controls durchgehen. Du machst auch nichts bei jedem Malen.

Nur wenn Du speichern willst, machst Du:
  • Erzeugen des SVGGraphics2D Instanz
  • Aufrufen von paint
  • Speichern des erzeugten Dokuments.

Der Algorithmus, den ich da noch kurz erläutert habe, war nur zur Veranschaulichung, was da intern abläuft. Das ist nichts, das Du noch einmal selbst implementieren musst.
 
Mal ein Beispiel:

Java:
public class SaveFrame {
    
    JFrame frame;
    
    public void run() {
        JLabel label = new JLabel("Text");
        JButton button = new JButton("Speichern");
        button.addActionListener(this::save);
        
        frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.add(label);
        frame.add(button, BorderLayout.SOUTH);
        
        frame.pack();
        frame.setVisible(true);
    }

    private void save(ActionEvent e) {
        Container content = frame.getContentPane();
        BufferedImage image = new BufferedImage(content.getWidth(), content.getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D graphics = image.createGraphics();
        content.paint(graphics);
        graphics.dispose();
        
        try {
            ImageIO.write(image, "png", new File("Test.png"));
        } catch (IOException ex) {
            Logger.getLogger(SaveFrame.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

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

In der save-Methode wird ein Graphics2D-Context für das Bild erzeugt und mit diesem wird einfach paint() des zu zeichnenden Containers aufgerufen. Danach enthält das Bild den gezeichneten Inhalt des Containers, so dass der Spaß als Datei abgespeichert werden kann.
 
ach, ok, jetzt hab ichs!
Ich hatte nicht kapiert, dass ich einfach den Graphics-Kontext des Containers verwenden kann ...
Cool, es klappt.
Vielen Dank!
 
Ich hatte nicht kapiert, dass ich einfach den Graphics-Kontext des Containers verwenden kann ...
Das ist nicht ganz richtig. Der Container verwendet den Graphics-Kontext zum Zeichnen, den er beim Aufruf von paint() übergeben bekommt und genau das ist der Trick: während das Framework dem Container einen Graphics-Kontext für die Bildschirmausgabe unterjubelt, kannst Du paint() genauso gut mit irgendeinem anderen Graphics-Kontext aufrufen. Dem Container ist das Schnuppe, denn er muss nur wissen, dass es sich um ein Graphics-Objekt (bzw. in der Regel um ein Graphics2D-Objekt) handelt, um die entsprechenden Methoden aufrufen zu können.
 

Zurück
Oben