package MyGame;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.*;
public class planewars extends JFrame{
//管理所有的组件/方法
public void init() {
//注册(调用)
createwindow();
control();
}
//创建白窗口,用到JFrame类
public void createwindow() {
//禁止窗口大小调整,因为背景图很小,窗口变大之后背景不会变
setResizable(false);
//关闭窗口的同时停止程序运行
setDefaultCloseOperation(EXIT_ON_CLOSE);
//设置窗口在屏幕上显示的位置和宽高
setBounds(500,60,512,768);
/*
屏幕左上角是坐标轴的原点,向右是x轴,向下是y轴
向右500px,向下60px,512px是屏幕宽度,768px是屏幕高度
*/
//java窗口默认情况下不显示,我们得设置为显示
setVisible(true);//不写这行时,默认值是false
//在窗口工具栏上面加上游戏的名字
setTitle("飞机大战");
//不断的自动重绘窗口
while(true) {
repaint();
//让重绘的速度变慢
try {
Thread.sleep(80);//让当前死循环每隔50毫秒停顿一下
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//启动入口
public static void main(String args[]) {
//启动程序
planewars pw = new planewars();
pw.init();
}
//paint方法会被自动调用
//画图的笔 //重绘repaint()
public void paint(Graphics g) {//理解为粉笔
// g.setColor(Color.red);//选红色的粉笔
// g.drawLine(20, 100, 150, 200);//x1,y1的坐标为(20,100),x2,y2的坐标为(150,200)
//封装
// //把硬盘上的背景图片加载进程序,,让java能看懂
// Image bg = new ImageIcon("img/背景.jpg").getImage();
// g.drawImage(bg, 0, 0, 100, 768, 0, 0, 512, 768, null);
// //前面的0,0,512,768是窗口左上角和右下角的坐标,
// //后面的0,0,512,768是图片左上角和右下角的坐标
// /*在代码中传入 null 作为 observer 参数,意味着你不关心图像加载的状态,
// * 也不需要接收图像加载完成或者出现错误等相关的通知。在很多情况下,
// * 如果图像已经完全加载好,或者你不打算处理图像加载过程中的状态变化,
// * 就可以传入 null。
// */
//调用加入背景图
addBackground(g);
addOurplane(g);
}
//背景图片向下移动的速度,每次移动10px
int bg_y = 0;
//加入背景图
public void addBackground(Graphics g) {
//把硬盘上的背景图片加载进程序,,让java能看懂
Image bg = new ImageIcon("img/背景图片.jpg").getImage();
//窗口中的背景图片(因为要实现滚动起来的效果还需要一张在窗口外的图片)
g.drawImage(bg, 0, 0+bg_y, 512, 768+bg_y, 0, 0, 512, 768, null);
//前面的0,0,512,768是窗口左上角坐标和宽度高度,
//后面的0,0,512,768是图片左上角坐标和宽度高度
/*在代码中传入 null 作为 observer 参数,意味着你不关心图像加载的状态,
* 也不需要接收图像加载完成或者出现错误等相关的通知。在很多情况下,
* 如果图像已经完全加载好,或者你不打算处理图像加载过程中的状态变化,
* 就可以传入 null。
*/
//窗口上面看不到的背景图片
g.drawImage(bg, 0, -768+bg_y, 512, 0+bg_y, 0, 0, 512, 768, null);
bg_y += 5;
if (bg_y>=768)
{
bg_y = 0;
}
}
//加入我方飞机
Plane ourplane = new Plane();
public void addOurplane(Graphics g) {
ourplane.drawPlane(g);
}
//键盘控制飞机
public void control() {
//添加键盘监听事件
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
System.out.println("键盘按下了");
}
});
}
}
以上代码在按下键盘后并没有出现 键盘按下了
Java 的事件处理基于委托事件模型(Delegation Event Model)。模型中有三个关键部分:事件源(Event Source)、事件监听器(Event Listener)和事件对象(Event Object)
- 事件源:产生事件的对象。此代码中,
planewars
类继承自JFrame
,JFrame
作为事件源。addKeyListener
方法就是在JFrame
(即事件源)上添加键盘事件监听器。 - 事件监听器:实现了特定监听接口的类的实例。
KeyAdapter
是一个抽象类,它实现了KeyListener
接口。KeyListener
接口定义了三个方法:keyPressed
(按键按下)、keyReleased
(按键释放)、keyTyped
(输入字符)。在你的代码中,通过匿名内部类的方式创建了一个KeyAdapter
的实例,并重写了keyPressed
方法,当有按键按下时会执行重写后的keyPressed
方法。 - 事件对象:封装了事件相关信息的对象。
KeyEvent
类是一个事件对象,它包含了与键盘事件相关的信息,例如按下的键的代码(getKeyCode
)、按下的字符(getKeyChar
)等。keyPressed
方法接收一个KeyEvent
类型的参数e
,通过这个参数可以获取到具体的键盘事件信息。
具体执行过程:
- 当调用
addKeyListener
方法时,会将创建的KeyAdapter
实例注册到JFrame
(事件源)上。 - 当用户按下键盘上的键时,系统会创建一个
KeyEvent
对象,并将其传递给注册在JFrame
上的keyPressed
方法。 keyPressed
方法被调用后,执行其中的代码,此代码中就是System.out.println("键盘按下了");
,从而在控制台输出相应的信息。
事件分发线程阻塞问题
在 Java Swing 中,EDT 负责处理所有与 GUI 相关的操作,例如界面绘制、事件处理等。当你在代码里使用 while (true)
循环时,这个循环会持续占用 EDT,使得 EDT 无法处理其他任务
在 createwindow
方法里,存在一个 while (true)
循环,该循环会持续调用 repaint
方法,并且使用 Thread.sleep(80)
来控制重绘的速度。不过,这个循环会阻塞事件分发线程(Event Dispatch Thread, EDT),导致键盘事件无法被正常处理。
解决办法
运用 javax.swing.Timer
来替代 while (true)
循环,以此实现定时重绘,这样就能保证事件分发线程可以正常处理键盘事件。
将以下代码:
//不断的自动重绘窗口
while(true) {
repaint();
//让重绘的速度变慢
try {
Thread.sleep(80);//让当前死循环每隔50毫秒停顿一下
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
改为:
//不断的自动重绘窗口
Timer timer = new Timer(80, e -> repaint());
timer.start();
javax.swing.Timer
是一个轻量级的工具,用于在指定的时间间隔后触发事件。它会在后台线程中运行,不会阻塞 EDT。