Skip to content

10.2 Gráfico de escena

1. Descripción general

Un gráfico de escena es una estructura de datos de árbol que organiza (y agrupa) objetos gráficos para una representación lógica más sencilla. También permite que el motor de gráficos represente los objetos de la manera más eficiente al omitir total o parcialmente los objetos que no se verán en la imagen final. La siguiente figura muestra un ejemplo de la arquitectura del gráfico de escena JavaFX.

scene_graph

En la parte superior de la arquitectura hay un Stage. Una etapa es una representación JavaFX de una ventana de sistema operativo nativo. En un momento dado, un escenario puede tener un solo Scene adjunto. Una escena es un contenedor para el gráfico de escena JavaFX.

Todos los elementos en el gráfico de escena JavaFX (como pueden ser botones, etiquetas, paneles, etc) se representan como Node objetos. Hay tres tipos de nodos: raíz, rama y hoja. El nodo raíz es el único nodo que no tiene un padre y está contenido directamente en una escena, que se puede ver en la figura anterior. La diferencia entre una rama y una hoja es que un nodo hoja no tiene hijos.

En el gráfico de escena, los nodos secundarios comparten muchas propiedades de un nodo principal. Por ejemplo, una transformación o un evento aplicado a un nodo padre también se aplicará recursivamente a sus hijos. Como tal, una jerarquía compleja de nodos se puede ver como un solo nodo para simplificar el modelo de programación. Exploraremos transformaciones y eventos en secciones posteriores.

En la siguiente figura se puede ver un ejemplo de un gráfico de escena "Hola Mundo".

specific_scene_graph

Una posible implementación que producirá un gráfico de escena que coincida con la figura anterior es el siguiente ejemplo:

Ejemplo Hola Mundo: E01_HolaMundo.java

Vamos comenzar por el archifamoso programa de inicialización en el mundo de la programación:

  1. Heredando la clase principal de la clase Application.
  2. Tendremos que importar las clases de la librería.
  3. Implementar el método abstracto, añadir el launch() al main() y añadir los imports necesarios.

package projectejavafx;

