Viele ActionListener (MouseListener) - Performance

Hag2bard

Bekanntes Mitglied
Guten Abend,
Ich möchte einen Map Generator erstellen, der in meinem Fall etwa 900 JLabels hat, mit jeweils einem eigenen ActionListener. Ist dieses Vorhaben realistisch oder ist das zuviel?
 
Natuerlich ist es realstisch. Aber was die Performance angeht, musst du das probieren. Wenn alles statisch ist, dann koennte das schon funktioniere, wenn du 900 JLabels in Masse aendern willst, vielleicht nicht so. Musst du probieren was passiert.

Vielleicht ist eine Alternative angebrachter? Zum Beispiel eine Custom-Component auf welcher zu selbst zeichnest und den Klick nur fuer diese Komponente faengst (und dann selbst ausrechnest) was geklickt wurde.
 
MouseListener und dann kannst du die Mausposition abfragen.

Aber ich frage mich gerade warum - 900 Labels hört sich nicht nach einer sinnvollen Anwendung an. Wenn es um Spiele oder ähnliches geht - dann lieber direkt malen und nicht versuchen über Labels das zu platzieren, das kann nur schief gehen.
 
AdvanceMap.png
Ich möchte dieses Programm nachbauen.
Rechts ein Tileset und links soll die Map gebaut werden. Ich hatte auf jeden Fall vor, ein Bufferedimage für beides zu nutzen aber die Mausevents wollte ich mit JLabels machen.
Ich schätze mit dem Holen der Mauskoordinaten bin ich gut dabei
 
Naja, eine solche Map dient normalerweise gerade dazu, nicht das Bild zu speichern. Dann könntest Du auch einfach ein Grafikprogramm verwenden.
 
Ich denke heute stehe ich auf dem Schlauch da ich nicht weiß wie du das meinst. Ich möchte in Java einen MapEditor selber bauen und das dazugehörige Spiel(Pokemon). Beides gibt es schon aber ich möchte es dennoch selber programmieren.
 
Ich möchte in Java einen MapEditor selber bauen und das dazugehörige Spiel(Pokemon).
Was Du am Bildschirm siehst ist nur eine Darstellung dessen, was das Modell liefert.

Eine solche Map hat ihr eigenes Koordinatensystem, in dem Du mit Tiles statt Pixeln (als Größeneinheiten) arbeitest und statt Farbinformationen eben speicherst, welches Tile verwendet wird.

Ich frag mal andersrum: soll am Ende nur das Bild gespeichert werden?
 
Ich hatte auf jeden Fall vor, ein Bufferedimage für beides zu nutzen aber die Mausevents wollte ich mit JLabels machen.

Mach' dir dann am besten einen "MouseListener" auf die Komponente in der du das "BufferedImage" anzeigst, und berechne dann das Tile per Hand basierend auf dem "MouseEvent" welches du gerade erhalten hast, das wird auf jeden Fall ohne Probleme funktionieren (eventuell muss die Komponente fokussierbar sein, das weisz ich aber nicht mehr, musst du probieren).
 
Das mit den 900 Labels hätt ich gerade noch verstanden, aber warum jeweils ein eigener Mauslistener ... einer für alle würd's ziemlich sicher auch tun.

Nimm dir ein Beispiel an Tabellen oder Listen.
Die haben einen Renderer, der aus EINEM Label besteht. Je nachdem, um welche Zeile oder Spalte es sich handelt, wird dieses Label formatiert, mit Text befüllt und dann gerendert. Nach dem Rendern merkt sich die UI-Engine das gerenderte Bild, das Label selbst ist ab jetzt unwichtig.
Für die nächste Zelle wird es wiederverwendet. Dies passiert so lange, bis die Tabelle gezeichnet ist.
Was Maus-Steuerung angeht, hat auch nicht jede Zelle einen eigenen Listener.
Es gibt EINEN Listener für die gesamte Fläche der Tabelle, anhand der Position wird ermittelt, welche Zelle sich unter der Maus befindet, dann wird die entsprechende Aktion ausgelöst.

Es gibt hier eine ganz klare Trennung zwischen Zellinhalt (Daten) und UI (Rendering, Maus-Listener).
 
Also der Plan bis jetzt ist folgender:

