
本文深入探讨了在Java Swing绘图应用中,当使用JPanel和JFrame绘制线条和圆形时,只显示最后一个图形而非所有图形的常见问题。核心原因在于图形坐标点对象的引用传递不当,导致所有绘制的图形都共享同一组坐标点。教程将详细解释这一问题,并提供两种有效的解决方案:在鼠标事件处理器中创建新的坐标点实例,以及在图形构造器中进行防御性拷贝,确保每个图形拥有独立的坐标数据,从而正确地显示所有绘制的图形。
理解问题:为何只显示最后一个图形?
在java swing应用程序中,尤其是在自定义绘图组件时,一个常见的问题是,即使我们向绘图列表中添加了多个图形对象,最终屏幕上却只显示最后绘制的那一个,或者所有图形都重叠在同一个位置。这通常不是绘图逻辑本身的错误,而是由于对java中对象引用传递机制的误解。
以本案例为例,Painter 类中定义了两个 Point 类型的成员变量 startPoint 和 endPoint:
public class Painter implements ActionListener, MouseListener, MouseMotionListener { // ... Point startPoint = new Point(); // 初始化的Point对象 Point endPoint = new Point(); // 初始化的Point对象 // ...}
在鼠标事件处理方法 mousePressed 和 mouseReleased 中,这些 Point 对象的坐标会被更新:
@Overridepublic void mousePressed(MouseEvent e) { startPoint.setLocation(e.getPoint()); // 更新现有startPoint对象的坐标}@Overridepublic void mouseReleased(MouseEvent e) { endPoint.setLocation(e.getPoint()); // 更新现有endPoint对象的坐标 if (object == 0) { canvas.addPrimitive(new Line(startPoint, endPoint, temp)); // 将startPoint和endPoint传递给Line对象 } // ...}
问题就出在这里。startPoint 和 endPoint 在 Painter 类的生命周期中只被初始化了一次。每次鼠标按下或释放时,setLocation() 方法仅仅是修改了这两个 现有 Point 对象的内部坐标值,而不是创建新的 Point 对象。
当 canvas.addPrimitive(new Line(startPoint, endPoint, temp)) 被调用时,Line 类的构造函数接收的是 Painter 类中 startPoint 和 endPoint 这两个 相同 Point 对象的引用。这意味着,无论你创建多少个 Line 或 Circle 对象,它们内部存储的 startPoint 和 endPoint 引用都指向 Painter 类中那两个唯一的 Point 实例。
立即学习“Java免费学习笔记(深入)”;
因此,每次用户绘制新图形时,Painter 类的 startPoint 和 endPoint 会被更新到最新的鼠标位置。由于所有先前创建的 Line 或 Circle 对象都引用着这两个相同的 Point 实例,当 paintComponent 方法被调用并遍历 primitives 列表进行绘制时,所有图形都会根据 startPoint 和 endPoint 的 当前 值(即最后一次鼠标释放时的值)进行绘制,从而导致所有图形都重叠在最后绘制的位置。
解决方案:确保每个图形拥有独立的坐标
要解决这个问题,核心思想是确保每个绘制的图形对象都拥有其自己独立的坐标数据,而不是共享同一个 Point 实例。这可以通过两种方式实现:
方法一:在事件处理器中创建新的 Point 实例
最直接的解决方案是在 mousePressed 和 mouseReleased 方法中,每次都创建新的 Point 对象来存储当前的鼠标位置,而不是修改现有的 startPoint 和 endPoint 成员变量。
修改 Painter 类中的 mousePressed 和 mouseReleased 方法如下:
public class Painter implements ActionListener, MouseListener, MouseMotionListener { // ... // startPoint 和 endPoint 仍然可以是成员变量,但现在它们将在每次事件中被重新赋值为新对象 Point startPoint; Point endPoint; // ... @Override public void mousePressed(MouseEvent e) { // 创建一个新的 Point 实例来存储鼠标按下的位置 startPoint = new Point(e.getPoint()); } @Override public void mouseReleased(MouseEvent e) { // 创建一个新的 Point 实例来存储鼠标释放的位置 endPoint = new Point(e.getPoint()); if (object == 0) { // 现在传递给 Line 构造器的是新创建的、独立的 Point 对象 canvas.addPrimitive(new Line(startPoint, endPoint, temp)); } if (object == 1){ // 同样,Circle 也会接收到独立的 Point 对象 canvas.addPrimitive(new Circle(startPoint, endPoint, temp)); } canvas.repaint(); } // ...}
通过这种修改,每次鼠标事件发生时,startPoint 和 endPoint 都会指向一个新的 Point 对象。当这些新的 Point 对象被传递给 Line 或 Circle 的构造器时,每个图形实例将拥有其自己独立的坐标数据,不再受后续鼠标事件的影响。
方法二:在图形构造器中进行防御性拷贝
作为一种防御性编程实践,即使 Painter 类已经创建了新的 Point 实例,在 Line(以及 Circle)的构造器中对传入的 Point 对象进行“防御性拷贝”也是一个好习惯。这意味着,Line 对象不会直接存储传入的 Point 引用,而是创建一个新的 Point 对象,并用传入 Point 的值进行初始化。
这样做的好处是,即使外部代码(例如 Painter 类)不小心修改了传递给 Line 构造器的 Point 对象,Line 实例内部存储的坐标也不会受到影响,从而保证了图形的独立性和数据完整性。
修改 Line 类(以及 Circle 类)的构造器如下:
import java.awt.Graphics;import java.awt.Point;import java.awt.Color;public class Line extends PaintingPrimitive{ Point startPoint; // 声明为成员变量,但不在声明时初始化,而是在构造器中初始化 Point endPoint; public Line(Point start, Point end, Color c) { super(c); // 对传入的 Point 对象进行防御性拷贝,确保 Line 实例拥有独立的 Point 数据 this.startPoint = new Point(start); this.endPoint = new Point(end); } public void drawGeometry(Graphics g) { System.out.println("draw geo called"); g.drawLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y); } @Override public String toString() { return "Line"; }}
通过这种修改,Line 对象内部持有的 startPoint 和 endPoint 将是其私有的副本,与外部任何 Point 对象都无关。这提供了更强的封装性和健壮性。
整合修改后的关键代码示例
为了清晰起见,以下是整合了上述两种修改方案的关键代码片段:
Painter 类 (部分修改)
import java.awt.*;import java.awt.event.*;import javax.swing.*;import java.util.ArrayList; // 假设 PaintingPanel 在同一文件或可访问public class Painter implements ActionListener, MouseListener, MouseMotionListener { // ... 其他成员变量 Color temp = Color.RED; int object = 0; // 0 = line, 1 = circle PaintingPanel canvas; // 不再在声明时初始化,而是在 mousePressed/Released 中赋值新对象 Point startPoint; Point endPoint; Painter() { // ... 构造器中的其他UI初始化代码 JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(500, 500); // ... 其他面板和按钮设置 canvas = new PaintingPanel(); // ... 将 canvas 添加到 holder // 注册监听器 // ... 按钮监听器 canvas.addMouseListener(this); // 将鼠标监听器添加到 canvas 上 // ... frame.setContentPane(holder); frame.setVisible(true); } // ... actionPerformed, mouseDragged, mouseMoved, mouseClicked, mouseEntered, mouseExited 方法 @Override public void mousePressed(MouseEvent e) { // 关键修改:每次按下鼠标时创建新的 Point 对象 startPoint = new Point(e.getPoint()); } @Override public void mouseReleased(MouseEvent e) { // 关键修改:每次释放鼠标时创建新的 Point 对象 endPoint = new Point(e.getPoint()); if (object == 0) { canvas.addPrimitive(new Line(startPoint, endPoint, temp)); } if (object == 1){ canvas.addPrimitive(new Circle(startPoint, endPoint, temp)); } canvas.repaint(); // 通知 PaintingPanel 重新绘制 } public static void main(String[] args) { SwingUtilities.invokeLater(() -> new Painter()); // 推荐在事件分发线程中创建UI }}
Line 类 (部分修改)
import java.awt.Graphics;import java.awt.Point;import java.awt.Color;public class Line extends PaintingPrimitive{ Point startPoint; // 不再在声明时初始化 Point endPoint; // 不再在声明时初始化 public Line(Point start, Point end, Color c) { super(c); // 关键修改:在构造器中进行防御性拷贝 this.startPoint = new Point(start); this.endPoint = new Point(end); } public void drawGeometry(Graphics g) { // System.out.println("draw geo called"); // 调试信息可以移除 g.drawLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y); } @Override public String toString() { return "Line"; }}
PaintingPanel 类 (无需修改,但为了完整性列出)
import java.util.ArrayList;import javax.swing.JPanel;import java.awt.Graphics;import java.awt.Color;public class PaintingPanel extends JPanel { ArrayList primitives = new ArrayList(); PaintingPanel() { setBackground(Color.WHITE); } public void addPrimitive(PaintingPrimitive obj) { primitives.add(obj); // this.repaint(); // 可以在 Painter 中统一调用,或者在这里调用,确保UI更新 } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 始终调用父类的 paintComponent 来清空背景等 for (PaintingPrimitive shape : primitives) { // g.drawLine(0,0,100,100); // 这行调试代码可以移除 shape.draw(g); // 调用每个图形的 draw 方法 } }}
注意事项
对象引用与值传递: 理解Java中对象是按引用传递的至关重要。当一个对象作为参数传递给方法时,实际传递的是该对象的引用(地址),而不是对象本身的副本。因此,在方法内部对对象属性的修改会影响到原始对象。防御性编程: 在构造器中进行防御性拷贝是一个良好的编程习惯,尤其是在处理可变对象(如 Point)时。它能有效防止外部对对象内部状态的意外修改,增强代码的健壮性和可维护性。性能考虑: 每次创建新的 Point 对象会带来轻微的内存分配和垃圾回收开销。然而,对于大多数交互式绘图应用而言,这种开销通常可以忽略不计。只有在绘制极其大量的微小图形(例如每秒成千上万个)时,才需要考虑对象池或其他优化策略。SwingUtilities.invokeLater: 在 main 方法中创建 Swing UI 组件时,推荐使用 SwingUtilities.invokeLater(() -> new Painter());。这确保了UI组件的创建和更新都在事件分发线程(Event Dispatch Thread, EDT)上进行,避免潜在的线程安全问题。
总结
只显示最后一个图形的问题,通常是由于Java中对对象引用传递机制理解不足导致的。通过在鼠标事件处理器中每次创建新的 Point 实例,以及在图形构造器中进行防御性拷贝,我们可以确保每个图形对象都拥有独立的坐标数据,从而正确地在 JPanel 上显示所有绘制的图形。掌握这些概念对于开发健壮和可维护的Java Swing绘图应用程序至关重要。
以上就是Java Swing绘图应用中图形重叠问题的根源与解决方案的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/62236.html
微信扫一扫
支付宝扫一扫