import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class HolaMundo extends Application {

    private Parent createContent() {
        return new StackPane(new Text("Hola Mundo"));
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("Hola Mundo");
        stage.setScene(new Scene(createContent(), 400, 400));
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
Notas importantes:

  1. Un nodo puede tener un máximo de 1 padre.
  2. Un nodo en el gráfico de escena "activo" (adjunto a una escena actualmente visible) solo se puede modificar desde el subproceso de la aplicación JavaFX.
  3. La clase HolaMundo extiende Application. Application es la clase base para cualquier aplicación JavaFX y proporciona el método start que debe ser sobrescrito.
  4. El método createContent() devuelve un objeto Parent, que es un nodo raíz en la escena. Aquí, se usa StackPane, que es un contenedor que organiza los elementos apilándolos. Dentro de StackPane, se coloca un Text("Hola Mundo"), que es el nodo de texto que se verá en la ventana.
  5. start(Stage stage) es el punto de entrada de la aplicación JavaFX. Se ejecuta cuando la aplicación se inicia:
    • stage.setTitle("Hola Mundo"); establece el título de la ventana.
    • stage.setScene(new Scene(createContent(), 400, 400)); se crea una escena (Scene) con el contenido generado en createContent(). La escena tiene un tamaño de 400x400 píxeles.
    • stage.show(); muestra la ventana en la pantalla.
  6. launch(args); inicia la aplicación JavaFX. Este método llama internamente a start(Stage stage) después de inicializar el entorno de JavaFX.

Si hemos seguido todos los pasos correctamente, podremos ejecutar nuestra aplicación y ver la ventana titulada "Hola Mundo":

Hola Mundo

2. Transformaciones

Usaremos la siguiente aplicación como ejemplo para demostrar las 3 transformaciones más comunes: E02_TransformApp.java

Ejemplo E02_TransformApp.java
package projectejavafx;

import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class E02_TransformApp extends Application {

    private Parent createContent() {
        Rectangle box = new Rectangle(100, 50, Color.BLUE);
        transform(box);
        return new Pane(box);
    }

    private void transform(Rectangle box) {
        // 1.Translación
        box.setTranslateX(100);
        box.setTranslateY(200);

        // 2. Escala
        box.setScaleX(1.5);
        box.setScaleY(1.5);

        // 3. Rotación
        box.setRotate(30);
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent(), 300, 300, Color.GRAY));
        stage.show();
    }

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

Ejecutar la aplicación dará como resultado la siguiente imagen:

bluebox

En JavaFX, puede ocurrir una transformación simple en uno de los 3 ejes: X, Y o Z. La aplicación de ejemplo está en 2D, por lo que solo consideraremos los ejes X e Y.

2.1. Translación

En JavaFX y gráficos por computadora, translate significa moverse.

Un ejemplo de esto es que podemos trasladar nuestra caja en 100 píxeles en el eje X y 200 píxeles en el eje Y:

1
2
3
4
private void transform(Rectangle box) {
    box.setTranslateX(100);
    box.setTranslateY(200);
}
bluebox

2.2. Escala

Puede aplicar la escala para hacer un nodo más grande o más pequeño. El valor de escala es una relación. Por defecto, un nodo tiene un valor de escala de 1 (100%) en cada eje.

Un ejemplo de esto es que podemos agrandar nuestra caja aplicando una escala de 1.5 en los ejes X e Y:

1
2
3
4
private void transform(Rectangle box) {
    box.setScaleX(1.5);
    box.setScaleY(1.5);
}
bluebox

2.3. Rotación

La rotación de un nodo determina el ángulo en el que se representa el nodo. En 2D el único eje de rotación sensible es el eje Z.

Un ejemplo de esto es que podemos girar la caja 30 grados:

1
2
3
private void transform(Rectangle box) {
    box.setRotate(30);
}
bluebox

3. Manejo de eventos

Un evento notifica que ha ocurrido algo importante. Los eventos suelen ser lo "primitivo" de un sistema de eventos (también conocido como bus de eventos). Generalmente, un sistema de eventos tiene las siguientes 3 responsabilidades:

  • fire (desencadenar) un evento,
  • notificar listeners (a las partes interesadas) sobre el evento y
  • handle (procesar) el evento.

El mecanismo de notificación de eventos lo realiza la plataforma JavaFX automáticamente. Por lo tanto, solo consideraremos cómo disparar eventos, escuchar eventos y cómo manejarlos.

Primero, vamos a crear un evento personalizado:

Ejemplo E03_EventoUsuario.java
package projectejavafx;

import javafx.event.Event;
import javafx.event.EventType;

public class E03_EventoUsuario extends Event {

    public static final EventType<E03_EventoUsuario> ANY = new EventType<>(Event.ANY, "ANY");

    public static final EventType<E03_EventoUsuario> LOGIN_SUCCEEDED = new EventType<>(ANY, "LOGIN_SUCCEEDED");

    public static final EventType<E03_EventoUsuario> LOGIN_FAILED = new EventType<>(ANY, "LOGIN_FAILED");

    public E03_EventoUsuario(EventType<? extends Event> eventType) {
        super(eventType);
    }
    // cualquier otro atributo importante como la fecha, la hora...
}

Dado que los tipos de eventos son fijos, generalmente se crean dentro del mismo archivo de origen que el evento. Podemos ver que hay 2 tipos específicos de eventos: LOGIN_SUCCEEDEDy LOGIN_FAILED. Podemos escuchar estos tipos específicos de eventos:

1
2
3
4
Node node = ...
node.addEventHandler(UserEvent.LOGIN_SUCCEEDED, event -> {
    // handle event
});

Alternativamente, podemos manejar cualquier UserEvent:

1
2
3
4
Node node = ...
node.addEventHandler(UserEvent.ANY, event -> {
    // handle event
});

Finalmente, podemos construir y disparar nuestros propios eventos:

1
2
3
UserEvent event = new UserEvent(UserEvent.LOGIN_SUCCEEDED);
Node node = ...
node.fireEvent(event);

Por ejemplo, LOGIN_SUCCEEDEDo LOGIN_FAILED podría activarse cuando un usuario intenta iniciar sesión en una aplicación. Según el resultado del inicio de sesión, podemos permitir que el usuario acceda a la aplicación o bloquearlo. Si bien se puede lograr la misma funcionalidad con una if declaración simple, hay una ventaja significativa de un sistema de eventos. Los sistemas de eventos se diseñaron para permitir la comunicación entre varios módulos (subsistemas) en una aplicación sin acoplarlos estrechamente. Como tal, un sistema de audio puede reproducir un sonido cuando el usuario inicia sesión. Por lo tanto, mantiene todo el código relacionado con el audio en su propio módulo. Sin embargo, no profundizaremos en los estilos arquitectónicos.

3.1. Eventos de entrada

Los eventos de teclado y ratón son los tipos de eventos más comunes utilizados en JavaFX. Cada Node proporciona los llamados "métodos de conveniencia" para manejar estos eventos. Por ejemplo, podemos ejecutar algún código cuando se presiona un botón:

1
2
3
4
Button button = ...
button.setOnAction(event -> {
    // button was pressed
});

Para mayor flexibilidad también podemos usar lo siguiente:

1
2
3
4
5
Button button = ...
button.setOnMouseEntered(e -> ...);
button.setOnMouseExited(e -> ...);
button.setOnMousePressed(e -> ...);
button.setOnMouseReleased(e -> ...);

El objeto e anterior es de tipo MouseEvent y se puede consultar para obtener información diversa sobre el evento, por ejemplo, x posiciones y, número de clics, etc. Finalmente, podemos hacer lo mismo con las teclas:

1
2
3
Button button = ...
button.setOnKeyPressed(e -> ...);
button.setOnKeyReleased(e -> ...);

El objeto eaquí es de tipo KeyEvent y lleva información sobre el código de la tecla, que luego se puede asignar a una tecla física real en el teclado.

4. Sincronización

Es importante comprender la diferencia de tiempo entre la creación de controles de interfaz de usuario de JavaFX y la visualización de los controles. Al crear los controles de la interfaz de usuario, ya sea a través de la creación directa de objetos API o mediante FXML, es posible que te falten ciertos valores de geometría de pantalla, como las dimensiones de una ventana. Eso está disponible más tarde, en el instante en que se muestra la pantalla al usuario. Ese evento de visualización, llamado OnShown, es el momento en que se ha asignado una ventana y se completan los cálculos de diseño final.

Para demostrar esto, considere el siguiente programa que muestra las dimensiones de la pantalla mientras se crean los controles de la interfaz de usuario y las dimensiones de la pantalla cuando se muestra la pantalla. La siguiente captura de pantalla muestra la ejecución del programa. Cuando se crean los controles de la interfaz de usuario (new VBox(), new Scene(), primaryStage.setScene()), no hay valores reales de alto y ancho de ventana disponibles como lo demuestran los valores "NaN" indefinidos.

strartvsshow

Sin embargo, los valores de ancho y alto están disponibles una vez que se muestra la ventana. El programa registra un controlador de eventos para el evento OnShown y prepara la misma salida.

La siguiente es la clase Java del programa de demostración:

Ejemplo E04_StartVsShown.java
package projectejavafx;

public class StartVsShownJavaFXApp extends Application {

    private DoubleProperty startX = new SimpleDoubleProperty();
    private DoubleProperty startY = new SimpleDoubleProperty();
    private DoubleProperty shownX = new SimpleDoubleProperty();
    private DoubleProperty shownY = new SimpleDoubleProperty();

    @Override
    public void start(Stage primaryStage) throws Exception {

        Label startLabel = new Label("Start Dimensions");
        TextField startTF = new TextField();
        startTF.textProperty().bind(
                Bindings.format("(%.1f, %.1f)", startX, startY)
        );

        Label shownLabel = new Label("Shown Dimensions");
        TextField shownTF = new TextField();
        shownTF.textProperty().bind(
                Bindings.format("(%.1f, %.1f)", shownX, shownY)
        );

        GridPane gp = new GridPane();
        gp.add( startLabel, 0, 0 );
        gp.add( startTF, 1, 0 );
        gp.add( shownLabel, 0, 1 );
        gp.add( shownTF, 1, 1 );
        gp.setHgap(10);
        gp.setVgap(10);

        HBox hbox = new HBox(gp);
        hbox.setAlignment(CENTER);

        VBox vbox = new VBox(hbox);
        vbox.setAlignment(CENTER);

        Scene scene = new Scene( vbox, 480, 320 );

        primaryStage.setScene( scene );

        // before show()...I just set this to 480x320, right?
        startX.set( primaryStage.getWidth() );
        startY.set( primaryStage.getHeight() );

        primaryStage.setOnShown( (evt) -> {
            shownX.set( primaryStage.getWidth() );
            shownY.set( primaryStage.getHeight() );  // all available now
        });

        primaryStage.setTitle("Start Vs. Shown");
        primaryStage.show();
    }

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

A veces, se conocerá las dimensiones de la pantalla de antemano y se puede usar esos valores en cualquier punto del programa JavaFX. Esto incluye antes del evento OnShown.

Sin embargo, si su secuencia de inicialización contiene lógica que necesita estos valores, deberá trabajar con el evento OnShown. Un caso de uso podría ser trabajar con las últimas dimensiones guardadas o dimensiones basadas en la entrada del programa.