Reputation: 45
I originally attempted to perform a hit test on geometric shapes within a Canvas, checking if the mouse click position is within the bounds of the shape. However, the results were not as expected.
Therefore, I created a Minimal, Reproducible Example. The code snippet in this example creates a rectangle in the Canvas based on the mouse's position. When we select the 'select' RadioButton and click within the bounds of the rectangle on the Canvas, I expected the hit method to return true, but it actually returns false.
import javax.swing.*;
import javax.swing.event.MouseInputAdapter;
import java.awt.*;
import java.awt.event.MouseEvent;
class CanvasArea extends JPanel {
Rectangle rectangle = new Rectangle();
MouseInputAdapter mode;
Graphics2D storedG2d;
CanvasArea() {
setBackground(Color.WHITE);
setPreferredSize(new Dimension(1000, 600));
}
void setRectangle(Rectangle r) {
repaint(rectangle.x, rectangle.y, rectangle.width + 1, rectangle.height + 1);
rectangle = r;
repaint(rectangle.x, rectangle.y, rectangle.width + 1, rectangle.height + 1);
}
void setMode(MouseInputAdapter mode) {
removeMouseListener(this.mode);
this.mode = mode;
addMouseListener(this.mode);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.draw(rectangle);
storedG2d = (Graphics2D) g2d.create();
}
}
class RectangleListener extends MouseInputAdapter {
@Override
public void mouseClicked(MouseEvent e) {
HitTest.canvas.setRectangle(new Rectangle(e.getPoint(), new Dimension(100, 120)));
}
}
class SelectListener extends MouseInputAdapter {
@Override
public void mousePressed(MouseEvent e) {
System.out.println(e.getPoint());
System.out.println(HitTest.canvas.rectangle);
Rectangle mousePosition = new Rectangle(e.getPoint(), new Dimension(1, 1));
System.out.println(HitTest.canvas.storedG2d.hit(mousePosition, HitTest.canvas.rectangle, false));
}
}
public class HitTest {
static CanvasArea canvas = new CanvasArea();
private Container createContentPane() {
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.setOpaque(true);
JRadioButton rb1 = new JRadioButton("rectangle");
rb1.addActionListener(e -> canvas.setMode(new RectangleListener()));
JRadioButton rb2 = new JRadioButton("select");
rb2.addActionListener(e -> canvas.setMode(new SelectListener()));
JToolBar toolBar = new JToolBar(JToolBar.VERTICAL);
ButtonGroup group = new ButtonGroup();
toolBar.add(rb1);
group.add(rb1);
toolBar.add(rb2);
group.add(rb2);
contentPane.add(toolBar, BorderLayout.LINE_START);
contentPane.add(canvas, BorderLayout.CENTER);
return contentPane;
}
private static void createAndShowGUI() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
HitTest hitTest = new HitTest();
frame.setContentPane(hitTest.createContentPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(HitTest::createAndShowGUI);
}
}
It can be observed that the mouse click position is within the bounds of the rectangle, but the hit method is returning false.
I used debugging to step into the hit method and noticed that the position and dimensions of the rectangle are being transformed (according to the documentation for hit method, I assume it is transformed into device space, but I'm not sure). After the transformation, the rectangle does not intersect with the mouse click position, resulting in a false return value. I'm unsure how to proceed to get the correct result.
Upvotes: 0
Views: 70
Reputation: 347184
IMHO, this...
class CanvasArea extends JPanel {
Graphics2D storedG2d;
//...
@Override
protected void paintComponent(Graphics g) {
//...
storedG2d = (Graphics2D) g2d.create();
}
}
is a bad idea. You shouldn't be holding onto a reference of the system Graphics
context, to many things can simply go wrong.
I've never used Graphics2D#hit
, I've always used the Shape
API instead, which has it's own built in hit detection.
However, if I was going to use Graphics2D#hit
, I would do so from within the paintComponent
method, for example...
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.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new MainPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class MainPane extends JPanel {
private DrawPane drawPane;
public MainPane() {
setLayout(new BorderLayout());
drawPane = new DrawPane();
add(drawPane);
drawPane.addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
drawPane.didHit(e.getPoint());
}
});
}
}
public class DrawPane extends JPanel {
private Rectangle rectangle;
private Rectangle hitBounds;
final private Dimension hitSize = new Dimension(1, 1);
public DrawPane() {
rectangle = new Rectangle(150, 150, 100, 100);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
// It's VERY important that the Point is within the context of the
// this component, otherwise you will get werid results
public void didHit(Point p) {
hitBounds = new Rectangle(p, hitSize);
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);
if (hitBounds != null) {
if (g2d.hit(hitBounds, rectangle, false)) {
g2d.fill(rectangle);
} else {
g2d.draw(rectangle);
}
} else {
g2d.draw(rectangle);
}
g2d.dispose();
}
}
}
But that raises a bunch of new issues, as you should avoid having to much logic in the paintComponent
... and how do you flag/notify the fact that a hit occurred?
Instead, I'd make use the Shape#contains(Point)
method for hit detection instead. This can be done independently of the Graphics
context and painting workflows, which make it easier to use as part of the "update" phase or for other conditional workflows.
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.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new MainPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class MainPane extends JPanel {
private DrawPane drawPane;
public MainPane() {
setLayout(new BorderLayout());
drawPane = new DrawPane();
add(drawPane);
drawPane.addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
drawPane.didHit(e.getPoint());
}
});
}
}
public class DrawPane extends JPanel {
private Rectangle rectangle;
private boolean hit = false;
public DrawPane() {
rectangle = new Rectangle(150, 150, 100, 100);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
// It's VERY important that the Point is within the context of the
// this component, otherwise you will get werid results
public boolean didHit(Point p) {
hit = false;
if (rectangle.contains(p)) {
hit = true;
}
repaint();
return hit;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);
if (hit) {
g2d.fill(rectangle);
} else {
g2d.draw(rectangle);
}
g2d.dispose();
}
}
}
This would also allow you to decouple the management of the Shape
s from the paint workflow, supporting concepts like "model-view-controller"
Upvotes: 0