来自JFrame点绘图的奇怪行为

时间:2017-03-11 02:08:57

标签: java swing graphics jpanel

我正在编写一个程序,用户可以通过单击并拖动鼠标在JPanel上绘制点。另外,绘图区域被划分为多个扇区,并且旋转这些点使得每个扇区是相同的。例如,内部具有单个点的十二个扇区布置将使该点旋转12次到360/12度。旋转工作正常,但在尝试绘制点时有一些非常奇怪的行为。如果试图在原点周围绘制一个圆圈,那么这些点在短时间内会非常零星地出现,然后才能顺利添加。此图显示了我的意思(在其中一个扇区中绘制四分之一圆的结果): enter image description here

您可以看到,当接近扇区划分的一侧时,这些点会平滑地添加。但是,最初这些点是分开的并且不能平滑地绘制。代码如下所示(多余的GUI元素和导入已被删除以便于阅读):

public class Doiles extends JPanel implements MouseListener,ActionListener,MouseMotionListener
{
    //global variable declarations
    JFrame window = new JFrame("Draw");
    final int linelength = 340;//length of sector defining lines
    int nlines = 12;//store the number of sector defining lines
    String numsectors=null;
    int currentovalsize = 10;
    Color currentcolour = Color.WHITE;
    Deque<DoilyPoint> points = new LinkedList<DoilyPoint>();

    public Doiles()
    {
        window.setSize(2000,1000);



        //drawing panel + paint method
        JPanel drawingPanel = new JPanel()
        {   
            public void paintComponent(Graphics g)
            {
                super.paintComponent(g);

                //calculate angle between sectors
                double theta = (2*Math.PI)/nlines;
                g.setColor(Color.WHITE);

                //calculate line coordinates and draw the sector lines
                for(int i=0; i <nlines;i++)
                {
                    g.drawLine(400, 350, 400+(int)Math.round(linelength*Math.cos(theta*i)), 350+(int)Math.round(linelength*Math.sin(theta*i)));
                }
                for(DoilyPoint j : points)
                {
                    g.fillOval(j.getX(), j.getY(), j.getSize(), j.getSize());

                    for(int h = 1;h<nlines;h++)
                    {

                        double rtheta;
                        if(j.getX()==400)
                            rtheta = Math.PI/2;
                        else
                            rtheta = Math.atan((j.getY()-350)/(j.getX()-400));
                        System.out.println(rtheta);
                        double r = Math.sqrt(Math.pow(j.getX()-400,2)+Math.pow(j.getY()-350,2));
                        double angle = (h*theta)+rtheta;
                        double x = r*Math.cos(angle);
                        double y = r*Math.sin(angle);
                        g.fillOval((int)Math.round(x)+400,(int)Math.round(y)+350, j.getSize(), j.getSize());


                    }
                }

            }
        };


        }



    public static void main(String[] args)
     {
        new Doiles();        
     }


    public void addPoint(int x, int y)
    {
        points.addFirst(new DoilyPoint(currentovalsize,x,y,currentcolour));
        window.repaint();
    }


    @Override
    public void mouseDragged(MouseEvent e)
    {
        addPoint(e.getX(),e.getY());
    }
}



class DoilyPoint
{
    private int size;
    private int x;
    private int y;
    private Color colour;
    void setSize(int a){this.size = a;}
    int getSize(){return size;}
    void setX(int a){this.x =a;}
    int getX(){return x;}
    void setY(int a){this.y = a;}
    int getY(){return y;}
    void setColor(Color r){this.colour = r;}
    Color getColor(){return colour;}

    public DoilyPoint(int size,int x, int y,Color colour)
    {
        this.size = size;
        this.x = x;
        this.y = y;
        this.colour = colour;
    }
}

我认为它与Java处理拖动鼠标的方式有关,但我想知道如何平滑绘图。谁能告诉我什么是错的?

2 个答案:

答案 0 :(得分:4)

为什么它不起作用会带一个拥有更好数学技能的人,然后我必须弄清楚,我会让我的4.5岁的孩子看看她玩完玩偶娃娃之后;)

