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)

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