如何使用 Java 实现后台全局监听快捷键

  说起使用 Java 实现快捷键,很多人都能想到,借助 Java 领域的 GUI 技术(如 Java Swing、JavaFX 等)就可以实现快捷键的功能。但是,使用这种方法实现的快捷键有一个致命的缺陷,那就是如果光标焦点离开此 UI 界面,则所有的快捷键都将失效。另外,这些快捷键是依附于一种 UI 界面来运行的,因此将占用额外的资源。这些都是一个很大的限制。本文就来探究一下如何设置一个后台运行的全局快捷键。

  给出的解决方案必须要让别人知道这个解决方案帮助别人解决了什么问题。本文的全局快捷键可实现如下功能:

  • 程序可后台运行,无需依赖界面来维持。

  • 快捷键不会因为光标及界面切换而失效。

  充分利用上面的功能就已经可以实现绝大多数的需求了。如何实现呢?这里有一个叫 JIntellitype 的三方件,借助于它就可以轻松实现上述功能。

   JIntellitype 初次使用起来稍微有此麻烦。使用它需要 2 样东西,一是 JIntellitype 的 JAR 包、二是一种 DLL 文件。


【踩坑提醒】

  JIntellitype JAR 包的版本需要与 DLL 文件相匹配,否则将导致程序运行时异常中止。


  如何保证 JIntellitype JAR 包的版本需要与 DLL 文件相匹配呢?这一点,JIntellitype 官网做得并不好,还好 JIntellitype 官方的开源代码中提供了相应的示例程序以及相匹配 DLL 文件。因此,读者只需要在 JIntellitype 官方开源代码中分别拷贝官方 JIntellitype 的 Java 代码以及相匹配 DLL 文件即可。不过,笔者这里已经帮读者完成了这个工作,完整代码将在文末给出。


【提示】

  • JIntellitype 的 Java 源代码只需要拷贝 Java 包目录 com.melloware.jintellitype 中的即可,其它的比如 Maven 的各种配置信息不需要拷贝。

  • 与之相匹配的 DLL 文件位于 Maven 项目的资源目录 resources 中,这里有两个文件(JIntellitype.dllJIntellitype64.dll),拷贝后的相对路径也不能改变。


实战

  为了便于演示,笔者编写了一个示例代码(位于包 org.wangpai.demo 下的 GlobalShortcutsDemo.java 中),该代码模拟了使用快捷键进行某程序的开启和关闭功能,其中演示了使用单个快捷键以及组合键,和使用多线程来避免阻塞的问题。该程序相当容易理解,所以不作过多阐述,只是有一些注意事项如下。


【注意事项】

  • 如果想要设置组合键,应使用加号 + 而不是异或 |

  • 应该将快捷键触发后的耗时任务置入其它线程来执行,否则就会阻塞快捷键线程,从而导致此时所有的快捷键均失效。

  • 设置快捷键应考虑其与已有快捷键之间的冲突的问题。


笔者的运行环境:

  • Windows 10 专业版

  • JDK 17

  • Maven 3.8.3

  • IntelliJ IDEA 2021.3 (Ultimate Edition)

GlobalShortcutsDemo.java

package org.wangpai.demo;

import com.melloware.jintellitype.HotkeyListener;
import com.melloware.jintellitype.JIntellitype;
import org.wangpai.commonutil.multithreading.CentralDatabase;
import org.wangpai.commonutil.multithreading.Multithreading;

/**
 * JIntellitype 用法示例
 *
 * @since 2022-6-25
 */
public class GlobalShortcutsDemo {
    public static final int FIRST_SHORTCUT = 1; // 模拟任务开始快捷键
    public static final int SECOND_SHORTCUT = 2; // 模拟手动结束任务快捷键
    private static HotkeyListener hotkeyListener = null;

    public static void main(String[] args) {
        addGlobalShortcuts();
    }

    /**
     * @since 2022-6-25
     */
    private static void run() {
        try {
            Thread.sleep(10000); // 模拟耗时任务(10 秒)
        } catch (InterruptedException exception) {
            System.out.println("任务运行在完成前被中止!");
            exception.printStackTrace();
        }
    }

    /**
     * @since 2022-6-25
     */
    private static void addGlobalShortcuts() {
        JIntellitype.getInstance().registerHotKey(FIRST_SHORTCUT, 0, 'K'); // K
        JIntellitype.getInstance().registerHotKey(SECOND_SHORTCUT,
                JIntellitype.MOD_SHIFT + JIntellitype.MOD_ALT, 'L'); // SHIFT + ALT + L
        hotkeyListener = code -> {
            switch (code) {
                case FIRST_SHORTCUT -> {
                    System.out.println("快捷键 K 被触发,任务开始");
                    Multithreading.execute(GlobalShortcutsDemo::run); // 开启子线程来运行耗时任务
                }
                case SECOND_SHORTCUT -> {
                    System.out.println("快捷键 SHIFT + ALT + L 被触发,任务结束");
                    JIntellitype.getInstance().removeHotKeyListener(hotkeyListener); // 移除快捷键触发后的动作
                    JIntellitype.getInstance().unregisterHotKey(FIRST_SHORTCUT); // 移除快捷键 FIRST_SHORTCUT
                    JIntellitype.getInstance().unregisterHotKey(SECOND_SHORTCUT); // 移除快捷键 SECOND_SHORTCUT
                    CentralDatabase.multithreadingClosed(); // 中断子线程,回收子线程资源
                    System.exit(0); // 关闭主程序
                }
                default -> System.out.println("监听了此快捷键但是未定义其行为");
            }
        };
        JIntellitype.getInstance().addHotKeyListener(hotkeyListener); // 添加监听
        System.out.println("正在监听快捷键...");
    }
}

