JavaFX Überlagerte EventHandler

hk

Bekanntes Mitglied
Hallo Forum,
ich habe eine Pane und darauf ein ImageView. Für beide ist je ein EventHandler (MouseEvent.MOUSE_RELEASED) definiert. Nun kommt es häufig vor, dass beim Klick auf den ImageView der EventHandler für die Pane aktiviert wird.

Warum ist das so und wie kann ich das vermeiden?
lg heinz
 
K

kneitzel

Gast
Das ist das normale Handling, denn die Maus wurde released über dem ImageView und über dem Pane. Daher bekommen beide dieses Event.

Du kannst dir aber das MouseEvent ansehen: dort gibt es ein getTarget(). Das ist das oberste Element.

Also ein einfacher Test:
fxml mit Pane und auf der Pane einfach ein ImageView. Ist ansonsten alles leer, also einfach merken, wo man das ImageView hin gepackt hat :)

XML:
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>

<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" onMouseReleased="#mouseReleasedOnPane" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="helloworld.HelloWorldController">
   <children>
      <ImageView fitHeight="331.0" fitWidth="360.0" layoutX="37.0" layoutY="37.0" onMouseReleased="#mouseReleasedOnImageView" pickOnBounds="true" preserveRatio="true" />
   </children>
</Pane>

Der Controller kann relativ einfach sein:
Java:
package helloworld;

import javafx.fxml.FXML;
import javafx.scene.input.MouseEvent;

public class HelloWorldController {

    @FXML public void initialize() {
        System.out.println("Initialize ...");
    }

    public void mouseReleasedOnImageView(MouseEvent mouseEvent) {
        System.out.println("Mouse released on ImageView: " + mouseEvent.getTarget().getClass().getSimpleName());
    }

    public void mouseReleasedOnPane(MouseEvent mouseEvent) {
        System.out.println("Mouse released on Pane:" + mouseEvent.getTarget().getClass().getSimpleName());
    }
}

Und dann siehst Du auch das genaue Verhalten sowie, welches Control im target enthalten ist:
Code:
> Task :run
Initialize ...
Mouse released on ImageView: ImageView
Mouse released on Pane:ImageView
Mouse released on Pane:Pane

Man erkennt zuerst den MouseRelease auf dem ImageView -> beide Handler werden angesprochen.
Dann auf dem Pane und schon ist da auch das Pane hinterlegt.
 

hk

Bekanntes Mitglied
Das ist das normale Handling, denn die Maus wurde released über dem ImageView und über dem Pane. Daher bekommen beide dieses Event.

Du kannst dir aber das MouseEvent ansehen: dort gibt es ein getTarget(). Das ist das oberste Element.

Also ein einfacher Test:
fxml mit Pane und auf der Pane einfach ein ImageView. Ist ansonsten alles leer, also einfach merken, wo man das ImageView hin gepackt hat :)

XML:
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>

<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" onMouseReleased="#mouseReleasedOnPane" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="helloworld.HelloWorldController">
   <children>
      <ImageView fitHeight="331.0" fitWidth="360.0" layoutX="37.0" layoutY="37.0" onMouseReleased="#mouseReleasedOnImageView" pickOnBounds="true" preserveRatio="true" />
   </children>
</Pane>

Der Controller kann relativ einfach sein:
Java:
package helloworld;

import javafx.fxml.FXML;
import javafx.scene.input.MouseEvent;

public class HelloWorldController {

    @FXML public void initialize() {
        System.out.println("Initialize ...");
    }

    public void mouseReleasedOnImageView(MouseEvent mouseEvent) {
        System.out.println("Mouse released on ImageView: " + mouseEvent.getTarget().getClass().getSimpleName());
    }

    public void mouseReleasedOnPane(MouseEvent mouseEvent) {
        System.out.println("Mouse released on Pane:" + mouseEvent.getTarget().getClass().getSimpleName());
    }
}

