获取Shell使用的文件图标

53
在 .Net 中(无论是 C# 还是 VB),给定一个文件路径字符串、FileInfo 结构或 FileSystemInfo 结构,用于真实存在的文件,我该如何确定 shell(资源管理器)为该文件使用的图标?我目前没有计划将其用于任何事情,但在查看这个问题时,我变得好奇如何做到这一点,并且我认为在 SO 上将其存档会很有用。

相关链接:https://dev59.com/clgQ5IYBdhLWcg3wWCnP - Joel Coehoorn
10个回答

65
Imports System.Drawing
Module Module1

    Sub Main()    
        Dim filePath As String =  "C:\myfile.exe"  
        Dim TheIcon As Icon = IconFromFilePath(filePath)  

        If TheIcon IsNot Nothing Then    
            ''#Save it to disk, or do whatever you want with it.
            Using stream As New System.IO.FileStream("c:\myfile.ico", IO.FileMode.CreateNew)
                TheIcon.Save(stream)          
            End Using
        End If
    End Sub

    Public Function IconFromFilePath(filePath As String) As Icon
        Dim result As Icon = Nothing
        Try
            result = Icon.ExtractAssociatedIcon(filePath)
        Catch ''# swallow and return nothing. You could supply a default Icon here as well
        End Try
        Return result
    End Function
End Module

2
测试表明这确实有效。我对他最初的解释不太清楚 - 我以为它只在文件本身中查找图标资源,但令人高兴的是,事实并非如此。 - Joel Coehoorn
1
太棒了!我怎样才能为文件夹获取一个图标? - bohdan_trotsenko
7
如果你想在WPF中做同样的事情,可以使用System.Drawing.Icon的Handle属性为图像创建一个BitmapSource: image.Source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHIcon( result.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions() ); 请注意,ExtractAssociatedIcon始终返回图标的32x32像素版本。 - Christian Rodemeyer
@Stefan,请问你能帮忙解决一个问题吗?当我把系统托盘图标换成其他的图标时,它就变成了空白/透明。 - ganders
当我在一个不包含图标图像的.dll上使用它时,它只会为我返回通用的未知文件类型图标。我希望它能够返回系统.dll图标。 - xr280xr
显示剩余8条评论

18

您应该使用SHGetFileInfo。

在大多数情况下,Icon.ExtractAssociatedIcon与SHGetFileInfo的效果相同,但SHGetFileInfo可以处理UNC路径(例如网络路径,如“\\计算机名称\共享文件夹\”),而Icon.ExtractAssociatedIcon则不能。如果需要或可能需要使用UNC路径,则最好使用SHGetFileInfo而不是Icon.ExtractAssociatedIcon。

这是一篇关于如何使用SHGetFileInfo的好的CodeProject文章


这些API是否可以获取动态图标,例如为PDF文档和图像生成的预览图标?链接的CodeProject项目通过文件扩展名缓存图像,因此答案似乎是否定的。 - Triynko

17

4
既然我们追求C#或VB,Stefan的答案更简单明了。 - Wim Coenen
如果有人想在Unity3D中获取图标,那么这个方法是可行的。我曾尝试过使用System.Drawing.Icon.ExtractAssociatedIconshell32.dll ExtractAssociatedIcon,第一种方法给了我错误的图标,第二种方法虽然可行但图标并不总是正确的。最终偶然发现了这个答案,它完全符合我的意图。 - killer_mech
不使用注册表 - 你可能是指 PInvoke? - TarmoPikaro

10

仅仅是 Stefan 回答的 C# 版本。

using System.Drawing;

class Class1
{
    public static void Main()
    {
        var filePath =  @"C:\myfile.exe";
        var theIcon = IconFromFilePath(filePath);

        if (theIcon != null)
        {
            // Save it to disk, or do whatever you want with it.
            using (var stream = new System.IO.FileStream(@"c:\myfile.ico", System.IO.FileMode.CreateNew))
            {
                theIcon.Save(stream);
            }
        }
    }

    public static Icon IconFromFilePath(string filePath)
    {
        var result = (Icon)null;

        try
        {
            result = Icon.ExtractAssociatedIcon(filePath);
        }
        catch (System.Exception)
        {
            // swallow and return nothing. You could supply a default Icon here as well
        }

        return result;
    }
}

1
我想要一种从命令行执行的方式,因此我将这个答案转换为了在BAT脚本中自编译和执行的C#程序。并且我还增加了接受文件路径作为参数的功能。详见:https://github.com/jgstew/tools/blob/master/CSharp/ExtractAssociatedIcon.bat - jgstew

7

这对我的项目有效,希望能帮助到别人。

这是使用P/Invokes的C#代码,在x86/x64系统上运行良好,支持WinXP及以上版本。

(Shell.cs)

using System;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;

namespace IconExtraction
{
    internal sealed class Shell : NativeMethods
    {
        #region OfExtension

        ///<summary>
        /// Get the icon of an extension
        ///</summary>
        ///<param name="filename">filename</param>
        ///<param name="overlay">bool symlink overlay</param>
        ///<returns>Icon</returns>
        public static Icon OfExtension(string filename, bool overlay = false)
        {
            string filepath;
            string[] extension = filename.Split('.');
            string dirpath = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "cache");
            Directory.CreateDirectory(dirpath);
            if (String.IsNullOrEmpty(filename) || extension.Length == 1)
            {
                filepath = Path.Combine(dirpath, "dummy_file");
            }
            else
            {
                filepath = Path.Combine(dirpath, String.Join(".", "dummy", extension[extension.Length - 1]));
            }
            if (File.Exists(filepath) == false)
            {
                File.Create(filepath);
            }
            Icon icon = OfPath(filepath, true, true, overlay);
            return icon;
        }
        #endregion

        #region OfFolder

        ///<summary>
        /// Get the icon of an extension
        ///</summary>
        ///<returns>Icon</returns>
        ///<param name="overlay">bool symlink overlay</param>
        public static Icon OfFolder(bool overlay = false)
        {
            string dirpath = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "cache", "dummy");
            Directory.CreateDirectory(dirpath);
            Icon icon = OfPath(dirpath, true, true, overlay);
            return icon;
        }
        #endregion

        #region OfPath

        ///<summary>
        /// Get the normal,small assigned icon of the given path
        ///</summary>
        ///<param name="filepath">physical path</param>
        ///<param name="small">bool small icon</param>
        ///<param name="checkdisk">bool fileicon</param>
        ///<param name="overlay">bool symlink overlay</param>
        ///<returns>Icon</returns>
        public static Icon OfPath(string filepath, bool small = true, bool checkdisk = true, bool overlay = false)
        {
            Icon clone;
            SHGFI_Flag flags;
            SHFILEINFO shinfo = new SHFILEINFO();
            if (small)
            {
                flags = SHGFI_Flag.SHGFI_ICON | SHGFI_Flag.SHGFI_SMALLICON;
            }
            else
            {
                flags = SHGFI_Flag.SHGFI_ICON | SHGFI_Flag.SHGFI_LARGEICON;
            }
            if (checkdisk == false)
            {
                flags |= SHGFI_Flag.SHGFI_USEFILEATTRIBUTES;
            }
            if (overlay)
            {
                flags |= SHGFI_Flag.SHGFI_LINKOVERLAY;
            }
            if (SHGetFileInfo(filepath, 0, ref shinfo, Marshal.SizeOf(shinfo), flags) == 0)
            {
                throw (new FileNotFoundException());
            }
            Icon tmp = Icon.FromHandle(shinfo.hIcon);
            clone = (Icon)tmp.Clone();
            tmp.Dispose();
            if (DestroyIcon(shinfo.hIcon) != 0)
            {
                return clone;
            }
            return clone;
        }
        #endregion
    }
}

