原文: https://zetcode.com/gfx/java2d/hitmove/

在 Java 2D 编程教程的这一部分中,我们首先讨论命中测试。 我们展示了如何确定是否在面板上的某个形状内单击。 在第二个示例中,我们创建两个形状,可以用鼠标在面板上移动它们,并用鼠标滚轮调整它们的大小。 在最后一个示例中,我们将调整具有两个控制点的矩形的大小。

命中测试

命中测试确定我们是否已经用鼠标指针单击了Shape内部。 每个Shape都有contains()方法。 该方法测试指定的Point2D是否在Shape的边界内。

HitTestingEx.java

  1. package com.zetcode;
  2. import java.awt.AlphaComposite;
  3. import java.awt.Color;
  4. import java.awt.EventQueue;
  5. import java.awt.Graphics;
  6. import java.awt.Graphics2D;
  7. import java.awt.RenderingHints;
  8. import java.awt.event.MouseAdapter;
  9. import java.awt.event.MouseEvent;
  10. import java.awt.geom.Ellipse2D;
  11. import java.awt.geom.Rectangle2D;
  12. import java.util.logging.Level;
  13. import java.util.logging.Logger;
  14. import javax.swing.JFrame;
  15. import javax.swing.JPanel;
  16. class Surface extends JPanel {
  17. private Rectangle2D rect;
  18. private Ellipse2D ellipse;
  19. private float alpha_rectangle;
  20. private float alpha_ellipse;
  21. public Surface() {
  22. initSurface();
  23. }
  24. private void initSurface() {
  25. addMouseListener(new HitTestAdapter());
  26. rect = new Rectangle2D.Float(20f, 20f, 80f, 50f);
  27. ellipse = new Ellipse2D.Float(120f, 30f, 60f, 60f);
  28. alpha_rectangle = 1f;
  29. alpha_ellipse = 1f;
  30. }
  31. private void doDrawing(Graphics g) {
  32. Graphics2D g2d = (Graphics2D) g.create();
  33. g2d.setPaint(new Color(50, 50, 50));
  34. RenderingHints rh = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
  35. RenderingHints.VALUE_ANTIALIAS_ON);
  36. rh.put(RenderingHints.KEY_RENDERING,
  37. RenderingHints.VALUE_RENDER_QUALITY);
  38. g2d.setRenderingHints(rh);
  39. g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
  40. alpha_rectangle));
  41. g2d.fill(rect);
  42. g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
  43. alpha_ellipse));
  44. g2d.fill(ellipse);
  45. g2d.dispose();
  46. }
  47. @Override
  48. public void paintComponent(Graphics g) {
  49. super.paintComponent(g);
  50. doDrawing(g);
  51. }
  52. class RectRunnable implements Runnable {
  53. private Thread runner;
  54. public RectRunnable() {
  55. initThread();
  56. }
  57. private void initThread() {
  58. runner = new Thread(this);
  59. runner.start();
  60. }
  61. @Override
  62. public void run() {
  63. while (alpha_rectangle >= 0) {
  64. repaint();
  65. alpha_rectangle += -0.01f;
  66. if (alpha_rectangle < 0) {
  67. alpha_rectangle = 0;
  68. }
  69. try {
  70. Thread.sleep(50);
  71. } catch (InterruptedException ex) {
  72. Logger.getLogger(Surface.class.getName()).log(Level.SEVERE,
  73. null, ex);
  74. }
  75. }
  76. }
  77. }
  78. class HitTestAdapter extends MouseAdapter
  79. implements Runnable {
  80. private RectRunnable rectAnimator;
  81. private Thread ellipseAnimator;
  82. @Override
  83. public void mousePressed(MouseEvent e) {
  84. int x = e.getX();
  85. int y = e.getY();
  86. if (rect.contains(x, y)) {
  87. rectAnimator = new RectRunnable();
  88. }
  89. if (ellipse.contains(x, y)) {
  90. ellipseAnimator = new Thread(this);
  91. ellipseAnimator.start();
  92. }
  93. }
  94. @Override
  95. public void run() {
  96. while (alpha_ellipse >= 0) {
  97. repaint();
  98. alpha_ellipse += -0.01f;
  99. if (alpha_ellipse < 0) {
  100. alpha_ellipse = 0;
  101. }
  102. try {
  103. Thread.sleep(50);
  104. } catch (InterruptedException ex) {
  105. Logger.getLogger(Surface.class.getName()).log(Level.SEVERE,
  106. null, ex);
  107. }
  108. }
  109. }
  110. }
  111. }
  112. public class HitTestingEx extends JFrame {
  113. public HitTestingEx() {
  114. add(new Surface());
  115. setTitle("Hit testing");
  116. setSize(250, 150);
  117. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  118. setLocationRelativeTo(null);
  119. }
  120. public static void main(String[] args) {
  121. EventQueue.invokeLater(new Runnable() {
  122. @Override
  123. public void run() {
  124. HitTestingEx ex = new HitTestingEx();
  125. ex.setVisible(true);
  126. }
  127. });
  128. }
  129. }

