java - 在 JavaFX 中逐像素生成图像,进度可见
问题描述
我正在编写一个 JavaFX 应用程序,它逐像素地生成抽象图案图像。结果应该有点像这样。这是我的主要课程:
package application;
public class Main extends Application {
private static final int WIDTH = 800;
private static final int HEIGHT = 600;
@Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
Scene scene = new Scene(root, WIDTH, HEIGHT);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
final Canvas canvas = new Canvas(WIDTH, HEIGHT);
root.getChildren().add(canvas);
final GraphicsContext gc = canvas.getGraphicsContext2D();
final PixelWriter pw = gc.getPixelWriter();
final PixelGenerator generator = new PixelGenerator(WIDTH, HEIGHT, pw);
final Thread th = new Thread(generator);
th.setDaemon(true);
th.start();
}
public static void main(String[] args) {
launch(args);
}
}
PixelGenerator 类正在一个一个地生成新像素,使用 PixelWriter.setColor() 方法填充画布。它根据一些随机数和先前生成的颜色计算新像素的颜色。
如果我在应用程序线程上运行 PixelGenerator,GUI 会被阻塞,直到整个可用空间都被填满,然后我才能看到完整的画面。
为了避免这种情况,我让我的 PixelGenerator 类扩展了 javafx.concurrent.Task 并且它的 call() 方法一次生成所有像素。有时它按预期工作,我可以看到图像是如何逐步生成的,但有时图片仍然未完成,好像任务不会运行到最后。调试显示一直运行到最后,但是后面的 PixelWriter.setColor() 调用没有效果。
我尝试了不同的方法来修复它。例如,我添加了一个 onSucceeded 事件处理程序,并试图使 Canvas “刷新”,因为我认为它只是以某种方式“跳过”了它的最后一次刷新迭代。没有成功。奇怪的是,即使为更多像素着色,在听者内部也没有效果。
我还尝试使用 AnimationTimer 而不是使用任务。它有效,但我的问题是我无法预测在它的 handle() 调用之间我能够生成多少像素。生成算法的复杂性以及像素生成所需的 CPU 时间将随着算法的发展而变化。
我的理想目标是花费所有可用的 CPU 时间来生成像素,但同时能够详细查看生成进度(60 甚至 30 FPS 都可以)。
请帮帮我,我做错了什么,给定我的目标我应该走什么方向?
解决方案
确实,您不能在 JavaFX 应用程序线程以外的线程中使用 PixelWriter。但是,您的 Task 可以将像素数据本身放在非 JavaFX 值对象中,例如IntBuffer,然后应用程序线程可以将其传递给setPixels。一个示例程序:
import java.nio.IntBuffer;
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
public class PixelGeneratorTest
extends Application {
public static final int WIDTH = Integer.getInteger("width", 500);
public static final int HEIGHT = Integer.getInteger("height", 500);
public class PixelGenerator
extends Task<IntBuffer> {
private final int width;
private final int height;
public PixelGenerator(int width,
int height) {
this.width = width;
this.height = height;
}
@Override
public IntBuffer call() {
IntBuffer buffer = IntBuffer.allocate(width * height);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
Color pixel = Color.hsb(
y * 360.0 / HEIGHT, 1, (double) x / WIDTH);
int argb = 0xff000000 |
((int) (pixel.getRed() * 255) << 16) |
((int) (pixel.getGreen() * 255) << 8) |
(int) (pixel.getBlue() * 255);
buffer.put(argb);
}
}
buffer.flip();
return buffer;
}
}
@Override
public void start(Stage stage) {
Canvas canvas = new Canvas(WIDTH, HEIGHT);
stage.setTitle("PixelGenerator Test");
stage.setScene(new Scene(new BorderPane(canvas)));
stage.show();
GraphicsContext gc = canvas.getGraphicsContext2D();
PixelWriter pw = gc.getPixelWriter();
PixelGenerator generator = new PixelGenerator(WIDTH, HEIGHT);
generator.valueProperty().addListener((o, oldValue, pixels) ->
pw.setPixels(0, 0, WIDTH, HEIGHT,
PixelFormat.getIntArgbInstance(), pixels, WIDTH));
Thread th = new Thread(generator);
th.setDaemon(true);
th.start();
}
}
如果您希望您的图像非常大,因此一次保存在内存中是不切实际的,您可以编写 Task 以接受构造函数参数,使其仅生成图像的一部分,然后创建多个任务来处理像素生成一块一块的。另一种选择是拥有一个重复调用updateValue的任务,但您必须创建一个自定义值类,其中包含缓冲区和图像中应应用该缓冲区的矩形区域。
更新:
您已经澄清您需要渐进式图像渲染。Task 不适用于快速更新,因为对 Task 值的更改可能会被 JavaFX 合并。所以它回到了基础:创建一个在Platform.runLater调用中调用 setPixels 的 Runnable,以确保正确的线程使用:
import java.nio.IntBuffer;
import java.util.Objects;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
public class PixelGeneratorTest2
extends Application {
public static final int WIDTH = Integer.getInteger("width", 500);
public static final int HEIGHT = Integer.getInteger("height", 500);
private static final PixelFormat<IntBuffer> pixelFormat =
PixelFormat.getIntArgbInstance();
public class PixelGenerator
implements Runnable {
private final int width;
private final int height;
private final PixelWriter writer;
public PixelGenerator(int width,
int height,
PixelWriter pw) {
this.width = width;
this.height = height;
this.writer = Objects.requireNonNull(pw, "Writer cannot be null");
}
@Override
public void run() {
int blockHeight = 4;
IntBuffer buffer = IntBuffer.allocate(width * blockHeight);
try {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
Color pixel = Color.hsb(
y * 360.0 / HEIGHT, 1, (double) x / WIDTH);
int argb = 0xff000000 |
((int) (pixel.getRed() * 255) << 16) |
((int) (pixel.getGreen() * 255) << 8) |
(int) (pixel.getBlue() * 255);
buffer.put(argb);
}
if (y % blockHeight == blockHeight - 1 || y == height - 1) {
buffer.flip();
int regionY = y - y % blockHeight;
int regionHeight =
Math.min(blockHeight, height - regionY);
Platform.runLater(() ->
writer.setPixels(0, regionY, width, regionHeight,
pixelFormat, buffer, width));
buffer.clear();
}
// Pretend pixel calculation was CPU-intensive.
Thread.sleep(25);
}
} catch (InterruptedException e) {
System.err.println("Interrupted, exiting.");
}
}
}
@Override
public void start(Stage stage) {
Canvas canvas = new Canvas(WIDTH, HEIGHT);
stage.setTitle("PixelGenerator Test");
stage.setScene(new Scene(new BorderPane(canvas)));
stage.show();
GraphicsContext gc = canvas.getGraphicsContext2D();
PixelWriter pw = gc.getPixelWriter();
PixelGenerator generator = new PixelGenerator(WIDTH, HEIGHT, pw);
Thread th = new Thread(generator);
th.setDaemon(true);
th.start();
}
}
您可以为每个单独的像素调用 Platform.runLater,但我怀疑这会使 JavaFX 应用程序线程不堪重负。
推荐阅读
- php - 如何通过 url 发送带有选择标签的查询
- laravel - 为什么我的 laravel 项目与旧数据库连接
- ios - 如何在不更改主数组中原始顺序的情况下通过 BOOL 快速排序?
- spring - bean JndiObjectFactoryBean 的 Spring NotWritablePropertyException 和无效属性“lazyInit”
- python - 制作字谜游戏
- javascript - 如何打破这两个 javascript 模块之间的循环依赖关系?
- excel - 运行时错误“424”:需要对象 IE.Document.GetElementById
- php - 如何从外部视图获取 Laravel 刀片 @foreach 循环中的变量
- javascript - Angular platformLocation pushState 不起作用
- spring - 在 Thymeleaf 中获取带有参数的完整 URL?