JavaFx 创建快捷方式及设置开机启动

Stars-one 2021年06月11日 239次浏览 本篇字数为18,157字

本文为作者原创,转载请注明出处,谢谢配合
作者:Stars-one
链接:https://stars-one.site/2021/06/11/javafx-set-startup


原本是想整个桌面启动器,需要在windows平台上实现开机启动,但我的软件都是jar文件,不是传统的exe文件,也不知道能不能设置开机启动,稍微搜集了资料研究了会,发现有思路,而且可以成功实现

本文只研究了如何在windows进行,不清楚macos和linux的情况,各位有具体的实现思路欢迎分享出来

简单说明

windows如何实现开机启动的?

在Windows系统中,设置软件开机启动并不是太难的事情,大多数工具类软件都是有提供开机启动的选项

那软件没有体用选项,就不能设置为开机启动了?答案当然是否定的

看到网络的教程,都说要去设置任务定时器,其实有种更为方便的做法,就是将软件或者快捷方式放在windows指定的文件夹即可

文件路径格式如下:C:\Users\starsone\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup,这个文件夹暂且称为启动文件夹

注意:如果你是将软件放在这个文件夹想要实现开机启动,需要保证你的软件是绿色版,所以更为推荐使用快捷方式的方式进行设置开机启动

PS: Windows中启动文件夹有两种,一种是系统启动文件夹C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp,另一种则是用户启动文件夹,也就是上面说的

如果你是将快捷方式放在系统启动文件夹,那么这个软件不管是当前是什么用户登录进来都会启动

实现思路

但是,刚开始我不确定jar文件是否也能直接被windows启动,于是便是拿了之前蓝奏云批量下载作为测试,由于我这是单文件,所以我直接把jar包放在启动文件夹,测试是可以的

所以,想要实现jar文件自动启动,思路就是给jar文件创建一个快捷方式,然后将此快捷方式移动到启动文件夹即可实现

难点在于如何使用java给文件创建快捷方式?

网上的资料十分少,有个方法还需要使用dll文件,我也不懂window开发,于是便放弃了

然后找的过程中,发现了有位大佬通过浏览微软官方文档,直接通过字节流方式实现创建了快捷方式,而且代码及其简单,于是稍微参考了他的源码,改造了个工具类,用来实现创建快捷方式及开机启动

考虑到原本的Java用户,工具类代码补充了Java版本的,用Java同学可以直接拷贝一份,直接使用,如果是Kotlin的,两份都可使用

吐槽下Java版写的有点繁琐,有些API都没有(如获取不带扩展名的文件名),Kotlin中直接有对应方法,不需要自己去处理实现...

Kotlin版

注:本工具类已集成在我的开源项目里了Stars-One/common-controls: TornadoFx的常用控件 controls for tornadofx

创建快捷方式使用

val lnkFile = File("D:\\project\\javafx\\lanzou-downloader\\out\\蓝奏云批量下载器3.2.lnk")
val targetFile = File("D:\\project\\javafx\\lanzou-downloader\\out\\蓝奏云批量下载器3.2.jar")

ShortCutUtils.createShortCut(lnkFile,targetFile)

上述代码是将lnk文件输出在了同级目录,我们到文件夹中查看,可以发现已经生成成功了,点击也是能正常打开

设置某软件开机启动和取消开机启动

val targetFile = File("D:\\project\\javafx\\lanzou-downloader\\out\\蓝奏云批量下载器3.2.jar")

////设置开机启动(可以是File对象或者是路径)
ShortCutUtils.setAppStartup(targetFile)

//取消开机启动
ShortCutUtils.cancelAppStartup(targetFile)

这里可以看到,生成的快捷方式已经存在于启动文件夹,这样下次开机的时候就会自动启动软件了

2021.6.15补充:
既然知道了是如何生成快捷方式,那么读取也是可以举一反三,经测试,本方法仅适合使用本文生成的快捷方式

下面补充一个通用的读取快捷方式的方法,想使用通用的请跳过此部分