在我们的示例中,我们有两个Shapes:一个矩形和一个圆形。 通过单击它们,它们逐渐开始消失。 在此示例中,我们使用线程。

  1. private Rectangle2D rect;
  2. private Ellipse2D ellipse;

我们使用矩形和椭圆形。

  1. private float alpha_rectangle;
  2. private float alpha_ellipse;

这两个变量控制两个几何对象的透明度。

  1. g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
  2. alpha_rectangle));
  3. g2d.fill(rect);

doDrawing()方法内部,我们设置矩形的透明度。 alpha_rectangle在专用的Thread内部进行计算。

HitTestAdapter类负责处理鼠标事件。 它确实实现了Runnable接口,这意味着它还创建了第一个线程。

  1. if (ellipse.contains(x, y)) {
  2. ellipseAnimator = new Thread(this);
  3. ellipseAnimator.start();
  4. }

如果我们在椭圆内按下,将创建一个新的Thread。 该线程调用run()方法。 在我们的例子中,它是类本身的run()方法(HitTestAdapter)。

  1. if (rect.contains(x, y)) {
  2. rectAnimator = new RectRunnable();
  3. }

对于矩形,我们有一个单独的内部类-RectRunnable类。 此类在构造器中创建自己的线程。

  1. public void run() {
  2. while (alpha_ellipse >= 0) {
  3. repaint();
  4. alpha_ellipse += -0.01f;
  5. ...
  6. }

请注意,run()方法仅被调用一次。 要实际执行某项操作,我们必须实现一个while循环。 while循环重新绘制面板并减小alpha_ellipse变量。

命中测试,移动物体 - 图1

图:点击测试

移动和缩放

在下一部分中,我们将学习如何使用面板上的鼠标移动和缩放图形对象。 它可用于在我们的应用中移动和缩放图表,图表或其他各种对象。

MovingScalingEx.java

  1. package com.zetcode;
  2. import java.awt.Color;
  3. import java.awt.EventQueue;
  4. import java.awt.Font;
  5. import java.awt.Graphics;
  6. import java.awt.Graphics2D;
  7. import java.awt.RenderingHints;
  8. import java.awt.event.MouseAdapter;
  9. import java.awt.event.MouseEvent;
  10. import java.awt.event.MouseWheelEvent;
  11. import java.awt.event.MouseWheelListener;
  12. import java.awt.geom.Ellipse2D;
  13. import java.awt.geom.Rectangle2D;
  14. import javax.swing.JFrame;
  15. import javax.swing.JPanel;
  16. class Surface extends JPanel {
  17. private ZRectangle zrect;
  18. private ZEllipse zell;
  19. public Surface() {
  20. initUI();
  21. }
  22. private void initUI() {
  23. MovingAdapter ma = new MovingAdapter();
  24. addMouseMotionListener(ma);
  25. addMouseListener(ma);
  26. addMouseWheelListener(new ScaleHandler());
  27. zrect = new ZRectangle(50, 50, 50, 50);
  28. zell = new ZEllipse(150, 70, 80, 80);
  29. }
  30. private void doDrawing(Graphics g) {
  31. Graphics2D g2d = (Graphics2D) g;
  32. Font font = new Font("Serif", Font.BOLD, 40);
  33. g2d.setFont(font);
  34. g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
  35. RenderingHints.VALUE_ANTIALIAS_ON);
  36. g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
  37. RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
  38. g2d.setPaint(new Color(0, 0, 200));
  39. g2d.fill(zrect);
  40. g2d.setPaint(new Color(0, 200, 0));
  41. g2d.fill(zell);
  42. }
  43. @Override
  44. public void paintComponent(Graphics g) {
  45. super.paintComponent(g);
  46. doDrawing(g);
  47. }
  48. class ZEllipse extends Ellipse2D.Float {
  49. public ZEllipse(float x, float y, float width, float height) {
  50. setFrame(x, y, width, height);
  51. }
  52. public boolean isHit(float x, float y) {
  53. return getBounds2D().contains(x, y);
  54. }
  55. public void addX(float x) {
  56. this.x += x;
  57. }
  58. public void addY(float y) {
  59. this.y += y;
  60. }
  61. public void addWidth(float w) {
  62. this.width += w;
  63. }
  64. public void addHeight(float h) {
  65. this.height += h;
  66. }
  67. }
  68. class ZRectangle extends Rectangle2D.Float {
  69. public ZRectangle(float x, float y, float width, float height) {
  70. setRect(x, y, width, height);
  71. }
  72. public boolean isHit(float x, float y) {
  73. return getBounds2D().contains(x, y);
  74. }
  75. public void addX(float x) {
  76. this.x += x;
  77. }
  78. public void addY(float y) {
  79. this.y += y;
  80. }
  81. public void addWidth(float w) {
  82. this.width += w;
  83. }
  84. public void addHeight(float h) {
  85. this.height += h;
  86. }
  87. }
  88. class MovingAdapter extends MouseAdapter {
  89. private int x;
  90. private int y;
  91. @Override
  92. public void mousePressed(MouseEvent e) {
  93. x = e.getX();
  94. y = e.getY();
  95. }
  96. @Override
  97. public void mouseDragged(MouseEvent e) {
  98. doMove(e);
  99. }
  100. private void doMove(MouseEvent e) {
  101. int dx = e.getX() - x;
  102. int dy = e.getY() - y;
  103. if (zrect.isHit(x, y)) {
  104. zrect.addX(dx);
  105. zrect.addY(dy);
  106. repaint();
  107. }
  108. if (zell.isHit(x, y)) {
  109. zell.addX(dx);
  110. zell.addY(dy);
  111. repaint();
  112. }
  113. x += dx;
  114. y += dy;
  115. }
  116. }
  117. class ScaleHandler implements MouseWheelListener {
  118. @Override
  119. public void mouseWheelMoved(MouseWheelEvent e) {
  120. doScale(e);
  121. }
  122. private void doScale(MouseWheelEvent e) {
  123. int x = e.getX();
  124. int y = e.getY();
  125. if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
  126. if (zrect.isHit(x, y)) {
  127. float amount = e.getWheelRotation() * 5f;
  128. zrect.addWidth(amount);
  129. zrect.addHeight(amount);
  130. repaint();
  131. }
  132. if (zell.isHit(x, y)) {
  133. float amount = e.getWheelRotation() * 5f;
  134. zell.addWidth(amount);
  135. zell.addHeight(amount);
  136. repaint();
  137. }
  138. }
  139. }
  140. }
  141. }
  142. public class MovingScalingEx extends JFrame {
  143. public MovingScalingEx() {
  144. initUI();
  145. }
  146. private void initUI() {
  147. add(new Surface());
  148. setTitle("Moving and scaling");
  149. setSize(300, 300);
  150. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  151. setLocationRelativeTo(null);
  152. }
  153. public static void main(String[] args) {
  154. EventQueue.invokeLater(new Runnable() {
  155. @Override
  156. public void run() {
  157. MovingScalingEx ex = new MovingScalingEx();
  158. ex.setVisible(true);
  159. }
  160. });
  161. }
  162. }

