java源码使用教程:新手完成第一张作品的方法

java源码使用教程:新手完成第一张作品的方法

java源码使用教程:新手完成第一张作品的方法

第一次接触 Java 源码,容易把注意力全放在语法和开发工具上,最后却没有一个看得见的结果。更适合入门的方式,是先完成一个范围明确的小作品:它能运行、有交互,最后还能保存下来。

本文会带你用 Java 自带的 Swing 和 Java2D 做一个简易绘图板。你可以选择画笔颜色,在画布上拖动绘画,清空画布,再把作品导出为 PNG 图片。程序只有一个 Java 源文件,不依赖第三方库,适合用来熟悉源码、事件监听、图形绘制和文件输出。

一、先明确这份示例适合做什么

这个绘图板适合学习 Java 桌面图形程序的基本结构,也适合做简单的手绘草图、涂鸦或课堂演示。它使用 Swing 创建窗口和按钮,用 Java2D 在内存中的图片上画线,再通过 ImageIO 输出 PNG。

它不是专业绘图软件,也不适合作为大型绘画项目的基础版本。当前实现没有图层、撤销、缩放、压力感应、自动保存或矢量编辑功能;画笔宽度固定,画布尺寸固定,作品以 PNG 位图保存。理解这个边界很重要:先完成可运行的小作品,再决定是否要增加功能。

二、运行前需要准备什么

需要安装 Java Development Kit(JDK),而不只是运行 Java 程序所需的运行环境。建议使用仍在维护的 JDK 版本,例如 JDK 17 或更高版本。Swing 和 Java2D 属于 JDK 提供的桌面图形相关 API,不需要另外下载绘图库。

安装完成后,在终端或命令提示符中检查:

java -version
javac -version

如果系统提示找不到命令,通常是 JDK 没有安装成功,或 JDK 的 bin 目录没有配置到系统 PATH。也可以先不调整 PATH,直接使用 JDK 安装目录中的 java 和 javac 可执行文件。

本文的程序需要图形桌面环境。若在没有图形界面的服务器、容器或纯命令行环境中运行,Swing 窗口可能无法显示;这不代表绘图代码本身一定有问题。

三、创建源文件并粘贴代码

新建一个名为 FirstArtwork.java 的文件。文件名要与下面的 public class FirstArtwork 一致,大小写也要一致。将以下代码完整复制进去:

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileNameExtensionFilter;

public class FirstArtwork {
    private static final int CANVAS_WIDTH = 900;
    private static final int CANVAS_HEIGHT = 600;
    private static final float BRUSH_WIDTH = 8.0f;

    private final DrawingPanel drawingPanel = new DrawingPanel();
    private Color brushColor = Color.BLACK;

    private void createAndShowWindow() {
        JFrame frame = new JFrame("我的第一张作品");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(new BorderLayout());

        JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT));

        JButton colorButton = new JButton("选择颜色");
        colorButton.addActionListener(event -> {
            Color selected = JColorChooser.showDialog(
                    frame, "选择画笔颜色", brushColor);
            if (selected != null) {
                brushColor = selected;
            }
        });

        JButton clearButton = new JButton("清空画布");
        clearButton.addActionListener(event -> drawingPanel.clear());

        JButton saveButton = new JButton("保存 PNG");
        saveButton.addActionListener(event -> saveArtwork(frame));

        toolbar.add(colorButton);
        toolbar.add(clearButton);
        toolbar.add(saveButton);

        frame.add(toolbar, BorderLayout.NORTH);
        frame.add(drawingPanel, BorderLayout.CENTER);
        frame.pack();
        frame.setResizable(false);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    private void saveArtwork(JFrame frame) {
        JFileChooser chooser = new JFileChooser();
        chooser.setDialogTitle("保存作品");
        chooser.setFileFilter(
                new FileNameExtensionFilter("PNG 图片 (*.png)", "png"));

        if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
            return;
        }

        File selectedFile = chooser.getSelectedFile();
        String fileName = selectedFile.getName();
        if (!fileName.toLowerCase().endsWith(".png")) {
            selectedFile = new File(
                    selectedFile.getParentFile(), fileName + ".png");
        }

        try {
            boolean written = ImageIO.write(
                    drawingPanel.getImage(), "png", selectedFile);
            if (written) {
                JOptionPane.showMessageDialog(frame, "作品已保存到:"
                        + selectedFile.getAbsolutePath());
            } else {
                JOptionPane.showMessageDialog(frame,
                        "当前环境没有可用的 PNG 写入器。",
                        "保存失败", JOptionPane.ERROR_MESSAGE);
            }
        } catch (IOException ex) {
            JOptionPane.showMessageDialog(frame,
                    "无法保存文件:" + ex.getMessage(),
                    "保存失败", JOptionPane.ERROR_MESSAGE);
        }
    }

    private class DrawingPanel extends JPanel {
        private final BufferedImage image;
        private int previousX;
        private int previousY;

        DrawingPanel() {
            setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT));

            image = new BufferedImage(
                    CANVAS_WIDTH, CANVAS_HEIGHT, BufferedImage.TYPE_INT_RGB);
            clear();

            MouseAdapter mouseHandler = new MouseAdapter() {
                @Override
                public void mousePressed(MouseEvent event) {
                    previousX = event.getX();
                    previousY = event.getY();
                    drawLine(previousX, previousY, previousX, previousY);
                }

                @Override
                public void mouseDragged(MouseEvent event) {
                    int currentX = event.getX();
                    int currentY = event.getY();
                    drawLine(previousX, previousY, currentX, currentY);
                    previousX = currentX;
                    previousY = currentY;
                }
            };

            addMouseListener(mouseHandler);
            addMouseMotionListener(mouseHandler);
        }

        private BufferedImage getImage() {
            return image;
        }

        private void drawLine(int x1, int y1, int x2, int y2) {
            Graphics2D g2 = image.createGraphics();
            g2.setRenderingHint(
                    RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            g2.setColor(brushColor);
            g2.setStroke(new BasicStroke(
                    BRUSH_WIDTH,
                    BasicStroke.CAP_ROUND,
                    BasicStroke.JOIN_ROUND));
            g2.drawLine(x1, y1, x2, y2);
            g2.dispose();
            repaint();
        }

        private void clear() {
            Graphics2D g2 = image.createGraphics();
            g2.setColor(Color.WHITE);
            g2.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
            g2.dispose();
            repaint();
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.drawImage(image, 0, 0, null);
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() ->
                new FirstArtwork().createAndShowWindow());
    }
}

