线程内的实例化组件不会重新绘制到Java中的JFrame中

时间:2010-02-20 05:24:43

标签: java multithreading components jframe paint

我有一个类似这样的课程

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构造函数中手动实例化新块,它们可以工作,它们会重新渲染并掉线。但每当我想从其他线程实例化Blocks时,它们似乎都会掉落(即它的属性会更新每个循环),但它们不会在JFrame中绘制。

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

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

}

我对Java Threads,awt事件和swing组件没有太多经验。但我读到的here让我觉得我的问题是只有一个线程可以控制swing组件,或者其他东西......有什么方法可以解决我的问题吗?

提前致谢。

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

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问题有某种关系吗?或者它现在有所不同?

3 个答案:

答案 0 :(得分:4)

对我来说(虽然我从上面的代码中无法分辨),您试图将组件添加到实时JFrame(即已在屏幕上显示或已实现'的组件)事件派发线程。这违反了Swing线程模型,并且会让您无法解决问题。

如果要从其他线程更改Swing对象,可以将更改打包在Runnable中,并使用EventQueue.invokeLater()或invokeAndWait()将其提交到调度线程。

编辑:更多信息

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

依次采取这些:

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

  2. 对JFrame进行子类化以添加组件通常不是一个好主意。为什么?如果您决定要使用具有其他所需行为的专用JFrame,该怎么办?如果您决定使用提供 you 与JFrame的应用程序框架(在框架可能希望跟踪窗口关闭事件以便它可以保存窗口大小和位置的情况下),这会怎么样?无论如何 - 很多原因。将您的行为打包到非GUI相关类中,并使用它将行为注入JFrame(或JPanel)。

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

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

    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)。这实际上取决于你正在做的事情的性质。

    3. 如果计算移动块的位置是粗糙的,请考虑将计算与实际移动分开。所以你有一个调用会获得对象的新Point,然后是另一个实际上会移动的调用。

答案 1 :(得分:2)

Swing不支持多线程,因此无论何时需要与它进行交互,都需要从AWT事件线程中进行。

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

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

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

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

答案 2 :(得分: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中使用Timer类而不是java.util,否则您可能无法在事件派发线程上执行。