JavaFx 創建快捷方式及設置開機啟動


原文地址:JavaFx 創建快捷方式及設置開機啟動 | Stars-One的雜貨小窩

原本是想整個桌面啟動器,需要在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 )
        }
    }

}

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM