JavaFX:使用服务 class 定期重新绘制 ImageView。图像保持不变
JavaFX: Periodically repainting an ImageView using the Service class. The image remains unchanged
我在 JavaFX 中遇到多线程问题。我使用 javafx.concurrent
包中的 class 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();
}
}
这里 GameWindow
是 ImageView
的简单子 class 而 RepaintingLoopService
是 javafx.concurrent.Service
的子class 它做了一些复杂的重新计算图像并将 returns 新图像作为其值的逻辑。现在我使用调试器和日志记录验证,此代码中的 service.getValue()
实际上确实 return 正确重新计算的图像,因此重新计算逻辑是正确的,但仍然在 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()
。
您的代码中的问题是场景图形中处于活动状态的节点 (imageView) 的 属性(此处:图像中的像素)在 fx 应用程序线程中更新。这有效地阻止了 ui 本身的更新。
一个解决方案是让后台线程return复制它正在处理的图像:
// 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;
}
我在 JavaFX 中遇到多线程问题。我使用 javafx.concurrent
包中的 class 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();
}
}
这里 GameWindow
是 ImageView
的简单子 class 而 RepaintingLoopService
是 javafx.concurrent.Service
的子class 它做了一些复杂的重新计算图像并将 returns 新图像作为其值的逻辑。现在我使用调试器和日志记录验证,此代码中的 service.getValue()
实际上确实 return 正确重新计算的图像,因此重新计算逻辑是正确的,但仍然在 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()
。
您的代码中的问题是场景图形中处于活动状态的节点 (imageView) 的 属性(此处:图像中的像素)在 fx 应用程序线程中更新。这有效地阻止了 ui 本身的更新。
一个解决方案是让后台线程return复制它正在处理的图像:
// 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;
}