16-JavaSE基础巩固项目:拼图小游戏

news/2024/4/30 7:50:35/文章来源:https://blog.csdn.net/yelitoudu/article/details/128100026

阶段项目-拼图小游戏

一、项目介绍

在这里插入图片描述


1、目的

  • 锻炼逻辑思维能力,让我们知道前面学习的知识点在实际开发中的应用场景

  • 1、为了学习一个新知识:GUI

    • GUI全称:Graphical User Interface(又称图形用户接口)
    • 是指采用图形化的方式显示操作界面。
  • 2、为什么图形化用户界面这个知识点很多Java课程中都不讲呢?

    • 因为我们Java语言主要是做后台服务器开发的,在企业中很少用得到GUI这个技术的。
    • 那服务器又是什么??
      • 举个例子:
        • 1、比如我们在网页上看小说,那些小说内容就是服务器通过网络传递给客户端(电脑、手机等)的。
          • 这样我们才能看到小说内容。
        • 2、比如网页上的登录、注册、内容、订阅、打赏等等这些功能其实都是服务器在做的。
        • 3、在一个项目中,用户是接触不到服务器的,只能接触到客户端:
          • 客户端:浏览器、客户端的安装包(用户需要下载、安装使用)、手机中写一个APP
          • 这些都是跟用户直接接触的。
    • 因此,我们Java是主要做后端服务器开发的,是很少去跟用户直接接触的
    • 所以,图形化(GUI)这个知识点在企业中很少会有人去用,因为这个原因,很多课程当中,就把这章节的内容删掉了
  • 3、难道图形化用户界面这个知识点是真的一点用都没有了吗?

    • 我是不敢苟同的,因为我觉得这个知识点对于初学者来说用处很大:
      • (1)我们之前学习的知识点,都是一个一个模块进行学习的,非常的零散,而且用的并不是很多
      • (2)所以在我们的头脑中,这些一个一个模块的知识点是零散的,哪怕我们都学完了,代码也能写出来了!依然没有太大的底气和别人说我们有技术!
    • 因此,我们要做一个阶段项目,将这些零零散散的知识点整合起来,形成系统的概念(重点)
      • 假如说现在我们不写这个阶段项目,继续的往后学习新的知识点,接下来要学习第二阶段的JavaWeb
      • 这样的话,又要用一两个月学完一整个JavaWeb之后,才会有项目做,这样的话,就会产生一个问题:
        • 就是JavaSE基础各模块的知识点都没有整合成系统的概念,都没做过项目整合过。
        • 就开始做JavaSE、JavaWeb一起学习完之后的项目,根本就是很难接受的,脑子就会乱。
    • 所以,我们要做一个阶段项目,练练手,将JavaSE基础知识点整合起来!用起来!
  • 4、那我们之前做的学生管理系统、文字版格斗游戏、评委打分等等那些难道不是项目吗?

    • 可以很负责任的告诉你,那些只能算是综合小案例,完全不能算是项目。
    • 为什么这么说?
      • 因为你想啊!我们平时玩电脑游戏、看小说、登录注册等等这些,是不是都是用鼠标点一下就会出界面了!
      • 点一下就做对应的事情!这种思想是非常重要的!
      • 没有图形化界面,是实现不了这种效果的。


2、所用知识

  • 1、面向对象三大特征:
    • 封装、继承、多态
  • 2、抽象类、接口、内部类
  • 3、集合、字符串、数组、循环、判断
  • 4、…


3、主界面分析

  • 拆分成三部分:

    • 1、最外层的窗体

      • JFrame
        • 全称Java Frame:
          • Java:就是我们的Java语言
          • Frame:窗体、边框、界面

      在这里插入图片描述



    • 2、最上层的菜单

      • JMenuBar
        • 全称Java Menu Bar:
          • Java:就是我们的Java语言
          • Meun:菜单
          • Bar:栏目、条目

      在这里插入图片描述



    • 3、管理文字和图片的容器

      • JLabel

        • 全称Java Label
          • Java:就是我们的Java语言
          • Label:理解为容器、区域的意思就可以了

        在这里插入图片描述



  • 最外层的窗体、最上层的菜单、管理文字和图片的容器都称为组件。
  • 包括以后我们看到的图片、按钮、进度条、文字等等都是组件。
  • 组件它是一个统称

在这里插入图片描述




二、界面搭建

1、需求

  • 创建主界面1
    • 最外层的窗体:
      • 1、创建一个宽603像素,高680像素的游戏主界面
      • 2、创建一个宽501像素,高437像素的登录界面
      • 3、创建一个宽501像素,高437像素的注册界面

2、分析

  • 从需求可以看出,游戏主界面、登录界面、注册界面都有一个共同的地方。
  • 那就是它们都是界面,因此它们都是JFrame的子类,所以需要将每个界面都独立成一个界面类,继承父类:JFrame
  • 如此一来:
    • 1、关于游戏相关的逻辑代码就可以全部写在游戏主界面类中
    • 2、关于登录相关的逻辑代码就可以全部写在登录界面类中
    • 3、关于注册相关的逻辑代码就可以全部写在注册界面类中
  • 最后,只需要在main方法中构造各个界面就可以显示出来了

3、实现

(1)构建项目文件

在这里插入图片描述


(2)游戏主界面类

package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame{// 表示游戏相关的逻辑代码都写在这!/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有setSize、setVisible方法,就会自动调用父类JFrame的// 调用setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面this.setVisible(true);}
}

(3)登录界面类

package cn.edu.gxufe.ui;import javax.swing.*;/*** 登录界面类:* 继承父类:JFrame*/
public class LoginJFrame extends JFrame {// 表示登录相关的逻辑代码都写在这!/*提供无参数的构造器需求:初始化一个宽488像素,高430像素的登录界面*/public LoginJFrame() {// 注:如果当前类没有setSize、setVisible方法,就会自动调用父类JFrame的// 设置界面宽高// 调用setSize方法,初始化一个宽501像素,高437像素大小的界面this.setSize(501, 437);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面this.setVisible(true);}
}

(4)注册界面类

package cn.edu.gxufe.ui;import javax.swing.*;/*** 注册界面类:* 继承父类:JFrame*/
public class RegisterJFrame extends JFrame{// 表示注册相关的逻辑代码都写在这!/*提供无参数的构造器需求:初始化一个宽488像素,高500像素的登录界面*/public RegisterJFrame() {// 注:如果当前类没有setSize、setVisible方法,就会自动调用父类JFrame的// 调用setSize方法,初始化一个宽501像素,高437像素大小的界面this.setSize(501, 437);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面this.setVisible(true);}
}

(5)程序的启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面new LoginJFrame();      // 登录界面new RegisterJFrame();   // 注册界面}
}

(6)测试结果

  • 可以看到,三个界面已经构建好了!

在这里插入图片描述


(7)使用继承的好处

在这里插入图片描述

  • 可以将代码分类出来,避免将全部逻辑代码全部写在一个类中



三、界面设置、菜单搭建

1、界面设置需求

  • 设置各个界面:
    • 设置界面宽高:
      • 已设置
    • 设置界面标题:
      • 游戏主界面:奥利gei拼图单机版 v1.0
      • 登录界面:奥利gei拼图-登录
      • 注册界面:奥利gei拼图-注册
    • 设置界面置顶:
      • 用户在点击其他界面时,该界面都永远置顶。
    • 设置界面居中:
      • 用户打开界面时,自动居中。
    • 设置界面的关闭模式:
      • 用户只需要关闭一个界面,其他界面自动关闭,并且结束JVM虚拟机运行。
    • 设置界面为显示的:
      • 已设置。
      • 由于界面默认是隐藏的,因此需要设置为显示的。

2、界面设置实现

  • 游戏主界面类

    package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
    public class GameJFrame extends JFrame{// 表示游戏相关的逻辑代码都写在这!/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 1、初始化游戏主界面initJFrame();// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);}
    }
    

  • 登录界面类

    package cn.edu.gxufe.ui;import javax.swing.*;/*** 登录界面类:* 继承父类:JFrame*/
    public class LoginJFrame extends JFrame {// 表示登录相关的逻辑代码都写在这!/*提供无参数的构造器需求:初始化一个宽501像素,高437像素的登录界面*/public LoginJFrame() {// 初始化界面initJFrame();// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面this.setVisible(true);}// 初始化登录界面private void initJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 设置界面宽高// 调用setSize方法,初始化一个宽501像素,高437像素大小的界面this.setSize(501, 437);// 设置界面标题this.setTitle("奥利gei拼图-登录");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行this.setDefaultCloseOperation(3);// 取消默认布局,这样子才可以让布局按照x和y轴的形式添加组件this.setLayout(null);}
    }
    

  • 注册界面类

    package cn.edu.gxufe.ui;import javax.swing.*;/*** 注册界面类:* 继承父类:JFrame*/
    public class RegisterJFrame extends JFrame{// 表示注册相关的逻辑代码都写在这!/*提供无参数的构造器需求:初始化一个宽501像素,高437像素的登录界面*/public RegisterJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 初始化注册界面initJFrame();// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面this.setVisible(true);}// 初始化注册界面private void initJFrame() {// 设置界面宽高// 调用setSize方法,初始化一个宽501像素,高437像素大小的界面this.setSize(501, 437);// 设置界面标题this.setTitle("奥利gei拼图-注册");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 取消默认布局,这样子才可以让布局按照x和y轴的形式添加组件this.setLayout(null);}
    }
    

  • 程序的启动入口类

    import cn.edu.gxufe.ui.GameJFrame;
    import cn.edu.gxufe.ui.LoginJFrame;
    import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面new LoginJFrame();      // 登录界面new RegisterJFrame();   // 注册界面}
    }
    

  • 测试结果

    在这里插入图片描述



3、游戏主界面的菜单搭建需求

在这里插入图片描述

在这里插入图片描述


4、游戏主界面的菜单结构分析

在这里插入图片描述

  • 1、需要用到JMenuBar菜单栏类:
    • JMenuBar:是一整根大长条的菜单栏
    • JMeun:是菜单栏下的一个个的菜单
    • JMenuItem:是菜单下的一个个条目
  • 2、因此需要先创建JMenuBar:菜单栏
  • 3、接着创建JMenu:菜单
  • 4、然后创建JMenuItem:菜单条目
  • 5、最后做整合操作
    • 先把JMenuItem放到JMenu里面
    • 然后把JMenu放到JMenuBar里面
    • 最后把JMenuBar放到GameJFrame里面
  • 注意:更换图片的功能较为复杂,我们放到后面再写!!

5、游戏界面的菜单搭建实现

(1)游戏主界面类
package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame{// 表示游戏相关的逻辑代码都写在这!/*创建二维数组:目的:管理数据加载图片的时候,会根据二位数组中的数据进行加载*/int[][] data = new int[4][4];// 定义x、y变量,用于记录空白方块所在的XY的坐标位置int x = 0;int y = 0;// 创建一个成员的随机数对象,用于生成一个随机数Random rd = new Random();// 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现String path = "puzzle_game\puzzleimages\girl\girl1\";// 定义一个正确顺序数据的二维数组int[][] win = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 定义一个计数器变量,用于记录玩家拼图移动的步数int step = 0;// c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏// 更换图片栏目放到后面再写,因为比较复杂JMenuItem replayGameItem = new JMenuItem("重新游戏");JMenuItem replayLoginItem = new JMenuItem("重新登录");JMenuItem closeGameItem = new JMenuItem("关闭游戏");// d.创建关于我们菜单下的一个栏目:公众号JMenuItem accountItem = new JMenuItem("公众号");// 创建更换图片菜单下的两个栏目:美女、动物JMenuItem belleItem = new JMenuItem("美女");JMenuItem animalItem = new JMenuItem("动物");/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 1、初始化游戏主界面initJFrame();// 2、初始化游戏主界面里的菜单栏initJMenu();// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);}// 初始化游戏主界面里的菜单栏private void initJMenu() {// a.创建一个菜单栏对象JMenuBar jMenuBar = new JMenuBar();// b.创建两个菜单栏下的菜单:功能、关于我们JMenu functionJMenu = new JMenu("功能");JMenu aboutUSJMenu = new JMenu("关于我们");// 创建菜单栏下的菜单:更换图片JMenu changeJMenu = new JMenu("更换图片");// 将更换图片的菜单嵌套进功能菜单中functionJMenu.add(changeJMenu);// e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下functionJMenu.add(replayGameItem);functionJMenu.add(replayLoginItem);functionJMenu.add(closeGameItem);// 将美女、动物这两个栏目放到更换图片菜单下changeJMenu.add(belleItem);changeJMenu.add(animalItem);// f.将公众号这个栏目放到关于我们菜单下aboutUSJMenu.add(accountItem);// g.将功能、关于我们这两个菜单放到菜单栏下jMenuBar.add(functionJMenu);jMenuBar.add(aboutUSJMenu);// h.最后将菜单栏放到设置到游戏界面中this.setJMenuBar(jMenuBar);}
}

(2)程序的启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面}
}

(3)测试结果

在这里插入图片描述


在这里插入图片描述




四、添加图片

1、需求

在这里插入图片描述

  • 一整张图片的像素是420x420
  • 将一整张图片拆分成15张小图片,每张小图片的像素是105x105

2、分析

  • 1、需要用到管理容器类JLabel
    • 用于管理文字、图片
  • 2、需要用到图像类ImageIcon
    • 用于根据指定文件路径创建一个图片对象
  • 3、使用循环嵌套动态控制添加4行图片,每行添加4张小图片
    • 根据指定文件路径创建一个图片对象
    • 创建一个管理容器,并将图片对象放进来
    • 将管理容器放到游戏主界面中

3、实现

(1)游戏主界面类

package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame{// 表示游戏相关的逻辑代码都写在这!/*创建二维数组:目的:管理数据加载图片的时候,会根据二位数组中的数据进行加载*/int[][] data = new int[4][4];// 定义x、y变量,用于记录空白方块所在的XY的坐标位置int x = 0;int y = 0;// 创建一个成员的随机数对象,用于生成一个随机数Random rd = new Random();// 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现String path = "puzzle_game\puzzleimages\girl\girl1\";// 定义一个正确顺序数据的二维数组int[][] win = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 定义一个计数器变量,用于记录玩家拼图移动的步数int step = 0;// c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏// 更换图片栏目放到后面再写,因为比较复杂JMenuItem replayGameItem = new JMenuItem("重新游戏");JMenuItem replayLoginItem = new JMenuItem("重新登录");JMenuItem closeGameItem = new JMenuItem("关闭游戏");// d.创建关于我们菜单下的一个栏目:公众号JMenuItem accountItem = new JMenuItem("公众号");// 创建更换图片菜单下的两个栏目:美女、动物JMenuItem belleItem = new JMenuItem("美女");JMenuItem animalItem = new JMenuItem("动物");/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 初始化游戏主界面initJFrame();// 初始化游戏主界面里的菜单栏initJMenu();// 初始化图片到游戏主界面中initImages(path);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);}// 初始化游戏主界面里的菜单栏private void initJMenu() {// a.创建一个菜单栏对象JMenuBar jMenuBar = new JMenuBar();// b.创建两个菜单栏下的菜单:功能、关于我们JMenu functionJMenu = new JMenu("功能");JMenu aboutUSJMenu = new JMenu("关于我们");// 创建菜单栏下的菜单:更换图片JMenu changeJMenu = new JMenu("更换图片");// 将更换图片的菜单嵌套进功能菜单中functionJMenu.add(changeJMenu);// e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下functionJMenu.add(replayGameItem);functionJMenu.add(replayLoginItem);functionJMenu.add(closeGameItem);// 将美女、动物这两个栏目放到更换图片菜单下changeJMenu.add(belleItem);changeJMenu.add(animalItem);// f.将公众号这个栏目放到关于我们菜单下aboutUSJMenu.add(accountItem);// g.将功能、关于我们这两个菜单放到菜单栏下jMenuBar.add(functionJMenu);jMenuBar.add(aboutUSJMenu);// h.最后将菜单栏放到设置到游戏界面中this.setJMenuBar(jMenuBar);}/*初始化图片到游戏主界面中细节:先加载的图片在上方,后加载的图片会在下方*/private void initImages(String path) {// 先清空原本出现的所有图片this.getContentPane().removeAll();// 判断victory方法的返回结果是否为trueif (victory()) {// 是,则说明玩家胜利了!显示胜利图标!// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));// 设置胜利图片的坐标位置以及宽高winJLabel.setBounds(193, 300, 194, 75);// 将管理容器添加到游戏主界面中this.getContentPane().add(winJLabel);}// 统计步数// 创建一个管理容器对象,用于管理文字(步数: 0)JLabel stepJLabel = new JLabel("步数:" + step);// 设置管理容器的坐标位置stepJLabel.setBounds(40, 20, 400, 50);// 将容器添加到游戏主界面中this.getContentPane().add(stepJLabel);// 再加载图片/*外循环和内循环执行流程解析:外循环第一次执行:当 i = 0 时:i<4,为true,表示添加第一行的四张图片:当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第二次执行:当 i = 1 时:i<4,为true,表示添加第二行的四张图片:当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第三次执行:当 i = 2 时:i<4,为true,表示添加第三行的四张图片:当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第四次执行:当 i = 3 时:i<4,为true,表示添加第四行的四张图片:当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)当 j = 4 时:j<4,为false,内循环结束!外循环第五次执行:当 i = 4 时:i<4,为false,外循环结束!*/// 外循环:控制添加4行图片for (int i = 0; i < data.length; i++) { // Y轴:纵向// 内循环:控制每行添加4张图片for (int j = 0; j < data[i].length; j++) { // X轴:横向/*解析:比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3二维数组的索引:       0             1              2              3(1) 外循环执行第一次:i=0, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(2) 外循环执行第二次:i=1, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(3) 外循环执行第三次:i=2, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(4) 外循环执行第四次:i=3, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(5) 外循环执行第五次:i=0, i<二维数组长度(4), 为false, 外循环结束!!*/// 接收二维数组i索引的一维数组j索引的数据int number = data[i][j];// 1、根据指定的文件相对路径创建一个ImageIcon图片对象ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");// JLabel:管理容器,用于管理图片、文字// 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,// 并将图片对象imageIcon放到管理容器jLabel中。JLabel jLabel = new JLabel(imageIcon);// 3、指定图片位置:XY轴jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);// 4、每添加完一张小图片,给这张小图片添加边框/*0: 表示让图片凸起来1: 表示让图片凹下去*/jLabel.setBorder(new BevelBorder(1));// 5、将管理容器JLabel对象放到游戏主界面中// getContentPane:获取隐藏容器this.getContentPane().add(jLabel);}}// 在所有小图片都添加到界面之后,开始添加背景图片// 根据指定的相对路径创建背景图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将背景图片管理容器添加到界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}
}

(2)程序启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面}
}

(3)测试结果

  • 这是我自己
    在这里插入图片描述



五、打乱图片

1、需求

  • 打乱图片顺序

    在这里插入图片描述


2、分析

  • 1、可以用0~15(包含15)来代表每张小的图片

    //定义一个一维数组,将0~15的数据先存储起来
    int arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
    

    在这里插入图片描述



  • 2、然后将这些数据随机打乱顺序

    // 打乱后
    int arr = {10, 5, 14, 15, 6, 12, 3, 11, 0, 8, 4, 13, 1, 9, 7, 2};
    


  • 3、将这些随机打乱后的数据按照每4个作为一个一维数组存储到二维数组中

    // 二维数组
    int[][] data = { {10, 5, 14, 15}, {6, 12, 3, 11}, {0, 8, 4, 13}, {1, 9, 7, 2} };
    

    在这里插入图片描述


    在这里插入图片描述



  • 4、最终按照二维数组中的每个一维数组的数据来初始化每张小图片到游戏主界面中


3、实现

(1)游戏主界面类