Java版的就不写了,原理其实就是用获取下标,把盘符和文件路径重新获取出来,之后重新拼接成路径即可,注意下文件路径是用gbk格式

/**
 * 读取快捷方式指向的源文件
 */
fun readLnkFile(file: File): File {
	val bytes = file.readBytes()
	var start = headFile.size + fileAttributes.size + fixedValueOne.size
	val panfu = String(byteArrayOf(bytes[start]))
	start += 1
	start += fixedValueTwo.size
	val pathBytes = bytes.copyOfRange(start,bytes.size)
	val path = String(pathBytes, charset("gbk"))
	val filepath = "${panfu}:\\$path"
	return File(filepath)
}

源码

class ShortCutUtils{

    companion object{
        /**
         * 创建快捷方式
         *
         * @param lnkFile 快捷文件
         * @param targetFile 源文件
         */
        fun createShortCut(lnkFile: File, targetFile: File) {
            if (!System.getProperties().getProperty("os.name").toUpperCase().contains("WINDOWS")) {
                println("当前系统不是window系统,无法创建快捷方式!!")
                return
            }

            val targetPath = targetFile.path
            if (!lnkFile.parentFile.exists()) {
                lnkFile.mkdirs()
            }
            //原快捷方式存在,则删除
            if (lnkFile.exists()) {
                lnkFile.delete()
            }

            lnkFile.appendBytes(headFile)
            lnkFile.appendBytes(fileAttributes)
            lnkFile.appendBytes(fixedValueOne)
            lnkFile.appendBytes(targetPath.toCharArray()[0].toString().toByteArray())
            lnkFile.appendBytes(fixedValueTwo)
            lnkFile.appendBytes(targetPath.substring(3).toByteArray(charset("gbk")))
        }

        /**
         * 设置软件开机启动
         *
         * @param targetFile 源文件
         */
        fun setAppStartup(targetFile: File) {
            val lnkFile = File(targetFile.parentFile, "temp.lnk")
            createShortCut(lnkFile, targetFile)
            val startUpFile = File(startup, "${targetFile.nameWithoutExtension}.lnk")
            //复制到启动文件夹,若快捷方式已存在则覆盖原来的
            lnkFile.copyTo(startUpFile, true)
            //删除缓存的快捷方式
            lnkFile.delete()
        }

        /**
         * 设置软件开机启动
         *
         * @param targetFile 源文件路径
         */
        fun setAppStartup(targetFilePath: String) {
            setAppStartup(File(targetFilePath))
        }

        /**
         * 创建快捷方式
         *
         * @param lnkFilePath 快捷方式文件生成路径
         * @param targetFilePath 源文件路径
         */
        fun createShortCut(lnkFilePath: String, targetFilePath: String) {
            createShortCut(File(lnkFilePath),File(targetFilePath))
        }

        /**
         * 取消开机启动
         *
         * @param targetFile
         */
        fun cancelAppStartup(targetFile: File) {
            val startupDir = File(startup)
            val files = startupDir.listFiles { file -> file.nameWithoutExtension==targetFile.nameWithoutExtension }
            if (files.isNotEmpty()) {
                //删除启动文件夹中的快捷方式文件
                files.first().delete()
            }
        }

        fun cancelAppStartup(targetFilePath: String) {
            cancelAppStartup(File(targetFilePath))
        }

        /**
         * 开机启动目录
         */
        val startup =  "${System.getProperty("user.home")}\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\"

        /**
         * 桌面目录
         */
        val desktop = FileSystemView.getFileSystemView().homeDirectory.absolutePath + "\\"

        /**
         * 文件头,固定字段
         */
        private val headFile = byteArrayOf(
            0x4c, 0x00, 0x00, 0x00, 0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
            0xc0.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46
        )

        /**
         * 文件头属性
         */
        private val fileAttributes = byteArrayOf(
            0x93.toByte(), 0x00, 0x08, 0x00,  //可选文件属性
            0x20, 0x00, 0x00, 0x00,  //目标文件属性
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  //文件创建时间
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  //文件修改时间
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  //文件最后一次访问时间
            0x00, 0x00, 0x00, 0x00,  //文件长度
            0x00, 0x00, 0x00, 0x00,  //自定义图标个数
            0x01, 0x00, 0x00, 0x00,  //打开时窗口状态
            0x00, 0x00, 0x00, 0x00,  //热键
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 //未知
        )

        private val fixedValueOne = byteArrayOf(
            0x83.toByte(), 0x00, 0x14, 0x00, 0x1F, 0x50, 0xE0.toByte(), 0x4F, 0xD0.toByte(),
            0x20, 0xEA.toByte(), 0x3A, 0x69, 0x10, 0xA2.toByte(),
            0xD8.toByte(), 0x08, 0x00, 0x2B, 0x30, 0x30, 0x9D.toByte(), 0x19, 0x00, 0x2f
        )

        /**
         * 固定字段2
         */
        private val fixedValueTwo = byteArrayOf(
            0x3A, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x00,
            0x32, 0x00, 0x04, 0x00, 0x00, 0x00, 0x67, 0x50, 0x91.toByte(), 0x3C, 0x20, 0x00
        )


    }
}