(NativeMethods.cs)

using System;
using System.Drawing;
using System.Runtime.InteropServices;

namespace IconExtraction
{
    internal class NativeMethods
    {
        public struct SHFILEINFO
        {
            public IntPtr hIcon;
            public int iIcon;
            public uint dwAttributes;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
            public string szDisplayName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
            public string szTypeName;
        };

        [DllImport("user32.dll")]
        public static extern int DestroyIcon(IntPtr hIcon);

        [DllImport("shell32.dll", CharSet = CharSet.Auto, BestFitMapping = false, ThrowOnUnmappableChar = true)]
        public static extern IntPtr ExtractIcon(IntPtr hInst, string lpszExeFileName, int nIconIndex);

        [DllImport("Shell32.dll", BestFitMapping = false, ThrowOnUnmappableChar = true)]
        public static extern int SHGetFileInfo(string pszPath, int dwFileAttributes, ref SHFILEINFO psfi, int cbFileInfo, SHGFI_Flag uFlags);

        [DllImport("Shell32.dll")]
        public static extern int SHGetFileInfo(IntPtr pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, int cbFileInfo, SHGFI_Flag uFlags);
    }

    public enum SHGFI_Flag : uint
    {
        SHGFI_ATTR_SPECIFIED = 0x000020000,
        SHGFI_OPENICON = 0x000000002,
        SHGFI_USEFILEATTRIBUTES = 0x000000010,
        SHGFI_ADDOVERLAYS = 0x000000020,
        SHGFI_DISPLAYNAME = 0x000000200,
        SHGFI_EXETYPE = 0x000002000,
        SHGFI_ICON = 0x000000100,
        SHGFI_ICONLOCATION = 0x000001000,
        SHGFI_LARGEICON = 0x000000000,
        SHGFI_SMALLICON = 0x000000001,
        SHGFI_SHELLICONSIZE = 0x000000004,
        SHGFI_LINKOVERLAY = 0x000008000,
        SHGFI_SYSICONINDEX = 0x000004000,
        SHGFI_TYPENAME = 0x000000400
    }
}

