【问题标题】:Getting directory icons using tasks使用任务获取目录图标
【发布时间】:2015-08-08 16:09:40
【问题描述】:

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

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:

无法将“System.__ComObject”类型的 COM 对象转换为接口 输入“IImageList”。此操作失败,因为 QueryInterface 调用具有 IID 的接口的 COM 组件 “{46EB5926-582E-4017-9FDF-E8998DAA0950}”由于以下原因而失败 错误:不支持此类接口(HRESULT 异常:0x80004002 (E_NOINTERFACE))。

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

GetSystemImageListHandle 方法的行。

我不知道我做错了什么。任何帮助表示赞赏。

【问题讨论】:

  • 哪一行抛出异常?
  • @andlabs , int ret = SHGetImageList((int)sizeType, ref imageListGuid, ref iImageList);在 GetSystemImageListHandle 方法中。
  • @Gosha_Fighten 您面临的问题是由于跨线程元帅对象。您需要做的就是在工作线程上调用GetDirectories() 方法。请参阅下面的答案以解决问题。

标签: c# .net winapi datagridview task


【解决方案1】:

插入

Marshal.FinalReleaseComObject(iImageList);

之后

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

行。

另外,您可能感兴趣的是,当您传递 SHGFI_SYSICONINDEX 时,SHGetFileInfo 实际上会返回 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);
    }

【讨论】:

    【解决方案2】:

    更新:[STAThread] 更改为 [MTAThread]主要方法,使用给定的代码@Gosha_Fighten 的问题,一切似乎都在工作。

    仅当应用程序在 [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 参数的传递。现在 Control 的调用方法将在工作线程上执行任务。 注意 - 由于所有工作都在 WorkerThread 上完成,因此无需保留 Task

    【讨论】:

    • 调用任务然后调用回 UI 线程有什么意义?你不妨完全摆脱这个任务。
    • 在我的示例中没有看到任何“跨线程构造函数调用”,我只是对其进行了测试,并且似乎工作正常。你能解释一下你的意思吗?
    • 如果前后调用button1和button2的handler,就会出现异常。
    • 更改为 [MTAThread] 可以解决问题并不奇怪,因为现在所有东西都在同一个公寓里。但是,这不是一个安全的更改,WinForms 在 MTAThread 上不受支持。
    【解决方案3】:

    Windows shell 函数只能从 UI 线程调用。您有与此问题相同的根本问题:Unable to cast COM object of type 'System.__ComObject' to interface type 'IImageList'。或者更正式地说,它们只能在single thread apartment 中创建。 WPF 和 WinForms 中的 UI 线程在单线程单元中是默认的(这就是 Program.Main() 上的 [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 消息泵。

    【讨论】:

      【解决方案4】:

      .NET 有SynchronizationContext 的概念:

      提供传播同步的基本功能 各种同步模型中的上下文。

      此上下文的主要方法名为Post,它基本上将异步消息分派到上下文。在这里,根据底层 UI 技术(Winforms、WPF 等)或非 UI 技术,可以调整事物并在这些技术的约束下优雅地工作。

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

      由于您正在运行 Winforms 应用程序,当前同步上下文 (Synchronization.Current) 应该是 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());
      

      它应该可以工作。

      【讨论】:

      • @vendettamit - 是的,但在 StartNew 中没有。这有很大的不同
      猜你喜欢
      • 1970-01-01
      • 2021-11-20
      • 2020-10-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-26
      相关资源
      最近更新 更多