Ein JScrollPane inkl einem JLabel welches ein ImageIcon beinhaltet und einen Mauslistener. Durch ein simples Teilen durch 16 und dank int bekomme ich eine Ganzzahl welches mir sagt welcher Block in x und y Richtung geklickt wurde.
Dann werde ich auf die linke Seite meines JFrames noch ein JLabel einfügen, welches ebenso ein ImageIcon beinhaltet und einen MouseListener.
Die JLabels werden in Wirklichkeit Objekte eigener Klassen sein, die von JLabel erben und am besten wiederverwendbar sind. Das mein Code Spaghetti-Code ist liegt daran dass ich erstmal nur experimentiert habe, wie das ganze am Ende funktioniert.
 
Du beschreibst nur das fast uninteressante UI. Der wichtigste Punkt, das Modell dahinter, fehlt mir nach wie vor.

Ich würde mir Gedanken in der Art machen: was ist ein Tile, was ein Tileset?

Ein Tile ist einfach ein Bild, das als Baustein für größere Bilder gedacht ist. Ein Tileset ist nun ein Bild, das ein Tile oder mehrere unterschiedliche Tiles gleicher Größe enthält. So gesehen kann man ein Tile auch als einelementiges Tileset auffassen.

Da ein Tileset auf einem Bild basiert, sind die Tiles rechteckig angeordnet, d. h. es gibt Höhe und Breite in der Einheit "Tiles". Also kann man sich erstmal ein Modell wie
Java:
interface Tileset {
    Size tileSize();
    Size size();
    Image image();
}
überlegen: tileSize() liefert die Größe eines Tiles im Tileset, size() die Größe des Tilesets in Tiles und image() das Bild des Tilesets.

Jetzt habe ich mir oben überlegt, dass man ein Tile als einelementiges Tileset auffassen kann. Es gibt also Teilmengen eines Teilsets. Die können aber irgendwo im Tileset liegen, müssen also nicht bei (0,0) beginnen. Um diese zu beschreiben, bräuchte man also noch die Lage. Dann können wir auch beliebige Teilmengen zurückgeben lassen... Kleine Ergänzung:
Java:
    Location location();
    Tileset subset(Location location, Size size);
location() liefert die Position im Tileset und subset() eine Teilmenge des Tilesets.

Schon hat man ein erstes Modell, mit dem arbeiten kann.

Was soll der Käse mit den Teilmengen? Ganz einfach: im Tileset sind oft Tiles enthalten, die zusammengehören. Zum Beispiel ein Haus. Durch die Teilmengen macht es keinen Unterschied, ob man ein einzelnes Tile oder das ganze Haus beschreibt. Und image() liefert immer das dazugehörige Bild. Man kann also im Editor später ganze Blöcke von Tiles auswählen...
 
Also würde ich das Interface dann in meine Panelklasse implementieren?
Ich komme heute nicht mehr dazu aber ich habe mich gestern mal an einen saubereren Aufbau der Panel Klasse gewagt. Vielleicht könntet ihr da mal drüber schauen.
Mit dem Interface bin ich mir noch nicht sicher wann ich das einsetzen soll. Fakt ist, das mein TilePanel aus einem Bild besteht, nur die Geschichte mit den ausgewählten subTilesets kommt da halt noch dazu.

edit:

Hier doch noch die 2 Klassen

[CODE lang="java" title="Klasse TilePanel"]package pokemon;

import javafx.scene.layout.TilePane;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.io.IOException;

