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() 的调用:
- Space
- 时间
我将提供一个示例来说明如何使用这两种方法。
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 中的其他学生分享此信息,因为重复基本的一些解决方案变得很烦人
我正在编写一个程序,用户可以通过单击并拖动鼠标在 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 处理拖动鼠标的方式有关,但我想知道如何平滑绘图。谁能告诉我怎么了?
- Space
- 时间
我将提供一个示例来说明如何使用这两种方法。
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 中的其他学生分享此信息,因为重复基本的一些解决方案变得很烦人