在这里插入图片描述

改进

  上面的功能显然有一个缺陷,那就是当用户使用快捷键关闭另一个线程的运行时,那个线程直接就停止了。有时候,我们希望这种中止只是一种暂停,并且我们希望可以通过快捷键来反复控制同一线程的暂停与继续执行。

  一个问题是,如何通过一个线程(不妨称为线程 A)来控制另一个线程(不妨称为线程 B)的暂停与恢复呢?

  有的读者可能想到使用多线程技术中所谓的 生产者-消费者 模式来实现,该模式借助于悲观锁中的一种 wait(阻塞调用 wait 方法的线程,并使该线程放弃锁)、notify(使之前因 wait 方法而阻塞的线程可以在调用 notify 方法的线程放弃锁之后解除阻塞) 之类的函数来实现。

  但是很遗憾,本问题不能通过此技术来实现,原因是,这种技术旨在实现多线程的串行化有序执行。也就是说,通过这项技术,可以让原本互相之间相对独立的线程转化为依次执行的线程,这会使得在任何时间,就只有一个线程可以运行。而对于本文的问题,是需要一个线程来控制另一个线程的运行,这两个线程都需要同时运行,所以,本文的这个问题不能通过此技术来实现。具体来说,上面的 wait、notify 方法都要求每个线程在获取锁之后才能调用,而这种悲观锁是一种独占锁,因此,在任何时间,就只有一个线程可以运行。本质来说,这种技术是用于实现线程的之间的互相协调(互相控制)运行,而本文的要求是一个线程控制另一个线程,这里不能使用这个技术。

  另一种简单的思路是,使用一种 布尔类型 的开关变量,线程 A 通过改变此变量的值来表示线程 B 的所被要求进入的状态。然后线程 B 不断监听这个布尔类型的值,并根据值的不同来调整自己的运行状态。不过,这个设计有一个问题。当线程 B 进入“暂停状态”时,对于线程 B 的实际行为应该如何设计呢?一般来说,此时应该让线程 B 进入休眠状态,这样线程 B 就不会占用 CPU 资源。不过,问题在于,当线程 B 进入“暂停状态”时,线程 A 如何使得线程 B 进入“恢复运行状态”呢?因为此时线程 B 已经休眠,所以它无法查看上面开关变量的值,也无关改变自已的状态。一种调整方法是,使线程 B 的休眠为一种“循环休眠”,并使用每次休眠的时间不太长,这样当每次退出休眠时就可以查看上面开关变量的值了。不过,虽然这种方法可以做到控制线程的暂停与恢复,但是这会使得线程的恢复出现延迟。而且休眠时间越短,对 CPU 的不必要消耗也就越大,因此,这种方法不是一种最好的方法。

  那么,应该如何优化呢?可以借助线程的 休眠中断 技术。线程有一个内置机制,如果一个线程正在休眠,然后其它线程如果对其设置了中断状态,那么该线程就会立刻解除休眠并抛出一个 InterruptedException 异常。利用此机制,就可以解决上面的“恢复延迟”问题。

  下面改进之后的代码就实现了这样的功能。

GlobalShortcutsDemo.java

package org.wangpai.demo;

import com.melloware.jintellitype.HotkeyListener;
import com.melloware.jintellitype.JIntellitype;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;

/**
 * JIntellitype 用法示例
 *
 * @since 2022-6-25
 */
public class GlobalShortcutsDemo {
    private static final int FIRST_SHORTCUT = 1; // 模拟任务开始快捷键
    private static final int SECOND_SHORTCUT = 2; // 模拟手动暂停任务快捷键
    private static final int THIRD_SHORTCUT = 3; // 模拟手动结束任务快捷键
    private static final State state = new State();
    private static boolean started = false;
    private static int suspendWaitedLoop = 1;
    private static int runningLoop = 1;
    private static HotkeyListener hotkeyListener = null;

    public static void main(String[] args) {
        new Thread(GlobalShortcutsDemo::addGlobalShortcuts).start(); // 开启子线程来运行
    }

    /**
     * @since 2022-6-25
     * @lastModified 2022-6-29
     */
    private static void run() {
        while (true) {
            if (state.isRunning()) {
                try {
                    suspendWaitedLoop = 0;
                    System.out.println(String.format("-----第 %d 圈任务开始执行------", runningLoop++));
                    Thread.sleep(10000); // 模拟耗时任务(10 秒)
                    System.out.println(String.format("-----第 %d 圈任务执行完毕------", runningLoop));
                } catch (InterruptedException exception) {
                    exception.printStackTrace();
                    runningLoop = 1;
                    System.out.println("-----本圈任务未完成就被打断------");
                }
            } else {
                runningLoop = 1;
                if (suspendWaitedLoop == 0) {
                    System.out.println("-----任务暂停------");
                }
                ++suspendWaitedLoop;
                try {
                    Thread.interrupted(); // 在休眠前清除中断标志,否则有可能导致接下来的那次休眠失败
                    Thread.sleep(10000); // 休眠等待恢复运行
                } catch (InterruptedException exception) {
                    exception.printStackTrace();
                    suspendWaitedLoop = 0;
                    System.out.println("-----任务恢复运行-----");
                }
            }
        }
    }

