在JFrame

时间:2016-12-08 19:40:38

标签: java swing

我有一个创建JFrame的类,在其上会播放一个简单的俄罗斯方块游戏,我也有一个类DrawSquare,它完全按照你的想法做,但是当我初始化DrawSquare类的一个新实例时然后尝试绘制那个和所有其他的东西到我的JFrame开始出错了,代码用于在左上角绘制一个正方形,然后一次放下一行直到它到达底部框架(它这样做),然后应该在框架顶部的第二列中绘制一个新的正方形,以及左下角的第一个正方形,但是一旦它开始下降第二列我得到一系列正方形画在右上角的对角线上。目前,我计划要做的代码是从每列的顶行开始有一个方形的下降,当它到达框架的底部时停止,我是否将类的实例存储在代码中的错误位置?编辑:事实上,我非常确定,我希望在它到达底部时存储该实例。该类的每个实例都需要自己的计时器吗?

public class Tetris extends JFrame {
    public static final int height = 20; //height of a square
    public static final int width = 20; //width of a square
    public int xPos = 0; //column number of the square
    public int yPos = 0; //row number of the square

    public static void main(String[] args){
            Tetris tet = new Tetris();
    }

    public Tetris() {
        DrawSquare square = new DrawSquare(xPos, yPos, width, height, false);
        add(square);
        DrawSquare.squares.add(square);
        setSize(220,440);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
    }
}


public class DrawSquare extends JPanel {
    public static List<DrawSquare> squares = new ArrayList<>();
    protected int xPos;
    protected int yPos;
    protected int width;
    protected int height;
    protected Timer timer = new Timer(200, new TimerListener());
    protected boolean endFall = false;


    public DrawSquare(int xPos, int yPos, int width, int height, boolean endFall) {
        this.xPos = xPos;
        this.yPos = yPos;
        this.width = width;
        this.height = height;
        this.endFall = endFall;
        this.timer.start();
    }

    class TimerListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            yPos++;
            if (yPos > 19) {
                yPos = 19;
                endFall = true;
            }
            if (endFall == true) {
                timer.stop();
                if (xPos > 8) {
                    xPos = 8;
                }
                xPos++;
                endFall = false;
                yPos = 0;
                DrawSquare newSqr = new DrawSquare(xPos, yPos, width, height, true);
                squares.add(newSqr);
                add(newSqr);
            }
            timer.start();
            repaint();
        }
    }

    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Iterator<DrawSquare> it = squares.iterator();
        while (it.hasNext()) {
            DrawSquare square = it.next();
            g.fillRect(square.xPos * square.width, square.yPos * square.height, square.width, square.height);
        }
    }
}

2 个答案:

答案 0 :(得分:2)

你给出了一个很好的例子,说明初学者对摆动(以及许多其他图形工具包)如何将内容渲染到屏幕上的根本误解。我将概述这一点,因为它与您有关,然后回答您的即时问题并解释如何修复您的代码。

我花了很长时间才知道这些东西是如何起作用的,所以请耐心等待。我希望通过阅读这个答案会比回答这个问题更加一般地帮助你。

异步绘图

Swing以完全不同的顺序(事件调度线程)绘制窗口,而不是修改程序状态的窗口(主线程,以及计时器和其他线程)。您可以在主线程中根据需要多次修改要绘制的内容的坐标,但在您通过在其中一个组件上调用JComponent.repaint()请求更改之前,更改将不会显示。这通常会触发几乎立即重新绘制组件,显示您的最新状态。

如果您更改主线程中的JPanel小部件的坐标,则可能会立即显示。这是因为用于设置位置的方法将在内部触发重绘请求。

重新绘制请求排队并最终由事件调度线程处理。这是调用paintComponent方法的地方。因此paintComponent方法只能绘制。它不应该做任何其他逻辑。如果它需要知道如何绘制一些专门的东西,那么它的信息应该隐藏在其他一个线程可以访问的地方。

简而言之,您可以在主线程或计时器中根据需要进行计算和更新状态。然后,您可以通过paintComponent方法在事件调度线程中访问该状态。

计时器

有许多方法可以使用计时器来运行GUI,但是您只需要一个用于当前应用程序的方法。在您的情况下,计时器只需要做两件事:

  1. 检查挡块是否完全掉落并且不再需要移动。
  2. 触发对您的面板进行重新绘制。
  3. 如果块的位置是关于时间的简单等式,则需要计算定时器中块的更新位置。如果您知道屏幕上出现一个块的时间和当前时间,您就知道该块移动了多远,因此您可以根据经过的时间在正确的位置绘制它。

    如果你有一个更复杂的系统,其路径无法完全根据时间进行预测,我建议将移动逻辑也固定在计时器事件中。在这种情况下,您可以考虑使用多个计时器,或切换到java.util.timer。但同样,这不适用于您当前的情况(即使有多个块)。

    模型和视图

    你的程序模型是持有抽象状态的东西。在这种情况下,关于所有块的位置和其他元数据。视图是进行渲染的部分。将这两件事分开通常是一个好主意。 GUI的第三个组件通常称为控制器,它将模型和视图连接到用户。我们将忽略它,因为你还没有询问有关控制块的信息。

    在您当前的代码中,您尝试使用JPanel的扩展名和现有块的static列表来表示您的块。虽然JPanel可能是显示矩形块的便捷方式,其中包含一些自定义图形(如图标),但我建议您首先使用传递给{{Graphics的{​​{1}}对象直接绘制块。 1}}。至少在最初,它会帮助您将绘图代码和游戏逻辑视为单独的实体。

    代码转储前的最终咆哮

    我已经对你的代码进行了重写,以便将我之前所做的所有咆哮封装到代码中。以下是关于我所做的一些额外的小问题,可能有助于解释我的推理:

    • 当您致电paintComponentJFrame.add(...)添加组件时,您实际上正在呼叫JFrame。内容窗格是90%的正常挥杆组件进入窗口的位置。因此,我们可以将要进行渲染的JFrame.getContentPane().add(...)设置为内容窗格,也可以将其添加到当前内容窗格。我已选择执行后者,以便您以后可以添加其他小部件,例如记分板。
    • 类名通常应该是名词,而方法通常是动词。这不是绝对的规则(实际上并非如此),但以这种方式命名事物通常可以帮助您以更有意义的方式可视化对象之间的交互。由于这个原因,我已将JPanel重命名为DrawSquare
    • GamePiece没有任何理由成为GamePiece。它只需要知道它自己的宽度,高度和出现时间。
    • 试图让JPanel绘制自己的另一个问题是组件只能在自己的边界框内绘制。所以你真的想要覆盖任何包含矩形的DrawSquare
    • 渲染类维护对两个paintComponent列表的引用。一个是移动物体,一个是坠落物体。在列表之间移动它们的逻辑是在计时器中。这比向GamePiece添加标志更好,因为它有助于增量重绘。我将在这里仅部分地说明这一点,但是有一个版本的重绘只需要绘制一个小区域。这对加速运动很有用。

    代码

    GamePiece

答案 1 :(得分:1)

简而言之,您的主要问题是:您需要将JPanel组件类与方形逻辑类分开。现在,它们是同一个,每次你创建一个新的DrawSqaure时,你都要创建一个新的JPanel,启动一个新的Swing Timer,从而调用不需要调用的代码。这也迫使你使List静止,否则你会发生堆栈溢出错误。解决方案:将两者分开,使List非静态,并且只使用一个Swing Timer。