package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame{// 表示游戏相关的逻辑代码都写在这!/*创建二维数组:目的:管理数据加载图片的时候,会根据二位数组中的数据进行加载*/int[][] data = new int[4][4];// 定义x、y变量,用于记录空白方块所在的XY的坐标位置int x = 0;int y = 0;// 创建一个成员的随机数对象,用于生成一个随机数Random rd = new Random();// 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现String path = "puzzle_game\puzzleimages\girl\girl1\";// 定义一个正确顺序数据的二维数组int[][] win = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 定义一个计数器变量,用于记录玩家拼图移动的步数int step = 0;// c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏// 更换图片栏目放到后面再写,因为比较复杂JMenuItem replayGameItem = new JMenuItem("重新游戏");JMenuItem replayLoginItem = new JMenuItem("重新登录");JMenuItem closeGameItem = new JMenuItem("关闭游戏");// d.创建关于我们菜单下的一个栏目:公众号JMenuItem accountItem = new JMenuItem("公众号");// 创建更换图片菜单下的两个栏目:美女、动物JMenuItem belleItem = new JMenuItem("美女");JMenuItem animalItem = new JMenuItem("动物");/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 初始化游戏主界面initJFrame();// 初始化游戏主界面里的菜单栏initJMenu();// 初始化数据:打乱initData();// 初始化图片到游戏主界面中initImages(path);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化数据:打乱private void initData() {// 1、创建一个一维数组,用于存储一些数据int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};// 2、开始打乱// 遍历数组,依次获取到数组中的每个数据for (int i = 0; i < arr.length; i++) {// 循环每执行一次,就生成一个随机索引int rdIndex = rd.nextInt(arr.length);// 每遍历到一个数据,就用临时变量存储一下int temp = arr[i];// 开始交换// 将随机索引位置的数据 放到 当前遍历到的数据的位置arr[i] = arr[rdIndex];// 将当前遍历到的数据 放到 随机索引位置arr[rdIndex] = temp;}/*解析:比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15循环第一次执行:i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++循环第二次执行:i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++循环第三次执行:i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++后面的都是以此类推了!直到i<16,为false,循环结束!*/// 3、遍历arr一维数组,依次得到打乱后的每个数据for (int i = 0; i < arr.length; i++) {// 判断当前数据是否为0if (arr[i] == 0) {// 是,则记录空白方块XY的坐标位置x = i / 4;y = i % 4;}// 否,则依次将打乱后的数据添加到二维数组中data[i / 4][i % 4] = arr[i];}}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);}// 初始化游戏主界面里的菜单栏private void initJMenu() {// a.创建一个菜单栏对象JMenuBar jMenuBar = new JMenuBar();// b.创建两个菜单栏下的菜单:功能、关于我们JMenu functionJMenu = new JMenu("功能");JMenu aboutUSJMenu = new JMenu("关于我们");// 创建菜单栏下的菜单:更换图片JMenu changeJMenu = new JMenu("更换图片");// 将更换图片的菜单嵌套进功能菜单中functionJMenu.add(changeJMenu);// e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下functionJMenu.add(replayGameItem);functionJMenu.add(replayLoginItem);functionJMenu.add(closeGameItem);// 将美女、动物这两个栏目放到更换图片菜单下changeJMenu.add(belleItem);changeJMenu.add(animalItem);// f.将公众号这个栏目放到关于我们菜单下aboutUSJMenu.add(accountItem);// g.将功能、关于我们这两个菜单放到菜单栏下jMenuBar.add(functionJMenu);jMenuBar.add(aboutUSJMenu);// h.最后将菜单栏放到设置到游戏界面中this.setJMenuBar(jMenuBar);}/*初始化图片到游戏主界面中细节:先加载的图片在上方,后加载的图片会在下方*/private void initImages(String path) {// 先清空原本出现的所有图片this.getContentPane().removeAll();// 判断victory方法的返回结果是否为trueif (victory()) {// 是,则说明玩家胜利了!显示胜利图标!// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));// 设置胜利图片的坐标位置以及宽高winJLabel.setBounds(193, 300, 194, 75);// 将管理容器添加到游戏主界面中this.getContentPane().add(winJLabel);}// 统计步数// 创建一个管理容器对象,用于管理文字(步数: 0)JLabel stepJLabel = new JLabel("步数:" + step);// 设置管理容器的坐标位置stepJLabel.setBounds(40, 20, 400, 50);// 将容器添加到游戏主界面中this.getContentPane().add(stepJLabel);// 再加载图片/*外循环和内循环执行流程解析:外循环第一次执行:当 i = 0 时:i<4,为true,表示添加第一行的四张图片:当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第二次执行:当 i = 1 时:i<4,为true,表示添加第二行的四张图片:当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第三次执行:当 i = 2 时:i<4,为true,表示添加第三行的四张图片:当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第四次执行:当 i = 3 时:i<4,为true,表示添加第四行的四张图片:当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)当 j = 4 时:j<4,为false,内循环结束!外循环第五次执行:当 i = 4 时:i<4,为false,外循环结束!*/// 外循环:控制添加4行图片for (int i = 0; i < data.length; i++) { // Y轴:纵向// 内循环:控制每行添加4张图片for (int j = 0; j < data[i].length; j++) { // X轴:横向/*解析:比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3二维数组的索引:       0             1              2              3(1) 外循环执行第一次:i=0, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(2) 外循环执行第二次:i=1, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(3) 外循环执行第三次:i=2, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(4) 外循环执行第四次:i=3, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(5) 外循环执行第五次:i=0, i<二维数组长度(4), 为false, 外循环结束!!*/// 接收二维数组i索引的一维数组j索引的数据int number = data[i][j];// 1、根据指定的文件相对路径创建一个ImageIcon图片对象ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");// JLabel:管理容器,用于管理图片、文字// 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,// 并将图片对象imageIcon放到管理容器jLabel中。JLabel jLabel = new JLabel(imageIcon);// 3、指定图片位置:XY轴jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);// 4、每添加完一张小图片,给这张小图片添加边框/*0: 表示让图片凸起来1: 表示让图片凹下去*/jLabel.setBorder(new BevelBorder(1));// 5、将管理容器JLabel对象放到游戏主界面中// getContentPane:获取隐藏容器this.getContentPane().add(jLabel);}}// 在所有小图片都添加到界面之后,开始添加背景图片// 根据指定的相对路径创建背景图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将背景图片管理容器添加到界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}
}

(2)程序启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面}
}

(3)测试结果

  • 多执行几次,看看是不是随机的

    在这里插入图片描述


    在这里插入图片描述




六、事件

1、什么是事件?

  • 事件是可以被组件识别的操作。
  • 当你对组件做了某件事情之后,就会执行对应的程序代码。
  • 举个例子
    • 比如一个猥琐男对另一个男的施行了猥亵,就触发了警察抓他的事件!
    • 比如小时候我们拿弹弓打碎了玻璃,就触发了妈妈打我们屁股的事件!
    • 比如当用户点击登录按钮之后,就触发了校验用户名和密码的事件!
    • 比如…

2、事件三大个核心要素

  • 1、事件源:按钮、图片、窗体…
  • 2、事件:某些操作
    • 如:鼠标单击,鼠标划入…
  • 3、绑定监听:当事件源上发生了某个事件,就会执行相应的程序代码。
    • 如:当登录按钮被点击之后,就会执行校验用户名和密码的程序代码。

3、介绍常见监听

  • KeyListener:键盘监听

    • 比如电脑的快捷键就是这么干的!
    • 当你按下键盘的Ctrl+C的快捷键时,就会执行复制的程序代码!
  • MouseListener:鼠标监听

    • 比如lol英雄联盟,当你用鼠标点击界面时,会出现很多问号!并且还会出现叮叮叮的声音!!

      在这里插入图片描述

  • ActionListener:动作监听

    • 可以说是键盘监听、鼠标监听的精简版!
    • 动作监听,只能监听鼠标的左键点击操作、键盘的空格操作!


(1)ActionListener:动作监听

  • 只能监听到鼠标的左键点击操作、键盘的空格操作!
实现方式一:创建实现ActionListener实现类
package cn.edu.gxufe.test;import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;// 创建动作监听ActionListener接口的实现类,用于实现接口里的单击按钮方法
public class MyActionListenerImpl implements ActionListener{// 实现ActionListener接口中的单击按钮方法@Overridepublic void actionPerformed(ActionEvent e) {System.out.println("我是ActionListener动作监听接口的单击按钮实现方法,我被你点击了!");}
}
package cn.edu.gxufe.test;import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;// 测试类
public class Test2 extends JFrame {public static void main(String[] args) {// 初始化界面JFrame jFrame = new JFrame();// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面jFrame.setSize(603, 680);// 设置界面标题jFrame.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的jFrame.setAlwaysOnTop(true);// 设置界面打开时自动居中jFrame.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置jFrame.setLayout(null);// 重点:// 创建一个按钮对象JButton jbt1 = new JButton("点我呗");// 设置位置和宽高jbt1.setBounds(0, 0, 100, 50);// 给按钮添加动作监听// jbt1: 组件对象,表示你要给哪个组件添加事件// addActionListener: 表示我要给组件添加哪个事件监听(动作监听包含鼠标左键点击,空格)// 参数: 表示事件被触发之后要执行的代码// 方式一:实现类实现接口的单击按钮方法jbt1.addActionListener(new MyActionListenerImpl());// 把按钮添加到界面当中jFrame.getContentPane().add(jbt1);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面jFrame.setVisible(true);}
}

实现方式一的测试结果

在这里插入图片描述



实现方式二:匿名内部类
package cn.edu.gxufe.test;import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;// 测试类
public class Test2 extends JFrame {public static void main(String[] args) {// 初始化界面JFrame jFrame = new JFrame();// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面jFrame.setSize(603, 680);// 设置界面标题jFrame.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的jFrame.setAlwaysOnTop(true);// 设置界面打开时自动居中jFrame.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置jFrame.setLayout(null);// 重点:// 创建一个按钮对象JButton jbt1 = new JButton("点我呗");// 设置位置和宽高jbt1.setBounds(0, 0, 100, 50);// 给按钮添加动作监听// jbt1: 组件对象,表示你要给哪个组件添加事件// addActionListener: 表示我要给组件添加哪个事件监听(动作监听包含鼠标左键点击,空格)// 参数: 表示事件被触发之后要执行的代码// 方式一:实现类实现接口的单击按钮方法
//        jbt1.addActionListener(new MyActionListenerImpl());// 方式二:匿名内部类直接实现接口的单击按钮方法jbt1.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {System.out.println("达咩!不要点我喔~ 会疼喔!!");}});// 把按钮添加到界面当中jFrame.getContentPane().add(jbt1);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面jFrame.setVisible(true);}
}

实现方式二的测试结果

在这里插入图片描述



开发中的写法
  • 直接在本界面类中实现ActionListener接口的单击按钮方法
(1)界面类
package cn.edu.gxufe.test;import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;public class MyFrame extends JFrame implements ActionListener {// 创建按钮对象1JButton jbt1 = new JButton("点我啊");// 创建按钮对象2JButton jbt2 = new JButton("再点我啊~");public MyFrame() {// 初始化界面initJFrame();// 设置按钮的位置和宽高jbt1.setBounds(0, 0, 100, 50);// 给按钮添加动作监听事件// jbt1: 组件对象,表示你要给哪个组件添加事件// addActionListener: 表示我要给组件添加哪个事件监听(动作监听包含鼠标左键点击,空格)// 参数: 表示事件被触发之后要执行的代码jbt1.addActionListener(this);// 设置按钮的位置和宽高jbt2.setBounds(100, 0, 100, 50);// 给按钮添加动作监听事件jbt2.addActionListener(this);// 添加两个按钮到当前界面中this.getContentPane().add(jbt1);this.getContentPane().add(jbt2);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);}/*直接在本类实现ActionListener接口的单击按钮*/@Overridepublic void actionPerformed(ActionEvent e) {// 获取当前的按钮对象Object source = e.getSource();// 判断当前的按钮对象是否为jbt1if (source == jbt1) {// 是,则将jbt1按钮的宽高都设置为150像素jbt1.setSize(150, 150);}else if (source == jbt2) { // 判断当前的按钮对象是否为jbt2// 是,则当按下jbt2按钮后,该按钮的坐标随机到500内的范围Random rd = new Random();jbt2.setLocation(rd.nextInt(500), rd.nextInt(500));}}
}

(2)测试类
package cn.edu.gxufe.test;public class Test3 {public static void main(String[] args) {// 构造界面new MyFrame();}
}

(3)测试结果

在这里插入图片描述




(2)MouseListener:鼠标监听

  • 鼠标监听

    • 单击事件:
      • 按下动作、松开动作
    • 鼠标划入动作、划出动作
  • 思考

    1. 如果以后我想监听一个按钮的单击事件,有几种方式?
      • 动作监听:ActionListener
      • 鼠标监听中的单击事件
      • 鼠标监听中的松开事件


方法摘要
返回类型方法名称
voidmouseClicked(MouseEvent e)在组件上单击(按下并释放)鼠标按钮时调用
voidmouseEntered(MouseEvent e)当鼠标进入组件时调用
voidmouseExited(MouseEvent e)当鼠标退出组件时调用
voidmousePressed(MouseEvent e)在组件上按下鼠标按钮时调用
voidmouseReleased(MouseEvent e)在组件上释放鼠标按钮时调用


实现
(1)界面类
package cn.edu.gxufe.test;import javax.swing.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;// 自定义界面类:需要继承父类(JFrame)
public class MyJFrame2 extends JFrame implements MouseListener {// 创建按钮对象JButton jbt = new JButton("点我啊");public MyJFrame2() {// 初始化界面initJFrame();// 设置按钮在界面中的坐标位置jbt.setBounds(0, 0, 100, 50);// 给按钮添加鼠标监听事件jbt.addMouseListener(this);// 将按钮添加到界面中this.getContentPane().add(jbt);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前界面类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);}// 实现鼠标监听接口MouseListener的鼠标单击事件方法@Overridepublic void mouseClicked(MouseEvent e) {System.out.println("鼠标单击了一下");}// 实现鼠标监听接口MouseListener的按下鼠标事件方法@Overridepublic void mousePressed(MouseEvent e) {System.out.println("被鼠标按了一下");}// 实现鼠标监听接口MouseListener的松开鼠标事件方法@Overridepublic void mouseReleased(MouseEvent e) {System.out.println("鼠标松开了");}// 实现鼠标监听接口MouseListener的鼠标划入事件方法@Overridepublic void mouseEntered(MouseEvent e) {System.out.println("鼠标划进来了");}// 实现鼠标监听接口MouseListener的鼠标划出事件方法@Overridepublic void mouseExited(MouseEvent e) {System.out.println("鼠标划走了");}
}

(2)测试类
package cn.edu.gxufe.test;public class Test4 {public static void main(String[] args) {// 构造界面new MyJFrame2();}
}

(3)测试结果

在这里插入图片描述




(3)KeyListener:键盘监听

方法摘要
返回类型方法名称描述
voidkeyPressed(KeyEvent e)按下键时调用
voidkeyReleased(KeyEvent e)当键已被释放时调用
voidkeyTyped(KeyEvent e)键入键时调用


实现
(1)界面类
package cn.edu.gxufe.test;import javax.swing.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;// 自定义界面类:继承父类JFrame
public class MyJFrame3 extends JFrame implements KeyListener{// 提供无参数的构造器public MyJFrame3(){// 初始化界面initJFrame();// 给整个界面添加键盘监听事件// 调用者this:本类对象,当前的界面对象,表示我要给整个界面添加监听// addKeyListener:表示要给本界面添加键盘监听// 参数this:当事件被触发之后,会执行本类中的对应代码this.addKeyListener(this);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前界面类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);}// 这个可以不用管@Overridepublic void keyTyped(KeyEvent e) {}// 实现键盘监听接口KeyListener的按下键盘方法/*细节1:如果我们按下键盘的一个键没有松开,那么会重复的去调用keyPressed方法细节2:键盘里面那么多按键,如果进行区分?答:每一个按键都有一个编号与之对应的*/@Overridepublic void keyPressed(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断当前按下的按键的编号是否为65if (keyCode == 65) {// 是,则说明按下的按键是ASystem.out.println("按下按键A");}else if (keyCode == 66){   // 否,则判断当前按下的按键的编号是否为66// 是,则说明按下的按键是BSystem.out.println("按下按键B");}else {// 否,则说明按下的是其他按键System.out.println("按下的是其他按键");}}// 实现键盘监听接口KeyListener的释放(松开)键盘方法@Overridepublic void keyReleased(KeyEvent e) {System.out.println("按键已松开");}
}

(2)测试类
package cn.edu.gxufe.test;public class Test5 {public static void main(String[] args) {// 构造界面new MyJFrame3();}
}

(3)测试结果

在这里插入图片描述




七、美化界面

1、需求

  • 按照以下美化后的要求,优化游戏界面

  • 美化之前

    在这里插入图片描述



  • 美化之后

    在这里插入图片描述



2、分析

  • 1、将15张小图片的坐标位置调整到中央偏下方
  • 2、在添加完15张小图片的代码后面,添加背景图片:
    • 细节:先加载的图片在上方,后加载的图片会在下方。
  • 3、在每添加完1张小图片的代码后面,给每张小图片添加斜角边框,让图片凹下去。
  • 4、优化文件路径:
    • 从盘符开始的:绝对路径
    • 非盘符开始的:相对路径
    • 使用相对路径优化!


3、实现

(1)游戏主界面类

package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame{// 表示游戏相关的逻辑代码都写在这!/*创建二维数组:目的:管理数据加载图片的时候,会根据二位数组中的数据进行加载*/int[][] data = new int[4][4];// 定义x、y变量,用于记录空白方块所在的XY的坐标位置int x = 0;int y = 0;// 创建一个成员的随机数对象,用于生成一个随机数Random rd = new Random();// 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现String path = "puzzle_game\puzzleimages\girl\girl1\";// 定义一个正确顺序数据的二维数组int[][] win = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 定义一个计数器变量,用于记录玩家拼图移动的步数int step = 0;// c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏// 更换图片栏目放到后面再写,因为比较复杂JMenuItem replayGameItem = new JMenuItem("重新游戏");JMenuItem replayLoginItem = new JMenuItem("重新登录");JMenuItem closeGameItem = new JMenuItem("关闭游戏");// d.创建关于我们菜单下的一个栏目:公众号JMenuItem accountItem = new JMenuItem("公众号");// 创建更换图片菜单下的两个栏目:美女、动物JMenuItem belleItem = new JMenuItem("美女");JMenuItem animalItem = new JMenuItem("动物");/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 初始化游戏主界面initJFrame();// 初始化游戏主界面里的菜单栏initJMenu();// 初始化数据:打乱initData();// 初始化图片到游戏主界面中initImages(path);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化数据:打乱private void initData() {// 1、创建一个一维数组,用于存储一些数据int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};// 2、开始打乱// 遍历数组,依次获取到数组中的每个数据for (int i = 0; i < arr.length; i++) {// 循环每执行一次,就生成一个随机索引int rdIndex = rd.nextInt(arr.length);// 每遍历到一个数据,就用临时变量存储一下int temp = arr[i];// 开始交换// 将随机索引位置的数据 放到 当前遍历到的数据的位置arr[i] = arr[rdIndex];// 将当前遍历到的数据 放到 随机索引位置arr[rdIndex] = temp;}/*解析:比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15循环第一次执行:i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++循环第二次执行:i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++循环第三次执行:i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++后面的都是以此类推了!直到i<16,为false,循环结束!*/// 3、遍历arr一维数组,依次得到打乱后的每个数据for (int i = 0; i < arr.length; i++) {// 判断当前数据是否为0if (arr[i] == 0) {// 是,则记录空白方块XY的坐标位置x = i / 4;y = i % 4;}// 否,则依次将打乱后的数据添加到二维数组中data[i / 4][i % 4] = arr[i];}}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);}// 初始化游戏主界面里的菜单栏private void initJMenu() {// a.创建一个菜单栏对象JMenuBar jMenuBar = new JMenuBar();// b.创建两个菜单栏下的菜单:功能、关于我们JMenu functionJMenu = new JMenu("功能");JMenu aboutUSJMenu = new JMenu("关于我们");// 创建菜单栏下的菜单:更换图片JMenu changeJMenu = new JMenu("更换图片");// 将更换图片的菜单嵌套进功能菜单中functionJMenu.add(changeJMenu);// e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下functionJMenu.add(replayGameItem);functionJMenu.add(replayLoginItem);functionJMenu.add(closeGameItem);// 将美女、动物这两个栏目放到更换图片菜单下changeJMenu.add(belleItem);changeJMenu.add(animalItem);// f.将公众号这个栏目放到关于我们菜单下aboutUSJMenu.add(accountItem);// g.将功能、关于我们这两个菜单放到菜单栏下jMenuBar.add(functionJMenu);jMenuBar.add(aboutUSJMenu);// h.最后将菜单栏放到设置到游戏界面中this.setJMenuBar(jMenuBar);}/*初始化图片到游戏主界面中细节:先加载的图片在上方,后加载的图片会在下方*/private void initImages(String path) {// 先清空原本出现的所有图片this.getContentPane().removeAll();// 判断victory方法的返回结果是否为trueif (victory()) {// 是,则说明玩家胜利了!显示胜利图标!// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));// 设置胜利图片的坐标位置以及宽高winJLabel.setBounds(193, 300, 194, 75);// 将管理容器添加到游戏主界面中this.getContentPane().add(winJLabel);}// 统计步数// 创建一个管理容器对象,用于管理文字(步数: 0)JLabel stepJLabel = new JLabel("步数:" + step);// 设置管理容器的坐标位置stepJLabel.setBounds(40, 20, 400, 50);// 将容器添加到游戏主界面中this.getContentPane().add(stepJLabel);// 再加载图片/*外循环和内循环执行流程解析:外循环第一次执行:当 i = 0 时:i<4,为true,表示添加第一行的四张图片:当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第二次执行:当 i = 1 时:i<4,为true,表示添加第二行的四张图片:当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第三次执行:当 i = 2 时:i<4,为true,表示添加第三行的四张图片:当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第四次执行:当 i = 3 时:i<4,为true,表示添加第四行的四张图片:当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)当 j = 4 时:j<4,为false,内循环结束!外循环第五次执行:当 i = 4 时:i<4,为false,外循环结束!*/// 外循环:控制添加4行图片for (int i = 0; i < data.length; i++) { // Y轴:纵向// 内循环:控制每行添加4张图片for (int j = 0; j < data[i].length; j++) { // X轴:横向/*解析:比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3二维数组的索引:       0             1              2              3(1) 外循环执行第一次:i=0, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(2) 外循环执行第二次:i=1, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(3) 外循环执行第三次:i=2, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(4) 外循环执行第四次:i=3, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(5) 外循环执行第五次:i=0, i<二维数组长度(4), 为false, 外循环结束!!*/// 接收二维数组i索引的一维数组j索引的数据int number = data[i][j];// 1、根据指定的文件相对路径创建一个ImageIcon图片对象ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");// JLabel:管理容器,用于管理图片、文字// 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,// 并将图片对象imageIcon放到管理容器jLabel中。JLabel jLabel = new JLabel(imageIcon);// 3、指定图片位置:XY轴jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);// 4、每添加完一张小图片,给这张小图片添加边框/*0: 表示让图片凸起来1: 表示让图片凹下去*/jLabel.setBorder(new BevelBorder(1));// 5、将管理容器JLabel对象放到游戏主界面中// getContentPane:获取隐藏容器this.getContentPane().add(jLabel);}}// 在所有小图片都添加到界面之后,开始添加背景图片// 根据指定的相对路径创建背景图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将背景图片管理容器添加到界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}
}

(2)程序启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面}
}

(3)测试结果

在这里插入图片描述




八、上下左右移动图片

1、需求

  • 实现每张小图片可以上下左右移动


2、分析

  • 我们都知道,每张小图片,都有对应的数据存放在二维数组中

    在这里插入图片描述

  • 1、我们需要先给整个界面添加键盘监听事件,监听键盘的上下左右键就可以了!

  • 2、需要定义两个变量:x、y,用于记录空白方块所在的索引位置,然后记录空白方块的所在索引

  • 3、需要获取每个按键的编号,不需要牢记,只需要在按下键盘按键的时候,输出一下编号就可以知道上下左右按键的编号了!

  • 4、要先清空原本已经出现的所有图片,然后加载完所有图片后,再刷新一下界面就好了!

  • 5、向上移动

    • 其实就是将空白方块下方的图片上移:
      • 也就是将空白方块下方的图片的索引3,1的数据 赋值到 空白方块的索引2,1中。
      • 然后再按照二维数组中的最新数据,来重新加载图片就可以了。
  • 6、向下移动

    • 其实就是将空白方块上方的图片下移:
      • 也就是将空白方块上方的图片的索引1,1的数据 赋值到 空白方块的索引2,1中。
      • 然后再按照二维数组中的最新数据,来重新加载图片就可以了。
  • 7、向左移动

    • 其实就是将空白方块右方的图片左移:
      • 也就是将空白方块右方的图片的索引2,2的数据 赋值到 空白方块的索引2,1中。
      • 然后再按照二维数组中的最新数据,来重新加载图片就可以了。
  • 8、向右移动

    • 其实就是将空白方块左方的图片右移:
      • 也就是将空白方块左方的图片的索引2,0的数据 赋值到 空白方块的索引2,1中。
      • 然后再按照二维数组中的最新数据,来重新加载图片就可以了。


3、实现

(1)游戏主界面类

package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame implements KeyListener{// 表示游戏相关的逻辑代码都写在这!/*创建二维数组:目的:管理数据加载图片的时候,会根据二位数组中的数据进行加载*/int[][] data = new int[4][4];// 定义x、y变量,用于记录空白方块所在的XY的坐标位置int x = 0;int y = 0;// 创建一个成员的随机数对象,用于生成一个随机数Random rd = new Random();// 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现String path = "puzzle_game\puzzleimages\girl\girl1\";// 定义一个正确顺序数据的二维数组int[][] win = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 定义一个计数器变量,用于记录玩家拼图移动的步数int step = 0;// c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏// 更换图片栏目放到后面再写,因为比较复杂JMenuItem replayGameItem = new JMenuItem("重新游戏");JMenuItem replayLoginItem = new JMenuItem("重新登录");JMenuItem closeGameItem = new JMenuItem("关闭游戏");// d.创建关于我们菜单下的一个栏目:公众号JMenuItem accountItem = new JMenuItem("公众号");// 创建更换图片菜单下的两个栏目:美女、动物JMenuItem belleItem = new JMenuItem("美女");JMenuItem animalItem = new JMenuItem("动物");/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 初始化游戏主界面initJFrame();// 初始化游戏主界面里的菜单栏initJMenu();// 初始化数据:打乱initData();// 初始化图片到游戏主界面中initImages(path);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化数据:打乱private void initData() {// 1、创建一个一维数组,用于存储一些数据int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};// 2、开始打乱// 遍历数组,依次获取到数组中的每个数据for (int i = 0; i < arr.length; i++) {// 循环每执行一次,就生成一个随机索引int rdIndex = rd.nextInt(arr.length);// 每遍历到一个数据,就用临时变量存储一下int temp = arr[i];// 开始交换// 将随机索引位置的数据 放到 当前遍历到的数据的位置arr[i] = arr[rdIndex];// 将当前遍历到的数据 放到 随机索引位置arr[rdIndex] = temp;}/*解析:比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15循环第一次执行:i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++循环第二次执行:i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++循环第三次执行:i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++后面的都是以此类推了!直到i<16,为false,循环结束!*/// 3、遍历arr一维数组,依次得到打乱后的每个数据for (int i = 0; i < arr.length; i++) {// 判断当前数据是否为0if (arr[i] == 0) {// 是,则记录空白方块XY的坐标位置x = i / 4;y = i % 4;}// 否,则依次将打乱后的数据添加到二维数组中data[i / 4][i % 4] = arr[i];}}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);// 给整个游戏界面添加键盘监听事件this.addKeyListener(this);}// 初始化游戏主界面里的菜单栏private void initJMenu() {// a.创建一个菜单栏对象JMenuBar jMenuBar = new JMenuBar();// b.创建两个菜单栏下的菜单:功能、关于我们JMenu functionJMenu = new JMenu("功能");JMenu aboutUSJMenu = new JMenu("关于我们");// 创建菜单栏下的菜单:更换图片JMenu changeJMenu = new JMenu("更换图片");// 将更换图片的菜单嵌套进功能菜单中functionJMenu.add(changeJMenu);// e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下functionJMenu.add(replayGameItem);functionJMenu.add(replayLoginItem);functionJMenu.add(closeGameItem);// 将美女、动物这两个栏目放到更换图片菜单下changeJMenu.add(belleItem);changeJMenu.add(animalItem);// f.将公众号这个栏目放到关于我们菜单下aboutUSJMenu.add(accountItem);// g.将功能、关于我们这两个菜单放到菜单栏下jMenuBar.add(functionJMenu);jMenuBar.add(aboutUSJMenu);// h.最后将菜单栏放到设置到游戏界面中this.setJMenuBar(jMenuBar);}/*初始化图片到游戏主界面中细节:先加载的图片在上方,后加载的图片会在下方*/private void initImages(String path) {// 先清空原本出现的所有图片this.getContentPane().removeAll();// 判断victory方法的返回结果是否为trueif (victory()) {// 是,则说明玩家胜利了!显示胜利图标!// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));// 设置胜利图片的坐标位置以及宽高winJLabel.setBounds(193, 300, 194, 75);// 将管理容器添加到游戏主界面中this.getContentPane().add(winJLabel);}// 统计步数// 创建一个管理容器对象,用于管理文字(步数: 0)JLabel stepJLabel = new JLabel("步数:" + step);// 设置管理容器的坐标位置stepJLabel.setBounds(40, 20, 400, 50);// 将容器添加到游戏主界面中this.getContentPane().add(stepJLabel);// 再加载图片/*外循环和内循环执行流程解析:外循环第一次执行:当 i = 0 时:i<4,为true,表示添加第一行的四张图片:当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第二次执行:当 i = 1 时:i<4,为true,表示添加第二行的四张图片:当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第三次执行:当 i = 2 时:i<4,为true,表示添加第三行的四张图片:当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第四次执行:当 i = 3 时:i<4,为true,表示添加第四行的四张图片:当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)当 j = 4 时:j<4,为false,内循环结束!外循环第五次执行:当 i = 4 时:i<4,为false,外循环结束!*/// 外循环:控制添加4行图片for (int i = 0; i < data.length; i++) { // Y轴:纵向// 内循环:控制每行添加4张图片for (int j = 0; j < data[i].length; j++) { // X轴:横向/*解析:比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3二维数组的索引:       0             1              2              3(1) 外循环执行第一次:i=0, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(2) 外循环执行第二次:i=1, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(3) 外循环执行第三次:i=2, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(4) 外循环执行第四次:i=3, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(5) 外循环执行第五次:i=0, i<二维数组长度(4), 为false, 外循环结束!!*/// 接收二维数组i索引的一维数组j索引的数据int number = data[i][j];// 1、根据指定的文件相对路径创建一个ImageIcon图片对象ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");// JLabel:管理容器,用于管理图片、文字// 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,// 并将图片对象imageIcon放到管理容器jLabel中。JLabel jLabel = new JLabel(imageIcon);// 3、指定图片位置:XY轴jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);// 4、每添加完一张小图片,给这张小图片添加边框/*0: 表示让图片凸起来1: 表示让图片凹下去*/jLabel.setBorder(new BevelBorder(1));// 5、将管理容器JLabel对象放到游戏主界面中// getContentPane:获取隐藏容器this.getContentPane().add(jLabel);}}// 在所有小图片都添加到界面之后,开始添加背景图片// 根据指定的相对路径创建背景图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将背景图片管理容器添加到界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}// 这个可以不用管@Overridepublic void keyTyped(KeyEvent e) {}/*监听按下键盘按键的事件细节:当按下按键不松开,会不断移动图片*/@Overridepublic void keyPressed(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断按下的按键是否为Aif (keyCode == 65) {// 需要先清空原本的所有图片this.getContentPane().removeAll();// 加载完整图片// 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));// 设置图片的坐标位置以及宽高masterMap.setBounds(80, 130, 420, 420);// 将管理容器添加到游戏界面中this.getContentPane().add(masterMap);// 在完整图片下面加载背景图片// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将管理容器添加到游戏界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}}/*监听松开键盘按键的事件:当按下按键松开后,会调用该方法*/@Overridepublic void keyReleased(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断玩家是否已经胜利!if (victory()) {// 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!return;}/*上下左右按键的编号:左:37、上:38、右:39、下:40*/// 判断松开的按键是否为上if (keyCode == 38) {// 判断空白方块的X轴是否为3if (x == 3) {// 是,说明空白方块下方已没有图片可以向上移动了,则提示!System.out.println("下方已没有图片可以向上移动了");return; // 结束方法!}// 否,则图片向上移动System.out.println("图片向上移动");/*图片向上移动逻辑:把空白方块下方的数字向上移动x, y: 表示空白方块x + 1, y: 表示空白方块下方的数字*/// 将空白方块下方图片的数据 赋值给 空白方块处data[x][y] = data[x + 1][y];// 空白方块下方图片的数据赋值为0data[x + 1][y] = 0;// 将空白方块往下移动x++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 40) { // 否,则判断松开的按键是否为下// 是,则判断空白方块的X轴是否在0位置if (x == 0) {// 是,说明空白方块的上方已没有图片可以向下移动了,则提示!System.out.println("上方已没有图片可以向下移动了");return; // 结束方法!}// 否,则图片向下移动System.out.println("图片向下移动");/*图片向下移动逻辑:把空白方块上方的数字向下移动x, y: 表示空白方块x - 1, y: 表示空白方块上方的数字*/// 将空白方块上方图片的数据 赋值给 空白方块处data[x][y] = data[x - 1][y];// 空白下方图片的数据赋值为0data[x - 1][y] = 0;// 将空白方块往上移动x--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 37) { // 否,则判断松开的按键是否为左// 是,则判断空白方块的Y轴是否为3if (y == 3) {// 是,说明空白方块的右方已没有图片可以向左移动了,则提示!System.out.println("右方已没有图片可以向左移动了");return; // 结束方法!}// 否,则图片向左移动System.out.println("图片向左移动");/*图片向左移动逻辑:把空白方块右方的数字向左移动x, y: 表示空白方块x, y + 1: 表示空白方块右方的数字*/// 将空表方块右方图片的数据 赋值给 空白方块处data[x][y] = data[x][y + 1];// 空白右方图片的数据赋值为0data[x][y + 1] = 0;// 将空白方块往右移动y++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 39) { // 否,则判断松开的按键是否为右// 是,则判断空白方块的Y轴是否在0位置if (y == 0) {// 是,说明空白方块的左方已没有图片可以向右移动了,则提示!System.out.println("左方已没有图片可以向右移动了");return; // 结束方法!}// 否,则图片向右移动System.out.println("图片向右移动");/*图片向右移动逻辑:把空白方块左方的数字向右移动x, y: 表示空白方块x, y - 1: 表示空白方块左方的数字*/// 将空表方块左方图片的数据 赋值给 空白方块处data[x][y] = data[x][y - 1];// 空白左方图片的数据赋值为0data[x][y - 1] = 0;// 将空白方块往左移动y--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 65) { // 否,则判断松开的按键是否为A// 显示随机打乱的图片initImages(path);} else if (keyCode == 87) { // 否,则判断松开的按键是否为W// 是,则显示拼图完成后的效果!// 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组data = new int[][]{{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 按照二维数组的最新数据重新加载所有小图片initImages(path);}}
}


(2)程序启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面}
}


(3)测试结果

在这里插入图片描述



4、移动图片业务小结

