拼图小游戏

游戏整体介绍

Java 的 GUI 有两套体系, 分别是定义在 AWT 包下和定义在 Swing 包下的.

AWT 包较早出现, 有一点兼容性的问题, 比如显示中文可能会乱码等.

Java 语言主要是做后端服务器开发的.

什么是服务器? 以看小说为例, 只有通过网络, 才能把小说从服务器传递到浏览器中, 并在浏览器中展示出来. 很多情况下, 前端只是做内容的展示, 具体的业务逻辑, 比如注册, 登录等, 都是服务器做的.

在一个项目中, 用户是接触不到服务器的, 只能接触到客户端. 这个客户端, 可能是一个浏览器, 也可能是用 C/C++ 写的一个客户端的安装包, 用户下载安装包进行安装, 要么是手机上的一个 app.

客户端是和用户直接接触的.

Java 所做的后端开发是运行在服务器上的, 很少和用户直接接触. 因此, Java 中的 GUI 很少被使用.


图1 游戏主界面

主界面可以分为三个部分, 三个部分有各自的作用.

第一部分叫做 JFrame, JFrame 由两个单词组成, J 指的是 Java, Frame 表示窗体, 界面. 后续代码里所写的文字, 图片等等都需要放到这个窗体中进行展示. 所以写代码时最先写的就是这个窗体.


图2 第一部分 JFrame

第二部分叫做菜单, 菜单的名字叫做 JMenuBar, J 表示 Java, Menu 表示菜单, Bar 表示栏目, 条目.


图3 第二部分 菜单

第三部分叫做 JLabel. JLabel 不是图片, 而是一个管理容器, 可以管理文字和图片.


图4 第三部分 JLabel

对于左上角计算步数这里, 是一个文字, 需要先创建一个 JLabel 管理容器的对象, 再把文字放进去, 最后再把这个 JLabel 容器对象放到整个窗体当中.

图片也是同理, 先创建一个 JLabel 管理容器对象, 再把图片放进去, 最后再把这个 JLabel 容器对象放到整个窗体当中.

JFrame, JMenuBar 和 JLabel 有一个统一的称呼, 叫做组件. 包括图片, 按钮, 文字, 进度条等等都是组件. 组件是一个统称.

创建窗体

窗体的主体部分

需求:

游戏主界面: 宽度为 603px, 高度为 680px

登录界面: 宽度为 488px, 高度为 430px

注册界面: 宽度为 488px, 高度为 500px

创建一个 JFrame 对象就出现了一个窗体, 需要三个窗体就创建三个 JFrame 对象, 而且还可以通过 set 方法给不同的窗体设置不同的大小.

放置三个窗体的代码的路径:


图1 代码路径

JFrame 是 Java 中的一个类, 且是一个 Javabean 类, 就是用来描述界面的. 既然是 Javabean 类, 就包含了一些属性和行为, 属性有宽度, 高度等, 行为有设置尺寸, 设置是否可见等.

新建一个测试类, 在测试类中创建三个窗体.


图2 测试类

在测试类中创建三个窗体, 测试类代码:

import javax.swing.*;
public class Test {
public static void main(String[] args) {
// 创建主界面
JFrame gameJframe = new JFrame();
// 给界面设置尺寸, 单位是像素
gameJframe.setSize(603, 680);
// 到此处如果运行这个测试类, 程序是可以运行的, 但是看不见这个窗体, 是因为窗体默认是隐藏的, 需要调用 setVisible 方法将其展示出来
gameJframe.setVisible(true);
// 此时运行这个测试类, 窗体就出现了, 但是每一次运行程序, 窗体都出现在屏幕的左上角
// 创建登录界面
JFrame loginJframe = new JFrame();
loginJframe.setSize(488, 430);
loginJframe.setVisible(true);
// 创建注册界面
JFrame registerJframe = new JFrame();
registerJframe.setSize(488, 500);
registerJframe.setVisible(true);
// 此时运行这个测试类, 将出现三个窗体, 全部出现在屏幕的左上角
}
}

阅读 JFrame 的源码:

宽和高是各种组件的共有属性, 所以不是在 JFrame 里面定义的, 而是在其父类中定义的.


图3

图4

图5

图6

图7

Componet 是顶层的父类, x, y 表示当前组件的位置, width 和 height 表示当前组件的宽度和高度.

setVisible() 方法的源码:


图8

直接进入了 Window.java 文件中:


图9

从父类的 setVisible() 方法进去:


图10

图11

show() 方法根据传递进来的布尔值判断是展示窗体还是隐藏窗体.

if 里面的 show() 方法:


图12 if 里面的 show() 方法

else 里面的 hide() 方法:


图13 else 里面的 hide() 方法

在代码中, 可以用窗体对象直接调用图 12 的 show() 方法, 效果和给图 11 的 show() 方法传递 true 的效果是一样的.

程序示例:

import javax.swing.*;
public class Test {
public static void main(String[] args) {
// 创建主界面
JFrame gameJframe = new JFrame();
// 给界面设置尺寸, 单位是像素
gameJframe.setSize(603, 680);
// 到此处如果运行这个测试类, 程序是可以运行的, 但是看不见这个窗体, 是因为窗体默认是隐藏的, 需要调用 setVisible 方法将其展示出来
// gameJframe.setVisible(true);
// 窗体对象直接调用 show() 方法, 效果和 gameJframe.setVisible(true); 一致
gameJframe.show();
// 此时运行这个测试类, 窗体就出现了, 但是每一次运行程序, 窗体都出现在屏幕的左上角
/*// 创建登录界面
JFrame loginJframe = new JFrame();
loginJframe.setSize(488, 430);
loginJframe.setVisible(true);
// 创建注册界面
JFrame registerJframe = new JFrame();
registerJframe.setSize(488, 500);
registerJframe.setVisible(true);
// 此时运行这个测试类, 将出现三个窗体, 全部出现在屏幕的左上角*/
}
}

从源码中可以看出, 直接调用 show() 的方式是废弃的, 被 setVisible() 代替了.

所以 Test 的代码还是改为:

import javax.swing.*;
public class Test {
public static void main(String[] args) {
// 创建主界面
JFrame gameJframe = new JFrame();
// 给界面设置尺寸, 单位是像素
gameJframe.setSize(603, 680);
// 到此处如果运行这个测试类, 程序是可以运行的, 但是看不见这个窗体, 是因为窗体默认是隐藏的, 需要调用 setVisible 方法将其展示出来
gameJframe.setVisible(true);
// 此时运行这个测试类, 窗体就出现了, 但是每一次运行程序, 窗体都出现在屏幕的左上角
// 创建登录界面
JFrame loginJframe = new JFrame();
loginJframe.setSize(488, 430);
loginJframe.setVisible(true);
// 创建注册界面
JFrame registerJframe = new JFrame();
registerJframe.setSize(488, 500);
registerJframe.setVisible(true);
// 此时运行这个测试类, 将出现三个窗体, 全部出现在屏幕的左上角
}
}

但是不建议将所有的代码都写着 main 方法里面, 因为在以后的实际开发中, main 方法是作为程序的启动入口的, 里面是不会写什么业务逻辑代码的.

这里新建三个类 GameJFrame, LoginJFrame, RegisterJFrame, 分别创建游戏主界面窗体, 登录窗体和注册窗体, 这三个类都继承 JFrame 类, JFrame 类表示窗体, 那么这三个类当然也都表示窗体.

再新建一个类 App, 作为游戏的主入口.


图14

GameJFrame 类的代码:

package ui;
import javax.swing.*;
public class GameJFrame extends JFrame {
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
setSize(603, 680);
setVisible(true);
}
}

LoginJFrame 类的代码:

package ui;
import javax.swing.*;
public class LoginJFrame extends JFrame {
// 表示登录的窗体, 和登录相关的代码, 都放到这里
public LoginJFrame() {
setSize(488, 430);
setVisible(true);
}
}

RegisterJFrame 类的代码:

package ui;
import javax.swing.*;
public class RegisterJFrame extends JFrame {
// 表示注册的窗体, 和注册相关的代码, 都放到这里
public RegisterJFrame() {
setSize(488, 500);
setVisible(true);
}
}

App 类的代码:

import ui.GameJFrame;
import ui.LoginJFrame;
import ui.RegisterJFrame;
public class App {
public static void main(String[] args) {
// 表示程序的启动入口
// 如果想要开启一个窗体, 就创建该窗体的对象, 创建窗体对象时, 调用构造方法, 设置该窗体的尺寸和可见性
new GameJFrame();
new LoginJFrame();
new RegisterJFrame();
// 此时如果执行这个 main 方法, 则创建三个窗体
}
}

此时, 这个 Test 类就没有作用了, 可以直接删除了.


图15 删除 Test 类

图16 删除 Test 类

现在的代码目录:


图17

窗体的细节设置

接下来, 需要给窗体增加最左上角的标题, 并让三个窗体出现时能出现在屏幕的最前方, 且默认出现在屏幕的正中央.

修改三个窗体的代码:

GameJFrame 类的代码:

package ui;
import javax.swing.*;
public class GameJFrame extends JFrame {
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
}

LoginJFrame 类的代码:

package ui;
import javax.swing.*;
public class LoginJFrame extends JFrame {
// 表示登录的窗体, 和登录相关的代码, 都放到这里
public LoginJFrame() {
// 给窗体设置宽高
setSize(488, 430);
// 设置窗体的标题
setTitle("拼图 登录");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
}

RegisterJFrame 类的代码:

package ui;
import javax.swing.*;
public class RegisterJFrame extends JFrame {
// 表示注册的窗体, 和注册相关的代码, 都放到这里
public RegisterJFrame() {
// 给窗体设置宽高
setSize(488, 500);
// 设置窗体的标题
setTitle("拼图 注册");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
}

图18

菜单制作

Java 中有一个类, 是专门用来描述菜单的, 这个类叫做 JMenuBar. JMenuBar 表示整个菜单, 是一个长条区域. 这一个长条区域有很多个选项, 每一个选项都对应一个 JMenu 的类.

菜单就是下图红色边框的内容.


图19 菜单

菜单的组成:


图20 菜单的组成

在菜单中有: JMenuBar, JMenu, JMenuItem三个角色.

JMenuBar: 如上图中红色边框.

JMenu: 如上图蓝色边框.

JMenuItem: 如上图绿色字体处.

其中 JMenuBar 是整体, 一个界面中一般只有一个 JMenuBar.

而 JMenu 是菜单中的选项, 可以有多个.

JMenuItem 是选项下面的条目, 也可以有多个.

代码书写步骤:

  1. 创建 JMenuBar 对象

  2. 创建 JMenu 对象

  3. 创建 JMenuItem 对象

  4. 把 JMenuItem 添加到 JMenu 中

  5. 把 JMenu 添加到 JMenuBar 中

  6. 把整个 JMenuBar 设置到整个界面中


图21 把 JMenuItem 添加到 JMenu 中

图22 把 JMenu 添加到 JMenuBar 中

菜单中更换图片较为复杂, 先暂时不写.

GameJFrame.java 的代码:

package ui;
import javax.swing.*;
public class GameJFrame extends JFrame {
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 初始化菜单
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
}

此时运行 App.java, 得到的游戏主窗体:


图23 添加了菜单的游戏主窗体

此时可以发现, GameJFrame.java 的构造方法里面的代码实在是太多了, 显然需要优化下, 将构造方法中不同功能的代码进行抽取, 抽取成不同功能的代码, 在构造方法中进行调用.

抽取的快捷键: Ctrl+Alt+m

优化之后的 GameJFrame.java:

package ui;
import javax.swing.*;
public class GameJFrame extends JFrame {
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}
}

添加图片

初步添加图片


图1

这是由 15 张图片和一些空白组成的.


图2

移动图片时, 移动的就是这些小图片.

每一张小图片对应的类叫做 ImageIcon, 所以一共有 15 个 ImageIcon 对象. 需要给这些对象进行宽高, 位置, 边框等设置, 因此需要将表示图片的 ImageIcon 对象先放到 JLabel 当中, JLabel 就是一个管理者, 可以管理两样东西, 一个是图片, 另一个是文字. 当 JLabel 管理图片时, 就可以设置图片的宽高, 位置和边框.

代码的写法: 先创建 ImageIcon 对象, 给定想要展示的图片在电脑中的路径, 再创建 JLabel 对象, 将 ImageIcon 对象交给 JLabel 对象, 再将 JLabel 对象放到游戏主窗体中, 默认是展示在窗体的正中央.

图片资源:


图3 图片资源

当不知道怎么用这些 Java 内置的类去创建对象时, 可以去看这些类的源码中的构造方法.

把图片路径复制到代码中时, 如果先复制粘贴路径, 再加双引号, 则路径为 \ 分割, 如果先写好双引号再将路径复制粘贴进双引号, 则路径用 \\ 分割. 显然, 我们需要的是 \\.


图4 先复制粘贴路径, 再加双引号

图5 先写好双引号再将路径复制粘贴进双引号

此时的 GameJFrame.java 的代码:

package ui;
import javax.swing.*;
public class GameJFrame extends JFrame {
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化图片
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化图片
private void initImage() {
// 创建 ImageIcon 对象
ImageIcon icon = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\8.jpg");
// 创建 JLabel 对象, 传递的参数是 icon, 表示这个管理容器就管理 icon
JLabel label = new JLabel(icon);
// 将 JLabel 对象放到窗体中, 默认放在窗体的正中央
add(label);
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}
}

运行 App.java 的效果:


图6 添加了一张图片

给一张图片添加坐标

此时图片处于窗体的正中央, 如果添加多张图片, 则后添加的图片会覆盖先添加的图片, 所以需要给图片添加坐标.


图7 坐标

界面左上角的点可以看做是坐标的原点, 横向的是 X 轴, 纵向的是 Y 轴. 图片的位置取决于图片左上角的点, 在坐标中的位置. 如果是 (0 , 0) 那么该图片会显示再屏幕的左上角.

可以在这里查看图片的尺寸信息:


图8

这里准备的图片素材都是 105×105 像素的.

目前的主窗体可以看作三个部分, 分别是标题区域, 菜单部分和菜单下面那个一大块区域.


图9 窗体分三个部分

JFrame 只是一个架子, 第三部分才是用来装载所有的组件 (包括图片, 文字, 按钮, 进度条等等) 的.

可以用方法 窗体.getContentPane() 获取这个第三部分, 再把显示的组件交给这个第三部分 (而不是整个 JFrame 窗体), 如果不给定位置, 则默认显示在这个第三部分的中间位置, 所以需要在初始化这个窗体的时候就将这个机制取消掉, 使用的语句是 setLayout(null), 如果不取消, 则指定的 x 和 y 坐标不生效.

这个第三部分是一个隐藏的容器, 不需要我们自己去创建, 而是在我们创建了 JFrame 对象 (也就是一个容器) 时自动创建的.

给定图片位置之后的 GameJFrame.java 代码:

package ui;
import javax.swing.*;
public class GameJFrame extends JFrame {
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化图片
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化图片
private void initImage() {
// 创建 ImageIcon 对象
ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
JLabel label = new JLabel(icon1);
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
label.setBounds(0, 0, 105, 105);
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
}
}

看一下 JFrame.java 的源码:


图10

图11

可以看到, 在创建 JFrame 对象时, Java 底层也执行了很多的初始化的操作.

setRootPane: 设置隐藏容器.

createRootPane: 创建一个隐藏容器.

这里的 Root 容器就是那个第三部分隐藏容器, Pane 是这个容器的名字, pane 的英文意思是窗玻璃.


图12

279 行创建了一个隐藏容器对象, 所以这个对象不需要我们自己创建, 只需要会获取就行.

运行 App.java 的效果:


图13 添加一张图片并指定坐标

添加其他图片

GameJFrame.java 的代码:

package ui;
import javax.swing.*;
public class GameJFrame extends JFrame {
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化图片
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化图片
private void initImage() {
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
int number = 1;
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\" + number + ".jpg"));
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
label.setBounds(105 * j, 105 * i, 105, 105);
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
// 添加完成后, number 自增, 表示下一次添加下一张照片
number++;
}
}
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
}
}