在我们的代码示例中,我们有两个图形对象:一个矩形和一个圆形。 我们可以通过单击它们并拖动它们来移动它们。 我们还可以通过将鼠标光标放在对象上并移动鼠标滚轮来放大或缩小它们。

  1. private ZRectangle zrect;
  2. private ZEllipse zell;

正如我们已经提到的,面板上有一个矩形和一个椭圆。 这两个类都扩展了 Java AWT 包中内置类的功能。

  1. addMouseMotionListener(ma);
  2. addMouseListener(ma);
  3. addMouseWheelListener(new ScaleHandler());

我们注册了三个监听器。 这些监听器捕获鼠标按下,鼠标拖动和鼠标滚轮事件。

  1. class ZEllipse extends Ellipse2D.Float {
  2. public ZEllipse(float x, float y, float width, float height) {
  3. setFrame(x, y, width, height);
  4. }
  5. public boolean isHit(float x, float y) {
  6. return getBounds2D().contains(x, y);
  7. }
  8. ...
  9. }

这段代码摘录显示了ZEllipse类。 它扩展了内置的Ellipse2D.Float类。 它增加了缩放和移动椭圆的功能。 例如,isHit()方法确定鼠标指针是否在椭圆区域内。

MovingAdapter类处理鼠标按下和鼠标拖动事件。

  1. @Override
  2. public void mousePressed(MouseEvent e) {
  3. x = e.getX();
  4. y = e.getY();
  5. }

