使用 WritableImage 的 JavaFX 透明光标

JavaFX Transparent Cursor using WritableImage

Edit-Answer:

您可以检查 Fabian's answer 以及这个库 (https://github.com/goxr3plus/JFXCustomCursor)

Actual Question

我想创建一个 JavaFX 中淡出 的光标,为此我正在使用 WritableImage 并且我不断从中读取像素原始 Image 并将它们写入新的 WritableImage。然后我使用 ImageCursor(writableImage) 将自定义光标设置为 Scene,下面是完整代码( 试一试).

问题是在预期透明像素的地方出现黑色像素。

Note that all the below classes have to be in package sample.

Code(Main):

package sample;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

public class Main extends Application {

    FadingCursor fade = new FadingCursor();

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

        primaryStage.setWidth(300);
        primaryStage.setHeight(300);

        Scene scene = new Scene(new FlowPane());

        primaryStage.setScene(scene);

        fade.startFade(scene,100);

        primaryStage.show();
    }

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

}

Code(FadingCursor)(Edited):

package sample;

import java.util.concurrent.CountDownLatch;

import javafx.application.Platform;
import javafx.scene.ImageCursor;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;

public class FadingCursor {

    private int counter;
    private Image cursorImage;

    /**
     * Change the image of the Cursor
     * 
     * @param image
     */
    public void setCursorImage(Image image) {
        this.cursorImage = image;
    }