1

如果您只对特定扩展名的图标感兴趣,并且不介意创建临时文件,您可以按照此处示例所示的方法进行操作。

C# 代码:

    public Icon LoadIconFromExtension(string extension)
    {
        string path = string.Format("dummy{0}", extension);
        using (File.Create(path)) { }
        Icon icon = Icon.ExtractAssociatedIcon(path);
        File.Delete(path);
        return icon;
    }

1

注册表方法的问题在于您没有明确获取图标索引ID。有时(如果不是所有时候),您会获得一个图标ResourceID,这是应用程序开发人员用来命名图标插槽的别名。

因此,注册表方法意味着所有开发人员都使用与隐式图标索引ID相同的ResourceIDs(基于零、绝对、确定性)。

扫描注册表位置,您将看到许多负数,甚至文本引用-即不是图标索引ID。隐式方法似乎更好,因为它让操作系统完成工作。

现在只测试这种新方法,但它很有道理,希望解决这个问题。


1
更新 - Zach的链接非常好!Shell完成了繁重的工作,我不再需要担心资源/图标ID :) 谢谢大家。 - OnyxxOr

0

应用程序可以包含多个图标,仅提取其中一个可能不足以满足您的需求。我自己想要挑选图标以便稍后在编译中重复使用它来制作 shim。

官方方法是使用 IconLib.Unofficial 0.73.0 或更高版本。

添加以下代码:

MultiIcon multiIcon = new MultiIcon();
multiIcon.Load(<in path>);
multiIcon.Save(<out path>, MultiIconFormat.ICO);

可以提取应用程序使用的图标。

但是 - 库本身在 .net framework 4.6.1 - v4.8 中工作,不适用于 .net core。

我尝试过的其他方法也是如此:

Icon icon = Icon.ExtractAssociatedIcon(<in path>);
using (FileStream stream = new FileStream(<out path>, FileMode.CreateNew))
{
    icon.Save(stream);
}

仅适用于一个图标,但某种程度上也已经损坏了。 当使用pinvoke方法SHGetFileInfo时,我得到了类似的效果。

使用PeNet库,代码如下:

var peFile = new PeFile(cmdArgs.iconpath);
byte[] icon = peFile.Icons().First().AsSpan().ToArray();
File.WriteAllBytes(iconPath, icon);

PeNet 允许提取图标,但它们不是原始格式,而且有多个。在这个提交中,整个功能被开发出来了 - 但还没有线索如何使用该功能。也许需要等待功能成熟。(请参见Penet问题#258

ICSharpCode.Decompiler 可以用于私有方法,以提供类似的功能:

PEFile file = new PEFile(cmdArgs.iconpath);
var resources = file.Reader.ReadWin32Resources();
if (resources != null)
{
    var createAppIcon = typeof(WholeProjectDecompiler).GetMethod("CreateApplicationIcon", BindingFlags.Static | BindingFlags.NonPublic);

    byte[] icon = (byte[])createAppIcon.Invoke(null, new[] { file });
    File.WriteAllBytes(iconPath, icon);
}

但是我在 .net core 3.1 编译的二进制文件中遇到了异常,可能这个库并不适用于所有情况。


0

这个链接似乎有一些信息。它涉及大量的注册表遍历,但似乎是可行的。示例是用C++编写的。


0
  • 确定文件扩展名
  • 在注册表中,前往"HKCR\.{扩展名}",读取默认值(我们称之为文件类型
  • "HKCR\{文件类型}\DefaultIcon"中,读取默认值:这是图标文件的路径(或嵌入图标资源的 .exe 等图标容器文件)
  • 如果需要,使用您喜欢的方法从提到的文件中提取图标资源

如果图标位于容器文件中(这很常见),则路径后面会有一个计数器,如这样:"foo.exe,3"。这意味着它是可用图标的第4个(索引从零开始)。",0" 的值是隐含的(也是可选的)。如果计数器为 0 或缺失,则 shell 将使用第一个可用的图标。


如果是包含多个图标的图标容器文件,如何知道要使用哪一个? - Joel Coehoorn
路径后面有一个计数器,例如“foo.exe,3”。这意味着它是可用图标中的第4个图标(索引从0开始)。值“,0”是隐式的,因此是可选的。如果缺失,则 shell 将使用第一个可用的图标。 - Tomalak
3
注册表不是API!还有其他方法可以指定图标,使用这种方式是错误的。请改用SHGetFileInfo API。 - Tron
@timbagas:“而且这种方法是错误的”……除了“不使用API”之外,它还有什么其他错误之处吗? - Tomalak

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