【问题标题】:paintComponent() and actionPerformed() get out of sync for JPanel iplements ActionListener由于 JPanel 实现 ActionListener,paintComponent() 和 actionPerformed() 不同步
【发布时间】:2012-09-21 10:51:21
【问题描述】:

我正在尝试制作一个运行动画的应用程序。为此,我有一个 Jframe,其中包含我的 Jpanel 子类,动画在其中运行。这是我的两个课程:

首先,这是我的驱动程序类:

import javax.swing.*;

public class Life {
    public static void main(String[] args){
         JFrame game = new JFrame("Life");

         game.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
         game.setSize(500, 500);

         MainPanel mainPanel = new MainPanel();
         game.setContentPane(mainPanel);


         game.setVisible(true);

     }
 }

其次,这是我的 Jpanel 子类:

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class MainPanel extends JPanel implements ActionListener{
    int i = 0;
    int j = 0;
    public MainPanel(){
        super();
    }

    public void paintComponent(Graphics g){
        j++;
        g.drawLine(10,10, 20 + i, 20 + i);
        Timer t = new Timer(1000, this);
        t.start();
    }

    @Override
    public void actionPerformed(ActionEvent actionEvent) {
        i++;
        repaint();
    }
}

请注意,每次调用 actionPreformed 时变量 i 都会递增,每次调用 paintComponent 时都会调用变量 j。最终发生的事情是 i 开始比 j 大得多,而由paintComponent 绘制的线似乎以越来越快的速度增长。

这是我的问题:

  • 为什么会发生这种情况?
  • 如何进行同步,以便每 1000 毫秒重绘一次?
  • 鉴于我正在尝试做的事情,我的方法是错误的吗?我应该以不同的方式做事吗?

提前致谢。

【问题讨论】:

    标签: java swing concurrency timer jpanel


    【解决方案1】:

    不要从 paintComponent 方法中启动 Swing Timer。

    这种方法应该做你的画,只有你的画,只有画。它应该包含绝对no 程序逻辑。了解您对此方法的控制非常有限,因为您无法预测何时或是否会调用它,以及调用它的频率。您甚至不能自己调用​​它,或者保证当您建议通过repaint() 调用它时,它实际上会被调用。

    此外,此方法必须快速,尽可能快,因为任何减慢它的东西,无论是对象创建还是读取文件都会降低你的 GUI 的感知响应能力,这是你最不想看到的事情发生。

    解决方案是将程序逻辑从该方法中分离出来,并分成更好的方法,例如您的构造函数。重复代码应该在 Swing Timer 中。

    编辑:
    你说:

    我这样做只是为了测试一下。还有一个问题:如果paintComponent 或paintComponent 中的工作所依赖的某个线程花费超过1000 毫秒(或其他任何时间)来完成其工作,会发生什么?我唯一能想到的就是让paintComponent 绘制到目前为止的动画进度,而不是等待动画到达下一步(如果这有意义的话)。想法?

    您应该永远不要在paintComponent 中包含需要那么长甚至10 毫秒的代码。如果有发生类似情况的风险,则在后台线程中绘制到 BufferedImage,然后在 Swing 事件线程上使用 Graphics#drawImage(...) 方法在 paintComponent 方法中显示 BufferedImage。

    【讨论】:

    • 在构造函数中启动计时器似乎效果更好。这听起来像是正确的方法吗?
    • @David:也不要在paintComponent 内部增加j(这是程序逻辑代码),而是在Timer 的ActionListener 中增加。此外,您似乎从来没有使用 j 变量做任何事情。
    • 我只是为了测试一下。还有一个问题:如果paintComponent 或paintComponent 中的工作所依赖的某个线程花费超过1000 毫秒(或其他任何时间)来完成其工作,会发生什么?我唯一能想到的就是让paintComponent 绘制到目前为止的动画进度,而不是等待动画到达下一步(如果这有意义的话)。想法?
    【解决方案2】:

    对@HFoE 的基本见解的一些小补充:

    • 公共start() 方法是一种方便的方法,可确保在开始之前完全构建视图。
    • 字段有明确定义的default values,它们有should be private
    • Swing GUI 对象应event dispatch thread 上构造和操作。
    • 覆盖getPreferredSize()pack() 封闭的Window

    修改后的代码:

    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Graphics;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import javax.swing.*;
    
    public class Life {
    
        public static void main(String[] args) {
            EventQueue.invokeLater(new Runnable() {
    
                private final JTabbedPane jtp = new JTabbedPane();
    
                @Override
                public void run() {
                    JFrame game = new JFrame("Life");
                    game.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    MainPanel mainPanel = new MainPanel();
                    game.setContentPane(mainPanel);
                    game.pack();
                    game.setVisible(true);
                    mainPanel.start();
                }
            });
        }
    
        private static class MainPanel extends JPanel implements ActionListener {
    
            private Timer t = new Timer(100, this);
            private int i;
            private int j;
    
            @Override
            public void paintComponent(Graphics g) {
                g.drawLine(10, 10, 20 + i, 20 + i);
            }
    
            public void start() {
                t.start();
            }
    
            @Override
            public void actionPerformed(ActionEvent actionEvent) {
                i++;
                repaint();
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(500, 500);
            }
        }
    }
    

    【讨论】:

      【解决方案3】:

      默认情况下,Timer 会继续运行。只有当你调用setRepeats( false ) 时它才会停止。

      所以下面几行

      Timer t = new Timer(1000, this);
      t.start();
      

      在您的paintComponent 方法中意味着在几次重绘后,您将运行许多Timer 实例,这解释了为什么i 的增长速度比j 增长得快。

      解决方案当然是将Timer 移到paintComponent 方法之外,并坚持一个Timer 实例。

      进一步的评论(其他人没有说,不再重复他们非常有用的建议):

      • 在不调用super 方法的情况下,切勿覆盖paintComponent 方法
      • 您不应该公开ActionListener 接口。只需在内部使用ActionListener

      【讨论】:

      • 感谢您的回复。两个问题: 1) 为什么我不应该公开 ActionListner 接口? 2) 我如何在内部使用 ActionListener?
      • @David 1) 这是一个实现细节。您的课程不打算用作ActionListener,因此无需公开它2)例如通过使用匿名类或内部类
      猜你喜欢
      • 2017-06-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-29
      • 1970-01-01
      相关资源
      最近更新 更多