Und dann siehst Du auch das genaue Verhalten sowie, welches Control im target enthalten ist:
Code:
> Task :run
Initialize ...
Mouse released on ImageView: ImageView
Mouse released on Pane:ImageView
Mouse released on Pane:Pane

Man erkennt zuerst den MouseRelease auf dem ImageView -> beide Handler werden angesprochen.
Dann auf dem Pane und schon ist da auch das Pane hinterlegt.
Ich muss mich erst korrigieren, es sind beide Objekte ImageViews die übereinander liegen daher bekomme ich in jeden Fall nur "ImageView" zurück. Geht die Präzisierung etwas tiefer oder kann ich bei der Erstellung einen eindeutigen, abfragbaren Wert mitgeben. Hier der Code:
Java:
private void setEvent() {
    // Event-Handler zum Testen
    Main.ivGameBoard.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> {
      String help = e.getTarget().getClass().getSimpleName();
      System.out.println("falscher Event!");
      String s = "NO";
      Point p = new Point((int) e.getX(), (int) e.getY());
      for(Field f : fieldList) {
        s = f.getSphere(p);
        if(s != "NO") break;
      }
        Main.anzeige.setText(s);
    });
    
    // Event-Handler für Programmende
    Main.ivClose.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> {
      String help = e.getTarget().getClass().getSimpleName();
      System.exit(0);
    });
  } //end setEvent ------------------------------
 
K

kneitzel

Gast
Also Du hast da natürlich die Controls selbst und damit vollen Zugriff auf alle Elemente.

Eine Möglichkeit wäre, fx:id zu vergeben und diese zu prüfen:
Java:
        Node view = (Node) mouseEvent.getTarget();
        System.out.println("Mouse released on ImageView: " + view.getId());
(Evtl. vorsichtshalber mit instanceof prüfen, dass es auch wirklich ein Node ist. Sollte aber immer ein Node sein aber man weiss ja nie, welcher Spaßvogel die Methode vielleicht noch anders aufruft ...)

Aber Du kannst natürlich mittels @FXML dir die Instancen der Controls geben lassen - und dann einfach prüfen, ob getTarget() die entsprechende Instanz hat ...

Aber mir fällt gerade noch ein: Du willst ja nur dann etwas machen, wenn der Click direkt auf dem Control war. Das MouseEvent hat auch die Source des Events, d.h. bei welchem Control ist es ausgelöst worden.
Daher reicht ggf. als erstes ein einfaches:
Java:
if (mouseEvent.getTarget() != mouseEvent.getSource()) return;
Damit sollte jede Abarbeitung bei Events, die sozusagen nur "Beifang" waren, direkt abgebrochen werden.
 

hk

Bekanntes Mitglied
Also Du hast da natürlich die Controls selbst und damit vollen Zugriff auf alle Elemente.

Eine Möglichkeit wäre, fx:id zu vergeben und diese zu prüfen:
Java:
        Node view = (Node) mouseEvent.getTarget();
        System.out.println("Mouse released on ImageView: " + view.getId());
(Evtl. vorsichtshalber mit instanceof prüfen, dass es auch wirklich ein Node ist. Sollte aber immer ein Node sein aber man weiss ja nie, welcher Spaßvogel die Methode vielleicht noch anders aufruft ...)

Aber Du kannst natürlich mittels @FXML dir die Instancen der Controls geben lassen - und dann einfach prüfen, ob getTarget() die entsprechende Instanz hat ...

Aber mir fällt gerade noch ein: Du willst ja nur dann etwas machen, wenn der Click direkt auf dem Control war. Das MouseEvent hat auch die Source des Events, d.h. bei welchem Control ist es ausgelöst worden.
Daher reicht ggf. als erstes ein einfaches:
Java:
if (mouseEvent.getTarget() != mouseEvent.getSource()) return;
Damit sollte jede Abarbeitung bei Events, die sozusagen nur "Beifang" waren, direkt abgebrochen werden.
Das funktioniert beides nicht. getTarget und getSource sind bei beiden beiden Handler immer gleich, und mit view.getId() bekomme ich die Id die dem ImageView des Handlers entspricht und nicht dem geklickten ImageView.
lg heinz
 