  • 1、本类实现键盘监听接口:KeyListener,并重写所有抽象方法
  • 2、给整个界面添加键盘监听事件
  • 3、统计一下空白方块对应的数字0在二维数组中的位置
  • 4、在keyPressed方法当中实现图片移动的逻辑程序代码,当然也可以在keyReleased方法中实现。
    • 注意:
      • keyPressed方法是按下按键触发事件,只要你不松开按键,就会重复触发!
      • keyReleased方法是松开按键触发事件,如果你不松开按键,就不会触发,一松开就触发!
  • 5、Bug修复:
    • 当空白方块在最上方的时候,无法再次进行下移;
    • 当空白方块在最下方的时候,无法再次进行上移;
    • 当空白方块在最左方的时候,无法再次进行右移;
    • 当空白方块在最右方的时候,无法再次进行左移。



九、查看完整图片、作弊码和判断胜利

1、查看完整图片

(1)需求

  • 当按住按键A不松开的时候,显示完整图片
  • 当松开按键A的时候,显示随机打乱的图片

(2)分析

  • 1、先需要给整个界面添加键盘监听事件
  • 2、再需要在按下键盘的方法中实现当按下按键A的时候,显示完整图片的逻辑代码
  • 3、最后只需要在松开键盘的方法中实现当松开按键A的时候,显示随机打乱的图片的逻辑代码
  • 4、优化一下相对路径:
    • 将图片的所有上一层路径定义成一个成员变量path,这样以后要做更换图片的逻辑代码的时候,
    • 就只需要改写path变量里面记录的值就好了!

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame implements KeyListener{// 表示游戏相关的逻辑代码都写在这!/*创建二维数组:目的:管理数据加载图片的时候,会根据二位数组中的数据进行加载*/int[][] data = new int[4][4];// 定义x、y变量,用于记录空白方块所在的XY的坐标位置int x = 0;int y = 0;// 创建一个成员的随机数对象,用于生成一个随机数Random rd = new Random();// 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现String path = "puzzle_game\puzzleimages\girl\girl1\";// 定义一个正确顺序数据的二维数组int[][] win = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 定义一个计数器变量,用于记录玩家拼图移动的步数int step = 0;// c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏// 更换图片栏目放到后面再写,因为比较复杂JMenuItem replayGameItem = new JMenuItem("重新游戏");JMenuItem replayLoginItem = new JMenuItem("重新登录");JMenuItem closeGameItem = new JMenuItem("关闭游戏");// d.创建关于我们菜单下的一个栏目:公众号JMenuItem accountItem = new JMenuItem("公众号");// 创建更换图片菜单下的两个栏目:美女、动物JMenuItem belleItem = new JMenuItem("美女");JMenuItem animalItem = new JMenuItem("动物");/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 初始化游戏主界面initJFrame();// 初始化游戏主界面里的菜单栏initJMenu();// 初始化数据:打乱initData();// 初始化图片到游戏主界面中initImages(path);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化数据:打乱private void initData() {// 1、创建一个一维数组,用于存储一些数据int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};// 2、开始打乱// 遍历数组,依次获取到数组中的每个数据for (int i = 0; i < arr.length; i++) {// 循环每执行一次,就生成一个随机索引int rdIndex = rd.nextInt(arr.length);// 每遍历到一个数据,就用临时变量存储一下int temp = arr[i];// 开始交换// 将随机索引位置的数据 放到 当前遍历到的数据的位置arr[i] = arr[rdIndex];// 将当前遍历到的数据 放到 随机索引位置arr[rdIndex] = temp;}/*解析:比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15循环第一次执行:i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++循环第二次执行:i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++循环第三次执行:i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++后面的都是以此类推了!直到i<16,为false,循环结束!*/// 3、遍历arr一维数组,依次得到打乱后的每个数据for (int i = 0; i < arr.length; i++) {// 判断当前数据是否为0if (arr[i] == 0) {// 是,则记录空白方块XY的坐标位置x = i / 4;y = i % 4;}// 否,则依次将打乱后的数据添加到二维数组中data[i / 4][i % 4] = arr[i];}}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);// 给整个游戏界面添加键盘监听事件this.addKeyListener(this);}// 初始化游戏主界面里的菜单栏private void initJMenu() {// a.创建一个菜单栏对象JMenuBar jMenuBar = new JMenuBar();// b.创建两个菜单栏下的菜单:功能、关于我们JMenu functionJMenu = new JMenu("功能");JMenu aboutUSJMenu = new JMenu("关于我们");// 创建菜单栏下的菜单:更换图片JMenu changeJMenu = new JMenu("更换图片");// 将更换图片的菜单嵌套进功能菜单中functionJMenu.add(changeJMenu);// e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下functionJMenu.add(replayGameItem);functionJMenu.add(replayLoginItem);functionJMenu.add(closeGameItem);// 将美女、动物这两个栏目放到更换图片菜单下changeJMenu.add(belleItem);changeJMenu.add(animalItem);// f.将公众号这个栏目放到关于我们菜单下aboutUSJMenu.add(accountItem);// g.将功能、关于我们这两个菜单放到菜单栏下jMenuBar.add(functionJMenu);jMenuBar.add(aboutUSJMenu);// h.最后将菜单栏放到设置到游戏界面中this.setJMenuBar(jMenuBar);}/*初始化图片到游戏主界面中细节:先加载的图片在上方,后加载的图片会在下方*/private void initImages(String path) {// 先清空原本出现的所有图片this.getContentPane().removeAll();// 判断victory方法的返回结果是否为trueif (victory()) {// 是,则说明玩家胜利了!显示胜利图标!// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));// 设置胜利图片的坐标位置以及宽高winJLabel.setBounds(193, 300, 194, 75);// 将管理容器添加到游戏主界面中this.getContentPane().add(winJLabel);}// 统计步数// 创建一个管理容器对象,用于管理文字(步数: 0)JLabel stepJLabel = new JLabel("步数:" + step);// 设置管理容器的坐标位置stepJLabel.setBounds(40, 20, 400, 50);// 将容器添加到游戏主界面中this.getContentPane().add(stepJLabel);// 再加载图片/*外循环和内循环执行流程解析:外循环第一次执行:当 i = 0 时:i<4,为true,表示添加第一行的四张图片:当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第二次执行:当 i = 1 时:i<4,为true,表示添加第二行的四张图片:当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第三次执行:当 i = 2 时:i<4,为true,表示添加第三行的四张图片:当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第四次执行:当 i = 3 时:i<4,为true,表示添加第四行的四张图片:当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)当 j = 4 时:j<4,为false,内循环结束!外循环第五次执行:当 i = 4 时:i<4,为false,外循环结束!*/// 外循环:控制添加4行图片for (int i = 0; i < data.length; i++) { // Y轴:纵向// 内循环:控制每行添加4张图片for (int j = 0; j < data[i].length; j++) { // X轴:横向/*解析:比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3二维数组的索引:       0             1              2              3(1) 外循环执行第一次:i=0, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(2) 外循环执行第二次:i=1, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(3) 外循环执行第三次:i=2, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(4) 外循环执行第四次:i=3, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(5) 外循环执行第五次:i=0, i<二维数组长度(4), 为false, 外循环结束!!*/// 接收二维数组i索引的一维数组j索引的数据int number = data[i][j];// 1、根据指定的文件相对路径创建一个ImageIcon图片对象ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");// JLabel:管理容器,用于管理图片、文字// 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,// 并将图片对象imageIcon放到管理容器jLabel中。JLabel jLabel = new JLabel(imageIcon);// 3、指定图片位置:XY轴jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);// 4、每添加完一张小图片,给这张小图片添加边框/*0: 表示让图片凸起来1: 表示让图片凹下去*/jLabel.setBorder(new BevelBorder(1));// 5、将管理容器JLabel对象放到游戏主界面中// getContentPane:获取隐藏容器this.getContentPane().add(jLabel);}}// 在所有小图片都添加到界面之后,开始添加背景图片// 根据指定的相对路径创建背景图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将背景图片管理容器添加到界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}// 这个可以不用管@Overridepublic void keyTyped(KeyEvent e) {}/*监听按下键盘按键的事件细节:当按下按键不松开,会不断移动图片*/@Overridepublic void keyPressed(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断按下的按键是否为Aif (keyCode == 65) {// 需要先清空原本的所有图片this.getContentPane().removeAll();// 加载完整图片// 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));// 设置图片的坐标位置以及宽高masterMap.setBounds(80, 130, 420, 420);// 将管理容器添加到游戏界面中this.getContentPane().add(masterMap);// 在完整图片下面加载背景图片// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将管理容器添加到游戏界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}}/*监听松开键盘按键的事件:当按下按键松开后,会调用该方法*/@Overridepublic void keyReleased(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断玩家是否已经胜利!if (victory()) {// 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!return;}/*上下左右按键的编号:左:37、上:38、右:39、下:40*/// 判断松开的按键是否为上if (keyCode == 38) {// 判断空白方块的X轴是否为3if (x == 3) {// 是,说明空白方块下方已没有图片可以向上移动了,则提示!System.out.println("下方已没有图片可以向上移动了");return; // 结束方法!}// 否,则图片向上移动System.out.println("图片向上移动");/*图片向上移动逻辑:把空白方块下方的数字向上移动x, y: 表示空白方块x + 1, y: 表示空白方块下方的数字*/// 将空白方块下方图片的数据 赋值给 空白方块处data[x][y] = data[x + 1][y];// 空白方块下方图片的数据赋值为0data[x + 1][y] = 0;// 将空白方块往下移动x++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 40) { // 否,则判断松开的按键是否为下// 是,则判断空白方块的X轴是否在0位置if (x == 0) {// 是,说明空白方块的上方已没有图片可以向下移动了,则提示!System.out.println("上方已没有图片可以向下移动了");return; // 结束方法!}// 否,则图片向下移动System.out.println("图片向下移动");/*图片向下移动逻辑:把空白方块上方的数字向下移动x, y: 表示空白方块x - 1, y: 表示空白方块上方的数字*/// 将空白方块上方图片的数据 赋值给 空白方块处data[x][y] = data[x - 1][y];// 空白下方图片的数据赋值为0data[x - 1][y] = 0;// 将空白方块往上移动x--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 37) { // 否,则判断松开的按键是否为左// 是,则判断空白方块的Y轴是否为3if (y == 3) {// 是,说明空白方块的右方已没有图片可以向左移动了,则提示!System.out.println("右方已没有图片可以向左移动了");return; // 结束方法!}// 否,则图片向左移动System.out.println("图片向左移动");/*图片向左移动逻辑:把空白方块右方的数字向左移动x, y: 表示空白方块x, y + 1: 表示空白方块右方的数字*/// 将空表方块右方图片的数据 赋值给 空白方块处data[x][y] = data[x][y + 1];// 空白右方图片的数据赋值为0data[x][y + 1] = 0;// 将空白方块往右移动y++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 39) { // 否,则判断松开的按键是否为右// 是,则判断空白方块的Y轴是否在0位置if (y == 0) {// 是,说明空白方块的左方已没有图片可以向右移动了,则提示!System.out.println("左方已没有图片可以向右移动了");return; // 结束方法!}// 否,则图片向右移动System.out.println("图片向右移动");/*图片向右移动逻辑:把空白方块左方的数字向右移动x, y: 表示空白方块x, y - 1: 表示空白方块左方的数字*/// 将空表方块左方图片的数据 赋值给 空白方块处data[x][y] = data[x][y - 1];// 空白左方图片的数据赋值为0data[x][y - 1] = 0;// 将空白方块往左移动y--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 65) { // 否,则判断松开的按键是否为A// 显示随机打乱的图片initImages(path);} else if (keyCode == 87) { // 否,则判断松开的按键是否为W// 是,则显示拼图完成后的效果!// 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组data = new int[][]{{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 按照二维数组的最新数据重新加载所有小图片initImages(path);}}
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面}
}

