如何在窗体上实现图像运动?比如,在窗体上画一个黑色的点,然后让这个点以一定的方向运动。请往下看:
一、可视化窗体的生成
首先我们需要在电脑屏幕上面生成一个窗体,可以设定大小以及关闭按钮。
代码如下所示:
import javax.swing.JFrame;
public class GUI extends JFrame{
/**简单生成一个窗体即可
*/
private static final long serialVersionUID = 1L;
public GUI() {
setTitle("生成窗体");
setSize(1000, 1000);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setVisible(true);
}
public static void main(String[] args) {
new GUI();
}
}
运行之后屏幕上就弹出了一个空的窗体
二、在窗体中画一个圆点
要画图形,先简要介绍一下Graphics类。Graphics类是所有图形上下文的抽象基类,允许应用程序绘制到在各种设备上实现的组件以及屏幕外图像。我们可以把他比喻成一个画笔。在之前生成的窗体中,我们并没有创建一个Graphics对象,但是我们需要注意的是,窗体自带一个Graphics对象,也就是窗体已经有一个画笔可以在窗体上画图了,但在代码中没有显示。我们需要利用窗体自带的画笔,怎么办呢?
- 在类方法中添加一个方法 paint();
- 在方法体中调用画笔的方法
public void paint(Graphics g) {
super.paint(g);
g.fillOval(100, 100, 10, 10);//给窗体画圆点
}
这里的paint实际上是JFrame父类的一个方法,在此进行重写,重写时记得先调用父类的方法体内容。在调用Graphics方法给窗体画图形时,会在底层调用paint方法载入画笔进行绘图(个人理解,如有不正欢迎批判指正)。如果在此处的GUI类中不加入paint方法,利用如下方式,再调用g的方法,则画不出图形。
Graphics g;
g = GUI.getGraphics;
//在不加入paint方法的情况下
运行效果如下:
三、让点动起来
- 不做其他处理,单纯通过不断的改变fillOval方法的坐标画点.
为了使过程直观可控,我们添加一个按钮事件,功能是:点一下按钮,就开始不断的画点。最简单的方法是利用循环,不断的改变fillOval方法的坐标,使得画出的点的位置看上去像是连续变化。
(Tip:关于按钮添加和功能触发的原理,可参考接口与事件)
添加的按钮监听器类,并修改一下GUI的构造方法:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.Graphics;
public class ButtonListener implements ActionListener{
Graphics g;
public ButtonListener(Graphics g) {//利用构造方法把窗体的Graphics画笔给按钮事件监听器使用
this.g = g;
}
@Override
public void actionPerformed(ActionEvent e) {
for (int i = 0; i < 10000; i++) {
g.fillOval(i, 500, 30, 30);
}
}
}
//========================分割线=======================
public GUI() {
setTitle("生成窗体");
setSize(1000, 1000);
setDefaultCloseOperation(EXIT_ON_CLOSE);
FlowLayout fl = new FlowLayout();
setLayout(fl);
JButton btn = new JButton("画图");
add(btn);
setVisible(true);
ButtonListener bl = new ButtonListener(this.getGraphics());
//注意这里获得Graphics的方法要放在setVisible后,否则画笔还未加载,方法无效
btn.addActionListener(bl);
}
运行之后,你将看到如下效果:
别眨眼,我们可以看见这个线条飞快地从左往右画过去,实现了点动成线的效果。但是,我想实现单个圆点从左往右运动,怎么做呢?
一个简答的办法就是:边画边擦除。也就是说,画上一个点之后,再拿原来的背景把点盖住,再在下一个坐标画点,再盖住,再在下一个坐标画点…这样我们就可以在屏幕上只看见一个点在运动了。
- 修改一下按钮事件的方法:
public void actionPerformed(ActionEvent e) {
for (int i = 0; i < 10000; i++) {
g.setColor(Color.BLACK);
g.fillOval(i, 500, 30, 30); //在下一个坐标画点
g.setColor(Color.white);
g.fillRect(0, 0, 1000, 1000); //使用白色背景擦除
}
}
再来看看运行效果:
这里就只看得见白色背景,看不见圆点。这是因为循环执行的速度过快,人眼帧数较低所,圆点的画面来不及被捕捉,就又被白色背景盖住了。为了显示效果,在画圆点后添加sleep即可。
public void actionPerformed(ActionEvent e) {
for (int i = 0; i < 10000; i++) {
g.setColor(Color.BLACK);
g.fillOval(i, 500, 30, 30);
try {
Thread.sleep(10);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
g.setColor(Color.white);
g.fillRect(0, 0, 1000, 1000);
}
}
- 这样就显示出了一个圆点运动的效果!但是,这时候点右上角的关闭按钮,却关不掉窗口,因为程序要等到这个时间(循环)执行完毕再进行下一条指令。并且发现小球有时候会闪动。
四、使用多个线程进行画图
- 对于想随时关闭窗口的问题,我们可以采用多线程的方式,把画图的方法放在线程里面。
- 可以通过多线程同时在屏幕上画多个小球(每按一下按钮就新启动一个线程)
- 修改代码如下:
import java.awt.Graphics;
import java.awt.Color;
public class MoveThread extends Thread{
Graphics g;
public MoveThread(Graphics g) {
this.g = g;
}
public void run() {
for (int i = 0; i < 10000; i++) {
g.setColor(Color.BLACK);
g.fillOval(i, 500, 30, 30);
try {
Thread.sleep(10);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
g.setColor(Color.white);
g.fillRect(0, 0, 1000, 1000);
}
}
}
===================分割线=====================
public void actionPerformed(ActionEvent e) {
new MoveThread(g).start();
}
效果如下所示:
这里出现了几个问题:
- 添加多个小球后,小球闪动更加明显。因为多个线程一起在这个窗体上进行画点-覆盖刷新-画点-覆盖刷新…的操作,并且每个线程开始的时间不一样,导致覆盖刷新不同步。
- 背景也出现闪动,因为这么多个线程共用一个窗体的Graphics画笔,有一个线程画圆点时执行g.setColor(color.black)方法,把画笔设成黑色,其他线程这时正好在画背景,这样就把背景画成了黑色。
解决方法:
- 启用线程队列,将所有添加的圆点放在一个List中,只需启动一个线程来把这个队列的元素画出来就可以了。
- 用一个自定义的图片当作背景。
- 我们需要把圆点单独作为一个类Point,并在GUI类中创建一个Point类型的List,然后用一个线程不断遍历这个List把点画出并进行背景覆盖刷新。
//GUI类:
ArrayList<Point> list = new ArrayList<>();
public GUI() {
setTitle("生成窗体");
setSize(1000, 1000);
setDefaultCloseOperation(EXIT_ON_CLOSE);
FlowLayout fl = new FlowLayout();
setLayout(fl);
JButton btn = new JButton("画图");
add(btn);
setVisible(true);
ButtonListener bl = new ButtonListener(this.getGraphics(),list);//注意这里获得Graphics的方法要放在setVisible后,否则画笔还未加载,方法无效
btn.addActionListener(bl);
new MoveThread(this.getGraphics(),list).start();
}
//Point类:
import java.awt.Color;
import java.awt.Graphics;
public class Point {
int x, y; //横纵坐标
public Point() {}
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public void move() {
x += 1;
}
public void draw(Graphics g) {
g.setColor(Color.black);
g.fillOval(x, y, 30, 30);
}
}
//线程类:
import java.awt.Graphics;
import java.util.ArrayList;
import javax.swing.ImageIcon;
public class MoveThread extends Thread{
Graphics g;
ArrayList<Point> list;
ImageIcon bgm = new ImageIcon("D:\\Download\\远山.jpg");
public MoveThread(Graphics g, ArrayList<Point> list) {//利用构造方法传参
this.g = g;
this.list = list;
}
public void run() {
while (true) {
for (int i = 0; i < list.size(); i++) {
list.get(i).move();
list.get(i).draw(g);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
g.drawImage(bgm.getImage(), 0, 100, null);
}
}
}
//按钮事件监听器类:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.awt.Graphics;
public class ButtonListener implements ActionListener{
Graphics g;
ArrayList<Point> list;
public ButtonListener(Graphics g, ArrayList<Point> list) { //利用构造方法把窗体的Graphics画笔给按钮事件监听器使用
this.g = g;
this.list = list;
}
@Override
public void actionPerformed(ActionEvent e) {
list.add(new Point(0, 500));
}
}
- 再来看看效果:
这样就OK了,尽管还有略微的屏幕闪烁。
(未完待续…)