news 2026/10/9 3:54:53

Java Swing实战:从零实现一个简易画图工具

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Swing实战:从零实现一个简易画图工具

做Java一段时间后,难免会有这样的时刻:看了不少Spring Boot的服务端代码,却总觉得对“界面”二字的理解停留在HTML和浏览器层面。这时候,如果一个学生或者刚入行的新人跑来问我,想快速理解事件驱动编程、GUI架构、图形渲染这些概念,我通常会推荐他用Java Swing写一个简易画图工具。Swing是Java自带的标准GUI组件库,不需要额外引入任何依赖,一套代码能跨平台运行,而且它的事件监听、组件布局、自定义绘制机制,足够把Java基础里的面向对象、集合、异常处理全部串起来。这篇实战记录我就带着大家从头走一遍完整的开发流程——从窗口搭建、工具栏设计,到鼠标拖拽绘制直线、矩形、椭圆和手绘笔迹,再到颜色选择、清空画布和扩展保存图片功能,每一步都会解释清楚“为什么这么做”,也会把我在实际编码中踩过的坑和排查思路一并整理出来。不管你是正在找Java课程设计案例的学生,还是想在桌面开发领域补一块拼图的Java工程师,这篇内容应该都能帮到你。

1. 整体设计与思路拆解

1.1 为什么还用 Swing 做桌面画图工具

先说说选型。现在谈起Java GUI,JavaFX、TornadoFX、SWT、Qt Jambi这些方案都有,但Swing依然是学习桌面应用最简单直接的入口。原因有三:第一,Swing是Java SE的一部分,JVM装好就能跑,没有额外的SDK、Scene Builder之类的工具链负担;第二,Swing的知识体系高度结构化——容器、布局管理器、事件监听器、自定义组件,这些概念抽出来放到任何GUI框架里都通用;第三,Swing的绘制机制基于Graphics2D,和图像处理、游戏开发的底层绘图模型一致,学完以后看Canvas、OpenGL的概念都会有触类旁通的感觉。

当然,Swing在商业项目里的确已经边缘化了,大部分企业桌面应用要么转向Web端,要么用Electron这类方案。但正因为它边缘化,才更适合当教学工具:功能少而完整,没有框架魔法,所有行为都是显式调用。你要知道一个Hello World窗口为什么能显示出来,必须自己搞懂JFrame、事件派发线程、paint机制,这套底层逻辑在JavaFX里反而被封装得看不到了。这一点我强烈建议没有GUI经验的人先动手做一遍,不要被网上“Swing已死”的言论劝退。知识的价值不取决于框架的新旧,而在于它能不能帮你建立正确的模型,Java基础越是扎实的人,做完这个项目收获越大。

1.2 画图工具的功能边界与拆解

这个实验项目第一版的功能我控制在六个:

  • 选择绘制类型:直线、矩形、椭圆、手绘笔迹
  • 选择画笔颜色
  • 实时预览拖拽过程中的临时图形
  • 松开鼠标后图形固定到画布
  • 一键清空画布
  • 保存画布内容为PNG图片(可作为扩展)

为什么这样拆?因为每个功能背后都对应Swing里一个重要知识点。绘制类型对应Graphics2D的绘图方法;颜色对应JColorChooser;实时预览对应paintComponent叠加绘制临时状态;清空对应集合管理和重绘;保存图片对应BufferedImage和ImageIO。功能不多不少,刚好覆盖Swing桌面开发的主干场景。

这里要提醒一个常见的反面案例:很多人做课程设计时喜欢把功能堆得很满,什么剪切、粘贴、图层、多撤销都加进去。作为初学者,这样做往往会让代码变成一堆if-else,最后连自己都维护不了。画图工具的正确起点是先跑通“画一笔、显示一笔、存一笔”这个最小闭环,再谈扩展。功能边界越清晰,代码结构越简单,出问题的概率就越低。

1.3 用 MVC 视角看待这个小项目

虽然是一个简单的画图工具,但如果从MVC(Model-View-Controller)的角度去组织代码,后续扩展会非常舒服。在这个项目里:

  • Model:ShapeRecord这个内部类,记录了图形的类型、颜色、起点终点坐标(手绘则是点的集合)
  • View:JFrame主窗口 + JPanel画布,通过paintComponent把Model渲染出来
  • Controller:鼠标监听器把用户操作转换成Model的创建、修改和删除

