JavaFX Canvas appendSVG 路径呈现行为
JavaFX Canvas appendSVG Path Rendering Behavior
我想逐个绘制矢量,每次添加新的部分时都重新绘制矢量。我有一个字符串,其中包含循环通过的最终 svg 的每条路径(由字符串中的“;”划定)。此外,我添加了 strokeLine
作为某种进度条来跟踪绘制了多少矢量。
public void renderObject(GraphicsContext playGraphics, Canvas toUpdate)
{
playGraphics.beginPath();
String toAppend = "M 215 256; L 215 256; L 215 256; L 215 256; L 225 241; L 234 231; L 246 223; L 266 214; L 284 208; L 309 204; L 340 200; L 378 199; L 416 199; L 444 199; L 473 203; L 485 206; L 496 211; L 506 218; L 510 224; L 513 233; L 515 243; L 516 257; L 512 270; L 502 285; L 493 298; L 483 308; L 476 315; L 472 318; L 469 320; L 468 320; L 468 320; L 468 320; L 468 317; L 472 309; L 480 300; L 492 293; L 510 287; L 535 283; L 557 282; L 580 283; L 593 287; L 607 295; L 623 311; L 634 333; L 640 355; L 642 396; L 639 430; L 624 467; L 602 508; L 582 536; L 557 563; L 524 585; L 490 602; L 464 611; L 432 619; L 420 621; L 404 622; L 393 622; L 383 621; L 376 620; L 372 618; L 365 610; L 360 598; L 358 578; L 357 554; L 361 514; L 371 493; L 386 463; L 412 422; L 432 395; L 456 362; L 480 329; L 506 299; L 533 271; L 560 247; L 600 213; L 620 194; L 626 189; L 629 184; L 630 182; L 632 178";
for (int i=0; i < toAppend.split(";").length; i++)
{
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
playGraphics.clearRect(0, 0, toUpdate.getWidth(), toUpdate.getHeight());
playGraphics.strokeLine(50, 50, 50+10*i, 50);
playGraphics.appendSVGPath(toAppend.split(";")[i]);
playGraphics.stroke();
}
}
然而,一旦实施到现场 canvas,矢量不会被绘制,只有进度条可见。考虑到在渲染完成后,如果再次使用 graphicsContext
对象并调用另一个 playGraphics.stroke();
,则矢量图像可以完整显示在 canvas 上,这是非常奇特的。
为什么 playGraphics.stroke();
无法 运行 但 playGraphics.strokeLine
运行 没问题?
指示正在渲染的进度条,但未显示矢量
指示正在渲染的进度条,但未显示矢量
最终显示矢量,但仅在 canvas 上绘制了一个点后,重新调用 playGraphics.stroke();
当您调用 stroke()
时,SVG 路径似乎以某种方式从当前路径中删除(之后它们无法附加到当前路径)。如果您只在循环完成时调用 stroke()
(我意识到这会破坏动画效果,因此不是解决方案),您的绘图将按预期显示。
由于您的睡眠调用忽略了 JavaFX 的线程规则,因此情况变得复杂。您的 renderObject
方法是否在 JavaFX 应用程序线程中被调用?
- 如果是这样,
sleep
调用会阻止事件的处理,包括绘制。
- 否则,GraphicsContext 方法调用是非法的,可能会失败或行为不可预测。来自 GraphicsContext documentation:
… Once a Canvas
node is attached to a scene, it must be modified on the JavaFX Application Thread.
Calling any method on the GraphicsContext
is considered modifying its corresponding Canvas
and is subject to the same threading rules.
显示 Canvas 后,您必须在应用程序线程中调用这些 GraphicsContext。但是,您不能在应用程序线程中休眠;这会导致所有 GUI 事件挂起,包括所有视觉更新和对用户输入的所有响应。
有几种方法可以正确地进行多线程处理。最简单的方法是创建一个新线程,其中允许 sleep
调用,同时确保您的 Canvas GraphicsContext 调用使用 Platform.runLater.
在 JavaFX 应用程序线程中进行
一些其他注意事项:
- 正则表达式很昂贵。
toAppend.split(";")
不是一个变量,它是一个昂贵的 操作。 并且您在每个循环迭代中重复该操作 两次 !您应该在循环开始之前调用 split(";")
一次 ,并将返回的数组保存在变量中。 (JVM 可能能够在运行时进行此优化,但不能保证。)
- 就此而言,您根本不需要
split
;您可以将所有 SVG 命令放入一个数组中,这样的额外好处是更易于阅读。
- 中断不是偶然发生的。如果您的线程被打断,则意味着有人明确要求您停止您正在做的事情并优雅地退出。忽略中断意味着您的线程是无法停止的流氓线程。在大多数情况下,最好的做法是将整个方法体放在 try/catch 中,这样如果被中断它会自动退出。
所以,考虑到以上所有内容,您想要这样的东西:
public void renderObject(GraphicsContext playGraphics, Canvas toUpdate)
{
if (!Platform.isFxApplicationThread())
{
throw new IllegalStateException(
"Must be called in JavaFX application thread");
}
String[] toAppend = {
"M 215 256", "L 215 256", "L 215 256", "L 215 256",
"L 225 241", "L 234 231", "L 246 223", "L 266 214",
"L 284 208", "L 309 204", "L 340 200", "L 378 199",
"L 416 199", "L 444 199", "L 473 203", "L 485 206",
"L 496 211", "L 506 218", "L 510 224", "L 513 233",
"L 515 243", "L 516 257", "L 512 270", "L 502 285",
"L 493 298", "L 483 308", "L 476 315", "L 472 318",
"L 469 320", "L 468 320", "L 468 320", "L 468 320",
"L 468 317", "L 472 309", "L 480 300", "L 492 293",
"L 510 287", "L 535 283", "L 557 282", "L 580 283",
"L 593 287", "L 607 295", "L 623 311", "L 634 333",
"L 640 355", "L 642 396", "L 639 430", "L 624 467",
"L 602 508", "L 582 536", "L 557 563", "L 524 585",
"L 490 602", "L 464 611", "L 432 619", "L 420 621",
"L 404 622", "L 393 622", "L 383 621", "L 376 620",
"L 372 618", "L 365 610", "L 360 598", "L 358 578",
"L 357 554", "L 361 514", "L 371 493", "L 386 463",
"L 412 422", "L 432 395", "L 456 362", "L 480 329",
"L 506 299", "L 533 271", "L 560 247", "L 600 213",
"L 620 194", "L 626 189", "L 629 184", "L 630 182",
"L 632 178"
};
// If the SVG string is not hard-coded, create the array here:
// String[] toAppend = svgString.split(";");
Runnable pathBuilder = () -> {
try
{
StringBuilder path = new StringBuilder();
for (String segment : toAppend)
{
Thread.sleep(100);
path.append(" ").append(segment);
String pathToDraw = path.toString();
Platform.runLater(() -> {
playGraphics.beginPath();
playGraphics.appendSVGPath(pathToDraw);
playGraphics.stroke();
});
}
}
catch (InterruptedException e)
{
// Someone wants us to exit, so fall through and return.
e.printStackTrace();
}
};
Thread pathBuildingThread = new Thread(pathBuilder);
pathBuildingThread.setDaemon(true);
pathBuildingThread.start();
}
我想逐个绘制矢量,每次添加新的部分时都重新绘制矢量。我有一个字符串,其中包含循环通过的最终 svg 的每条路径(由字符串中的“;”划定)。此外,我添加了 strokeLine
作为某种进度条来跟踪绘制了多少矢量。
public void renderObject(GraphicsContext playGraphics, Canvas toUpdate)
{
playGraphics.beginPath();
String toAppend = "M 215 256; L 215 256; L 215 256; L 215 256; L 225 241; L 234 231; L 246 223; L 266 214; L 284 208; L 309 204; L 340 200; L 378 199; L 416 199; L 444 199; L 473 203; L 485 206; L 496 211; L 506 218; L 510 224; L 513 233; L 515 243; L 516 257; L 512 270; L 502 285; L 493 298; L 483 308; L 476 315; L 472 318; L 469 320; L 468 320; L 468 320; L 468 320; L 468 317; L 472 309; L 480 300; L 492 293; L 510 287; L 535 283; L 557 282; L 580 283; L 593 287; L 607 295; L 623 311; L 634 333; L 640 355; L 642 396; L 639 430; L 624 467; L 602 508; L 582 536; L 557 563; L 524 585; L 490 602; L 464 611; L 432 619; L 420 621; L 404 622; L 393 622; L 383 621; L 376 620; L 372 618; L 365 610; L 360 598; L 358 578; L 357 554; L 361 514; L 371 493; L 386 463; L 412 422; L 432 395; L 456 362; L 480 329; L 506 299; L 533 271; L 560 247; L 600 213; L 620 194; L 626 189; L 629 184; L 630 182; L 632 178";
for (int i=0; i < toAppend.split(";").length; i++)
{
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
playGraphics.clearRect(0, 0, toUpdate.getWidth(), toUpdate.getHeight());
playGraphics.strokeLine(50, 50, 50+10*i, 50);
playGraphics.appendSVGPath(toAppend.split(";")[i]);
playGraphics.stroke();
}
}
然而,一旦实施到现场 canvas,矢量不会被绘制,只有进度条可见。考虑到在渲染完成后,如果再次使用 graphicsContext
对象并调用另一个 playGraphics.stroke();
,则矢量图像可以完整显示在 canvas 上,这是非常奇特的。
为什么 playGraphics.stroke();
无法 运行 但 playGraphics.strokeLine
运行 没问题?
playGraphics.stroke();
当您调用 stroke()
时,SVG 路径似乎以某种方式从当前路径中删除(之后它们无法附加到当前路径)。如果您只在循环完成时调用 stroke()
(我意识到这会破坏动画效果,因此不是解决方案),您的绘图将按预期显示。
由于您的睡眠调用忽略了 JavaFX 的线程规则,因此情况变得复杂。您的 renderObject
方法是否在 JavaFX 应用程序线程中被调用?
- 如果是这样,
sleep
调用会阻止事件的处理,包括绘制。 - 否则,GraphicsContext 方法调用是非法的,可能会失败或行为不可预测。来自 GraphicsContext documentation:
… Once a
Canvas
node is attached to a scene, it must be modified on the JavaFX Application Thread.Calling any method on the
GraphicsContext
is considered modifying its correspondingCanvas
and is subject to the same threading rules.
显示 Canvas 后,您必须在应用程序线程中调用这些 GraphicsContext。但是,您不能在应用程序线程中休眠;这会导致所有 GUI 事件挂起,包括所有视觉更新和对用户输入的所有响应。
有几种方法可以正确地进行多线程处理。最简单的方法是创建一个新线程,其中允许 sleep
调用,同时确保您的 Canvas GraphicsContext 调用使用 Platform.runLater.
一些其他注意事项:
- 正则表达式很昂贵。
toAppend.split(";")
不是一个变量,它是一个昂贵的 操作。 并且您在每个循环迭代中重复该操作 两次 !您应该在循环开始之前调用split(";")
一次 ,并将返回的数组保存在变量中。 (JVM 可能能够在运行时进行此优化,但不能保证。) - 就此而言,您根本不需要
split
;您可以将所有 SVG 命令放入一个数组中,这样的额外好处是更易于阅读。 - 中断不是偶然发生的。如果您的线程被打断,则意味着有人明确要求您停止您正在做的事情并优雅地退出。忽略中断意味着您的线程是无法停止的流氓线程。在大多数情况下,最好的做法是将整个方法体放在 try/catch 中,这样如果被中断它会自动退出。
所以,考虑到以上所有内容,您想要这样的东西:
public void renderObject(GraphicsContext playGraphics, Canvas toUpdate)
{
if (!Platform.isFxApplicationThread())
{
throw new IllegalStateException(
"Must be called in JavaFX application thread");
}
String[] toAppend = {
"M 215 256", "L 215 256", "L 215 256", "L 215 256",
"L 225 241", "L 234 231", "L 246 223", "L 266 214",
"L 284 208", "L 309 204", "L 340 200", "L 378 199",
"L 416 199", "L 444 199", "L 473 203", "L 485 206",
"L 496 211", "L 506 218", "L 510 224", "L 513 233",
"L 515 243", "L 516 257", "L 512 270", "L 502 285",
"L 493 298", "L 483 308", "L 476 315", "L 472 318",
"L 469 320", "L 468 320", "L 468 320", "L 468 320",
"L 468 317", "L 472 309", "L 480 300", "L 492 293",
"L 510 287", "L 535 283", "L 557 282", "L 580 283",
"L 593 287", "L 607 295", "L 623 311", "L 634 333",
"L 640 355", "L 642 396", "L 639 430", "L 624 467",
"L 602 508", "L 582 536", "L 557 563", "L 524 585",
"L 490 602", "L 464 611", "L 432 619", "L 420 621",
"L 404 622", "L 393 622", "L 383 621", "L 376 620",
"L 372 618", "L 365 610", "L 360 598", "L 358 578",
"L 357 554", "L 361 514", "L 371 493", "L 386 463",
"L 412 422", "L 432 395", "L 456 362", "L 480 329",
"L 506 299", "L 533 271", "L 560 247", "L 600 213",
"L 620 194", "L 626 189", "L 629 184", "L 630 182",
"L 632 178"
};
// If the SVG string is not hard-coded, create the array here:
// String[] toAppend = svgString.split(";");
Runnable pathBuilder = () -> {
try
{
StringBuilder path = new StringBuilder();
for (String segment : toAppend)
{
Thread.sleep(100);
path.append(" ").append(segment);
String pathToDraw = path.toString();
Platform.runLater(() -> {
playGraphics.beginPath();
playGraphics.appendSVGPath(pathToDraw);
playGraphics.stroke();
});
}
}
catch (InterruptedException e)
{
// Someone wants us to exit, so fall through and return.
e.printStackTrace();
}
};
Thread pathBuildingThread = new Thread(pathBuilder);
pathBuildingThread.setDaemon(true);
pathBuildingThread.start();
}