3、测试结果

在这里插入图片描述



2、作弊码(一键通关)

(1)需求

  • 当松开按键W的时候,直接通关,也就是将所有的小图片都拼成与原图一模一样的!

(2)分析

  • 1、先需要给整个游戏主界面添加键盘监听事件
  • 2、然后需要定义一个正确顺序数据的新二维数组,存储正确顺序的数据。
  • 3、最后需要在松开按键的方法中实现当松开按键W的时候,显示与原图一样的拼图效果:
    • 就是把正确顺序数据的新二维数组 赋值给 乱序的旧二维数组,
    • 然后再按照二维数组的最新数据重新初始化所有小图片即可。

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame implements KeyListener{// 表示游戏相关的逻辑代码都写在这!/*创建二维数组:目的:管理数据加载图片的时候,会根据二位数组中的数据进行加载*/int[][] data = new int[4][4];// 定义x、y变量,用于记录空白方块所在的XY的坐标位置int x = 0;int y = 0;// 创建一个成员的随机数对象,用于生成一个随机数Random rd = new Random();// 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现String path = "puzzle_game\puzzleimages\girl\girl1\";// 定义一个正确顺序数据的二维数组int[][] win = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 定义一个计数器变量,用于记录玩家拼图移动的步数int step = 0;// c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏// 更换图片栏目放到后面再写,因为比较复杂JMenuItem replayGameItem = new JMenuItem("重新游戏");JMenuItem replayLoginItem = new JMenuItem("重新登录");JMenuItem closeGameItem = new JMenuItem("关闭游戏");// d.创建关于我们菜单下的一个栏目:公众号JMenuItem accountItem = new JMenuItem("公众号");// 创建更换图片菜单下的两个栏目:美女、动物JMenuItem belleItem = new JMenuItem("美女");JMenuItem animalItem = new JMenuItem("动物");/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 初始化游戏主界面initJFrame();// 初始化游戏主界面里的菜单栏initJMenu();// 初始化数据:打乱initData();// 初始化图片到游戏主界面中initImages(path);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化数据:打乱private void initData() {// 1、创建一个一维数组,用于存储一些数据int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};// 2、开始打乱// 遍历数组,依次获取到数组中的每个数据for (int i = 0; i < arr.length; i++) {// 循环每执行一次,就生成一个随机索引int rdIndex = rd.nextInt(arr.length);// 每遍历到一个数据,就用临时变量存储一下int temp = arr[i];// 开始交换// 将随机索引位置的数据 放到 当前遍历到的数据的位置arr[i] = arr[rdIndex];// 将当前遍历到的数据 放到 随机索引位置arr[rdIndex] = temp;}/*解析:比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15循环第一次执行:i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++循环第二次执行:i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++循环第三次执行:i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++后面的都是以此类推了!直到i<16,为false,循环结束!*/// 3、遍历arr一维数组,依次得到打乱后的每个数据for (int i = 0; i < arr.length; i++) {// 判断当前数据是否为0if (arr[i] == 0) {// 是,则记录空白方块XY的坐标位置x = i / 4;y = i % 4;}// 否,则依次将打乱后的数据添加到二维数组中data[i / 4][i % 4] = arr[i];}}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);// 给整个游戏界面添加键盘监听事件this.addKeyListener(this);}// 初始化游戏主界面里的菜单栏private void initJMenu() {// a.创建一个菜单栏对象JMenuBar jMenuBar = new JMenuBar();// b.创建两个菜单栏下的菜单:功能、关于我们JMenu functionJMenu = new JMenu("功能");JMenu aboutUSJMenu = new JMenu("关于我们");// 创建菜单栏下的菜单:更换图片JMenu changeJMenu = new JMenu("更换图片");// 将更换图片的菜单嵌套进功能菜单中functionJMenu.add(changeJMenu);// e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下functionJMenu.add(replayGameItem);functionJMenu.add(replayLoginItem);functionJMenu.add(closeGameItem);// 将美女、动物这两个栏目放到更换图片菜单下changeJMenu.add(belleItem);changeJMenu.add(animalItem);// f.将公众号这个栏目放到关于我们菜单下aboutUSJMenu.add(accountItem);// g.将功能、关于我们这两个菜单放到菜单栏下jMenuBar.add(functionJMenu);jMenuBar.add(aboutUSJMenu);// h.最后将菜单栏放到设置到游戏界面中this.setJMenuBar(jMenuBar);}/*初始化图片到游戏主界面中细节:先加载的图片在上方,后加载的图片会在下方*/private void initImages(String path) {// 先清空原本出现的所有图片this.getContentPane().removeAll();// 判断victory方法的返回结果是否为trueif (victory()) {// 是,则说明玩家胜利了!显示胜利图标!// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));// 设置胜利图片的坐标位置以及宽高winJLabel.setBounds(193, 300, 194, 75);// 将管理容器添加到游戏主界面中this.getContentPane().add(winJLabel);}// 统计步数// 创建一个管理容器对象,用于管理文字(步数: 0)JLabel stepJLabel = new JLabel("步数:" + step);// 设置管理容器的坐标位置stepJLabel.setBounds(40, 20, 400, 50);// 将容器添加到游戏主界面中this.getContentPane().add(stepJLabel);// 再加载图片/*外循环和内循环执行流程解析:外循环第一次执行:当 i = 0 时:i<4,为true,表示添加第一行的四张图片:当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第二次执行:当 i = 1 时:i<4,为true,表示添加第二行的四张图片:当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第三次执行:当 i = 2 时:i<4,为true,表示添加第三行的四张图片:当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第四次执行:当 i = 3 时:i<4,为true,表示添加第四行的四张图片:当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)当 j = 4 时:j<4,为false,内循环结束!外循环第五次执行:当 i = 4 时:i<4,为false,外循环结束!*/// 外循环:控制添加4行图片for (int i = 0; i < data.length; i++) { // Y轴:纵向// 内循环:控制每行添加4张图片for (int j = 0; j < data[i].length; j++) { // X轴:横向/*解析:比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3二维数组的索引:       0             1              2              3(1) 外循环执行第一次:i=0, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(2) 外循环执行第二次:i=1, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(3) 外循环执行第三次:i=2, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(4) 外循环执行第四次:i=3, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(5) 外循环执行第五次:i=0, i<二维数组长度(4), 为false, 外循环结束!!*/// 接收二维数组i索引的一维数组j索引的数据int number = data[i][j];// 1、根据指定的文件相对路径创建一个ImageIcon图片对象ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");// JLabel:管理容器,用于管理图片、文字// 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,// 并将图片对象imageIcon放到管理容器jLabel中。JLabel jLabel = new JLabel(imageIcon);// 3、指定图片位置:XY轴jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);// 4、每添加完一张小图片,给这张小图片添加边框/*0: 表示让图片凸起来1: 表示让图片凹下去*/jLabel.setBorder(new BevelBorder(1));// 5、将管理容器JLabel对象放到游戏主界面中// getContentPane:获取隐藏容器this.getContentPane().add(jLabel);}}// 在所有小图片都添加到界面之后,开始添加背景图片// 根据指定的相对路径创建背景图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将背景图片管理容器添加到界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}// 这个可以不用管@Overridepublic void keyTyped(KeyEvent e) {}/*监听按下键盘按键的事件细节:当按下按键不松开,会不断移动图片*/@Overridepublic void keyPressed(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断按下的按键是否为Aif (keyCode == 65) {// 需要先清空原本的所有图片this.getContentPane().removeAll();// 加载完整图片// 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));// 设置图片的坐标位置以及宽高masterMap.setBounds(80, 130, 420, 420);// 将管理容器添加到游戏界面中this.getContentPane().add(masterMap);// 在完整图片下面加载背景图片// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将管理容器添加到游戏界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}}/*监听松开键盘按键的事件:当按下按键松开后,会调用该方法*/@Overridepublic void keyReleased(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断玩家是否已经胜利!if (victory()) {// 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!return;}/*上下左右按键的编号:左:37、上:38、右:39、下:40*/// 判断松开的按键是否为上if (keyCode == 38) {// 判断空白方块的X轴是否为3if (x == 3) {// 是,说明空白方块下方已没有图片可以向上移动了,则提示!System.out.println("下方已没有图片可以向上移动了");return; // 结束方法!}// 否,则图片向上移动System.out.println("图片向上移动");/*图片向上移动逻辑:把空白方块下方的数字向上移动x, y: 表示空白方块x + 1, y: 表示空白方块下方的数字*/// 将空白方块下方图片的数据 赋值给 空白方块处data[x][y] = data[x + 1][y];// 空白方块下方图片的数据赋值为0data[x + 1][y] = 0;// 将空白方块往下移动x++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 40) { // 否,则判断松开的按键是否为下// 是,则判断空白方块的X轴是否在0位置if (x == 0) {// 是,说明空白方块的上方已没有图片可以向下移动了,则提示!System.out.println("上方已没有图片可以向下移动了");return; // 结束方法!}// 否,则图片向下移动System.out.println("图片向下移动");/*图片向下移动逻辑:把空白方块上方的数字向下移动x, y: 表示空白方块x - 1, y: 表示空白方块上方的数字*/// 将空白方块上方图片的数据 赋值给 空白方块处data[x][y] = data[x - 1][y];// 空白下方图片的数据赋值为0data[x - 1][y] = 0;// 将空白方块往上移动x--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 37) { // 否,则判断松开的按键是否为左// 是,则判断空白方块的Y轴是否为3if (y == 3) {// 是,说明空白方块的右方已没有图片可以向左移动了,则提示!System.out.println("右方已没有图片可以向左移动了");return; // 结束方法!}// 否,则图片向左移动System.out.println("图片向左移动");/*图片向左移动逻辑:把空白方块右方的数字向左移动x, y: 表示空白方块x, y + 1: 表示空白方块右方的数字*/// 将空表方块右方图片的数据 赋值给 空白方块处data[x][y] = data[x][y + 1];// 空白右方图片的数据赋值为0data[x][y + 1] = 0;// 将空白方块往右移动y++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 39) { // 否,则判断松开的按键是否为右// 是,则判断空白方块的Y轴是否在0位置if (y == 0) {// 是,说明空白方块的左方已没有图片可以向右移动了,则提示!System.out.println("左方已没有图片可以向右移动了");return; // 结束方法!}// 否,则图片向右移动System.out.println("图片向右移动");/*图片向右移动逻辑:把空白方块左方的数字向右移动x, y: 表示空白方块x, y - 1: 表示空白方块左方的数字*/// 将空表方块左方图片的数据 赋值给 空白方块处data[x][y] = data[x][y - 1];// 空白左方图片的数据赋值为0data[x][y - 1] = 0;// 将空白方块往左移动y--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 65) { // 否,则判断松开的按键是否为A// 显示随机打乱的图片initImages(path);} else if (keyCode == 87) { // 否,则判断松开的按键是否为W// 是,则显示拼图完成后的效果!// 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组data = new int[][]{{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 按照二维数组的最新数据重新加载所有小图片initImages(path);}}
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面}
}

3、测试结果
  • 先玩一下

    在这里插入图片描述



  • 不想玩了!直接作弊通关吧!

    在这里插入图片描述



3、判断胜利

(1)需求

  • 当玩家拼图完成了!则显示一个胜利图标!

  • 例如:

    在这里插入图片描述


(2)分析

  • 1、需要在初始化每张小图片之前判断旧的二维数组中的数据 是否都等于 正确顺序数据的新二维数组中的数据:
    • 是,则说明胜利了!显示胜利图标!
    • 否,则说明还没有胜利!则不显示胜利图标!
  • 2、细节:如果显示胜利图标后,将不能继续使用上下左右键进行拼图了!

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame implements KeyListener{// 表示游戏相关的逻辑代码都写在这!/*创建二维数组:目的:管理数据加载图片的时候,会根据二位数组中的数据进行加载*/int[][] data = new int[4][4];// 定义x、y变量,用于记录空白方块所在的XY的坐标位置int x = 0;int y = 0;// 创建一个成员的随机数对象,用于生成一个随机数Random rd = new Random();// 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现String path = "puzzle_game\puzzleimages\girl\girl1\";// 定义一个正确顺序数据的二维数组int[][] win = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 定义一个计数器变量,用于记录玩家拼图移动的步数int step = 0;// c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏// 更换图片栏目放到后面再写,因为比较复杂JMenuItem replayGameItem = new JMenuItem("重新游戏");JMenuItem replayLoginItem = new JMenuItem("重新登录");JMenuItem closeGameItem = new JMenuItem("关闭游戏");// d.创建关于我们菜单下的一个栏目:公众号JMenuItem accountItem = new JMenuItem("公众号");// 创建更换图片菜单下的两个栏目:美女、动物JMenuItem belleItem = new JMenuItem("美女");JMenuItem animalItem = new JMenuItem("动物");/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 初始化游戏主界面initJFrame();// 初始化游戏主界面里的菜单栏initJMenu();// 初始化数据:打乱initData();// 初始化图片到游戏主界面中initImages(path);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化数据:打乱private void initData() {// 1、创建一个一维数组,用于存储一些数据int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};// 2、开始打乱// 遍历数组,依次获取到数组中的每个数据for (int i = 0; i < arr.length; i++) {// 循环每执行一次,就生成一个随机索引int rdIndex = rd.nextInt(arr.length);// 每遍历到一个数据,就用临时变量存储一下int temp = arr[i];// 开始交换// 将随机索引位置的数据 放到 当前遍历到的数据的位置arr[i] = arr[rdIndex];// 将当前遍历到的数据 放到 随机索引位置arr[rdIndex] = temp;}/*解析:比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15循环第一次执行:i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++循环第二次执行:i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++循环第三次执行:i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++后面的都是以此类推了!直到i<16,为false,循环结束!*/// 3、遍历arr一维数组,依次得到打乱后的每个数据for (int i = 0; i < arr.length; i++) {// 判断当前数据是否为0if (arr[i] == 0) {// 是,则记录空白方块XY的坐标位置x = i / 4;y = i % 4;}// 否,则依次将打乱后的数据添加到二维数组中data[i / 4][i % 4] = arr[i];}}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);// 给整个游戏界面添加键盘监听事件this.addKeyListener(this);}// 初始化游戏主界面里的菜单栏private void initJMenu() {// a.创建一个菜单栏对象JMenuBar jMenuBar = new JMenuBar();// b.创建两个菜单栏下的菜单:功能、关于我们JMenu functionJMenu = new JMenu("功能");JMenu aboutUSJMenu = new JMenu("关于我们");// 创建菜单栏下的菜单:更换图片JMenu changeJMenu = new JMenu("更换图片");// 将更换图片的菜单嵌套进功能菜单中functionJMenu.add(changeJMenu);// e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下functionJMenu.add(replayGameItem);functionJMenu.add(replayLoginItem);functionJMenu.add(closeGameItem);// 将美女、动物这两个栏目放到更换图片菜单下changeJMenu.add(belleItem);changeJMenu.add(animalItem);// f.将公众号这个栏目放到关于我们菜单下aboutUSJMenu.add(accountItem);// g.将功能、关于我们这两个菜单放到菜单栏下jMenuBar.add(functionJMenu);jMenuBar.add(aboutUSJMenu);// h.最后将菜单栏放到设置到游戏界面中this.setJMenuBar(jMenuBar);}/*初始化图片到游戏主界面中细节:先加载的图片在上方,后加载的图片会在下方*/private void initImages(String path) {// 先清空原本出现的所有图片this.getContentPane().removeAll();// 判断victory方法的返回结果是否为trueif (victory()) {// 是,则说明玩家胜利了!显示胜利图标!// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));// 设置胜利图片的坐标位置以及宽高winJLabel.setBounds(193, 300, 194, 75);// 将管理容器添加到游戏主界面中this.getContentPane().add(winJLabel);}// 统计步数// 创建一个管理容器对象,用于管理文字(步数: 0)JLabel stepJLabel = new JLabel("步数:" + step);// 设置管理容器的坐标位置stepJLabel.setBounds(40, 20, 400, 50);// 将容器添加到游戏主界面中this.getContentPane().add(stepJLabel);// 再加载图片/*外循环和内循环执行流程解析:外循环第一次执行:当 i = 0 时:i<4,为true,表示添加第一行的四张图片:当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第二次执行:当 i = 1 时:i<4,为true,表示添加第二行的四张图片:当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第三次执行:当 i = 2 时:i<4,为true,表示添加第三行的四张图片:当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第四次执行:当 i = 3 时:i<4,为true,表示添加第四行的四张图片:当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)当 j = 4 时:j<4,为false,内循环结束!外循环第五次执行:当 i = 4 时:i<4,为false,外循环结束!*/// 外循环:控制添加4行图片for (int i = 0; i < data.length; i++) { // Y轴:纵向// 内循环:控制每行添加4张图片for (int j = 0; j < data[i].length; j++) { // X轴:横向/*解析:比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3二维数组的索引:       0             1              2              3(1) 外循环执行第一次:i=0, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(2) 外循环执行第二次:i=1, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(3) 外循环执行第三次:i=2, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(4) 外循环执行第四次:i=3, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(5) 外循环执行第五次:i=0, i<二维数组长度(4), 为false, 外循环结束!!*/// 接收二维数组i索引的一维数组j索引的数据int number = data[i][j];// 1、根据指定的文件相对路径创建一个ImageIcon图片对象ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");// JLabel:管理容器,用于管理图片、文字// 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,// 并将图片对象imageIcon放到管理容器jLabel中。JLabel jLabel = new JLabel(imageIcon);// 3、指定图片位置:XY轴jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);// 4、每添加完一张小图片,给这张小图片添加边框/*0: 表示让图片凸起来1: 表示让图片凹下去*/jLabel.setBorder(new BevelBorder(1));// 5、将管理容器JLabel对象放到游戏主界面中// getContentPane:获取隐藏容器this.getContentPane().add(jLabel);}}// 在所有小图片都添加到界面之后,开始添加背景图片// 根据指定的相对路径创建背景图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将背景图片管理容器添加到界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}// 这个可以不用管@Overridepublic void keyTyped(KeyEvent e) {}/*监听按下键盘按键的事件细节:当按下按键不松开,会不断移动图片*/@Overridepublic void keyPressed(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断按下的按键是否为Aif (keyCode == 65) {// 需要先清空原本的所有图片this.getContentPane().removeAll();// 加载完整图片// 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));// 设置图片的坐标位置以及宽高masterMap.setBounds(80, 130, 420, 420);// 将管理容器添加到游戏界面中this.getContentPane().add(masterMap);// 在完整图片下面加载背景图片// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将管理容器添加到游戏界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}}/*监听松开键盘按键的事件:当按下按键松开后,会调用该方法*/@Overridepublic void keyReleased(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断玩家是否已经胜利!if (victory()) {// 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!return;}/*上下左右按键的编号:左:37、上:38、右:39、下:40*/// 判断松开的按键是否为上if (keyCode == 38) {// 判断空白方块的X轴是否为3if (x == 3) {// 是,说明空白方块下方已没有图片可以向上移动了,则提示!System.out.println("下方已没有图片可以向上移动了");return; // 结束方法!}// 否,则图片向上移动System.out.println("图片向上移动");/*图片向上移动逻辑:把空白方块下方的数字向上移动x, y: 表示空白方块x + 1, y: 表示空白方块下方的数字*/// 将空白方块下方图片的数据 赋值给 空白方块处data[x][y] = data[x + 1][y];// 空白方块下方图片的数据赋值为0data[x + 1][y] = 0;// 将空白方块往下移动x++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 40) { // 否,则判断松开的按键是否为下// 是,则判断空白方块的X轴是否在0位置if (x == 0) {// 是,说明空白方块的上方已没有图片可以向下移动了,则提示!System.out.println("上方已没有图片可以向下移动了");return; // 结束方法!}// 否,则图片向下移动System.out.println("图片向下移动");/*图片向下移动逻辑:把空白方块上方的数字向下移动x, y: 表示空白方块x - 1, y: 表示空白方块上方的数字*/// 将空白方块上方图片的数据 赋值给 空白方块处data[x][y] = data[x - 1][y];// 空白下方图片的数据赋值为0data[x - 1][y] = 0;// 将空白方块往上移动x--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 37) { // 否,则判断松开的按键是否为左// 是,则判断空白方块的Y轴是否为3if (y == 3) {// 是,说明空白方块的右方已没有图片可以向左移动了,则提示!System.out.println("右方已没有图片可以向左移动了");return; // 结束方法!}// 否,则图片向左移动System.out.println("图片向左移动");/*图片向左移动逻辑:把空白方块右方的数字向左移动x, y: 表示空白方块x, y + 1: 表示空白方块右方的数字*/// 将空表方块右方图片的数据 赋值给 空白方块处data[x][y] = data[x][y + 1];// 空白右方图片的数据赋值为0data[x][y + 1] = 0;// 将空白方块往右移动y++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 39) { // 否,则判断松开的按键是否为右// 是,则判断空白方块的Y轴是否在0位置if (y == 0) {// 是,说明空白方块的左方已没有图片可以向右移动了,则提示!System.out.println("左方已没有图片可以向右移动了");return; // 结束方法!}// 否,则图片向右移动System.out.println("图片向右移动");/*图片向右移动逻辑:把空白方块左方的数字向右移动x, y: 表示空白方块x, y - 1: 表示空白方块左方的数字*/// 将空表方块左方图片的数据 赋值给 空白方块处data[x][y] = data[x][y - 1];// 空白左方图片的数据赋值为0data[x][y - 1] = 0;// 将空白方块往左移动y--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 65) { // 否,则判断松开的按键是否为A// 显示随机打乱的图片initImages(path);} else if (keyCode == 87) { // 否,则判断松开的按键是否为W// 是,则显示拼图完成后的效果!// 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组data = new int[][]{{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 按照二维数组的最新数据重新加载所有小图片initImages(path);}}
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面}
}