mousePressed()方法中,我们存储对象的初始 x 和 y 坐标。

  1. int dx = e.getX() - x;
  2. int dy = e.getY() - y;

doMove()方法内部,我们计算拖动对象的距离。

  1. if (zrect.isHit(x, y)) {
  2. zrect.addX(dx);
  3. zrect.addY(dy);
  4. repaint();
  5. }

如果在矩形区域内,则更新矩形的 x 和 y 坐标并重新绘制面板。

  1. x += dx;
  2. y += dy;

初始坐标将更新。

ScaleHandler类处理对象的缩放。

  1. if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
  2. if (zrect.isHit(x, y)) {
  3. float amount = e.getWheelRotation() * 5f;
  4. zrect.addWidth(amount);
  5. zrect.addHeight(amount);
  6. repaint();
  7. }
  8. ...
  9. }

如果移动鼠标滚轮,并且光标位于矩形区域内,则将调整矩形大小并重新绘制面板。 通过getWheelRotation()方法计算缩放比例,该方法返回车轮旋转量。

调整矩形大小

在下一个示例中,我们显示如何调整形状的大小。 我们的形状是一个矩形。 在矩形上,我们绘制了两个小的黑色矩形。 通过单击这些小矩形并拖动它们,我们可以调整主矩形的大小。

