最近写了个看图的小程序(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;
}
}
参考文档:
(7条消息) C#关联自定义文件类型到应用程序并实现自动导入_安替-AnTi的博客-CSDN博客
[注册表] 文件关联与关联文件 - 砹小翼 - 博客园 (cnblogs.com)
c# - Associate File Extension with Application - Stack Overflow
默认程序 - Win32 apps | Microsoft Docs