3、测试结果

在这里插入图片描述




十、统计步数、菜单业务实现

1、统计步数

(1)需求

  • 需要统计玩家一共移动了多少步,并且让数据显示在游戏主界面的左上方

    在这里插入图片描述


(2)分析

  • 1、需要定义一个计数器变量,用于记录玩家移动的步数
  • 2、需要创建一个管理容器对象,用于管理文字:步数
  • 3、需要将管理容器对象添加到游戏主界面中
  • 4、需要在上下左右监听事件里让步数自增,每移动一次,自增一次

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame implements KeyListener{// 表示游戏相关的逻辑代码都写在这!/*创建二维数组:目的:管理数据加载图片的时候,会根据二位数组中的数据进行加载*/int[][] data = new int[4][4];// 定义x、y变量,用于记录空白方块所在的XY的坐标位置int x = 0;int y = 0;// 创建一个成员的随机数对象,用于生成一个随机数Random rd = new Random();// 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现String path = "puzzle_game\puzzleimages\girl\girl1\";// 定义一个正确顺序数据的二维数组int[][] win = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 定义一个计数器变量,用于记录玩家拼图移动的步数int step = 0;// c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏// 更换图片栏目放到后面再写,因为比较复杂JMenuItem replayGameItem = new JMenuItem("重新游戏");JMenuItem replayLoginItem = new JMenuItem("重新登录");JMenuItem closeGameItem = new JMenuItem("关闭游戏");// d.创建关于我们菜单下的一个栏目:公众号JMenuItem accountItem = new JMenuItem("公众号");// 创建更换图片菜单下的两个栏目:美女、动物JMenuItem belleItem = new JMenuItem("美女");JMenuItem animalItem = new JMenuItem("动物");/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 初始化游戏主界面initJFrame();// 初始化游戏主界面里的菜单栏initJMenu();// 初始化数据:打乱initData();// 初始化图片到游戏主界面中initImages(path);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化数据:打乱private void initData() {// 1、创建一个一维数组,用于存储一些数据int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};// 2、开始打乱// 遍历数组,依次获取到数组中的每个数据for (int i = 0; i < arr.length; i++) {// 循环每执行一次,就生成一个随机索引int rdIndex = rd.nextInt(arr.length);// 每遍历到一个数据,就用临时变量存储一下int temp = arr[i];// 开始交换// 将随机索引位置的数据 放到 当前遍历到的数据的位置arr[i] = arr[rdIndex];// 将当前遍历到的数据 放到 随机索引位置arr[rdIndex] = temp;}/*解析:比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15循环第一次执行:i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++循环第二次执行:i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++循环第三次执行:i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++后面的都是以此类推了!直到i<16,为false,循环结束!*/// 3、遍历arr一维数组,依次得到打乱后的每个数据for (int i = 0; i < arr.length; i++) {// 判断当前数据是否为0if (arr[i] == 0) {// 是,则记录空白方块XY的坐标位置x = i / 4;y = i % 4;}// 否,则依次将打乱后的数据添加到二维数组中data[i / 4][i % 4] = arr[i];}}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);// 给整个游戏界面添加键盘监听事件this.addKeyListener(this);}// 初始化游戏主界面里的菜单栏private void initJMenu() {// a.创建一个菜单栏对象JMenuBar jMenuBar = new JMenuBar();// b.创建两个菜单栏下的菜单:功能、关于我们JMenu functionJMenu = new JMenu("功能");JMenu aboutUSJMenu = new JMenu("关于我们");// 创建菜单栏下的菜单:更换图片JMenu changeJMenu = new JMenu("更换图片");// 将更换图片的菜单嵌套进功能菜单中functionJMenu.add(changeJMenu);// e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下functionJMenu.add(replayGameItem);functionJMenu.add(replayLoginItem);functionJMenu.add(closeGameItem);// 将美女、动物这两个栏目放到更换图片菜单下changeJMenu.add(belleItem);changeJMenu.add(animalItem);// f.将公众号这个栏目放到关于我们菜单下aboutUSJMenu.add(accountItem);// g.将功能、关于我们这两个菜单放到菜单栏下jMenuBar.add(functionJMenu);jMenuBar.add(aboutUSJMenu);// h.最后将菜单栏放到设置到游戏界面中this.setJMenuBar(jMenuBar);}/*初始化图片到游戏主界面中细节:先加载的图片在上方,后加载的图片会在下方*/private void initImages(String path) {// 先清空原本出现的所有图片this.getContentPane().removeAll();// 判断victory方法的返回结果是否为trueif (victory()) {// 是,则说明玩家胜利了!显示胜利图标!// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));// 设置胜利图片的坐标位置以及宽高winJLabel.setBounds(193, 300, 194, 75);// 将管理容器添加到游戏主界面中this.getContentPane().add(winJLabel);}// 统计步数// 创建一个管理容器对象,用于管理文字(步数: 0)JLabel stepJLabel = new JLabel("步数:" + step);// 设置管理容器的坐标位置stepJLabel.setBounds(40, 20, 400, 50);// 将容器添加到游戏主界面中this.getContentPane().add(stepJLabel);// 再加载图片/*外循环和内循环执行流程解析:外循环第一次执行:当 i = 0 时:i<4,为true,表示添加第一行的四张图片:当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第二次执行:当 i = 1 时:i<4,为true,表示添加第二行的四张图片:当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第三次执行:当 i = 2 时:i<4,为true,表示添加第三行的四张图片:当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第四次执行:当 i = 3 时:i<4,为true,表示添加第四行的四张图片:当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)当 j = 4 时:j<4,为false,内循环结束!外循环第五次执行:当 i = 4 时:i<4,为false,外循环结束!*/// 外循环:控制添加4行图片for (int i = 0; i < data.length; i++) { // Y轴:纵向// 内循环:控制每行添加4张图片for (int j = 0; j < data[i].length; j++) { // X轴:横向/*解析:比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3二维数组的索引:       0             1              2              3(1) 外循环执行第一次:i=0, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(2) 外循环执行第二次:i=1, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(3) 外循环执行第三次:i=2, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(4) 外循环执行第四次:i=3, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(5) 外循环执行第五次:i=0, i<二维数组长度(4), 为false, 外循环结束!!*/// 接收二维数组i索引的一维数组j索引的数据int number = data[i][j];// 1、根据指定的文件相对路径创建一个ImageIcon图片对象ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");// JLabel:管理容器,用于管理图片、文字// 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,// 并将图片对象imageIcon放到管理容器jLabel中。JLabel jLabel = new JLabel(imageIcon);// 3、指定图片位置:XY轴jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);// 4、每添加完一张小图片,给这张小图片添加边框/*0: 表示让图片凸起来1: 表示让图片凹下去*/jLabel.setBorder(new BevelBorder(1));// 5、将管理容器JLabel对象放到游戏主界面中// getContentPane:获取隐藏容器this.getContentPane().add(jLabel);}}// 在所有小图片都添加到界面之后,开始添加背景图片// 根据指定的相对路径创建背景图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将背景图片管理容器添加到界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}// 这个可以不用管@Overridepublic void keyTyped(KeyEvent e) {}/*监听按下键盘按键的事件细节:当按下按键不松开,会不断移动图片*/@Overridepublic void keyPressed(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断按下的按键是否为Aif (keyCode == 65) {// 需要先清空原本的所有图片this.getContentPane().removeAll();// 加载完整图片// 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));// 设置图片的坐标位置以及宽高masterMap.setBounds(80, 130, 420, 420);// 将管理容器添加到游戏界面中this.getContentPane().add(masterMap);// 在完整图片下面加载背景图片// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将管理容器添加到游戏界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}}/*监听松开键盘按键的事件:当按下按键松开后,会调用该方法*/@Overridepublic void keyReleased(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断玩家是否已经胜利!if (victory()) {// 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!return;}/*上下左右按键的编号:左:37、上:38、右:39、下:40*/// 判断松开的按键是否为上if (keyCode == 38) {// 判断空白方块的X轴是否为3if (x == 3) {// 是,说明空白方块下方已没有图片可以向上移动了,则提示!System.out.println("下方已没有图片可以向上移动了");return; // 结束方法!}// 否,则图片向上移动System.out.println("图片向上移动");/*图片向上移动逻辑:把空白方块下方的数字向上移动x, y: 表示空白方块x + 1, y: 表示空白方块下方的数字*/// 将空白方块下方图片的数据 赋值给 空白方块处data[x][y] = data[x + 1][y];// 空白方块下方图片的数据赋值为0data[x + 1][y] = 0;// 将空白方块往下移动x++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 40) { // 否,则判断松开的按键是否为下// 是,则判断空白方块的X轴是否在0位置if (x == 0) {// 是,说明空白方块的上方已没有图片可以向下移动了,则提示!System.out.println("上方已没有图片可以向下移动了");return; // 结束方法!}// 否,则图片向下移动System.out.println("图片向下移动");/*图片向下移动逻辑:把空白方块上方的数字向下移动x, y: 表示空白方块x - 1, y: 表示空白方块上方的数字*/// 将空白方块上方图片的数据 赋值给 空白方块处data[x][y] = data[x - 1][y];// 空白下方图片的数据赋值为0data[x - 1][y] = 0;// 将空白方块往上移动x--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 37) { // 否,则判断松开的按键是否为左// 是,则判断空白方块的Y轴是否为3if (y == 3) {// 是,说明空白方块的右方已没有图片可以向左移动了,则提示!System.out.println("右方已没有图片可以向左移动了");return; // 结束方法!}// 否,则图片向左移动System.out.println("图片向左移动");/*图片向左移动逻辑:把空白方块右方的数字向左移动x, y: 表示空白方块x, y + 1: 表示空白方块右方的数字*/// 将空表方块右方图片的数据 赋值给 空白方块处data[x][y] = data[x][y + 1];// 空白右方图片的数据赋值为0data[x][y + 1] = 0;// 将空白方块往右移动y++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 39) { // 否,则判断松开的按键是否为右// 是,则判断空白方块的Y轴是否在0位置if (y == 0) {// 是,说明空白方块的左方已没有图片可以向右移动了,则提示!System.out.println("左方已没有图片可以向右移动了");return; // 结束方法!}// 否,则图片向右移动System.out.println("图片向右移动");/*图片向右移动逻辑:把空白方块左方的数字向右移动x, y: 表示空白方块x, y - 1: 表示空白方块左方的数字*/// 将空表方块左方图片的数据 赋值给 空白方块处data[x][y] = data[x][y - 1];// 空白左方图片的数据赋值为0data[x][y - 1] = 0;// 将空白方块往左移动y--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 65) { // 否,则判断松开的按键是否为A// 显示随机打乱的图片initImages(path);} else if (keyCode == 87) { // 否,则判断松开的按键是否为W// 是,则显示拼图完成后的效果!// 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组data = new int[][]{{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 按照二维数组的最新数据重新加载所有小图片initImages(path);}}
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面}
}

3、测试结果

在这里插入图片描述




2、菜单业务实现

(1)需求

  • 当玩家用鼠标单击重新游戏的时候,重新开始游戏。
  • 当玩家用鼠标单击重新登录的时候,关闭游戏主界面,去到登录界面。
  • 当玩家用鼠标单击关闭游戏的时候,游戏主界面就关闭。
  • 当玩家用鼠标单击关于我们的时候,弹出一个彩蛋。

