基于JavaFX的扫雷游戏实现(五)——设置和自定义控件
它来了它来了,最后一期终于来了。理论上该讲的全都讲完了,只剩下那个拖了好几期的自定义控件和一个比较没有存在感的设置功能没有讲。所以这次就重点介绍它们俩吧。
首先我们快速浏览下设置的实现,上图:

然后是控制器代码:
SettingsController.java
package controllers;
import components.GameEnum;
import javafx.animation.FadeTransition;
import javafx.animation.RotateTransition;
import javafx.animation.SequentialTransition;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Duration;
import static components.Constant.*;
/**
* @description: 设置界面控制逻辑
* @author: 郭小柒w
* @time: 2023/6/15
*/
public class SettingsController {
@FXML // 单选按钮, 难度
private RadioButton easy, medium, hard, custom;
@FXML // 文本框组, 自定义游戏数据
private TextField numWidth, numHeight, numBomb;
@FXML // 保存按钮
private Button save;
@FXML // 辅助效果图
private ImageView loading;
// 单选按钮组
private ToggleGroup degree;
public void initialize() {
// 文本框默认不可编辑
numWidth.setEditable(false);
numHeight.setEditable(false);
numBomb.setEditable(false);
// 首先尝试使用已保存设置
switch (GAME) {
case MEDIUM:
medium.setSelected(true);
break;
case HARD:
hard.setSelected(true);
break;
case CUSTOM:
custom.setSelected(true);
numWidth.setEditable(true);
numHeight.setEditable(true);
numBomb.setEditable(true);
numWidth.setText(GAME.width + "");
numHeight.setText(GAME.height + "");
numBomb.setText(GAME.bomb + "");
break;
default:
easy.setSelected(true);
break;
}
// 单选按钮分组
degree = new ToggleGroup();
easy.setToggleGroup(degree);
medium.setToggleGroup(degree);
hard.setToggleGroup(degree);
custom.setToggleGroup(degree);
// 难度按钮选中事件
degree.selectedToggleProperty().addListener(((observable, oldValue, newValue) -> {
String id = ((RadioButton) newValue).getId();
// 只有选中自定义难度情况下可编辑文本框, 默认不可编辑
if (id.equals("custom")) {
GAME = GameEnum.CUSTOM;
numWidth.setEditable(true);
numHeight.setEditable(true);
numBomb.setEditable(true);
} else {
// 清空文本框并设置为不可编辑
numWidth.setText(null);
numHeight.setText(null);
numBomb.setText(null);
numWidth.setEditable(false);
numHeight.setEditable(false);
numBomb.setEditable(false);
if (id.equals("easy")) {
GAME = GameEnum.EASY;
} else if (id.equals("medium")) {
GAME = GameEnum.MEDIUM;
} else {
GAME = GameEnum.HARD;
}
}
}));
// 保存按钮点击事件
save.setOnMouseClicked(event -> {
try {
// 如果是自定义难度, 保存输入的值
if (GAME == GameEnum.CUSTOM) {
try {
// 保存自定义输入
GAME.setWidth(Integer.parseInt(numWidth.getText()));
GAME.setHeight(Integer.parseInt(numHeight.getText()));
GAME.setBomb(Integer.parseInt(numBomb.getText()));
} catch (NumberFormatException e) {
// 输入问题导致的转换失败, 按简单设置处理
GAME.setWidth(9);
GAME.setHeight(9);
GAME.setBomb(10);
}
}
// 设置用于动画效果的图片
loading.setImage(new Image(LOAD_IMG));
loading.setVisible(true);
// 点击保存时的动画效果,分两步, 1:旋转缓冲 2:图片淡出
RotateTransition transition1 = new RotateTransition(Duration.seconds(1), loading);
// 旋转角度
transition1.setByAngle(360);
transition1.setOnFinished(event1 -> {
loading.setImage(new Image(SAVE_IMG));
});
FadeTransition transition2 = new FadeTransition(Duration.seconds(1), loading);
// 不透明度变化
transition2.setFromValue(1);
transition2.setToValue(0);
SequentialTransition sequence = new SequentialTransition(transition1, transition2);
// 播放动画
sequence.play();
} catch (Exception e) {
System.out.println("Error on [Class:SettingsController, Method:initialize, Event: save]=>");
e.printStackTrace();
}
});
}
}
和上期排行版难度按钮类似,都是单选按钮分组然后设置对应点击事件。不同的是不像排行版切换那样直观,这里需要一个提示来让玩家清楚保存是生效了的,所以我设置了保存按钮和对应的动画提示。下面介绍自定义控件的实现(设置真的没有存在感,哈哈哈)。
LedNumber,这个东西可是费了我老半天劲。我的思路是既然想在界面上显示,他要么是布局要么是控件,经过尝试后发现还是控件合理。所以这个自定义类要继承 Control 类,然后按照要求实现 createDefaultSkin 方法。到这里我就不会了,它要求的返回值类型为 Skin<?>,这是啥,没见过啊。求助万能的GPT后大概明白了它的要求(我理解的不一定准确)——控件显示是需要有Skin的,没有的话就类似无内容的Label,不设置背景色在界面上看起来就跟没有一样。所以我按GPT的提示创建了对应的skin类 LedNumberSkin,并在里面进行外观设计。
设计思路受这篇文章启发:https://blog.csdn.net/hx0_0_8/article/details/8012448
其思想就是将数字看作由以下七个线段组合而成,不同数字使用不同的线段:

