renderHanoi() 方法应该移动磁盘,方法是从 VBox 中清除磁盘,然后在每次移动后以新顺序再次添加它们,但似乎没有显示任何内容,除非它是最后一个移动,这使得一切都毫无意义。
我尝试了不同的创建延迟的方法,如 Thread.sleep、Platform.runLater 等。它们似乎都不起作用。我该如何解决这个问题?
import java.util.Arrays;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class App extends Application {
@Override
public void start(Stage stage) {
HBox platform = new HBox();
VBox[] towerBoxes = new VBox[] { new VBox(), new VBox(), new VBox()};
platform.getChildren().addAll(Arrays.asList(towerBoxes));
Hanoi testing = new Hanoi(10);
testing.towerBoxes = towerBoxes;
var scene = new Scene(platform, 640, 480);
stage.setScene(scene);
stage.show();
testing.solve();
}
public static void main(String[] args) {
launch();
}
}
class Tower {
private int sp = 0;
private Rectangle[] disks;
Tower(int n) {
disks = new Rectangle[n];
}
public void push(Rectangle entry) {
if (sp < disks.length)
disks[sp++] = entry;
else
System.err.println(this + ".push(" + entry + ") failed, stack is full");
}
public Rectangle pop() {
if (sp > 0)
return disks[--sp];
else {
System.err.println(this + ".pop() failed, stack is empty");
return null;
}
}
public boolean hasEntry() {
return sp > 0;
}
@Override
public Tower clone() {
Tower copy = new Tower(disks.length);
copy.sp = this.sp;
copy.disks = this.disks.clone();
return copy;
}
}
class Hanoi {
Tower src;
Tower aux;
Tower dest;
int n;
public VBox[] towerBoxes;
public Hanoi(int n) {
src = new Tower(n);
aux = new Tower(n);
dest = new Tower(n);
this.n = n;
for (int i = 0; i < n; i++) {
Rectangle disk = new Rectangle(30 + 20 * i, 10);
Color diskColor = generateRandomColor();
disk.setFill(diskColor);
disk.setStroke(diskColor);
src.push(disk);
}
}
private static Color generateRandomColor() {
Random random = new Random();
double red = random.nextDouble();
double green = random.nextDouble();
double blue = random.nextDouble();
return new Color(red, green, blue, 1.0);
}
private void solve(int n, Tower src, Tower aux, Tower dest) {
if (n < 1) {
return;
}
solve(n-1, src, dest, aux);
dest.push(src.pop());
System.out.println(n);
solve(n-1, aux, src, dest);
}
public void solve() {
renderHanoi();
timer.start();
solve(n, src, aux, dest);
}
AnimationTimer timer = new AnimationTimer() {
@Override
public void handle(long now) {
renderHanoi(); // Update UI after each frame
}
};
private void renderHanoi() {
for (VBox towerBox:towerBoxes)
towerBox.getChildren().clear();
Tower[] towersCopy = new Tower[]{src.clone(), aux.clone(), dest.clone()};
for (int i = 0; i < 3; i++)
while (towersCopy[i].hasEntry())
towerBoxes[i].getChildren().add(towersCopy[i].pop());
}
}
问题是您的solve()方法在不到一秒的时间内返回。它发生得如此之快,以至于没有什么可以动画化的。
你走在正确的轨道上。我们希望
solve
在不同的线程中运行该方法,并调用 Thread.sleep,因此它不会运行得太快。我们不能也绝对不能在JavaFX应用程序线程(调用的线程start
和所有事件处理程序)中调用Thread.sleep,因为这将导致所有事件处理被延迟,包括窗口的绘制以及鼠标和键盘输入的处理。首先,让我们添加 Thread.sleep、对 Platform.runLater 的一些调用来更新窗口,以及一个安全地完成这一切的 Thread:
我们仍然有一个问题:在一个线程中对变量或字段进行的更改不能保证在其他线程中看到。 Java 语言规范的这一部分对此进行了简洁的解释:
Java 有很多方法可以安全地处理多线程访问。
synchronized
在这种情况下,在Tower类和final
Hanoi类中使用就足够了。因此我们将 Tower 的方法声明更改为如下所示:
这保证了方法中对 Tower 字段所做的更改
solve
对于 JavaFX 应用程序线程是可见的。Hanoi 类本身也在不同线程中引用 src、aux 和 dest。我们可以
synchronized
在这里使用,但将这些字段设为最终更简单:当从多个线程访问字段时, Java 可以对字段做出很多安全的假设
final
,因为无需担心多个线程无法看到这些字段的更改。[TLDR:这是一个不需要线程的解决方案]
正如@VGR 的优秀答案所指出的,问题的出现是因为解决塔问题几乎是立即发生的。因此,一旦你尝试渲染它,它就已经解决了。如果您尝试通过在每次移动之间设置暂停来减慢速度,那么您必须在单独的线程上运行求解代码。上面引用的答案展示了如何做到这一点。
我坚信在不必要的情况下不要使用多线程:最重要的是,因为它使代码更难以正确编写,而且(尽管这不太重要)因为它会消耗额外的系统资源。@VGR 的答案通过线程正确地实现了这一点,但是当您跨多个线程共享数据时,要知道代码是正确的,需要比单线程解决方案更高级的编程。
我还坚信将应用程序中的数据与数据的表示(视图)分开。
这是对分离数据的代码的轻微修改。首先,塔的表示,本质上是一个整数列表,表示塔上有哪些磁盘。还有一个枚举来确定它是哪座塔:
TowerID.java
塔楼.java
谜题的表现只需要三座塔,以及从一个塔移动到另一个塔的方法。事实证明,将一个动作封装为一个单独的对象(这里只是一个简单的记录)会很有用:
移动.java
进而
河内.java
请注意,这里没有解决这个难题的方法。解决这个谜题仅仅意味着在给定谜题中的圆盘数量的情况下提出一系列的动作。这是与 OP 中相同的算法,分解为一个单独的求解器类:
HanoiSolver.java
请注意,我们现在可以在没有 UI 的情况下解决河内塔难题:
我们编写的 API 还允许我们在解决方案中每次移动后检查状态:
要为谜题制作 UI,我们需要一些 UI 类来可视化塔:
TowerView.java:
对于整个事情:
河内视图.java:
To visualize a solution in an animation, we can get a list of the moves from the solver, create a timeline with a keyframe for each move, and in each keyframe update the model with each move and rerender the view:
Here is this animation assembled into an application, with a few controls added:
Note we don't use any built-in observability here. We could create the
Tower
implementation with anObservableList
for the disks and have the view class observe the list, in order to avoid re-rendering the views "by hand" in each frame.