(2)分析

  • 1、先给重新游戏、重新登录、关闭游戏、关于我们的条目绑定动作监听事件
  • 2、根据当前单击的条目进行相应的逻辑操作:
    • 重新游戏
      • 第一步肯定是先将步数清零。
      • 第二步将二维数组中的数据重新打乱。
      • 第三步按照打乱后的数据重新加载所有小图片。
    • 重新登录
      • 第一步是先关闭当前游戏主界面。
      • 第二步是构造登录界面。
    • 关闭游戏
      • 直接结束JVM虚拟机运行。
    • 关于我们
      • 第一步创建对话窗对象
      • 第二步根据相对路径创建图片对象,并添加到管理容器中,并设置图片的坐标和宽高
      • 第三步将管理容器添加到对话窗中
      • 第四步设置对话窗的大小
      • 第五步将对话窗置顶
      • 第六步让对话窗居中
      • 第七步设置对话窗不关闭则无法操作下面的界面
      • 第八步设置对话窗显示出来,因为默认是隐藏的。

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;import javax.swing.*;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame implements KeyListener, ActionListener{// 表示游戏相关的逻辑代码都写在这!/*创建二维数组:目的:管理数据加载图片的时候,会根据二位数组中的数据进行加载*/int[][] data = new int[4][4];// 定义x、y变量,用于记录空白方块所在的XY的坐标位置int x = 0;int y = 0;// 创建一个成员的随机数对象,用于生成一个随机数Random rd = new Random();// 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现String path = "puzzle_game\puzzleimages\girl\girl1\";// 定义一个正确顺序数据的二维数组int[][] win = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 定义一个计数器变量,用于记录玩家拼图移动的步数int step = 0;// c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏// 更换图片栏目放到后面再写,因为比较复杂JMenuItem replayGameItem = new JMenuItem("重新游戏");JMenuItem replayLoginItem = new JMenuItem("重新登录");JMenuItem closeGameItem = new JMenuItem("关闭游戏");// d.创建关于我们菜单下的一个栏目:公众号JMenuItem accountItem = new JMenuItem("公众号");// 创建更换图片菜单下的两个栏目:美女、动物JMenuItem belleItem = new JMenuItem("美女");JMenuItem animalItem = new JMenuItem("动物");/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 初始化游戏主界面initJFrame();// 初始化游戏主界面里的菜单栏initJMenu();// 初始化数据:打乱initData();// 初始化图片到游戏主界面中initImages(path);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化数据:打乱private void initData() {// 1、创建一个一维数组,用于存储一些数据int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};// 2、开始打乱// 遍历数组,依次获取到数组中的每个数据for (int i = 0; i < arr.length; i++) {// 循环每执行一次,就生成一个随机索引int rdIndex = rd.nextInt(arr.length);// 每遍历到一个数据,就用临时变量存储一下int temp = arr[i];// 开始交换// 将随机索引位置的数据 放到 当前遍历到的数据的位置arr[i] = arr[rdIndex];// 将当前遍历到的数据 放到 随机索引位置arr[rdIndex] = temp;}/*解析:比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15循环第一次执行:i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++循环第二次执行:i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++循环第三次执行:i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++后面的都是以此类推了!直到i<16,为false,循环结束!*/// 3、遍历arr一维数组,依次得到打乱后的每个数据for (int i = 0; i < arr.length; i++) {// 判断当前数据是否为0if (arr[i] == 0) {// 是,则记录空白方块XY的坐标位置x = i / 4;y = i % 4;}// 否,则依次将打乱后的数据添加到二维数组中data[i / 4][i % 4] = arr[i];}}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);// 给整个游戏界面添加键盘监听事件this.addKeyListener(this);}// 初始化游戏主界面里的菜单栏private void initJMenu() {// a.创建一个菜单栏对象JMenuBar jMenuBar = new JMenuBar();// b.创建两个菜单栏下的菜单:功能、关于我们JMenu functionJMenu = new JMenu("功能");JMenu aboutUSJMenu = new JMenu("关于我们");// 创建菜单栏下的菜单:更换图片JMenu changeJMenu = new JMenu("更换图片");// 将更换图片的菜单嵌套进功能菜单中functionJMenu.add(changeJMenu);// 给各个栏目绑定动作监听事件replayGameItem.addActionListener(this);     // 重新游戏replayLoginItem.addActionListener(this);    // 重新登录closeGameItem.addActionListener(this);      // 关闭游戏accountItem.addActionListener(this);        // 公众号belleItem.addActionListener(this);          // 美女animalItem.addActionListener(this);         // 动物// e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下functionJMenu.add(replayGameItem);functionJMenu.add(replayLoginItem);functionJMenu.add(closeGameItem);// 将美女、动物这两个栏目放到更换图片菜单下changeJMenu.add(belleItem);changeJMenu.add(animalItem);// f.将公众号这个栏目放到关于我们菜单下aboutUSJMenu.add(accountItem);// g.将功能、关于我们这两个菜单放到菜单栏下jMenuBar.add(functionJMenu);jMenuBar.add(aboutUSJMenu);// h.最后将菜单栏放到设置到游戏界面中this.setJMenuBar(jMenuBar);}/*初始化图片到游戏主界面中细节:先加载的图片在上方,后加载的图片会在下方*/private void initImages(String path) {// 先清空原本出现的所有图片this.getContentPane().removeAll();// 判断victory方法的返回结果是否为trueif (victory()) {// 是,则说明玩家胜利了!显示胜利图标!// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));// 设置胜利图片的坐标位置以及宽高winJLabel.setBounds(193, 300, 194, 75);// 将管理容器添加到游戏主界面中this.getContentPane().add(winJLabel);}// 统计步数// 创建一个管理容器对象,用于管理文字(步数: 0)JLabel stepJLabel = new JLabel("步数:" + step);// 设置管理容器的坐标位置stepJLabel.setBounds(40, 20, 400, 50);// 将容器添加到游戏主界面中this.getContentPane().add(stepJLabel);// 再加载图片/*外循环和内循环执行流程解析:外循环第一次执行:当 i = 0 时:i<4,为true,表示添加第一行的四张图片:当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第二次执行:当 i = 1 时:i<4,为true,表示添加第二行的四张图片:当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第三次执行:当 i = 2 时:i<4,为true,表示添加第三行的四张图片:当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第四次执行:当 i = 3 时:i<4,为true,表示添加第四行的四张图片:当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)当 j = 4 时:j<4,为false,内循环结束!外循环第五次执行:当 i = 4 时:i<4,为false,外循环结束!*/// 外循环:控制添加4行图片for (int i = 0; i < data.length; i++) { // Y轴:纵向// 内循环:控制每行添加4张图片for (int j = 0; j < data[i].length; j++) { // X轴:横向/*解析:比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3二维数组的索引:       0             1              2              3(1) 外循环执行第一次:i=0, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(2) 外循环执行第二次:i=1, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(3) 外循环执行第三次:i=2, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(4) 外循环执行第四次:i=3, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(5) 外循环执行第五次:i=0, i<二维数组长度(4), 为false, 外循环结束!!*/// 接收二维数组i索引的一维数组j索引的数据int number = data[i][j];// 1、根据指定的文件相对路径创建一个ImageIcon图片对象ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");// JLabel:管理容器,用于管理图片、文字// 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,// 并将图片对象imageIcon放到管理容器jLabel中。JLabel jLabel = new JLabel(imageIcon);// 3、指定图片位置:XY轴jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);// 4、每添加完一张小图片,给这张小图片添加边框/*0: 表示让图片凸起来1: 表示让图片凹下去*/jLabel.setBorder(new BevelBorder(1));// 5、将管理容器JLabel对象放到游戏主界面中// getContentPane:获取隐藏容器this.getContentPane().add(jLabel);}}// 在所有小图片都添加到界面之后,开始添加背景图片// 根据指定的相对路径创建背景图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将背景图片管理容器添加到界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}// 这个可以不用管@Overridepublic void keyTyped(KeyEvent e) {}/*监听按下键盘按键的事件细节:当按下按键不松开,会不断移动图片*/@Overridepublic void keyPressed(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断按下的按键是否为Aif (keyCode == 65) {// 需要先清空原本的所有图片this.getContentPane().removeAll();// 加载完整图片// 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));// 设置图片的坐标位置以及宽高masterMap.setBounds(80, 130, 420, 420);// 将管理容器添加到游戏界面中this.getContentPane().add(masterMap);// 在完整图片下面加载背景图片// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将管理容器添加到游戏界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}}/*监听松开键盘按键的事件:当按下按键松开后,会调用该方法*/@Overridepublic void keyReleased(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断玩家是否已经胜利!if (victory()) {// 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!return;}/*上下左右按键的编号:左:37、上:38、右:39、下:40*/// 判断松开的按键是否为上if (keyCode == 38) {// 判断空白方块的X轴是否为3if (x == 3) {// 是,说明空白方块下方已没有图片可以向上移动了,则提示!System.out.println("下方已没有图片可以向上移动了");return; // 结束方法!}// 否,则图片向上移动System.out.println("图片向上移动");/*图片向上移动逻辑:把空白方块下方的数字向上移动x, y: 表示空白方块x + 1, y: 表示空白方块下方的数字*/// 将空白方块下方图片的数据 赋值给 空白方块处data[x][y] = data[x + 1][y];// 空白方块下方图片的数据赋值为0data[x + 1][y] = 0;// 将空白方块往下移动x++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 40) { // 否,则判断松开的按键是否为下// 是,则判断空白方块的X轴是否在0位置if (x == 0) {// 是,说明空白方块的上方已没有图片可以向下移动了,则提示!System.out.println("上方已没有图片可以向下移动了");return; // 结束方法!}// 否,则图片向下移动System.out.println("图片向下移动");/*图片向下移动逻辑:把空白方块上方的数字向下移动x, y: 表示空白方块x - 1, y: 表示空白方块上方的数字*/// 将空白方块上方图片的数据 赋值给 空白方块处data[x][y] = data[x - 1][y];// 空白下方图片的数据赋值为0data[x - 1][y] = 0;// 将空白方块往上移动x--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 37) { // 否,则判断松开的按键是否为左// 是,则判断空白方块的Y轴是否为3if (y == 3) {// 是,说明空白方块的右方已没有图片可以向左移动了,则提示!System.out.println("右方已没有图片可以向左移动了");return; // 结束方法!}// 否,则图片向左移动System.out.println("图片向左移动");/*图片向左移动逻辑:把空白方块右方的数字向左移动x, y: 表示空白方块x, y + 1: 表示空白方块右方的数字*/// 将空表方块右方图片的数据 赋值给 空白方块处data[x][y] = data[x][y + 1];// 空白右方图片的数据赋值为0data[x][y + 1] = 0;// 将空白方块往右移动y++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 39) { // 否,则判断松开的按键是否为右// 是,则判断空白方块的Y轴是否在0位置if (y == 0) {// 是,说明空白方块的左方已没有图片可以向右移动了,则提示!System.out.println("左方已没有图片可以向右移动了");return; // 结束方法!}// 否,则图片向右移动System.out.println("图片向右移动");/*图片向右移动逻辑:把空白方块左方的数字向右移动x, y: 表示空白方块x, y - 1: 表示空白方块左方的数字*/// 将空表方块左方图片的数据 赋值给 空白方块处data[x][y] = data[x][y - 1];// 空白左方图片的数据赋值为0data[x][y - 1] = 0;// 将空白方块往左移动y--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 65) { // 否,则判断松开的按键是否为A// 显示随机打乱的图片initImages(path);} else if (keyCode == 87) { // 否,则判断松开的按键是否为W// 是,则显示拼图完成后的效果!// 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组data = new int[][]{{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 按照二维数组的最新数据重新加载所有小图片initImages(path);}}// 动作监听事件:监听鼠标左键单击、键盘空格操作@Overridepublic void actionPerformed(ActionEvent e) {// 获取当前被点击的栏目对象Object obj = e.getSource();// 判断当前被点击的是否为重新游戏栏目if (obj == replayGameItem) {System.out.println("重新游戏");// 1、先将步数清零step = 0;// 2、重新打乱二维数组的数据initData();// 3、重新按照打乱后的数据加载所有小图片initImages(path);} else if (obj == replayLoginItem) {// 否,则判断当前被点击的是否为重新登录栏目System.out.println("重新登录");// 1、先关闭当前游戏主界面this.setVisible(false); // 隐藏起来// 2、构造登录界面new LoginJFrame();} else if (obj == closeGameItem) {// 否,则判断当前被点击的是否为关闭游戏栏目System.out.println("关闭游戏");// 直接结束JVM虚拟机运行System.exit(0);} else if (obj == accountItem) {// 否,则判断当前被点击的是否为公众号栏目System.out.println("公众号");// 1、创建对话窗对象JDialog jDialog = new JDialog();// 2、根据相对路径创建图片对象,并添加到管理容器中,并设置图片的坐标和宽高JLabel aboutJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\about.jpg"));aboutJLabel.setBounds(0, 0, 220, 220);// 3、将管理容器添加到对话窗中jDialog.getContentPane().add(aboutJLabel);// 4、设置对话窗的大小jDialog.setSize(300, 300);// 5、将对话窗置顶jDialog.setAlwaysOnTop(true);// 6、让对话窗居中jDialog.setLocationRelativeTo(null);// 7、设置对话窗不关闭则无法操作下面的界面jDialog.setModal(true);// 8、让对话窗显示出来,因为默认是隐藏的jDialog.setVisible(true);} else if (obj == belleItem) {  // 否,则判断当前被点击的是否为美女栏目// 是,则随机更换美女图片中的一张System.out.println("更换美女图片");// 随机一个1~2之间的数int rdNumber = rd.nextInt(2) + 1;// 将path的值改为美女图片所在的相对路径path = "puzzle_game\puzzleimages\girl\girl" + rdNumber + "\";// 重新打乱二维数组中的数据initData();// 根据美女图片的相对路径,随机初始化一张美女图片initImages(path);} else if (obj == animalItem) { // 否,则判断当前被点击的是否为动物栏目// 是,则随机更换动物图片中的一张System.out.println("更换动物图片");// 随机一个1~2之间的数int rdNumber = rd.nextInt(2) + 1;// 将path的值改为美女图片所在的相对路径path = "puzzle_game\puzzleimages\animal\animal" + rdNumber + "\";// 重新打乱二维数组中的数据initData();// 根据动物图片的相对路径,随机初始化一张动物图片initImages(path);}}
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面}
}

3、测试结果
  • 重新游戏:

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述



  • 重新登录

    在这里插入图片描述



  • 关闭游戏

    在这里插入图片描述

    在这里插入图片描述



  • 公众号

    在这里插入图片描述




十一、更换图片

1、需求

在这里插入图片描述

  • 需要在菜单栏下的功能菜单下添加一个更换图片的菜单,更换图片的样式有:美女、动物

2、分析

(1)所用技术

  • 菜单栏是:JMenuBar
  • 菜单栏下的菜单是:JMenu
  • 菜单下的栏目是:JMenuItem
  • 动作监听:ActionListener
    • 监听鼠标左键单击、键盘空格的操作

(2)思路分析

  • 功能、关于我们、更换图片,属于菜单栏下的JMenu菜单对象。
  • 更换图片属于嵌套在功能菜单里的菜单对象。
  • 重新游戏、重新登录、关闭游戏、美女、动物,属于菜单下的JMenuItem栏目对象。
  • 1、创建更换图片的JMenu菜单对象 嵌套到 功能菜单中
  • 2、创建美女、动物的JMenuItem栏目对象,并添加到更换图片的菜单下
  • 3、给美女、动物的栏目对象绑定动作监听事件:
    • 只要用鼠标左键单击、按下键盘空格按键,就会触发更换图片的事件。
  • 4、判断当前点击的是否为美女:
    • 是:
      • 则将path的路径改为美女图片的相对路径,然后随机一张美女图片
      • 然后重新打乱二维数组中的数据,最后重新加载所有小图片即可。
    • 否:
      • 则判断当前点击的是否为动物:
        • 是:则将path的路径改为动物图片的相对路径,然后随机一张动物图片
        • 然后重新打乱二维数组中的数据,最后重新加载所有小图片即可。
  • 细节1:选择更换完毕之后,游戏界面中需要加载所有的小图片并且打乱顺序。
  • 细节2:按A、点击重新游戏的时候,显示的是更换之后的图片。

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.*;
import java.util.Random;/*** 游戏主界面类:* 继承父类:JFrame*/
public class GameJFrame extends JFrame implements KeyListener, ActionListener {// 表示游戏相关的逻辑代码都写在这!/*创建二维数组:目的:管理数据加载图片的时候,会根据二位数组中的数据进行加载*/int[][] data = new int[4][4];// 定义x、y变量,用于记录空白方块所在的XY的坐标位置int x = 0;int y = 0;// 创建一个成员的随机数对象,用于生成一个随机数Random rd = new Random();// 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现String path = "puzzle_game\puzzleimages\girl\girl1\";// 定义一个正确顺序数据的二维数组int[][] win = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 定义一个计数器变量,用于记录玩家拼图移动的步数int step = 0;// c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏// 更换图片栏目放到后面再写,因为比较复杂JMenuItem replayGameItem = new JMenuItem("重新游戏");JMenuItem replayLoginItem = new JMenuItem("重新登录");JMenuItem closeGameItem = new JMenuItem("关闭游戏");// d.创建关于我们菜单下的一个栏目:公众号JMenuItem accountItem = new JMenuItem("公众号");// 创建更换图片菜单下的两个栏目:美女、动物JMenuItem belleItem = new JMenuItem("美女");JMenuItem animalItem = new JMenuItem("动物");/*提供无参数的构造器需求:初始化一个宽603像素,高680像素的游戏主界面*/public GameJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 1、初始化游戏主界面initJFrame();// 2、初始化游戏主界面里的菜单栏initJMenu();// 3、初始化数据:打乱initData();// 4、利用打乱后的数据初始化图片到游戏主界面中initImages(path);// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面this.setVisible(true);}// 初始化数据:打乱private void initData() {// 1、创建一个一维数组,用于存储一些数据int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};// 2、开始打乱// 遍历数组,依次获取到数组中的每个数据for (int i = 0; i < arr.length; i++) {// 循环每执行一次,就生成一个随机索引int rdIndex = rd.nextInt(arr.length);// 每遍历到一个数据,就用临时变量存储一下int temp = arr[i];// 开始交换// 将随机索引位置的数据 放到 当前遍历到的数据的位置arr[i] = arr[rdIndex];// 将当前遍历到的数据 放到 随机索引位置arr[rdIndex] = temp;}/*解析:比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15循环第一次执行:i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++循环第二次执行:i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++循环第三次执行:i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++后面的都是以此类推了!直到i<16,为false,循环结束!*/// 3、遍历arr一维数组,依次得到打乱后的每个数据for (int i = 0; i < arr.length; i++) {// 判断当前数据是否为0/*if (arr[i] == 0) {// 是,则记录空白方块XY的坐标位置x = i / 4;y = i % 4;} else {// 否,则依次将打乱后的数据添加到二维数组中data[i / 4][i % 4] = arr[i];}*/// 判断当前数据是否为0if (arr[i] == 0) {// 是,则记录空白方块XY的坐标位置x = i / 4;y = i % 4;}// 否,则依次将打乱后的数据添加到二维数组中data[i / 4][i % 4] = arr[i];}}/*初始化图片到游戏主界面中细节:先加载的图片在上方,后加载的图片会在下方*/private void initImages(String path) {// 先清空原本出现的所有图片this.getContentPane().removeAll();// 判断victory方法的返回结果是否为trueif (victory()) {// 是,则说明玩家胜利了!显示胜利图标!// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));// 设置胜利图片的坐标位置以及宽高winJLabel.setBounds(193, 300, 194, 75);// 将管理容器添加到游戏主界面中this.getContentPane().add(winJLabel);}// 统计步数// 创建一个管理容器对象,用于管理文字(步数: 0)JLabel stepJLabel = new JLabel("步数:" + step);// 设置管理容器的坐标位置stepJLabel.setBounds(40, 20, 400, 50);// 将容器添加到游戏主界面中this.getContentPane().add(stepJLabel);// 再加载图片/*外循环和内循环执行流程解析:外循环第一次执行:当 i = 0 时:i<4,为true,表示添加第一行的四张图片:当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第二次执行:当 i = 1 时:i<4,为true,表示添加第二行的四张图片:当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第三次执行:当 i = 2 时:i<4,为true,表示添加第三行的四张图片:当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片当 j = 4 时:j<4,为false,内循环结束!外循环第四次执行:当 i = 3 时:i<4,为true,表示添加第四行的四张图片:当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)当 j = 4 时:j<4,为false,内循环结束!外循环第五次执行:当 i = 4 时:i<4,为false,外循环结束!*/// 外循环:控制添加4行图片for (int i = 0; i < data.length; i++) { // Y轴:纵向// 内循环:控制每行添加4张图片for (int j = 0; j < data[i].length; j++) { // X轴:横向/*解析:比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3二维数组的索引:       0             1              2              3(1) 外循环执行第一次:i=0, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(2) 外循环执行第二次:i=1, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(3) 外循环执行第三次:i=2, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(4) 外循环执行第四次:i=3, i<二维数组长度(4), 为true, 进入内循环:a.内循环执行第一次j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++b.内循环执行第二次j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++c.内循环执行第三次j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++d.内循环执行第四次j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++e.内循环执行第五次j=4, j<一维数组长度(4), 为false,内循环结束(5) 外循环执行第五次:i=0, i<二维数组长度(4), 为false, 外循环结束!!*/// 接收二维数组i索引的一维数组j索引的数据int number = data[i][j];// 1、根据指定的文件相对路径创建一个ImageIcon图片对象ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");// JLabel:管理容器,用于管理图片、文字// 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,// 并将图片对象imageIcon放到管理容器jLabel中。JLabel jLabel = new JLabel(imageIcon);// 3、指定图片位置:XY轴jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);// 4、每添加完一张小图片,给这张小图片添加边框/*0: 表示让图片凸起来1: 表示让图片凹下去*/jLabel.setBorder(new BevelBorder(1));// 5、将管理容器JLabel对象放到游戏主界面中// getContentPane:获取隐藏容器this.getContentPane().add(jLabel);}}// 在所有小图片都添加到界面之后,开始添加背景图片// 根据指定的相对路径创建背景图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将背景图片管理容器添加到界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}/*** 判断玩家是否胜利!** @return 都等于返回true,否则返回false*/private 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]) {// 只要有一个数据不相等,则直接返回false,后面的就没必要判断了!return false;}}}// 循环都结束了!说明两个二维数组的数据都相等!则返回truereturn true;}// 初始化游戏主界面里的菜单栏private void initJMenu() {// a.创建一个菜单栏对象JMenuBar jMenuBar = new JMenuBar();// b.创建两个菜单栏下的菜单:功能、关于我们JMenu functionJMenu = new JMenu("功能");JMenu aboutUSJMenu = new JMenu("关于我们");// 创建菜单栏下的菜单:更换图片JMenu changeJMenu = new JMenu("更换图片");// 将更换图片的菜单嵌套进功能菜单中functionJMenu.add(changeJMenu);// 给各个栏目绑定动作监听事件replayGameItem.addActionListener(this);     // 重新游戏replayLoginItem.addActionListener(this);    // 重新登录closeGameItem.addActionListener(this);      // 关闭游戏accountItem.addActionListener(this);        // 公众号belleItem.addActionListener(this);          // 美女animalItem.addActionListener(this);         // 动物// e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下functionJMenu.add(replayGameItem);functionJMenu.add(replayLoginItem);functionJMenu.add(closeGameItem);// 将美女、动物这两个栏目放到更换图片菜单下changeJMenu.add(belleItem);changeJMenu.add(animalItem);// f.将公众号这个栏目放到关于我们菜单下aboutUSJMenu.add(accountItem);// g.将功能、关于我们这两个菜单放到菜单栏下jMenuBar.add(functionJMenu);jMenuBar.add(aboutUSJMenu);// h.最后将菜单栏放到设置到游戏界面中this.setJMenuBar(jMenuBar);}// 初始化游戏主界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面this.setSize(603, 680);// 设置界面标题this.setTitle("奥利gei拼图单机版 v1.0");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件// 因为只有这样,添加图片的时候才不会一直处于居中位置this.setLayout(null);// 给整个游戏界面添加键盘监听事件this.addKeyListener(this);}// 这个可以不用管@Overridepublic void keyTyped(KeyEvent e) {}/*监听按下键盘按键的事件细节:当按下按键不松开,会不断移动图片*/@Overridepublic void keyPressed(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断按下的按键是否为Aif (keyCode == 65) {// 需要先清空原本的所有图片this.getContentPane().removeAll();// 加载完整图片// 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));// 设置图片的坐标位置以及宽高masterMap.setBounds(80, 130, 420, 420);// 将管理容器添加到游戏界面中this.getContentPane().add(masterMap);// 在完整图片下面加载背景图片// 根据指定图片的相对路径创建图片对象,并添加到管理容器中JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));// 设置背景图片的坐标位置以及宽高background.setBounds(36, 36, 508, 560);// 将管理容器添加到游戏界面中this.getContentPane().add(background);// 最后刷新一下游戏界面this.getContentPane().repaint();}}/*监听松开键盘按键的事件:当按下按键松开后,会调用该方法*/@Overridepublic void keyReleased(KeyEvent e) {// 获取键盘每个按键的编号int keyCode = e.getKeyCode();// 判断玩家是否已经胜利!if (victory()) {// 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!return;}/*上下左右按键的编号:左:37、上:38、右:39、下:40*/// 判断松开的按键是否为上if (keyCode == 38) {// 判断空白方块的X轴是否为3if (x == 3) {// 是,说明空白方块下方已没有图片可以向上移动了,则提示!System.out.println("下方已没有图片可以向上移动了");return; // 结束方法!}// 否,则图片向上移动System.out.println("图片向上移动");/*图片向上移动逻辑:把空白方块下方的数字向上移动x, y: 表示空白方块x + 1, y: 表示空白方块下方的数字*/// 将空白方块下方图片的数据 赋值给 空白方块处data[x][y] = data[x + 1][y];// 空白方块下方图片的数据赋值为0data[x + 1][y] = 0;// 将空白方块往下移动x++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 40) { // 否,则判断松开的按键是否为下// 是,则判断空白方块的X轴是否在0位置if (x == 0) {// 是,说明空白方块的上方已没有图片可以向下移动了,则提示!System.out.println("上方已没有图片可以向下移动了");return; // 结束方法!}// 否,则图片向下移动System.out.println("图片向下移动");/*图片向下移动逻辑:把空白方块上方的数字向下移动x, y: 表示空白方块x - 1, y: 表示空白方块上方的数字*/// 将空白方块上方图片的数据 赋值给 空白方块处data[x][y] = data[x - 1][y];// 空白下方图片的数据赋值为0data[x - 1][y] = 0;// 将空白方块往上移动x--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 37) { // 否,则判断松开的按键是否为左// 是,则判断空白方块的Y轴是否为3if (y == 3) {// 是,说明空白方块的右方已没有图片可以向左移动了,则提示!System.out.println("右方已没有图片可以向左移动了");return; // 结束方法!}// 否,则图片向左移动System.out.println("图片向左移动");/*图片向左移动逻辑:把空白方块右方的数字向左移动x, y: 表示空白方块x, y + 1: 表示空白方块右方的数字*/// 将空表方块右方图片的数据 赋值给 空白方块处data[x][y] = data[x][y + 1];// 空白右方图片的数据赋值为0data[x][y + 1] = 0;// 将空白方块往右移动y++;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 39) { // 否,则判断松开的按键是否为右// 是,则判断空白方块的Y轴是否在0位置if (y == 0) {// 是,说明空白方块的左方已没有图片可以向右移动了,则提示!System.out.println("左方已没有图片可以向右移动了");return; // 结束方法!}// 否,则图片向右移动System.out.println("图片向右移动");/*图片向右移动逻辑:把空白方块左方的数字向右移动x, y: 表示空白方块x, y - 1: 表示空白方块左方的数字*/// 将空表方块左方图片的数据 赋值给 空白方块处data[x][y] = data[x][y - 1];// 空白左方图片的数据赋值为0data[x][y - 1] = 0;// 将空白方块往左移动y--;// 移动后统计步数step++;// 重新加载一下所有图片initImages(path);} else if (keyCode == 65) { // 否,则判断松开的按键是否为A// 显示随机打乱的图片initImages(path);} else if (keyCode == 87) { // 否,则判断松开的按键是否为W// 是,则显示拼图完成后的效果!// 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组data = new int[][]{{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12},{13, 14, 15, 0}};// 按照二维数组的最新数据重新加载所有小图片initImages(path);}}// 动作监听事件:监听鼠标左键单击、键盘空格操作@Overridepublic void actionPerformed(ActionEvent e) {// 获取当前被点击的栏目对象Object obj = e.getSource();// 判断当前被点击的是否为重新游戏栏目if (obj == replayGameItem) {System.out.println("重新游戏");// 1、先将步数清零step = 0;// 2、重新打乱二维数组的数据initData();// 3、重新按照打乱后的数据加载所有小图片initImages(path);} else if (obj == replayLoginItem) {// 否,则判断当前被点击的是否为重新登录栏目System.out.println("重新登录");// 1、先关闭当前游戏主界面this.setVisible(false); // 隐藏起来// 2、构造登录界面new LoginJFrame();} else if (obj == closeGameItem) {// 否,则判断当前被点击的是否为关闭游戏栏目System.out.println("关闭游戏");// 直接结束JVM虚拟机运行System.exit(0);} else if (obj == accountItem) {// 否,则判断当前被点击的是否为公众号栏目System.out.println("公众号");// 1、创建对话窗对象JDialog jDialog = new JDialog();// 2、根据相对路径创建图片对象,并添加到管理容器中,并设置图片的坐标和宽高JLabel aboutJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\about.jpg"));aboutJLabel.setBounds(0, 0, 220, 220);// 3、将管理容器添加到对话窗中jDialog.getContentPane().add(aboutJLabel);// 4、设置对话窗的大小jDialog.setSize(300, 300);// 5、将对话窗置顶jDialog.setAlwaysOnTop(true);// 6、让对话窗居中jDialog.setLocationRelativeTo(null);// 7、设置对话窗不关闭则无法操作下面的界面jDialog.setModal(true);// 8、让对话窗显示出来,因为默认是隐藏的jDialog.setVisible(true);} else if (obj == belleItem) {  // 否,则判断当前被点击的是否为美女栏目// 是,则随机更换美女图片中的一张System.out.println("更换美女图片");// 随机一个1~2之间的数int rdNumber = rd.nextInt(2) + 1;// 将path的值改为美女图片所在的相对路径path = "puzzle_game\puzzleimages\girl\girl" + rdNumber + "\";// 重新打乱二维数组中的数据initData();// 根据美女图片的相对路径,随机初始化一张美女图片initImages(path);} else if (obj == animalItem) { // 否,则判断当前被点击的是否为动物栏目// 是,则随机更换动物图片中的一张System.out.println("更换动物图片");// 随机一个1~2之间的数int rdNumber = rd.nextInt(2) + 1;// 将path的值改为美女图片所在的相对路径path = "puzzle_game\puzzleimages\animal\animal" + rdNumber + "\";// 重新打乱二维数组中的数据initData();// 根据动物图片的相对路径,随机初始化一张动物图片initImages(path);}}
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面}
}

