在Java中使用GUI时,我注意到repaint()
类的Component
方法表现出异常的行为。下面的代码在打印1
之后和2
之前更新了内容窗格,这大概是我的意图。
public class ErrorTest {
public static void main(String[] args) throws InterruptedException {
JFrame F = new JFrame();
F.setSize(100,100);
Panel P = new Panel();
F.setContentPane(P);
F.setVisible(true);
while(true) {
System.out.println("\n1");
P.repaint();
System.out.println("2");
Thread.sleep(100);
}
}
}
class Panel extends JPanel {
@Override
protected void paintComponent(Graphics G) {
System.out.println("painted");
G.drawOval(10, 10, 20, 30);
}
}
如果包含Thread.sleep(100);
,则此代码的输出为:
1
2
painted
1
2
painted
,依此类推。如果不是,则输出为:
1
2
1
2
painted
,依此类推,通常只打印1
和2
,很少在随机位置打印painted
。我正在寻找的输出是:
1
painted
2
1
painted
2
重复。似乎是在每次迭代的开始都调用此update()
方法的情况,无论我实际写在哪里。在执行该方法之前,似乎还有些延迟。我该怎么做才能达到期望的输出?
答案 0 :(得分:1)
TL; DR:您想要的是不可能的。
有一个叫做“ EDT”的东西-事件调度线程。摆动(和大多数UI库)的工作是只有一个线程(EDT),并且系统通过将事件注入队列来工作。线程不断将最上面的事件从队列中移出并处理。
用户按下按钮?调用该按钮上定义的任何动作侦听器的工作都被放入队列中。这意味着事件侦听器中的所有代码都在EDT中运行。
该电话吗?它根本不直接调用paintComponent。它在队列中注入一个事件以进行重新绘制,然后在EDT中完成重新绘制,然后它将调用paintComponent。
(swing和其他框架的规则是,您不能从除EDT之外的任何线程中编辑任何小部件,请参见swing的文档。)
换句话说,您的主要方法是一个线程,而EDT是另一个线程,并且repaint()
调用正在从一个线程传递到另一个线程:请运行此事件,然后继续。
EDT的另一个规则是,它绝不能“阻塞”(在磁盘,网络或其他线程上等待,或由于任何原因暂停执行)。如果您确实阻止了EDT,则该应用看起来完全没有响应,并且操作系统很快就会弹出一条通知,通知该应用似乎已崩溃。这是因为EDT还处理了各种GUI交互,例如将鼠标光标悬停在文本框上时将鼠标光标更新为不同的形状,因此如果EDT被冻结,则所有这些都不起作用。
因此,我们得出的结论是:您想要做的事无法完成-获得可靠的“在此之前发生”的唯一方法,其中“这”是一个线程中的工作,而“那”是另一个线程中的工作,具有锁定功能,这会导致冻结,这在EDT *中是不可接受的。
但是,没有人会发布一个应用程序,该应用程序“在调用重绘后继续之前先调用paint方法”。显然,您有要完成的工作,您认为将repaint()和paint()同步是到达那里的方法,现在您在问有关第二件事的问题。但这是错误的策略-有一个答案,但是这种“同步绘画和重画调用”是死胡同。
然后,在需要绘制组件的任何时候调用paintComponent方法。像在另一个应用程序的窗口上拖动另一个应用程序的窗口一样简单,将导致事件发生,这些事件将调用paintComponent来完成此工作。您无法控制此类操作。因此,即使您挥舞着魔杖并完成了不可能的事情(同步repaint()和paintComponent()调用),您仍然会发现一堆根本不是由repaint()引起的paintComponent调用。
*)您可以让调用repaint()的主线程在EDT上等待至少完成一次带油漆的绘画,我想这是一个闩锁,但这听起来不是一个好的计划,您无法保证当重新绘制实际上发生时,并且您不知道是否确实调用了paintComponent是因为您调用了重新绘制,或者是因为操作系统确定是时候询问您的应用程序它想要显示哪些像素了。