使用任务获取目录图标

Getting directory icons using tasks

我的任务是使用任务获取目录图标并将它们显示在 DataGridView 中(我正在通过文件夹执行搜索)。为此,我使用 SHGetImageList WinAPI 函数。我有一个助手 class 如下:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace WindowsFormsApplication6 {
    public class Helper {
        private const uint ILD_TRANSPARENT = 0x00000001;
        private const uint SHGFI_SYSICONINDEX = 0x000004000;
        private const uint SHGFI_ICON = 0x000000100;
        public static readonly int MaxEntitiesCount = 80;
        public static void GetDirectories(string path, List<Image> col, IconSizeType sizeType, Size itemSize) {
            DirectoryInfo dirInfo = new DirectoryInfo(path);
            DirectoryInfo[] dirs = dirInfo.GetDirectories("*", SearchOption.TopDirectoryOnly);
            for (int i = 0; i < dirs.Length && i < MaxEntitiesCount; i++) {
                DirectoryInfo subDirInfo = dirs[i];
                if (!CheckAccess(subDirInfo) || !MatchFilter(subDirInfo.Attributes)) {
                    continue;
                }
                col.Add(GetFileImage(subDirInfo.FullName, sizeType, itemSize));
            }
        }

        public static bool CheckAccess(DirectoryInfo info) {
            bool isOk = false;
            try {
                var secInfo = info.GetAccessControl();
                isOk = true;
            }
            catch {
            }
            return isOk;
        }

        public static bool MatchFilter(FileAttributes attributes) {
            return (attributes & (FileAttributes.Hidden | FileAttributes.System)) == 0;
        }

        public static Image GetFileImage(string path, IconSizeType sizeType, Size itemSize) {
            return IconToBitmap(GetFileIcon(path, sizeType, itemSize), sizeType, itemSize);
        }

        public static Image IconToBitmap(Icon ico, IconSizeType sizeType, Size itemSize) {
            if (ico == null) {
                return new Bitmap(itemSize.Width, itemSize.Height);
            }
            return ico.ToBitmap();
        }

        public static Icon GetFileIcon(string path, IconSizeType sizeType, Size itemSize) {
            SHFILEINFO shinfo = new SHFILEINFO();
            IntPtr retVal = SHGetFileInfo(path, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), (int)(SHGFI_SYSICONINDEX | SHGFI_ICON));
            int iconIndex = shinfo.iIcon;
            IImageList iImageList = (IImageList)GetSystemImageListHandle(sizeType);
            IntPtr hIcon = IntPtr.Zero;
            if (iImageList != null) {
                iImageList.GetIcon(iconIndex, (int)ILD_TRANSPARENT, ref hIcon);
            }
            Icon icon = null;
            if (hIcon != IntPtr.Zero) {
                icon = Icon.FromHandle(hIcon).Clone() as Icon;
                DestroyIcon(shinfo.hIcon);
            }
            return icon;
        }

        private static IImageList GetSystemImageListHandle(IconSizeType sizeType) {
            IImageList iImageList = null;
            Guid imageListGuid = new Guid("46EB5926-582E-4017-9FDF-E8998DAA0950");
            int ret = SHGetImageList((int)sizeType, ref imageListGuid, ref iImageList);
            return iImageList;
        }

        [DllImport("shell32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags);
        [DllImport("shell32.dll", EntryPoint = "#727")]
        private static extern int SHGetImageList(int iImageList, ref Guid riid, ref IImageList ppv);
        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool DestroyIcon(IntPtr hIcon);
        public enum IconSizeType {
            Medium = 0x0,
            Small = 0x1,
            Large = 0x2,
            ExtraLarge = 0x4
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        private 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;
        }

        [ComImport,
        Guid("46EB5926-582E-4017-9FDF-E8998DAA0950"),
        InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IImageList {
            [PreserveSig]
            int Add(IntPtr hbmImage, IntPtr hbmMask, ref int pi);
            [PreserveSig]
            int ReplaceIcon(int i, IntPtr hicon, ref int pi);
            [PreserveSig]
            int SetOverlayImage(int iImage, int iOverlay);
            [PreserveSig]
            int Replace(int i, IntPtr hbmImage, IntPtr hbmMask);
            [PreserveSig]
            int AddMasked(IntPtr hbmImage, int crMask, ref int pi);
            [PreserveSig]
            int Draw(ref IMAGELISTDRAWPARAMS pimldp);
            [PreserveSig]
            int Remove(int i);
            [PreserveSig]
            int GetIcon(int i, int flags, ref IntPtr picon);
            [PreserveSig]
            int GetImageInfo(int i, ref IMAGEINFO pImageInfo);
            [PreserveSig]
            int Copy(int iDst, IImageList punkSrc, int iSrc, int uFlags);
            [PreserveSig]
            int Merge(int i1, IImageList punk2, int i2, int dx, int dy, ref Guid riid, ref IntPtr ppv);
            [PreserveSig]
            int Clone(ref Guid riid, ref IntPtr ppv);
            [PreserveSig]
            int GetImageRect(int i, ref RECT prc);
            [PreserveSig]
            int GetIconSize(ref int cx, ref int cy);
            [PreserveSig]
            int SetIconSize(int cx, int cy);
            [PreserveSig]
            int GetImageCount(ref int pi);
            [PreserveSig]
            int SetImageCount(int uNewCount);
            [PreserveSig]
            int SetBkColor(int clrBk, ref int pclr);
            [PreserveSig]
            int GetBkColor(ref int pclr);
            [PreserveSig]
            int BeginDrag(int iTrack, int dxHotspot, int dyHotspot);
            [PreserveSig]
            int EndDrag();
            [PreserveSig]
            int DragEnter(IntPtr hwndLock, int x, int y);
            [PreserveSig]
            int DragLeave(IntPtr hwndLock);
            [PreserveSig]
            int DragMove(int x, int y);
            [PreserveSig]
            int SetDragCursorImage(ref IImageList punk, int iDrag, int dxHotspot, int dyHotspot);
            [PreserveSig]
            int DragShowNolock(int fShow);
            [PreserveSig]
            int GetDragImage(ref POINT ppt, ref POINT pptHotspot, ref Guid riid, ref IntPtr ppv);
            [PreserveSig]
            int GetItemFlags(int i, ref int dwFlags);
            [PreserveSig]
            int GetOverlayImage(int iOverlay, ref int piIndex);
        }
        ;

        [StructLayout(LayoutKind.Sequential)]
        private struct IMAGELISTDRAWPARAMS {
            public int cbSize;
            public IntPtr himl;
            public int i;
            public IntPtr hdcDst;
            public int x;
            public int y;
            public int cx;
            public int cy;
            public int xBitmap;
            public int yBitmap;
            public int rgbBk;
            public int rgbFg;
            public int fStyle;
            public int dwRop;
            public int fState;
            public int Frame;
            public int crEffect;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct IMAGEINFO {
            public IntPtr hbmImage;
            public IntPtr hbmMask;
            public int Unused1;
            public int Unused2;
            public RECT rcImage;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct RECT {
            public int Left, Top, Right, Bottom;
            public RECT(int l, int t, int r, int b) {
                Left = l;
                Top = t;
                Right = r;
                Bottom = b;
            }

            public RECT(Rectangle r) {
                Left = r.Left;
                Top = r.Top;
                Right = r.Right;
                Bottom = r.Bottom;
            }

            public Rectangle ToRectangle() {
                return Rectangle.FromLTRB(Left, Top, Right, Bottom);
            }

            public void Inflate(int width, int height) {
                Left -= width;
                Top -= height;
                Right += width;
                Bottom += height;
            }

            public override string ToString() {
                return string.Format("x:{0},y:{1},width:{2},height:{3}", Left, Top, Right - Left, Bottom - Top);
            }
        }

        [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
        public struct POINT {
            public int X, Y;
            public POINT(int x, int y) {
                this.X = x;
                this.Y = y;
            }

            public POINT(Point pt) {
                this.X = pt.X;
                this.Y = pt.Y;
            }

            public Point ToPoint() {
                return new Point(X, Y);
            }
        }
    }
}

在一个窗体上,我有两个 DataGridView 和两个按钮。单击第一个按钮时,我在 UI 线程中加载图标:

private void button1_Click(object sender, EventArgs e) {
    List<Image> list = new List<Image>();
    Helper.GetDirectories(fPath, list, Helper.IconSizeType.Small, new Size(16, 16));
    dataGridView1.DataSource = list;
}

在点击第二个按钮时,我做了:

private void button2_Click(object sender, EventArgs e) {
    Func<object, List<Image>> a = null;
    a = (p) => {
        string path = (string)p;
        List<Image> list = new List<Image>();
        Helper.GetDirectories(path, list, Helper.IconSizeType.Small, new Size(16, 16));
        return list;
    };
    Task.Factory.StartNew(a, fPath).ContinueWith(t => { dataGridView2.DataSource = t.Result;},
TaskScheduler.FromCurrentSynchronizationContext());
}

所以,我也这样做,但是在一个任务中。

当我点击第一个按钮然后点击第二个按钮时,我得到以下内容 System.InvalidCastException:

Unable to cast COM object of type 'System.__ComObject' to interface type 'IImageList'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{46EB5926-582E-4017-9FDF-E8998DAA0950}' failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).

中引发异常
int ret = SHGetImageList((int)sizeType, ref imageListGuid, ref iImageList);

GetSystemImageListHandle 方法的行。

我不知道自己做错了什么。感谢任何帮助。

Windows shell 函数只能从 UI 线程调用。您有与此问题相同的根本问题:Program.Main().

上的 Unable to cast COM object of type 'System.__ComObject' to interface type 'IImageList'. Or more formally they can only be created in a single thread apartment. The UI thread in WPF and WinForms is be default in a single threaded apartment (that's the meaning of the [STAThread] 属性

Task.Run()使用的线程池线程不会是单线程套间(会是多线程套间)。当您尝试访问线程单元类型中的 COM 对象时,您可能会收到 E_NOINTERFACE 错误。*

可以在单线程单元中手动创建新线程。然而,在这种情况下,这似乎确实可靠地工作。它仍然会偶尔抛出 E_NOINTERFACE exce[topms.只需在 UI 线程上调用 SHGetImageList。感谢@vendettamit 实际测试它并发现它不起作用。

可以创建 new thread manually that is in a single threaded apartment:

Thread thread = new Thread(ThreadStartMethod);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();

这可能适用于您的情况,但如果您尝试访问 IImageList 或不同线程上的任何 COM 对象,您将 运行 遇到问题**。由于您似乎不会进行跨线程调用,因此以下内容应该有效:

private void button2_Click(object sender, EventArgs e) {
    ParameterizedThreadStart a = null;
    a = (p) => {
        string path = (string)p;
        List<Image> list = new List<Image>();
        Helper.GetDirectories(path, list, Helper.IconSizeType.Small, new Size(16, 16));
        this.Invoke(new Action(() => dataGridView2.DataSource = list));
    };

    Thread thread = new Thread(a);
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start(fPath);
}

* 一些 COM 接口支持跨单元编组,但 COM 编组是一个完全不同的主题。

** 因为 COM STA 封送处理的工作原理,您需要 Windows 消息泵 运行 在您的后台线程上运行。

Update: Chaning the [STAThread] to [MTAThread] on Main method, with the given code @Gosha_Fighten's problem everything seems to be working.

以下解决方案仅适用于 运行 在 [STAThread] 下的应用程序。 @shf301 解释的很好!但是答案中提供的示例代码无法解决问题。通过 SHGetImageList 调用跨线程构造函数导致了这个问题。因此,如果在 Worker 线程上调用了 Helper.GetDirectories,则不会出现该问题,而不是调用 DataGrid 绑定语句。

以下代码运行良好:

    private void button2_Click_1(object sender, EventArgs e)
    {
        Action a = null;
        a = () =>
        {
            List<Image> list = null;

            this.BeginInvoke(new Action(() =>
            {
                list = new List<Image>();
                Helper.GetDirectories(fPath, list, Helper.IconSizeType.Small, new Size(16, 16));
                dataGridView2.DataSource = list;
            }));
        };

        Task.Factory.StartNew(a);
    }

这里我删除了 ContinueWith() 并传递了 Current synchronization 参数。现在控件的调用方法将在工作线程上执行任务。 注意 - 由于所有工作都在 WorkerThread 上完成,因此无需保留 Task

.NET有SynchronizationContext的概念:

Provides the basic functionality for propagating a synchronization context in various synchronization models.

此上下文的主要方法名为 Post,它主要是向上下文发送异步消息。在这里,根据底层 UI 技术(Winforms、WPF 等)或非 UI 技术,可以调整事物并在这些技术的限制下优雅地工作。

默认情况下,Tasks 的任务调度程序不使用当前同步上下文,而是使用您无法真正控制的 ThreadPool(顺便说一下,它不使用 Winforms,也不玩 WPF),所以你必须指定你想要来自 SynchronizationContext 的 TaskScheduler,你只是部分地做了。

由于您是 运行 Winforms 应用程序,因此当前的同步上下文(Synchronization.Current) should be of WindowsFormsSynchronizationContext 类型。如果您可以查看其 Post 的实现,您将看到此:

public override void Post(SendOrPostCallback callback, object state)
{
    if (controlToSendTo != null)
    {
        controlToSendTo.BeginInvoke(callback, new object[] { state });
    }
}

这个上下文的实现应该可以很好地与 Winforms UI 线程一起工作......只要你使用它。事实上你几乎做对了,你只是忘了在 StartNew 方法中使用它。

所以,只需将您的代码更改为:

Task.Factory.StartNew(a, fPath,
    CancellationToken.None, TaskCreationOptions.None, // unfortunately, there is no easier overload with just the context...
    TaskScheduler.FromCurrentSynchronizationContext()).ContinueWith(
        t => { dataGridView2.DataSource = t.Result; },
        TaskScheduler.FromCurrentSynchronizationContext());

它应该可以工作。

只需插入

Marshal.FinalReleaseComObject(iImageList);

之后

iImageList.GetIcon(iconIndex, (int)ILD_TRANSPARENT, ref hIcon);

行。

此外,您可能感兴趣的是,当您传递 SHGFI_SYSICONINDEX 时,SHGetFileInfo 实际上是 returns IImageList。所以,这样的事情会起作用:

    [DllImport("shell32.dll", EntryPoint = "SHGetFileInfo", CharSet = CharSet.Auto)]
    private static extern IImageList SHGetFileInfoAsImageList(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags);

整个图像提取可以很简单:

    public static Image GetFileImage(string path, IconSizeType sizeType, Size itemSize)
    {
        var shfi = new SHFILEINFO();
        var imageList = SHGetFileInfoAsImageList(path, 0, ref shfi, (uint)Marshal.SizeOf(shfi), (int)SHGFI_SYSICONINDEX);
        if (imageList != null)
        {
            var hIcon = IntPtr.Zero;
            imageList.GetIcon(shfi.iIcon, (int)ILD_TRANSPARENT, ref hIcon);
            Marshal.FinalReleaseComObject(imageList);
            if (hIcon != IntPtr.Zero)
            {
                var image = Bitmap.FromHicon(hIcon);
                DestroyIcon(hIcon);
                return image;
            }
        }
        return new Bitmap(itemSize.Width, itemSize.Height);
    }