一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
前言
如果问你,桌面图标除了双击启动程序外还可以干嘛?
当然是当游戏玩了,所以你就会看到下面这幅图。
下载地址: houxinlin.com:8080/Game.rar
注意这个地址的服务器是前几章使用汇编实现的,想看看现实中是否可以简单支撑,如果无法访问,请留言。
下面说如何实现。
实现原理
本文需要了解的东西居多,看官看看就行,如需详细了解,需要先了解Windows开发,本文不会详细说每个知识点。
这里面有两个问题是java实现不了的,也就是注册热键和移动桌面图标,所以这部分用c来实现,下面是定义的一些jni接口。
public class GameJNI {
public native void registerHotkeyCallback(HotkeyCallback callback);
public native int getDesktopIcon();
public native void moveDesktopIcon(int index,int x,int y);
}
复制代码
如果不懂Windows机制,可能会无法理解到底怎么移动桌面图标,简短的说一下,Windows是基于消息机制的,消息告诉某个窗口发生了什么事情,比如当你注册热键后,这个热键被按下,那么在Windows的窗口函数中就会得到响应,窗口函数中会得到具体消息的标识符,热键的标识符是WM_HOTKEY,而还有其他一同由系统传递过来的参数如wParam、lParam。
而移动图标就是向图标所在的窗口发送一个消息,表示有人想重新设置某个图标位置。
首先看registerHotkeyCallback的实现。
void RegisterHotkey(int id,int vkCode) {
if (RegisterHotKey(NULL, id, 0, vkCode) == 0) {
printf("fail %d\n", vkCode);
}
}
JNIEXPORT void JNICALL Java_com_h_game_jni_GameJNI_registerHotkeyCallback
(JNIEnv *env, jobject thisObj, jobject callbackObject) {
RegisterHotkey(LEFT_ID,37);
RegisterHotkey(UP_ID,38);
RegisterHotkey(RIGHT_ID,39);
RegisterHotkey(DOWN_ID,40);
fflush(stdout);
MSG lpMsg = {0};
while (GetMessage(&lpMsg, NULL, 0, 0)!=0){
jclass thisClass = (*env)->GetObjectClass(env, callbackObject);
jmethodID hotKeyCallbackMethodId = (*env)->GetMethodID(env, thisClass, "hotkey", "(I)V");
if (NULL == hotKeyCallbackMethodId) return;
if (lpMsg.message== WM_HOTKEY){
jstring result = (jstring)(*env)->CallObjectMethod(env, callbackObject, hotKeyCallbackMethodId, lpMsg.wParam);
}
}
}
复制代码
这部分代码是最多的,首先通过RegisterHotKey函数注册4个热键,分别是←↑→↓,并给他们起一个id分别为1、2、3、4,然后不断通过GetMessage从消息队列获取消息,GetMessage通常都是在窗体应用中使用的,而此时对于他来说,没有窗体,所以第二个参数为NULL,第一个参数是队列中到达的消息信息,当消息是WM_HOTKEY,表示我们按下了定义的热键,在通过java为我们提供的交互API,拿到回调地址,进行调用。
在看getDesktopIcon的实现,用来获取桌面图标个数,桌面也可以当做一个窗口,而桌面中排列图标的是一个ListView,当这个ListView收到LVM_GETITEMCOUNT消息时候,表示有人想获取他的的图标个数,然后他会返回给我们,而Windows提供了SendMessage函数,用来给指定窗口发送一个消息,并且是有返回值的,还有一个用于无返回值函数PostMessage,下面会说。
int GetDesktopIconCount() {
return SendMessage(GetWindowHwnd(), LVM_GETITEMCOUNT, 0, 0);
}
JNIEXPORT jint JNICALL Java_com_h_game_jni_GameJNI_getDesktopIcon
(JNIEnv *env, jobject thisObj) {
return GetDesktopIconCount();
}
复制代码
而要想得到桌面中ListView得句柄,需要这样做,但这段代码我不确定是否能在Win11上正常运行,因为每个系统中桌面结构可能不一样,Win7中和Win10就是不一样的,如果Win11中没有更改这个结构,这段代码会正常运行。
int GetWindowHwnd() {
HWND hwndWorkerW = { 0 };
hwndWorkerW = FindWindow(NULL,TEXT("Program Manager"));
hwndWorkerW = FindWindowEx(hwndWorkerW, 0, TEXT("SHELLDLL_DefView"), NULL);
hwndWorkerW = FindWindowEx(hwndWorkerW, 0,TEXT("SysListView32"), TEXT("FolderView"));
return hwndWorkerW;
}
复制代码
这里又牵扯到很多知识点,句柄和查找句柄,句柄是一个整数,用来标识唯一的一个应用窗口,查找句柄就可以根据类名或者窗口标题名进行查找,而这里会包含子父级关系,详细需要了解CreateWindow函数。
下面是移动图标实现,当那个ListView收到LVM_SETITEMPOSITION消息后,会根据其他参数设置位置,而这里使用PostMessage投递消息是因为我们不需要返回值,如果使用SendMessage,会慢很多。
void MoveDesktopIcon(int iconIndex,int x,int y) {
PostMessage(GetWindowHwnd(), LVM_SETITEMPOSITION, iconIndex, ConversionXY(x, y));
}
int ConversionXY(int x, int y) {
return y * 65536 + x;
}
JNIEXPORT void JNICALL Java_com_h_game_jni_GameJNI_moveDesktopIcon
(JNIEnv *env, jobject thisObj, jint index, jint x, jint y) {
MoveDesktopIcon(index, x, y);
}
复制代码
好了下面是java代码实现。
public class Main {
static {
System.load("Game.dll");
}
public static void main(String[] args) {
new SnakeGame();
}
}
复制代码
这部分比较简单了,没什么好说的,由c已经提供好了基本对图标的操作,这里直接调用即可。
注意这里我们拿图标索引0当作头,索引最后一个当做食物,比如有10个图标,0是头,9是食物,1-8是身体,每当根据方向移动头时,先记录下移动之前的位置,当头移动后,把他记录的位置传递给第1个图标,然后在把索引1的原来位置传递给第2个,依次类推,就形成了贪吃蛇。
public class SnakeGame extends JFrame implements HotkeyCallback {
private static final int WINDOW_WIDTH = 300;
private static final int WINDOW_HEIGHT = 200;
private GameJNI gameJNI = new GameJNI();
private ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
private Direction direction = Direction.RIGHT;
private Point snakePoint = new Point(0, 0);
private static final int MOVE_SIZE = 84;
private List<Point> snakeBodyPoint = new ArrayList<>();
private Point foodPoint = new Point(0, 0);
private List<Point> allPoint = generatorPoints();
private ScheduledFuture<?> scheduledFuture = null;
public SnakeGame() {
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
this.setLocation(screenSize.width / 2 - WINDOW_WIDTH / 2, screenSize.height / 2 - WINDOW_HEIGHT / 2);
this.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
init();
this.setVisible(true);
}
private void startGame() {
this.setVisible(false);
if (scheduledFuture == null) {
scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(() -> {
Point oldPoint = snakePoint.getLocation();
if (direction == Direction.LEFT) snakePoint.x -= MOVE_SIZE;
if (direction == Direction.UP) snakePoint.y -= MOVE_SIZE;
if (direction == Direction.RIGHT) snakePoint.x += MOVE_SIZE;
if (direction == Direction.DOWN) snakePoint.y += MOVE_SIZE;
moveSnakeHeader();
moveSnakeBody(oldPoint);
isCollision();
}, 0, 200, TimeUnit.MILLISECONDS);
}
}
private void isCollision() {
if (snakeBodyPoint.stream().anyMatch(point -> point.equals(snakePoint))) {
scheduledFuture.cancel(true);
scheduledFuture = null;
this.setVisible(true);
}
}
private void moveSnakeHeader() {
gameJNI.moveDesktopIcon(0, snakePoint.x, snakePoint.y);
if (foodPoint.equals(snakePoint)) resetBodyLocation();
}
private List<Point> generatorPoints() {
List<Point> all = new ArrayList<>();
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
for (int i = 0; i < screenSize.width; i += MOVE_SIZE) {
for (int j = 0; j < screenSize.height; j += MOVE_SIZE) {
all.add(new Point(i, j));
}
}
return all;
}
private void resetBodyLocation() {
List<Point> newPoint = allPoint.stream().filter(point -> !has(point)).collect(Collectors.toList());
Collections.shuffle(newPoint);
Point point = newPoint.get(0);
int desktopIcon = gameJNI.getDesktopIcon();
foodPoint.setLocation(point.x, point.y);
gameJNI.moveDesktopIcon(desktopIcon - 1, point.x, point.y);
}
private boolean has(Point hasPoint) {
return snakeBodyPoint.stream().anyMatch(point -> hasPoint.equals(point));
}
private void moveSnakeBody(Point oldPoint) {
for (int i = 1; i < snakeBodyPoint.size() - 1; i++) {
Point itemPoint = snakeBodyPoint.get(i);
gameJNI.moveDesktopIcon(i, oldPoint.x, oldPoint.y);
snakeBodyPoint.set(i, oldPoint.getLocation());
oldPoint = itemPoint;
}
}
private void init() {
this.setLayout(new BorderLayout());
String str ="<html>首先右击桌面,查看>取消自动排列图片、将网格与图片对齐。方向键为← ↑ → ↓</html>";
JLabel jLabel = new JLabel(str);
jLabel.setFont(new Font("黑体",0,18));
add(jLabel, BorderLayout.NORTH);
add(createButton(), BorderLayout.SOUTH);
registerHotkey();
reset();
}
private void reset() {
snakeBodyPoint.clear();
direction = Direction.RIGHT;
snakePoint.setLocation(0, 0);
int desktopIcon = gameJNI.getDesktopIcon();
int offsetX = -MOVE_SIZE;
for (int i = 0; i < desktopIcon; i++) {
snakeBodyPoint.add(new Point(offsetX, 0));
gameJNI.moveDesktopIcon(i, offsetX, 0);
offsetX -= MOVE_SIZE;
}
resetBodyLocation();
}
private JButton createButton() {
JButton jButton = new JButton("开始");
jButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
reset();
startGame();
}
});
return jButton;
}
@Override
public void hotkey(int key) {
if (key == 1) direction = Direction.LEFT;
if (key == 2) direction = Direction.UP;
if (key == 3) direction = Direction.RIGHT;
if (key == 4) direction = Direction.DOWN;
}
public void registerHotkey() {
new Thread(() -> gameJNI.registerHotkeyCallback(this)).start();
}
enum Direction {
LEFT, UP, RIGHT, DOWN
}
}
复制代码