使用任务获取目录图标

时间:2015-08-08 16:09:41

标签: c# .net winapi datagridview task

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

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;
}

在表单上,​​我有两个DataGridViews和两个按钮。单击第一个按钮,我在UI线程中加载图标:

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());
}

点击第二个按钮,我这样做:

int ret = SHGetImageList((int)sizeType, ref imageListGuid, ref iImageList);

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

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

  

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

中引发了异常
{{1}}

GetSystemImageListHandle方法的一行。

我无法弄清楚我做错了什么。任何帮助表示赞赏。

4 个答案:

答案 0 :(得分:3)

只需插入

即可
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);
    }

答案 1 :(得分: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的invoke方法将在工作线程上执行任务。 注意 - 由于所有工作都在WorkerThread上完成,因此无需保留Task

答案 2 :(得分:1)

只能从UI线程调用Windows shell函数。您遇到与此问题相同的根问题: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除了[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编组是一个完全不同的主题。

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

答案 3 :(得分:1)

.NET具有SynchronizationContext

的概念
  

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

此上下文的main方法名为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());

它应该有用。