    /**
     * @since 2022-6-25
     * @lastModified 2022-6-29
     */
    private static void addGlobalShortcuts() {
        JIntellitype.getInstance().registerHotKey(FIRST_SHORTCUT, 0, 'K'); // K
        JIntellitype.getInstance().registerHotKey(SECOND_SHORTCUT,
                JIntellitype.MOD_SHIFT + JIntellitype.MOD_ALT, 'L'); // SHIFT + ALT + L
        JIntellitype.getInstance().registerHotKey(THIRD_SHORTCUT,
                JIntellitype.MOD_SHIFT + JIntellitype.MOD_ALT, 'J'); // SHIFT + ALT + J
        final var mainTaskThread = new Thread(GlobalShortcutsDemo::run); // 开启子线程来运行
        hotkeyListener = code -> {
            switch (code) {
                case FIRST_SHORTCUT -> {
                    if (started) {
                        if (state.isRunning()) {
                            System.out.println("快捷键 K 被触发:任务正在运行,不需要再开始");
                        } else {
                            System.out.println("快捷键 K 被触发,任务恢复运行");
                            synchronized (state) { // 此处必须上锁
                                state.setRunning(true);
                                mainTaskThread.interrupt();
                            }
                        }
                    } else {
                        System.out.println("快捷键 K 被触发,任务开始");
                        synchronized (state) { // 此处必须上锁
                            state.setRunning(true);
                            mainTaskThread.start();
                        }
                        started = true;
                    }
                }
                case SECOND_SHORTCUT -> {
                    if (state.isRunning()) {
                        System.out.println("快捷键 SHIFT + ALT + L 被触发,任务暂停");
                        synchronized (state) { // 此处必须上锁
                            state.setRunning(false);
                            mainTaskThread.interrupt();
                        }
                    } else {
                        System.out.println("快捷键 SHIFT + ALT + L 被触发:任务已暂停,不需要再暂停");
                    }
                }
                case THIRD_SHORTCUT -> {
                    System.out.println("快捷键 SHIFT + ALT + J 被触发,任务中止");
                    JIntellitype.getInstance().removeHotKeyListener(hotkeyListener); // 移除快捷键触发后的动作
                    JIntellitype.getInstance().unregisterHotKey(FIRST_SHORTCUT); // 移除快捷键 FIRST_SHORTCUT
                    JIntellitype.getInstance().unregisterHotKey(SECOND_SHORTCUT); // 移除快捷键 SECOND_SHORTCUT
                    JIntellitype.getInstance().unregisterHotKey(THIRD_SHORTCUT); // 移除快捷键 THIRD_SHORTCUT
                    mainTaskThread.interrupt();
                    System.exit(0); // 关闭主程序
                }
                default -> System.out.println("监听了此快捷键但是未定义其行为");
            }
        };
        JIntellitype.getInstance().addHotKeyListener(hotkeyListener); // 添加监听
        System.out.println("正在监听快捷键...");
    }

    @Getter
    @Setter
    @ToString
    @Accessors(chain = true)
    static class State {
        private boolean running = false; // true 代表正在运行,false 代表暂停运行
    }
}

在这里插入图片描述

框架化

  笔者在之前的很多博客中已经提到了,框架是软件开发中最终的必然产物。任何软件在最终开发成熟之后都会框架化,框架是避免造轮子行为而必然诞生的产物。当前面已经编写好了设置全局快捷键的基本固定模式后,很容易感觉到,如果每次对一个新的功能就重新按照这种模式进行开发,就算能通过提取函数来重用公共代码,这依然使得每次都需要关注很多底层细节。比方说,每次都要记得需要在 XX 文件 XX 行增加新的内容等等。说实话,这很底层。虽然有人可能已经习惯干这样的事情,不过已经有技术可以避免每次都记住这些细节。

  IoC 是一个广为流传的思想,可以借助函数式编程来实现。这里不打算详细介绍函数式编程,笔者已经在之前的博客中深入探讨过了:【Java 函数式编程入门:https://blog.csdn.net/wangpaiblog/article/details/122762637】。


  函数式编程的基本步骤如下:

  • 将公共的程序运行流水线抽取出来,制成一套公共框架。

  • 将所有的非固定代码用接口方法来代替,设置在这套公共框架中,并将接口方法对外暴露。

  • 将非固定代码以依赖注入的方式注入到这套公共框架中。


  需要用户自定义的部分是触发快捷键之后的行为。当然,快捷键一般也需要自定义,但制定快捷键的文字格式的标准略微繁琐。简便起见,这一节选择将快捷键耦合到快捷键框架代码中。实际上不应该这么做,本文下一节 生命周期化 中将解决这个问题。

  现在就可以将设置全局快捷键的基本固定模式制成一套框架了。为此,需要一个对外暴露的接口,而且考虑编程的方便,最好能是 函数式接口。可以自行定义出这个接口,但由于类 Thread 已经附带了这个接口了,这个接口就是 Runnable,所以这里就干脆就借用这个接口好了。

  然后,需要将这个接口作为类字段以供框架类保存注入的代码,并将所有需要用户自定义的行为用这个接口的方法调用来代替。

  最后,在用户代码中将用户代码注入到这个框架类中。

  核心代码如下。

框架类 GlobalShortcutsFX

package org.wangpai.globalshortcuts;

import com.melloware.jintellitype.HotkeyListener;
import com.melloware.jintellitype.JIntellitype;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;