你可能觉得一个画图工具用MVC有点杀鸡用牛刀,但实际写起来会发现,正是这种分工让代码清晰了不止一个量级。鼠标监听器只负责维护状态,不负责绘图;paintComponent只负责渲染,不负责修改数据;工具栏按钮只负责调Controller方法,不直接碰图形集合。每一步都各司其职,出问题的时候定位特别快——画面不对就去查View,数据不对就去查Model,交互不对就去查Controller。这种分层思考的习惯,是面向对象编程Java实践里最值得带走的财富。

2. 核心细节解析与实操要点

2.1 主窗口 JFrame 的搭建细节

窗口这块代码量不大,但三个细节很容易踩坑。第一,窗口关闭策略务必设置为EXIT_ON_CLOSE,否则你会遇到关窗口之后Java进程还赖在后台不退出的诡异现象;第二,主窗口建议先setSize再setLocationRelativeTo(null),这样才能居中显示;第三,JFrame默认的BorderLayout布局里,画布面板add到CENTER,工具栏add到NORTH,顺序别反。

还有一个细节往往被忽略:JPanel作为画布,必须调用setPreferredSize给它一个合理的初始尺寸。如果你不设置PreferredSize而只设置窗口的size,在某些布局组合下会出现画面被挤压成一小条、或者窗口打开后内容区和预期不符的情况。配合setResizable可以更可控,但第一版我建议保留缩放,让读者直观感受布局管理器的行为。

注意:JFrame的内容区默认有BorderLayout,add的时候不指定方位默认放在CENTER,多个组件不指定方位会互相覆盖。工具栏放NORTH,画布放CENTER,是这类工具类应用的标准姿势。

2.2 画布绘制:为什么必须重写 paintComponent

画布绘制的核心是重写JPanel的paintComponent方法,而不是paint方法。这两者的区别我多说一句:paint方法负责组件的完整绘制流程,包括边框、子组件,直接重写它容易破坏Swing内部的绘制顺序,造成各种闪屏和重叠bug;paintComponent则只负责这个组件自身的绘制内容,Swing保证双缓冲、自动裁剪这些基础设施已经就绪,你只管画自己的图形。所以在自定义组件时,一律重写paintComponent,并且第一行要先调用super.paintComponent(g)来清掉旧画面。

绘图对象要强转成Graphics2D,然后开启抗锯齿。这一步几乎必做,不开启的话直线和椭圆边缘会有明显的锯齿,观感非常业余。开启方式:

Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

如果你后续还要画填充图形,还可以顺手设置RenderingHints.KEY_STROKE_CONTROL,让线条宽度在不同缩放比例下表现得更稳定。Graphics2D本身是个状态机,setColor、setStroke设置后会影响后续所有绘制调用,所以每个图形绘制前都要确保状态是它自己的,而不是上一次残留的状态。

2.3 鼠标事件监听:压住、拖动、松开的完整闭环

画图工具的交互核心是一个三段式的鼠标事件闭环:mousePressed记录起点并创建临时图形,mouseDragged实时更新终点并触发重绘,mouseReleased把临时图形存入已完成列表并重新重绘。这里的重点是,拖拽过程中你需要的不仅是起点和终点,而是一个“临时图形”对象,每次都根据最新的鼠标坐标改变终点的值,然后repaint。只有松开鼠标以后,这个临时对象才被正式放进shapes集合,否则画布上只会出现一条不断变形的临时线,最终无法留存。

为什么不用mouseMoved而用mouseDragged?因为mouseMoved是鼠标在组件上移动但不按键时触发,用来做悬停效果可以,但画图时没按下鼠标不应该产生任何笔迹。你只需要注册MouseAdapter和MouseMotionAdapter两个监听器,或者分别用MouseListener和MouseMotionListener接口。每次监听器回调里携带的MouseEvent对象都包含getX()、getY()、getPoint()这些方法,拿到的坐标是相对于当前组件左上角的,正好可以直接用来绘图。

2.4 repaint 机制与状态管理

很多新手困惑的是:为什么我改了数据,画面不动?答案很简单,Swing的绘制并不是主动式的——组件不会因为数据变化自动刷新,你必须显式调用repaint(),它才会把重绘请求排到事件队列里,然后在合适的时机触发paintComponent。也就是说,数据、状态和画面的关系是“数据改变 -> 手动repaint -> 画面更新”,这中间漏掉任何一环都会导致图形不显示或者显示陈旧画面。

