非常简单的Swing骰子程序中存在神秘的(并发/组件绘制?)错误

6

抱歉标题不够清晰,我被这个错误困扰了,不知道如何表达问题.

我正在学习基本的Swing并完成在线书籍《使用Java进行编程入门》中的练习.

我没有严格按照说明操作,而是尝试做到以下几点:

  • 显示两个骰子的图像窗口
  • 当单击其中一个骰子时,它会“滚动”并显示新值

我的实现方法:

  • 一个非常基本的JDie对象,扩展JPanel
  • 重写paintComponent方法以绘制骰子表示
  • 每次更改值时更改骰子颜色,只是为了视觉提示
  • 添加一个侦听器在鼠标按下时“滚动”骰子,然后重绘

该错误非常具体:

  1. 运行 DieTest 的main方法
  2. 调整窗口大小以适应两个骰子
  3. 单击第二个骰子以使其滚动
  4. 现在单击第一个骰子以使其滚动
  5. 第二个骰子的值会改回其原始值
  6. 如果调整窗口大小,则第二个骰子的值也会改回原始值

如果我在调整窗口大小之前点击骰子进行滚动,则不会出现该错误...

我猜测我可能犯了一些基本错误,它以这种奇怪的行为掩盖了自己.

我已经尽可能地缩小了代码,这花费了很长时间才弄清楚错误何时出现何时没有:

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

class JDie extends JPanel {

    private Color color;
    private int value;

    JDie(){

        value = getValue();
        color = Color.BLACK;

        //add listener
        addMouseListener(new MouseAdapter(){
            @Override
            public void mousePressed(MouseEvent arg0) {
                value = getValue(); //'roll' the die
                repaint();
            }
        });
    }

    /*private helper methods */
    private int getValue(){
        int v =(int)(Math.random()*6) + 1;
        //change color just to show that the
        //value has changed
        color = getRandomColor();
        return v;
    }
    private Color getRandomColor(){
        float r = (float)Math.random();
        float g = (float)Math.random();
        float b = (float)Math.random();
        return new Color(r, g, b);
    }

    //draws the pips for the die
    @Override
    public void paintComponent(Graphics g){
        super.paintComponent(g);
        g.setColor(color);


        //draw pips
        //set pip size
        int pip_side = 10;
        switch(value){
        case 1:
            g.fillRect(3*pip_side, 3*pip_side, pip_side, pip_side);
            break;
        case 2:
            g.fillRect(5*pip_side, pip_side, pip_side, pip_side);
            g.fillRect(pip_side, 5*pip_side, pip_side, pip_side);
            break;
        case 3:
            g.fillRect(5*pip_side, pip_side, pip_side, pip_side);
            g.fillRect(pip_side, 5*pip_side, pip_side, pip_side);
            g.fillRect(3*pip_side, 3*pip_side, pip_side, pip_side);
            break;
        case 4:
            g.fillRect(pip_side, pip_side, pip_side, pip_side);
            g.fillRect(5*pip_side, 5*pip_side, pip_side, pip_side);
            g.fillRect(5*pip_side, pip_side, pip_side, pip_side);
            g.fillRect(pip_side, 5*pip_side, pip_side, pip_side);
            break;
        case 5:
            g.fillRect(pip_side, pip_side, pip_side, pip_side);
            g.fillRect(5*pip_side, 5*pip_side, pip_side, pip_side);
            g.fillRect(5*pip_side, pip_side, pip_side, pip_side);
            g.fillRect(pip_side, 5*pip_side, pip_side, pip_side);
            g.fillRect(3*pip_side, 3*pip_side, pip_side, pip_side);
            break;
        case 6:
            g.fillRect(pip_side, pip_side, pip_side, pip_side);
            g.fillRect(5*pip_side, 5*pip_side, pip_side, pip_side);
            g.fillRect(5*pip_side, pip_side, pip_side, pip_side);
            g.fillRect(pip_side, 5*pip_side, pip_side, pip_side);
            g.fillRect(pip_side, 3*pip_side, pip_side, pip_side);
            g.fillRect(5*pip_side, 3*pip_side, pip_side, pip_side);
            break;
        }
    }
}

public class DieTest extends JFrame{

    DieTest(){
        setLayout(new GridLayout());
        add(new JDie());
        add(new JDie());

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        //if I set the size smaller than the JDie is displaying
        //and resize the window before 'rolling' the dice
        //then the bug appears...?!
        setSize(80, 80);
        //setting the size larger than both JDie
        //and it works fine whether you resize or not
//      setSize(200, 200);

        setVisible(true);

    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable(){

            @Override
            public void run() {
                new DieTest();
            }

        });
    }

}

--------------EDIT-----------------

重新运行代码后,我现在注意到它并不是100%发生了这个bug,但是问题仍然存在。下面这个gif演示了可能更好地说明问题:

Strange bug

当我再次单击第一个骰子时,可以清楚地看到原始值与原始颜色被重新绘制。当我调整窗口大小时,第二个骰子的值会向前跳一个,回到之前的值......我真的无法理解这个...

---------------EDIT 2---------------------

  • 在另一台(苹果)电脑上尝试相同的代码,但无法复制出现的问题。
  • 在Eclipse之外编译和运行代码,无法复制出现的问题。
  • 从命令行运行Eclipse编译的代码,只有一次出现问题,今天无法复制出现的问题。
  • 从Eclipse运行代码,我仍然会遇到这个问题,大约每5-10次就会出现一次?如果它在第一个“通过”中没有显示,它根本不会出现。这个gif很好说明了这个问题。

因此,似乎我的计算机设置也与此有关,详细信息如下:

  • Windows 7 64位
  • Eclipse Kepler Service Release 1
  • Java Version 7 Update 51
  • Java SE Development Kit 7 Update 3(64位)

这是一个棘手的问题,因为我不再知道是我的代码还是其他程序导致了问题。作为新手,我如何知道任何未来的问题是由于我的糟糕编程还是其他原因...令人沮丧。

----------EDIT 3-----------

为了快速调查并发方面的问题: 我将所有实例字段设置为volatile 我将包括paintComponent在内的所有方法设置为synchronized 我删除了Math.random()调用(尽管我读到另一个线程说这是线程安全的实现),并使用实例Random对象进行了替换

不幸的是,我仍然遇到视觉切换回来的情况。

另一件我注意到的事情是,现在它似乎不那么频繁地出现,大约每10次中只有1次。我一直对它已经被修复抱有希望,但接下来的尝试依旧有bug。在我最初的程序中,它似乎更像是3分之1的概率(我现在完全改变了那个程序,所以手头没有它)。

--------EDIT 4--------- 我想到了一个稍微简化一些的版本,不再使用任何随机值,但仍会产生视觉切换回来。使用此代码时似乎更容易出现问题:

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

public class ColorPanelsWindow extends JFrame{

    class ColorPanel extends JPanel {

        //color starts off black
        //once it is changed should never be 
        //black again
        private Color color = Color.BLACK;

        ColorPanel(){
            //add listener
                    //click on panel to rotate color
            addMouseListener(new MouseAdapter(){
                @Override
                public void mousePressed(MouseEvent arg0) {
                    color = rotateColor();
                    repaint();
                }
            });
        }
        //rotates the color black/blue > red > green > blue
        private Color rotateColor(){
            if (color==Color.BLACK || color == Color.BLUE)
                return Color.RED;
            if (color==Color.RED)
                return Color.GREEN;
            else return Color.BLUE;
        }

        @Override
        public void paintComponent(Graphics g){
            g.setColor(color);
            g.fillRect(0, 0, 100, 100);
        }
    }
ColorPanelsWindow(){
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    setLayout(new GridLayout(1,0));
    add(new ColorPanel());
    add(new ColorPanel());
    //the size must be set so that the window is too small
    // and the two ColorPanels are overlapping
    setSize(40, 40);
            //setSize(300, 200);

    setVisible(true);

}

public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable(){

        @Override
        public void run() {
            new ColorPanelsWindow();
        }

    });
}

}

以下是一些观察:

  • 据我所知,面板必须一开始重叠
  • 如果我使用流式布局、增加窗口大小或任何使面板最初不重叠的方法,似乎就不会出现这个问题(或者可能发生得更少?)

我尝试了您的代码并按照您的说明操作,但我无法重现错误。只有当我点击框架时,骰子才会改变,而不是在调整大小时。您确定在调整窗口大小时没有意外地点击框架中的某个位置吗?因为目前情况下,当我在它们下面或旁边单击时,骰子也会滚动。 - Múna
@Múna 感谢您的测试。我现在注意到它并不总是发生,但它绝对不仅仅是我点击位置的错误。我添加了一个gif以更好地说明它。 - mallardz
快速测试了一下jdk6:一切正常,所以看起来是在jdk7中引入的某些东西(我的版本在win上是7u45)-这可以解释为什么@Trashgod在使用不同的jdk实现的mac上没有看到它。 - kleopatra
在jdk8中也会发生... - kleopatra
忘了提到一个快速的技巧:在mouseListener中调用getParent().repaint(这种方法不太好,但似乎有效)。如果你有勇气,你可以考虑提交一个bug报告......我几年前就放弃了。;-) - kleopatra
显示剩余3条评论
2个回答

4

像Múna一样,我无法重现您的发现,但我有一些观察:

  • 覆盖 getPreferredSize() 以定义初始几何形状。

  • 定义强制几何关系的常量。

  • 使用尊重首选大小的布局,例如FlowLayout以查看效果。

  • 使用 Color.getHSBColor() 获取各种饱和色调。

  • 根据需要使用单个实例的Random

补充说明:问题的间歇性和平台可变性强烈表明同步不正确。在原始程序中,两个骰子共享一个由Math拥有的静态Random实例;在下面的示例中,每个骰子都有自己的实例。还要注意,调整框架的大小会间接地调用paintComponent()

image

测试结果如下:

import java.awt.*;
import java.awt.event.*;
import java.util.Random;
import javax.swing.*;

public class DieTest extends JFrame {