/**
 * JIntellitype 用法示例
 *
 * @since 2022-6-25
 */
public class GlobalShortcutsFX {
    private static final int FIRST_SHORTCUT = 1; // 模拟任务开始快捷键
    private static final int SECOND_SHORTCUT = 2; // 模拟手动暂停任务快捷键
    private static final int THIRD_SHORTCUT = 3; // 模拟手动结束任务快捷键
    private final State state = new State();
    private boolean started = false;
    private int suspendWaitedLoop = 1;
    private int runningLoop = 1;
    private HotkeyListener hotkeyListener = null;
    private Thread globalShortcutsThread;
    private Thread mainTaskThread;
    private Runnable customActivity;

    /**
     * 这是一个对外界暴露的函数,外界可以向其注入自定义方法以执行
     *
     * @since 2022-6-30
     */
    public void execute(Runnable function) {
        this.customActivity = function;
        this.globalShortcutsThread = new Thread(this::addGlobalShortcuts);
        this.globalShortcutsThread.start(); // 开启子线程来运行
    }

    /**
     * @since 2022-6-25
     * @lastModified 2022-6-29
     */
    private void run() {
        while (true) {
            if (this.state.isRunning()) {
                this.suspendWaitedLoop = 0;
                System.out.println(String.format("-----第 %d 圈任务开始执行------", this.runningLoop++));
                this.customActivity.run();
                System.out.println(String.format("-----第 %d 圈任务执行完毕------", this.runningLoop));
            } else {
                this.runningLoop = 1;
                if (this.suspendWaitedLoop == 0) {
                    System.out.println("-----任务暂停------");
                }
                ++this.suspendWaitedLoop;
                try {
                    Thread.interrupted(); // 在休眠前清除中断标志,否则有可能导致接下来的那次休眠失败
                    Thread.sleep(10000); // 休眠等待恢复运行
                } catch (InterruptedException exception) {
                    exception.printStackTrace();
                    this.suspendWaitedLoop = 0;
                    System.out.println("-----任务恢复运行-----");
                }
            }
        }
    }

    /**
     * @since 2022-6-25
     * @lastModified 2022-6-29
     */
    private void addGlobalShortcuts() {
        JIntellitype.getInstance().registerHotKey(FIRST_SHORTCUT, 0, 'K'); // K
        JIntellitype.getInstance().registerHotKey(SECOND_SHORTCUT,
                JIntellitype.MOD_SHIFT + JIntellitype.MOD_ALT, 'L'); // SHIFT + ALT + L
        JIntellitype.getInstance().registerHotKey(THIRD_SHORTCUT,
                JIntellitype.MOD_SHIFT + JIntellitype.MOD_ALT, 'J'); // SHIFT + ALT + J
        this.mainTaskThread = new Thread(this::run); // 开启子线程来运行
        this.hotkeyListener = code -> {
            switch (code) {
                case FIRST_SHORTCUT -> {
                    if (this.started) {
                        if (this.state.isRunning()) {
                            System.out.println("快捷键 K 被触发:任务正在运行,不需要再开始");
                        } else {
                            System.out.println("快捷键 K 被触发,任务恢复运行");
                            synchronized (this.state) { // 此处必须上锁
                                this.state.setRunning(true);
                                this.mainTaskThread.interrupt();
                            }
                        }
                    } else {
                        System.out.println("快捷键 K 被触发,任务开始");
                        synchronized (this.state) { // 此处必须上锁
                            this.state.setRunning(true);
                            this.mainTaskThread.start();
                        }
                        this.started = true;
                    }
                }
                case SECOND_SHORTCUT -> {
                    if (this.state.isRunning()) {
                        System.out.println("快捷键 SHIFT + ALT + L 被触发,任务暂停");
                        synchronized (this.state) { // 此处必须上锁
                            this.state.setRunning(false);
                            this.mainTaskThread.interrupt();
                        }
                    } else {
                        System.out.println("快捷键 SHIFT + ALT + L 被触发:任务已暂停,不需要再暂停");
                    }
                }
                case THIRD_SHORTCUT -> {
                    System.out.println("快捷键 SHIFT + ALT + J 被触发,任务中止");
                    JIntellitype.getInstance().removeHotKeyListener(this.hotkeyListener); // 移除快捷键触发后的动作
                    JIntellitype.getInstance().unregisterHotKey(FIRST_SHORTCUT); // 移除快捷键 FIRST_SHORTCUT
                    JIntellitype.getInstance().unregisterHotKey(SECOND_SHORTCUT); // 移除快捷键 SECOND_SHORTCUT
                    JIntellitype.getInstance().unregisterHotKey(THIRD_SHORTCUT); // 移除快捷键 THIRD_SHORTCUT
                    this.mainTaskThread.interrupt();
                    System.exit(0); // 关闭主程序
                }
                default -> System.out.println("监听了此快捷键但是未定义其行为");
            }
        };
        JIntellitype.getInstance().addHotKeyListener(this.hotkeyListener); // 添加监听
        System.out.println("正在监听快捷键...");
    }

    @Getter
    @Setter
    @ToString
    @Accessors(chain = true)
    static class State {
        private boolean running = false; // true 代表正在运行,false 代表暂停运行
    }
}

用户代码:GlobalShortcutsDemoMain.java

package org.wangpai.demo;

import org.wangpai.globalshortcuts.GlobalShortcutsFX;