另外,repaint()可以带参调用,比如repaint(x, y, w, h)只刷新指定区域,理论上性能更好。实际画图场景里这个优化收益很小,我建议第一版一律直接repaint(),先把正确性跑通,性能优化永远排第二位。还有一个新手容易钻牛角尖的问题:paintComponent执行完要不要自己调用repaint?不要。Swing引擎会在需要的时候自动重绘,你只需要在状态变迁的时机主动通知它即可。

2.5 工具栏与交互控件的组织

工具栏这块我直接用一个JToolBar,横向摆放图形类型按钮、颜色按钮和清空按钮。JToolBar默认是可拖拽的,会有一个浮动手柄,在小程序里这个特性反而容易让人误操作,建议setFloatable(false)关掉。这里有个实践技巧:颜色按钮的背景色可以跟着当前颜色一起更新,用户一眼就能看出当前选中的是什么颜色,比只显示一个“颜色”文字要直观得多。按钮之间用addSeparator()分隔,视觉上更清晰。

图形类型切换用JToggleButton会更专业,因为按下状态能直观表示当前选中哪种形状。不过第一版用普通JButton也完全够用,等想做得更顺手了再换成ToggleButton也不迟。核心逻辑都在drawPanel.setShapeType(ShapeType.XXX)这一行,按钮只是触发状态变更的入口,这样设计最大的好处是:以后增加新图形类型时,只需要加一个枚举值和对应的按钮,其余代码完全不用动。

3. 实操过程与核心环节实现

3.1 搭建主窗口与工具栏

主窗口代码:

public class DrawingBoard extends JFrame { private DrawPanel drawPanel; public DrawingBoard() { setTitle("简易画图工具"); setSize(900, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JToolBar toolbar = new JToolBar(); toolbar.setFloatable(false); JButton lineBtn = new JButton("直线"); lineBtn.addActionListener(e -> drawPanel.setShapeType(ShapeType.LINE)); toolbar.add(lineBtn); JButton rectBtn = new JButton("矩形"); rectBtn.addActionListener(e -> drawPanel.setShapeType(ShapeType.RECT)); toolbar.add(rectBtn); JButton ovalBtn = new JButton("椭圆"); ovalBtn.addActionListener(e -> drawPanel.setShapeType(ShapeType.OVAL)); toolbar.add(ovalBtn); JButton penBtn = new JButton("手绘"); penBtn.addActionListener(e -> drawPanel.setShapeType(ShapeType.FREEHAND)); toolbar.add(penBtn); toolbar.addSeparator(); JButton colorBtn = new JButton("选择颜色"); colorBtn.addActionListener(e -> { Color selected = JColorChooser.showDialog(this, "选择画笔颜色", drawPanel.getCurrentColor()); if (selected != null) { drawPanel.setCurrentColor(selected); colorBtn.setBackground(selected); colorBtn.setOpaque(true); } }); toolbar.add(colorBtn); toolbar.addSeparator(); JButton clearBtn = new JButton("清空画布"); clearBtn.addActionListener(e -> drawPanel.clearAllShapes()); toolbar.add(clearBtn); add(toolbar, BorderLayout.NORTH); drawPanel = new DrawPanel(); drawPanel.setBackground(Color.WHITE); drawPanel.setPreferredSize(new Dimension(900, 550)); add(drawPanel, BorderLayout.CENTER); } public static void main(String[] args) { SwingUtilities.invokeLater(() -> new DrawingBoard().setVisible(true)); } }

代码里有几个地方值得展开说明。构造方法里所有组件初始化完成后再add,而不是边创建边add,这样避免半初始化状态下的重绘。lambda表达式的使用简化了ActionListener的写法,这是Java基础里函数式接口的经典应用——ActionListener本身就是只有一个方法的函数式接口,所以lambda可以直接替换匿名内部类。

main方法里用SwingUtilities.invokeLater把窗口创建任务丢到事件派发线程(EDT)上执行,这是Swing的黄金法则——所有UI操作必须在EDT上发生,否则可能出现线程安全问题,窗口偶尔显示不出来或者界面卡死。很多新手写的Swing程序没有这一行也能跑,是因为main线程恰好没撞上并发修改,但这纯属运气,不是正确做法。

最后一个细节:setVisible(true)要放在整个构造过程的最末尾,不是第一行。因为setVisible一旦调用,界面立即会被渲染,如果你在这之后再修改组件状态,就会造成界面闪烁甚至内容错乱。

3.2 画布与图形数据结构

画布这边核心数据结构是DrawPanel内部的shapes集合,存放所有已经完成绘制的图形。每个图形用内部类ShapeRecord表示:

static class ShapeRecord { ShapeType type; Color color; int startX, startY; int endX, endY; List<Point> points; ShapeRecord(ShapeType type, Color color) { this.type = type; this.color = color; } }

这里用int坐标而不是Point,是因为ShapeRecord要频繁更新endX/endY,直接用字段比包装对象访问更快也更直观。points字段专门服务手绘模式,其他三种图形模式不适用。图形类型用枚举:

enum ShapeType { LINE, RECT, OVAL, FREEHAND }

用枚举而不是字符串的好处是编译期就能发现拼写错误,switch分支也能保证覆盖所有情况。这种用枚举表达类型的方式,是Java基础里面向对象设计思想的一个典型应用,比用字符串常量安全得多。这里分享一个小经验:如果以后要做撤销功能,除了shapes列表,你再加一个history栈,每次add之前先push一份旧状态,撤销时从栈里弹出来重新repaint就行,数据结构上完全不需要改动ShapeRecord。

3.3 鼠标交互三阶段实现

实现鼠标交互时我同时添加MouseListener和MouseMotionListener两个监听器。下面是把两者结合在一起的写法:

drawPanel.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { startX = e.getX(); startY = e.getY(); tempShape = new ShapeRecord(currentType, currentColor); tempShape.startX = startX; tempShape.startY = startY; tempShape.endX = startX; tempShape.endY = startY; if (currentType == ShapeType.FREEHAND) { tempShape.points = new ArrayList<>(); tempShape.points.add(e.getPoint()); } } @Override public void mouseReleased(MouseEvent e) { if (tempShape == null) { return; } tempShape.endX = e.getX(); tempShape.endY = e.getY(); if (currentType == ShapeType.FREEHAND) { tempShape.points.add(e.getPoint()); } shapes.add(tempShape); tempShape = null; repaint(); } }); drawPanel.addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseDragged(MouseEvent e) { if (tempShape == null) { return; } tempShape.endX = e.getX(); tempShape.endY = e.getY(); if (currentType == ShapeType.FREEHAND) { tempShape.points.add(e.getPoint()); } repaint(); } });

