C# 调用 Win10/11 文件关联对话框

44 阅读3分钟

前言

在日常使用电脑的过程中,文件关联是一个非常常见的需求。不管是打开 .txt 文件时希望默认使用某个文本编辑器,还是双击 .jpg 文件时希望启动特定的图片查看工具,这些操作都依赖于操作系统的文件关联设置。

Windows 10 和 Windows 11 提供了内置的文件关联对话框,可以轻松地管理和修改文件类型与应用程序之间的关联。然而,作为开发者我们有时需要在自己的应用程序中调用这一功能,为用户提供更加便捷的操作体验。

本文将探讨如何通过 C# 调用 Windows 10/11 的文件关联对话框,帮助大家实现这一功能,本文为大家提供清晰的思路和实用的代码示例。

正文

方法一:调用未公开接口 IOpenWithLauncher

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("6A283FE2-ECFA-4599-91C4-E80957137B26")]
interface IOpenWithLauncher
{
    [PreserveSig]
    int Launch(IntPtr hWndParent,
         [MarshalAs(UnmanagedType.LPWStr)] string lpszPath,
         IMMERSIVE_OPENWITH flags);
}
 
[Flags]
enum IMMERSIVE_OPENWITH
{
    NONE = 0,
    OVERRIDE = 0x1,
    DONOT_EXEC = 0x4,
    PROTOCOL = 0x8,
    URL = 0x10,
    USEPOSITION = 0x20,
    DONOT_SETDEFAULT = 0x40,
    ACTION = 0x80,
    ALLOW_EXECDEFAULT = 0x100,
    NONEDP_TO_EDP = 0x200,
    EDP_TO_NONEDP = 0x400,
    CALLING_IN_APP = 0x800,
};
 
public static void ShowSetAssocDialog(string extension)
{
    var CLSID_ExecuteUnknown = new Guid("{E44E9428-BDBC-4987-A099-40DC8FD255E7}");
    var obj = Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ExecuteUnknown));
    if (obj is IOpenWithLauncher launcher)
    {
        launcher.Launch(IntPtr.Zero, extension, IMMERSIVE_OPENWITH.DONOT_EXEC);
        Marshal.ReleaseComObject(launcher);
    }
}

方法二:通过模拟点击属性对话框"更改"打开方式

[DllImport("shell32.dll", CharSet = CharSet.Auto)]
static extern bool SHObjectProperties(IntPtr hWnd, SHOP shopObjectType, string pszObjectName, string pszPropertyPage);
 
enum SHOP
{
    PRINTERNAME = 1,
    FILEPATH = 2,
    VOLUMEGUID = 4
}
 
[DllImport("kernel32.dll")]
static extern int GetCurrentProcessId();
 
[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
 
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
 
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
 
[DllImport("user32.dll")]
static extern bool SetForegroundWindow(IntPtr hWnd);
 
[DllImport("user32.dll")]
static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
 
delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
 
public static void ShowSetAssocDialog(string extension)
{
    string fileName = Path.ChangeExtension(Path.GetRandomFileName(), extension);
    string filePath = Path.Combine(Path.GetTempPath(), fileName);
    fileName = Path.GetFileNameWithoutExtension(fileName);
    File.WriteAllText(filePath, string.Empty); // 创建临时文件
    var frame = new DispatcherFrame();
    int pid = GetCurrentProcessId();
    SHObjectProperties(IntPtr.Zero, SHOP.FILEPATH, filePath, null); // 显示属性对话框
    while (true)
    {
        bool found = !EnumWindows((hWnd, lParam) => // 枚举窗口
        {
            GetWindowThreadProcessId(hWnd, out int id);
            if (id == pid) // 比较进程 id
            {
                const int MAX_PATH = 260;
                var sb = new StringBuilder(MAX_PATH);
                GetClassName(hWnd, sb, sb.Capacity);
                if (sb.ToString() == "#32770") // 对话框类名
                {
                    GetWindowText(hWnd, sb, sb.Capacity);
                    if (sb.ToString().Contains(fileName)) // 对话框标题是否包含文件名
                    {
                        SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP.HIDEWINDOW);// 隐藏属性对话框
                        MessageHooker.AddHook(hWnd, (ref Message m) =>
                        {
                            const int PSM_CHANGED = 0x400 + 104;
                            if (m.Msg == PSM_CHANGED)// 监测属性表页更改
                            {
                                frame.Continue = false;
                                PostMessage(hWnd, WM.CLOSE, 0, 0); // 等效 EndDialog(hWnd, 0)
                            }
                            return false;
                        });
                        SetForegroundWindow(hWnd);
                        SendKeys.SendWait("%C");// ALT + C 快捷键
                        return false;
                    }
                }
            }
            return true;
        }, IntPtr.Zero);
        if (found) break;
    }
    File.Delete(filePath); // 删除临时文件
    Dispatcher.PushFrame(frame);
}

此方法来自两年前我的自问自答,代码引用了 [《C# 窗口过程消息处理 WndProc》中的附加到其他窗口辅助类

总结

通过本文的介绍,我们了解了如何使用 C# 调用 Windows 10/11 的文件关联对话框,并实现了在应用程序中为用户提供直接修改文件关联的功能。

这一功能不仅提升了用户体验,还让我们的应用程序更加贴近用户的实际需求。尽管涉及到一些 Windows API 的调用,但通过合理的封装和代码组织,整个过程并不复杂。希望本文能够为大家在开发类似功能时提供有价值的参考。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!