public class GlobalShortcutsDemoMain {
    private static void customActivity() {
        try {
            Thread.sleep(10000); // 模拟耗时任务(10 秒)
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new GlobalShortcutsFX().execute(GlobalShortcutsDemoMain::customActivity);
    }
}

生命周期化

  前面已经提到如何使用函数式编程技术来向本 全局快捷键 应用注入需要执行的用户代码。现在来考虑更复杂的场景。

  在前面的场景中,已经可以通过快捷键来暂停用户主线程了,但如果用户在自已在代码中,又创建了新的线程,这种情况下,通过快捷键就只能暂停用户的主线程,而此时用户创建的子线程仍然继续执行,这显然是一个很严重的缺陷。但是,我们编写的框架又无法过多干涉用户的程序。出于封装、解耦的考虑,我们需要设计出一套用户程序的生命周期,让用户自已在程序生命周期的不同阶段注入并运行自已需要运行的代码。

  具体来说,可以设计出用户程序运行的一个生命周期,比如:开始、运行、暂停、恢复运行、终止运行等。这些生命周期将以钩子的形式暴露给用户,期待用户向其注入自已所需要在那个生命周期的阶段执行的代码。然后本 全局快捷键 应用框架只负责在不同阶段执行用户所注入的钩子函数。如果用户没有注入某个钩子,就执行默认行为,比如什么也不做。

  这里,可以向用户提供一个暂停的钩子方法,用户需要自已在此暂停钩子中去做一些用户主线程暂停时需要做的一些操作,如使用与暂停本用户主线程类似的方法,来暂停所有的自已创建的子线程。

  另外,上一节错误地将快捷键耦合进了框架中,这里也来解决这个问题。

  此外,本节还增加了其它功能。比如,在不违反逻辑的情况下,有时候需要一个快捷键能同时完成很多事情,如对于同一个快捷键,希望它在程序暂停时恢复程序,在程序运行时暂停程序等等。

  核心代码如下。

用户示例运行代码:GlobalShortcutsDemoMain.java

package org.wangpai.globalshortcuts.demo;

import com.melloware.jintellitype.JIntellitype;
import org.wangpai.globalshortcuts.GlobalShortcutsFX;
import org.wangpai.globalshortcuts.exception.GlobalShortcutsException;
import org.wangpai.globalshortcuts.model.JIntellitypeShortcut;

import static org.wangpai.globalshortcuts.model.GlobalShortcutsLifecycle.AFTER_ALL_INSTANCES_EXIT;
import static org.wangpai.globalshortcuts.model.GlobalShortcutsLifecycle.AFTER_EXIT;
import static org.wangpai.globalshortcuts.model.GlobalShortcutsLifecycle.AFTER_SUSPEND;
import static org.wangpai.globalshortcuts.model.GlobalShortcutsLifecycle.BEFORE_RESUME;
import static org.wangpai.globalshortcuts.model.GlobalShortcutsLifecycle.BEFORE_START;

public class GlobalShortcutsDemoMain {
    private static void customActivity() {
        try {
            Thread.sleep(1000); // 模拟耗时任务(1 秒)
        } catch (InterruptedException exception) {
            exception.printStackTrace();
        }
    }

    public static void main(String[] args) throws GlobalShortcutsException {
        new GlobalShortcutsFX()
                .setMainActivityAction(GlobalShortcutsDemoMain::customActivity)
                .setRepeatable(true)
                .setShortcut(BEFORE_START,
                        new JIntellitypeShortcut(JIntellitype.MOD_SHIFT + JIntellitype.MOD_ALT, 'J'),
                        () -> System.out.println("######## customActivity 开始执行 ########")
                )
                .setShortcut(AFTER_SUSPEND,
                        new JIntellitypeShortcut(JIntellitype.MOD_SHIFT + JIntellitype.MOD_ALT, 'K'),
                        () -> System.out.println("######## customActivity 暂停执行 ########")
                )
                .setShortcut(BEFORE_RESUME,
                        new JIntellitypeShortcut(JIntellitype.MOD_SHIFT + JIntellitype.MOD_ALT, 'J'),
                        () -> System.out.println("######## customActivity 恢复执行 ########")
                )
                .setShortcut(AFTER_EXIT,
                        new JIntellitypeShortcut(JIntellitype.MOD_SHIFT + JIntellitype.MOD_ALT, 'L'),
                        () -> System.out.println("######## customActivity 中止执行 ########")
                )
                .setShortcut(AFTER_ALL_INSTANCES_EXIT,
                        new JIntellitypeShortcut(JIntellitype.MOD_SHIFT + JIntellitype.MOD_ALT, 'M'),
                        () -> System.out.println("######## 本程序终止执行 ########")
                )
                .execute();
    }
}

框架类 GlobalShortcutsFX

package org.wangpai.globalshortcuts;

import com.melloware.jintellitype.HotkeyListener;
import com.melloware.jintellitype.JIntellitype;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.wangpai.globalshortcuts.exception.GlobalShortcutsException;
import org.wangpai.globalshortcuts.exception.IncompatibleShortcutException;
import org.wangpai.globalshortcuts.exception.RepeatedShortcutException;
import org.wangpai.globalshortcuts.model.GlobalShortcutsLifecycle;
import org.wangpai.globalshortcuts.model.JIntellitypeShortcut;

import static org.wangpai.globalshortcuts.model.GlobalShortcutsLifecycle.AFTER_ALL_INSTANCES_EXIT;
import static org.wangpai.globalshortcuts.model.GlobalShortcutsLifecycle.AFTER_EXIT;
import static org.wangpai.globalshortcuts.model.GlobalShortcutsLifecycle.AFTER_SUSPEND;
import static org.wangpai.globalshortcuts.model.GlobalShortcutsLifecycle.BEFORE_RESUME;
import static org.wangpai.globalshortcuts.model.GlobalShortcutsLifecycle.BEFORE_START;

/**
 * JIntellitype 用法示例
 *
 * @since 2022-6-25
 */
@Accessors(chain = true)
public class GlobalShortcutsFX {
    private final GlobalShortcutsRegister register = new GlobalShortcutsRegister();
    private volatile boolean isRunning = false; // true 代表正在运行,false 代表暂停运行
    private volatile boolean isStarted = false;
    private volatile boolean needTerminate = false;
    private int suspendWaitedLoop = 1;
    private int runningLoop = 1;
    private Thread globalShortcutsThread = null; // 此线程很快就会自动结束
    private Thread customActivityThread = null;