这套逻辑演进过程是这样的:按下鼠标,创建一个临时图形并让起点等于终点,此时画布上预期什么都还看不到或只看到一个点;拖动过程中,终点跟随鼠标位置实时变化并持续repaint,这样用户就能看到图形“被拉出来”的预览效果;松开鼠标,临时图形的终点被最终确定,图形加入已完成列表,tempShape清空。注意mouseReleased里的repaint不能省,因为此时tempShape已经变成null,如果不重绘,画面停留在最后一个拖拽的状态,图形内容虽然存在却不会显示。

手绘模式的points集合的处理手法是:按住鼠标期间的每个move事件产生的点都追加进列表,最后在paintComponent里把这些点用drawLine连接成折线。点足够密集时,视觉上就是一条连续的自由笔迹。如果点过疏,画出来会有棱角,这时候可以在mouseDragged里做插值,或者减小鼠标移动频率,但第一版直接收集原始点就够用了。

3.4 绘制渲染与图形坐标修正

paintComponent里的实现是整套代码的另一个重点:

@Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); for (ShapeRecord shape : shapes) { drawShape(g2, shape); } if (tempShape != null) { drawShape(g2, tempShape); } } private void drawShape(Graphics2D g2, ShapeRecord shape) { g2.setColor(shape.color); int x = Math.min(shape.startX, shape.endX); int y = Math.min(shape.startY, shape.endY); int w = Math.abs(shape.endX - shape.startX); int h = Math.abs(shape.endY - shape.startY); switch (shape.type) { case LINE: g2.drawLine(shape.startX, shape.startY, shape.endX, shape.endY); break; case RECT: g2.drawRect(x, y, w, h); break; case OVAL: g2.drawOval(x, y, w, h); break; case FREEHAND: List<Point> pts = shape.points; for (int i = 1; i < pts.size(); i++) { Point p1 = pts.get(i - 1); Point p2 = pts.get(i); g2.drawLine(p1.x, p1.y, p2.x, p2.y); } break; } }

