关联文件扩展名与应用程序

65

我编写了一个可以编辑特定文件类型的程序,并希望在启动时为用户提供将我的应用设置为该文件类型的默认编辑器的选项(因为我不想使用安装程序)。

我已经尝试编写了一个可重复使用的方法来关联文件,通过向HKEY_CLASSES_ROOT中添加键来实现(最好在任何操作系统上,尽管我正在运行Vista),但似乎并没有起作用。

public static void SetAssociation(string Extension, string KeyName, string OpenWith, string FileDescription)
{
    RegistryKey BaseKey;
    RegistryKey OpenMethod;
    RegistryKey Shell;
    RegistryKey CurrentUser;

    BaseKey = Registry.ClassesRoot.CreateSubKey(Extension);
    BaseKey.SetValue("", KeyName);

    OpenMethod = Registry.ClassesRoot.CreateSubKey(KeyName);
    OpenMethod.SetValue("", FileDescription);
    OpenMethod.CreateSubKey("DefaultIcon").SetValue("", "\"" + OpenWith + "\",0");
    Shell = OpenMethod.CreateSubKey("Shell");
    Shell.CreateSubKey("edit").CreateSubKey("command").SetValue("", "\"" + OpenWith + "\"" + " \"%1\"");
    Shell.CreateSubKey("open").CreateSubKey("command").SetValue("", "\"" + OpenWith + "\"" + " \"%1\"");
    BaseKey.Close();
    OpenMethod.Close();
    Shell.Close();

    CurrentUser = Registry.CurrentUser.CreateSubKey(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" + Extension);
    CurrentUser = CurrentUser.OpenSubKey("UserChoice", RegistryKeyPermissionCheck.ReadWriteSubTree, System.Security.AccessControl.RegistryRights.FullControl);
    CurrentUser.SetValue("Progid", KeyName, RegistryValueKind.String);
    CurrentUser.Close();
}

为什么它不起作用?一个使用示例可能是

SetAssociation(".ucs", "UCS_Editor_File", Application.ExecutablePath, "UCS File"); 

如果我使用注册表编辑器做同样的操作,那么使用“CurrentUser”部分的方法似乎是有效的,但在我的应用程序中使用它则不行。


你尝试以管理员身份运行程序了吗? - Colin Newell
UAC 意味着除非你明确要求,否则你的应用程序不会以管理员身份运行。你正在运行 Vista,Vista 包括 UAC。你能否再次确认程序是否以管理员身份运行? - Benjamin Podszun
我已经尝试过以管理员身份运行,并且已经关闭了UAC,但是程序运行后文件仍然没有关联。 - User2400
我认为你方法中倒数第三行可能是错误的。我认为你不想将“CurrentUser”设置为子键。 - Maestro1024
9个回答

39
答案:
这个答案比我预想的要简单得多。Windows资源管理器有自己的覆盖打开应用程序方式,而我试图在代码的最后几行修改它。如果你只是删除资源管理器的覆盖,那么文件关联就会生效。 我还通过使用P / Invoke调用未托管函数SHChangeNotify()来告诉资源管理器我已更改了文件关联。
public static void SetAssociation(string Extension, string KeyName, string OpenWith, string FileDescription)
{
    // The stuff that was above here is basically the same

    // Delete the key instead of trying to change it
    var CurrentUser = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\" + Extension, true);
    CurrentUser.DeleteSubKey("UserChoice", false);
    CurrentUser.Close();

    // Tell explorer the file association has been changed
    SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero);
}

[DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);

8
我知道这已经有点过时了,你可能已经注意到了,但我在这段代码和你的第一篇文章中注意到了一个问题,就是CurrentUser = 的第一行在调用OpenSubKey()时硬编码了.ucs扩展名。 - Chuck Savage
对我来说非常好用!但要注意 - 需要管理员权限。 - Kamornik Cola
删除已知扩展名(例如.png)的子键是有效的,但一旦我通知资源管理器,它就会恢复用户选择。我该如何阻止资源管理器还原用户选择? - HGMamaci

36

这里是一个完整的例子:

public class FileAssociation
{
    public string Extension { get; set; }
    public string ProgId { get; set; }
    public string FileTypeDescription { get; set; }
    public string ExecutableFilePath { get; set; }
}

public class FileAssociations
{
    // needed so that Explorer windows get refreshed after the registry is updated
    [System.Runtime.InteropServices.DllImport("Shell32.dll")]
    private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2);

    private const int SHCNE_ASSOCCHANGED = 0x8000000;
    private const int SHCNF_FLUSH = 0x1000;

    public static void EnsureAssociationsSet()
    {
        var filePath = Process.GetCurrentProcess().MainModule.FileName;
        EnsureAssociationsSet(
            new FileAssociation
            {
                Extension = ".ucs",
                ProgId = "UCS_Editor_File",
                FileTypeDescription = "UCS File",
                ExecutableFilePath = filePath
            });
    }

    public static void EnsureAssociationsSet(params FileAssociation[] associations)
    {
        bool madeChanges = false;
        foreach (var association in associations)
        {
            madeChanges |= SetAssociation(
                association.Extension,
                association.ProgId,
                association.FileTypeDescription,
                association.ExecutableFilePath);
        }

        if (madeChanges)
        {
            SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, IntPtr.Zero, IntPtr.Zero);
        }
    }

    public static bool SetAssociation(string extension, string progId, string fileTypeDescription, string applicationFilePath)
    {
        bool madeChanges = false;
        madeChanges |= SetKeyDefaultValue(@"Software\Classes\" + extension, progId);
        madeChanges |= SetKeyDefaultValue(@"Software\Classes\" + progId, fileTypeDescription);
        madeChanges |= SetKeyDefaultValue($@"Software\Classes\{progId}\shell\open\command", "\"" + applicationFilePath + "\" \"%1\"");
        return madeChanges;
    }

    private static bool SetKeyDefaultValue(string keyPath, string value)
    {
        using (var key = Registry.CurrentUser.CreateSubKey(keyPath))
        {
            if (key.GetValue(null) as string != value)
            {
                key.SetValue(null, value);
                return true;
            }
        }

        return false;
    }

18
你可以通过 ClickOnce 进行 受控 方式的操作,而无需自己烦恼地处理注册表。在 VS2008 及以上版本中(包括Express),你可以通过工具(即无需 xml)在项目属性 => 发布 => 选项 => 文件关联中实现。查看更多详情。

2
很好的回答,但不幸的是我正在使用VS2005,所以我必须等到我得到VS2010吗? - User2400
这很好,但是,你如何使用这个方法并设置自定义命令行参数?这个方法强制你使用 app.exe "%1",但如果我想让它执行 app.exe /config "%1" 呢? - Andy

11

上面的方法在我使用 Windows 10 时无法生效。 以下是我的解决方案,可以让当前用户使用 %localappdata%\MyApp\MyApp.exe 打开 .myExt 格式的文件。根据评论进行了优化。

 String App_Exe = "MyApp.exe";
 String App_Path = "%localappdata%";
 SetAssociation_User("myExt", App_Path + App_Exe, App_Exe);

 public static void SetAssociation_User(string Extension, string OpenWith, string ExecutableName)
 {
    try {
                using (RegistryKey User_Classes = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Classes\\", true))
                using (RegistryKey User_Ext = User_Classes.CreateSubKey("." + Extension))
                using (RegistryKey User_AutoFile = User_Classes.CreateSubKey(Extension + "_auto_file"))
                using (RegistryKey User_AutoFile_Command = User_AutoFile.CreateSubKey("shell").CreateSubKey("open").CreateSubKey("command"))
                using (RegistryKey ApplicationAssociationToasts = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\ApplicationAssociationToasts\\", true))
                using (RegistryKey User_Classes_Applications = User_Classes.CreateSubKey("Applications"))
                using (RegistryKey User_Classes_Applications_Exe = User_Classes_Applications.CreateSubKey(ExecutableName))
                using (RegistryKey User_Application_Command = User_Classes_Applications_Exe.CreateSubKey("shell").CreateSubKey("open").CreateSubKey("command"))
                using (RegistryKey User_Explorer = Registry.CurrentUser.CreateSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\." + Extension))
                using (RegistryKey User_Choice = User_Explorer.OpenSubKey("UserChoice"))
                {
                    User_Ext.SetValue("", Extension + "_auto_file", RegistryValueKind.String);
                    User_Classes.SetValue("", Extension + "_auto_file", RegistryValueKind.String);
                    User_Classes.CreateSubKey(Extension + "_auto_file");
                    User_AutoFile_Command.SetValue("", "\"" + OpenWith + "\"" + " \"%1\"");
                    ApplicationAssociationToasts.SetValue(Extension + "_auto_file_." + Extension, 0);
                    ApplicationAssociationToasts.SetValue(@"Applications\" + ExecutableName + "_." + Extension, 0);
                    User_Application_Command.SetValue("", "\"" + OpenWith + "\"" + " \"%1\"");
                    User_Explorer.CreateSubKey("OpenWithList").SetValue("a", ExecutableName);
                    User_Explorer.CreateSubKey("OpenWithProgids").SetValue(Extension + "_auto_file", "0");
                    if (User_Choice != null) User_Explorer.DeleteSubKey("UserChoice");
                    User_Explorer.CreateSubKey("UserChoice").SetValue("ProgId", @"Applications\" + ExecutableName);
                }
                SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero);
            }
            catch (Exception excpt)
            {
                //Your code here
            }
        }

  [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  public static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);

这个答案中可以看出:"RegistryKey类实现了IDisposable接口,因此您应该将您的键包装在一个using语句中。" 或者,当您完成使用RegistryKey时,您应该调用Close或Dispose。这意味着像这个例子中展示的链接调用CreateSubKey是一个坏主意。 - DavidRR
同意你的观点,这样做更加简洁,但了解上述代码可能带来的最严重的副作用仍然很有趣。有什么想法吗? - sofsntp
如果您不释放非托管资源(例如RegistryKey),则您的应用程序将遭受内存泄漏的影响。请参见资源泄漏、内存泄漏和性能之间的关系IDisposable接口。请注意,问题和已接受答案中的代码示例都包括所需的RegistryKey.Close调用。 - DavidRR
哎呀,看了这段代码30秒,它全部都是错的。User_Extension键在CURRENT_USER中创建了一个.ext键。它应该是CLASSES_ROOT。我在那之后就停止查看了。如果这个是错的,那么其余的也很可能是错的。 - Andy
@Sonic,不,这样更好,因为CURRENT_USER\Software\Classes是当前用户等效于CLASSES_ROOT,而且不需要管理员权限。代码之所以有问题是因为其他原因。 - Kirill Osenkov

8
如果您将键写入HKEY_CURRENT_USER\Software\Classes而不是HKEY_CLASSES_ROOT,则在Vista及更高版本下,此操作应该可以在没有管理员权限的情况下完成。

4
您正在使用旧版的Visual Studio,Vista将把您的程序视为“遗留”Windows应用程序,并重定向您进行的注册表写入。请在您的程序中包含清单文件,以便您看起来是Vista感知的。VS2008及以上版本会自动包含此清单文件。
请注意,这仍然不能解决用户的问题,她很难在关闭UAC的情况下运行您的应用程序。您需要编写一个独立的应用程序,它具有链接的清单文件并请求管理员权限。它需要设置requestedExecutionLevel为requireAdministrator的清单文件。

我无法向项目添加清单,因为实际的exe不在我的项目中。有什么办法可以让它出现,以便我可以添加资源吗?(尝试添加 -> 现有项并选择obj文件夹中的.exe只会复制它) - User2400
如果这确实是一个古老的应用程序,那么您很可能会破坏一些东西。 但是您可以使用mt.exe SDK工具注入清单。 - Hans Passant

4
如果您正在使用Visual Studio 2015,则安装设置和部署扩展。创建一个安装向导,然后将您的.exe文件附加到其中。在解决方案资源管理器中右键单击主程序,转到-查看,-文件类型,然后右键单击文件类型并选择添加新文件类型。根据您的需求更改所有属性,然后构建MSI安装程序。
注意:我重新阅读了您的问题并意识到您不想要安装程序。对此感到抱歉,尽管您应该考虑使用安装程序,因为它可以为您的程序提供更多的自定义选项。

2

将文件扩展名与您自己的程序关联的实际方法:

using Microsoft.Win32;
using System;
using System.IO;
using System.Runtime.InteropServices;
 
private static void RegisterForFileExtension(string extension, string applicationPath)
    {
        RegistryKey FileReg = Registry.CurrentUser.CreateSubKey("Software\\Classes\\" + extension);
        FileReg.CreateSubKey("shell\\open\\command").SetValue("", $"\"{applicationPath}\" \"%1\"");
        FileReg.Close();

        SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero);
    }
[DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);

编辑:感谢您的建议,我已经根据您的建议改变了解决方案。


1
实际上,唯一真正帮助我的答案。但是请确保转义您的应用程序路径和参数。否则,当您处理包含空格的文件名时,会遇到问题:我会像这样设置值:...SetValue("", $""{applicationPath}" "%1"") - Alan

1

这行代码:

FileReg.CreateSubKey("shell\open\command").SetValue("", applicationPath + " %1");

应该修改为:

FileReg.CreateSubKey("shell\open\command").SetValue("", $"\"{applicationPath}\" \"%1\"");

如果你不想在路径中出现空格时出现问题,比如:

C:\my folder\my file.txt


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接