运行 App.java 的效果:


图14 添加了全部的图片

打乱图片顺序

每一张图片都对应 1~15 之间的数字, 空白处为 0, 打乱图片实际上就是把数字打乱, 添加图片的时候按照打乱的图片添加即可.

先将 0 ~ 15 这 16 个数字放进一个数组中, 再将数组打乱, 加载图片时, 就从这个打乱之后的数组中取数字.

但是数字每四个为一行, 所以在加载一个数字即一个图片时需要判断该数字处于第几行, 比较麻烦.

所以可以将这 16 个数字放到一个二维数组中, 就不用判断数字处于第几行了, 而且在交换图片位置也就是交换数字位置时也会比一维数组方便得多.


图1

打乱数组的小练习:

写法一:

点击查看代码
package test;
import java.util.Random;
public class Test1 {
public static void main(String[] args) {
// 需求:
// int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
// 打乱一维数组中的数据, 并按照 4 个一组的方式添加到二维数组中.
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 打印打乱之后的一维数组
for (int i = 0; i < tempArr.length; i++) {
System.out.print(tempArr[i] + " ");
}
System.out.println();
// 创建一个二维数组
int[][] arr = new int[tempArr.length / 4][tempArr.length / 4];
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
arr[i / 4][i % 4] = tempArr[i];
}
// 打印二维数组
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
}
}

写法二:

点击查看代码
package test;
import java.util.Random;
public class Test2 {
public static void main(String[] args) {
// 需求:
// int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
// 打乱一维数组中的数据, 并按照 4 个一组的方式添加到二维数组中.
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 打印打乱之后的一维数组
for (int i = 0; i < tempArr.length; i++) {
System.out.print(tempArr[i] + " ");
}
System.out.println();
// 创建一个二维数组
int[][] arr = new int[tempArr.length / 4][tempArr.length / 4];
// 给二维数组添加数据
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
arr[i][j] = tempArr[i * 4 + j];
}
}
// 打印二维数组
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
}
}

写法三:

点击查看代码
package test;
import java.util.Random;
public class Test3 {
public static void main(String[] args) {
// 需求:
// int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
// 打乱一维数组中的数据, 并按照 4 个一组的方式添加到二维数组中.
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 打印打乱之后的一维数组
for (int i = 0; i < tempArr.length; i++) {
System.out.print(tempArr[i] + " ");
}
System.out.println();
// 创建一个二维数组
int[][] arr = new int[tempArr.length / 4][tempArr.length / 4];
// 给二维数组添加数据
int index = 0;
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
arr[i][j] = tempArr[index];
index++;
}
}
// 打印二维数组
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
}
}

下面开始改写 GameJFrame.java 的代码, 在初始化菜单和初始化图片之间, 打乱图片的顺序, 然后初始化图片的时候, 就是根据打乱之后的结果去加载图片.

GameJFrame.java 的代码:

package ui;
import javax.swing.*;
import java.util.Random;
public class GameJFrame extends JFrame {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\" + number + ".jpg"));
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
label.setBounds(105 * j, 105 * i, 105, 105);
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
}
}

此时, 执行 App.java 的效果:


图2 图片顺序被打乱

事件

介绍

事件: 可以被组件识别的操作.


图1

在事件中, 有三个核心要素:

事件源: 要对哪些组件进行操作, 比如按钮, 图片和窗体等等.

事件: 对事件进行的某些操作. 比如: 鼠标单击, 鼠标划入, 键盘键入等等.

绑定监听: 当事件源发生了某个事件, 则执行某段代码. 比如, 当登录按钮被点击, 则校验用户名和密码, 也就是执行校验用户名和密码的那部分代码.

给组件绑定的监听常见的有: KeyListener, MouseListener, ActionListener.

KeyListener: 键盘监听, 对组件用键盘进行了操作, 比如很多软件的快捷键.

MouseListener: 鼠标监听, 用鼠标对组件进行了操作.

ActionListener: 动作监听, 可以看作是键盘监听和鼠标监听的精简版. 键盘监听能监听键盘的所有的按键, 鼠标监听可以监听鼠标按下不松, 松开, 点击等等事件, 可以把单击分解为很多步骤.
动作监听是一个精简版, 在监听鼠标时, 只能监听左键点击, 监听键盘时只能监听空格.

按钮对象使用 JButton 类.


图2

图3

动作监听

一个动作监听的示例:

package test;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class Test4 {
public static void main(String[] args) {
JFrame jf = new JFrame();
jf.setSize(300, 300);
jf.setTitle("事件演示");
jf.setAlwaysOnTop(true);
jf.setLocationRelativeTo(null);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setLayout(null);
// 创建一个按钮对象, 对按钮对象添加事件
JButton jButton = new JButton("一个按钮");
jButton.setBounds(10, 10, 100, 30);
// 给按钮添加动作监听, 使用 addActionListener, 动作监听只能监听鼠标左键点击和键盘的空格
// 可以使用匿名内部类, 也可以自己写一个类实现 ActionListener 接口, 然后这里创建该类的对象作为参数传递进来
jButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("点击了按钮");
}
});
// 把按钮添加到整个 JFrame 窗体中
jf.getContentPane().add(jButton);
jf.setVisible(true);
}
}

执行该文件, 效果:


图4

每一次鼠标左键点击这个按钮或者按下键盘的空格键, IDEA 的控制台都可以输出: 点击了按钮.

实际业务中, 每一个按钮的业务逻辑是不一样的, 对于传递给 addActionListener 的对象, 每一次都定义一个类再去 new 一个这个类的对象的话, 这个类往往只能被使用一次, 因此更建议使用匿名内部类.

另一个示例:

思路: 在 MyJFrame 类中, 继承了 JFrame 类, 并实现接口 ActionListener, 重写了接口的方法 actionPerformed, 在给 addActionListener 方法传递参数 (一个对象) 时, 就传递当前对象, 然后会自动执行当前对象的 actionPerformed 方法.

Javabean 类:

package test;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;
public class MyJFrame extends JFrame implements ActionListener {
// 创建一个按钮对象, 对按钮对象添加事件
JButton jButton1 = new JButton("一个按钮");
// 添加第二个按钮
JButton jButton2 = new JButton("第二个按钮");
public MyJFrame() {
setSize(400, 300);
setTitle("事件演示");
setAlwaysOnTop(true);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(null);
jButton1.setBounds(10, 10, 100, 30);
// 给按钮添加动作监听
jButton1.addActionListener(this);
jButton2.setBounds(10, 200, 120, 40);
jButton2.addActionListener(this);
// 把按钮添加到整个 JFrame 窗体中
getContentPane().add(jButton1);
getContentPane().add(jButton2);
setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
// 对当前按钮进行判断
// 获取当前被操作的那个按钮对象
Object source = e.getSource();
if (source == jButton1) {
jButton1.setSize(90, 20);
} else if (source == jButton2) {
Random rand = new Random();
jButton2.setLocation(rand.nextInt(300), rand.nextInt(200));
} else {
System.out.println("Something went wrong");
}
}
}

测试类:

package test;
public class Test5 {
public static void main(String[] args) {
MyJFrame frame = new MyJFrame();
}
}

执行这个测试类的效果:


图5

鼠标监听

把鼠标从按钮之外放到按钮之上, 并不点击: 划入动作.

点击鼠标但是没有松开: 按下动作.

松开鼠标: 松开动作.

把鼠标从按钮上面挪动到按钮的外面: 划出动作.

按下和松开被归为了一组, 叫做单击动作.

如果针对按钮的每一个动作, 都要有不同的回调函数, 那就只能用鼠标监听机制, 因为动作监听只能监听到鼠标的左键单击动作和键盘的空格.

以后, 如果想要监听按钮的单击事件, 有三种写法:

方法一: 动作监听.

方法二: 鼠标监听中的单击事件.

方法三: 鼠标监听中的松开事件.


图6

图7

图8

有五个方法, 代表了五个动作.

程序示例:

Javabean 类:

package test;
import javax.swing.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
public class MyJFrame2 extends JFrame implements MouseListener {
JButton button = new JButton("button");
public MyJFrame2() {
setSize(400, 300);
setTitle("鼠标事件监听");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setAlwaysOnTop(true);
setLayout(null);
setLocationRelativeTo(null);
button.setBounds(10, 20, 100, 30);
getContentPane().add(button);
// 给按钮绑定鼠标监听事件
button.addMouseListener(this);
setVisible(true);
}
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("单击");
}
@Override
public void mousePressed(MouseEvent e) {
System.out.println("按下不松");
}
@Override
public void mouseReleased(MouseEvent e) {
System.out.println("松开");
}
@Override
public void mouseEntered(MouseEvent e) {
System.out.println("鼠标划入");
}
@Override
public void mouseExited(MouseEvent e) {
System.out.println("鼠标划出");
}
}

测试类:

package test;
public class Test6 {
public static void main(String[] args) {
MyJFrame2 frame = new MyJFrame2();
}
}

执行这个测试类的效果:


图9

可以看见, 当触发了松开事件之后, 还会触发单击事件.

键盘监听


图10

第三个方法用到较少, 有局限性, 有些按键无法被监听, 比如 Ctrl, Alt 等.


图11

图12

程序示例:

Javabean 类:

package test;
import javax.swing.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public class MyJFrame3 extends JFrame implements KeyListener {
public MyJFrame3() {
setTitle("MyJFrame3");
setSize(400, 300);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setLayout(null);
setAlwaysOnTop(true);
// 给整个窗体添加键盘监听
addKeyListener(this);
setVisible(true);
}
@Override
public void keyTyped(KeyEvent e) {
}
// 如果按下键盘上的一个按键没有松开, 那么会重复调用这个 keyPressed 方法, 当松开时, 会调用一次 keyReleased 方法
// 参数 keyEvent 表示动作对象
@Override
public void keyPressed(KeyEvent e) {
System.out.println("keyPressed: " + e.getKeyChar());
}
// 参数 keyEvent 表示动作对象
@Override
public void keyReleased(KeyEvent e) {
System.out.println("keyReleased: " + e.getKeyChar());
// 键盘上的每一个按键都有一个编号与之对应
// e.getKeyCode() 可以获取键盘上每一个按键的编号, 返回值是 int 类型的, 这个值和 ASCII 码表没有什么关系
System.out.println("e.getKeuCode(): " + e.getKeyCode());
}
}

测试类:

package test;
public class Test7 {
public static void main(String[] args) {
MyJFrame3 frame = new MyJFrame3();
}
}

执行测试类的效果:


图13

可以看见, e.getKeyChar() 方法只能输出部分按键, 最后按下的是 shift 和 空格, 但是 shift 打印的是乱码, 空格打印出来后啥也看不见.

再来看一个长按不松的效果:


图14

美化界面

界面搭建好之后, 就需要美化界面了, 本次需要美化下面四个地方:

  1. 将 15 张小图片移动到界面的中央偏下方

  2. 添加背景图片

  3. 添加图片的边框

  4. 优化路径

将 15 张小图片移动到界面的中央偏下方

现在小图片是在窗体的左上角开始出现然后依次排列的, 需要将全部小图片都向右, 向下移动.

