JFrame 点绘图的奇怪行为

Strange behaviour from JFrame point drawing

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

可以看到,当接近一个扇区划分的一侧时,加点是平滑的。但是,最初这些点是分开的并且绘制不流畅。代码如下所示(为了便于阅读,已删除多余的 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 处理拖动鼠标的方式有关,但我想知道如何平滑绘图。谁能告诉我怎么了?

。在 1080p 屏幕上每次移动鼠标时调用 repaint() 会刷新它太多次,而不是必要的。有两种方法可以解决这个问题。通过以下方式限制对 addPoint() 的调用:

  1. Space
  2. 时间

我将提供一个示例来说明如何使用这两种方法。

Space

在 Doiles 的实例变量中保存最后更新的点的位置 class:

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 还需要考虑其他因素,则下面的时间解决方案会更好:

时间

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

boolean isMousePressed;

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

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

然后,使用 javax.swing.Timer (Thread-safe for Swing components) to update it the canvas every so often using 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' 实例变量,将第一个与第二个结合起来。然后您将拥有均匀的刷新率和一致的点位置。

为什么它不起作用需要数学技能更好的人,然后我必须弄清楚,我会让我 4.5 岁的孩子玩完她的洋娃娃后看看 ;)

虽然我所做的是回到 API 的可用功能,特别是 AffineTransform,它允许您旋转 Graphics 上下文(在其他事情)。

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

我还花了一些时间删除所有 "magic" 数字并专注于处理已知值(例如根据组件的宽度和高度计算组件的实际中心)

魔法

所以,魔术基本上就发生在这里...

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;
        }
    }
}

建议...

  • 当我测试这个时,我将段数减少到两个和三个,以使其更简单一些
  • 我使用鼠标单击而不是鼠标拖动,这样我可以更好地控制点的创建并查看实际情况
  • 我为每个部分设置了单独的颜色,这样我就可以看到点实际被绘制的位置
  • 鉴于与此问题和类似问题相关的问题出现的频率,请与您 class 中的其他学生分享此信息,因为重复基本的一些解决方案变得很烦人