ResizingRectangleEx.java

  1. package com.zetcode;
  2. import java.awt.EventQueue;
  3. import java.awt.Graphics;
  4. import java.awt.Graphics2D;
  5. import java.awt.Point;
  6. import java.awt.event.MouseAdapter;
  7. import java.awt.event.MouseEvent;
  8. import java.awt.geom.Point2D;
  9. import java.awt.geom.Rectangle2D;
  10. import javax.swing.JFrame;
  11. import javax.swing.JPanel;
  12. class Surface extends JPanel {
  13. private Point2D[] points;
  14. private final int SIZE = 8;
  15. private int pos;
  16. public Surface() {
  17. initUI();
  18. }
  19. private void initUI() {
  20. addMouseListener(new ShapeTestAdapter());
  21. addMouseMotionListener(new ShapeTestAdapter());
  22. pos = -1;
  23. points = new Point2D[2];
  24. points[0] = new Point2D.Double(50, 50);
  25. points[1] = new Point2D.Double(150, 100);
  26. }
  27. private void doDrawing(Graphics g) {
  28. Graphics2D g2 = (Graphics2D) g;
  29. for (Point2D point : points) {
  30. double x = point.getX() - SIZE / 2;
  31. double y = point.getY() - SIZE / 2;
  32. g2.fill(new Rectangle2D.Double(x, y, SIZE, SIZE));
  33. }
  34. Rectangle2D r = new Rectangle2D.Double();
  35. r.setFrameFromDiagonal(points[0], points[1]);
  36. g2.draw(r);
  37. }
  38. @Override
  39. public void paintComponent(Graphics g) {
  40. super.paintComponent(g);
  41. doDrawing(g);
  42. }
  43. private class ShapeTestAdapter extends MouseAdapter {
  44. @Override
  45. public void mousePressed(MouseEvent event) {
  46. Point p = event.getPoint();
  47. for (int i = 0; i < points.length; i++) {
  48. double x = points[i].getX() - SIZE / 2;
  49. double y = points[i].getY() - SIZE / 2;
  50. Rectangle2D r = new Rectangle2D.Double(x, y, SIZE, SIZE);
  51. if (r.contains(p)) {
  52. pos = i;
  53. return;
  54. }
  55. }
  56. }
  57. @Override
  58. public void mouseReleased(MouseEvent event) {
  59. pos = -1;
  60. }
  61. @Override
  62. public void mouseDragged(MouseEvent event) {
  63. if (pos == -1) {
  64. return;
  65. }
  66. points[pos] = event.getPoint();
  67. repaint();
  68. }
  69. }
  70. }
  71. public class ResizingRectangleEx extends JFrame {
  72. public ResizingRectangleEx() {
  73. initUI();
  74. }
  75. private void initUI() {
  76. add(new Surface());
  77. setTitle("Resize rectangle");
  78. setSize(300, 300);
  79. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  80. setLocationRelativeTo(null);
  81. }
  82. public static void main(String[] args) {
  83. EventQueue.invokeLater(new Runnable() {
  84. @Override
  85. public void run() {
  86. ResizingRectangleEx ex = new ResizingRectangleEx();
  87. ex.setVisible(true);
  88. }
  89. });
  90. }
  91. }

有两种创建矩形的方法。 一种方法是提供左上角点的 x 和 y 坐标以及矩形的宽度和高度。 另一种方法是提供左上角和右下角点。 在我们的代码示例中,我们将同时使用这两种方法。

  1. private Point2D[] points;

在此数组中,我们存储构成矩形的点。

  1. private final int SIZE = 8;

这是黑色小矩形的大小。

  1. points = new Point2D[2];
  2. points[0] = new Point2D.Double(50, 50);
  3. points[1] = new Point2D.Double(150, 100);

这些是矩形的初始坐标。

  1. for (int i = 0; i < points.length; i++) {
  2. double x = points[i].getX() - SIZE / 2;
  3. double y = points[i].getY() - SIZE / 2;
  4. g2.fill(new Rectangle2D.Double(x, y, SIZE, SIZE));
  5. }

此代码绘制了两个小的控制矩形。

  1. Rectangle2D s = new Rectangle2D.Double();
  2. s.setFrameFromDiagonal(points[0], points[1]);
  3. g2.draw(s);

在这里,我们从这些点绘制一个矩形。

  1. @Override
  2. public void mousePressed(MouseEvent event) {
  3. Point p = event.getPoint();
  4. for (int i = 0; i < points.length; i++) {
  5. double x = points[i].getX() - SIZE / 2;
  6. double y = points[i].getY() - SIZE / 2;
  7. Rectangle2D r = new Rectangle2D.Double(x, y, SIZE, SIZE);
  8. if (r.contains(p)) {
  9. pos = i;
  10. return;
  11. }
  12. }
  13. }

mousePressed()方法中,我们确定是否单击了两个控制点之一。 如果我们点击其中一个,则pos变量将存储其中的哪个。

  1. @Override
  2. public void mouseDragged(MouseEvent event) {
  3. if (pos == -1) {
  4. return;
  5. }
  6. points[pos] = event.getPoint();
  7. repaint();
  8. }

在这里,矩形是动态调整大小的。 在mouseDragged()事件期间,我们获取当前点,更新点数组并重新绘制面板。

命中测试,移动物体 - 图2

图:缩放矩形

在 Java 2D 教程的这一部分中,我们介绍了命中测试和移动对象。