Java版

创建快捷方式使用

File lnkFile = new File("D:\\project\\javafx\\lanzou-downloader\\out\\蓝奏云批量下载器3.2.lnk");
File targetFile = new File("D:\\project\\javafx\\lanzou-downloader\\out\\蓝奏云批量下载器3.2.jar");

//创建快捷方式(可以传File对象或者是路径)
ShortCutUtil.createShortCut(lnkFile,targetFile);

上述代码是将lnk文件输出在了同级目录,我们到文件夹中查看,可以发现已经生成成功了,点击也是能正常打开

设置某软件开机启动和取消开机启动

File targetFile = new File("D:\\project\\javafx\\lanzou-downloader\\out\\蓝奏云批量下载器3.2.jar");

//设置开机启动(可以是File对象或者是路径)
ShortCutUtil.setAppStartup(targetFile);

//取消开机启动
ShortCutUtil.cancelAppStartup(targetFile);

这里可以看到,生成的快捷方式已经存在于启动文件夹,这样下次开机的时候就会自动启动软件了

源码

package site.starsone;


import javax.swing.filechooser.FileSystemView;
import java.io.*;
import java.nio.channels.FileChannel;

/**
 * @author StarsOne
 * @url <a href="http://stars-one.site">http://stars-one.site</a>
 * @date Create in  2021/06/11 21:28
 */
public class ShortCutUtil { ;
    /**
     * 开机启动目录
     */
    public final static String startup=System.getProperty("user.home")+
            "\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\";
    /**
     * 桌面目录
     */
    public final static String desktop= FileSystemView.getFileSystemView().getHomeDirectory().getAbsolutePath()+"\\";
    /**
     * 文件头,固定字段
     */
    private static byte[] headFile={0x4c,0x00,0x00,0x00,
            0x01, 0x14,0x02,0x00,0x00,0x00,0x00,0x00,
            (byte) 0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46
    };
    /**
     * 文件头属性
     */
    private static byte[] fileAttributes={(byte) 0x93,0x00,0x08,0x00,//可选文件属性
            0x20, 0x00, 0x00, 0x00,//目标文件属性
            0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//文件创建时间
            0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//文件修改时间
            0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//文件最后一次访问时间
            0x00,0x00,0x00,0x00,//文件长度
            0x00,0x00,0x00,0x00,//自定义图标个数
            0x01,0x00,0x00,0x00,//打开时窗口状态
            0x00,0x00,0x00,0x00,//热键
            0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00//未知
    };
    /**
     * 固定字段1
     */
    private static byte[] fixedValueOne={
            (byte) 0x83 ,0x00 ,0x14 ,0x00
            ,0x1F ,0x50 ,(byte)0xE0 ,0x4F
            ,(byte)0xD0 ,0x20 ,(byte)0xEA
            ,0x3A ,0x69 ,0x10 ,(byte)0xA2
            ,(byte)0xD8 ,0x08 ,0x00 ,0x2B
            ,0x30,0x30,(byte)0x9D,0x19,0x00,0x2f
    };