方法: 在初始化图片时, 给 x 方向和 y 方向都加上偏移量.

改写之后的 GameJFrame.java 文件:

package ui;
import javax.swing.*;
import java.util.Random;
public class GameJFrame extends JFrame {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\" + number + ".jpg"));
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
}
}

效果:

原来的样子:


图1 原来的样子

现在的样子:


图2 现在的样子

添加背景图片

资源文件里的背景图片:


图3 资源文件里的背景图片

图4 背景图片

改写之后的 GameJFrame.java 文件:

package ui;
import javax.swing.*;
import java.util.Random;
public class GameJFrame extends JFrame {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\" + number + ".jpg"));
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
}
}

现在执行 App.java 的效果:


图5 添加了背景图的效果

添加图片的边框

这个业务的实现代码要放到初始化图片的方法里面.

优化之后的 GameJFrame.java 代码:

package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.util.Random;
public class GameJFrame extends JFrame {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\" + number + ".jpg"));
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
}
}

执行 App.java 的效果:


图6 给图片添加了边框的效果

优化路径

现在的 GameJFrame.java 中使用的路径是完整路径, 也就是绝对路径, 这样会有两个坏处:

  1. 路径太长, 代码阅读不方便.

  2. 项目拿到别人电脑上时, 如果别人电脑上没有对应的文件夹, 就找不到图片.

计算机中的两种路径:

  • 绝对路径

    从盘符开始的路径, 此时路径是固定的.

C:\\a.txt
  • 相对路径

    没有从盘符开始的路径

aaa\\bbb\\a.txt

目前为止, 在 IDEA 中, 相对路径是相对当前项目而言的.

以下面的路径为例:

aaa\\bbb\\a.txt

在寻找的时候, 先找当前项目, 在当前项目下找 aaa, 在 aaa 里面找 bbb, 在 bbb 里面找 a.txt

利用这个原理, 我们可以修改项目中路径的书写.


图7 优化之后的路径

修改之后的 GameJFrame.java 文件:

package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.util.Random;
public class GameJFrame extends JFrame {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon("jigsaw\\image\\animal\\animal1\\" + number + ".jpg"));
// JLabel label = new JLabel(new ImageIcon("..\\image\\animal\\animal1\\" + number + ".jpg")); // 不行, 找不到
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
}
}

移动图片

上下左右移动时的业务逻辑为:

  • 上移: 是把空白区域下方的图片上移.

  • 下移: 是把空白区域上方的图片下移.

  • 左移: 是把空白区域右方的图片左移.

  • 右移: 是把空白区域左方的图片右移.

移动时要注意 (下面的 x 和 y 表示空白区域在二维数组中的坐标):

  • 如果空白区域已经在最上面了, 此时 x = 0, 那么就无法再下移了.

  • 如果空白区域已经在最下面了, 此时 x = 3, 那么就无法再上移了.

  • 如果空白区域已经在最左侧了, 此时 y = 0, 那么就无法再右移了.

  • 如果空白区域已经在最右侧了, 此时 y = 3, 那么就无法再左移了.

空白区域的图片的编号为 0.

还需要给 GameJFrame.java 这个类的对象, 也就是游戏主窗体, 添加一个键盘监听事件, 用来监听键盘的上下左右键的动作. 方法:

  1. GameJFrame.java 这个类实现 KeyListener 接口, 并重写所有抽象方法.

  2. 给整个窗体添加键盘监听事件 (在 initJFrame 方法中实现).

然后再移动图片, 方法:

  1. 计算出空白方块对应的数字 0 在二维数组中的位置 (在 initData 方法中实现).

  2. 在 keyReleased 方法当中实现移动的逻辑.


图1 图片和二维数组的对应关系

改写之后的 GameJFrame.java 代码:

package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameJFrame extends JFrame implements KeyListener {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 定义两个变量, 记录 0 号图片在二维数组中的位置
int x = 0;
int y = 0;
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0) {
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 清空原本已经被加载出来的所有图片
getContentPane().removeAll();
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon("jigsaw\\image\\animal\\animal1\\" + number + ".jpg"));
// JLabel label = new JLabel(new ImageIcon("..\\image\\animal\\animal1\\" + number + ".jpg")); // 不行, 找不到
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新一下
// 可能的原因:
// (确保图片在初始化之后立即显示在界面上, 如果不刷新则图片会一个一个地显示出来, 显得比较卡, 刷新的话则是一口气全部加载全部 15 张图片)
// 实际测试: 如果不刷新则不生效, 按下键盘上的方向键后不生效
getContentPane().repaint();
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
// 给整个窗体添加键盘监听事件
addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
// 根据 keyCode 对上下左右进行判断
// 左: 37, 上: 38, 右: 39, 下: 40
// 如果不知道这四个值是多少, 打印出来即可
int keyCode = e.getKeyCode();
// System.out.println(keyCode); // 打印出来就知道这四个方向按键对应的 keyCode 值了
if (keyCode == 37) {
System.out.println("向左移动");
if (y == 3) {
System.out.println("空白区域已经在最右边了, 无法向左移动");
return;
}
// 业务逻辑:
// 把空白方块右方的数字向左移动
// x , y 表示空白方块的位置
// x , y+1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 38) {
System.out.println("向上移动");
if (x == 3) {
System.out.println("空白区域已经在最下边了, 无法向上移动");
return;
}
// 业务逻辑:
// 把空白方块下方的数字向上移动
// x , y 表示空白方块的位置
// x+1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 39) {
System.out.println("向右移动");
if (y == 0) {
System.out.println("空白区域已经在最左边了, 无法向右移动");
return;
}
// 业务逻辑:
// 把空白方块左方的数字向右移动
// x , y 表示空白方块的位置
// x , y-1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 40) {
System.out.println("向下移动");
if (x == 0) {
System.out.println("空白区域已经在最上边了, 无法向下移动");
return;
}
// 业务逻辑:
// 把空白方块上方的数字向下移动
// x , y 表示空白方块的位置
// x-1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
}
// 运行 App.java 发现输出是正确的, 然后继续补充 if 分支内部的业务逻辑代码
}
}

效果:


图2 修复移动图片时数组越界的错误

查看完整图片

查看完整图片的功能

游戏刚开始启动时, 图片是随机打乱的, 在玩游戏的过程中, 想要看看完整的图片, 方法: 设置一个键, 比如 A, 按住 A 不松手时显示出完整的图片, 松开 A 后再重新显示打乱的图片.

修改后的 GameJFrame.java 的代码:

package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameJFrame extends JFrame implements KeyListener {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 定义两个变量, 记录 0 号图片在二维数组中的位置
int x = 0;
int y = 0;
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0) {
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 清空原本已经被加载出来的所有图片
getContentPane().removeAll();
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon("jigsaw\\image\\animal\\animal1\\" + number + ".jpg"));
// JLabel label = new JLabel(new ImageIcon("..\\image\\animal\\animal1\\" + number + ".jpg")); // 不行, 找不到
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新一下
// 可能的原因:
// (确保图片在初始化之后立即显示在界面上, 如果不刷新则图片会一个一个地显示出来, 显得比较卡, 刷新的话则是一口气全部加载全部 15 张图片)
// 实际测试: 如果不刷新则不生效, 按下键盘上的方向键后不生效
getContentPane().repaint();
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
// 给整个窗体添加键盘监听事件
addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {
}
// 长按 A 不松时显示完整图片, 松开后显示打乱的图片的逻辑要放到 keyReleased 方法中
@Override
public void keyPressed(KeyEvent e) {
System.out.println("长按 A 不松时显示完整图片");
int keyCode = e.getKeyCode();
if (keyCode == 65) {
// 清空现在已经加载出来的图片
getContentPane().removeAll();
JLabel jLabel = new JLabel(new ImageIcon("jigsaw\\image\\animal\\animal1\\all.jpg"));
jLabel.setBounds(83, 134, 420, 420);
// 将图片添加到窗体中
getContentPane().add(jLabel);
// 加载背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新界面
getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
// 根据 keyCode 对上下左右进行判断
// 左: 37, 上: 38, 右: 39, 下: 40
// 如果不知道这四个值是多少, 打印出来即可
int keyCode = e.getKeyCode();
// System.out.println(keyCode); // 打印出来就知道这四个方向按键对应的 keyCode 值了
if (keyCode == 37) {
System.out.println("向左移动");
if (y == 3) {
System.out.println("空白区域已经在最右边了, 无法向左移动");
return;
}
// 业务逻辑:
// 把空白方块右方的数字向左移动
// x , y 表示空白方块的位置
// x , y+1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 38) {
System.out.println("向上移动");
if (x == 3) {
System.out.println("空白区域已经在最下边了, 无法向上移动");
return;
}
// 业务逻辑:
// 把空白方块下方的数字向上移动
// x , y 表示空白方块的位置
// x+1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 39) {
System.out.println("向右移动");
if (y == 0) {
System.out.println("空白区域已经在最左边了, 无法向右移动");
return;
}
// 业务逻辑:
// 把空白方块左方的数字向右移动
// x , y 表示空白方块的位置
// x , y-1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 40) {
System.out.println("向下移动");
if (x == 0) {
System.out.println("空白区域已经在最上边了, 无法向下移动");
return;
}
// 业务逻辑:
// 把空白方块上方的数字向下移动
// x , y 表示空白方块的位置
// x-1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 65) {
System.out.println("松开 A 后显示打乱的图片");
initImage();
}
// 先在每一个分支内写类似于 System.out.println("向下移动"); 这样的输出语句,
// 然后运行 App.java 发现输出是正确的, 然后继续补充 if 分支内部的业务逻辑代码
}
}

效果:


图1 长按 A 显示完整图片

修改图片路径

目前只是写了一个路径下的图片, 没有办法更换到别的路径下.


图2

这三个路径下的图片应当是可以进行选择性加载的.

这里的修改只涉及加载的 15 张小图片和那一张完整的图片, 不涉及背景图片, 背景图片是固定的.

修改之后的 GameJFrame.java 代码:

package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameJFrame extends JFrame implements KeyListener {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 定义两个变量, 记录 0 号图片在二维数组中的位置
int x = 0;
int y = 0;
// 定义一个变量, 记录当前展示图片的路径, 以后想要修改路径时, 就只需要修改这个变量
String path = "jigsaw\\image\\animal\\animal1\\";
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0) {
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 清空原本已经被加载出来的所有图片
getContentPane().removeAll();
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon(path + number + ".jpg"));
// JLabel label = new JLabel(new ImageIcon("..\\image\\animal\\animal1\\" + number + ".jpg")); // 不行, 找不到
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新一下
// 可能的原因:
// (确保图片在初始化之后立即显示在界面上, 如果不刷新则图片会一个一个地显示出来, 显得比较卡, 刷新的话则是一口气全部加载全部 15 张图片)
// 实际测试: 如果不刷新则不生效, 按下键盘上的方向键后不生效
getContentPane().repaint();
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
// 给整个窗体添加键盘监听事件
addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {
}
// 长按 A 不松时显示完整图片, 松开后显示打乱的图片的逻辑要放到 keyReleased 方法中
@Override
public void keyPressed(KeyEvent e) {
System.out.println("长按 A 不松时显示完整图片");
int keyCode = e.getKeyCode();
if (keyCode == 65) {
// 清空现在已经加载出来的图片
getContentPane().removeAll();
JLabel jLabel = new JLabel(new ImageIcon(path + "all.jpg"));
jLabel.setBounds(83, 134, 420, 420);
// 将图片添加到窗体中
getContentPane().add(jLabel);
// 加载背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新界面
getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
// 根据 keyCode 对上下左右进行判断
// 左: 37, 上: 38, 右: 39, 下: 40
// 如果不知道这四个值是多少, 打印出来即可
int keyCode = e.getKeyCode();
// System.out.println(keyCode); // 打印出来就知道这四个方向按键对应的 keyCode 值了
if (keyCode == 37) {
System.out.println("向左移动");
if (y == 3) {
System.out.println("空白区域已经在最右边了, 无法向左移动");
return;
}
// 业务逻辑:
// 把空白方块右方的数字向左移动
// x , y 表示空白方块的位置
// x , y+1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 38) {
System.out.println("向上移动");
if (x == 3) {
System.out.println("空白区域已经在最下边了, 无法向上移动");
return;
}
// 业务逻辑:
// 把空白方块下方的数字向上移动
// x , y 表示空白方块的位置
// x+1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 39) {
System.out.println("向右移动");
if (y == 0) {
System.out.println("空白区域已经在最左边了, 无法向右移动");
return;
}
// 业务逻辑:
// 把空白方块左方的数字向右移动
// x , y 表示空白方块的位置
// x , y-1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 40) {
System.out.println("向下移动");
if (x == 0) {
System.out.println("空白区域已经在最上边了, 无法向下移动");
return;
}
// 业务逻辑:
// 把空白方块上方的数字向下移动
// x , y 表示空白方块的位置
// x-1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 65) {
System.out.println("松开 A 后显示打乱的图片");
initImage();
}
// 先在每一个分支内写类似于 System.out.println("向下移动"); 这样的输出语句,
// 然后运行 App.java 发现输出是正确的, 然后继续补充 if 分支内部的业务逻辑代码
}
}

作弊码

业务功能: 不想玩了, 想要一键通关.

实现步骤:

  1. 给整个界面添加键盘事件.

  2. 在 keyReleased 中书写松开的逻辑, 当按下 W 的时候一键通关.

修改之后的 GameJFrame.java 代码:

package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameJFrame extends JFrame implements KeyListener {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 定义两个变量, 记录 0 号图片在二维数组中的位置
int x = 0;
int y = 0;
// 定义一个变量, 记录当前展示图片的路径, 以后想要修改路径时, 就只需要修改这个变量
String path = "jigsaw\\image\\animal\\animal1\\";
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0) {
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 清空原本已经被加载出来的所有图片
getContentPane().removeAll();
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon(path + number + ".jpg"));
// JLabel label = new JLabel(new ImageIcon("..\\image\\animal\\animal1\\" + number + ".jpg")); // 不行, 找不到
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新一下
// 可能的原因:
// (确保图片在初始化之后立即显示在界面上, 如果不刷新则图片会一个一个地显示出来, 显得比较卡, 刷新的话则是一口气全部加载全部 15 张图片)
// 实际测试: 如果不刷新则不生效, 按下键盘上的方向键后不生效
getContentPane().repaint();
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
// 给整个窗体添加键盘监听事件
addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {
}
// 长按 A 不松时显示完整图片, 松开后显示打乱的图片的逻辑要放到 keyReleased 方法中
@Override
public void keyPressed(KeyEvent e) {
System.out.println("长按 A 不松时显示完整图片");
int keyCode = e.getKeyCode();
if (keyCode == 65) {
// 清空现在已经加载出来的图片
getContentPane().removeAll();
JLabel jLabel = new JLabel(new ImageIcon(path + "all.jpg"));
jLabel.setBounds(83, 134, 420, 420);
// 将图片添加到窗体中
getContentPane().add(jLabel);
// 加载背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新界面
getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
// 根据 keyCode 对上下左右进行判断
// 左: 37, 上: 38, 右: 39, 下: 40
// 如果不知道这四个值是多少, 打印出来即可
int keyCode = e.getKeyCode();
// System.out.println(keyCode); // 打印出来就知道这四个方向按键对应的 keyCode 值了
if (keyCode == 37) {
System.out.println("向左移动");
if (y == 3) {
System.out.println("空白区域已经在最右边了, 无法向左移动");
return;
}
// 业务逻辑:
// 把空白方块右方的数字向左移动
// x , y 表示空白方块的位置
// x , y+1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 38) {
System.out.println("向上移动");
if (x == 3) {
System.out.println("空白区域已经在最下边了, 无法向上移动");
return;
}
// 业务逻辑:
// 把空白方块下方的数字向上移动
// x , y 表示空白方块的位置
// x+1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 39) {
System.out.println("向右移动");
if (y == 0) {
System.out.println("空白区域已经在最左边了, 无法向右移动");
return;
}
// 业务逻辑:
// 把空白方块左方的数字向右移动
// x , y 表示空白方块的位置
// x , y-1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 40) {
System.out.println("向下移动");
if (x == 0) {
System.out.println("空白区域已经在最上边了, 无法向下移动");
return;
}
// 业务逻辑:
// 把空白方块上方的数字向下移动
// x , y 表示空白方块的位置
// x-1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 65) { // A 是 65
System.out.println("松开 A 后显示打乱的图片");
initImage();
} else if (keyCode == 87) { // W 是 87
data = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
initImage();
}
// 先在每一个分支内写类似于 System.out.println("向下移动"); 这样的输出语句,
// 然后运行 App.java 发现输出是正确的, 然后继续补充 if 分支内部的业务逻辑代码
}
}

判断胜利

当游戏的图标排列正确了, 需要有胜利图标显示.

每次上下左右移动图片的时候都需要进行判断.

在 keyReleased 中方法一开始的地方就需要写判断是否胜利.

实现步骤:

  1. 定义一个正确的二维数组 win.

  2. 在加载图片之前, 先判断一下二维数组中的数字跟 win 数组中是否相同.

  3. 如果相同展示正确图标.

  4. 如果不同则不展示正确图标.

改写之后的 GameJFrame.java 代码:

package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameJFrame extends JFrame implements KeyListener {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 定义两个变量, 记录 0 号图片在二维数组中的位置
int x = 0;
int y = 0;
// 定义一个二维数组, 存储正确的数据
int[][] win = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
// 定义一个变量, 记录当前展示图片的路径, 以后想要修改路径时, 就只需要修改这个变量
String path = "jigsaw\\image\\animal\\animal1\\";
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0) {
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 清空原本已经被加载出来的所有图片
getContentPane().removeAll();
if (victory()) {
// 显示胜利的图标
JLabel winJLabel = new JLabel(new ImageIcon("jigsaw\\image\\win.png"));
winJLabel.setBounds(203, 283, 197, 73);
getContentPane().add(winJLabel);
}
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon(path + number + ".jpg"));
// JLabel label = new JLabel(new ImageIcon("..\\image\\animal\\animal1\\" + number + ".jpg")); // 不行, 找不到
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新一下
// 可能的原因:
// (确保图片在初始化之后立即显示在界面上, 如果不刷新则图片会一个一个地显示出来, 显得比较卡, 刷新的话则是一口气全部加载全部 15 张图片)
// 实际测试: 如果不刷新则不生效, 按下键盘上的方向键后不生效
getContentPane().repaint();
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
// 给整个窗体添加键盘监听事件
addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {
}
// 长按 A 不松时显示完整图片, 松开后显示打乱的图片的逻辑要放到 keyReleased 方法中
@Override
public void keyPressed(KeyEvent e) {
System.out.println("长按 A 不松时显示完整图片");
int keyCode = e.getKeyCode();
if (keyCode == 65) {
// 清空现在已经加载出来的图片
getContentPane().removeAll();
JLabel jLabel = new JLabel(new ImageIcon(path + "all.jpg"));
jLabel.setBounds(83, 134, 420, 420);
// 将图片添加到窗体中
getContentPane().add(jLabel);
// 加载背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新界面
getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
// 判断游戏是否胜利, 如果胜利, 则此方法需要直接结束, 不能再执行下面的移动代码了
if (victory()) {
return;
}
// 根据 keyCode 对上下左右进行判断
// 左: 37, 上: 38, 右: 39, 下: 40
// 如果不知道这四个值是多少, 打印出来即可
int keyCode = e.getKeyCode();
// System.out.println(keyCode); // 打印出来就知道这四个方向按键对应的 keyCode 值了
if (keyCode == 37) {
System.out.println("向左移动");
if (y == 3) {
System.out.println("空白区域已经在最右边了, 无法向左移动");
return;
}
// 业务逻辑:
// 把空白方块右方的数字向左移动
// x , y 表示空白方块的位置
// x , y+1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 38) {
System.out.println("向上移动");
if (x == 3) {
System.out.println("空白区域已经在最下边了, 无法向上移动");
return;
}
// 业务逻辑:
// 把空白方块下方的数字向上移动
// x , y 表示空白方块的位置
// x+1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 39) {
System.out.println("向右移动");
if (y == 0) {
System.out.println("空白区域已经在最左边了, 无法向右移动");
return;
}
// 业务逻辑:
// 把空白方块左方的数字向右移动
// x , y 表示空白方块的位置
// x , y-1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 40) {
System.out.println("向下移动");
if (x == 0) {
System.out.println("空白区域已经在最上边了, 无法向下移动");
return;
}
// 业务逻辑:
// 把空白方块上方的数字向下移动
// x , y 表示空白方块的位置
// x-1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--; // 更新空白区域的位置
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 65) { // A 是 65
System.out.println("松开 A 后显示打乱的图片");
initImage();
} else if (keyCode == 87) { // W 是 87
data = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
initImage();
}
// 先在每一个分支内写类似于 System.out.println("向下移动"); 这样的输出语句,
// 然后运行 App.java 发现输出是正确的, 然后继续补充 if 分支内部的业务逻辑代码
}
// 定义一个方法, 比较当前的二维数组和正确的二维数组是否相同, 如果相同, 则表示胜利, 方法返回 true
// 当前的二维数组存储在 data 中, 正确的二维数组存储在 win 中
public boolean victory() {
for (int i = 0; i < data.length; i++) {
for (int j = 0; j < data[i].length; j++) {
if (data[i][j] != win[i][j]) {
return false;
}
}
}
return true;
}
}

效果:


图1

计步功能

左上角的计步器, 每移动一次, 计步器就需要自增一次.

实现步骤:

  1. 定义一个变量用来统计已经玩了多少步.

  2. 每次按上下左右的时候计步器自增一次即可.

改写之后的 GameJFrame.java 代码:

点击查看代码
package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameJFrame extends JFrame implements KeyListener {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 定义两个变量, 记录 0 号图片在二维数组中的位置
int x = 0;
int y = 0;
// 定义一个二维数组, 存储正确的数据
int[][] win = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
// 定义一个变量, 记录当前展示图片的路径, 以后想要修改路径时, 就只需要修改这个变量
String path = "jigsaw\\image\\animal\\animal1\\";
// 定义一个变量, 用来统计步数
int step = 0;
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0) {
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 清空原本已经被加载出来的所有图片
getContentPane().removeAll();
if (victory()) {
// 显示胜利的图标
JLabel winJLabel = new JLabel(new ImageIcon("jigsaw\\image\\win.png"));
winJLabel.setBounds(203, 283, 197, 73);
getContentPane().add(winJLabel);
}
JLabel stepCount = new JLabel("步数: " + step);
stepCount.setBounds(50, 20, 100, 20);
getContentPane().add(stepCount);
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon(path + number + ".jpg"));
// JLabel label = new JLabel(new ImageIcon("..\\image\\animal\\animal1\\" + number + ".jpg")); // 不行, 找不到
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新一下
// 可能的原因:
// (确保图片在初始化之后立即显示在界面上, 如果不刷新则图片会一个一个地显示出来, 显得比较卡, 刷新的话则是一口气全部加载全部 15 张图片)
// 实际测试: 如果不刷新则不生效, 按下键盘上的方向键后不生效
getContentPane().repaint();
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 创建选项下面的条目
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
// 给整个窗体添加键盘监听事件
addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {
}
// 长按 A 不松时显示完整图片, 松开后显示打乱的图片的逻辑要放到 keyReleased 方法中
@Override
public void keyPressed(KeyEvent e) {
System.out.println("长按 A 不松时显示完整图片");
int keyCode = e.getKeyCode();
if (keyCode == 65) {
// 清空现在已经加载出来的图片
getContentPane().removeAll();
JLabel jLabel = new JLabel(new ImageIcon(path + "all.jpg"));
jLabel.setBounds(83, 134, 420, 420);
// 将图片添加到窗体中
getContentPane().add(jLabel);
// 加载背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新界面
getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
// 判断游戏是否胜利, 如果胜利, 则此方法需要直接结束, 不能再执行下面的移动代码了
if (victory()) {
return;
}
// 根据 keyCode 对上下左右进行判断
// 左: 37, 上: 38, 右: 39, 下: 40
// 如果不知道这四个值是多少, 打印出来即可
int keyCode = e.getKeyCode();
// System.out.println(keyCode); // 打印出来就知道这四个方向按键对应的 keyCode 值了
if (keyCode == 37) {
System.out.println("向左移动");
if (y == 3) {
System.out.println("空白区域已经在最右边了, 无法向左移动");
return;
}
// 业务逻辑:
// 把空白方块右方的数字向左移动
// x , y 表示空白方块的位置
// x , y+1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 38) {
System.out.println("向上移动");
if (x == 3) {
System.out.println("空白区域已经在最下边了, 无法向上移动");
return;
}
// 业务逻辑:
// 把空白方块下方的数字向上移动
// x , y 表示空白方块的位置
// x+1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 39) {
System.out.println("向右移动");
if (y == 0) {
System.out.println("空白区域已经在最左边了, 无法向右移动");
return;
}
// 业务逻辑:
// 把空白方块左方的数字向右移动
// x , y 表示空白方块的位置
// x , y-1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 40) {
System.out.println("向下移动");
if (x == 0) {
System.out.println("空白区域已经在最上边了, 无法向下移动");
return;
}
// 业务逻辑:
// 把空白方块上方的数字向下移动
// x , y 表示空白方块的位置
// x-1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 65) { // A 是 65
System.out.println("松开 A 后显示打乱的图片");
initImage();
} else if (keyCode == 87) { // W 是 87
data = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
initImage();
}
// 先在每一个分支内写类似于 System.out.println("向下移动"); 这样的输出语句,
// 然后运行 App.java 发现输出是正确的, 然后继续补充 if 分支内部的业务逻辑代码
}
// 定义一个方法, 比较当前的二维数组和正确的二维数组是否相同, 如果相同, 则表示胜利, 方法返回 true
// 当前的二维数组存储在 data 中, 正确的二维数组存储在 win 中
public boolean victory() {
for (int i = 0; i < data.length; i++) {
for (int j = 0; j < data[i].length; j++) {
if (data[i][j] != win[i][j]) {
return false;
}
}
}
return true;
}
}

重新开始, 关闭游戏, 关于我们

重新开始

重新开始: 点击之后, 重新打乱图片, 计步器清零.


图1

实现步骤:

  1. 给菜单上的重新开始选项添加 ActionListener 事件监听.

  2. 在 actionPerformed 方法中实现对应的逻辑: 重新打乱二维数组中的数字, 计步器变量清零, 再加载图片.

关闭游戏

关闭游戏: 点击之后, 全部关闭.


图1

实现步骤:

  1. 给菜单上的关闭游戏选项添加 ActionListener 事件监听.

  2. 在 actionPerformed 方法中实现对应的逻辑: 结束虚拟机.

关于我们

关于我们: 点击之后出现黑马程序员的公众号二维码.


图1

图2

ImageIcon 对象交给 JLabel 对象来管理, 再将 JLabel 对象交给 JDialog.

改写之后的 GameJFrame.java 代码:

点击查看代码
package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameJFrame extends JFrame implements KeyListener, ActionListener {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 定义两个变量, 记录 0 号图片在二维数组中的位置
int x = 0;
int y = 0;
// 定义一个二维数组, 存储正确的数据
int[][] win = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
// 定义一个变量, 记录当前展示图片的路径, 以后想要修改路径时, 就只需要修改这个变量
String path = "jigsaw\\image\\animal\\animal1\\";
// 定义一个变量, 用来统计步数
int step = 0;
// 创建选项下面的条目, 重写的 actionPerformed 方法需要用到这四个条目, 所以要把这四个条目放到成员位置
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0) {
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 清空原本已经被加载出来的所有图片
getContentPane().removeAll();
if (victory()) {
// 显示胜利的图标
JLabel winJLabel = new JLabel(new ImageIcon("jigsaw\\image\\win.png"));
winJLabel.setBounds(203, 283, 197, 73);
getContentPane().add(winJLabel);
}
JLabel stepCount = new JLabel("步数: " + step);
stepCount.setBounds(50, 20, 100, 20);
getContentPane().add(stepCount);
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon(path + number + ".jpg"));
// JLabel label = new JLabel(new ImageIcon("..\\image\\animal\\animal1\\" + number + ".jpg")); // 不行, 找不到
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新一下
// 可能的原因:
// (确保图片在初始化之后立即显示在界面上, 如果不刷新则图片会一个一个地显示出来, 显得比较卡, 刷新的话则是一口气全部加载全部 15 张图片)
// 实际测试: 如果不刷新则不生效, 按下键盘上的方向键后不生效
getContentPane().repaint();
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的两个选项, 功能和关于我们
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
// 给条目绑定事件
replayItem.addActionListener(this);
reLoginItem.addActionListener(this);
closeItem.addActionListener(this);
accountItem.addActionListener(this);
// 将选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
// 给整个窗体添加键盘监听事件
addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {
}
// 长按 A 不松时显示完整图片, 松开后显示打乱的图片的逻辑要放到 keyReleased 方法中
@Override
public void keyPressed(KeyEvent e) {
System.out.println("长按 A 不松时显示完整图片");
int keyCode = e.getKeyCode();
if (keyCode == 65) {
// 清空现在已经加载出来的图片
getContentPane().removeAll();
JLabel jLabel = new JLabel(new ImageIcon(path + "all.jpg"));
jLabel.setBounds(83, 134, 420, 420);
// 将图片添加到窗体中
getContentPane().add(jLabel);
// 加载背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新界面
getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
// 判断游戏是否胜利, 如果胜利, 则此方法需要直接结束, 不能再执行下面的移动代码了
if (victory()) {
return;
}
// 根据 keyCode 对上下左右进行判断
// 左: 37, 上: 38, 右: 39, 下: 40
// 如果不知道这四个值是多少, 打印出来即可
int keyCode = e.getKeyCode();
// System.out.println(keyCode); // 打印出来就知道这四个方向按键对应的 keyCode 值了
if (keyCode == 37) {
System.out.println("向左移动");
if (y == 3) {
System.out.println("空白区域已经在最右边了, 无法向左移动");
return;
}
// 业务逻辑:
// 把空白方块右方的数字向左移动
// x , y 表示空白方块的位置
// x , y+1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 38) {
System.out.println("向上移动");
if (x == 3) {
System.out.println("空白区域已经在最下边了, 无法向上移动");
return;
}
// 业务逻辑:
// 把空白方块下方的数字向上移动
// x , y 表示空白方块的位置
// x+1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 39) {
System.out.println("向右移动");
if (y == 0) {
System.out.println("空白区域已经在最左边了, 无法向右移动");
return;
}
// 业务逻辑:
// 把空白方块左方的数字向右移动
// x , y 表示空白方块的位置
// x , y-1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 40) {
System.out.println("向下移动");
if (x == 0) {
System.out.println("空白区域已经在最上边了, 无法向下移动");
return;
}
// 业务逻辑:
// 把空白方块上方的数字向下移动
// x , y 表示空白方块的位置
// x-1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 65) { // A 是 65
System.out.println("松开 A 后显示打乱的图片");
initImage();
} else if (keyCode == 87) { // W 是 87
data = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
initImage();
}
// 先在每一个分支内写类似于 System.out.println("向下移动"); 这样的输出语句,
// 然后运行 App.java 发现输出是正确的, 然后继续补充 if 分支内部的业务逻辑代码
}
@Override
public void actionPerformed(ActionEvent e) {
// 获取当前被点击的条目对象
Object source = e.getSource();
// 判断
if (source == replayItem) {
System.out.println("重新游戏");
// 打乱二维数组中的数据
initData();
// 计步器清零, 这一步必须要放在重新加载图片的上面
step = 0;
// 重新加载图片
initImage();
} else if (source == reLoginItem) {
System.out.println("重新登录");
// 返回登录界面
// 首先关闭当前的游戏界面
setVisible(false);
// 第二步, 打开登录界面
new LoginJFrame();
} else if (source == closeItem) {
System.out.println("关闭游戏");
// 直接关闭虚拟机
System.exit(0);
} else if (source == accountItem) {
System.out.println("公众号");
// 创建一个弹框对象
JDialog jDialog = new JDialog();
// 创建一个管理图片的容器对象 JLabel
JLabel jLabel = new JLabel(new ImageIcon("jigsaw\\image\\about.png"));
// 给 jLabel 设置位置, 这个位置是相对于弹框而言的
jLabel.setBounds(0, 0, 258, 258);
jDialog.getContentPane().add(jLabel);
// 设置弹框大小
jDialog.setSize(344, 344);
jDialog.setAlwaysOnTop(true);
jDialog.setLocationRelativeTo(null);
// 弹框不关闭则无法操作下面的界面
jDialog.setModal(true);
// 让弹框显示出来
jDialog.setVisible(true);
}
}
// 定义一个方法, 比较当前的二维数组和正确的二维数组是否相同, 如果相同, 则表示胜利, 方法返回 true
// 当前的二维数组存储在 data 中, 正确的二维数组存储在 win 中
public boolean victory() {
for (int i = 0; i < data.length; i++) {
for (int j = 0; j < data[i].length; j++) {
if (data[i][j] != win[i][j]) {
return false;
}
}
}
return true;
}
}

更换图片

更改菜单


图1

如果在菜单中, 还需要嵌套二级的菜单, 那么可以用 JMenu 完成. JMenu 里面是可以再次添加其他 JMenu 的.

整个的菜单就是 JMenuBar.

功能, 关于我们, 更换图片: JMenu.

重新游戏, 重新登录, 关闭游戏, 美女, 动物, 运动, 公众号: JMenuItem.

写代码的时候如何实现:

第一步: 创建 JMenuBar 对象

第二步: 创建三个 JMenu 对象 (功能, 关于我们, 更换图片)

第三步: 创建七个 JMenuItem 对象 (重新游戏, 重新登录, 关闭游戏, 美女, 动物, 运动, 公众号)

第四步: 把美女, 动物, 运动放到更换图片当中

第五步: 把更换图片, 重新游戏, 重新登录, 关闭游戏放到功能当中

第六步: 把功能, 关于我们放到 JMenuBar

第七步: 把 JMenuBar 放到整个界面当中

更新后的 GameJFrame.java 代码:

点击查看代码
package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameJFrame extends JFrame implements KeyListener, ActionListener {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 定义两个变量, 记录 0 号图片在二维数组中的位置
int x = 0;
int y = 0;
// 定义一个二维数组, 存储正确的数据
int[][] win = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
// 定义一个变量, 记录当前展示图片的路径, 以后想要修改路径时, 就只需要修改这个变量
String path = "jigsaw\\image\\animal\\animal1\\";
// 定义一个变量, 用来统计步数
int step = 0;
// 创建选项下面的条目, 重写的 actionPerformed 方法需要用到这四个条目, 所以要把这四个条目放到成员位置
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
JMenuItem girlItem = new JMenuItem("美女");
JMenuItem animalItem = new JMenuItem("动物");
JMenuItem sportItem = new JMenuItem("运动");
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0) {
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 清空原本已经被加载出来的所有图片
getContentPane().removeAll();
if (victory()) {
// 显示胜利的图标
JLabel winJLabel = new JLabel(new ImageIcon("jigsaw\\image\\win.png"));
winJLabel.setBounds(203, 283, 197, 73);
getContentPane().add(winJLabel);
}
JLabel stepCount = new JLabel("步数: " + step);
stepCount.setBounds(50, 20, 100, 20);
getContentPane().add(stepCount);
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon(path + number + ".jpg"));
// JLabel label = new JLabel(new ImageIcon("..\\image\\animal\\animal1\\" + number + ".jpg")); // 不行, 找不到
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新一下
// 可能的原因:
// (确保图片在初始化之后立即显示在界面上, 如果不刷新则图片会一个一个地显示出来, 显得比较卡, 刷新的话则是一口气全部加载全部 15 张图片)
// 实际测试: 如果不刷新则不生效, 按下键盘上的方向键后不生效
getContentPane().repaint();
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的三个选项, 功能, 关于我们和更换图片
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
JMenu changePicture = new JMenu("更换图片");
// 给条目绑定事件
replayItem.addActionListener(this);
reLoginItem.addActionListener(this);
closeItem.addActionListener(this);
accountItem.addActionListener(this);
// 将选项下面的条目添加到选项当中
functionJMenu.add(changePicture);
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
changePicture.add(girlItem);
changePicture.add(animalItem);
changePicture.add(sportItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
// 给整个窗体添加键盘监听事件
addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {
}
// 长按 A 不松时显示完整图片, 松开后显示打乱的图片的逻辑要放到 keyReleased 方法中
@Override
public void keyPressed(KeyEvent e) {
System.out.println("长按 A 不松时显示完整图片");
int keyCode = e.getKeyCode();
if (keyCode == 65) {
// 清空现在已经加载出来的图片
getContentPane().removeAll();
JLabel jLabel = new JLabel(new ImageIcon(path + "all.jpg"));
jLabel.setBounds(83, 134, 420, 420);
// 将图片添加到窗体中
getContentPane().add(jLabel);
// 加载背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新界面
getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
// 判断游戏是否胜利, 如果胜利, 则此方法需要直接结束, 不能再执行下面的移动代码了
if (victory()) {
return;
}
// 根据 keyCode 对上下左右进行判断
// 左: 37, 上: 38, 右: 39, 下: 40
// 如果不知道这四个值是多少, 打印出来即可
int keyCode = e.getKeyCode();
// System.out.println(keyCode); // 打印出来就知道这四个方向按键对应的 keyCode 值了
if (keyCode == 37) {
System.out.println("向左移动");
if (y == 3) {
System.out.println("空白区域已经在最右边了, 无法向左移动");
return;
}
// 业务逻辑:
// 把空白方块右方的数字向左移动
// x , y 表示空白方块的位置
// x , y+1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 38) {
System.out.println("向上移动");
if (x == 3) {
System.out.println("空白区域已经在最下边了, 无法向上移动");
return;
}
// 业务逻辑:
// 把空白方块下方的数字向上移动
// x , y 表示空白方块的位置
// x+1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 39) {
System.out.println("向右移动");
if (y == 0) {
System.out.println("空白区域已经在最左边了, 无法向右移动");
return;
}
// 业务逻辑:
// 把空白方块左方的数字向右移动
// x , y 表示空白方块的位置
// x , y-1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 40) {
System.out.println("向下移动");
if (x == 0) {
System.out.println("空白区域已经在最上边了, 无法向下移动");
return;
}
// 业务逻辑:
// 把空白方块上方的数字向下移动
// x , y 表示空白方块的位置
// x-1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 65) { // A 是 65
System.out.println("松开 A 后显示打乱的图片");
initImage();
} else if (keyCode == 87) { // W 是 87
data = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
initImage();
}
// 先在每一个分支内写类似于 System.out.println("向下移动"); 这样的输出语句,
// 然后运行 App.java 发现输出是正确的, 然后继续补充 if 分支内部的业务逻辑代码
}
@Override
public void actionPerformed(ActionEvent e) {
// 获取当前被点击的条目对象
Object source = e.getSource();
// 判断
if (source == replayItem) {
System.out.println("重新游戏");
// 打乱二维数组中的数据
initData();
// 计步器清零, 这一步必须要放在重新加载图片的上面
step = 0;
// 重新加载图片
initImage();
} else if (source == reLoginItem) {
System.out.println("重新登录");
// 返回登录界面
// 首先关闭当前的游戏界面
setVisible(false);
// 第二步, 打开登录界面
new LoginJFrame();
} else if (source == closeItem) {
System.out.println("关闭游戏");
// 直接关闭虚拟机
System.exit(0);
} else if (source == accountItem) {
System.out.println("公众号");
// 创建一个弹框对象
JDialog jDialog = new JDialog();
// 创建一个管理图片的容器对象 JLabel
JLabel jLabel = new JLabel(new ImageIcon("jigsaw\\image\\about.png"));
// 给 jLabel 设置位置, 这个位置是相对于弹框而言的
jLabel.setBounds(0, 0, 258, 258);
jDialog.getContentPane().add(jLabel);
// 设置弹框大小
jDialog.setSize(344, 344);
jDialog.setAlwaysOnTop(true);
jDialog.setLocationRelativeTo(null);
// 弹框不关闭则无法操作下面的界面
jDialog.setModal(true);
// 让弹框显示出来
jDialog.setVisible(true);
}
}
// 定义一个方法, 比较当前的二维数组和正确的二维数组是否相同, 如果相同, 则表示胜利, 方法返回 true
// 当前的二维数组存储在 data 中, 正确的二维数组存储在 win 中
public boolean victory() {
for (int i = 0; i < data.length; i++) {
for (int j = 0; j < data[i].length; j++) {
if (data[i][j] != win[i][j]) {
return false;
}
}
}
return true;
}
}

现在的效果:


图2

增加美女, 动物和运动的具体业务逻辑

分析业务逻辑:

  1. 给美女, 动物, 运动添加单击事件 (动作监听)

  2. 当我们点击了美女之后, 就会从 13 组美女图片中随机选择一组.

  3. 当我们点击了动物之后, 就会从 8 组动物图片中随机选择一组.

  4. 当我们点击了运动之后, 就会从 10 组运动图片中随机选择一组.

  5. 细节 1: 选择完毕之后, 游戏界面中需要加载所有的小图片并且打乱顺序.

  6. 细节 2: 按 A 的时候显示的是选择之后的图片.

改写之后的 GameJFrame.java 代码:

点击查看代码
package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameJFrame extends JFrame implements KeyListener, ActionListener {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 定义两个变量, 记录 0 号图片在二维数组中的位置
int x = 0;
int y = 0;
// 定义一个二维数组, 存储正确的数据
int[][] win = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
// 定义一个变量, 记录当前展示图片的路径, 以后想要修改路径时, 就只需要修改这个变量
String path = "jigsaw\\image\\animal\\animal1\\";
// 定义一个变量, 用来统计步数
int step = 0;
// 创建选项下面的条目, 重写的 actionPerformed 方法需要用到这四个条目, 所以要把这四个条目放到成员位置
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
JMenuItem girlItem = new JMenuItem("美女");
JMenuItem animalItem = new JMenuItem("动物");
JMenuItem sportItem = new JMenuItem("运动");
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0) {
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 清空原本已经被加载出来的所有图片
getContentPane().removeAll();
if (victory()) {
// 显示胜利的图标
JLabel winJLabel = new JLabel(new ImageIcon("jigsaw\\image\\win.png"));
winJLabel.setBounds(203, 283, 197, 73);
getContentPane().add(winJLabel);
}
JLabel stepCount = new JLabel("步数: " + step);
stepCount.setBounds(50, 20, 100, 20);
getContentPane().add(stepCount);
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon(path + number + ".jpg"));
// JLabel label = new JLabel(new ImageIcon("..\\image\\animal\\animal1\\" + number + ".jpg")); // 不行, 找不到
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新一下
// 可能的原因:
// (确保图片在初始化之后立即显示在界面上, 如果不刷新则图片会一个一个地显示出来, 显得比较卡, 刷新的话则是一口气全部加载全部 15 张图片)
// 实际测试: 如果不刷新则不生效, 按下键盘上的方向键后不生效
getContentPane().repaint();
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的三个选项, 功能, 关于我们和更换图片
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
JMenu changePicture = new JMenu("更换图片");
// 给条目绑定事件
replayItem.addActionListener(this);
reLoginItem.addActionListener(this);
closeItem.addActionListener(this);
accountItem.addActionListener(this);
girlItem.addActionListener(this);
animalItem.addActionListener(this);
sportItem.addActionListener(this);
// 将选项下面的条目添加到选项当中
functionJMenu.add(changePicture);
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
changePicture.add(girlItem);
changePicture.add(animalItem);
changePicture.add(sportItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
// 给整个窗体添加键盘监听事件
addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {
}
// 长按 A 不松时显示完整图片, 松开后显示打乱的图片的逻辑要放到 keyReleased 方法中
@Override
public void keyPressed(KeyEvent e) {
System.out.println("长按 A 不松时显示完整图片");
int keyCode = e.getKeyCode();
if (keyCode == 65) {
// 清空现在已经加载出来的图片
getContentPane().removeAll();
JLabel jLabel = new JLabel(new ImageIcon(path + "all.jpg"));
jLabel.setBounds(83, 134, 420, 420);
// 将图片添加到窗体中
getContentPane().add(jLabel);
// 加载背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新界面
getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
// 判断游戏是否胜利, 如果胜利, 则此方法需要直接结束, 不能再执行下面的移动代码了
if (victory()) {
return;
}
// 根据 keyCode 对上下左右进行判断
// 左: 37, 上: 38, 右: 39, 下: 40
// 如果不知道这四个值是多少, 打印出来即可
int keyCode = e.getKeyCode();
// System.out.println(keyCode); // 打印出来就知道这四个方向按键对应的 keyCode 值了
if (keyCode == 37) {
System.out.println("向左移动");
if (y == 3) {
System.out.println("空白区域已经在最右边了, 无法向左移动");
return;
}
// 业务逻辑:
// 把空白方块右方的数字向左移动
// x , y 表示空白方块的位置
// x , y+1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 38) {
System.out.println("向上移动");
if (x == 3) {
System.out.println("空白区域已经在最下边了, 无法向上移动");
return;
}
// 业务逻辑:
// 把空白方块下方的数字向上移动
// x , y 表示空白方块的位置
// x+1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 39) {
System.out.println("向右移动");
if (y == 0) {
System.out.println("空白区域已经在最左边了, 无法向右移动");
return;
}
// 业务逻辑:
// 把空白方块左方的数字向右移动
// x , y 表示空白方块的位置
// x , y-1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 40) {
System.out.println("向下移动");
if (x == 0) {
System.out.println("空白区域已经在最上边了, 无法向下移动");
return;
}
// 业务逻辑:
// 把空白方块上方的数字向下移动
// x , y 表示空白方块的位置
// x-1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 65) { // A 是 65
System.out.println("松开 A 后显示打乱的图片");
initImage();
} else if (keyCode == 87) { // W 是 87
data = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
initImage();
}
// 先在每一个分支内写类似于 System.out.println("向下移动"); 这样的输出语句,
// 然后运行 App.java 发现输出是正确的, 然后继续补充 if 分支内部的业务逻辑代码
}
@Override
public void actionPerformed(ActionEvent e) {
// 获取当前被点击的条目对象
Object source = e.getSource();
// 判断
if (source == replayItem) {
System.out.println("重新游戏");
// 打乱二维数组中的数据
initData();
// 计步器清零, 这一步必须要放在重新加载图片的上面
step = 0;
// 重新加载图片
initImage();
} else if (source == reLoginItem) {
System.out.println("重新登录");
// 返回登录界面
// 首先关闭当前的游戏界面
setVisible(false);
// 第二步, 打开登录界面
new LoginJFrame();
} else if (source == closeItem) {
System.out.println("关闭游戏");
// 直接关闭虚拟机
System.exit(0);
} else if (source == accountItem) {
System.out.println("公众号");
// 创建一个弹框对象
JDialog jDialog = new JDialog();
// 创建一个管理图片的容器对象 JLabel
JLabel jLabel = new JLabel(new ImageIcon("jigsaw\\image\\about.png"));
// 给 jLabel 设置位置, 这个位置是相对于弹框而言的
jLabel.setBounds(0, 0, 258, 258);
jDialog.getContentPane().add(jLabel);
// 设置弹框大小
jDialog.setSize(344, 344);
jDialog.setAlwaysOnTop(true);
jDialog.setLocationRelativeTo(null);
// 弹框不关闭则无法操作下面的界面
jDialog.setModal(true);
// 让弹框显示出来
jDialog.setVisible(true);
} else if (source == girlItem) {
// 在 13 组美女图片中随机选择一组
Random random = new Random();
int index = random.nextInt(13) + 1;
System.out.println(index);
// 修改 path 变量记录的值
path = "jigsaw\\image\\girl\\girl" + index + "\\";
System.out.println(path);
// 重开一把
// 打乱二维数组中的数据
initData();
// 计步器清零, 这一步必须要放在重新加载图片的上面
step = 0;
// 重新加载图片
initImage();
} else if (source == animalItem) {
// 在 8 组动物图片中随机选择一组
Random random = new Random();
int index = random.nextInt(8) + 1;
System.out.println(index);
// 修改 path 变量记录的值
path = "jigsaw\\image\\animal\\animal" + index + "\\";
System.out.println(path);
// 重开一把
// 打乱二维数组中的数据
initData();
// 计步器清零, 这一步必须要放在重新加载图片的上面
step = 0;
// 重新加载图片
initImage();
} else if (source == sportItem) {
// 在 10 组运动图片中随机选择一组
Random random = new Random();
int index = random.nextInt(10) + 1;
System.out.println(index);
// 修改 path 变量记录的值
path = "jigsaw\\image\\sport\\sport" + index + "\\";
System.out.println(path);
// 重开一把
// 打乱二维数组中的数据
initData();
// 计步器清零, 这一步必须要放在重新加载图片的上面
step = 0;
// 重新加载图片
initImage();
}
}
// 定义一个方法, 比较当前的二维数组和正确的二维数组是否相同, 如果相同, 则表示胜利, 方法返回 true
// 当前的二维数组存储在 data 中, 正确的二维数组存储在 win 中
public boolean victory() {
for (int i = 0; i < data.length; i++) {
for (int j = 0; j < data[i].length; j++) {
if (data[i][j] != win[i][j]) {
return false;
}
}
}
return true;
}
}

登录界面


图1 登录界面

解释:


图2 登录界面解释

现有的登录代码:


图3

LoginJFrame.java 的代码:

package ui;
import javax.swing.*;
public class LoginJFrame extends JFrame {
// 表示登录的窗体, 和登录相关的代码, 都放到这里
public LoginJFrame() {
// 给窗体设置宽高
setSize(488, 430);
// 设置窗体的标题
setTitle("拼图 登录");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
}

效果:


图4

暂时将可以登录的用户名和密码先写死.

生成验证码的代码是自己写的一个工具类.

现在的代码目录结构:


图5

注册部分先暂时不起作用.

RegisterJFrame.java 的代码:

点击查看代码
package ui;
import javax.swing.*;
public class RegisterJFrame extends JFrame {
// 表示注册的窗体, 和注册相关的代码, 都放到这里
public RegisterJFrame() {
// 给窗体设置宽高
setSize(488, 500);
// 设置窗体的标题
setTitle("拼图 注册");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
}

GameJFrame.java 代码:

点击查看代码
package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameJFrame extends JFrame implements KeyListener, ActionListener {
// 创建一个二维数组, 作用是管理图片
// 加载图片时, 会根据二维数组中的数据进行加载
// 在 initData 方法和 initImage 方法中都会用到这个数组, 所以这个数据要定义在成员位置
int[][] data = new int[4][4];
// 定义两个变量, 记录 0 号图片在二维数组中的位置
int x = 0;
int y = 0;
// 定义一个二维数组, 存储正确的数据
int[][] win = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
// 定义一个变量, 记录当前展示图片的路径, 以后想要修改路径时, 就只需要修改这个变量
String path = "jigsaw\\image\\animal\\animal1\\";
// 定义一个变量, 用来统计步数
int step = 0;
// 创建选项下面的条目, 重写的 actionPerformed 方法需要用到这四个条目, 所以要把这四个条目放到成员位置
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
JMenuItem girlItem = new JMenuItem("美女");
JMenuItem animalItem = new JMenuItem("动物");
JMenuItem sportItem = new JMenuItem("运动");
// 表示游戏主窗口的窗体, 和游戏主窗口相关的代码, 都放到这里
public GameJFrame() {
// 初始化窗体
initJFrame();
// 初始化菜单
initJMenuBar();
// 初始化数据 (打乱图片)
initData();
// 初始化图片 (根据打乱之后的图片进行加载)
initImage();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
// 初始化数据 (打乱)
private void initData() {
int[] tempArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Random rand = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = rand.nextInt(tempArr.length);
int temp = tempArr[index];
tempArr[index] = tempArr[i];
tempArr[i] = temp;
}
// 给二维数组添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0) {
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
// 初始化图片
// 添加图片时, 需要按照二维数组中管理的数据添加图片
private void initImage() {
// 清空原本已经被加载出来的所有图片
getContentPane().removeAll();
if (victory()) {
// 显示胜利的图标
JLabel winJLabel = new JLabel(new ImageIcon("jigsaw\\image\\win.png"));
winJLabel.setBounds(203, 283, 197, 73);
getContentPane().add(winJLabel);
}
JLabel stepCount = new JLabel("步数: " + step);
stepCount.setBounds(50, 20, 100, 20);
getContentPane().add(stepCount);
// 创建 ImageIcon 对象
// ImageIcon icon1 = new ImageIcon("E:\\Computer\\Java\\JavaCodes\\jigsaw\\image\\animal\\animal1\\1.jpg");
// 这里有一个比较方便的点, 就是图片的名字是按照数字顺序来命名的, 也是应该被加载的顺序, 因此可以定义一个变量, 表示应该加载哪一张图片
// 外循环: 将内循环重复执行了 4 次, 即 4 行
for (int i = 0; i < 4; i++) {
// 内循环: 在一行添加四张图片
for (int j = 0; j < 4; j++) {
int number = data[i][j];
// 创建 JLabel 对象, 传递的参数是 ImageIcon 对象, 表示这个管理容器就管理 ImageIcon 对象
// 如果路径指定的图片不存在, 则得到一个空白区域, 将这个 JLabel 对象添加到窗体时, 显示的就是一个空白
JLabel label = new JLabel(new ImageIcon(path + number + ".jpg"));
// JLabel label = new JLabel(new ImageIcon("..\\image\\animal\\animal1\\" + number + ".jpg")); // 不行, 找不到
// 指定图片的位置, 一定要在将 JLabel 对象放到窗体之前指定位置
// x 方向给定偏移量 83, y 方向给定偏移量 105
label.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
// 给图片添加边框
label.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将 JLabel 对象放到 JFrame 窗体中, 默认放在窗体的正中央
// add(label);
// 获取窗体 JFrame 的第三部分这个对象, 将 JLabel 对象添加到这个对象中
getContentPane().add(label);
}
}
// 先添加的图片在上方, 后添加的图片在下方, 所以背景图要放到 for 循环后面
// 添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新一下
// 可能的原因:
// (确保图片在初始化之后立即显示在界面上, 如果不刷新则图片会一个一个地显示出来, 显得比较卡, 刷新的话则是一口气全部加载全部 15 张图片)
// 实际测试: 如果不刷新则不生效, 按下键盘上的方向键后不生效
getContentPane().repaint();
}
// 初始化菜单
private void initJMenuBar() {
// 创建整个菜单对象
JMenuBar menuBar = new JMenuBar();
// 创建菜单的三个选项, 功能, 关于我们和更换图片
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
JMenu changePicture = new JMenu("更换图片");
// 给条目绑定事件
replayItem.addActionListener(this);
reLoginItem.addActionListener(this);
closeItem.addActionListener(this);
accountItem.addActionListener(this);
girlItem.addActionListener(this);
animalItem.addActionListener(this);
sportItem.addActionListener(this);
// 将选项下面的条目添加到选项当中
functionJMenu.add(changePicture);
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
changePicture.add(girlItem);
changePicture.add(animalItem);
changePicture.add(sportItem);
// 将菜单的两个选项添加到菜单中
menuBar.add(functionJMenu);
menuBar.add(aboutJMenu);
// 将菜单添加到整个窗体中
setJMenuBar(menuBar);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(603, 680);
// 设置窗体的标题
setTitle("拼图单机版 v1.0");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消组件在窗体中默认居中的摆放位置, 从而让指定的 x 和 y 坐标生效
setLayout(null);
// 给整个窗体添加键盘监听事件
addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {
}
// 长按 A 不松时显示完整图片, 松开后显示打乱的图片的逻辑要放到 keyReleased 方法中
@Override
public void keyPressed(KeyEvent e) {
System.out.println("长按 A 不松时显示完整图片");
int keyCode = e.getKeyCode();
if (keyCode == 65) {
// 清空现在已经加载出来的图片
getContentPane().removeAll();
JLabel jLabel = new JLabel(new ImageIcon(path + "all.jpg"));
jLabel.setBounds(83, 134, 420, 420);
// 将图片添加到窗体中
getContentPane().add(jLabel);
// 加载背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\background.png"));
// x 和 y 的位置是调出来的, 宽和高是图片自身的值
background.setBounds(40, 40, 508, 560);
// 把背景图加到窗体中
getContentPane().add(background);
// 刷新界面
getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
// 判断游戏是否胜利, 如果胜利, 则此方法需要直接结束, 不能再执行下面的移动代码了
if (victory()) {
return;
}
// 根据 keyCode 对上下左右进行判断
// 左: 37, 上: 38, 右: 39, 下: 40
// 如果不知道这四个值是多少, 打印出来即可
int keyCode = e.getKeyCode();
// System.out.println(keyCode); // 打印出来就知道这四个方向按键对应的 keyCode 值了
if (keyCode == 37) {
System.out.println("向左移动");
if (y == 3) {
System.out.println("空白区域已经在最右边了, 无法向左移动");
return;
}
// 业务逻辑:
// 把空白方块右方的数字向左移动
// x , y 表示空白方块的位置
// x , y+1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 38) {
System.out.println("向上移动");
if (x == 3) {
System.out.println("空白区域已经在最下边了, 无法向上移动");
return;
}
// 业务逻辑:
// 把空白方块下方的数字向上移动
// x , y 表示空白方块的位置
// x+1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 39) {
System.out.println("向右移动");
if (y == 0) {
System.out.println("空白区域已经在最左边了, 无法向右移动");
return;
}
// 业务逻辑:
// 把空白方块左方的数字向右移动
// x , y 表示空白方块的位置
// x , y-1 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 40) {
System.out.println("向下移动");
if (x == 0) {
System.out.println("空白区域已经在最上边了, 无法向下移动");
return;
}
// 业务逻辑:
// 把空白方块上方的数字向下移动
// x , y 表示空白方块的位置
// x-1 , y 表示空白方块下方的位置
// 所以需要交换二维数组中这两个位置的元素
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--; // 更新空白区域的位置
// 计数器自增一次, 且在重新加载界面之前自增, 这样重新加载界面的时候, 就会显示新的步数
step++;
// 按照最新的二维数组重新加载图片, 但是需要清空原本已经被加载出来的所有图片, 加载完了之后要刷新窗体, 这个代码放到 initImage 方法中
initImage();
} else if (keyCode == 65) { // A 是 65
System.out.println("松开 A 后显示打乱的图片");
initImage();
} else if (keyCode == 87) { // W 是 87
data = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
initImage();
}
// 先在每一个分支内写类似于 System.out.println("向下移动"); 这样的输出语句,
// 然后运行 App.java 发现输出是正确的, 然后继续补充 if 分支内部的业务逻辑代码
}
@Override
public void actionPerformed(ActionEvent e) {
// 获取当前被点击的条目对象
Object source = e.getSource();
// 判断
if (source == replayItem) {
System.out.println("重新游戏");
// 打乱二维数组中的数据
initData();
// 计步器清零, 这一步必须要放在重新加载图片的上面
step = 0;
// 重新加载图片
initImage();
} else if (source == reLoginItem) {
System.out.println("重新登录");
// 返回登录界面
// 首先关闭当前的游戏界面
setVisible(false);
// 第二步, 打开登录界面
new LoginJFrame();
} else if (source == closeItem) {
System.out.println("关闭游戏");
// 直接关闭虚拟机
System.exit(0);
} else if (source == accountItem) {
System.out.println("公众号");
// 创建一个弹框对象
JDialog jDialog = new JDialog();
// 创建一个管理图片的容器对象 JLabel
JLabel jLabel = new JLabel(new ImageIcon("jigsaw\\image\\about.png"));
// 给 jLabel 设置位置, 这个位置是相对于弹框而言的
jLabel.setBounds(0, 0, 258, 258);
jDialog.getContentPane().add(jLabel);
// 设置弹框大小
jDialog.setSize(344, 344);
jDialog.setAlwaysOnTop(true);
jDialog.setLocationRelativeTo(null);
// 弹框不关闭则无法操作下面的界面
jDialog.setModal(true);
// 让弹框显示出来
jDialog.setVisible(true);
} else if (source == girlItem) {
// 在 13 组美女图片中随机选择一组
Random random = new Random();
int index = random.nextInt(13) + 1;
System.out.println(index);
// 修改 path 变量记录的值
path = "jigsaw\\image\\girl\\girl" + index + "\\";
System.out.println(path);
// 重开一把
// 打乱二维数组中的数据
initData();
// 计步器清零, 这一步必须要放在重新加载图片的上面
step = 0;
// 重新加载图片
initImage();
} else if (source == animalItem) {
// 在 8 组动物图片中随机选择一组
Random random = new Random();
int index = random.nextInt(8) + 1;
System.out.println(index);
// 修改 path 变量记录的值
path = "jigsaw\\image\\animal\\animal" + index + "\\";
System.out.println(path);
// 重开一把
// 打乱二维数组中的数据
initData();
// 计步器清零, 这一步必须要放在重新加载图片的上面
step = 0;
// 重新加载图片
initImage();
} else if (source == sportItem) {
// 在 10 组运动图片中随机选择一组
Random random = new Random();
int index = random.nextInt(10) + 1;
System.out.println(index);
// 修改 path 变量记录的值
path = "jigsaw\\image\\sport\\sport" + index + "\\";
System.out.println(path);
// 重开一把
// 打乱二维数组中的数据
initData();
// 计步器清零, 这一步必须要放在重新加载图片的上面
step = 0;
// 重新加载图片
initImage();
}
}
// 定义一个方法, 比较当前的二维数组和正确的二维数组是否相同, 如果相同, 则表示胜利, 方法返回 true
// 当前的二维数组存储在 data 中, 正确的二维数组存储在 win 中
public boolean victory() {
for (int i = 0; i < data.length; i++) {
for (int j = 0; j < data[i].length; j++) {
if (data[i][j] != win[i][j]) {
return false;
}
}
}
return true;
}
}

LoginJFrame.java 代码:

点击查看代码
package ui;
import util.CodeUtil;
import domain.User;
import javax.swing.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
public class LoginJFrame extends JFrame implements MouseListener {
static ArrayList<User> allUsers = new ArrayList<>();
static {
allUsers.add(new User("zhangsan", "123"));
allUsers.add(new User("lisi", "1234"));
}
JButton login = new JButton();
JButton register = new JButton();
JTextField username = new JTextField();
JPasswordField password = new JPasswordField();
JTextField code = new JTextField();
// 正确的验证码
JLabel rightCode = new JLabel();
// 表示登录的窗体, 和登录相关的代码, 都放到这里
public LoginJFrame() {
// 初始化窗体
initJFrame();
// 在这个窗体中添加内容
initView();
// 让窗体显示出来, 这行代码建议写在最后面
setVisible(true);
}
private void initView() {
// 1. 添加用户名文字
JLabel usernameText = new JLabel(new ImageIcon("jigsaw\\image\\login\\用户名.png"));
usernameText.setBounds(116, 135, 47, 17);
getContentPane().add(usernameText);
// 2.添加用户名输入框
username.setBounds(195, 134, 200, 30);
getContentPane().add(username);
// 3.添加密码文字
JLabel passwordText = new JLabel(new ImageIcon("jigsaw\\image\\login\\密码.png"));
passwordText.setBounds(130, 195, 32, 16);
getContentPane().add(passwordText);
// 4.密码输入框
password.setBounds(195, 195, 200, 30);
getContentPane().add(password);
// 验证码提示
JLabel codeText = new JLabel(new ImageIcon("jigsaw\\image\\login\\验证码.png"));
codeText.setBounds(133, 256, 50, 30);
getContentPane().add(codeText);
// 验证码的输入框
code.setBounds(195, 256, 100, 30);
getContentPane().add(code);
String codeStr = CodeUtil.getCode();
// 设置内容
rightCode.setText(codeStr);
// 绑定鼠标事件
rightCode.addMouseListener(this);
// 位置和宽高
rightCode.setBounds(300, 256, 50, 30);
// 添加到界面
getContentPane().add(rightCode);
// 5.添加登录按钮
login.setBounds(123, 310, 128, 47);
login.setIcon(new ImageIcon("jigsaw\\image\\login\\登录按钮.png"));
// 去除按钮的边框
login.setBorderPainted(false);
// 去除按钮的背景
login.setContentAreaFilled(false);
// 给登录按钮绑定鼠标事件
login.addMouseListener(this);
getContentPane().add(login);
// 6.添加注册按钮
register.setBounds(256, 310, 128, 47);
register.setIcon(new ImageIcon("jigsaw\\image\\login\\注册按钮.png"));
// 去除按钮的边框
register.setBorderPainted(false);
// 去除按钮的背景
register.setContentAreaFilled(false);
// 给注册按钮绑定鼠标事件
register.addMouseListener(this);
getContentPane().add(register);
// 7.添加背景图片
JLabel background = new JLabel(new ImageIcon("jigsaw\\image\\login\\background.png"));
background.setBounds(0, 0, 470, 390);
getContentPane().add(background);
}
// 初始化窗体
private void initJFrame() {
// 给窗体设置宽高
setSize(488, 430);
// 设置窗体的标题
setTitle("拼图游戏 V1.0登录");
// 设置窗体置顶, 窗体出现后, 一直处于屏幕的最前面, 点击窗体之外的范围时, 窗体不会被别的页面盖住, 就类似于有的页面的图钉 (pin) 功能
setAlwaysOnTop(true);
// 设置窗体第一次出现的位置是屏幕的中央 (默认是在屏幕的最左上角), 传递参数是 null
setLocationRelativeTo(null);
// 目前为止, 当游戏主窗体关闭时, 程序并没有结束, 现在希望关闭这个游戏主窗体时, 程序也随之结束
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 或者:
// setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 取消内部默认布局
this.setLayout(null);
}
// 点击
@Override
public void mouseClicked(MouseEvent e) {
if (e.getSource() == login) {
System.out.println("点击了登录按钮");
// 获取两个文本输入框中的内容
String usernameInput = username.getText();
String passwordInput = password.getText();
// 获取用户输入的验证码
String codeInput = code.getText();
// 创建一个 User 对象
User userInfo = new User(usernameInput, passwordInput);
System.out.println("用户输入的用户名为" + usernameInput);
System.out.println("用户输入的密码为" + passwordInput);
if (codeInput.length() == 0) {
showJDialog("验证码不能为空");
} else if (usernameInput.length() == 0 || passwordInput.length() == 0) {
// 校验用户名和密码是否为空
System.out.println("用户名或者密码为空");
// 调用 showJDialog 方法并展示弹框
showJDialog("用户名或者密码为空");
} else if (!codeInput.equalsIgnoreCase(rightCode.getText())) {
showJDialog("验证码输入错误");
} else if (contains(userInfo)) {
System.out.println("用户名和密码正确可以开始玩游戏了");
// 关闭当前登录界面
this.setVisible(false);
// 打开游戏的主界面
// 需要把当前登录的用户名传递给游戏界面
new GameJFrame();
} else {
System.out.println("用户名或密码错误");
showJDialog("用户名或密码错误");
}
} else if (e.getSource() == register) {
System.out.println("点击了注册按钮");
} else if (e.getSource() == rightCode) {
System.out.println("更换验证码");
// 获取一个新的验证码
String code = CodeUtil.getCode();
rightCode.setText(code);
}
}
// 按下不松
@Override
public void mousePressed(MouseEvent e) {
if (e.getSource() == login) {
login.setIcon(new ImageIcon("jigsaw\\image\\login\\登录按下.png"));
} else if (e.getSource() == register) {
register.setIcon(new ImageIcon("jigsaw\\image\\login\\注册按下.png"));
}
}
// 松开按钮
@Override
public void mouseReleased(MouseEvent e) {
if (e.getSource() == login) {
login.setIcon(new ImageIcon("jigsaw\\image\\login\\登录按钮.png"));
} else if (e.getSource() == register) {
register.setIcon(new ImageIcon("jigsaw\\image\\login\\注册按钮.png"));
}
}
// 鼠标划入
@Override
public void mouseEntered(MouseEvent e) {
}
// 鼠标划出
@Override
public void mouseExited(MouseEvent e) {
}
public void showJDialog(String content) {
// 创建一个弹框对象
JDialog jDialog = new JDialog();
// 给弹框设置大小
jDialog.setSize(200, 150);
// 让弹框置顶
jDialog.setAlwaysOnTop(true);
// 让弹框居中
jDialog.setLocationRelativeTo(null);
// 弹框不关闭永远无法操作下面的界面
jDialog.setModal(true);
// 创建 JLabel 对象管理文字并添加到弹框当中
JLabel warning = new JLabel(content);
warning.setBounds(0, 0, 200, 150);
jDialog.getContentPane().add(warning);
// 让弹框展示出来
jDialog.setVisible(true);
}
// 判断用户在集合中是否存在
public boolean contains(User userInput) {
for (int i = 0; i < allUsers.size(); i++) {
User rightUser = allUsers.get(i);
if (userInput.getUsername().equals(rightUser.getUsername()) && userInput.getPassword().equals(rightUser.getPassword())) {
// 有相同的代表存在, 返回 true, 后面的不需要再比了
return true;
}
}
// 循环结束之后还没有找到就表示不存在
return false;
}
}

CodeUtil.java 代码:

点击查看代码
package util;
import java.util.ArrayList;
import java.util.Random;
public class CodeUtil {
public static String getCode(){
// 1. 创建一个集合
ArrayList<Character> list = new ArrayList<>(); // 52 索引的范围: 0 ~ 51
// 2. 添加字母 a - z A - Z
for (int i = 0; i < 26; i++) {
list.add((char)('a' + i)); // a - z
list.add((char)('A' + i)); // A - Z
}
// 3. 打印集合
// System.out.println(list);
// 4. 生成 4 个随机字母
String result = "";
Random r = new Random();
for (int i = 0; i < 4; i++) {
// 获取随机索引
int randomIndex = r.nextInt(list.size());
char c = list.get(randomIndex);
result = result + c;
}
// System.out.println(result); // 长度为 4 的随机字符串
// 5. 在后面拼接数字 0~9
int number = r.nextInt(10);
// 6. 把随机数字拼接到 result 的后面
result = result + number;
// System.out.println(result); // ABCD5
// 7. 把字符串变成字符数组
char[] chars = result.toCharArray(); // [A, B, C, D, 5]
// 8. 在字符数组中生成一个随机索引
int index = r.nextInt(chars.length);
// 9. 拿着 4 索引上的数字, 跟随机索引上的数字进行交换
char temp = chars[4];
chars[4] = chars[index];
chars[index] = temp;
// 10. 把字符数组再变回字符串
String code = new String(chars);
// System.out.println(code);
return code;
}
}

User.java 代码:

点击查看代码
package domain;
public class User {
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
/**
* 获取
*
* @return username
*/
public String getUsername() {
return username;
}
/**
* 设置
*
* @param username
*/
public void setUsername(String username) {
this.username = username;
}
/**
* 获取
*
* @return password
*/
public String getPassword() {
return password;
}
/**
* 设置
*
* @param password
*/
public void setPassword(String password) {
this.password = password;
}
}

App.java 的代码:

点击查看代码
import ui.GameJFrame;
import ui.LoginJFrame;
import ui.RegisterJFrame;
public class App {
public static void main(String[] args) {
// 表示程序的启动入口
// 如果想要开启一个窗体, 就创建该窗体的对象, 创建窗体对象时, 调用构造方法, 设置该窗体的尺寸和可见性
// new GameJFrame();
new LoginJFrame();
// new RegisterJFrame();
}
}

打包为 exe 文件

准备工作

需要考虑的事情:

  1. 必须包含 GUI 界面

  2. 打包的内容, 除了代码, 还有游戏中用到的图片和 JDK, 因为别人的电脑上未必有 Java 的运行环境.

主要实现步骤:

  1. 将代码打包成 jar 包

  2. 整合资源文件

  3. 将 jar 包打包成 exe (这个 exe 只包含代码)

  4. 将 JDK、资源文件、jar 包转换后的 exe 三者再次打包成最终的 exe

准备软件

  1. IDEA: 将代码打包成 jar 包 (Java 形式的压缩包)

  2. exe4j: 将 jar 包转换成 exe 的工具

  3. innosetup: 将游戏用到的图片, Java 的运行环境和第二步打包的代码, 组合成最终的安装程序

安装 JDK 11

exe4j 支持的 JDK 版本是 8~11, 其他版本的 JDK 不行.


图1 JDK 11 安装包

双击安装包开始安装:


图2 双击安装包开始安装

更改安装路径:


图3 更改安装路径

正在安装:


图4 正在安装

安装完成:


图5 安装完成

JDK 11 不需要配置环境变量.

安装 exe4j

安装包:


图6 exe4j 安装包

右键以管理员身份运行安装包开始安装:


图7

图8

图9

更改安装路径:


图10 更改安装路径

正在安装:


图11 正在安装

安装完成:


图12 安装完成

安装 innosetup


图13

图14

图15

图16

图17

图18

图19

图20

更改图片路径

需要将原来代码里面的图片路径改为从图片的文件夹开始, 即从 image 开始, 而不是从项目或者模块开始.

改写之后的代码示例:


图21

制作 jar 包

点击 File, 再点 Project Structure

点击 Artifaces

下方图片箭头指向位置应为空白, 如果有其他内容, 可以选中之后, 点击减号删除.


图1

选中左侧的 Artifacts, 点击中间上方位置的 +, 点击 JAR, 点击 From modules with dependencies...


图2

Module: 选择要打包的项目.


图3

Main Class: 点击红框最后的那个小图标, 项目中是哪个 main 方法所在的类去启动项目, 这里就选择哪个类. 我的项目是由 App 里面的 main 方法启动的, 所以我就选择 App.


图4

图5

设置 META-INF/MANIFEST.MF, 点击箭头指向的图标进行设置. 选择当前模块,点击 OK.


图6

图7

图8

此时这里的路径就是模块所在路径, 点击 OK.


图9

如果没有弹框报错, 直接继续向下进行. 如果出现弹框报错, 表示当前模块下已存在 META-INF 文件夹了, 可以在本地找到已存在的 META-INF 文件夹, 右键点击 DELETE 删除即可.

查找方式: 右键模块点击 open in, 点击 Explorer, 在模块里面找到 META-INF 文件删除即可.


图10

如果没有报错, 就出现一个 jigsaw:jar 的提示. 点击右下角的 OK.


图11

在 IDEA 主界面上方, 点击 Build 里面的 Build Artifacts. 如果是灰色的不能按, 请确定在上述步骤中有没有配置好 jar 包的信息. 如果已经配置好了 jar 的信息, 此处就可以正常点击.


图12

在主界面正中央, 选择 jigsaw:jar, 再选择 Build. IDEA 会帮我们生成 jar 包.


图13

等页面右下角的进度条结束, jar 就已经生成完毕.

点击 File, 再点 Project Structure, 点击 Artifaces, 在下图红色框中的路径下, 可以找到生成好的 jar 包.


图14

图15

整合资源文件

将第一步创建好的 jar 包拷贝到桌面上. 在桌面上新建一个文件夹 resource.

将拼图游戏中的 image 文件夹粘贴到 resource 文件夹中. 此时在桌面中的 resource 文件夹下的 image 文件夹下, 就会看到游戏里面用到的所有图片.


图1

将 jar 包打包成 exe

双击打开安装好的exe4j.exe, 注册软件.

输入用户名, 公司名和注册码后点击ok,用户名, 公司名随便填, 最好都是小写字母. 注册码: L-g782dn2d-1f1yqxx1rv1sqd

注册完毕之后点击右下角的next. exe4j.exe只识别JDK8到JDK11, 如果安装时出现弹框报错, 请检查JDK版本.

4,选择JAVA转EXE.
点击右下角的next

5,输入名称jigsaw
输出保存exe的路径, 建议选择到桌面上.
点击右下角的next

6,选择以图形界面的形式启动游戏
输入应用名称, jigsaw
点击高级设置Advanced Options, 选择32-bit or 64-bit

7,勾选Generate 64-bit executable
表示要生成64位的exe安装包, 如果未勾选默认生成32位的安装包.
点击右下角next

8,然后下一步, 然后开始添加jar包并以及配置启动类.

第一行, VM参数配置的地方加上: -Dfile.encoding=utf-8

点击右侧绿色的+, 添加jar包

添加jar包.

选择桌面上的 jigsaw.jar, 点击下面的打开.

检查路径, 如果无误点击右下角的OK

选择项目启动类

因为程序主入口main方法写在App类中, 所有选择app, 并点击OK.

16,本页面中, 一共修改了三处.
三处全部操作完毕. 点击右下角的next

17,填写最小启动的JDK版本. 输入1.8
配置exe加载的JDK, 选择第一个

点击+

19,选择Directory
下面输入.\jdk
(注意: 输入点杠jdk, 都要是英文状态下输入)
点击OK

20,再次点击+

21,选择Directory
下面输入.\image
(输入点杠image, 都要是英文状态下输入)
点击OK

点击右下角的next

23,选择Client VM
点击右下角的next

24,然后一直下一步, 最终出现如下界面
点击右下角的Exit退出.

点击Exit后, 会提升是否需要保存刚刚的配置信息, 可以点击Yes, 并选择一个路径进行保存.

点击OK, 退出.

26,如果第三步选择的exe保存的路径是桌面, 那么在桌面上
就能看到生成的jigsaw.exe文件了.
四个文件分别为:
左一: 刚刚用jar生成的exe文件.
左二: idea生成的jar包
左三: 游戏用到的资源图片
左四: 刚刚用exe4J设置完毕之后保存的信息.

将jdk、资源文件、jar包转换后的exe三者再次打包成最终的exe

刚刚, 我们仅仅是把java代码变成了exe. 下面我们要把游戏中依赖的资源文件, 也就是使用到的所有图片, 还有JDK三者再次打包成最终的exe, 这样在没有jdk电脑环境下也能运行.

打开inno setup

在欢迎页面点击右下角的关闭

4,点击左上角的File
再点击NEW

5,点击next

6,输入应用名称jigsaw
点击next

不修改任何东西, 直接点击next

点击这里, 选择桌面上已经生成好的jigsaw.exe

9,点击Add folder

选择桌面的resource, 再点击确定.

11,如果出现下面弹框, 则点击是.
如果没有出现也没有任何关系.

12,再点击 Add file(s)…

13,选中桌面的puzzlegame.exe, 再次添加一次.
点击下面的打开.

14,在本页面中一共设置了三处地方.
全部设置完毕, 点击next.

15,默认不用选择, 点击next

16,默认不用选择, 点击next

选择语言, 还是默认, 点击next

18,选择输出路径, 还是选择桌面.
输入最终安装包的名字, 不能跟已有的puzzlegame重名.
所以我写setup, 再点击右下角next

19,默认点击next
有部分同学电脑不显示这一步, 也没有关系.

20,完成, 点击finish

21,配置到最后一步了, 脚本文件, 到这里会弹出问你是否马
上编译, 选择否, 先把脚本写好再自己编译.

22,上面红色箭头处添加一行脚本.

define MyJdkName "jdk"

添加前:

添加完毕之后, 如下图所示

往下拉, 把有红色框起来的这一行删掉

25,在上一步删除位置添加一段行的文字
Source: "自己本地JDK路径"; DestDir: "{app}{#MyJdkName}"; Flags: ignoreversion recursesubdirs createallsubdirs

添加完毕之后如下图

点击上方的绿色按钮开始编译

27,此时会询问, 是否需要保存.
可以点击是, 选择一个位置保存一下刚刚修改之后的结果.

然后等待绿色滚动条结束

29,当绿色滚动条结束后, 会自动安装setup.exe文件.
此时可以点击否, 先不安装.
在桌面上, 会多了一个setup.exe文件和一个后缀名为iss的文件
setup.exe: 打包成功的游戏安装包.
iss文件: 就是刚刚设置的脚本文件.

30,现在就可以把这个exe文件发给你的好基友了, 他的电脑
上不需要安装JDK, 直接双击这个安装包就可以玩游戏了.
在安装的时候可以选择安装路径.
还可以在桌面生成快捷方式.

如果日后源码修改, 需要重新生成exe安装包, 重新安装游戏.

最终的所有的生成物:

1,安装完毕之后, 可以到安装目录去找jigsaw.exe
双击就可以玩游戏了. 刚刚桌面上的5个文件可以全部删除. 以后双击jigsaw.exe就可以玩游戏了.

开始安装

双击setup.exe文件

在桌面生成了快捷方式:

双击快捷方式开始游戏:

posted @   有空  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示

目录导航