K

kneitzel

Gast
Also gib exakte Details, was Du machst und was nicht geht. Das was Du beschrieben hast, verhält sich so wie ich beschrieben habe. Bis auf die Tatsache, dass ich die zwei ImageView Elemente in einer Pane bisher nicht getestet habe. Da gibt es die beschriebene Thematik nicht!

Du hast Controls in einer Baumstruktur und ein Event geht die Baumstruktur hoch, d.h. ein Pane, das eine ImageView enthält, erhält das MouseEvent ebenfalls.

Aber so wie es in der Dokumentation beschrieben wurde:
getSource: The object on which the Event initially occurred.
getTarget: Returns the event target of this event
Das ist also für alle Elemente oberhalb des angeklickten Elements unterschiedlich, denn Source ist das Objekt, das initial das Event bekommen hat.

Zwei ImageView in einem Pane können aber nicht das Mouse-Event bekommen. Das ImageView, das als zweites hinzu gefügt wurde, ist zu oberst und bekommt das Event. Das andere ImageView dahinter bekommt das Event nicht. (Ist ja auch klar, wenn das Event die Baumstruktur hoch gegeben wird).

Und das lässt sich problemlos testen und daran dann zeigen:
[CODE lang="xml" title="fxml"]<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>

<Pane fx:id="Pane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" onMouseReleased="#mouseListener" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="helloworld.HelloWorldController">
<children>
<ImageView fx:id="View1" fitHeight="343.0" fitWidth="486.0" layoutX="6.0" layoutY="7.0" onMouseReleased="#mouseListener" pickOnBounds="true" preserveRatio="true" />
<ImageView fx:id="View2" fitHeight="361.0" fitWidth="522.0" layoutX="78.0" layoutY="39.0" onMouseReleased="#mouseListener" pickOnBounds="true" preserveRatio="true" />
</children>
</Pane>
[/CODE]

[CODE lang="java" title="Controller"]public class HelloWorldController {

@FXML public void initialize() {
System.out.println("Initialize ...");
}

public void mouseListener(final MouseEvent mouseEvent) {
Node source = (Node) mouseEvent.getSource();
Node target = (Node) mouseEvent.getTarget();
if (mouseEvent.getTarget() != mouseEvent.getSource()) {
System.out.println("Source != Target: " + source.getId() + " / " + target.getId());
} else {
System.out.println("Source == Target: " + source.getId());
}
}
}[/CODE]

Und dann kann man rum klicken ... in der Mitte überlappen sich die ImageViews und dennoch ist View2 immer die einzige ImageView auf der das Event ausgelöst wird. Und natürlich das Pane bekommt das Event.

Oben links ist die erste ImageView anklickbar. Oben rechts oder unten links nur das Pane.

Und man hat dann an Ausgabe:

Click auf Pane:
Source == Target: Pane

Click auf View1:
Source == Target: View1
Source != Target: Pane / View1

Click auf View2:
Source == Target: View2
Source != Target: Pane / View2

Click auf View2 mit überlapptem View1:
Source == Target: View2
Source != Target: Pane / View2