我已经完成的工作可以追溯到API的可用功能,特别是AffineTransform,它允许您旋转Graphics上下文(以及其他内容)。

所以,基本上,对于每个片段,我旋转上下文,并绘制所有点。

Pretty Patterns

我还花了一些时间删除所有“神奇”数字并专注于使用已知值(比如根据宽度和高度计算组件的实际中心)

魔术

所以,魔法基本上就在这里......

double delta = 360.0 / (double) nlines;
Graphics2D gCopy = (Graphics2D) g.create();
AffineTransform at = AffineTransform.getRotateInstance(
        Math.toRadians(delta),
        centerPoint.x,
        centerPoint.x);
for (int h = 0; h < nlines; h++) {
    for (DoilyPoint j : points) {
        gCopy.fillOval(j.getX(), j.getY(), j.getSize(), j.getSize());
    }
    gCopy.transform(at);
}
gCopy.dispose();

有许多重要概念需要理解

  • 首先,我们制作图形上下文的副本(这只是复制当前状态),这很重要,因为我们不想弄乱当前上下文,因为这与其他组件共享,并且撤消是痛苦
  • 接下来,我们创建一个轮转AffineTransform。这是非常基本的,我们提供一个锚点,围绕该锚点进行旋转,在这种情况下,组件的中心和要应用的旋转量。
  • 每个细分的下一个,我们绘制所有点
  • 然后我们使用AffineTransform转换复制的上下文。这是一个记住的巧妙技巧,转换是复合的,所以我们只需要知道要改变的增量,而不是实际的角度

可运行示例

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.util.Deque;
import java.util.LinkedList;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new Doiles());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class Doiles extends JPanel implements MouseListener, ActionListener, MouseMotionListener {
        //global variable declarations

        int nlines = 12;//store the number of sector defining lines
        int currentovalsize = 10;
        Color currentcolour = Color.WHITE;
        Deque<DoilyPoint> points = new LinkedList<DoilyPoint>();

        Color test[] = {Color.RED,
                                        Color.GREEN,
                                        Color.BLUE, Color.MAGENTA, Color.CYAN};

        public Doiles() {

            //drawing panel + paint method
            JPanel drawingPanel = new JPanel() {
                public void paintComponent(Graphics g) {
                    super.paintComponent(g);

                    int lineLength = Math.max(getWidth(), getHeight());
                    Point centerPoint = new Point(getWidth() / 2, getHeight() / 2);

                    //calculate angle between sectors
                    double theta = Math.toRadians(360.0 / nlines);
                    g.setColor(Color.WHITE);

                    //calculate line coordinates and draw the sector lines
                    for (int i = 0; i < nlines; i++) {
                        g.drawLine(centerPoint.x, centerPoint.y,
                                             centerPoint.x + (int) Math.round(lineLength * Math.cos(theta * i)),
                                             centerPoint.y + (int) Math.round(lineLength * Math.sin(theta * i)));
                    }
                    double delta = 360.0 / (double) nlines;
                    Graphics2D gCopy = (Graphics2D) g.create();
                    AffineTransform at = AffineTransform.getRotateInstance(
                            Math.toRadians(delta),
                            centerPoint.x,
                            centerPoint.x);
                    for (int h = 0; h < nlines; h++) {
                        for (DoilyPoint j : points) {
                            gCopy.fillOval(j.getX(), j.getY(), j.getSize(), j.getSize());
                        }
                        gCopy.transform(at);
                    }
                    gCopy.dispose();
                }
            };
            drawingPanel.setBackground(Color.BLACK);
            drawingPanel.addMouseMotionListener(this);
            drawingPanel.addMouseListener(this);
            setLayout(new BorderLayout());
            add(drawingPanel);

        }

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

        public void addPoint(int x, int y) {
            points.addFirst(new DoilyPoint(currentovalsize, x, y, currentcolour));
            repaint();
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            addPoint(e.getX(), e.getY());
        }

        @Override
        public void mouseClicked(MouseEvent e) {
//          addPoint(e.getX(), e.getY());
        }

        @Override
        public void mousePressed(MouseEvent e) {
        }

        @Override
        public void mouseReleased(MouseEvent e) {
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }

        @Override
        public void actionPerformed(ActionEvent e) {
        }

        @Override
        public void mouseMoved(MouseEvent e) {
        }
    }

    class DoilyPoint {

        private int size;
        private int x;
        private int y;
        private Color colour;

        void setSize(int a) {
            this.size = a;
        }

        int getSize() {
            return size;
        }

        void setX(int a) {
            this.x = a;
        }

        int getX() {
            return x;
        }

        void setY(int a) {
            this.y = a;
        }

        int getY() {
            return y;
        }

        void setColor(Color r) {
            this.colour = r;
        }

        Color getColor() {
            return colour;
        }

        public DoilyPoint(int size, int x, int y, Color colour) {
            this.size = size;
            this.x = x;
            this.y = y;
            this.colour = colour;
        }
    }
}