    @Setter
    private Runnable mainActivityAction = null; // 用户单次任务

    /**
     * 允许快捷键重复
     *
     * @since 2022-9-30
     */
    @Setter
    private boolean repeatable = false;

    /**
     * 如果对不同生命周期设置了同一个快捷键,则当此快捷键按下时,视为同时触发了所有相同快捷键的生命周期。这有可能会有危险
     *
     * 不支持为同一生命周期设置多个快捷键
     *
     * @since 2022-9-30
     */
    public GlobalShortcutsFX setShortcut(GlobalShortcutsLifecycle lifecycle, JIntellitypeShortcut shortcut,
                                         Runnable action) throws GlobalShortcutsException {
        if (!this.repeatable && this.register.isShortcutExist(shortcut)) {
            throw new RepeatedShortcutException("异常:此快捷键已存在");
        }
        this.register.registerShortcut(lifecycle, shortcut, action);
        if (!this.register.checkTheLogicOfShortcuts()) {
            throw new IncompatibleShortcutException("异常:此快捷键重复且不兼容");
        }
        return this;
    }

    /**
     * 这是一个对外界暴露的函数,外界可以向其注入自定义方法以执行
     *
     * 注意:此方法会清除之前所有设置的生命周期方法
     *
     * @since 2022-7-27
     */
    public void easyExecute(Runnable function) {
        this.mainActivityAction = function;
        if (this.globalShortcutsThread == null) {
            this.globalShortcutsThread = new Thread(this::addGlobalShortcuts);
            this.globalShortcutsThread.setName("globalShortcutsThread" + GlobalShortcutsRegister.random.nextInt());

        }
        this.globalShortcutsThread.start(); // 开启子线程来运行
    }

    /**
     * @since 2022-7-27
     */
    public void execute() {
        if (this.globalShortcutsThread == null) {
            this.globalShortcutsThread = new Thread(this::addGlobalShortcuts);
            this.globalShortcutsThread.setName("globalShortcutsThread" + GlobalShortcutsRegister.random.nextInt());
        }
        this.globalShortcutsThread.start(); // 开启子线程来运行
    }

    /**
     * @since 2022-6-25
     * @lastModified 2022-9-30
     */
    private void run() {
        while (!this.needTerminate) {
            if (this.isRunning) {
                this.suspendWaitedLoop = 0;
                System.out.println(String.format("-----第 %d 圈任务开始执行------", this.runningLoop));
                try { // 此处必须使用 try 块吞掉所有可能的异常,否则本线程容易因注入代码抛出异常而无声中止
                    if (this.mainActivityAction != null) {
                        this.mainActivityAction.run();
                    }
                } catch (Throwable throwable) {
                    System.out.println(throwable);
                }
                System.out.println(String.format("-----第 %d 圈任务执行完毕------", this.runningLoop));
                ++this.runningLoop;
            } else {
                this.runningLoop = 1;
                if (this.suspendWaitedLoop == 0) {
                    System.out.println("-----任务暂停------");
                }
                ++this.suspendWaitedLoop;
                try {
                    Thread.interrupted(); // 在休眠前清除中断标志,否则有可能导致接下来的那次休眠失败
                    Thread.sleep(Integer.MAX_VALUE); // 休眠等待恢复运行
                } catch (InterruptedException exception) {
                    exception.printStackTrace();
                    this.suspendWaitedLoop = 0;
                    System.out.println("-----任务恢复运行-----");
                }
            }
        }
    }

