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

Posted

技术标签:

【中文标题】来自线程内的实例化组件不会重绘到 Java 中的 JFrame【英文标题】:Instantiated components from within a thread aren't repainting into a JFrame in Java 【发布时间】:2011-01-19 01:27:26 【问题描述】:

我有一个像这样的课程

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 组件方面没有太多经验。但是我读到here 的东西让我觉得我的问题是只有一个线程可以控制摆动组件,或者什么......有什么办法可以解决我的问题吗?

提前致谢。

编辑:附加信息,每当我从线程检查实例化多维数据集上的 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 问题有关吗?还是现在不一样了?

【问题讨论】:

【参考方案1】:

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

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

编辑:更多信息

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

依次处理:

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

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

    考虑使用 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 作为块列表,因为该对象不会有竞争条件。

对此的一些其他想法:

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

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

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

【讨论】:

非常感谢您的建议凯文,我已经尝试过这种方法,但是我在子线程上使用了 InvokeLater 而不是 InvokeAndWait……但我得到了相同的结果:组件没有绘制;我明天用 InvokeAndWait 试试;如果即使这样,组件也不会绘制......我将使用 Aaron 所说的 SwingWorker 类。再次感谢。 我敢打赌,您仍在从非调度线程添加组件。在您了解线程做错了什么之前,Swing 工作人员不会在这里提供帮助。对于你正在做的事情,我真的不认为摇摆工人是正确的工具。【参考方案2】:

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

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

通常您可以对 BlockSpawner Runnable 执行相同的操作,但因为您需要延迟,所以 sleep() 会阻塞事件队列并导致用户输入出现问题/延迟。

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

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

【讨论】:

哇,感谢Aaron的解释,它真的总结了我的问题。哎哟!但是我在这里有一个限制……嗯,有点:除了标准 Java API,我不能使用任何东西; SwingWorker 不包含在 Java 版本中 :( 还有其他想法吗? @Rigo SwingWorker 自版本 6 以来已正式成为 Java 的一部分。如果您使用的是早期版本且无法升级,您将需要使用带有额外线程的 invokeLater(),如下 Kevin Days 回答所建议. 嘿,这是真的!问题是我使用的是 JDK 5,我现在更改为版本 6。我将尝试实现 SwingWorker(明天......现在是凌晨 3 点)......谢谢大家!【参考方案3】:

我的其他答案的替代方法是使用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 ,否则您可能不会在事件调度线程上执行。

【讨论】:

是否需要先将此 ActionListener 添加到 JFrame 内的组件中?

以上是关于来自线程内的实例化组件不会重绘到 Java 中的 JFrame的主要内容,如果未能解决你的问题,请参考以下文章

实例方法内的私有变量为什么不会出现线程安全问题

Java并发组件二之CyclicBarriar

线程“主”java.lang.IllegalArgumentException 中的异常:无法实例化接口 org.springframework.context.ApplicationListener

.map函数内的子组件显示所有子实例[重复]

Java - 从单独的组件中重绘组件

Android 插件化Hook 插件化框架 ( Hook Activity 启动流程 | 主线程创建 Activity 实例之前使用插件 Activity 类替换占位的组件 )