四、编译并运行

在终端进入 FirstArtwork.java 所在目录,执行:

javac FirstArtwork.java
java FirstArtwork

第一条命令将源码编译为 FirstArtwork.class;第二条命令启动程序。成功后会出现一个固定尺寸的窗口。按住鼠标左键并拖动即可绘画,单击“选择颜色”可以更换画笔颜色,“清空画布”会把当前内容全部清除,“保存 PNG”会打开文件选择窗口。

使用支持 Java 的集成开发环境也可以运行:创建普通 Java 项目,将代码放进名为 FirstArtwork.java 的源文件,再运行 main 方法即可。若项目使用包声明,例如 package demo;,文件应放在对应的目录中;本文为了方便直接运行,没有添加包声明。

五、代码是怎样工作的

1. 窗口和程序入口

main 方法通过 SwingUtilities.invokeLater 创建界面。Swing 通常要求界面创建和更新在事件派发线程上进行。按钮点击和鼠标事件也由界面事件机制处理,所以示例把窗口搭建、绘画交互都留在这套机制内。

JFrame 是主窗口,工具栏放在上方,绘图区放在中间。pack() 根据组件的首选尺寸调整窗口大小;setResizable(false) 禁止用户调整窗口尺寸。这个限制是为了让固定大小的画布和图片保持一致,并不是 Swing 程序必须不可缩放。

2. 为什么要用 BufferedImage

绘图板把作品保存在 BufferedImage 中。鼠标移动时,程序在这张内存图片上画线;窗口需要重绘时,paintComponent 再把图片显示出来。

不能只在鼠标事件里直接对窗口的临时画面作画。窗口被遮挡、最小化后恢复,或系统要求重新绘制时,临时画面可能消失。将内容保存在图片对象中,重绘时再显示,才能保留已有笔迹。这里使用 TYPE_INT_RGB,意味着图片以 RGB 颜色存储,不包含透明通道;清空操作用白色填充,所以导出的背景也是白色。

3. 鼠标拖动怎样变成连续笔迹

mousePressed 记录按下位置,mouseDragged 每次收到拖动事件时,都将上一个位置与当前位置连成一条线,然后更新上一个位置。多个短线段连起来,就形成了连续的笔迹。

按下鼠标时还会绘制一个起点。因为按下和拖动之间不一定已经产生移动事件,如果不单独处理起点,轻点画布时就可能什么也看不到。示例通过从同一坐标画到同一坐标,并使用圆形笔帽,显示一个近似圆点。

4. 颜色、抗锯齿与画笔参数

JColorChooser.showDialog 会打开颜色选择器。用户取消选择时返回值为 null,因此代码只有在 selected != null 时才更新颜色。忽略这个判断可能导致取消选择后程序把空颜色传给绘图逻辑。