    /**
     * @since 2022-6-25
     * @lastModified 2022-9-30
     */
    private void addGlobalShortcuts() {
        this.customActivityThread = new Thread(this::run); // 开启子线程来运行
        this.customActivityThread.setName("customActivityThread" + GlobalShortcutsRegister.random.nextInt());
        HotkeyListener hotkeyListener = oneOfShortcutId -> {
            var lifecycles = this.register.getLifecycles(oneOfShortcutId);

            if (!this.isStarted && lifecycles != null && !lifecycles.contains(BEFORE_START)) {
                System.out.println("任务任务尚未开始");
                return;
            }
            if (!this.isStarted && lifecycles != null && lifecycles.contains(BEFORE_START)) {
                // 先执行用户注入的代码,然后才真正执行“开始”
                Runnable action = this.register.getAction(BEFORE_START);
                if (action != null) {
                    action.run();
                }
                System.out.println("任务开始");
                this.isStarted = true;
                this.isRunning = true;
                this.customActivityThread.start();
                return;
            }

            if (lifecycles.contains(BEFORE_RESUME)) {
                if (this.isRunning) {
                    System.out.println("任务正在运行,不需要恢复运行");
                } else {
                    // 先执行用户注入的代码,然后才真正执行“恢复运行”
                    var action = this.register.getAction(BEFORE_RESUME);
                    if (action != null) {
                        action.run();
                    }
                    System.out.println("任务恢复运行");
                    this.isRunning = true;
                    this.customActivityThread.interrupt();
                }
            }

            if (lifecycles.contains(AFTER_SUSPEND)) {
                if (this.isRunning) {
                    System.out.println("任务暂停");
                    this.isRunning = false;
                    this.customActivityThread.interrupt();
                    // 先暂停,然后执行用户注入的代码
                    var action = this.register.getAction(AFTER_SUSPEND);
                    if (action != null) {
                        action.run();
                    }
                } else {
                    System.out.println("任务已暂停,不需要再暂停");
                }
            }

            if (lifecycles.contains(AFTER_EXIT)) {
                this.isRunning = false;
                this.needTerminate = true;
                this.customActivityThread.interrupt();
                System.out.println("任务中止");

                // 先中止,然后执行用户注入的代码
                Runnable action = this.register.getAction(AFTER_EXIT);
                if (action != null) {
                    action.run();
                }

                this.register.removeRegisteredData();
            }

            if (lifecycles.contains(AFTER_ALL_INSTANCES_EXIT)) {
                this.isRunning = false;
                this.customActivityThread.interrupt();
                System.out.println("所有任务均中止");

                Runnable action = this.register.getAction(AFTER_ALL_INSTANCES_EXIT);
                if (action != null) {
                    action.run();
                }

                this.register.destroyAllData();
                System.exit(0); // 关闭主程序
            }
        };
        this.register.addHotKeyListener(hotkeyListener); // 添加监听
        System.out.println("正在监听快捷键...");
        // 运行本方法的 globalShortcutsThread 线程将在运行完本方法之后立刻就结束了
    }

    /**
     * 终止 customActivityThread 线程
     *
     * @since 2022-9-30
     */
    public GlobalShortcutsFX terminateCustomThread() {
        this.isRunning = false;
        this.needTerminate = true;
        this.customActivityThread.interrupt();
        return this;
    }

    /**
     * @since 2022-9-30
     */
    private class GlobalShortcutsRegister {
        public static final Random random = new Random();
        private static final List<GlobalShortcutsRegister> globalRegisters = new CopyOnWriteArrayList<>();
        private final JIntellitype jintellitype = JIntellitype.getInstance();
        private HotkeyListener hotkeyListener = null;

        /**
         * 考虑到允许用户创建本类的多个实例,为了在多个实例下,本类的快捷键标识符不重复,所以设置此字段
         *
         * 使用 DualHashBidiMap 是为了支持通过 id 来反向搜索到 lifecycle。
         * DualHashBidiMap 只能用于 1 对 1 映射,否则只要 key 或 value 存在相同,就会发生相应的覆盖
         *
         * @since 2022-9-30
         */
        private final DualHashBidiMap<GlobalShortcutsLifecycle, Integer> idOfLifecycles = new DualHashBidiMap<>();

        /**
         * 储存每个生命周期对应的快捷键
         *
         * @since 2022-9-30
         */
        private final Map<GlobalShortcutsLifecycle, JIntellitypeShortcut> lifecycleBindShortcut =
                new ConcurrentHashMap<>();

        /**
         * 储存同一快捷键对应的生命周期
         *
         * @since 2022-9-30
         */
        private final Map<JIntellitypeShortcut, Set<GlobalShortcutsLifecycle>> shortcutBindLifecycle =
                new ConcurrentHashMap<>();

        /**
         * 储存每个生命周期对应的要触发的行为
         *
         * @since 2022-9-30
         */
        private final Map<GlobalShortcutsLifecycle, Runnable> lifecycleBindAction =
                new ConcurrentHashMap<>();

        /**
         * @since 2022-9-30
         */
        public GlobalShortcutsRegister() {
            for (var lifecycle : GlobalShortcutsLifecycle.values()) {
                int code = random.nextInt();
                this.idOfLifecycles.put(lifecycle, code);
            }
            globalRegisters.add(this);
        }

        /**
         * @since 2022-9-30
         */
        public void addHotKeyListener(HotkeyListener listener) {
            this.hotkeyListener = listener;
            this.jintellitype.addHotKeyListener(this.hotkeyListener); // 添加监听
        }

        /**
         * 收集有相同快捷键的 lifecycle
         *
         * @since 2022-9-30
         */
        private void collectShortcuts() {
            for (var pair : this.lifecycleBindShortcut.entrySet()) {
                var lifecycle = pair.getKey();
                var shortcut = pair.getValue();
                if (this.shortcutBindLifecycle.containsKey(shortcut)) {
                    var lifecycles = this.shortcutBindLifecycle.get(shortcut);
                    lifecycles.add(lifecycle);
                } else {
                    var lifecycles = new HashSet<GlobalShortcutsLifecycle>();
                    lifecycles.add(lifecycle);
                    this.shortcutBindLifecycle.put(shortcut, lifecycles);
                }
            }
        }

