【问题标题】:Rendering with a timer and paintComponent is causing no rendering to happen使用计时器和 paintComponent 进行渲染会导致没有渲染发生
【发布时间】:2019-09-07 22:09:12
【问题描述】:

我一直在尝试开发一个游戏,我正处于让所有单独组件工作的初步阶段(因此我的代码很糟糕),在尝试渲染一个移动的正方形时,我遇到了一个问题。 paintComponent 方法会绘制正方形,但是当我使用计时器时,我无法让它工作。

我尝试移动计时器方法,在不同点使用重绘,并在某一点报废所有内容并从头开始重做。

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.time.Clock;
import java.time.Duration;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JFrame;

public class InputTest extends MyPanel implements KeyListener {
    private static int x;
    private static int y;

    public static void main(String[] args) {
        JFrame f = new JFrame();
        InputTest p = new InputTest();
        f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        f.setSize(500, 500);
        p.setSize(500, 500);
        f.add(p);
        f.addKeyListener(new InputTest());
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                x++;
                y++;
                if (x > 500) {
                    x = 0;
                }
                if (y > 500) {
                    y = 0;
                }
                System.out.println("Tick"+x+","+y);

            }
        }, 0, 500);

        p.setVisible(true);
        f.setVisible(true);
    }

    @Override

    public void keyPressed(KeyEvent e) {

    }

    @Override
    public void keyReleased(KeyEvent e) {
        // TODO Auto-generated method stub

    }

    @Override
    public void keyTyped(KeyEvent e) {
        // TODO Auto-generated method stub

    }

    public void update() {

    }

    @Override
    protected void paintComponent(Graphics g) {
        g.setColor(Color.BLACK);
        while(x!=501&&y!=501) {
            g.fillRect(x, y, 30, 30);
            repaint();
        }
    }
}

方块应该从左上角缓慢移动到右下角。相反,我没有得到任何图像。

【问题讨论】:

  • paintComponent 内部有一个无限循环不是一个好主意。它将阻塞整个线程,awt 应用程序需要这些线程才能工作。相反,您可以删除paintComponent 中的循环并在计时器中调用repaint(),以便在更新坐标时重新绘制面板。

标签: java swing graphics


【解决方案1】:

Swing 是单线程的

当 Swing 触发绘制循环时,直到层次结构中的所有组件都完成操作后,结果才会呈现到屏幕上。

这意味着当你做类似...

@Override
protected void paintComponent(Graphics g) {
    g.setColor(Color.BLACK);
    while (x != 501 && y != 501) {
        g.fillRect(x, y, 30, 30);
        repaint();
    }
}

在您退出 paintComponent 方法之前,什么都不会发生,这意味着,在这种情况下,您绘制的最后一件事就是将要渲染的内容(或者这是一个很好的连续性)。

附带说明,您永远不应该直接或间接地从绘制方法中调用repaint,这是消耗所有 CPU 周期并使您的系统崩溃的真正好方法。

相反,绘画应该简单地描绘当前/期望的状态。此外,除非您真的知道自己在做什么,否则您应该始终先致电super.paintComponent,例如...

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(Color.BLACK);
    g.fillRect(x, y, 30, 30);
}

详情请见Concurrency in Swing

Swing 不是线程安全的

您应该始终避免从事件调度线程之外更新 UI。这还包括更新 UI 所依赖的状态。这样做会导致各种难以诊断的故障。

因为java.util.Timer 使用它自己的Thread 来执行它的调度,所以它不适合在Swing 中使用。相反,您应该使用javax.swing.Timer,它将在 EDT 外等待,但会触发它分配给 EDT 上下文的 ActionListener

例如...

Timer timer = new Timer(500, new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        x++;
        y++;
        if (x > 500) {
            x = 0;
        }
        if (y > 500) {
            y = 0;
        }

        // Trigger a new paint cycle...
        repaint();
    }
});
timer.start();

详情请见How to Use Swing Timers

所有“其他”的东西

KeyListener 最好避免使用,原因有很多。使用Key Bindings API,它会为您节省很多头发拉扯。

组件的可用大小总是窗口的大小减去它的边框装饰。这意味着使用f.setSize(500, 500) 将使可用的上下文区域更小(取决于您运行代码的平台)。

最好覆盖组件的getPreferredSize 方法并返回大小提示。然后,您可以使用JFrame#pack 将窗口“打包”在内容周围,使窗口大于内容大小,但您不会浪费时间试图弄清楚为什么事情不会在您期望的地方停止.

可运行示例

我还修改了代码以消除对static 的依赖,这绝不是一个好主意

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class InputTest extends JPanel {

    private int x;
    private int y;

    public static void main(String[] args) {
        JFrame f = new JFrame();
        InputTest p = new InputTest();
        f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        f.add(p);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    public InputTest() {
        Timer timer = new Timer(500, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                x++;
                y++;
                if (x > 500) {
                    x = 0;
                }
                if (y > 500) {
                    y = 0;
                }

                // Trigger a new paint cycle...
                repaint();
            }
        });
        timer.start();
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(500, 500);
    }

    @Override
    protected void paintComponent(Graphics g) {
        g.setColor(Color.BLACK);
        g.fillRect(x, y, 30, 30);
    }
}

【讨论】:

  • 这是一个非常彻底和非混蛋的答案,这在我的堆栈交换经验中非常罕见。感谢您抽出时间成为新手程序员的导师 :) 我不知道这个替代计时器,但它似乎比我使用的那个更容易理解!跨度>
【解决方案2】:

您应该允许您的paintComponent 方法绘制当前状态并完成。在paintComponent 中绘制多个帧并不是一个好主意。它将阻塞整个线程,awt 应用程序需要这些线程才能工作。相反,您可以删除paintComponent 中的循环并在计时器中调用repaint(),以便在更新坐标时根据需要重新绘制面板。

类似这样的:

... 
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            x++;
            y++;
            if (x > 500) {
                x = 0;
            }
            if (y > 500) {
                y = 0;
            }
            repaint();
            System.out.println("Tick"+x+","+y);

        }
    }, 0, 500);

...

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(Color.BLACK);
    g.fillRect(x, y, 30, 30);
}

【讨论】:

    猜你喜欢
    • 2019-12-16
    • 1970-01-01
    • 2022-01-14
    • 2021-01-17
    • 2020-11-15
    • 2022-08-14
    • 2021-12-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多