只是这样看起来还不够美观,所以可以对这些线段的拼接处进行处理,比如下面这种形式(绘制的有些简陋,代码中是可以控制连接处贴合的,适当留白更立体):

那么代码中是如何实现的呢?对于每一条边,可以使用多边形Polygon类实现,只需要依次写入它的坐标即可(必须是顺时针或者逆时针,起始点位置不做要求),如下:
/**
* 计算自定义多边形各顶点坐标
*
* @param toward 多边形朝向 [01234: 右下左上中]
* @param x 起始点横坐标
* @param y 起始点纵坐标
* @return 坐标数组
*/
public ArrayList<Double> getPoints(int toward, double x, double y) {
ArrayList<Double> points = new ArrayList();
// 添加起始点坐标
points.add(x);
points.add(y);
// 按顺时针方向依次添加其余坐标
switch (toward) {
case 0:
points.add(x + height);
points.add(y + height);
points.add(x + height);
points.add(y + height + lenShort);
points.add(x);
points.add(y + lenLong);
break;
case 1:
points.add(x + lenLong);
points.add(y);
points.add(x + height + lenShort);
points.add(y + height);
points.add(x + height);
points.add(y + height);
break;
case 2:
points.add(x + height);
points.add(y - height);
points.add(x + height);
points.add(y + height + lenShort);
points.add(x);
points.add(y + lenShort);
break;
case 3:
points.add(x + lenShort);
points.add(y);
points.add(x + height + lenShort);
points.add(y + height);
points.add(x - height);
points.add(y + height);
break;
case 4:
points.add(x + height);
points.add(y - height + 2);
points.add(x + height + lenShort);
points.add(y - height + 2);
points.add(x + lenLong);
points.add(y);
points.add(x + height + lenShort);
points.add(y + height - 2);
points.add(x + height);
points.add(y + height - 2);
break;
}
return points;
}
有了绘制方法,接下来就是具体数字需要的初始化方法:
/**
* 构建点阵数字需要的边
*/
public void init() {
// 初始化
for (int i = 0; i < 7; ++i) {
polygons[i] = new Polygon();
}
// 计算出各多边形的顶点坐标
polygons[0].getPoints().addAll(getPoints(0, 1, 1));
polygons[1].getPoints().addAll(getPoints(1, 2, 0));
polygons[2].getPoints().addAll(getPoints(2, height + lenShort + 3, height + 1));
polygons[3].getPoints().addAll(getPoints(2, height + lenShort + 3, height + lenLong + 3));
polygons[4].getPoints().addAll(getPoints(3, height + 2, lenLong * 2 - 1));
polygons[5].getPoints().addAll(getPoints(0, 1, lenLong + 3));
polygons[6].getPoints().addAll(getPoints(4, 2, lenLong + 2));
// 根据edges数组判断每条边待设置的颜色
for (int i = 0; i < 7; ++i) {
if(edges[index][i]) {
polygons[i].setFill(Color.web("#FF0000"));
} else {
polygons[i].setFill(Color.web("#680404"));
}
pane.getChildren().add(polygons[i]);
}
getChildren().add(pane);
}
注:edges为二维boolean数组,控制每条边的颜色显示 [true:亮红色, false:暗红色]
这样就有了每个数字对应的外观,由于我使用的jdk版本较早,所以不支持 LedNumber 类运行中修改 Skin,所以采用以下方式实现数字显示切换:
/**
* 切换数字显示
* @param index 要转化成的数字
*/
public void switchSkin(int index) {
// 清空当前控件的子节点, 重新添加
getChildren().clear();
LedNumber newLedNumber = new LedNumber(index);
getChildren().add(newLedNumber);
}
这样就完成了LED数字的显示,感觉这个想法还是很不错的。而且在这个基础上你甚至还可以自己实现电子万年历之类的程序,有兴趣的伙伴可以尝试下。到此本次扫雷项目已经介绍差不多了,如果您还有疑问欢迎在评论区留言,有缘明年再见(开鸽!)
——————————————我———是———分———割———线—————————————
稀里糊涂地讲完啦,不知道大家能理解多少。过一阵子应该会去上海吧,找朋友聚聚,散散心开启新生活,如正文结尾所说,我们有缘的话明年博客园再会啦,886️!
基于JavaFX的扫雷游戏实现(五)——设置和自定义控件的更多相关文章
- 基于jQuery经典扫雷游戏源码
分享一款基于jQuery经典扫雷游戏源码.这是一款网页版扫雷小游戏特效代码下载.效果图如下: 在线预览 源码下载 实现的代码. html代码: <center> <h1>j ...
- C# -- HttpWebRequest 和 HttpWebResponse 的使用 C#编写扫雷游戏 使用IIS调试ASP.NET网站程序 WCF入门教程 ASP.Net Core开发(踩坑)指南 ASP.Net Core Razor+AdminLTE 小试牛刀 webservice创建、部署和调用 .net接收post请求并把数据转为字典格式
C# -- HttpWebRequest 和 HttpWebResponse 的使用 C# -- HttpWebRequest 和 HttpWebResponse 的使用 结合使用HttpWebReq ...
- 基于JavaFX图形界面演示的迷宫创建与路径寻找
事情的起因是收到了一位网友的请求,他的java课设需要设计实现迷宫相关的程序--如标题概括. 我这边不方便透露相关信息,就只把任务要求写出来. 演示视频指路: 视频过审后就更新链接 完整代码链接: 网 ...
- [置顶] 使用红孩儿工具箱完成基于Cocos2d-x的简单游戏动画界面
[Cocos2d-x相关教程来源于红孩儿的游戏编程之路CSDN博客地址:http://blog.csdn.net/honghaier 红孩儿Cocos2d-X学习园地QQ3群:205100149,47 ...
- 原生javascript扫雷游戏
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- 【Android】自己动手做个扫雷游戏
1. 游戏规则 扫雷是玩法极其简单的小游戏,点击玩家认为不存在雷的区域,标记出全部地雷所在的区域,即可获得胜利.当点击不包含雷的块的时候,可能它底下存在一个数,也可能是一个空白块.当点击中有数字的块时 ...
- C#编写扫雷游戏
翻看了下以前大学学习的一些小项目,突然发现有个项目比较有意思,觉得有必要把它分享出来.当然现在看来,里面有很多的不足之处,但因博主现在已经工作,没有时间再去优化.这个项目就是利用C#编写一个Windo ...
- Java基于opencv实现图像数字识别(五)—投影法分割字符
Java基于opencv实现图像数字识别(五)-投影法分割字符 水平投影法 1.水平投影法就是先用一个数组统计出图像每行黑色像素点的个数(二值化的图像): 2.选出一个最优的阀值,根据比这个阀值大或小 ...
- (转载)WinformGDI+入门级实例——扫雷游戏(附源码)
本文将作为一个入门级的.结合源码的文章,旨在为刚刚接触GDI+编程或对相关知识感兴趣的读者做一个入门讲解.游戏尚且未完善,但基本功能都有,完整源码在文章结尾的附件中. 整体思路: 扫雷的游戏界面让我从 ...
- java实现简单扫雷游戏
/** * 一个简单的扫雷游戏 MainFram.java */ package www.waston; import java.awt.BorderLayout; import java.awt.C ...
随机推荐
- 08列表(list)与元组(tuple)
列表(list)与元组(tuple) 列表的格式 >- [数据1,数据2,数据3,数据4,......] >- 列表可以存储多个数据,数据之间的逗号以英文分割而且可以数据是不同类型的数据, ...
- Django笔记二十三之case、when操作条件表达式搜索、更新等操作
本文首发于公众号:Hunter后端 原文链接:Django笔记二十三之条件表达式搜索.更新等操作 这一篇笔记将介绍条件表达式,就是如何在 model 的使用中根据不同的条件筛选数据返回. 这个操作类似 ...
- Hyperledger Fabric 使用 CouchDB 和复杂智能合约开发
前言 在上个实验中,我们已经实现了简单智能合约实现及客户端开发,但该实验中智能合约只有基础的增删改查功能,且其中的数据管理功能与传统 MySQL 比相差甚远.本文将在前面实验的基础上,将 Hyperl ...
- Karmada v1.5发布:多调度组助力成本优化
摘要:在最新发布的1.5版本中,Karmada 提供了多调度组的能力,利用该能力,用户可以实现将业务优先调度到成本更低的集群,或者在主集群故障时,优先迁移业务到指定的备份集群. 本文分享自华为云社区& ...
- Django 如何使用 Celery 完成异步任务或定时任务
以前版本的 Celery 需要一个单独的库(django-celery)才能与 Django 一起工作, 但从 Celery 3.1 开始,情况便不再如此,我们可以直接通过 Celery 库来完成在 ...
- Django笔记三十之log日志记录详解
本文首发于公众号:Hunter后端 原文链接:Django笔记三十之log日志的记录详解 这一节介绍在 Django 系统里使用 logging 记录日志 以下是一个简单的 logging 模块示例, ...
- IE盒模型和标准盒模型之间的差别
1.W3C标准盒子模型 w3c盒子模型的范围包括margin.border.padding.content,并且content部分不包含其他部分 2.IE盒子模型 IE盒子模型的范围包括margin. ...
- CSS绘制虚线的方案
一.实现效果 二.代码实现 <div class="line"></div> .line { width: 1px; /* 虚线宽度 */ backgrou ...
- 讯飞星火大模型 与New Bing实测对比
昨天科大讯飞发布了讯飞星火认知大模型,在发布会现场实测大模型的7种核心能力,并发布了它在教育.办公.汽车.数字员工领域的应用成果.科大讯飞董事长刘庆峰表示:认知大模型展示了通用人工智能的曙光,讯飞星火 ...
- SaaS化开源项目之HouseKeeper云上部署实践
摘要:华为云DTSE技术专家从源码构建.应用部署到系统调测,详细解读云原生SaaS应用构建的全过程. 本文分享自华为云社区<HouseKeeper云上部署实践>,作者:华为云DTSE. H ...