Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting directory icons using tasks

My task is to get directory icons using tasks and display them in a DataGridView (I'm performing search through folders). For this, I use the SHGetImageList WinAPI function. I have a helper class as the following:

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

On a form I have two DataGridViews and two buttons. On a click of the first button, I load icons in the UI thread:

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

On the second button click, I do:

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

So, I do the same, but in a task.

When I click the first button and then the second one, I'm getting the following 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)).

The exception is raised in the

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

line of the GetSystemImageListHandle method.

I can't figure out what I'm doing wrong. Any help is appreciated.

like image 481
Gosha_Fighten Avatar asked Aug 08 '15 16:08

Gosha_Fighten


4 Answers

Just insert

Marshal.FinalReleaseComObject(iImageList);

after

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

line.

Also, you might be interested that when you pass SHGFI_SYSICONINDEX, SHGetFileInfo actually returns IImageList. So, something like this would work:

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

and the whole image extraction could be simply:

    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);
    }
like image 170
Ivan Stoev Avatar answered Nov 13 '22 10:11

Ivan Stoev


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

The Below solution applied only if application is running under [STAThread]. @shf301 Explained it well! But the sample code provided in answer wouldn't solve the problem. The cross thread constructor invocation via SHGetImageList is causing the issue. So rather than invoking DataGrid binding statement, if the Helper.GetDirectories has been called on Worker thread the issue will not occur.

The below code is working fine:

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

Here I have removed the ContinueWith() and passing of Current synchronization parameter. Now the Control's invoke method will execute the task on worker thread. Note - Since all the work is being done on WorkerThread then there's no need to keep the Task.

like image 23
vendettamit Avatar answered Nov 13 '22 11:11

vendettamit


The Windows shell functions can only be called from a UI thread. You have the same root problem as in this question: 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] attribute on Program.Main().

The thread pool thread used by Task.Run() will not be a single threaded apartment (it will be a multi-threaded apartment). When you try to access a COM object in the thread apartment type you can get an E_NOINTERFACE error.*

It is possible to create a new thread manually that is in a single threaded apartment. However that does to appear to work reliably in this case. It will still sporadically throw E_NOINTERFACE exce[topms. Just call SHGetImageList on the UI thread. Thanks to @vendettamit for actually testing it and finding that it doesn't work.

It is possible to create a new thread manually that is in a single threaded apartment:

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

This may work in your case, but you will run into problems if you try to access the IImageList or any COM object on a different thread**. Since it doesn't appear that you will be making an cross threaded calls, the following should work:

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

* Some COM interfaces support marshaling across apartments, but COM marshaling is an entire different topic.

** Because then you'd need a Windows message pump running on your background thread because of how COM STA marshaling works.

like image 1
shf301 Avatar answered Nov 13 '22 09:11

shf301


.NET has the concept of SynchronizationContext:

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

This context's main method is named Post and it basically dispatches an asynchronous message to the context. This is where, depending on the underlying UI technology (Winforms, WPF, etc.), or non UI technology, things can be tweaked and gracefully work with the constraints these technology has.

By default, the Tasks' task scheduler does not use the current synchronization context, but instead uses the ThreadPool which you cannot really control (and does not play with Winforms, nor WPF by the way), so you have to specify you want the TaskScheduler from the SynchronizationContext, which you only partially did.

Since you're running a Winforms app, the current synchronization context (Synchronization.Current) should be of WindowsFormsSynchronizationContext type. If you can have a look at its implementation of Post, you will see this:

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

This context's implementation should work fine with Winforms UI thread ... provided you use it. In fact you almost got it right, you just forgot to use it in the StartNew method.

So, just change your code into this:

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

And it should work.

like image 1
Simon Mourier Avatar answered Nov 13 '22 10:11

Simon Mourier