WIN10下使用代碼修改註冊表,改變文件關聯程序(C#)

最近寫了個看圖的小程序(ruikocon/spic: image viewer (github.com)  有興趣的可以試下)

有一個需求是:修改jpg、png等文件的關聯程序,實現雙擊圖片文件調用看圖程序打開。

網上找了半天,基本沒找到什麼現成的回答,沒辦法只能自己琢磨,這裏整理一下實現辦法,我先把原理介紹一下,第二部分放代碼,需要的話可以直接跳到第二部分。

 

第一部分 原理介紹

首先感謝這位同學指明瞭win7下的方向:修改註冊表改變文件關聯程序 (funjan.com)

但他的辦法只對winxp、win7系統有效。

這裏只討論已存在的文件類型的關聯程序,自己新定義的擴展名添加方法可以看上述引文之中的1-4這幾步。

說下win10的情況,以png爲例,註冊表在"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts"項之下有三個子項:

其中第二個"OpenWithProgids"可以不用管,第一個"OpenWithList"下面的鍵值是這樣的:

看似"MRUList"之中標記了打開文件的管理程序優先級,但實際上這裏的值改了也沒用,是由第三項"UserChoice"自動生成的。

所以Win10下面修改關聯程序的關鍵就是這個UserChoice

內容看似很簡單,第一個值無用,第二個hash值,第三個是關聯的程序。但實際上這個hash值是用來防僞的,你隨便寫一個是會報錯的,必須通過算法生成,而第二個ProgId裏的鍵值,實際上指向註冊表裏另一項,而這一項如果沒有手動指定關聯程序的話,也是需要使用代碼添加的。

問題一下子變複雜了,在網上找了好久,終於發現一個開源項目可以算出hash:mullerdavid/tools_setfta: Set file type or url association in windows 10 programmatically. (github.com)

而ProgId的鍵值:Applications\spic.exe 指向的是註冊表中的:\HKEY_CLASSES_ROOT\Applications\,並且其結構爲

前面幾級值都是空,在command下面有一個鍵值指向了程序所在位置,同時後面有一個%1,表示使用該程序打開文件。同時路徑和參數需要用"括起來。

所以最終WIn10下面,關聯文件的設置方法是這樣的,假設你的程序是spic.exe

1. 添加文件類型等,我們這裏是改變已有的文件類型管理程序,所以這一步略,可參見修改註冊表改變文件關聯程序 (funjan.com)

2. 添加\HKEY_CLASSES_ROOT\Applications\spic.exe,在項目下建立shell\open\command各級子項,在command下修改鍵值爲程序位置。

注意這一步可能會因爲權限不足而報錯,我的解決辦法是提示用戶使用管理員權限運行程序。也可以參考這裏要求程序在管理員權限下運行,按照這裏所說,添加應用程序清單文件,設置好權限即可:讓WPF程序啓動時以管理員身份運行(轉載) - LeeMacrofeng - 博客園 (cnblogs.com)

 

3. 刪掉HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts下面對應文件類型下的UserChoice(直接修改內容需要很高的權限,但刪除不需要高權限,所以先刪再添加,而不是直接修改)

4. 添加UserChoice,計算生成Hash,修改ProgId爲第二步之中添加的Applications\spic.exe

5. 使用SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero); 刷新,不刷新的話關聯文件的圖標不會變。

 

 

第二部分 C#代碼

使用方法:

//獲取當前程序完整路徑
string location = GetType().Assembly.Location;
//假設程序名爲spic.exe,首先是將spic.exe添加進\HKEY_CLASSES_ROOT\Applications\
RegAct.AddApplicationToReg("spic.exe", location);
//將.png關聯到spic.exe
RegAct.AssociationWith(".png", "spic.exe");
//刷新文件管理器,告知它文件關聯發生了改變,以改變關聯文件的圖標
RegAct.SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero);

RegAct類:

internal class RegAct
{
    [DllImport("shell32.dll")]
    public static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);

    [DllImport("advapi32.dll", EntryPoint = "RegQueryInfoKey", CallingConvention = CallingConvention.Winapi, SetLastError = true)]
    extern private static int RegQueryInfoKey(
            IntPtr handle,
            IntPtr /*out StringBuilder*/ lpClass,
            IntPtr /*ref uint*/ lpcbClass,
            IntPtr lpReserved,
            IntPtr /*out uint*/ lpcSubKeys,
            IntPtr /*out uint*/ lpcbMaxSubKeyLen,
            IntPtr /*out uint*/ lpcbMaxClassLen,
            IntPtr /*out uint*/ lpcValues,
            IntPtr /*out uint*/ lpcbMaxValueNameLen,
            IntPtr /*out uint*/ lpcbMaxValueLen,
            IntPtr /*out uint*/ lpcbSecurityDescriptor,
            out long lpftLastWriteTime
        );



    /// <summary>
    /// 添加程序到\HKEY_CLASSES_ROOT\Applications\
    /// appname示例: spic.exe
    /// location爲程序所在位置,例如:F:\Study\Csharp\spic\spic\bin\Release\net48\spic.exe
    /// </summary>
    public static void AddApplicationToReg(string appname, string location)
    {
        //使用\\分割註冊表各級
        RegistryKey key = Registry.ClassesRoot.OpenSubKey("Applications\\" + appname + "\\shell\\open\\command");

        //command下面鍵值形如:"F:\Study\Csharp\spic\spic\bin\Release\net48\spic.exe" "%1"
        string val = string.Format("\"{0}\" \"%1\"", location);

        if (key == null || val != key.GetValue("").ToString()) //如果Applications\\appname\\shell\\open\\command爲空,或者鍵值指向並非程序所在位置,則需要刪除重新添加
        {
            RegistryKey AppKey = Registry.ClassesRoot.OpenSubKey("Applications");
            Registry.ClassesRoot.DeleteSubKey("Applications\\" + appname, false);

            //注意!下面CreateSubKey這句代碼需要管理員權限,可以使用try判斷一下如果沒有提示用戶以管理員權限運行,也可以參照https://www.cnblogs.com/mtudou/articles/9181600.html 不過我沒有試過
            try
            {
                key = Registry.ClassesRoot.CreateSubKey("Applications\\" + appname + "\\shell\\open\\command");
                key.SetValue("", val);
            }
            catch
            {
                ///.......提示用戶需要以管理員權限運行
            }
        }
    }




    /// <summary>
    /// 將extension的文件類型關聯到appname程序上
    /// extension格式示例: .png
    /// appname示例: spic.exe
    /// </summary>
    public static void AssociationWith(string extension, string appname)
    {
        //ProgId爲之前添加進Applications的
        string progid = "Applications\\" + appname;
        String regpath = String.Format("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\{0}\\UserChoice", extension);

        //刪除UserChoice
        Registry.CurrentUser.DeleteSubKey(regpath, false);
        RegistryKey regnode = Registry.CurrentUser.CreateSubKey(regpath);

        //生成hash值,超複雜  源碼來自 https://github.com/mullerdavid/tools_setfta
        System.Security.Principal.WindowsIdentity user = System.Security.Principal.WindowsIdentity.GetCurrent();
        String sid = user.User.Value;

        long ftLastWriteTime;
        RegQueryInfoKey(regnode.Handle.DangerousGetHandle(), IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, out ftLastWriteTime);
        DateTime time = DateTime.FromFileTime(ftLastWriteTime);
        time = time.AddTicks(-(time.Ticks % 600000000)); //clamp to minute part (1min=600000000*100ns)
        ftLastWriteTime = time.ToFileTime();

        String regdate = ftLastWriteTime.ToString("x16");

        String experience = "user choice set via windows user experience {d18b6dd5-6124-4341-9318-804003bafa0b}";

        //Step1: String (Unicode with 0 terminator) from the following: extension, user sid, progid, last modification time for the registry node clamped to minute part, secret experience string
        //Step2: Lowercase
        byte[] bytes = Encoding.Unicode.GetBytes((extension + sid + progid + regdate + experience + "\0").ToLower());
        System.Security.Cryptography.MD5 md5Hash = System.Security.Cryptography.MD5.Create();
        //Step3: MD5
        byte[] md5 = md5Hash.ComputeHash(bytes);
        //Step4: Microsoft hashes from data and md5, xored together
        byte[] mshash1 = sub_1(bytes, md5);
        byte[] mshash2 = sub_2(bytes, md5);
        byte[] finalraw = xorbytes(mshash1, mshash2);
        //Step5: Base64
        String hash = System.Convert.ToBase64String(finalraw);

        //設置Hash和ProgId
        regnode.SetValue("ProgId", progid);
        regnode.SetValue("Hash", hash);
    }


    

    internal static byte[] xorbytes(byte[] data1, byte[] data2)
    {
        byte[] retval = new byte[Math.Max(data1.Length, data2.Length)];
        for (int i = 0; i < data1.Length; i++) retval[i] ^= data1[i];
        for (int i = 0; i < data2.Length; i++) retval[i] ^= data2[i];
        return retval;
    }

    internal static byte[] sub_1(byte[] data, byte[] md5)
    {
        byte[] retval = new byte[8];

        UInt32 length = (UInt32)(((((data.Length) >> 2) & 1) < 1 ? 1 : 0) + ((data.Length) >> 2) - 1);// (UInt32)Math.Floor((double)(data.Length/8))*2; //length in dword
        UInt32[] dword_data = new UInt32[length];
        UInt32[] dword_md5 = new UInt32[4];
        for (int i = 0; i < dword_data.Length; i++)
        {
            dword_data[i] = System.BitConverter.ToUInt32(data, i * 4);
        }
        dword_md5[0] = System.BitConverter.ToUInt32(md5, 0);
        dword_md5[1] = System.BitConverter.ToUInt32(md5, 4);
        dword_md5[2] = System.BitConverter.ToUInt32(md5, 8);
        dword_md5[3] = System.BitConverter.ToUInt32(md5, 12);

        if (length <= 1 || (length & 1) == 1)
            return retval;

        UInt32 v5 = 0;
        UInt32 v6 = 0;
        UInt32 v7 = (length - 2) >> 1;
        UInt32 v18 = v7++;
        UInt32 v8 = v7;
        UInt32 v19 = v7;
        UInt32 result = 0;
        UInt32 v9 = (dword_md5[1] | 1) + 0x13DB0000u;
        UInt32 v10 = (dword_md5[0] | 1) + 0x69FB0000u;

        UInt32 v11 = 0;
        UInt32 v12 = 0;
        UInt32 v13 = 0;
        UInt32 v14 = 0;
        UInt32 v15 = 0;
        UInt32 v16 = 0;
        UInt32 v17 = 0;

        do //TODO: based on asm
        {
            v11 = dword_data[v6] + result;
            v6 += 2;
            v12 = 0x79F8A395u * (v10 * v11 - 0x10FA9605u * (v11 >> 16)) + 0x689B6B9Fu * ((v10 * v11 - 0x10FA9605u * (v11 >> 16)) >> 16);
            v13 = (0xEA970001u * v12 - 0x3C101569u * (v12 >> 16));
            v14 = v13 + v5;
            v15 = v9 * (dword_data[v6 - 1] + v13) - 0x3CE8EC25u * ((dword_data[v6 - 1] + v13) >> 16);
            result = 0x1EC90001u * (0x59C3AF2Du * v15 - 0x2232E0F1u * (v15 >> 16)) + 0x35BD1EC9u * ((0x59C3AF2Du * v15 - 0x2232E0F1u * (v15 >> 16)) >> 16);
            v5 = result + v14;
            --v8;
        }
        while (v8 != 0);
        if (length - 2 - 2 * v18 == 1)
        {
            v16 = (dword_data[2 * v19] + result) * v10 - 0x10FA9605u * ((dword_data[2 * v19] + result) >> 16);
            v17 = 0x39646B9Fu * (v16 >> 16) + 0x28DBA395u * v16 - 0x3C101569u * ((0x689B6B9Fu * (v16 >> 16) + 0x79F8A395u * v16) >> 16);
            result = 0x35BD1EC9u * ((0x59C3AF2Du * (v17 * v9 - 0x3CE8EC25u * (v17 >> 16)) - 0x2232E0F1u * ((v17 * v9 - 0x3CE8EC25u * (v17 >> 16)) >> 16)) >> 16) + 0x2A18AF2Du * (v17 * v9 - 0x3CE8EC25u * (v17 >> 16)) - 0xFD6BE0F1u * ((v17 * v9 - 0x3CE8EC25u * (v17 >> 16)) >> 16);
            v5 += result + v17;
        }

        BitConverter.GetBytes(result).CopyTo(retval, 0);
        BitConverter.GetBytes(v5).CopyTo(retval, 4);
        return retval;

    }


    internal static byte[] sub_2(byte[] data, byte[] md5)
    {
        byte[] retval = new byte[8];

        UInt32 length = (UInt32)(((((data.Length) >> 2) & 1) < 1 ? 1 : 0) + ((data.Length) >> 2) - 1);// (UInt32)Math.Floor((double)(data.Length/8))*2; //length in dword
        UInt32[] dword_data = new UInt32[length];
        UInt32[] dword_md5 = new UInt32[4];
        for (int i = 0; i < dword_data.Length; i++)
        {
            dword_data[i] = System.BitConverter.ToUInt32(data, i * 4);
        }
        dword_md5[0] = System.BitConverter.ToUInt32(md5, 0);
        dword_md5[1] = System.BitConverter.ToUInt32(md5, 4);
        dword_md5[2] = System.BitConverter.ToUInt32(md5, 8);
        dword_md5[3] = System.BitConverter.ToUInt32(md5, 12);

        if (length <= 1 || (length & 1) == 1)
            return retval;

        UInt32 v5 = 0;
        UInt32 v6 = 0;
        UInt32 v7 = 0;
        UInt32 v25 = (length - 2) >> 1;
        UInt32 v21 = dword_md5[0] | 1;
        UInt32 v22 = dword_md5[1] | 1;
        UInt32 v23 = (UInt32)(0xB1110000u * v21);
        UInt32 v24 = 0x16F50000u * v22;
        UInt32 v8 = v25 + 1;

        UInt32 v9 = 0;
        UInt32 v10 = 0;
        UInt32 v11 = 0;
        UInt32 v12 = 0;
        UInt32 v13 = 0;
        UInt32 v14 = 0;
        UInt32 v15 = 0;
        UInt32 v16 = 0;
        UInt32 v17 = 0;
        UInt32 v18 = 0;
        UInt32 v19 = 0;
        UInt32 v20 = 0;

        do //TODO: based on asm
        {
            v6 += 2;
            v9 = (dword_data[v6 - 2] + v5) * v23 - 0x30674EEFu * (v21 * (dword_data[v6 - 2] + v5) >> 16);
            v10 = v9 >> 16;
            v11 = 0xE9B30000u * v10 + 0x12CEB96Du * ((0x5B9F0000u * v9 - 0x78F7A461u * v10) >> 16);
            v12 = 0x1D830000u * v11 + 0x257E1D83u * (v11 >> 16);
            v13 = ((v12 + dword_data[v6 - 1]) * v24 - 0x5D8BE90Bu * ((v22 * (v12 + dword_data[v6 - 1])) >> 16)) >> 16;
            v14 = 0x96FF0000u * ((v12 + dword_data[v6 - 1]) * v24 - 0x5D8BE90Bu * ((v22 * (v12 + dword_data[v6 - 1])) >> 16)) - 0x2C7C6901u * v13 >> 16;
            v5 = 0xF2310000u * v14 - 0x405B6097u * ((0x7C932B89u * v14 - 0x5C890000u * v13) >> 16);
            v7 += v5 + v12;
            --v8;
        }
        while (v8 != 0);
        if (length - 2 - 2 * v25 == 1)
        {
            v15 = 0xB1110000u * v21 * (dword_data[2 * (v25 + 1)] + v5) - 0x30674EEFu * (v21 * (dword_data[2 * (v25 + 1)] + v5) >> 16);
            v16 = v15 >> 16;
            v17 = (0x5B9F0000u * v15 - 0x78F7A461u * (v15 >> 16)) >> 16;
            v18 = 0x257E1D83u * ((0xE9B30000u * v16 + 0x12CEB96Du * v17) >> 16) + 0x3BC70000u * v17;
            v19 = (0x16F50000u * v18 * v22 - 0x5D8BE90Bu * (v18 * v22 >> 16)) >> 16;
            v20 = (0x96FF0000u * (0x16F50000u * v18 * v22 - 0x5D8BE90Bu * (v18 * v22 >> 16)) - 0x2C7C6901u * v19) >> 16;
            v5 = 0xF2310000u * v20 - 0x405B6097u * ((0x7C932B89u * v20 - 0x5C890000u * v19) >> 16);
            v7 += v5 + v18;
        }

        BitConverter.GetBytes(v5).CopyTo(retval, 0);
        BitConverter.GetBytes(v7).CopyTo(retval, 4);

        return retval;
    }
}

 

 

參考文檔:

修改註冊表改變文件關聯程序 (funjan.com)

(7條消息) C#關聯自定義文件類型到應用程序並實現自動導入_安替-AnTi的博客-CSDN博客

[註冊表] 文件關聯與關聯文件 - 砹小翼 - 博客園 (cnblogs.com)

mullerdavid/tools_setfta: Set file type or url association in windows 10 programmatically. (github.com)

c# - Associate File Extension with Application - Stack Overflow

默認程序 - Win32 apps | Microsoft Docs

WPF-要求以管理員身份打開應用程序 - mtudou - 博客園 (cnblogs.com)

讓WPF程序啓動時以管理員身份運行(轉載) - LeeMacrofeng - 博客園 (cnblogs.com)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章