01-窗体对象JFrame(窗体创建)


02-组件

按钮组件创建(JButton)

注意事项
注意:JFrame的面板,对于组件,默认有一套摆放规则,如果希望按照自己的意思进行摆放,需要做一个设置
// 取消默认布局 frame.setLayout(null); // 添加一个按钮 JButton btn = new JButton("点我啊~"); btn.setBounds(50, 50, 100, 100); // 获取面板对象,并添加按钮 frame.getContentPane().add(btn);
完善前面代码演示

JLabel组件
JLabel:窗体中的一个区域(空间),在这个空间里面摆放图片和文字
- JLabel(String text)使用指定的文本创建一个JLabel对象
- JLabel(Icon image)创建一个具有指定图像的JLabel对象
代码演示

JLabel的注意事项
- 展示图片的时候,要注意图片的大小
- 如果多个组件重叠,后添加的组件,是从底部塞进去的
事件
事件是可以被组件识别的操作,当你对组件干了某件操作之后,就会执行对应的代码

ActionListener:动作监听(鼠标点击,空格按下了)

MouseListener:鼠标监听(鼠标滑入滑出,按下松开,点击)

适配器设计模式
情况:我需要用一个接口,但是只需要接口中的一部分方法 ---------------------------------------------------------------------------- 解决方案:使用适配器设计模式 编写一个适配器类(中间类),实现接口并重写所有抽象方法 这里的抽象方法都是空实现,还要将适配器类改为抽象类,避免被别人创建对象使用 再让具体的类,继承适配器类,重写自己需要的方法即可 可以用功能自带的Adapter类,来达到一个适配器的效果 例如: //鼠标 frame.addMouseListener(new MouseAdapter(){ } //键盘 frame.addKeyListener(new KeyAdapter(){ }
KeyListener:键盘监听(此代码演示用的适配器设计模式改进)

石头迷阵游戏编写
游戏界面编写

import javax.swing.*; public class Test { public static void main(String[] args) { // 创建窗体对象 JFrame frame = new JFrame("石头迷阵单机版v1.0"); //设置窗体大小 frame.setSize(514, 595); // 设置窗体居中 frame.setLocationRelativeTo(null); // 设置窗体置顶 frame.setAlwaysOnTop(true); // 取消默认布局 frame.setLayout(null); // 设置窗体的关闭模式 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 定义一个统计变量定义初始值,让for循环来完成多个图片的加载 int count = 0; // 添加石头方块 for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { JLabel imageLabel = new JLabel(new ImageIcon("F:\\JavaCodes\\Pre class\\day04-code\\src\\com\\itheima\\stonepuzzle\\image\\" + count + ".png")); imageLabel.setBounds(50 + 100 * j, 90 + 100 * i, 100, 100); frame.getContentPane().add(imageLabel); count++; } } // 添加背景图片 JLabel background = new JLabel(new ImageIcon("F:\\JavaCodes\\Pre class\\day04-code\\src\\com\\itheima\\stonepuzzle\\image\\background.png")); background.setBounds(26, 30, 450, 484); frame.getContentPane().add(background); //设置窗体可见:需要放在最后调用 frame.setVisible(true); } }
根据二维数组优化代码
我们要想石头移动,应该把它们放在一个数组当中,以后在加载图片的时候就可以根据二维数组来加载图片
import javax.swing.*; public class Test { public static void main(String[] args) { int[][] data = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 0} }; // 创建窗体对象 JFrame frame = new JFrame("石头迷阵单机版v1.0"); //设置窗体大小 frame.setSize(514, 595); // 设置窗体居中 frame.setLocationRelativeTo(null); // 设置窗体置顶 frame.setAlwaysOnTop(true); // 取消默认布局 frame.setLayout(null); // 设置窗体的关闭模式 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 添加石头方块 for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { JLabel imageLabel = new JLabel(new ImageIcon("F:\\JavaCodes\\Pre class\\day04-code\\src\\com\\itheima\\stonepuzzle\\image\\" + data[i][j] + ".png")); imageLabel.setBounds(50 + 100 * j, 90 + 100 * i, 100, 100); frame.getContentPane().add(imageLabel); } } // 添加背景图片 JLabel background = new JLabel(new ImageIcon("F:\\JavaCodes\\Pre class\\day04-code\\src\\com\\itheima\\stonepuzzle\\image\\background.png")); background.setBounds(26, 30, 450, 484); frame.getContentPane().add(background); //设置窗体可见:需要放在最后调用 frame.setVisible(true); }
根据继承改进优化代码
整体步骤
1.定义MainFrame类继承JFrame
2.将代码抽取到一个单独的方法initFrame()
3.将绘制界面的代码,抽取为一个单独的方法paintView()
4.将二维数组提取到成员变量的位置
5,对JFrame类中方法的调用方式,更换为super,或直接省略
6.在构造方法中,调用initFrame()和paintView()
7.在构造方法的最后调用setVisible(true);
Test测试代码
public class Test { public static void main(String[] args) { new MainFrame(); } }
MainFrame子类继承父类改进代码
因为JFrame不允许添加功能,所以我们应该自己定义一个子类来继承父类,用子类来丰富我们的代码
因为子类继承了父类,那我们就不需要使用JFrame frame = new JFrame的方式来调用代码,也不用frame.的方式来进行调用父类方法
例如:
frame.setSize(514, 595); 可以改进为: setSize(514, 595);
import javax.swing.*; /** * 继承JFrame,让自己的类,也变成窗体类,方便丰富新的功能 */ public class MainFrame extends JFrame { int[][] data = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 0} }; public MainFrame() { initFrame(); paintView(); //设置窗体可见 setVisible(true); } /** * 此方法用于构建窗体 */ public void initFrame() { // 设置窗体标题 setTitle("石头迷阵单机版v1.0"); //设置窗体大小 setSize(514, 595); // 设置窗体居中 setLocationRelativeTo(null); // 设置窗体置顶 setAlwaysOnTop(true); // 取消默认布局 setLayout(null); // 设置窗体的关闭模式 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } /** * 此方法绘制游戏界面 */ public void paintView() { // 添加石头方块 for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { JLabel imageLabel = new JLabel(new ImageIcon("F:\\JavaCodes\\Pre class\\day04-code\\src\\com\\itheima\\stonepuzzle\\image\\" + data[i][j] + ".png")); imageLabel.setBounds(50 + 100 * j, 90 + 100 * i, 100, 100); getContentPane().add(imageLabel); } } // 添加背景图片 JLabel background = new JLabel(new ImageIcon("F:\\JavaCodes\\Pre class\\day04-code\\src\\com\\itheima\\stonepuzzle\\image\\background.png")); background.setBounds(26, 30, 450, 484); getContentPane().add(background); } }
石头方块打乱
核心思路
遍历二维数组获取每一个元素,和其他元素随机交换
可以用随机数来进行操作
int randomX = r.nextInt(4);
int randomY = r.nextInt(4);
实现步骤
1.遍历二维数组,获取到每一个元素
2.在遍历的过程中,产生两个随机的索引
3.让当前元素,和随机索引所指向的元素进行交换
代码实现
/** * 此方法用于初始化二维数组的数据,打乱二维数组 */ public void initData() { Random r = new Random(); for (int i = 0; i < data.length; i++) { for (int j = 0; j < data[i].length; j++) { // 在遍历的过程中,产生两个随机的索引 int randomX = r.nextInt(4); int randomY = r.nextInt(4); //让当前元素,和随机索引所指向的元素进行交换 //当前元素data[i][j] //随机索引指向的元素 data[randomX][randomY] int temp = data[i][j]; data[i][j] = data[randomX][randomY]; data[randomX][randomY] = temp; } } }
石头移动业务
注册监听事件
既然子类都继承了父类方法,那我们也可以直接用子类调用父类中的KeyListener接口
public class MainFrame extends JFrame implements KeyListener {
// 为窗体注册键盘监听 // 思路:需要对窗体对象注册监听,而我自己的这个类就是窗体类,this代表当前类对象 this.addKeyListener(this);
重写父类中的KeyListener接口的keyPressed方法
@Override public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); if (keyCode == 37) { System.out.println("左"); } else if (keyCode == 38) { System.out.println("上"); } else if (keyCode == 39) { System.out.println("右"); } else if (keyCode == 40) { System.out.println("下"); } } // 以下代码不用管 @Override public void keyTyped(KeyEvent e) { } @Override public void keyReleased(KeyEvent e) { }
获取0号元素的地址
在public class MainFrame extends JFrame implements KeyListener {下定义两个成员变量 int row; //行 int column; //列
for (int i = 0; i < data.length; i++) { for (int j = 0; j < data[i].length; j++) { if (data[i][j] == 0) { // 记录0号元素(空白块的坐标) row = i; column = j; } } }
石头移动代码
在paintView()方法中加入移除面包组件和刷新界面的逻辑
public void paintView() { // 移除面板中的组件 getContentPane().removeAll(); // 添加石头方块 for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { JLabel imageLabel = new JLabel(new ImageIcon("F:\\JavaCodes\\Pre class\\day04-code\\src\\com\\itheima\\stonepuzzle\\image\\" + data[i][j] + ".png")); imageLabel.setBounds(50 + 100 * j, 90 + 100 * i, 100, 100); getContentPane().add(imageLabel); } } // 添加背景图片 JLabel background = new JLabel(new ImageIcon("F:\\JavaCodes\\Pre class\\day04-code\\src\\com\\itheima\\stonepuzzle\\image\\background.png")); background.setBounds(26, 30, 450, 484); getContentPane().add(background); // 刷新界面 getContentPane().repaint(); }
在注册监听的方法里接入paintView()方法
public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); move(keyCode); // 每一次移动完,重新绘制界面 paintView(); }
我们从public void keyPressed(KeyEvent e) 方法中把实现石头移动的代码单独抽取出来,避免public void keyPressed(KeyEvent e) 方法代码太臃肿
/** * 此方法用于处理移动业务 * * @param keyCode */ private void move(int keyCode) { if (keyCode == 37) { // (空白块)和(右侧的数据)交换 // data[row][column] data[row][column + 1] int temp = data[row][column]; data[row][column] = data[row][column + 1]; data[row][column + 1] = temp; // 修改空白坐标 column++; } else if (keyCode == 38) { // (空白块)和(下方的数据)交换 // data[row][column] data[row + 1][column] int temp = data[row][column]; data[row][column] = data[row + 1][column]; data[row + 1][column] = temp; // 修改空白坐标 row++; } else if (keyCode == 39) { // (空白块)和(左侧的数据)交换 // data[row][column] data[row][column - 1] int temp = data[row][column]; data[row][column] = data[row][column - 1]; data[row][column - 1] = temp; // 修改空白坐标 column--; } else if (keyCode == 40) { // (空白块)和(上方的数据)交换 // data[row][column] data[row - 1][column] int temp = data[row][column]; data[row][column] = data[row - 1][column]; data[row - 1][column] = temp; // 修改空白坐标 row--; } }
Bug分析和解决
在上面代码的时候,移动一次后,在向相同的方向移动的话,会出现索引越界的报错(用下方图片来if判断即可,如果超了下方图片的值返回return;)

因为这个游戏的算法很基础,可能会导致无法完成游戏,我们可以做一个作弊器来完成游戏(是不是很骚)
// 这里设置的90是键盘上z的地址,当我们按下z的时候,直接重新生成数组结束游戏 else if (keyCode == 90) { data = new int[][] { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 0} }; }
判定游戏胜利

/** * 此方法用于校验游戏是否胜利 */ public boolean victory() { for (int i = 0; i < data.length; i++) { for (int j = 0; j < data[i].length; j++) { if (data[i][j] != win[i][j]) { return false; } } } return true; }
在public void paintView() { 方法中的移除面板中的组件后添加,假如在移除面板中的组件的前面添加的话,它又会移除面板组件
// 统计游戏步数 JLabel scoreLabel = new JLabel("步数:" + count); scoreLabel.setBounds(50, 20, 100, 20); getContentPane().add(scoreLabel); // 添加游戏胜利图片 if (victory()) { JLabel winJLabel = new JLabel(new ImageIcon("F:\\JavaCodes\\Pre class\\day04-code\\src\\com\\itheima\\stonepuzzle\\image\\win.png")); winJLabel.setBounds(124, 230, 266, 88); getContentPane().add(winJLabel); }
统计步数
- 在子类中定义一个count成员变量用于统计步数
- 在每个移动方块逻辑下添加count++统计移动的步数(作弊器不添加)
private void move(int keyCode) { if (victory()) { return; } if (keyCode == 37) { if (column == 3) { return; } // (空白块)和(右侧的数据)交换 // data[row][column] data[row][column + 1] int temp = data[row][column]; data[row][column] = data[row][column + 1]; data[row][column + 1] = temp; // 修改空白坐标 column++; count++; } else if (keyCode == 38) { if (row == 3) { return; } // (空白块)和(下方的数据)交换 // data[row][column] data[row + 1][column] int temp = data[row][column]; data[row][column] = data[row + 1][column]; data[row + 1][column] = temp; // 修改空白坐标 row++; count++; } else if (keyCode == 39) { if (column == 0) { return; } // (空白块)和(左侧的数据)交换 // data[row][column] data[row][column - 1] int temp = data[row][column]; data[row][column] = data[row][column - 1]; data[row][column - 1] = temp; // 修改空白坐标 column--; count++; } else if (keyCode == 40) { if (row == 0) { return; } // (空白块)和(上方的数据)交换 // data[row][column] data[row - 1][column] int temp = data[row][column]; data[row][column] = data[row - 1][column]; data[row - 1][column] = temp; // 修改空白坐标 row--; count++; } else if (keyCode == 90) { data = new int[][]{ {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 0} }; } }
重新开始游戏
重新开始游戏也是属于绘制游戏界面的方法,所以我们在绘制游戏界面的方法里添加,注意:也是添加在移除面板中的组件后面
// 重新游戏 JButton btn = new JButton("重新游戏"); btn.setBounds(350, 20, 100 ,20); btn.setFocusable(false); btn.addActionListener(this); getContentPane().add(btn);
我们知道接口可以多实现,所以,我们在子类的后面,号分割后,添加ActionListener,选中后,快捷键(Alt + Enter)进行重构
public class MainFrame extends JFrame implements KeyListener, ActionListener {
重构代码
/** * 此方法用于重新游戏的逻辑 */ @Override public void actionPerformed(ActionEvent e) { count = 0; initData(); paintView(); }
游戏练习编写完成
完整代码
Test测试类代码:
public class Test { public static void main(String[] args) { new MainFrame(); } }
游戏所有逻辑代码