Das ist also trivial nachzuvollziehen. Das einfache fxml laden ist ja kein Thema - aber dennoch einfach mal die HelloWorld Klasse:
Java:
package helloworld;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloWorld extends Application {

    public static void main(final String[] args) {
        launch(args);
    }

    @Override
    public void start(final Stage primaryStage) throws IOException {
        Parent root = FXMLLoader.load(getClass().getResource("HelloWorld.fxml"));
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
 

hk

Bekanntes Mitglied
Also gib exakte Details, was Du machst und was nicht geht. Das was Du beschrieben hast, verhält sich so wie ich beschrieben habe. Bis auf die Tatsache, dass ich die zwei ImageView Elemente in einer Pane bisher nicht getestet habe. Da gibt es die beschriebene Thematik nicht!

Du hast Controls in einer Baumstruktur und ein Event geht die Baumstruktur hoch, d.h. ein Pane, das eine ImageView enthält, erhält das MouseEvent ebenfalls.

Aber so wie es in der Dokumentation beschrieben wurde:
getSource: The object on which the Event initially occurred.
getTarget: Returns the event target of this event
Das ist also für alle Elemente oberhalb des angeklickten Elements unterschiedlich, denn Source ist das Objekt, das initial das Event bekommen hat.

Zwei ImageView in einem Pane können aber nicht das Mouse-Event bekommen. Das ImageView, das als zweites hinzu gefügt wurde, ist zu oberst und bekommt das Event. Das andere ImageView dahinter bekommt das Event nicht. (Ist ja auch klar, wenn das Event die Baumstruktur hoch gegeben wird).

Und das lässt sich problemlos testen und daran dann zeigen:
[CODE lang="xml" title="fxml"]<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>

<Pane fx:id="Pane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" onMouseReleased="#mouseListener" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="helloworld.HelloWorldController">
<children>
<ImageView fx:id="View1" fitHeight="343.0" fitWidth="486.0" layoutX="6.0" layoutY="7.0" onMouseReleased="#mouseListener" pickOnBounds="true" preserveRatio="true" />
<ImageView fx:id="View2" fitHeight="361.0" fitWidth="522.0" layoutX="78.0" layoutY="39.0" onMouseReleased="#mouseListener" pickOnBounds="true" preserveRatio="true" />
</children>
</Pane>
[/CODE]

[CODE lang="java" title="Controller"]public class HelloWorldController {

@FXML public void initialize() {
System.out.println("Initialize ...");
}

public void mouseListener(final MouseEvent mouseEvent) {
Node source = (Node) mouseEvent.getSource();
Node target = (Node) mouseEvent.getTarget();
if (mouseEvent.getTarget() != mouseEvent.getSource()) {
System.out.println("Source != Target: " + source.getId() + " / " + target.getId());
} else {
System.out.println("Source == Target: " + source.getId());
}
}
}[/CODE]

Und dann kann man rum klicken ... in der Mitte überlappen sich die ImageViews und dennoch ist View2 immer die einzige ImageView auf der das Event ausgelöst wird. Und natürlich das Pane bekommt das Event.

Oben links ist die erste ImageView anklickbar. Oben rechts oder unten links nur das Pane.

Und man hat dann an Ausgabe:

Click auf Pane:
Source == Target: Pane

Click auf View1:
Source == Target: View1
Source != Target: Pane / View1

Click auf View2:
Source == Target: View2
Source != Target: Pane / View2

Click auf View2 mit überlapptem View1:
Source == Target: View2
Source != Target: Pane / View2

Das ist also trivial nachzuvollziehen. Das einfache fxml laden ist ja kein Thema - aber dennoch einfach mal die HelloWorld Klasse:
Java:
package helloworld;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloWorld extends Application {

    public static void main(final String[] args) {
        launch(args);
    }

    @Override
    public void start(final Stage primaryStage) throws IOException {
        Parent root = FXMLLoader.load(getClass().getResource("HelloWorld.fxml"));
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
Den einzigen Unterschied welchen ich sehe ist dass mein zweiter ImageView nicht überlappt sondern komplett im ersten ImageView positioniert ist.
Was mir weiters unklar ist warum es manchmal funktioniert und manchmal nicht.
 

hk

Bekanntes Mitglied
Den einzigen Unterschied welchen ich sehe ist dass mein zweiter ImageView nicht überlappt sondern komplett im ersten ImageView positioniert ist.
Was mir weiters unklar ist warum es manchmal funktioniert und manchmal nicht.
Ich habe nun bei der Definition des zweiten (obenliegenden) ImageView die Funktion .setPickOnBounds(true) gesetzt. Und nun funktioniert es. Ist das erklärbar?
lg heinz
 
K

kneitzel

Gast
Also die Problematik ist mir immer noch nicht klar und mit den neuen Informationen kann ich jetzt nur raten.

Einmal die Fakten durchgehen:
a) Du hast zwei ImageViews, die sich überlappen.
b) Wenn Du klickst, bekommt immer nur eine ImageView ein Event. (Nicht wie ursprünglich geschrieben beide gleichzeitig. Das ließ sich in meinen Tests nicht darstellen! Das geht nur bei Hierarchien wie ImageView in Pane und beide bekommen den Click...
c) Ein anderes ImageView bekommt das Event als du erwartest.

Kommen wir zu dem Standard Verhalten (setPickOnBounds(false)):
Hier bekommt das oberste ImageView immer den click. Unabhängig, ob da ein Bild dargestellt wird oder nicht (z.B. Transparente Bereiche im Bild oder Bild kleiner als Control)

Das kann verwirrend sein und evtl. nicht gewünscht. Bei setPickOnBounds(true) ändert sich das Verhalten. Da werden nicht mehr einfach die Grenzen des Controls genommen.

If pickOnBounds is true, then picking is computed by intersecting with the bounds of this node, else picking is computed by intersecting with the geometric shape of this node.
Das ist jetzt aus Sicht der Node: bei false die geometrische Form des Nodes. Aber bei true werden die "echten" Bounds genommen. Bezüglich des genauen Verhaltens habe ich aber keine gute Dokumentation gefunden, daher bekommt ein SO Artikel ein gutes Rating:

Auf SO hat jemand dazu sogar eine kleine App gebaut zum Spielen: https://stackoverflow.com/questions/15525001/javafx-how-to-make-a-node-partially-mouse-transparent - Da kann man schön spielen und sehen, wie ein Click auf den transparenten Bereich mal beim ersten Element landet und mal beim hinteren.
 

hk

Bekanntes Mitglied
Also die Problematik ist mir immer noch nicht klar und mit den neuen Informationen kann ich jetzt nur raten.

Einmal die Fakten durchgehen:
a) Du hast zwei ImageViews, die sich überlappen.
b) Wenn Du klickst, bekommt immer nur eine ImageView ein Event. (Nicht wie ursprünglich geschrieben beide gleichzeitig. Das ließ sich in meinen Tests nicht darstellen! Das geht nur bei Hierarchien wie ImageView in Pane und beide bekommen den Click...
c) Ein anderes ImageView bekommt das Event als du erwartest.

