【问题标题】:JavaFX: Periodically repainting an ImageView using the Service class. The image remains unchangedJavaFX:使用 Service 类定期重绘 ImageView。图像保持不变
【发布时间】:2019-11-15 18:33:19
【问题描述】:

我在 JavaFX 中遇到了多线程问题。我使用javafx.concurrent 包中的Service 类定期重新计算和更改ImageView 中的图像。 为此,我有这个方法,它是从 JavaFX 应用程序的 start() 方法直接调用的:

public void startRepaintingThread() {
    for (GameWindow gamewindow : gameWindows) {
        RepaintingLoopService service = new RepaintingLoopService(gamewindow);
        service.setOnSucceeded((eh) -> {
            gamewindow.setImage(service.getValue());
            service.reset();
            service.start();
        });
        service.start();
    }
}

这里GameWindowImageView 的一个简单子类,RepaintingLoopServicejavafx.concurrent.Service 的一个子类,它执行一些复杂的逻辑来重新计算图像并将新图像作为其值返回。现在我使用调试器和日志记录验证,此代码中的 service.getValue() 实际上确实返回了正确重新计算的图像,因此重新计算逻辑是正确的,但仍然在 UI 中图像保持不变!或者,更准确地说:在一些非常罕见的情况下,它实际上会发生变化,但在 95% 的情况下,它仍然是静态图像(第一个绘制的图像),所以它似乎取决于某种竞争条件或其他东西......好吧,我想,也许你知道什么可能是错的?我将全局变量gameWindows 设置为volatile,以及多个线程使用的所有其他全局变量。也许我以某种方式错误地使用了Service

========= 编辑 ====================

我确实可以构建一个完整的小例子来重现错误。请参阅下面的代码。此应用程序中的Service 会定期重新绘制图像,从白色图像开始,每次添加一行黑色像素。 再一次:它似乎工作......有时:对我来说,第一次运行它时它工作得很好,但是在以下任何一次我启动程序时,UI 中的图像在前 2 或 3 条黑线之后不再改变添加 … 代码如下:

import javafx.application.Application;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.stage.Stage;


public class BugFix extends Application {

    private static final int WINDOW_WIDTH = 800;
    private static final int WINDOW_HEIGHT = 800;

    /**
     * Time in ms between repainting attempts
     **/
    private static final long REPAINTING_TIME = 100;

    private ImageView imageView;
    private RepaintingService service = new RepaintingService();

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        setUpStage(primaryStage);
        startService();
    }

    private void setUpStage(Stage stage) {
        Group group = new Group();
        imageView = new ImageView(new WritableImage(WINDOW_WIDTH, WINDOW_HEIGHT));
        group.getChildren().add(imageView);

        stage.setScene(new Scene(group, WINDOW_WIDTH, WINDOW_HEIGHT));
        stage.show();
    }

    private void startService() {
        service.setOnSucceeded((eh) -> {
            imageView.setImage(service.getValue());
            int firstWhiteLine = findFirstWhiteLineInImage(service.getValue());
            System.out.println("First white line in received image: " + firstWhiteLine);
            try {
                Thread.sleep(REPAINTING_TIME);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            service.reset();
            service.start();
        });
        service.start();
    }

    /**
     * For debug purposes: Do find the number of the first line with white pixels
     * in the given image.
     **/
    private int findFirstWhiteLineInImage(Image repaintedImage) {
        for (int line = 0; line < repaintedImage.getHeight(); line++) {
            if (Color.WHITE.equals(repaintedImage.getPixelReader().getColor(0, line))) {
                return line;
            }
        }
        return -1;
    }

    /**
     * A service to periodically repaint the image,
     * starting off with a white image and with each repainting adding a black line of pixels.
     **/
    private class RepaintingService extends Service<Image> {

        private volatile WritableImage image = new WritableImage(WINDOW_WIDTH, WINDOW_HEIGHT);
        private int blackLinesCount = 0;

        @Override
        protected Task<Image> createTask() {
            return new Task<Image>() {

                @Override
                protected Image call() {
                    repaintImage();
                    blackLinesCount++;
                    return image;
                }
            };
        }

        /**
         * Repaints the image with the upper n lines being black
         * and the remaining lines being white.
         **/
        private void repaintImage() {
            for (int line = 0; line < WINDOW_HEIGHT; line++) {
                for (int column = 0; column < WINDOW_HEIGHT; column++) {
                    Color color = line <= blackLinesCount ? Color.BLACK : Color.WHITE;
                    image.getPixelWriter().setColor(column, line, color);
                }
            }
        }
    }
}

无论如何,程序的控制台输出总是一样的:

First white line in received image: 1
First white line in received image: 2
First white line in received image: 3
First white line in received image: 4
First white line in received image: 5

(等等...)所以,这再次意味着,从service.getValue() 读取的图像始终是正确重绘的图像。但由于某种原因,它并没有(总是)显示在 UI 中,尽管该图像恰好在同一行中传递给 imageView.setImage()

【问题讨论】:

  • 我的猜测是 RepaintingLoopService 对象正在被垃圾收集。尝试保持对它们的强烈引用。
  • @Slaw 谢谢你的提示!我在 GameWindow 类中添加了一个 repaintingService 字段,现在我从那里获取服务实例,但不幸的是行为保持不变:(
  • 您能展示一下您的ServiceTask 实现(即minimal reproducible example)吗?
  • 我确实设法构建了一个简短但完整的示例来重现相同的问题(请参阅我的编辑)。
  • 它始终是同一个图像实例,所以基本上,您正在更新一个(属性)视图,该视图在 fx 应用程序线程之外的场景图中存在 - 您绝对不能这样做,永远不要这样做。

标签: java multithreading javafx worker


【解决方案1】:

您的代码中的问题是,在场景图中处于活动状态的节点 (imageView) 的属性(此处为:图像中的像素)在 fx 应用程序线程中更新。这有效地阻止了 ui 本身的更新。

一种解决方案是让后台线程返回它正在处理的图像的副本:

// in your task

@Override
protected Task<Image> createTask() {
    return new Task<Image>() {

        @Override
        protected Image call() {
            repaintImage();
            blackLinesCount++;
            return copyImage(image);
        }
    };
}

一个实用方法——简单取自another answer只是为了演示效果:

/**
 * copy the given image to a writeable image
 * @param image
 * @return a writeable image
 */
public static WritableImage copyImage(Image image) {
    int height = (int) image.getHeight();
    int width = (int) image.getWidth();
    PixelReader pixelReader = image.getPixelReader();
    WritableImage writableImage = new WritableImage(width, height);
    PixelWriter pixelWriter = writableImage.getPixelWriter();

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            Color color = pixelReader.getColor(x, y);
            pixelWriter.setColor(x, y, color);
        }
    }
    return writableImage;
}

【讨论】:

  • 谢谢!每次实例都使用新图像的解决方案确实解决了问题!我还将尝试以某种方式将更新视图的调用放入应用程序线程中,也许如果有办法做到这一点,这将是一个更优化的解决方案。我真的不喜欢每隔几毫秒创建一个新图像的想法 :) 再次,非常感谢您的解释,我已经与这个问题作斗争了好几天 :)
猜你喜欢
  • 1970-01-01
  • 2014-05-01
  • 2018-03-04
  • 1970-01-01
  • 2016-07-23
  • 2018-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多