3、测试结果
  • 未更换之前:

    在这里插入图片描述



  • 更换图片为动物

    在这里插入图片描述

    在这里插入图片描述



  • 按A的时候

    在这里插入图片描述



  • 点击重新游戏的时候

    在这里插入图片描述

    在这里插入图片描述




十二、用户注册功能实现(先有注册才有登录)

1、需求

在这里插入图片描述

  • 开发用户注册功能:
    • 1、输入注册的用户名:
      • 需要校验用户名的唯一性
    • 2、输入注册的密码:
      • 只能是字母+数字的组合
      • 需要校验两次输入的密码是否一致
    • 3、当用户用鼠标点击注册图标的时候,颜色下沉,并提示注册成功!
    • 4、当用户用鼠标点击重置图标的时候,颜色下沉,并清空输入框的内容!


2、分析

(1)所用技术

  • 字符串、String类的API调用、字符判断
  • for循环、List集合、标准JavaBean
  • JFrame界面对象、JLabel管理容器对象、JButton按钮对象、JTextField明文输入框、ImageIcon图片对象
  • MouseListener鼠标监听

(2)思路分析

界面实现思路:
  • 1、先将注册界面给制作出来:
    • 设置界面宽高、标题、置顶界面、界面打开时居中、界面的关闭模式
    • 取消界面的默认布局:这样才可以让布局按照xy轴的形式添加组件
  • 2、然后初始化信息输入框:
    • 创建管理容器对象,用于管理用户名输入框:
      • 设置管理容器的坐标及宽高
      • 创建文本输入框,用于用户输入用户名
      • 设置输入框的坐标及宽高
      • 将输入框对象添加到管理容器中
      • 将管理容器添加到注册界面中
    • 创建管理容器对象,用于管理用户密码输入框:
      • 与上面一样的操作!
    • 创建管理容器对象,用于管理用户确认密码输入框:
      • 与上面一样的操作!
    • 根据指定注册图片的相对路径创建图片对象,添加到按钮对象中:
      • 设置按钮的坐标及宽高
      • 给按钮绑定鼠标监听
      • 将按钮对象添加到注册界面中
    • 根据指定重置图片的相对路径创建图片对象,添加到按钮对象中:
      • 与上面一样的操作!
  • 3、最后:
    • 根据指定背景图片的相对路径创建图片对象,并添加到管理容器中
    • 设置管理容器的坐标及宽高
    • 将管理容器添加到注册界面中
  • 4、在本类中实现MouseListener鼠标监听接口的所有方法:
    • 然后在mousePressed按下鼠标方法中实现按下鼠标的事件监听:
      • 当用户按下注册、重置按钮时,颜色变暗!
    • 然后在mouseReleased释放鼠标方法中实现松开鼠标的事件监听:
      • 当用户松开注册按钮时,又恢复原样!

注册功能实现思路:
  • 1、先定义用户类,用于封装用户信息为一个用户对象

  • 2、定义一个集合,用于存储用户对象

  • 3、当用户输入注册的用户名时:

    • 先获取用户输入的用户名
  • 4、当用户输入注册的密码时:

    • 先获取输入的密码
  • 5、当用户确认注册的密码时:

    • 先获取输入的确认密码
  • 6、当用户按下注册按钮时:

    • 实现校验用户名唯一性的逻辑代码
    • 实现密码不能是中文的逻辑代码
    • 实现对两次密码一致性的判断
    • 将用户录入的用户名和密码封装成一个用户对象并添加进集合中
  • 7、当用户按下重置按钮时:

    • 将所有输入框的内容清空!


3、实现

(1)用户类

package cn.edu.gxufe.entity;/*** 用户类*/
public class User {// 定义用户属性:用户名、密码private String username;private String password;// 提供无参、有参构造器public User(){}public User(String username, String password) {this.username = username;this.password = password;}// 提供成员变量全套的get和set方法,方便赋值和取值public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}
}

(2)注册界面类

package cn.edu.gxufe.ui;import cn.edu.gxufe.entity.User;import javax.swing.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;/*** 注册界面类:* 继承父类:JFrame*/
public class RegisterJFrame extends JFrame implements MouseListener {// 表示注册相关的逻辑代码都写在这!// 1、创建管理容器:管理用户名输入框JLabel nameJLabel = new JLabel("注册用户名");// 2、创建管理容器:管理用户密码输入框JLabel passwordJLabel = new JLabel("注册密码");// 3、创建管理容器:管理用户确认密码输入框JLabel okPasswordJLabel = new JLabel("再次确认密码");// 4、根据指定注册图片的相对路径创建图片对象,并添加到按钮对象中JButton registerJButton = new JButton(new ImageIcon("puzzle_game\puzzleimages\register\register_button.jpg"));// 5、根据指定重置图片的相对路径创建图片对象,并添加到按钮对象中JButton resetJButton = new JButton(new ImageIcon("puzzle_game\puzzleimages\register\reset_button.jpg"));// 定义一个静态的List集合对象常量,用于存储用户对象public static final ArrayList<User> userList = new ArrayList<>();// b.创建文本输入框:用于用户输入用户名JTextField nameJText = new JTextField();// b.创建文本输入框:用于用户输入密码JTextField passwordJText = new JTextField();// b.创建文本输入框:用于用户输入密码JTextField okPasswordJText = new JTextField();/*提供无参数的构造器需求:初始化一个宽501像素,高437像素的登录界面*/public RegisterJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 初始化注册界面initJFrame();// 初始化注册信息框initRegisterMess();// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面this.setVisible(true);}// 初始化注册界面private void initJFrame() {// 设置界面宽高// 调用当前GameJFrame类的setSize方法,初始化一个宽501像素,高437像素大小的界面this.setSize(501, 437);// 设置界面标题this.setTitle("奥利gei拼图-注册");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行// this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);// 取消默认布局,这样子才可以让布局按照x和y轴的形式添加组件this.setLayout(null);}// 初始化注册信息框private void initRegisterMess() {// a.设置管理容器的坐标位置及宽高nameJLabel.setBounds(90, 100, 350, 100);// c.设置输入框的坐标位置及宽高nameJText.setBounds(80, 35, 200, 30);nameJLabel.add(nameJText);// 将管理容器对象添加到当前注册界面中this.getContentPane().add(nameJLabel);// a.设置管理容器的坐标位置及宽高passwordJLabel.setBounds(90, 150, 350, 100);// c.设置输入框的坐标位置及宽高passwordJText.setBounds(80, 35, 200, 30);passwordJLabel.add(passwordJText);// 将管理容器对象添加到当前注册界面中this.getContentPane().add(passwordJLabel);// a.设置管理容器的坐标位置及宽高okPasswordJLabel.setBounds(90, 200, 350, 100);// c.设置输入框的坐标位置及宽高okPasswordJText.setBounds(80, 35, 200, 30);okPasswordJLabel.add(okPasswordJText);// 将管理容器对象添加到当前注册界面中this.getContentPane().add(okPasswordJLabel);// a.设置按钮的坐标位置及宽高registerJButton.setBounds(90, 300, 115, 35);// 给按钮对象绑定鼠标监听registerJButton.addMouseListener(this);// 将按钮对象添加到当前注册界面中this.getContentPane().add(registerJButton);// a.设置按钮的坐标位置及宽高resetJButton.setBounds(256, 300, 115, 35);// 给按钮对象绑定鼠标监听resetJButton.addMouseListener(this);// 将按钮对象添加到当前注册界面中this.getContentPane().add(resetJButton);// 最后// a.根据指定背景图片的相对路径创建图片对象,并添加到管理容器中JLabel bgJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\register_login_background.jpg"));// b.设置背景图管理容器的坐标位置及宽高bgJLabel.setBounds(0, 0, 488, 400);// c.将背景图管理容器对象添加到当前注册界面中this.getContentPane().add(bgJLabel);}// 鼠标单击事件@Overridepublic void mouseClicked(MouseEvent e) {}// 按下鼠标事件@Overridepublic void mousePressed(MouseEvent e) {// 获取当前按下的按钮对象Object obj = e.getSource();// 判断当前按下的按钮是否为注册按钮if (obj == registerJButton) {// 是System.out.println("按下注册按钮");// 设置注册按钮图片registerJButton.setIcon(new ImageIcon("puzzle_game\puzzleimages\register\register_down.jpg"));// 注册用户registerUser();} else if (obj == resetJButton) { // 否,则判断当前按下的按钮是否为重置按钮// 是System.out.println("按下重置按钮");// 设置重置按钮图片resetJButton.setIcon(new ImageIcon("puzzle_game\puzzleimages\register\reset_down.jpg"));// 将用户名、密码、确认密码的输入框的内容都清空nameJText.setText("");passwordJText.setText("");okPasswordJText.setText("");}}// 鼠标释放事件@Overridepublic void mouseReleased(MouseEvent e) {// 获取当前按下的按钮对象Object obj = e.getSource();// 判断当前按下的按钮是否为重置按钮if (obj == resetJButton) {// 是System.out.println("松开重置按钮");resetJButton.setIcon(new ImageIcon("puzzle_game\puzzleimages\register\reset_button.jpg"));}}// 鼠标划入事件@Overridepublic void mouseEntered(MouseEvent e) {}// 鼠标划出事件@Overridepublic void mouseExited(MouseEvent e) {}/*** 注册用户功能*/private void registerUser() {// 1、校验注册的用户名不能为空字符串if (nameJText.getText().equals("")) {// 是空字符,创建一个对话窗,提示一下!createJDialog(new JLabel("注册的用户名不能为空!"));}// 2、校验用户名的唯一性if (!checkUsername()) {// 用户不存在// 校验密码不能是空字符串if (passwordJText.getText().equals("")) {// 是空字符,创建一个对话窗,提示一下!createJDialog(new JLabel("注册的密码不能为空!"));} else {// 否,则校验密码是否不为中文if (checkPassword()) {// 是,则说明都不是中文// 判断两次输入的密码是否一致if (passwordJText.getText().equals(okPasswordJText.getText())) {// 一致!封装用户信息为一个用户对象,并添加进集合中userList.add(new User(nameJText.getText(), passwordJText.getText()));// 注册成功!创建一个对话窗,提示一下!createJDialog(new JLabel("注册成功!"));// 注册成功后!关闭注册界面,返回登录界面this.setVisible(false);new LoginJFrame();} else {// 两次输入的密码不一致,创建一个对话窗,提示一下!createJDialog(new JLabel("两次输入的密码不一致!"));}} else {// 注册的密码为中文,创建一个对话窗,提示一下!createJDialog(new JLabel("注册的密码不能为中文!"));}}} else {// 用户名已存在,创建一个对话窗,提示一下!createJDialog(new JLabel("用户名:" + nameJText.getText() + "已注册!请您换一个名字~"));}}/*** 创建一个对话窗** @param jLabel 管理容器对象*/private void createJDialog(JLabel jLabel) {// 1、创建对话窗对象JDialog jDialog = new JDialog();// 2、设置管理容器的坐标及宽高jLabel.setBounds(0, 0, 100, 100);// 3、将管理容器添加到对话窗中jDialog.getContentPane().add(jLabel);// 设置对话窗的大小jDialog.setSize(300, 100);// 4、将对话窗置顶jDialog.setAlwaysOnTop(true);// 5、让对话窗居中jDialog.setLocationRelativeTo(null);// 6、设置对话窗不关闭则无法操作下面的界面jDialog.setModal(true);// 7、让对话窗显示出来,因为默认是隐藏的jDialog.setVisible(true);// 8、当用户关闭对话窗时,注册按钮恢复原样!registerJButton.setIcon(new ImageIcon("puzzle_game\puzzleimages\register\register_button.jpg"));}/*** 校验密码** @return 校验通过返回true,否则返回false*/private boolean checkPassword() {// 1、遍历密码字符串,依次得到每个字符for (int i = 0; i < passwordJText.getText().length(); i++) {// 判断密码的每个字符是否为中文char c = passwordJText.getText().charAt(i);if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))) {// 有一个是中文,就返回falsereturn false;}}// 循环结束,说明都不是中文,返回truereturn true;}/*** 校验用户名** @return 用户名不存在返回true,否则返回false*/private boolean checkUsername() {// 2、根据注册的用户名得到该用户对象在集合中的索引int index = getUserIndex(nameJText.getText());// 3、判断索引是否小于0if (index < 0) {// 该用户不存在!返回nullreturn false;}// 用户存在return true;}/*** 根据用户名得到该用户对象在集合中的索引** @param username 注册的用户名* @return 存在就返回索引,否则返回-1*/public static int getUserIndex(String username) {// 1、遍历用户对象集合,依次得到集合中的每个用户对象for (int i = 0; i < userList.size(); i++) {// 判断当前遍历到的用户对象的用户名 是否匹配 注册的用户名if (userList.get(i).getUsername().equals(username)) {// 是,则说明匹配成功!返回该用户对象在集合中的索引return i;}}// 2、循环结束,仍然找不到与注册的用户名 匹配的用户对象,返回-1return -1;}
}

(3)程序启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!
//        new GameJFrame();       // 游戏主界面new RegisterJFrame();   // 注册界面
//         new LoginJFrame();      // 登录界面}
}

(4)测试结果

  • 直接按下注册按钮:

    在这里插入图片描述


    在这里插入图片描述



  • 注册一个用户:

    在这里插入图片描述


    在这里插入图片描述



  • 当用户关掉注册成功提示框时:

    在这里插入图片描述




十三、用户登录功能实现(程序启动入口)

1、需求

在这里插入图片描述

  • 实现用户登陆功能:
    • 1、需要输入用户名、密码、验证码:
      • 校验用户名是否未注册!是,则弹出一个窗口提示用户注册!
      • 校验用户名、密码的正确性!
      • 校验验证码的正确性!
    • 2、点击验证码的时候可以重新生成一个验证码
    • 3、当用户按下登录按钮的时候,颜色下沉,弹出一个窗口提示登录成功!关闭弹窗后按钮恢复原本的样子!
    • 4、当用户按下注册按钮的时候,颜色下沉,并跳转到注册界面!


2、分析

(1)所用技术

  • 字符串比较、String类API调用、随机数
  • for循环、标准JavaBean、List集合
  • 图形化类:
    • JFrame界面、JDialog对话窗、JLabel管理容器、JButton按钮、ImageIcon图片
    • JTextField明文输入框、JPasswordField密文输入框
  • 事件监听:
    • MouseListener鼠标监听


(2)思路分析

登录界面初始化:
  • 1、需要创建一个LoginJFrame类,继承父类JFrame界面类
  • 2、提供无参数的构造器:
    • 设置界面的宽高、标题、置顶、打开时自动居中、关闭模式、取消默认布局(这样才可以让布局按照xy轴的形式添加组件)
    • 最后设置界面为显示的,建议写在所有程序代码的后面。

美化登录界面:
  • 1、创建一个JLabel管理容器,用于管理用户名输入框
  • 2、创建一个JTextField明文输入框,用于输入用户名,放进管理容器中,然后将管理容器放进界面中
  • 3、创建一个JLabel管理容器,用于管理密码输入框
  • 4、创建一个JPasswordField密文输入框,用于输入密码,放进管理容器中,然后将管理容器放进界面中
  • 5、先创建生成验证码的工具类,用于生成一个由4个大小写字母+1个数字的验证码
  • 6、创建一个JLabel管理容器,用于管理验证码输入框
  • 7、创建一个JTextField明文输入框,用于输入验证码,放进管理容器中,然后将管理容器放进界面中
  • 8、创建一个JLabel管理容器,用于管理显示的验证码,然后将管理容器放进界面中
  • 9、创建一个JButton按钮对象,用于管理登录按钮图片,然后将按钮对象放进界面中
  • 10、创建一个JButton按钮对象,用于管理注册按钮图片,然后将按钮对象放进界面中
  • 最后,创建一个JLabel管理容器,用于管理背景图,然后将管理容器放进界面中

登录功能实现:
  • 1、需要得到注册界面类中的List集合

  • 2、需要给显示验证码的管理容器绑定鼠标监听:

    • 在鼠标单击事件的方法中实现:
      • 1、获取当前鼠标单击的事件
      • 2、判断当前鼠标单击的事件是否为验证码管理容器:
        • 是,则重新生成新的验证码
  • 3、需要给登录按钮、注册按钮、眼睛按钮都绑定鼠标监听:

    • 在按下鼠标事件的方法中实现:
      • 1、获取当前按下的事件
      • 2、判断当前按下的事件是否为登录按钮:
          • (1)说明当前按下的是登录按钮,则让按钮变暗
          • (2)校验用户名输入框的用户名是否未注册:
            • 是,则说明该用户名未注册,弹窗提示:“用户名未注册!请先注册!”
            • 否,则说明该用户名已注册,则校验用户名和密码是否正确:
              • 是,则说明正确,则校验验证码是否正确:
                • 是,则说明验证码正确,弹窗提示:“登录成功!”,关闭弹窗后跳转到游戏界面!
                • 否,则说明验证码不正确,弹窗提示:“验证码错误!”
              • 否,则说明不正确,则弹窗提示:“您的用户名或密码有误!”
        • 否,则判断当前按下的事件是否为注册按钮:
            • (1)说明当前按下的是注册按钮,则让按钮变暗
            • (2)关闭登录界面后,跳转到注册界面中