Kommen wir zu dem Standard Verhalten (setPickOnBounds(false)):
Hier bekommt das oberste ImageView immer den click. Unabhängig, ob da ein Bild dargestellt wird oder nicht (z.B. Transparente Bereiche im Bild oder Bild kleiner als Control)

Das kann verwirrend sein und evtl. nicht gewünscht. Bei setPickOnBounds(true) ändert sich das Verhalten. Da werden nicht mehr einfach die Grenzen des Controls genommen.


Das ist jetzt aus Sicht der Node: bei false die geometrische Form des Nodes. Aber bei true werden die "echten" Bounds genommen. Bezüglich des genauen Verhaltens habe ich aber keine gute Dokumentation gefunden, daher bekommt ein SO Artikel ein gutes Rating:

Auf SO hat jemand dazu sogar eine kleine App gebaut zum Spielen: https://stackoverflow.com/questions/15525001/javafx-how-to-make-a-node-partially-mouse-transparent - Da kann man schön spielen und sehen, wie ein Click auf den transparenten Bereich mal beim ersten Element landet und mal beim hinteren.
Nun ist mir alles klar! Das obere ImageView hat transparente Bereiche (besteht nur aus 4 waagrechten Linien) und dann ist klar dass es das untere ImageView anspricht wenn ein transparenter Bereich geklickt wird.
Danke für die intensive Hilfe und lg heinz
 

Ähnliche Java Themen

Neue Themen


Oben