    /**
     * 固定字段2
     */
    private static byte[] fixedValueTwo={
            0x3A ,0x5C ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00
            ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00
            ,0x00 ,0x54 ,0x00 ,0x32 ,0x00 ,0x04
            ,0x00 ,0x00 ,0x00 ,0x67 ,0x50 ,(byte)0x91 ,0x3C ,0x20 ,0x00
    };

    /**
     * 生成快捷方式
     * @param start 完整的文件路径
     * @param target 完整的快捷方式路径
     */
    private static void start(String start,String target){
        FileOutputStream fos= null;
        try {
            fos = new FileOutputStream(createDirectory(start));
            fos.write(headFile);
            fos.write(fileAttributes);
            fos.write(fixedValueOne);
            fos.write((target.toCharArray()[0]+"").getBytes());
            fos.write(fixedValueTwo);
            fos.write(target.substring(3).getBytes("gbk"));
            fos.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(fos!=null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 解决父路径问题
     */
    private static File createDirectory(String file){
        File f=new File(file);
        //获取父路径
        File fileParent = f.getParentFile();
        //如果文件夹不存在
        if (fileParent!=null&&!fileParent.exists()) {
            //创建文件夹
            fileParent.mkdirs();
        }
        //快捷方式已存在,则删除原来存在的文件
        if (f.exists()) {
            f.delete();
        }
        return f;
    }

    /**
     * 复制文件
     * @param source
     * @param dest
     * @throws IOException
     */
    private static void copyFileUsingFileChannels(File source, File dest) throws IOException {
        FileChannel inputChannel = null;
        FileChannel outputChannel = null;
        try {
            inputChannel = new FileInputStream(source).getChannel();
            outputChannel = new FileOutputStream(dest).getChannel();
            outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
        } finally {
            inputChannel.close();
            outputChannel.close();
        }
    }

    /**
     * 创建快捷方式
     * @param lnkFilePath 快捷方式文件路径
     * @param targetFilePath 快捷方式对应源文件的文件路径
     */
    public static void createShortCut(String lnkFilePath,String targetFilePath) {
        if (!System.getProperties().getProperty("os.name").toUpperCase().contains("WINDOWS")) {
            System.out.println("当前系统不是window系统,无法创建快捷方式!!");
            return;
        }
        start(lnkFilePath,targetFilePath);
    }

    /**
     * 生成快捷方式
     * @param lnkFile 快捷方式文件
     * @param targetFile 快捷方式对应源文件
     */
    public static void createShortCut(File lnkFile,File targetFile) {

        if (!System.getProperties().getProperty("os.name").toUpperCase().contains("WINDOWS")) {
            System.out.println("当前系统不是window系统,无法创建快捷方式!!");
            return;
        }
        start(lnkFile.getPath(),targetFile.getPath());
    }
    
    /**
     * 设置某软件开启启动
     * @param targetFile 源文件
     * @return 是否设置成功
     */
    public static boolean setAppStartup(File targetFile) {

        File lnkFile = new File(targetFile.getParent(),"temp.lnk");
        createShortCut(lnkFile,targetFile);
        try {
            //获取不带扩展名的文件名
            String name = targetFile.getName();
            int end = name.lastIndexOf(".");
            String extendName = name.substring(0,end);

            //将软件复制到软件想
            copyFileUsingFileChannels(lnkFile, new File(startup,extendName+".lnk"));
            //删除缓存的快捷方式文件
            lnkFile.delete();
            return true;
        } catch (IOException e) {
            System.out.println("移动到startup文件夹失败");
            return false;
        }
    }

    /**
     * 设置某软件开启启动
     * @param targetFilePath 源文件路径
     * @return 是否设置成功
     */
    public static boolean setAppStartup(String targetFilePath) {
        File targetFile = new File(targetFilePath);
        return setAppStartup(targetFile);
    }

    /**
     * 取消开机启动
     * @param targetFile
     */
    public static void cancelAppStartup(File targetFile) {
        File startupDir = new File(startup);
        String targetFileName = targetFile.getName();
        int endIndex = targetFileName.lastIndexOf(".");
        final String targetName = targetFileName.substring(0, endIndex);

        File[] files = startupDir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                //获取不带扩展名的文件名
                int end = name.lastIndexOf(".");
                String filename = name.substring(0, end);
                if (filename.equals(targetName)) {
                    return true;
                }
                return false;
            }
        });
        if (files.length > 0) {
            files[0].delete();
        }
    }

}

补充:读取lnk快捷方式(通用)

参考了大神的改造成工具类,使用也是传一个lnk快捷方式的File对象即可

PS:Java版和Kotlin版的工具类名字不一样

File file = LnkParser.parse(new File("xx.lnk"));

val file = LnkParserUtil.parse(File("xx.lnk"))

Java版

package site.starsone.dbtool.view;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.Charset;

public class LnkParser {

    public static File parse(File f) throws Exception {
        // read the entire file into a byte buffer
        FileInputStream fin = new FileInputStream(f);
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        byte[] buff = new byte[256];
        while (true) {
            int n = fin.read(buff);
            if (n == -1) {
                break;
            }
            bout.write(buff, 0, n);
        }
        fin.close();
        byte[] link = bout.toByteArray();

        // get the flags byte
        byte flags = link[0x14];

        // if the shell settings are present, skip them
        final int shell_offset = 0x4c;
        int shell_len = 0;
        if ((flags & 0x1) > 0) {
            // the plus 2 accounts for the length marker itself
            shell_len = bytes2short(link, shell_offset) + 2;
        }

        // get to the file settings
        int file_start = 0x4c + shell_len;

        // get the local volume and local system values
        int local_sys_off = link[file_start + 0x10] + file_start;
        String real_file = getNullDelimitedString(link, local_sys_off);
        return new File(real_file);
    }

    private static String getNullDelimitedString(byte[] bytes, int off) {
        int len = 0;
        // count bytes until the null character (0)
        while (true) {
            if (bytes[off + len] == 0) {
                break;
            }
            len++;
        }
        return new String(bytes, off, len, Charset.forName("gbk"));
    }

    // convert two bytes into a short // note, this is little endian because
    // it's for an // Intel only OS.
    private static int bytes2short(byte[] bytes, int off) {
        byte a1 = bytes[off];
        int temp = bytes[off + 1];
        int a2 = (bytes[off + 1] << 8);
        return a1 | a2;
    }

}

Kotlin版

package site.starsone.dbtool.view

import java.io.File
import java.nio.charset.Charset
import kotlin.experimental.and
import kotlin.experimental.or

class LnkParserUtil {

    companion object {

        fun parse(file: File): File {
            val link = file.readBytes()
            // get the flags byte
            val flags = link[0x14]

            // if the shell settings are present, skip them
            val shell_offset = 0x4c
            var shell_len = 0
            if (flags and 0x1 > 0) {
                // the plus 2 accounts for the length marker itself
                shell_len = bytes2short(link, shell_offset) + 2
            }

            // get to the file settings
            val file_start = 0x4c + shell_len

            // get the local volume and local system values
            val local_sys_off = link[file_start + 0x10] + file_start
            val realFilename = getNullDelimitedString(link, local_sys_off)
            return File(realFilename)
        }

        private fun getNullDelimitedString(bytes: ByteArray, off: Int): String {
            var len = 0
            // count bytes until the null character (0)
            while (true) {
                if (bytes[off + len] == 0.toByte()) {
                    break
                }
                len++
            }
            return String(bytes, off, len, Charset.forName("gbk"))
        }

        private fun bytes2short(bytes: ByteArray, off: Int): Int {
            val start = bytes[off].toInt()
            val end = (bytes[off + 1].toInt() shl 8)
            return (start or end )
        }
    }

}

参考