矩形和椭圆用Math.min和Math.abs做了坐标修正,这一步很关键。因为用户可以从左上往右下拖,也可以从右下往左上拖,如果直接拿startX和endX计算宽高,窗口就会出现负数的宽高,绘制结果会完全错乱。对于需要填充颜色的场景,同样的修正逻辑同样适用,区别只在drawRect改成fillRect、drawOval改成fillOval。如果以后要支持填充模式,这里只需加一个boolean字段,然后在绘制时判断一下就行。

另外,Graphics2D支持设置Stroke来控制线条粗细。想加画笔粗细功能的话,在drawShape里加一句g2.setStroke(new BasicStroke(shape.strokeWidth))即可,这个扩展非常自然。同样要提醒的是,Graphics2D的状态是持久的,每次绘制前把Color、Stroke都设置好,绘制完不用刻意复原,因为下一次drawShape会重新覆盖这些状态。

3.5 颜色选择、清空与保存图片

颜色选择用JColorChooser.showDialog这个静态方法最省事,它会弹出一个系统风格的颜色选择对话框。这里有个小坑:showDialog返回null表示用户点了取消,这时候不要应用任何变化,否则会把颜色误设置成黑色。清空画布的实现本质就是shapes.clear(),然后马上调用repaint()。代码很简单,但很多人在这个环节翻车——只clear了集合忘了repaint,画布看起来毫无变化,还以为是清空按钮没生效。

保存图片用BufferedImage把画布内容捞出来就行:

