Java2D绘图

Java2D API是Java中的一种图形绘制接口,它能够让开发者创建和在屏幕上显示2D图形、图像、文本等图元。这篇笔记我们对Java2D绘图进行介绍。

使用Graphics2D

Java2D API中绘制图像主要使用Graphics2D类,它类似于画笔提供了各种绘图方法。我们这里使用扩展JComponent的方式,介绍如何使用Graphics2D绘制2D图形,一个基础的代码框架如下。

package com.gacfox.demo;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;

import javax.swing.JComponent;

class MyComponent extends JComponent {
    @Override
    protected void paintComponent(Graphics g) {
        Graphics2D graphics2D = (Graphics2D) g;
        // 绘制我们的图形
        // ...
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(640, 480);
    }
}

我们重写了JComponent的绘制paintComponent和获得预定义大小getPreferredSize函数,外层将MyComponent放入JFrame中即可。我们要进行实验的就是Graphics2D这个类。Graphics2D库采用面向对象的方式将各种几何图形组织了起来,常用的包括Line2D直线、Rectangle2D矩形、Ellipse2D椭圆等,他们实现了都Shape接口。

绘制矩形

下面例子代码实现了绘制矩形。

Rectangle2D rectangle2D = new Rectangle2D.Double(20, 20, 100, 100);
graphics2D.draw(rectangle2D);

Rectangle2D构造函数传入坐标和长宽,调用draw()即可绘制。

注意Rectangle2D.Double,实际上还有一个Rectangle2D.Float,这两个画的都是矩形,但是我们知道绘图时一般都使用单精度浮点数,Graphics2D也是如此,但是这就带来了类型转换的麻烦,因此Graphics2D提供了double版的Rectangle2D供我们使用,类型转换在其内部实现了。

运行结果如下图所示。

绘制圆形

下面例子代码实现了绘制圆形。

Ellipse2D ellipse2D = new Ellipse2D.Double(20, 20, 100, 100);
graphics2D.draw(ellipse2D);

构造函数传入的参数和矩形完全相同,只不过绘制的是矩形的内切圆。

运行结果如下图所示。

绘制直线

下面例子代码实现了绘制直线。

Line2D line2D = new Line2D.Double(20, 20, 250, 300);
graphics2D.draw(line2D);

传入参数为起始点和终止点坐标。

运行结果如下图所示。

获得图形对象的属性

当然,Rectangle2D等对象都提供了若干get方法获取其关键的属性,这里就不全部介绍了。

绘制文字

graphics2D.drawString("你好世界!", 100, 100);

我们使用drawString函数,参数传入文字和坐标。

运行结果如下图所示。

设置字体和大小

Font font = new Font("Sans", Font.BOLD, 20);
graphics2D.setFont(font);
graphics2D.drawString("你好,世界!", 20, 20);

我们可以直接指定字体名称,或者从流加载。

运行结果如下图所示。

使用颜色

Graphics2D预定义了一系列颜色,例如Color.RED等,我们也可以实例化Color对象,定义自己的颜色。

画笔颜色和填充颜色

graphics2D.setPaint(Color.blue);
graphics2D.drawString("你好,世界!", 100, 100);

graphics2D.setPaint(new Color(255, 0, 0));
Ellipse2D ellipse2D = new Ellipse2D.Double(200, 200, 20, 50);
graphics2D.fill(ellipse2D);

setPaint()函数可以设置一个画笔颜色,注意:这个函数的参数为Paint,但是Color类实现了Paint。设置画笔颜色后,绘制的图形就是新设置的颜色了。

fill()函数传入一个Shape类型的参数,会为这个图形指定填充颜色。

运行结果如下图所示。

背景颜色

如果我们继承的是JComponent,绘制背景颜色时就不得不自己画一个大矩形充当背景了,如果我们继承JPanel,可以直接对JPanel对象设置背景颜色。

void setBackground(java.awt.Color bg)

这里要注意,如果覆盖了JPanelpaintComponent方法,方法里要调用super.paintComponent(g)才能让背景颜色设置生效。

绘制图像

绘制图像需要一个Image对象。

URL iconUrl = getClass().getResource("/icon.png");
ImageIcon icon = new ImageIcon(iconUrl);
graphics2D.drawImage(icon.getImage(), 10, 10, null);

运行结果如下图所示。

启用帧循环

上面我们绘制的都是静态图像,实际上我们也能实现帧动画的效果。虽然Swing是一个事件驱动的GUI框架,但实际上我们也可以单独开一个线程,实现类似游戏引擎的帧循环。Swing组件默认的绘制方法实现了双缓冲绘制,因此我们实现帧动画其实非常简单,下面例子我们绘制了一个来回移动的小球,并绘制显示了当前帧数。

package com.gacfox.demo;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.Ellipse2D;

public class MyComponent extends JComponent {
    private volatile boolean frameLoop;
    private volatile long lastFrameTimeMillis = 0;
    private static int MIN_FRAME_INTERVAL = 16;

    private int direction = 1;
    private int posX = 0;
    private int posY = 0;
    private long currentFrameTimer = 0;
    private int currentFrame = -1;

    public MyComponent() {
        super();
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(640, 50);
    }

    public void init() {
        // 开启帧循环线程,循环周期为MIN_FRAME_INTERVAL
        frameLoop = true;
        Thread frameThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (frameLoop) {
                        long currentTimeMillis = System.currentTimeMillis();
                        final long deltaTime = currentTimeMillis
                                - lastFrameTimeMillis;
                        if (deltaTime >= MIN_FRAME_INTERVAL) {
                            SwingUtilities.invokeLater(new Runnable() {
                                @Override
                                public void run() {
                                    update(deltaTime);
                                }
                            });
                            lastFrameTimeMillis = System.currentTimeMillis();
                        } else {
                            try {
                                long sleepMillis = MIN_FRAME_INTERVAL
                                        - (currentTimeMillis - lastFrameTimeMillis);
                                Thread.sleep(sleepMillis > 0 ? sleepMillis : 0);
                            } catch (InterruptedException ex) {
                                throw new RuntimeException(ex);
                            }
                        }
                    }
                }
            }
        });
        frameThread.start();
    }

    public void destroy() {
        frameLoop = false;
    }

    private void update(long deltaTime) {
        // 计算小球方向和位置
        if (posX >= 590) {
            direction = 0;
        }
        if (posX <= 0) {
            direction = 1;
        }
        if (direction == 1) {
            posX = posX + 1;
        } else {
            posX = posX - 1;
        }

        // 绘制当前帧数,每500ms更新
        currentFrameTimer = currentFrameTimer + deltaTime;
        if (currentFrameTimer > 500) {
            if (deltaTime <= 0) {
                currentFrame = -1;
            } else {
                currentFrame = (int) (1000 / deltaTime);
            }
            currentFrameTimer = 0;
        }

        // 重绘界面
        this.revalidate();
        this.repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        // 绘制小球
        Graphics2D graphics2D = (Graphics2D) g;
        Ellipse2D ellipse2D = new Ellipse2D.Double(posX, posY, 50, 50);
        graphics2D.draw(ellipse2D);

        // 绘制文字
        Font font = new Font("Sans", Font.PLAIN, 14);
        graphics2D.setFont(font);
        String currentFrameStr = "∞";
        if (currentFrame > 0) {
            currentFrameStr = String.valueOf(currentFrame);
        }
        graphics2D.drawString("当前帧数:" + currentFrameStr, 14, 14);
    }
}

运行结果如下图所示。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap