最近寫了個看圖的小程序(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