0

0

Java Swing绘图应用中图形重叠问题的根源与解决方案

心靈之曲

心靈之曲

发布时间:2025-09-24 14:44:02

|

389人浏览过

|

来源于php中文网

原创

Java Swing绘图应用中图形重叠问题的根源与解决方案

本文深入探讨了在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 对象的坐标会被更新:

@Override
public void mousePressed(MouseEvent e) {
    startPoint.setLocation(e.getPoint()); // 更新现有startPoint对象的坐标
}

@Override
public 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 的构造器时,每个图形实例将拥有其自己独立的坐标数据,不再受后续鼠标事件的影响。

闪念贝壳
闪念贝壳

闪念贝壳是一款AI 驱动的智能语音笔记,随时随地用语音记录你的每一个想法。

下载

方法二:在图形构造器中进行防御性拷贝

作为一种防御性编程实践,即使 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<PaintingPrimitive> primitives = new ArrayList<PaintingPrimitive>();

    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 方法
        }
    }
}

注意事项

  1. 对象引用与值传递: 理解Java中对象是按引用传递的至关重要。当一个对象作为参数传递给方法时,实际传递的是该对象的引用(地址),而不是对象本身的副本。因此,在方法内部对对象属性的修改会影响到原始对象。
  2. 防御性编程: 在构造器中进行防御性拷贝是一个良好的编程习惯,尤其是在处理可变对象(如 Point)时。它能有效防止外部对对象内部状态的意外修改,增强代码的健壮性和可维护性。
  3. 性能考虑: 每次创建新的 Point 对象会带来轻微的内存分配和垃圾回收开销。然而,对于大多数交互式绘图应用而言,这种开销通常可以忽略不计。只有在绘制极其大量的微小图形(例如每秒成千上万个)时,才需要考虑对象池或其他优化策略。
  4. SwingUtilities.invokeLater: 在 main 方法中创建 Swing UI 组件时,推荐使用 SwingUtilities.invokeLater(() -> new Painter());。这确保了UI组件的创建和更新都在事件分发线程(Event Dispatch Thread, EDT)上进行,避免潜在的线程安全问题。

总结

只显示最后一个图形的问题,通常是由于Java中对对象引用传递机制理解不足导致的。通过在鼠标事件处理器中每次创建新的 Point 实例,以及在图形构造器中进行防御性拷贝,我们可以确保每个图形对象都拥有独立的坐标数据,从而正确地在 JPanel 上显示所有绘制的图形。掌握这些概念对于开发健壮和可维护的Java Swing绘图应用程序至关重要。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

766

2023.08.10

Java 并发编程高级实践
Java 并发编程高级实践

本专题深入讲解 Java 在高并发开发中的核心技术,涵盖线程模型、Thread 与 Runnable、Lock 与 synchronized、原子类、并发容器、线程池(Executor 框架)、阻塞队列、并发工具类(CountDownLatch、Semaphore)、以及高并发系统设计中的关键策略。通过实战案例帮助学习者全面掌握构建高性能并发应用的工程能力。

100

2025.12.01

java值传递和引用传递有什么区别
java值传递和引用传递有什么区别

java值传递和引用传递的区别:1、基本数据类型的传递;2、对象的传递;3、修改引用指向的情况。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

109

2024.02.23

java值传递和引用传递有什么区别
java值传递和引用传递有什么区别

java值传递和引用传递的区别:1、基本数据类型的传递;2、对象的传递;3、修改引用指向的情况。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

109

2024.02.23

go语言引用传递
go语言引用传递

本专题整合了go语言引用传递机制,想了解更多相关内容,请阅读专题下面的文章。

175

2025.06.26

html5动画制作有哪些制作方法
html5动画制作有哪些制作方法

html5动画制作方法有使用CSS3动画、使用JavaScript动画库、使用HTML5 Canvas等。想了解更多html5动画制作方法相关内容,可以阅读本专题下面的文章。

550

2023.10.23

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

42

2026.03.13

Python异步编程与Asyncio高并发应用实践
Python异步编程与Asyncio高并发应用实践

本专题围绕 Python 异步编程模型展开,深入讲解 Asyncio 框架的核心原理与应用实践。内容包括事件循环机制、协程任务调度、异步 IO 处理以及并发任务管理策略。通过构建高并发网络请求与异步数据处理案例,帮助开发者掌握 Python 在高并发场景中的高效开发方法,并提升系统资源利用率与整体运行性能。

79

2026.03.12

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

234

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.4万人学习

C# 教程
C# 教程

共94课时 | 11.3万人学习

Java 教程
Java 教程

共578课时 | 82.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号