3

我有一堂像这样的课

public class BlockSpawner implements Runnable{

public static long timeToSpawn;
private GtrisJFrame frame;

public BlockSpawner(GtrisJFrame frame)
{

    this.frame = frame;
    timeToSpawn = 2000;
}

public void run()
{
    while(true)
    {
        try
        {
            Thread.sleep(timeToSpawn);
        }
        catch(InterruptedException e)
        {
            //Unhandled exception
        }

        //After awake, instanciate 2 blocks
        //get the position of the first one
        int index = Block.getRandomStartPosition();
        new Block(frame, index);
        new Block(frame, index+1);
    }
}

}

我在 JFrame 主类中实例化这个类,并像这样启动它的线程:

private void initBlockSpawner()
{
    spawner = new BlockSpawner(this);
    new Thread(spawner).start();
}

我在 JFrame 构造函数中调用了这个 initBlockSpawner() 函数。Block Class 确实有点大,但是简单来说,它实现了runnable,在其构造函数的末尾调用了它的run() 方法。run() 方法只会使块以一定的速度下落。我曾尝试在 JFrame 构造函数中手动实例化新块,它们可以工作,它们会重新绘制并掉落。但是每当我想从其他线程实例化块时,它们似乎会下降(即它的属性会更新每个循环),但它们不会在 JFrame 中绘制。

作为附加信息,我使用的是 NetBeans,并且由于应用程序入口点位于 JFrame 类上,因此 main 方法如下所示:

public static void main(String args[])
{
    java.awt.EventQueue.invokeLater(new Runnable() {
        public void run() {
            new GtrisJFrame().setVisible(true);
        }
    });

}

我在 Java 线程、awt 事件和 swing 组件方面没有太多经验。但是我在这里读到的东西让我觉得我的问题是只有一个线程可以控制摆动组件,或者什么......有什么办法可以解决我的问题吗?

提前致谢。

编辑:附加信息,每当我从线程检查实例化多维数据集上的 toString 方法时,它们都会给我这个 [,0,0,0x0],但是当我在同一个 JFrame 类中实例化它们时,它们会给我这个结果 [,0, 0,328x552],它们出现在框架上。这个 328x552 值与 getPreferredSize() 返回的组件维度相同...我试图通过像这样实例化它们来强制它们达到该维度:

new Block(this, index).setPreferredSize(new Dimension(328, 552));

但它没有用,任何人都知道这个 [,0,0,328x552] 值可能意味着什么?

谢谢大家,我想我们快到了!

编辑 2:我意识到组件的大小是 x:0 y:0,这是为什么呢?我将 BlockSpawner 的 run() 方法更改为如下内容:

public void run()
{
    while(true)
    {
        System.out.println("SPAWN");
        int index = Block.getRandomStartPosition();
        new Thread(new Block(frame, index)).start();
        new Thread(new Block(frame, index+1)).start();

        try
        {
            Thread.sleep(timeToSpawn);
        }
        catch(InterruptedException e)
        {
            //Unhandled exception
        }
    }
}

第一次运行,一切正常!甚至这对块在 JFrame 上绘制并正确落下,但是在 Thread.sleep() 之后,其余的只是被实例化,但是它们的 getSize() 方法给了我 x:0 y:0; 这仍然与 One Dispatcher Thread 问题有关吗?还是现在不同了?

4

3 回答 3

4

在我看来(尽管我无法从您上面的代码中看出)您正在尝试从事件调度以外的线程将组件添加到实时 JFrame(即,已在屏幕上显示或“实现”的组件)线。这违反了 Swing 线程模型,并且会给您带来无穷无尽的问题。

如果要从不同的线程对 Swing 对象进行更改,请将更改打包到 Runnable 中,然后使用 EventQueue.invokeLater() 或 invokeAndWait() 将其提交给调度线程。

编辑:更多信息

一些额外的评论(与您的问题没有直接关系,但很重要): 在构造函数中执行活动可能不是一个好主意。子类化 JFrame 以向其中添加组件也可能不是一个好主意。就此而言,在 JFrame 而不是 JPanel 中执行这些操作可能也不是最好的方法。