BufferedImage image = new BufferedImage( drawPanel.getWidth(), drawPanel.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D canvas = image.createGraphics(); drawPanel.paint(canvas); canvas.dispose(); ImageIO.write(image, "png", new File("drawing.png"));

这个方法直接调用JPanel的paint方法把当前画面渲染到内存图像上,十行不到就能实现导出功能。注意这里的paint调用不是重写,而是主动调用父类的绘制流程,它会把整个组件体系连同背景一起渲染进BufferedImage。想保存为JPG的话,把TYPE_INT_ARGB换成TYPE_INT_RGB,文件后缀改成jpg,ImageIO会自动处理编码格式。这里每次导出都写死文件名不太优雅,但作为课程设计完全够用,等以后想完善了再换JFileChooser也不迟。

4. 常见问题与排查技巧实录

4.1 画完图形消失不见,或者只显示最后一个图形

这个问题的根源几乎都是shapes集合或者重绘逻辑出了问题。最常见的三种:一,鼠标拖动过程中一直新建ShapeRecord而不是更新tempShape,于是每帧都新增一个临时对象,内存又涨界面又乱;二,mouseReleased里没有把tempShape加入shapes,图形画完就扔掉了;三,shapes列表被重新赋值成了新对象(比如shapes = new ArrayList<>()),而paintComponent读取的是旧引用。排查的时候先看paintComponent里遍历的是哪个集合,再确认监听器里修改的是不是同一个引用。

这种问题最有效的排查手段是加日志。在mouseRelease和paintComponent里分别打印shapes.size(),如果paintComponent打印出的数量比release后的数量少,说明遍历和写入用的不是同一个集合;如果数量一直涨但画面不变,说明重绘链路断了。日志排在System.out就行,不需要引入任何日志框架。

4.2 拖拽时图形残影、闪烁严重

闪屏和残影,90%原因是没有调用super.paintComponent(g)。这个方法会先填充默认背景色,把上一帧残留的像素清干净。如果不调用它,画布的背景不会自动清除,新画面叠加在旧画面之上,看起来就像拖了长长的尾巴。Swing默认开启了双缓冲,正常重写paintComponent不会闪,但如果你重写的是paint并且没有正确处理子组件,就可能绕过双缓冲机制,出现肉眼可见的闪烁。

另一个容易忽略的情况:面板的背景色设置。有些人为了画布好看用彩色背景,绘图颜色又恰好接近背景,这时候残影会特别明显。建议画布保持白色背景,用户选的其他颜色才能正常分辨。如果确实需要深色画布,记得把绘制颜色的默认值也调整成浅色系,否则黑色画笔在黑色背景上怎么画都看不见。

4.3 鼠标事件响应异常:有时绘制有时不绘制

如果你发现画图工具时灵时不灵,先检查是不是有某个透明组件遮挡在画布上方。比如工具栏里某些按钮设置成了不透明但未设置背景,或者画画时误把鼠标拖到了工具栏上,事件就会发给工具栏而不是画布。这时候要么把遮挡组件setOpaque(false),要么监听器只响应画布自身的坐标范围并做边界判断。

还有一点,MouseAdapter和MouseMotionAdapter是两个不同的接口,要分别add。只addMouseListener而不addMouseMotionListener,拖动事件永远不会触发;只addMouseMotionListener而不addMouseListener,则没有起点状态,画出来的东西没有确定的初始坐标。如果你发现图形跳到了左上角再展开,多半是startX和startY在mousePressed里没有被正确赋值,初始值一直是0。

4.4 常见问题速查表

现象直接原因排查与解决
画面完全不显示没有调用repaint,或组件没有add进窗口检查数据修改后是否有repaint;检查paintComponent是否被正确覆写
图形残影/拖尾paintComponent没调用super,或背景未清理确保第一行调用super.paintComponent(g)
只有边角出现图形布局和尺寸异常,组件被挤压给画布设置PreferredSize,检查add位置是否正确
拖动过程中图形不走只监听了mousePressed没监听mouseDragged同时注册MouseMotionListener
关闭窗口进程不退出setDefaultCloseOperation未设置设置为JFrame.EXIT_ON_CLOSE
颜色按钮点击没反应忽略了showDialog返回null先判断selected是否为空再应用颜色
导出图片全黑没调用完整paint流程,或Graphics2D未dispose使用paint而非paintComponent,完成后dispose资源

这张表基本覆盖了新手阶段能遇到的大部分问题,我把它们整理出来而不是零散写在代码注释里,就是希望大家做项目时先有个预期:遇到问题不要慌,按现象找原因,比从头读一遍代码快得多。

4.5 新手最容易忽略的三个进阶坑

第一,不要在paintComponent里做耗时操作。绘制代码里如果加载了图片、解析了文件,界面会明显卡顿。Swing的重绘是高频操作,耗时逻辑全部放到按钮触发的事件回调里异步处理。第二,如果在事件监听器里修改了画布尺寸或者添加了组件,记得validate()或者revalidate(),否则新尺寸可能不会生效,界面看起来还是旧的。第三,开发时建议开着控制台看异常输出,很多图形不显示的问题其实是因为空指针异常被Swing吞进了事件线程的日志里,不仔细看根本发现不了。这三条我在自己的项目里都真实踩过,尤其是第一条,当时直接在paintComponent里读取了一个图片文件,整个界面直接卡成PPT。

5. 扩展方向与个人经验

5.1 值得尝试的扩展功能

整套代码跑通以后,我建议下一步尝试做这几个扩展:撤销/重做功能、画笔粗细调整、图形选中与拖动、填充模式、保存为JPG格式、橡皮擦。其中撤销/重做只需要在shapes列表操作上叠加历史栈管理,代码量增加很少,但理解起来对设计模式训练很有帮助。橡皮擦的实现也不是真去擦像素,而是用画布背景色画一条与背景同色的线,这个偷懒方案在矢量绘图工具里其实很常见。

至于图形选中与拖动,这个稍微复杂一点,要自己实现命中检测,也就是判断鼠标点击点落在哪个图形的边界范围内。直线用距离公式,矩形和椭圆用包含判断。做完这一步,你就踏进了图形编辑器的核心领域,再往后做缩放、旋转,底层思路都是一样的。

5.2 做完这个项目后我对 Swing 的重新认识

我个人做这个项目最大的体会是:Swing虽然老了,但它把桌面应用开发的底层模型暴露得足够清晰。做完这几十行代码,你会真正理解什么是事件队列、什么是渲染管线、为什么UI框架都要强调“单线程模型”。这套底层的思维方式,做任何GUI或者客户端开发都用得上。很多人跳过了这个阶段直接去学JavaFX或者Web前端,虽然也能上手,但遇到性能问题、闪烁问题、线程安全问题的时候,经常一头雾水,不知道问题出在哪个环节。

反过来看,做完画图工具之后再去看任何GUI框架的文档,你会发现几乎是老朋友——只是容器换了个名字,监听器换了个接口,但“组件树 + 布局 + 事件 + 重绘”这个骨架完全一致。这就是我推荐这个项目的最核心理由:它是桌面开发通用模型的一个极佳剖面。

5.3 给新手的三个学习建议

每次有朋友或学生问我这个项目怎么做,我都会附上三条建议。第一,不要复制粘贴代码,一定要自己手敲一遍,敲的过程中你会自然理解每一行负责什么,出错时也能迅速定位;第二,先实现最基本的功能闭环,再逐步加装饰,一开始就追求完美反而容易卡在细节里出不来;第三,每完成一个功能就运行测试一次,不要攒到最后统一调试。画图工具的调试尤其方便——能不能画出图形、颜色对不对、清空是否生效,都是肉眼可见的,这本身就是最好的反馈循环。

最后分享一个小技巧:写这个工具的过程中,把paintComponent里绘制临时图形和完成图形的逻辑抽成同一个drawShape方法,代码整洁程度会高很多。这个抽取动作本身也是面向对象设计里“单一职责”的一个朴素实践——同一个方法只做同一件事,多个调用方共享它,改一处全部生效。初学者建议认真感受一下这种“写着写着代码自己就变清晰了”的过程,这是从写代码到设计代码之间,最实在的一次跨越。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 3:54:52

claude-mem:为Claude Code构建跨会话记忆层的深度教程

我大概花了三个星期折腾 claude-mem&#xff0c;才真正搞明白它跟“给 AI 塞一段 prompt”完全是两码事。最开始的场景特别实在&#xff1a;用 Claude Code 写一个多模块的项目&#xff0c;上一轮聊完目录结构、约束条件&#xff0c;下一轮开新会话&#xff0c;模型全忘了。文件…

作者头像 李华
网站建设 2026/10/9 3:53:19

风电功率预测:CNN-LSTM融合模型实战指南

简介&#xff1a;本资源是一套面向本科及以上层次学习者与科研初学者的风电功率预测实践方案&#xff0c;基于MATLAB实现CNN-LSTM混合神经网络建模&#xff0c;聚焦新能源场景下的时序功率预测问题&#xff0c;适用于电力系统分析、智能运维及毕业设计等实际需求。压缩包共45个…

作者头像 李华
网站建设 2026/10/9 3:53:18

Agent-Reach:面向LLM应用的CLI+API工作流调度中枢

1. “Agent-Reach”不是新模型&#xff0c;而是一套面向开发者的工作流调度中枢你搜“Agent-Reach”&#xff0c;首页跳出来的全是CLI、API、YouTube、Reddit这些词——没有论文、没有官网、没有GitHub star数破万的仓库&#xff0c;甚至没有一句像样的产品介绍。这很反常。我第…

作者头像 李华
网站建设 2026/10/9 3:53:02

Agent-Reach:CLI 工具链的声明式调度中枢

1. Agent-Reach 不是新玩具&#xff0c;而是 CLI 工具链的“调度中枢”你有没有遇到过这种场景&#xff1a;刚用zcode cli调通了智谱的 API&#xff0c;转头想把结果喂给comfyui做图&#xff0c;却发现得手动复制粘贴、改 JSON 格式、再塞进另一个命令行参数里&#xff1b;或者…

作者头像 李华
网站建设 2026/10/9 3:52:49

去中心化算力打破算力困局:Codigger分布式计算生态解析

算力紧张这件事&#xff0c;这两年凡是碰过 AI、渲染、数据训练的人应该都有体感。单机跑不动大模型&#xff0c;云上租 GPU 又贵又怕被绑定&#xff0c;本地机房扩容的周期和成本更是劝退。我自己也踩过几次坑&#xff0c;训练任务排着队等资源&#xff0c;高峰期只能干瞪眼。…

作者头像 李华
网站建设 2026/10/9 3:52:49

Three.js数字孪生实战:从模型轻量化到性能优化全记录

1. 数字孪生项目为什么折腾了三个月还在TODO&#xff1a;先说结论1.1 项目最初的真实形态&#xff1a;先用Three.js做一个“能动就行”的看板我不是那种拿到PRD就大干快上的人&#xff0c;但这个需求确实是被逼出来的。客户手里有一栋楼的完整BIM模型&#xff0c;Revit导出来的…

作者头像 李华