建议...

  • 当我测试这个时,我将分段数减少到两个和三个以使其更简单
  • 我用鼠标点击而不是用鼠标拖动,这样我就可以更好地控制点的创建,看看实际上是什么了
  • 我为每个片段设置了一个单独的颜色,所以我可以看到这些点实际上被绘制的位置
  • 鉴于有关此问题和类似问题的问题频繁发生,请与您班上的其他学生分享此信息,因为重复基本的某些解决方案变得令人烦恼

答案 1 :(得分:2)

Yes, the mouse is refreshing at a very fast rate causing it to lag。在1080p屏幕上移动的每一只鼠标上调用repaint()会比必要的次数刷新太多次。有两种方法可以解决这个问题。通过以下方式限制对addPoint()的调用:

  1. 空间
  2. 时间
  3. 我将提供一个如何两种方式的例子。

    空间

    保存Doiles类中实例变量中更新的最后一个点的位置:

    int previousX, previousY
    

    设置在绘制和重新绘制屏幕之前必须满足的偏移值(移动距离):

    static final in MINIMUM_OFFSET = 10;  //mess around with this and use whatever looks good and performs well
    

    然后,修改mouseDragged实现以考虑它移动的距离:

        @Override
        public void mouseDragged(MouseEvent e)
        {
            //you can add some trig to this to calculate the hypotenuse, but with pixels I wouldn't bother
            int distance = Math.abs(e.getY() - previousY) + Math.abs(e.getX - previousX);
    
            if(distance > OFFSET_VALUE){
    
                 //update the previous x,y values
                 this.previousX = e.getX();
                 this.previousY = e.getY();
    
                 //add point
                 addPoint(e.getX(),e.getY());               
            }
    
        }
    

    这将降低刷新率,具体取决于鼠标移动的距离。这适用于您所描述的内容,但如果此JPanel还有其他因素需要考虑,则下面的Time解决方案会更好:

    时间

    您真的不需要为此实现MouseMotionListener。在MouseListener实现中,更改类中的布尔标志,表示是否在JPanel上按下了鼠标:

    boolean isMousePressed;
    
    @Override
    mousePressed(MouseEvent e) {
        isMousePressed = true;
    }
    
    @Override
    mouseReleased(MouseEvent e) {
        isMousePressed = false;
    }
    

    然后,使用javax.swing.Timer(针对Swing组件的线程安全)使用MouseListener + PointerInfo来经常更新画布:

        // this is set to 60Hz I, mess around with it to get the best results
        Timer timer=new Timer(1000/60, e -> {
            if(isMousePressed) {
                 Point p = MouseInfo.getPointerInfo().getLocation();             
                 addPoint(p.x,p.y);               
            }
        });
    

    就个人而言,我更喜欢第二种解决方案,因为它不那么密集,而且刷新率是恒定的。

    编辑:通过每次调用Timer并按下鼠标时重新分配'Point'实例变量,将第一个与第二个结合起来。然后你甚至会有刷新率和一致的点位置。