        /**
         * @since 2022-9-30
         */
        public void registerShortcut(GlobalShortcutsLifecycle lifecycle,
                                     JIntellitypeShortcut shortcut, Runnable action) {
            var id = this.idOfLifecycles.get(lifecycle);
            this.jintellitype.registerHotKey(id, shortcut.getModifier(), shortcut.getKeycode());
            this.lifecycleBindShortcut.put(lifecycle, shortcut);
            this.lifecycleBindAction.put(lifecycle, action);
            this.collectShortcuts();
        }

        /**
         * 根据 lifecycle 的 id,查找与之有相同快捷键的 lifecycle
         *
         * @since 2022-9-30
         */
        public Set<GlobalShortcutsLifecycle> getLifecycles(int id) {
            // 根据 id 查找对应的 lifecycle
            var oneOfLifecycle = this.idOfLifecycles.getKey(id);
            var shortcut = this.lifecycleBindShortcut.get(oneOfLifecycle);
            return this.shortcutBindLifecycle.get(shortcut);
        }

        /**
         * @since 2022-9-30
         */
        public JIntellitypeShortcut getShortcut(GlobalShortcutsLifecycle lifecycle) {
            return this.lifecycleBindShortcut.get(lifecycle);
        }

        /**
         * @since 2022-9-30
         */
        public Runnable getAction(GlobalShortcutsLifecycle lifecycle) {
            return this.lifecycleBindAction.get(lifecycle);
        }


        /**
         * @return true 表示检查通过(无错误)
         * @since 2022-9-30
         */
        public boolean checkTheLogicOfShortcuts() {
            for (var lifecycles : this.shortcutBindLifecycle.values()) {
                if (lifecycles.size() == 1) {
                    continue;
                }
                // 如果是将“开始”、“恢复运行”快捷键合并
                if (lifecycles.size() == 2
                        && lifecycles.contains(BEFORE_START)
                        && lifecycles.contains(BEFORE_RESUME)) {
                    continue; // continue 表示合法
                }

                return false; // 只要有上述规定外的其它行为,即视为非法
            }
            return true; // 如果前面没有检测出问题,则视为合法
        }

        /**
         * 判断此快捷键是否已经存在
         *
         * @since 2022-9-30
         */
        public boolean isShortcutExist(JIntellitypeShortcut shortcut) {
            var lifecycles = this.shortcutBindLifecycle.get(shortcut);
            if (lifecycles == null || lifecycles.size() == 0) {
                return false;
            } else {
                return true;
            }
        }

        /**
         * 此方法不会销毁全局 JIntellitype 线程,只会清除本实例注册过的快捷键
         *
         * @since 2022-9-30
         */
        public void removeRegisteredData() {
            this.jintellitype.removeHotKeyListener(this.hotkeyListener);
            for (var lifecycleId : this.idOfLifecycles.values()) {
                this.jintellitype.unregisterHotKey(lifecycleId);
            }
        }

        /**
         * @since 2022-9-30
         */
        public void callOuterDestroy() {
            GlobalShortcutsFX.this.terminateCustomThread();
        }

        /**
         * 销毁所有 GlobalShortcutsFx 实例的数据
         *
         * @since 2022-9-30
         */
        public void destroyAllData() {
            for (var register : globalRegisters) {
                register.removeRegisteredData();
                register.callOuterDestroy();
            }
            this.jintellitype.cleanUp();
        }
    }
}

拓展

  不过,暂停是不是也可能出现延迟呢?没错,是这样。所以就要求线程 B(被控制线程)在循环运行时,在每次循环中,都不能运行一种耗时任务。因为线程 B 只有在执行检查开关变量的代码后才能改变运行状态,如果循环中存在耗时任务,那么相邻两次检查的时间间隔就会变长,就会带来“暂停延迟”的问题。

  我们当然不能期望每次循环中都只运行不耗时任务,如何解决这个问题呢?这个问题想一个简单而完美的解决方案很困难。一方面,编程人员需要合理的安排每次循环中的任务,另一方面,可以借助 Reactor 模式 中的思想,引入一个中介者作为中间层,介于线程 A(控制线程)与线程 B(被控制线程)之间,来协调线程 B 的资源。关于这方面的内容,读者可自行摸索。有巧妙思路的读者,也可以在下方留言指出。


【附】Reactor 模式:https://blog.csdn.net/wangpaiblog/article/details/124580590


完整代码 & 更多信息

  本文上面演示的完整代码已上传至 GitHub 中,可免费下载。同时笔者也将不定期免费进行升级维护,更新的内容也会即时上传。GitHub 地址:https://github.com/wangpaihovel/global-shortcuts-fx_Java


  源代码将会不断更新升级,最新的源代码已与本文所演示的有一些差异了。不过,熟悉 Git 的读者可以在历史 Git 提交中找到文中所有的代码。另外,如果读者对最新版本的代码不满意,也可以通过 Git 来回到之前的任何一个版本。


全局快捷键框架 global-shortcuts-fx 项目

分支 M # 版本 M.4.2 支持的功能:

  1. 支持如下功能的全局快捷键:

    1. 程序可后台运行,无需依赖界面来维持

    2. 快捷键不会因为光标及界面切换而失效

    3. 支持重复快捷键。支持对某些不冲突的行为设置同一个快捷键

    4. 支持创建多实例。支持在代码创建本框架的实体类的多个实例,以独立运行多个任务

  2. 本框架拥有可由快捷键控制的运行时生命周期,使用时只需要将快捷键、快捷键要触发的操作等,以 IoC 注入的方式注入到软件的不同生命周期后即可运行。

    故可以通过快捷键来控制本程序的启动、暂停、终止


  • 17
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值