3、实现

(1)登录界面类

package cn.edu.gxufe.ui;import cn.edu.gxufe.entity.User;
import cn.edu.gxufe.util.VerifyCodeUtil;import javax.swing.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;/*** 登录界面类:* 继承父类:JFrame*/
public class LoginJFrame extends JFrame implements MouseListener {// 表示登录相关的逻辑代码都写在这!// 需要得到注册界面类中的List集合public static ArrayList<User> userList = RegisterJFrame.userList;// 2、创建一个JTextField明文输入框,用于输入用户名JTextField usernameText = new JTextField();// 4、创建一个JPasswordField密文输入框,用于输入密码JPasswordField passwordText = new JPasswordField();// 7、创建一个JTextField明文输入框,用于输入验证码JTextField verifyCodeText = new JTextField();// 8、创建一个JLabel管理容器,用于管理显示的验证码JLabel displayVerifyCodeJLabel = new JLabel(VerifyCodeUtil.getVerifyCode());// 9、创建一个JButton按钮对象,用于管理登录按钮图片JButton loginButton = new JButton();// 10、创建一个JButton按钮对象,用于管理注册按钮图片JButton registerButton = new JButton();/*提供无参数的构造器需求:初始化一个宽501像素,高437像素的登录界面*/public LoginJFrame() {// 初始化界面initJFrame();// 初始化登录信息框initLoginMess();// 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面// 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面this.setVisible(true);}// 初始化登录信息框private void initLoginMess() {// 1、创建一个JLabel管理容器,用于管理用户名输入框JLabel usernameJLabel = new JLabel("用户名");// 设置管理容器的坐标及宽高usernameJLabel.setBounds(90, 100, 350, 100);// 设置输入框的坐标位置及宽高usernameText.setBounds(80, 35, 200, 30);// 将用户名输入框放进管理容器中usernameJLabel.add(usernameText);// 然后将管理容器放进界面中this.getContentPane().add(usernameJLabel);// 3、创建一个JLabel管理容器,用于管理密码输入框JLabel passwordJLabel = new JLabel("密码");// 设置管理容器的坐标及宽高passwordJLabel.setBounds(90, 150, 350, 100);// 设置输入框的坐标位置及宽高passwordText.setBounds(80, 35, 200, 30);// 将密码输入框放进管理容器中passwordJLabel.add(passwordText);// 然后将管理容器放进界面中this.getContentPane().add(passwordJLabel);// 6、创建一个JLabel管理容器,用于管理验证码输入框JLabel verifyCodeJLabel = new JLabel("验证码");// 设置管理容器的坐标及宽高verifyCodeJLabel.setBounds(90, 200, 350, 100);// 设置输入框的坐标位置及宽高verifyCodeText.setBounds(80, 35, 200, 30);// 将验证码输入框放进管理容器中verifyCodeJLabel.add(verifyCodeText);// 然后将管理容器放进界面中this.getContentPane().add(verifyCodeJLabel);// 设置显示验证码管理容器的坐标及宽高displayVerifyCodeJLabel.setBounds(380, 232, 200, 30);// 需要给显示验证码的管理容器绑定鼠标监听displayVerifyCodeJLabel.addMouseListener(this);// 然后将管理容器放进界面中this.getContentPane().add(displayVerifyCodeJLabel);// 根据指定登录图片的相对路径创建图片对象,并设置到登录按钮中loginButton.setIcon(new ImageIcon("puzzle_game\\puzzleimages\\login\\login_button.jpg"));// 设置登录按钮的坐标及宽高loginButton.setBounds(90, 300, 115, 35);// 需要给登录按钮绑定鼠标监听loginButton.addMouseListener(this);// 然后将登录按钮对象放进界面中this.getContentPane().add(loginButton);// 根据指定注册图片的相对路径创建图片对象,并设置到注册按钮中registerButton.setIcon(new ImageIcon("puzzle_game\\puzzleimages\\register\\register_button.jpg"));// 设置注册按钮的坐标位置及宽高registerButton.setBounds(256, 300, 115, 35);// 需要给注册按钮绑定鼠标监听registerButton.addMouseListener(this);// 然后将注册按钮对象放进界面中this.getContentPane().add(registerButton);// 最后,创建一个JLabel管理容器,用于管理背景图JLabel bgJLabel = new JLabel(new ImageIcon("puzzle_game\\puzzleimages\\sport\\register_login_background.jpg"));// 设置背景图管理容器的坐标位置及宽高bgJLabel.setBounds(0, 0, 488, 400);// 然后将管理容器放进界面中this.getContentPane().add(bgJLabel);}// 初始化登录界面private void initJFrame() {// 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的// 设置界面宽高// 调用setSize方法,初始化一个宽501像素,高437像素大小的界面this.setSize(501, 437);// 设置界面标题this.setTitle("奥利gei拼图-登录");// 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的this.setAlwaysOnTop(true);// 设置界面打开时自动居中this.setLocationRelativeTo(null);// 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行this.setDefaultCloseOperation(3);// 取消默认布局,这样子才可以让布局按照x和y轴的形式添加组件this.setLayout(null);}// 鼠标单击事件@Overridepublic void mouseClicked(MouseEvent e) {// 1、获取当前鼠标单击的事件Object obj = e.getSource();// 2、判断当前鼠标单击的事件是否为验证码管理容器:if (obj == displayVerifyCodeJLabel) {System.out.println("鼠标点击验证码");// 是,则重新生成新的验证码displayVerifyCodeJLabel.setText(VerifyCodeUtil.getVerifyCode());}}// 按下鼠标事件@Overridepublic void mousePressed(MouseEvent e) {// 1、获取当前按下的事件Object obj = e.getSource();// 2、判断当前按下的事件是否为登录按钮:if (obj == loginButton) {// 是System.out.println("按下登录按钮");// (1)说明当前按下的是登录按钮,则让按钮变暗loginButton.setIcon(new ImageIcon("puzzle_game\\puzzleimages\\login\\login_down.jpg"));// 用户登录userLogin();} else if (obj == registerButton) { // 否,则判断当前按下的事件是否为注册按钮:// 是System.out.println("按下注册按钮");// (1)说明当前按下的是注册按钮,则让按钮变暗registerButton.setIcon(new ImageIcon("puzzle_game\\puzzleimages\\register\\register_down.jpg"));// (2)关闭登录界面后,跳转到注册界面中this.setVisible(false);new RegisterJFrame();}}/*** 用户登录功能*/private void userLogin() {// 获取输入框的用户名、密码、验证码String inputUsername = usernameText.getText();String inputPassword = passwordText.getText();String inputVerifyCode = verifyCodeText.getText();// 先校验用户名是否为空字符串if (inputUsername.equals("")) {// 是空字符串,则创建一个弹窗提示:“用户名不能为空!”createJDialog(new JLabel("用户名不能为空!"));} else {// 否,说明用户名不是空字符串,则校验密码是否为空字符串if (inputPassword.equals("")) {// 是空字符串,则创建一个弹窗提示:“密码不能为空!”createJDialog(new JLabel("密码不能为空!"));} else {// 否,说明密码不是空字符串,则校验验证码是否为空字符if (inputVerifyCode.equals("")) {// 是,则创建一个弹窗提示:“验证码不能为空!”createJDialog(new JLabel("验证码不能为空!"));}else {// 否,说明验证码不为空字符串,校验用户名输入框的用户名是否未注册:if (!checkUsername(inputUsername)) {// 是,则说明该用户名未注册,弹窗提示:“用户名未注册!请先注册!”createJDialog(new JLabel("用户名:" + usernameText.getText() + "未注册!请先注册!"));} else {// 否,则说明该用户名已注册,则校验用户名和密码是否正确:if (checkUsernameAndPassword(inputUsername, inputPassword)) {// 是,说明用户名和密码正确,则校验验证码是否正确:if (inputVerifyCode.equalsIgnoreCase(displayVerifyCodeJLabel.getText())) {// 是,则说明验证码正确,弹窗提示:“登录成功!”createJDialog(new JLabel("登录成功!"));// 关闭弹窗后跳转到游戏界面!this.setVisible(false);new GameJFrame();} else {// 否,则说明验证码不正确,弹窗提示:“验证码错误!”createJDialog(new JLabel("验证码错误!"));}} else {// 否,则说明不正确,则创建一个弹窗提示:“您的用户名或密码有误!”createJDialog(new JLabel("您的用户名或密码有误!"));}}}}}}/*** 校验用户名和密码是否正确** @param inputUsername 输入框的用户名* @param inputPassword 输入框的密码* @return 正确返回true,否则返回false*/private boolean checkUsernameAndPassword(String inputUsername, String inputPassword) {// 1、遍历userList集合,依次得到每个用户对象for (User user : userList) {// 判断输入框的用户名和密码 是否匹配 当前用户对象的用户名和密码if ((inputUsername.equals(user.getUsername())&& (inputPassword.equals(user.getPassword())))) {// 匹配,返回truereturn true;}// 不匹配,则继续与集合中的下一个用户对象做比较}// 2、循环结束,仍然找不到匹配的用户名和密码,说明用户名或密码错误!返回falsereturn false;}/*** 校验用户是否已注册!** @param inputUsername 输入框的用户名* @return 已注册返回true,未注册返回true*/private boolean checkUsername(String inputUsername) {// 1、根据用户名获取该用户对象在集合中的索引int index = RegisterJFrame.getUserIndex(inputUsername);// 2、判断索引是否大于等于0if (index >= 0) {// 是,说明该用户存在,返回truereturn true;}// 否,说明该用户不存在,返回falsereturn false;}// 释放鼠标事件@Overridepublic void mouseReleased(MouseEvent e) {}// 鼠标划入事件@Overridepublic void mouseEntered(MouseEvent e) {}// 鼠标划出事件@Overridepublic void mouseExited(MouseEvent e) {}/*** 创建一个对话窗** @param jLabel 管理容器对象*/private void createJDialog(JLabel jLabel) {// 1、创建对话窗对象JDialog jDialog = new JDialog();// 2、设置管理容器的坐标及宽高jLabel.setBounds(0, 0, 100, 100);// 3、将管理容器添加到对话窗中jDialog.getContentPane().add(jLabel);// 设置对话窗的大小jDialog.setSize(300, 100);// 4、将对话窗置顶jDialog.setAlwaysOnTop(true);// 5、让对话窗居中jDialog.setLocationRelativeTo(null);// 6、设置对话窗不关闭则无法操作下面的界面jDialog.setModal(true);// 7、让对话窗显示出来,因为默认是隐藏的jDialog.setVisible(true);// 8、当用户关闭对话窗时,登录按钮恢复原样!loginButton.setIcon(new ImageIcon("puzzle_game\\puzzleimages\\login\\login_button.jpg"));}
}

(2)程序启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;public class App {public static void main(String[] args) {// 表示程序启动的入口// 如果我们想要开启一个界面,就创建对应界面的对象即可!new LoginJFrame();      // 登录界面}
}

(3)测试结果

  • 直接点击登录

    在这里插入图片描述


    在这里插入图片描述


    在这里插入图片描述


    在这里插入图片描述



  • 点击注册按钮

    在这里插入图片描述


    在这里插入图片描述



  • 直接点击注册按钮
    在这里插入图片描述


    在这里插入图片描述



  • 当关闭弹窗后

    在这里插入图片描述


    在这里插入图片描述



  • 当关闭弹窗后

    在这里插入图片描述



  • 当点击重新登录后

    在这里插入图片描述


    在这里插入图片描述






十四、游戏打包exe

1、要考虑的因素

(1)一定要包含图形化界面

(2)代码要打包起来

(3)游戏用到的图片素材也要打包起来

(4)JDK也要打包


2、核心步骤

(1)把所有代码打包成一个压缩包,jar后缀的压缩包。

(2)把jar包转换成exe安装包

(3)把第二步的exe,图片,JDK整合在一起,变成最终的exe安装包


微信公众号搜索黑马程序员,关注后自己拿资源、图片,游戏打包exe的word文档在第18天资料里
建议去bilibili将黑马程序员的2022年最新JavaSE基础上部的课程全部刷完再来做这个项目,否则你会思路混乱


十五、打包成exe后的最终测试

在这里插入图片描述


  • 登录界面测试

    在这里插入图片描述


    在这里插入图片描述



  • 注册界面测试

    • 点击注册后,会跳转到注册界面:

    在这里插入图片描述


    在这里插入图片描述


    在这里插入图片描述



  • 关闭弹窗后会自动回到登录界面,再次点击注册

    在这里插入图片描述


    在这里插入图片描述


    在这里插入图片描述



  • 关闭弹窗后会自动回到登录界面

    在这里插入图片描述


    在这里插入图片描述


    在这里插入图片描述



  • 关闭弹窗后会自动跳转到拼图游戏主界面

    在这里插入图片描述



  • 测试更换图片功能(更换图片时不重置步数)

    在这里插入图片描述


    在这里插入图片描述


    在这里插入图片描述



  • 测试重新游戏功能(重新游戏后的图片是更换后的图片)

    在这里插入图片描述



  • 测试关于我们菜单下的公众号功能

    在这里插入图片描述


    在这里插入图片描述



  • 测试重新登录功能

    • 点击重新登录后,会自动回到登录界面

    在这里插入图片描述


    在这里插入图片描述



  • 关闭弹窗后会自动跳转到拼图游戏主界面

    在这里插入图片描述



  • 测试关闭游戏功能

    • 点击关闭游戏之后肯定就拜拜啦!

    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_228002.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Android进阶之旅】内存泄漏的危害有哪些?(案例分析)

随着计算机应用需求的日益增加&#xff0c;应用程序的设计与开发也相应的日趋复杂&#xff1b; 开发人员在程序实现的过程中处理的变量也大量增加&#xff0c;如何有效进行内存分配和释放&#xff0c;防止内存泄漏的问题变得越来越突出 例如&#xff1a; 服务器应用软件&#x…

Redis 内存淘汰和过期删除策略

提起使用Redis的优点&#xff0c;大家可以列举出许多&#xff0c;比如&#xff1a;数据存储在内存&#xff0c;读写速度快&#xff0c;性能优异。比如数据持久化&#xff0c;便于数据备份及恢复等等。 分布式服务系统平台发展至今&#xff0c;Redis活跃在平台的各个领域&#…

RabbitMQ事务消息

通过对信道的设置实现 channel.txSelect()&#xff1b;通知服务器开启事务模式&#xff1b;服务端会返回Tx.Select-Ok channel.basicPublish&#xff1b;发送消息&#xff0c;可以是多条&#xff0c;可以是消费消息提交ackchannel.txCommit() &#xff1b;提交事务&#xff1b;…

mmdetection3d SUN RGB-D数据集预处理

SUN RGB-D是普林斯顿大学发布的一种关于室内场景理解的数据集&#xff0c;共包含了10335个样本&#xff0c;其中训练样本和验证测试样本数量分别为5285和5050。每个样本包含了彩色图像&#xff08;RGB&#xff09;和深度&#xff08;D&#xff09;信息&#xff0c;并且分别进行…

基于BDD的接口自动化框架开箱即用

1、背景说明 项目思想&#xff1a;BDD 行为驱动开发的思想褒贬不一&#xff0c;这里不多说。遵循的宗旨能解决业务痛点的思想就是好思想。 接口测试工具在实际的业务测试场景中往往会遇到一些使用上的局限性&#xff0c;自定义扩展要求技术较高&#xff0c;如果二次开发工具…

小程序瀑布流实现

什么是瀑布流布局 瀑布流布局&#xff0c;一般等宽&#xff0c;不等高的列表排列 原理是找出高度之和最小的那一列&#xff0c;在高度最小列继续添加元素 可以通过 absolute 定位实现&#xff0c;动态计算每一项的 top 和 left 封装瀑布流方法 function getAllRect(context…

[附源码]Python计算机毕业设计Django的疫苗接种管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

c#、wpf开发中页面在win10下被缩放125%引起页面错乱的解决办法。

正常情况下,我们开发的页面页面应该是100%缩放的,这样程序在win7和win10下保持一致,但是win10里面会根据显示器的情况自动调整“缩放与布局”,这使得桌面程序有时候会发生页面错乱,怎么调整就是个问题。 如图:在“缩放与布局”100%显示如下: 而在 “缩放与布局”125%显…

基于AD Event日志检测LSASS凭证窃取攻击

01、简介 简单介绍一下&#xff0c;LSASS(本地安全机构子系统服务)在本地或域中登录Windows时&#xff0c;用户生成的各种凭证将会存储在LSASS进程的内存中&#xff0c;以便用户不必每次访问系统时重新登录。 攻击者在获得起始攻击点后&#xff0c;需要获取目标主机上的相关凭证…

小程序中的confirm-type设置键盘的确认按钮

详情&#xff1a; confirm-type是很多小程序组件中的一种设置&#xff0c;用于改变输入键盘右下角的确认按钮。比如说&#xff0c;正常情况下&#xff0c;键盘上的默认提示可能是完成&#xff0c;但是你可以通过confirm-type将其设置为发送&#xff0c;搜索等&#xff0c;在特…

搬砖日记:关于sync用不了的问题

自己封装了个输入框的组件&#xff0c;想要实现的输入框的值的修改可以实时修改到父组件的值 印象中看到过人家用.sync修饰符去实现这个功能&#xff0c;大抵是 //父组件 <searchInput :value.sync"value"></searchInput> //子组件 <input v-model&qu…

Redeis缓存查询基于元注解与AOP结合使用——不过时的优雅

Redeis缓存查询基于元注解与AOP结合使用 根据优化需要&#xff0c;数据查询的时候无法避免的使用Redis基于缓存查询&#xff0c;进而减少对于数据库的查询压力&#xff0c;对于过多的方法基于缓存存储&#xff0c;为提高代码的复用性&#xff0c;采用一种不过时的写法。 整体的…

一文详解,数据仓库、数据库、数据中台、数据湖的区别

数据时代&#xff0c;各行业的企业都已经开始通过数据库来沉淀数据&#xff0c;但是真的论起数据库、数据仓库、数据中台&#xff0c;还是新出现的数据湖&#xff0c;它们的概念和区别&#xff0c;可能知道的人就比较少了&#xff0c;今天我们详细来比较了解一下。 数据仓库是…

你的数据库到底应该如何存储密码?

最近接手公司一个之前的服务&#xff0c;竟然发现用户密码是明文存储在数据库中&#xff01; 说实话还是有点吃惊的&#xff0c;这可不兴学 CSDN 呀&#xff08;手动狗头&#xff09;&#xff0c;至少也得搞个 MD5 存一存吧。 不过 MD5 其实也没啥用&#xff0c;今天我们就来…

JVM之运行时数据区 PC、虚拟机栈、本地方法栈

JVM之运行时数据区 PC、虚拟机栈、本地方法栈PC寄存器线程回顾寄存器实践面试使用PC寄存器存储字节码指令地址有什么用&#xff1f;为什么使用PC寄存器记录当前线程的执行地址PC寄存器为什么会被设定为线程私有虚拟机栈虚拟机栈出现背景简介栈可能出现的异常栈中存储着什么运行…

电商新趋势:Starday拿下黑色星期五的制胜法宝是物流速度

国内电商“双十一”购物狂欢季活动已经闭幕&#xff0c;“双十二”又将袭来&#xff0c;但更多人却将眼光放在蓬勃发展的跨境电商行业中。当下跨境电商卖家们正在各大跨境电商服务平台的带领下全力备战&#xff0c;在“黑色星期五”期间推出各类大促活动&#xff0c;奋力冲刺20…

Unknown custom element: <el-image>无法使用该组件,升级element-ui版本后项目报错

需求背景&#xff1a; 项目中需要使用图片点击放大&#xff0c;想要使用<el-image>组件&#xff0c;引入后报了下面的错&#xff0c;需要升级element版本&#xff0c;element-ui版本过低&#xff0c;没有该组件。 过程&#xff1a; cnpm i element-ui2.14.1 --save-dev…

clickhouse远程访问Oracle 11g数据库(clickhouse-jdbc-bridge)

1、简介 clickhouse-jdbc-bridge&#xff1a;是clickhouse提供的一个jdbc组件&#xff0c;用于通过JDBC的方式远程访问其他数据库表。 2、安装 &#xff08;1&#xff09;下载源文件并打包获取clickhouse-jdbc-bridge-2.0.7-shaded.jar 在官网&#xff1a;https://github.c…

tictoc例子理解10-13

tictoc10-13tictoc 10 几个模块连接&#xff0c;发送消息直到模块3收到消息tictoc 11 新增信道定义tictoc 12 双向连接信息简化定义tictoc 10 几个模块连接&#xff0c;发送消息直到模块3收到消息 让我们用几个(n)’ tic’模块让它更有趣&#xff0c;并将每个模块连接到其他模…

ANR 触发、监控、分析 一网打尽

平时看博客或者学知识&#xff0c;学到的东西比较零散&#xff0c;没有独立的知识模块概念&#xff0c;而且学了之后很容易忘。于是我建立了一个自己的笔记仓库 (一个我长期维护的笔记仓库&#xff0c;感兴趣的可以点个star~你的star是我写作的巨大大大大的动力)&#xff0c;将…