BRUSH_WIDTH 控制画笔粗细,示例设为 8.0f。BasicStroke.CAP_ROUND 和 JOIN_ROUND 分别让线段末端和连接处更圆润;抗锯齿设置用于改善边缘观感。它们是绘制效果选项,并不代表所有屏幕和缩放比例下都会呈现完全一致的像素效果。

5. PNG 保存流程

保存时先由 JFileChooser 让用户选择位置。示例若发现文件名没有以 .png 结尾,就自动补上扩展名,然后调用 ImageIO.write 写入 PNG。该方法可能因为没有可用的对应格式写入器而返回 false;文件操作也可能抛出 IOException,所以代码分别处理了这两种情况。

导出的图片尺寸是画布图片的尺寸,即本例中的 900×600 像素。窗口边框、工具栏和按钮不会保存进作品。PNG 是位图格式,放大查看时会看到像素细节;它并非可无限缩放的矢量图。

六、新手最容易遇到的错误

文件名和类名不一致

如果公共类名是 FirstArtwork,源码文件必须叫 FirstArtwork.java。在大小写敏感的系统中,firstartwork.java 和 FirstArtwork.java 不是同一个文件名。

在错误目录编译

执行 javac FirstArtwork.java 时,当前目录必须包含该文件。先用 cd 进入源码所在目录,或在编译命令中写出正确路径。编译报“找不到文件”时,先检查目录和文件扩展名;有些系统会隐藏已知扩展名,文件实际可能变成 FirstArtwork.java.txt。

JDK 和命令行环境不匹配

如果 javac 无法识别,可能只安装了 JRE,也可能 PATH 指向了旧版 Java。检查 java -version 与 javac -version,确认实际使用的是 JDK。IDE 中的项目 JDK 设置也可能与终端中的 JDK 不同。

窗口能打开,但保存失败

检查所选目录是否存在、当前用户是否有写入权限,以及目标文件是否正被其他程序占用。选择目录而不是文件、磁盘空间不足,或者目录权限受限,都可能导致保存失败。示例会把异常信息显示出来,但不会自动创建任意目录。

画布内容消失或尺寸不对

当前程序通过固定画布尺寸和禁止缩放避免图片与组件尺寸不一致。如果后续删除 setResizable(false) 并允许窗口调整大小,绘图区可能大于或小于底层图片,显示区域就会出现空白或截断。要支持缩放窗口,需要额外设计画布尺寸变化时如何扩展、裁切或缩放图片,不能只去掉这一行就认为功能完整。

七、参数边界与安全注意事项

  • CANVAS_WIDTH 和 CANVAS_HEIGHT 是图片的像素尺寸。创建图片时尺寸必须是有效的正整数;尺寸变得很大时会占用更多内存,分配失败并非不可能。示例使用 900×600,适合普通入门练习,不代表所有电脑上都适合无限增大。
  • BRUSH_WIDTH 是浮点数画笔宽度,示例使用正值。将其做成用户可输入的参数时,应验证输入是否为可接受的有限正数,并避免不合理的大值;本文没有实现参数输入界面。
  • 保存位置由用户通过文件选择器指定。程序会向所选位置写入文件,因此仍应选择自己有权写入的目录,并确认目标文件是否需要覆盖。不要把来源不明、未经检查的路径字符串直接交给程序批量写入。
  • 该程序只绘制用户的鼠标操作并保存图片,不会上传作品,也没有网络通信逻辑。但这不等同于经过安全审计的软件;运行任何源码前,都应理解其实际操作,并从可信来源获取代码。
  • 保存成功消息中的路径来自用户选择的文件名。若将程序改成服务端应用,不要把桌面文件选择器的行为误当成服务端文件安全方案;服务端需要单独设计身份验证、路径限制、文件大小限制和访问控制。

八、完成第一张作品后,可以怎样扩展

建议每次只加一个小功能,并在修改后重新编译运行。例如,增加几种预设颜色按钮;提供画笔粗细选项;增加“新建画布”确认框;或者加入撤销功能。撤销不能简单地从 PNG 中还原线条,通常要额外保存绘画操作记录,或保存若干历史画布状态。

还可以把画布宽高做成可配置参数,或在保存时弹出确认对话框。但扩展功能时应重新考虑输入边界、内存使用和异常处理。比如允许用户输入任意画布尺寸,就要避免不合理的大尺寸造成内存压力;允许自由指定文件路径,也要明确程序写入的范围和覆盖行为。

结语

这份小程序虽然简单,却覆盖了完成第一个 Java 桌面作品所需的一条完整路径:创建源文件、编译运行、响应鼠标事件、保存绘图状态、导出图片。先亲手运行,再逐项修改画笔宽度、窗口标题或颜色选择逻辑,比一次性堆叠很多功能更容易定位问题。完成并保存自己的第一张作品后,你就已经开始从“看懂代码”走向“用代码做东西”了。

© 版权声明

相关文章