public class TilePanel extends JPanel implements MouseListener {

private int selectedX = -1;
private int selectedY = -1;
private int preSelectedX = -1;
private int preSelectedY = -1;
private int tilesize = 16;
private int tileLength;
private int tileHeight;
private String filename = "tileset-advance.png";
private BufferedImage tilesetBufferedImage;

public TilePanel() {
try {
loadBufferedImage();
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (preSelectedX != -1 && preSelectedY != -1) {
g.drawImage(tilesetBufferedImage, preSelectedX, preSelectedY, preSelectedX, preSelectedY, null);
}
if (selectedX != -1 && selectedY != -1) {
g.setColor(Color.RED);
g.drawRect(selectedX * tilesize, selectedY * tilesize, tilesize, tilesize);
}
}

private void loadBufferedImage() throws IOException {
tilesetBufferedImage = ImageIO.read(getClass().getResourceAsStream("filename"));
}


@Override
public void mouseClicked(MouseEvent e) {
if (selectedX != -1 && selectedY != -1) {
preSelectedX = selectedX;
preSelectedY = selectedY;
}
selectedX = e.getPoint().x;
selectedY = e.getPoint().y;
selectedX = selectedX / 16;
selectedY = selectedY / 16;
repaint();
}

@Override
public void mousePressed(MouseEvent e) {

}

@Override
public void mouseReleased(MouseEvent e) {

}

@Override
public void mouseEntered(MouseEvent e) {

}

@Override
public void mouseExited(MouseEvent e) {

}

@Override
public Dimension getPreferredSize() {
return super.getPreferredSize();
}
}[/CODE]

[CODE lang="java" title="Klasse PokeEditor2"]package pokemon;

import javax.swing.*;


public class PokeEditor2 {

private String filename = "tileset-advance.png";
JScrollPane tileJScrollPane;


public PokeEditor2() {


JFrame mapCreator = new JFrame("Pokemap-Creator");
TilePanel tilepanel = new TilePanel();
tilepanel.addMouseListener(tilepanel);
tilepanel.setFocusable(true);

tileJScrollPane = new JScrollPane(tilepanel);
tileJScrollPane.setBounds(1387, 0, 150, 800);
tileJScrollPane.getVerticalScrollBar().setUnitIncrement(20);

mapCreator.add(tileJScrollPane);
mapCreator.pack();
mapCreator.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mapCreator.setVisible(true);
}
}[/CODE]
 
Also würde ich das Interface dann in meine Panelklasse implementieren?
Nein. Das ist völlig separat vom UI. Über das Interface arbeitet das UI mit einem Tileset zusammen.

Nachtrag: das UI würde ein Objekt mit Tileset-Interface bekommen. Das Zeichnen des Panels könnte dann z. B. so aussehen:
Java:
g2.drawImage(tileset.image(), 0, 0, null);
if (selected != null) {
    Rectangle r = getBounds(selected);
    g2.draw(r);
}

Das Tileset-Modell oben muss auch nicht vollständig sein. Das ist ein erster Entwurf!
 
Zuletzt bearbeitet:
Für was dann das Interface in diesem Fall? Die Methode kann ich doch schon direkt in meiner Tileset Klasse anlegen, dann muss ich nicht meine Klasse die das JFrame erstellt so zu klatschen.
 
Für was dann das Interface in diesem Fall?
Das Interface dient mir erstmal zur Modellierung; ich hätte in dieser frühen Phase auch ein UML-Diagramm zeichnen können.

Abgesehen davon hat das Interface den Vorteil, dass es Implementierungen entkoppelt. Auf diese Weise hast Du einen internen Plugin-Mechanismus: die Komponente, die mit einem Tileset arbeitet, weiß nicht, welche Implementierung sie bekommt.

Vielleicht nochmal im Zusammenhang: Du willst erstmal eine UI-Komponente (JComponent oder ähnliches), deren Aufgabe darin besteht, dem Anwender die Auswahl eines Tiles (oder eines Blocks von Tiles) aus einem Tileset zu ermöglichen. Nicht mehr, nicht weniger.

Die Komponente muss z. B. nicht wissen, woher das Bild des Tilesets kommt, ob es im Speicher erzeugt, aus einer lokalen Datei oder über das Internet geladen wurde. Sie muss auch den konkreten Typ des Bilds (z. B. BufferedImage) nicht kennen. Spielt alles keine Rolle.

Man könnte sich z. B. überlegen, ob man Methoden wie tileAtPixel(int x, int y) usw. Das sind alles erste Überlegungen.

dann muss ich nicht meine Klasse die das JFrame erstellt so zu klatschen.
Den Zusammenhang musst Du mir erklären. Das Tileset-Interface und deren Implementierung sind in separaten Dateien. Deine UI-Klassen werden dadurch übersichtlicher.
 
Ok ich glaube ich verstehe. Ich habe die Tileset Klasse und deren Methoden wie tileAtPixel implementiere ich über ein Interface. Ich überlege mir mal, welchen Code ich noch in Methoden auslagern kann, dann werden die in mein Interface integriert.
Bin mal gespannt ob dabei was sinnvolles heraus kommt.
 
So in etwa, ja. Du hast ein Tileset-Interface und deren Methoden implementierst Du in einer Klasse (z. B. class BufferedTileset implements Tileset). Deine UI-Komponente arbeitet gegen das Tileset-Interface.
 
Gut wenn ich Interfaces mal in der Praxis kennen lerne, ich kenne nur ein Beispiel was mir das vermeintliche Verständnis für Interfaces brachte:

Zahlung[] zahlungen = new Zahlungen[2];
zahlungen[0] = new PayPal();
zahlungen[1] = new GiroPay();

zahlung[0].erzeugeZahlung();
 
Das Prinzip ist genau das gleiche, Du musst Dir nur vorstellen, dass in dem Fall jemand das Zahlung-Array erhält, also nicht weiß, welche Impflementierungen drinsteckt. Trotzdem kann er für jedes Element erzeugeZahlung() aufrufen.
 
Ich habe ein Problem, welches ich selber nicht lösen kann.

Ich bin jetzt soweit, dass ich rechts das Tileset angezeigt bekomme, dazu in der Lage bin ein Feld nach Wunsch auszuwählen und in den linken Teil des JFrames zu zeichnen.
Abgesehen von der Performance, fehlenden Extras und noch sehr unsauberem Code, funktioniert das ja schon super.
Aber ich habe folgendes Problem:

1635865969046.png
Wenn ich rechts einen Block auswähle, und links in das Fenster rein klicke, zeichnet er mir den Block + zusätzlich das Tileset..
Ich verstehe absolut nicht warum er das macht.
Ich könnte mit dem ausgewählten grünen Block nun das Tileset, was da links hingezaubert wurde übermalen, sobald ich aber einen
anderen Block auswähle, wird das Tileset erneut links hin gemalt.
1635866182990.png

Hier ist mein Code:

[CODE lang="java" title="Klasse PokeEditor2"]package pokemon;

import javax.swing.*;

public class PokeEditor2 {

private int tilePanelSelectedX = -1;
private int tilePanelSelectedY = -1;
private String filename = "tileset-advance.png";
JScrollPane tileJScrollPane;
TilePanel tilePanel;
MapPanel mapPanel;

public PokeEditor2() {

JFrame mapCreator = new JFrame("Pokemap-Creator");
mapCreator.setLayout(null);

this.tilePanel = new TilePanel(this);
this.tilePanel.addMouseListener(tilePanel);
this.tilePanel.setFocusable(true);
this.tilePanelSelectedX = tilePanel.getSelectedX();
this.tilePanelSelectedY = tilePanel.getSelectedY();

this.mapPanel = new MapPanel(this.tilePanelSelectedX, this.tilePanelSelectedY, this);
this.mapPanel.addMouseListener(mapPanel);
this.mapPanel.setFocusable(true);
this.mapPanel.setBounds(0, 0, 1350, 800);

tileJScrollPane = new JScrollPane(tilePanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
tileJScrollPane.setBounds(1387, 0, 150, 800);
tileJScrollPane.getVerticalScrollBar().setUnitIncrement(20);

mapCreator.add(tileJScrollPane);
mapCreator.add(mapPanel);
mapCreator.pack();
mapCreator.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mapCreator.setVisible(true);

mapCreator.setSize(1500, 860);
mapCreator.setLocationRelativeTo(null);
mapCreator.setExtendedState(JFrame.MAXIMIZED_BOTH);

}

private void getAndSet(TilePanel tilePanel, MapPanel mapPanel) {
this.tilePanelSelectedX = tilePanel.getSelectedX();
this.tilePanelSelectedY = tilePanel.getSelectedY();
mapPanel.setTilePanelSelectedX(this.tilePanelSelectedX);
mapPanel.setTilePanelSelectedY(this.tilePanelSelectedY);
}

public void runGetandSet() {
getAndSet(tilePanel, mapPanel);
}


}[/CODE]

[CODE lang="java" title="Klasse TilePanel"]package pokemon;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.io.IOException;

public class TilePanel extends JPanel implements MouseListener {

private final PokeEditor2 pokeEditor2;
private int selectedX = -1;
private int selectedY = -1;
private int preSelectedX = -1;
private int preSelectedY = -1;
private int tilesize = 16;
private String filename = "tileset-advance.png";
private BufferedImage tilesetBufferedImage;

public TilePanel(PokeEditor2 pokeEditor2) {
this.pokeEditor2 = pokeEditor2;
try {
loadBufferedImage();
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
pokeEditor2.runGetandSet();
g.drawImage(tilesetBufferedImage, 0, 0, 128, 15971, null);
if (selectedX != -1 && selectedY != -1) {
g.setColor(Color.RED);
g.drawRect(selectedX * tilesize, selectedY * tilesize, tilesize, tilesize);
}
}

private void loadBufferedImage() throws IOException {
// tilesetBufferedImage = ImageIO.read(getClass().getResourceAsStream("filename"));
tilesetBufferedImage = ImageIO.read(getClass().getResource(filename));
}

@Override
public void mouseClicked(MouseEvent e) {
if (selectedX != -1 && selectedY != -1) {
preSelectedX = selectedX;
preSelectedY = selectedY;
}
selectedX = e.getPoint().x;
selectedY = e.getPoint().y;
selectedX = selectedX / 16;
selectedY = selectedY / 16;
repaint();
}

@Override
public void mousePressed(MouseEvent e) {

}

@Override
public void mouseReleased(MouseEvent e) {

}

@Override
public void mouseEntered(MouseEvent e) {

}

@Override
public void mouseExited(MouseEvent e) {

}

public int getSelectedX() {
return selectedX;
}

public int getSelectedY() {
return selectedY;
}

}[/CODE]

[CODE lang="java" title="Klasse MapPanel"]package pokemon;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.io.IOException;

public class MapPanel extends JPanel implements MouseListener {
private final PokeEditor2 pokeEditor2;
private int selectedX = -1;
private int selectedY = -1;
private int preSelectedX = -1;
private int preSelectedY = -1;
private final int tilesize = 16;
private String filename = "tileset-advance.png";
private BufferedImage tilesetBufferedImage;
private int repainted = 0;
private int tilePanelSelectedX = -1;
private int tilePanelSelectedY = -1;

public MapPanel(int tilePanelSelectedX, int tilePanelSelectedY, PokeEditor2 pokeEditor2) {
this.pokeEditor2 = pokeEditor2;
this.tilePanelSelectedX = tilePanelSelectedX;
this.tilePanelSelectedY = tilePanelSelectedY;
try {
loadBufferedImage();
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
protected void paintComponent(Graphics g) {
pokeEditor2.runGetandSet();
if (this.tilePanelSelectedX != -1 && this.tilePanelSelectedY != -1) {
if (selectedX != -1 && selectedY != -1) {
g.drawImage(tilesetBufferedImage, selectedX * tilesize, selectedY * tilesize, (selectedX + 1) * tilesize, (selectedY + 1) * tilesize, this.tilePanelSelectedX * tilesize, this.tilePanelSelectedY * tilesize, (this.tilePanelSelectedX + 1) * tilesize, (this.tilePanelSelectedY + 1) * tilesize, null); // Das muss geändert werden, soll wo anders gezeichnet werden
}
}
}

@Override
public void mouseClicked(MouseEvent e) {
if (selectedX != -1 && selectedY != -1) {
preSelectedX = selectedX;
preSelectedY = selectedY;
}
selectedX = e.getPoint().x;
selectedY = e.getPoint().y;
selectedX = selectedX / 16;
selectedY = selectedY / 16;
repaint();
}

@Override
public void mousePressed(MouseEvent e) {

}

@Override
public void mouseReleased(MouseEvent e) {

}

@Override
public void mouseEntered(MouseEvent e) {

}

@Override
public void mouseExited(MouseEvent e) {

}

private void loadBufferedImage() throws IOException {
//tilesetBufferedImage = ImageIO.read(getClass().getResourceAsStream(filename));
tilesetBufferedImage = ImageIO.read(getClass().getResource(filename));
}

public void setTilePanelSelectedX(int tilePanelSelectedX) {
this.tilePanelSelectedX = tilePanelSelectedX;
}

public void setTilePanelSelectedY(int tilePanelSelectedY) {
this.tilePanelSelectedY = tilePanelSelectedY;
}

}[/CODE]

Vielleicht habt ihr eine Idee, wo hier der Fehler liegt.

Also fakt ist auf jeden Fall, dass die überschriebene Methode paintComponent in TilePanel das Buffered Image nicht nur in das TilePanel zeichnet, sondern auch in das MapPanel.
Ich muss es irgendwie hinbekommen, dass er nur das TilePanel anmalt und nicht alle Komponenten
 
Zuletzt bearbeitet:
Ich habe rausgefunden, dass wenn ich MapPanel das super.paintComponent(g); in die paintComponent Methode schreibe, er mir das Tileset nicht links rein zeichnet, aber die gemalten Tiles nach jedem neuen Klick auf ein neues anzumalendes Feld verschwinden.
 
Zuletzt bearbeitet:

Zurück
Oben