写在前面
我的QQ宠物已经读大学啦,每天吃得饱饱的,每次回到家,我学习的时候也会让她也学习,我想着以后大学我一定有更多的时间陪她,可谁知2018年9月15日她却回到了自己的故乡。 时间过得也快,转眼就大三了,这些年也学了不少知识,爱上了动漫《罗小黑战记》,谁知又要停更三年呢?罗小黑说:“我想和小白一起学读书!”,好呀~那就来读书吧!
想过多种语言来编写,比如C#、Python、C++,但还是选择了自己最熟悉的Java,谢谢燕然都护的博客给的思路,打算做一个更完整的桌面宠物,模仿原来QQ宠物的饥饿度、健康值、心情值、金币系统、学习成长系统,结合番剧、电影的故事背景添加法力值等等等等。最终做成一款可安装式的C/S架构的游戏,让喜欢罗小黑战记的人在三年的等待时间内都可以来陪伴他~
第一版雏形
项目目录
使用的IDE是JetBrains Intellij IDEA,新建一个普通的Java项目,这个为项目目录
主入口程序
整个程序从主入口程序进入,下面是HelloHeiApplication类源码:
package org.taibai.hellohei;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.taibai.hellohei.constant.Constant;
import org.taibai.hellohei.event.GlobalEventListener;
import org.taibai.hellohei.img.ResourceGetter;
import org.taibai.hellohei.ui.InterfaceFunction;
import java.io.IOException;
public class HelloHeiApplication extends Application {
private ImageView imageView;
private AnchorPane pane;
private InterfaceFunction interfaceFunction;
private GlobalEventListener globalEventListener;
private final ResourceGetter resourceGetter = ResourceGetter.newInstance();
@Override
public void start(Stage primaryStage) throws IOException {
primaryStage.initStyle(StageStyle.UTILITY);
primaryStage.setOpacity(0);
Stage stage = new Stage();
stage.initOwner(primaryStage);
initImageView();
interfaceFunction = new InterfaceFunction(stage, imageView);
pane = new AnchorPane(interfaceFunction.getMessageBox(), interfaceFunction.getImageView());
pane.setStyle("-fx-background:transparent;");
globalEventListener = new GlobalEventListener(stage, imageView, pane);
initStage(stage);
primaryStage.show();
stage.show();
interfaceFunction.setTray(stage);
}
public static void main(String[] args) {
launch(args);
}
private void initImageView() {
Image image = resourceGetter.get(Constant.ImageShow.mainImage);
this.imageView = new ImageView(image);
imageView.setX(0);
imageView.setY(0);
imageView.setLayoutX(0);
imageView.setLayoutY(50);
imageView.setFitHeight(Constant.ImageShow.ImageHeight);
imageView.setFitHeight(Constant.ImageShow.ImageWidth);
imageView.setPreserveRatio(true);
imageView.setStyle("-fx-background:transparent;");
}
private void initStage(Stage stage) {
Scene scene = new Scene(pane, 400, 400);
scene.setFill(null);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
stage.setScene(scene);
stage.setX(850);
stage.setY(400);
stage.setAlwaysOnTop(true);
stage.getIcons().add(resourceGetter.get(Constant.ImageShow.iconImage));
stage.initStyle(StageStyle.TRANSPARENT);
stage.setOnCloseRequest(event -> {
event.consume();
interfaceFunction.exit();
});
}
}
依次说明一下:
- primaryStage并非正在的stage,在start方法中又新建了一个
Stage 实例,并且让primaryStage隐藏,这样做的目的是就不会在任务栏里面显示进程了,否则强迫症患者会很难受的。 ImageView 将作为整个程序展示的窗口,一系列动作也只是替换gif图片罢了- 交互平台
InterfaceFunction 提供了用户的一系列交互动作,例如显示隐藏、退出、切换状态,以后的各种功能也将在交互平台扩展 - 全局事件监听者
GlobalEventListener :考虑到各事件之间可能会相互干扰,于是开了一个类去集中管理。意在解决重复触发点击事件、拖动时不触发点击事件等问题。这样也让start方法更轻便一些。 - 最后将交互平台加入系统托盘,于是你可以在任务栏里像找到QQ程序一样找到
小黑后台
全局常量
虽然全局常量不太好,目前功能单一,全局常量有助于调试,希望在后面能选择更好的解决方案
package org.taibai.hellohei.constant;
public class Constant {
public static class ImageShow {
public static final int ImageHeight = 100;
public static final int ImageWidth = 100;
public static final String mainImage = "/org/taibai/hellohei/img/licking the claw.gif";
public static final String byeImage = "/org/taibai/hellohei/img/bye.gif";
public static final String iconImage = "/org/taibai/hellohei/img/icon.png";
public static final String guitarImage = "/org/taibai/hellohei/img/playing guitar.gif";
}
public static class UserInterface {
public static final int RunTime = 3;
public static final String[] selfTalking = {
"嘿咻~",
"点我~",
"小白,这个字怎么念呀",
"想吃甘蔗了……",
"在干嘛呢~"
};
}
}
动作
动作基本类
一个动作应该有如下属性
- path: 该动作是什么
- time: 执行多少时间
- isTemporaryAction: 是否是暂时的,比如点击后触发的动作是展示显示的,而恢复到默认状态是持续的
- recoverPath: 如果是暂时的那么应该恢复到什么动作
- interruptable: 是否可中断的,例如退出动画是不可中断的,而在做普通动画是可中断的,这样退出动画就得以显示
package org.taibai.hellohei.ui;
public class Action {
private final String path;
private final double time;
private final boolean isTemporaryAction;
private String recoverPath;
private final boolean interruptable;
public static final double PerpetualTime = -1.0;
private Action(String path, double time, boolean isTemporaryAction, String recoverPath, boolean interruptable) {
this.path = path;
this.time = time;
this.isTemporaryAction = isTemporaryAction;
this.recoverPath = recoverPath;
this.interruptable = interruptable;
}
private Action(String path, double time, boolean isTemporaryAction, boolean interruptable) {
this.path = path;
this.time = time;
this.isTemporaryAction = isTemporaryAction;
this.interruptable = interruptable;
}
public static Action creatTemporaryInterruptableAction(String path, double time, String recoverPath) {
return new Action(path, time, true, recoverPath, true);
}
public static Action creatContinuousInterruptableAction(String path) {
return new Action(path, PerpetualTime, false, true);
}
public static Action creatTemporaryUninterruptibleAction(String path, double time, String recoverPath) {
return new Action(path, time, true, recoverPath, false);
}
public static Action creatContinuousUninterruptibleAction(String path) {
return new Action(path, PerpetualTime, false, false);
}
public String getPath() {
return path;
}
public double getTime() {
return time;
}
public boolean isTemporaryAction() {
return isTemporaryAction;
}
public String getRecoverPath() {
return recoverPath;
}
public boolean isInterruptable() {
return interruptable;
}
}
并且采纳《Effective Java》“隐藏”了构造函数,并且提供公开的接口来构造四种类型的动作,分别是
creatTemporaryInterruptableAction :暂时的、可中断的动作creatContinuousInterruptableAction :持续的、可中断的动作creatTemporaryUninterruptibleAction :暂时的、不可中断的动作creatContinuousUninterruptibleAction :持续的、不可中断的动作
动作生成者
一个动作的产生是随机的,如果放在动作类或者动作执行类不太妥当,因此将其独立管理,构建了一个名为ActionGenerator 的动作生成者类
package org.taibai.hellohei.ui;
import org.taibai.hellohei.constant.Constant;
import java.util.HashMap;
import java.util.Map;
public class ActionGenerator {
private int actionIndex = NoAction;
private static final Map<Integer, String> resource = new HashMap<Integer, String>() {{
put(1, Constant.ImageShow.guitarImage);
}};
private static final int MinIndex = 1;
private static final int MaxIndex = 1;
public static final int NoAction = 0;
public boolean generateNewActionIndex() {
if (actionIndex != NoAction) return false;
actionIndex = (int) (Math.random() * (MaxIndex - MinIndex + 1) + MinIndex);
return true;
}
public void close() {
actionIndex = NoAction;
}
public String getActionPath() {
if (resource.containsKey(actionIndex))
return resource.get(actionIndex);
return null;
}
}
这里约定一个动作的开启必须要关闭,不关闭将不会再生成动作。
动作执行者
动作的执行是互相影响的,例如连续点击不应该连续触发动作等,因此将其独立出来,构建了一个动作执行者类ActionExecutor
package org.taibai.hellohei.ui;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Duration;
import org.taibai.hellohei.constant.Constant;
import org.taibai.hellohei.img.ResourceGetter;
public class ActionExecutor {
private ImageView imageView;
private Action curAction;
private final ResourceGetter resourceGetter = ResourceGetter.newInstance();
private final ActionGenerator actionGenerator = new ActionGenerator();
private static ActionExecutor actionExecutor;
private Timeline timeline;
public static ActionExecutor newInstance(ImageView imageView) {
if (actionExecutor == null) actionExecutor = new ActionExecutor(imageView);
return actionExecutor;
}
private ActionExecutor(ImageView imageView) {
this.imageView = imageView;
}
public boolean execute(Action action) {
if (curAction != null && !curAction.isInterruptable()) return false;
Image actionImage = resourceGetter.get(action.getPath());
imageView.setImage(actionImage);
curAction = action;
if (timeline != null) timeline.pause();
if (action.isTemporaryAction()) {
timeline = new Timeline(new KeyFrame(Duration.seconds(action.getTime()), e -> executeContinuousInterruptableActionAction(action.getRecoverPath())));
timeline.play();
}
return true;
}
public boolean executeClickAction() {
boolean ok = actionGenerator.generateNewActionIndex();
if (ok) {
execute(Action.creatTemporaryInterruptableAction(
actionGenerator.getActionPath(),
Constant.UserInterface.RunTime,
Constant.ImageShow.mainImage));
}
return ok;
}
private void executeContinuousInterruptableActionAction(String path) {
curAction = null;
timeline = null;
actionGenerator.close();
Action action = Action.creatContinuousInterruptableAction(path);
execute(action);
}
}
动作的执行是影响全局的,因此将其设计为单例模式,这样全局拿到的就是同一个对象。所产生的影响也是全局同步的。
事件
事件起初遇到了点麻烦,就是拖动也会触发点击事件,如果在主入口程序设置会很繁琐,因此我将事件管理划到了一个类中,这样拖动时记录初始坐标,松开鼠标时只需要判断坐标值是不是一样的,如果是一样的就说明在原地,执行点击事件(就是逗小黑玩)。虽然有可能拖动到同一个地方,但用户既然要拖拽肯定是想移动一个位置,所以不大可能回到原来的位置(就算故意移回到原位也很困难是吧~)
package org.taibai.hellohei.event;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import org.taibai.hellohei.constant.Constant;
import org.taibai.hellohei.img.ResourceGetter;
import org.taibai.hellohei.ui.Action;
import org.taibai.hellohei.ui.ActionExecutor;
import org.taibai.hellohei.ui.ActionGenerator;
public class GlobalEventListener {
private final Stage stage;
private final ImageView imageView;
private final AnchorPane anchorPane;
private final ActionExecutor actionExecutor;
private double xOffset = 0;
private double yOffset = 0;
private double preScreenX = 0;
private double preScreenY = 0;
public GlobalEventListener(Stage stage, ImageView imageView, AnchorPane anchorPane) {
this.stage = stage;
this.imageView = imageView;
this.anchorPane = anchorPane;
this.actionExecutor = ActionExecutor.newInstance(imageView);
enableDrag();
enableClick();
}
private void enableDrag() {
anchorPane.setOnMousePressed(e -> {
xOffset = e.getSceneX();
yOffset = e.getSceneY();
});
anchorPane.setOnMouseDragged(e -> {
stage.setX(e.getScreenX() - xOffset);
stage.setY(e.getScreenY() - yOffset);
});
}
private void enableClick() {
imageView.setOnMousePressed(e -> {
preScreenX = e.getScreenX();
preScreenY = e.getScreenY();
});
imageView.setOnMouseReleased(e -> {
if (e.getScreenX() == preScreenX && e.getScreenY() == preScreenY) {
actionExecutor.executeClickAction();
}
});
}
}
资源加载器
整个程序的运作都需要GIF图片的显示,因此需要用一个类去加载GIF图片,这里使用类级别与属性级别的单例模式,降低了创建类所需要的时间,当然如果使用HashMap容易导致内存泄漏,因此使用WeekHashMap
扩展阅读 WeekHashMap 和HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。不过WeakHashMap的键是“弱键”。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。
package org.taibai.hellohei.img;
import javafx.scene.image.Image;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.WeakHashMap;
public class ResourceGetter {
private static final Map<String, Image> images = new WeakHashMap<>();
private static ResourceGetter singleton;
public static ResourceGetter newInstance() {
if (singleton == null) singleton = new ResourceGetter();
return singleton;
}
private ResourceGetter() {
}
public Image get(String path) {
if (!images.containsKey(path)) {
images.put(path, new Image(Objects.requireNonNull(this.getClass().getResourceAsStream(path))));
}
return images.get(path);
}
}
交互平台
目前的交互功能仅仅只有碎碎念、显示隐藏,希望后面能扩充点功能。交互平台开启一个线程,随机事件后触发一次交互功能,比如开启碎碎念功能后,将在随机事件后弹出消息框。
package org.taibai.hellohei.ui;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.taibai.hellohei.constant.Constant;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Objects;
import java.util.Random;
public class InterfaceFunction {
private final ImageView imageView;
private final ActionExecutor actionExecutor;
private final Stage stage;
private VBox messageBox;
private CheckboxMenuItem itemSay = new CheckboxMenuItem("碎碎念");
private final String greet = "好久不见鸭,想你了~";
public InterfaceFunction(Stage stage, ImageView imageView) {
this.stage = stage;
this.imageView = imageView;
this.actionExecutor = ActionExecutor.newInstance(imageView);
this.messageBox = new VBox();
initMessage();
say(greet, 8);
RandomEvent randomEvent = new RandomEvent();
new Thread(randomEvent).start();
}
private void initMessage() {
Label bubble = new Label();
bubble.setPrefWidth(100);
bubble.setWrapText(true);
bubble.setStyle("-fx-background-color: rgba(255,255,255,0.7); -fx-background-radius: 8px;");
bubble.setPadding(new Insets(7));
bubble.setFont(new javafx.scene.text.Font(14));
bubble.setTextFill(Color.web("#000000"));
Polygon triangle = new Polygon(0.0, 0.0, 8.0, 10.0, 16.0, 0.0);
triangle.setFill(new Color(1, 1, 1, 0.7));
messageBox.getChildren().addAll(bubble, triangle);
messageBox.setAlignment(Pos.BOTTOM_CENTER);
messageBox.setStyle("-fx-background:transparent;");
messageBox.setLayoutX(0);
messageBox.setLayoutY(0);
messageBox.setVisible(true);
}
public void exit() {
double time = 1.5;
actionExecutor.execute(Action.creatTemporaryUninterruptibleAction(Constant.ImageShow.byeImage, time, Constant.ImageShow.mainImage));
Platform.runLater(() -> say("再见~", Constant.UserInterface.SayingRunTime));
new Timeline(new KeyFrame(
Duration.seconds(time),
ae -> System.exit(0)))
.play();
}
public void say(String msg, int duration) {
Label lbl = (Label) messageBox.getChildren().get(0);
lbl.setText(msg);
messageBox.setVisible(true);
new Timeline(new KeyFrame(
Duration.seconds(duration),
ae -> {
messageBox.setVisible(false);
}))
.play();
}
public void setTray(Stage stage) {
SystemTray tray = SystemTray.getSystemTray();
BufferedImage image;
try {
PopupMenu popMenu = new PopupMenu();
popMenu.setFont(new Font("微软雅黑", Font.PLAIN, 14));
MenuItem itemShow = new MenuItem("显示");
itemShow.addActionListener(e -> Platform.runLater(() -> stage.show()));
MenuItem itemHide = new MenuItem("隐藏");
itemHide.addActionListener(e -> {
Platform.setImplicitExit(false);
Platform.runLater(stage::hide);
});
MenuItem itemExit = new MenuItem("退出");
itemExit.addActionListener(e -> exit());
popMenu.add(itemSay);
popMenu.addSeparator();
popMenu.add(itemShow);
popMenu.add(itemHide);
popMenu.add(itemExit);
image = ImageIO.read(Objects.requireNonNull(getClass().getResourceAsStream(Constant.ImageShow.iconImage)));
TrayIcon trayIcon = new TrayIcon(image, "小黑", popMenu);
trayIcon.setToolTip("小黑");
trayIcon.setImageAutoSize(true);
tray.add(trayIcon);
} catch (IOException | AWTException e) {
e.printStackTrace();
}
}
public ImageView getImageView() {
return imageView;
}
public VBox getMessageBox() {
return messageBox;
}
class RandomEvent implements Runnable {
@Override
public void run() {
while (true) {
Random rand = new Random();
long time = (rand.nextInt(15) + 10) * 1000;
if (itemSay.getState()) {
String str = Constant.UserInterface.selfTalking[rand.nextInt(5)];
Platform.runLater(() -> say(str, Constant.UserInterface.SayingRunTime));
}
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
之后功能还会继续扩充,苦命考研狗,先去学习了~
|