    DieTest() {
        setLayout(new FlowLayout());
        add(new JDie());
        add(new JDie());
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        pack();
        setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                new DieTest();
            }
        });
    }

    private static class JDie extends JPanel {

        private static final int SIDE = 32;
        private static final Random r = new Random();
        private Color color;
        private int value = getValue();
        private final Timer t = new Timer(500, null);

        JDie() {
            setBorder(BorderFactory.createEtchedBorder(color, color.darker()));
            value = getValue();
            addMouseListener(new MouseAdapter() {
                @Override
                public void mousePressed(MouseEvent arg0) {
                    value = getValue();
                    repaint();
                }
            });
            t.addActionListener(new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {
                    value = getValue();
                    repaint();
                }
            });
            t.start();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(SIDE * 7, SIDE * 7);
        }

        private int getValue() {
            color = Color.getHSBColor(r.nextFloat(), 1, 1);
            return r.nextInt(6) + 1;
        }

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.setColor(color);
            switch (value) {
                case 1:
                    g.fillRect(3 * SIDE, 3 * SIDE, SIDE, SIDE);
                    break;
                case 2:
                    g.fillRect(5 * SIDE, SIDE, SIDE, SIDE);
                    g.fillRect(SIDE, 5 * SIDE, SIDE, SIDE);
                    break;
                case 3:
                    g.fillRect(5 * SIDE, SIDE, SIDE, SIDE);
                    g.fillRect(SIDE, 5 * SIDE, SIDE, SIDE);
                    g.fillRect(3 * SIDE, 3 * SIDE, SIDE, SIDE);
                    break;
                case 4:
                    g.fillRect(SIDE, SIDE, SIDE, SIDE);
                    g.fillRect(5 * SIDE, 5 * SIDE, SIDE, SIDE);
                    g.fillRect(5 * SIDE, SIDE, SIDE, SIDE);
                    g.fillRect(SIDE, 5 * SIDE, SIDE, SIDE);
                    break;
                case 5:
                    g.fillRect(SIDE, SIDE, SIDE, SIDE);
                    g.fillRect(5 * SIDE, 5 * SIDE, SIDE, SIDE);
                    g.fillRect(5 * SIDE, SIDE, SIDE, SIDE);
                    g.fillRect(SIDE, 5 * SIDE, SIDE, SIDE);
                    g.fillRect(3 * SIDE, 3 * SIDE, SIDE, SIDE);
                    break;
                case 6:
                    g.fillRect(SIDE, SIDE, SIDE, SIDE);
                    g.fillRect(5 * SIDE, 5 * SIDE, SIDE, SIDE);
                    g.fillRect(5 * SIDE, SIDE, SIDE, SIDE);
                    g.fillRect(SIDE, 5 * SIDE, SIDE, SIDE);
                    g.fillRect(SIDE, 3 * SIDE, SIDE, SIDE);
                    g.fillRect(5 * SIDE, 3 * SIDE, SIDE, SIDE);
                    break;
            }
        }
    }
}

感谢您的测试,trashgod。我回去制作了一个gif来更好地说明它的错误。这非常奇怪,这次花了我几次才弄明白。还要非常感谢您对代码的建议,我一定会在以后实现它们。实际上,我尝试尽可能剥离代码,只是为了说明这个错误。 - mallardz
我期望大约六分之一的情况下出现双倍或重复抛出。你看到了不同的结果吗? - trashgod
我并没有注意到随机值返回模式中的任何奇怪之处。我猜测随机值或某些并发问题可能与错误有关,这就是为什么它只会如此零星地显示出来的原因。我无法在其他计算机上重现这个错误,只有在Eclipse之外的一个地方出现过...这让我疯狂!我已经编辑了我的问题以包含额外的信息。 - mallardz
我注意到你的骰子共享一个在Math中拥有的Random的静态实例;而我的每个骰子都有自己的实例。还要注意,调整帧大小间接地调用了paintComponent() - trashgod
谢谢您的建议。我尝试了替换随机调用,但并没有解决问题。我更新了问题以反映这一点。它确实似乎是一个同步问题,但也许是在JDK端,就像kleopatra所建议的那样?我想起Bruce Eckel在他的书《Java编程思想》中对同步错误的评论:试错是不好的,你必须真正理解同步,并能够从理论上思考问题,因为你看到的实际效果太随机和罕见,无法管理。或者至少这就是我理解的! - mallardz
显示剩余2条评论

2

是的,我明白你在说什么。我也曾经遇到过类似的问题。

虽然我花了很长时间才找出原��,但现在我仍然不确定是否真的是我想的那样;发生了(太技术化,不适合在这里发布所有内容)。但我确实解决了它,而且我甚至不知道为什么它有效。

因此,不要重绘组件本身,而是重绘其容器(或其容器的容器——如果仍未修复)。

getParent().repaint();

希望这有所帮助。

非常感谢您的帮助,问题立刻得到了解决!我已经有一段时间没有看那段代码了,但我仍然很好奇它为什么会表现出这样的行为。等我有时间时,我可能会发布另一个具体的问题。 - mallardz
我在这里发布了新的问题:https://dev59.com/84Dba4cB1Zd3GeqPG6Qj - mallardz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接