    /**
     * Start fading the Cursor
     * 
     * @param scene
     */
    public void startFade(Scene scene, int millisecondsDelay) {

        // Create a Thread
        new Thread(() -> {

            // Keep the original image stored here
            Image image = new Image(getClass().getResourceAsStream("fire.png"), 64, 64, true, true);
            PixelReader pixelReader = image.getPixelReader();

            // Let's go
            counter = 10;
            for (; counter >= 0; counter--) {
                CountDownLatch count = new CountDownLatch(1);
                Platform.runLater(() -> {

                    // Create the fading image
                    WritableImage writable = new WritableImage(64, 64);
                    PixelWriter pixelWriter = writable.getPixelWriter();

                    // Fade out the image
                    for (int readY = 0; readY < image.getHeight(); readY++) {
                        for (int readX = 0; readX < image.getWidth(); readX++) {
                            Color color = pixelReader.getColor(readX, readY);

                            // Now write a brighter color to the PixelWriter.

                            // -------------------------Here some way happens
                            // the problem------------------
                            color = new Color(color.getRed(), color.getGreen(), color.getBlue(), (counter / 10.00) * color.getOpacity());
                            pixelWriter.setColor(readX, readY, color);
                        }
                    }

                    System.out.println("With counter:"+counter+" opacity is:" + writable.getPixelReader().getColor(32, 32).getOpacity());


                    scene.setCursor(new ImageCursor(writable));
                    count.countDown();
                });
                try {
                    // Wait JavaFX Thread to change the cursor
                    count.await();
                    // Sleep some time
                    Thread.sleep(millisecondsDelay);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

        }).start();

    }

}

The image(needs to be downloaded)(Right Click ->Save Image as...):

您将每个像素的不透明度设置为仅取决于此处循环变量的值:

color = new Color(color.getRed(), color.getGreen(), color.getBlue(), counter / 10.00);

对于透明像素(不透明度 = 0),您实际上 增加 不透明度,使存储在其他通道中的值(在本例中为 0 / 黑色)可见。您需要确保透明像素保持透明,这通常是这样完成的:

color = new Color(color.getRed(), color.getGreen(), color.getBlue(), (counter / 10.00) * color.getOpacity());

或者您可以使用 deriveColor:

color = color.deriveColor(0, 1, 1, counter / 10d);

编辑

出于某种原因 ImageCursor 似乎不喜欢完全透明的图像。您可以通过添加

检查是否有至少一个像素不完全透明
pixelWriter.setColor(0, 0, new Color(0, 0, 0, 0.01));

for 循环写入图像后。

要解决此问题,您可以简单地使用 Cursor.NONE 而不是具有完全透明图像的 ImageCursor

for (; counter >= 1; counter--) {
    ...
}
Platform.runLater(() -> scene.setCursor(Cursor.NONE));

无需重新创建 image/cursor

的替代方案

您可以通过在 Scene 的根部移动图像来自己模拟光标。这不会使图像显示超出 Scene 的边界,但您可以将动画应用于 ImageView 以淡化而不是手动修改每个像素的不透明度...

public class CursorSimulator {

    private final FadeTransition fade;

    public CursorSimulator(Image image, Scene scene, ObservableList<Node> rootChildrenWriteable, double hotspotX, double hotspotY) {
        ImageView imageView = new ImageView(image);
        imageView.setManaged(false);
        imageView.setMouseTransparent(true);

        fade = new FadeTransition(Duration.seconds(2), imageView);
        fade.setFromValue(0);
        fade.setToValue(1);

        // keep image on top
        rootChildrenWriteable.addListener((Observable o) -> {
            if (imageView.getParent() != null
                    && rootChildrenWriteable.get(rootChildrenWriteable.size() - 1) != imageView) {
                // move image to top, after changes are done...
                Platform.runLater(() -> imageView.toFront());
            }
        });
        scene.addEventFilter(MouseEvent.MOUSE_ENTERED, evt -> {
            rootChildrenWriteable.add(imageView);
        });
        scene.addEventFilter(MouseEvent.MOUSE_EXITED, evt -> {
            rootChildrenWriteable.remove(imageView);
        });
        scene.addEventFilter(MouseEvent.MOUSE_MOVED, evt -> {
            imageView.setLayoutX(evt.getX() - hotspotX);
            imageView.setLayoutY(evt.getY() - hotspotY);
        });
        scene.setCursor(Cursor.NONE);
    }

    public void fadeOut() {
        fade.setRate(-1);
        if (fade.getStatus() != Animation.Status.RUNNING) {
            fade.playFrom(fade.getTotalDuration());
        }
    }

    public void fadeIn() {
        fade.setRate(1);
        if (fade.getStatus() != Animation.Status.RUNNING) {
            fade.playFromStart();
        }
    }

}
@Override
public void start(Stage primaryStage) {
    Button btn = new Button("Say 'Hello World'");
    btn.setOnAction((ActionEvent event) -> {
        System.out.println("Hello World!");
    });

    StackPane root = new StackPane();
    root.getChildren().add(btn);

    Scene scene = new Scene(root, 500, 500);
    
    Image image = new Image("http://i.stack.imgur.com/OHj1R.png");
    CursorSimulator simulator = new CursorSimulator(image, scene, root.getChildren(), 32, 50);
    scene.setOnMouseClicked(new EventHandler<MouseEvent>() {

        private boolean fadeOut = true;
        
        @Override
        public void handle(MouseEvent event) {
            if (fadeOut) {
                simulator.fadeOut();
            } else {
                simulator.fadeIn();
            }
            fadeOut = !fadeOut;
        }
    });

    primaryStage.setScene(scene);
    primaryStage.show();
}

The reason of this question was to create a kind of cursor that can be modified.For example here i wanted to make it had a fade effect.For future users who want to create custom cursors i have created a library on github and i will show some code here: https://github.com/goxr3plus/JFXCustomCursor

Code:

import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;

/**
 * This class allows you to set as a Cursor in a JavaFX Scene,whatever you want
 * ,even a video!. <br>
 * <br>
 * <b>What you have to do is create a basic layout,for example:</b><br>
 * #-->A BorderPane which contains a MediaView,<br>
 * #-->A StackPane which contains an animated ImageView,<br>
 * #-->A Pane which contains an animated Rectangle or something more complex
 * etc..)<br>
 * 
 * <br>
 * <br>
 * The options are unlimited!
 * 
 * @author GOXR3PLUS
 * @param <T>
 * @Version 1.0
 */
public class JFXCustomCursor {

    private SimpleIntegerProperty hotSpotX = new SimpleIntegerProperty();
    private SimpleIntegerProperty hotSpotY = new SimpleIntegerProperty();

    private Scene scene;
    private Pane sceneRoot;
    private Pane content;
    private EventHandler<MouseEvent> eventHandler1;
    private EventHandler<MouseEvent> eventHandler2;
    private EventHandler<MouseEvent> eventHandler3;

    /**
     * Constructor
     * 
     * @param scene
     *            The Scene of your Stage
     * @param sceneRoot
     *            The Root of your Stage Scene
     * @param content
     *            The content of the JFXCustomCursor class
     * @param hotspotX
     *            Represents the location of the cursor inside the content on X
     *            axis
     * @param hotspotY
     *            Represents the location of the cursor inside the content on Y
     *            axis
     */
    public JFXCustomCursor(Scene scene, Pane sceneRoot, Pane content, int hotspotX, int hotspotY) {

        // Go
        setRoot(scene, sceneRoot, content, hotspotX, hotspotY);

    }

    /**
     * This method changes the content of the JFXCustomCursor
     * 
     * @param scene
     *            The Scene of your Stage
     * @param sceneRoot
     *            The Root of your Stage Scene
     * @param content
     *            The content of the JFXCustomCursor class
     * @param hotspotX
     *            Represents the location of the cursor inside the content on X
     *            axis
     * @param hotspotY
     *            Represents the location of the cursor inside the content on Y
     *            axis
     */
    public void setRoot(Scene scene, Pane sceneRoot, Pane content, int hotSpotX, int hotSpotY) {

        // Keep them in case of unRegister-reRegister
        unRegister(); // has to be called before the below happens
        this.scene = scene;
        this.sceneRoot = sceneRoot;
        this.content = content;

        // hot spots
        this.hotSpotX.set(hotSpotX);
        this.hotSpotX.set(hotSpotY);

        // cursor container
        content.setManaged(false);
        content.setMouseTransparent(true);

        // Keep the Content on the top of Scene
        ObservableList<Node> observable = sceneRoot.getChildren();
        observable.addListener((Observable osb) -> {
            if (content.getParent() != null && observable.get(observable.size() - 1) != content) {
                // move the cursor on the top
                Platform.runLater(content::toFront);
            }
        });

        if (!observable.contains(content))
            observable.add(content);

        // Add the event handlers
        eventHandler1 = evt -> {
            if (!sceneRoot.getChildren().contains(content))
                observable.add(content);
        };
        eventHandler2 = evt -> observable.remove(content);

        eventHandler3 = evt -> {
            content.setLayoutX(evt.getX() - hotSpotX);
            content.setLayoutY(evt.getY() - hotSpotY);
        };

        scene.addEventFilter(MouseEvent.MOUSE_ENTERED, eventHandler1);
        scene.addEventFilter(MouseEvent.MOUSE_EXITED, eventHandler2);
        scene.addEventFilter(MouseEvent.MOUSE_MOVED, eventHandler3);

    }

    /**
     * Unregisters the CustomCursor from the Scene completely
     */
    public void unRegister() {
        if (scene != null) {
            sceneRoot.getChildren().remove(content);
            scene.removeEventFilter(MouseEvent.MOUSE_ENTERED, eventHandler1);
            scene.removeEventFilter(MouseEvent.MOUSE_EXITED, eventHandler2);
            scene.removeEventFilter(MouseEvent.MOUSE_MOVED, eventHandler3);
        }
    }

    /**
     * Re register the CustomCursor to the Scene,<b>this method is
     * experimental(use with caution!)</b>
     */
    public void reRegister() {
        if (scene != null)
            setRoot(scene, sceneRoot, content, hotSpotX.get(), hotSpotY.get());
    }

    public SimpleIntegerProperty hotSpotXProperty() {
        return hotSpotX;
    }

    public SimpleIntegerProperty hotSpotYProperty() {
        return hotSpotY;
    }

}