依次采取这些:

  1. 构造函数应该用于对对象执行初始配置——而不是调用行为。这种分离有助于保持您的设计清洁和可维护。尽管这样做似乎更容易,但我建议不要这样做。在您的设计中的某个时刻,您可能会决定提前创建这些对象并仅在以后使用它们会更有效。

  2. 子类化一个 JFrame 来添加组件通常不是一个热门的想法。为什么?如果您决定要使用具有您想要的其他行为的专用 JFrame,该怎么办?如果您决定使用为提供 JFrame 的应用程序框架怎么办(这在框架可能想要跟踪窗口关闭事件以便它可以保存窗口大小和位置的情况下是典型的)。无论如何 - 大量的原因。将您的行为打包到与 GUI 无关的类中,并使用它将行为注入 JFrame(或 JPanel)。

  3. 考虑使用 JPanel 而不是 JFrame。如果需要,您可以随时将 JPanel 添加到 JFrame。如果您直接使用 JFrame,那么当您决定要将其中两个面板并排放置在一个容器中时会发生什么?

所以,我建议你做更多的事情:

BlockAnimator animator = new BlockAnimator();
DispatchThread.invokeLater( 
  new Runnable(){
    public void run(){
      JPanel blockAnimationPanel = new JPanel();
      Block block = new Block(...);
      blockAnimationPanel.add(block);
      JFrame mainFrame = new JFrame();
      mainFrame.add(blockAnimationPanel);
      animator.start(); // note that we probably should start the thread *after* the panel is realized - but we don't really have to.
    }
  }

public class BlockAnimator extends Thread{
  private final List<Block> blocks = new CopyOnWriteArray<Block>(); // either this, or synchronize adds to the list
  public void addBlock(Block block){
    blocks.add(block);
  }
  public void run(){
    while(true){ // either put in a cancel check boolean, or mark the thread as daemon!
      DispatchThread.invokeAndWait(
        new Runnable(){
          public void run(){
            for(Block block: blocks){
              block.moveTo(....); // do whatever you have to do to move the block
            }
          }
        }
      ); // I may have missed the brace/paren count on this, but you get the idea
      spawnNewBlockObjects();
      Thread.sleep(50);
    }
  }
}

上面的代码尚未检查准确性等...

从理论上讲,您可以有一个单独的线程来生成新块,但上面的内容非常简单。如果您确实决定像我在上面显示的那样使用单个后台线程来实现,您可以使用简单的 ArrayList 作为块列表,因为该对象不会有竞争条件。

对此还有一些其他的想法:

  1. 在上面,块动画师可以独立于块本身进行管理。例如,您可以添加一个暂停所有块的 pause() 方法。

  2. 我有在同一个调度线程调用中发生的所有块的动画更新。根据动画的成本,在后台线程上计算新坐标可能会更好,并且只在 EDT 上发布实际位置更新。您可以选择为每个块更新发出单独的 invokeAndWait(或可能使用 invokeLater)。这真的取决于你所做的事情的性质。

如果将块移动到何处的计算很粗糙,请考虑将计算与实际移动分开。因此,您将有一个调用来获取对象的新 Point,然后再调用另一个调用来实际执行移动。

于 2010-02-20T05:46:46.877 回答
2

Swing 不支持多线程,因此当您需要与它交互时,您需要从 AWT 事件线程中进行操作。

这就是 netbeans 添加的 main() 方法中发生的情况。 java.awt.EventQueue.invokeLater安排在 AWT 事件队列上执行的 runnable。

通常你可以对你的 BlockSpawner Runnable 做同样的事情,但是因为你需要延迟,所以 sleep() 会阻塞事件队列并导致用户输入出现问题/延迟。

要解决这个问题,我建议您使用SwingWorker,它允许在后台执行任务,然后在完成后与事件队列重新同步。

在您的情况下,您应该在 doInBackground() 方法中执行 sleep(),然后在 done() 方法中创建新组件。

于 2010-02-20T05:53:02.047 回答
0

我的其他答案的替代方法是使用javax.swing.Timer

这提供了以指定速率安排在事件调度线程上发生的操作的能力,并且不需要 Java 6

您可以使用以下代码安排您的 BlockSpawner:

  int timeToSpawn = 2000;

  ActionListener blockSpawner = new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
          int index = Block.getRandomStartPosition();
          new Block(frame, index);
          new Block(frame, index+1);
      }
  };
  new Timer(timeToSpawn, blockSpawner).start();

这可能是最简单的解决方案,因为它不需要额外的线程。只要确保在 javax.swing 而不是 java.util 中使用 Timer 类,否则您可能不会在事件调度线程上执行。

于 2010-02-20T22:20:09.030 回答