3

This problem is starting to haunt me!!

My google-fu may not be strong in the force, however what I have been able to come up with so far still results in an expensive operation.

I am trying to obtain the icons from folders, but ONLY folders that have an icon other than default assigned to them. What it has come down to is checking for the existence of a desktop.ini file within the folder, then getting the icon using SHGetFileInfo for the folder. The problem with this method, is windows seems to enjoy placing desktop.ini files in places where they really have no impact on anything such as c:\windows\assembly. So then I adjusted my code to not only check for the existence of desktop.ini, but to read it's contents to check for the string IconFile.

This is still painfully slow as looking at 10 directories takes approximately 7-10 seconds. The purpose of this is to dynamically add the icons to a treeview control on-the-fly since there doesn't appear to be any other way to determine or obtain the icons. So I decided to try an make the OnBeforeExpand() into an async/await by fudging around with Task.Run() and Invoke() the node changes as required. The resulting code is as follows :

protected async override void OnBeforeExpand(TreeViewCancelEventArgs e)
{
    await Task.Run(() =>
    {
        if (!_expandedCache.Contains(e.Node.FullPath))
        {
            ShellFileGetInfo.FolderIcons fi;
            _expandedCache.Add(e.Node.FullPath);
            string curPath;
            foreach (TreeNode n in e.Node.Nodes)
            {
                curPath = Path.Combine((string)Tag, n.FullPath.Replace('/', Path.DirectorySeparatorChar));
                if (File.Exists(Path.Combine(curPath, "desktop.ini")) == true)
                {
                    if (File.ReadAllText(Path.Combine(curPath, "desktop.ini")).Contains("IconFile"))
                    {
                        fi = ShellFileGetInfo.GetFolderIcon(curPath, false);
                        if (fi.closed != null || fi.open != null)
                        {
                            if (InvokeRequired)
                            {
                                Invoke((MethodInvoker)(() =>
                                    {
                                        BeginUpdate();
                                        ImageList.Images.Add(fi.closed);
                                        ImageList.Images.Add(fi.open);
                                        n.SelectedImageIndex = ImageList.Images.Count - 1;
                                        n.ImageIndex = ImageList.Images.Count - 2;
                                        EndUpdate();
                                    }
                                ));
                            }
                            else
                            {
                                BeginUpdate();
                                ImageList.Images.Add(fi.closed);
                                ImageList.Images.Add(fi.open);
                                n.SelectedImageIndex = ImageList.Images.Count - 1;
                                n.ImageIndex = ImageList.Images.Count - 2;
                                EndUpdate();
                            }
                        }
                    }
                }
                //EndUpdate();
            }

        }
    });
    base.OnBeforeExpand(e);
}

What can I do about this to make this perform so there is little or no noticeable lag when expanding the tree, as 1 second (approximately) per sub-folder scan is absolutely insane.

Is there another place I can scan for the icon information to read it before hand or am I stuck parsing/checking for Desktop.ini files ?? I believe there must be as I can delete my desktop.ini file from many locations and still have folders with assigned icons. I am unable to find any information on where Windows is hiding this information. My best guess is somewhere in the registry (which would still be faster than accessing the file system and parsing desktop.ini files).

Paste of the GetFolderIcons() method which returns a struct of two Icon types. - You will notice options to have any overlay embedded as part of the icon if required, as well as large or small icons. For the purpose of the above code, I am requesting small icons with automatic overlays embedded.

/// <summary>
/// Get a list of open and closed icons for the specified folder
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static FolderIcons GetFolderIcon(string path, bool largeIcon = true, bool autoOverlay = true)
{
    FolderIcons fi = new FolderIcons();
    SHFILEINFO shInfo = new SHFILEINFO();
    IntPtr ptr = new IntPtr();

    uint flags = SHGFI_ICON | SHGFI_SMALLICON | SHGFI_ADDOVERLAYS;
    uint flags_open = SHGFI_ICON | SHGFI_SMALLICON | SHGFI_ADDOVERLAYS;
    if (autoOverlay == false && largeIcon == false)
    {
        flags = SHGFI_ICON | SHGFI_SMALLICON;
        flags_open = SHGFI_ICON | SHGFI_SMALLICON | SHGFI_OPENICON;
    } else if(autoOverlay == false && largeIcon == true)
    {
        flags = SHGFI_ICON | SHGFI_LARGEICON;
        flags_open = SHGFI_ICON | SHGFI_LARGEICON | SHGFI_OPENICON;
    }
    else if(autoOverlay == true && largeIcon == false)
    {
        flags = SHGFI_ICON | SHGFI_SMALLICON | SHGFI_ADDOVERLAYS;
        flags_open = SHGFI_ICON | SHGFI_SMALLICON | SHGFI_ADDOVERLAYS | SHGFI_OPENICON;
    }
    else if(autoOverlay == true && largeIcon == true)
    {
        flags = SHGFI_ICON | SHGFI_LARGEICON | SHGFI_ADDOVERLAYS;
        flags_open = SHGFI_ICON | SHGFI_LARGEICON | SHGFI_ADDOVERLAYS | SHGFI_OPENICON;
    }

    try
    {
        ptr = SHGetFileInfo(path, 0x00000010, ref shInfo, (uint)Marshal.SizeOf(shInfo), flags);
        if (ptr != IntPtr.Zero)
        {
            fi.closed = (Icon)Icon.FromHandle(shInfo.hIcon).Clone();
        }
    }
    catch (Exception)
    {
        fi.closed = null;
    } finally
    {
        if(shInfo.hIcon != IntPtr.Zero)
        {
            DestroyIcon(shInfo.hIcon);
        }
    }

    try {
        ptr = SHGetFileInfo(path, 0x00000010, ref shInfo, (uint)Marshal.SizeOf(shInfo), flags_open);
        if (ptr != IntPtr.Zero) {
            fi.open = (Icon)Icon.FromHandle(shInfo.hIcon).Clone();
        }
    }
    catch (Exception)
    {
        fi.closed = null;
    } finally
    {
        if(shInfo.hIcon != IntPtr.Zero)
        {
            DestroyIcon(shInfo.hIcon);
        }
    }

    return fi;
}

I have since rewritten my method using Dictionary ( OnBeforeExpand async/await using Dictionary ), but it still lags, and worse, it skips icons. The behavior is slightly different, the Tree expands first, then lags, where before it would lag, then expand the tree.

Kraang Prime
  • 9,981
  • 10
  • 58
  • 124
  • This sounds like something that would be saved somewhere in the registry. – Hanlet Escaño Aug 09 '16 at 19:16
  • @HanletEscaño - I stated that here - I believe there are other problems in my code causing delay as well. – Kraang Prime Aug 09 '16 at 19:26
  • Gotcha, I didn't notice that. Well, are you sure the icons are preserved? I am deleting the desktop.ini files and I also lose the custom icons. I am using Windows 7. – Hanlet Escaño Aug 09 '16 at 19:37
  • Is using this system function slower? http://stackoverflow.com/a/23666202/752527 – Hanlet Escaño Aug 09 '16 at 19:49
  • The numeral in the INI is just the index into the specified DLL for the Icon, There is a PInvoke to get the folder based on the path name which would basically bundle 2 steps of your code together. – Ňɏssa Pøngjǣrdenlarp Aug 09 '16 at 19:59
  • @HanletEscaño - I am using `SHGetFileInfo` already. `ShellFileGetInfo.GetFolderIcon(curPath, false)` is a wrapper for it to return the open and closed folder icons for the specified path. I can paste the code for that method if you wish to see it. – Kraang Prime Aug 09 '16 at 20:00
  • @Plutonix - I am using the Pinvoke. Calling that for every folder is even worse and since there is no way from the Pinvoke to check if the icon has already been stored in the image list (aka, for a different folder) the result was almost a minute to go through even a list of 10 folders for their icons - hence the check for existence of desktop ini, and further if it contains info pertaining to an icon (eg, `c:\windows\assembly\Desktop.ini` does not, so that would result in not loading `SHGetFileInfo` icons for `c:\windows\assembly`) - pasing the SHGetFileInfo wrapper now, as it seems required. – Kraang Prime Aug 09 '16 at 20:04
  • @Plutonix - continued : Also, when i didn't do a check for the `desktop.ini `and it's contents first, it resulted in many visually identical images being added to the list as there are quite a number of folders that have a `desktop.ini` file which are not icon related (normal folder image). – Kraang Prime Aug 09 '16 at 20:09
  • @SamuelJackson the whole thing would be nice, maybe I can help with performance. – Hanlet Escaño Aug 09 '16 at 20:11
  • 1
    Just as an idea: Try to store all fi.closed and fi.open in a list / dictionary. It's likely that your invoke with beginupdate and endupdate is slow. When you have resolved all icons, Loop over the list with a single beginupdate/endupdate. – Thomas Voß Aug 09 '16 at 20:19
  • 1
    @HanletEscaño - here is all relevant info to reproduce. [ShellFileGetInfo](http://pastebin.com/gM6AziSh) , [Tree](http://pastebin.com/4Q8s4v9k) , and [NtfsUsnJournal](http://pastebin.com/nbMgTPVD) . Build, then add `Tree` from tools to a winform, on `Form_Load()` call `tree1.PopulateTree(@"c:\");` – Kraang Prime Aug 09 '16 at 20:21
  • @ThomasVoß - was thinking of that. Could you provide an example on how this could be done ideally. I was also considering using the `.AddRange()` but it's quite cute how `.AddRange()` requires `Image[]` where `.Add()` accepts `Icon`. This would require I add even more overhead converting each `Icon` to `Image` then casting. Also would make setting the `ImageIndex` and `SelectedImageIndex` more complex when adjusting for only the folders that had different icons. – Kraang Prime Aug 09 '16 at 20:27
  • I have narrowed down the delay to `ImageList.Images.AddRange(images.ToArray());` --- not sure what I can do about it though. – Kraang Prime Aug 09 '16 at 22:18
  • It seems that when adding an Image (or range of images) to ImageList, that it refreshes the entire TreeView icons, is there anyway to prevent this so that it only updates the nodes I specify have new icons ? – Kraang Prime Aug 09 '16 at 22:32

2 Answers2

1

Here the code I thought of in my comment. Please note that I wrote it on a tablet so I couldn't test it. Hope it points you in the right direction.

protected async override void OnBeforeExpand(TreeViewCancelEventArgs e)
{
    await Task.Run(() =>
    {
        if (!_expandedCache.Contains(e.Node.FullPath))
        {
            ShellFileGetInfo.FolderIcons fi;
            _expandedCache.Add(e.Node.FullPath);
            string curPath;
            List<Tuple<TreeNode,Icon,Icon>> nodesAndIcons = new List<Tuple<TreeNode,Icon,Icon>>();
            foreach (TreeNode n in e.Node.Nodes)
            {
                curPath = Path.Combine((string)Tag, n.FullPath.Replace('/', Path.DirectorySeparatorChar));
                if (File.Exists(Path.Combine(curPath, "desktop.ini")) == true)
                {
                    if (File.ReadAllText(Path.Combine(curPath, "desktop.ini")).Contains("IconFile"))
                    {
                        fi = ShellFileGetInfo.GetFolderIcon(curPath, false);
                        if (fi.closed != null || fi.open != null)
                        {
                            nodesAndIcons.Add(new Tuple<TreeNode,Icon,Icon>(n, fi.closed, fi.open));
                        }
                    }
                }
            }

            if (InvokeRequired)
            {
                Invoke((MethodInvoker)(() =>
                {
                    BeginUpdate();
                    foreach(var tuple in nodesAndIcons)
                    {
                        ImageList.Images.Add(tuple.Value2);
                        ImageList.Images.Add(tuple.Value3);
                        tuple.Value1.SelectedImageIndex = ImageList.Images.Count - 1;
                        tuple.Value1.ImageIndex = ImageList.Images.Count - 2;
                    }
                    EndUpdate();
                }));
             }
             else
             {
                BeginUpdate();
                foreach(var tuple in nodesAndIcons)
                {
                    ImageList.Images.Add(tuple.Value2);
                    ImageList.Images.Add(tuple.Value3);
                    tuple.Value1.SelectedImageIndex = ImageList.Images.Count - 1;
                    tuple.Value1.ImageIndex = ImageList.Images.Count - 2;
                }
                EndUpdate();
            }
        }
    }
}
Thomas Voß
  • 1,145
  • 8
  • 20
  • I have figured out a way to eliminate the lag. I was under the impression it was the file.io that was causing the issue, but it was the ImageList. I have since replaced the ImageList with `Dictionary` and use the USN id as the string. Performance is God-like now. Only two small nuissances as a result of this `OwnerDrawText`, is selecting an item in the tree has about at 100-150ms delay before switching the select (like it selects the row, but then delay for the edit select to move. Also, the Image size is a bit off. – Kraang Prime Aug 10 '16 at 04:55
  • I am up-voting you, as using a Dictionary is a good idea. I can't mark this as the solution though as it is still subject to the same latency issues of O(n) scaling. To speed this up, the ImageList object of the ListView needs to be completely ignored as it is garbage. Every image added has a factor of O(n*x) where x is the size of the TreeNodes. Drawing the images directly on each node using a Dictionary for lookup eliminates the delay. Will post my last dilemma on a separate thread to see if you can crack the last two minor issues :) – Kraang Prime Aug 10 '16 at 05:00
0

The problem is that using the ImageList object to hold the images, causes a factoring issue where the more images in the ImageList, the longer each new Image, Icon or Image[] takes to add. In addition to this, being bound to the TreeView, each time a new image is added to any of the bound ImageList collections, causes the TreeView data to refresh. Due to the large amount of data this treeview holds, this delay becomes rather irritating.

To resolve this, we must first eliminate the use of the ImageList. I was able to do this by creating a base Dictionary<string, Image> property, and setting DrawMode = TreeViewDrawMode.OwnerDrawText;. Detailed instructions on the changes below :

Declare new Properties

// to be used to hold the Image collection instead of ImageList, LargeImageList, etc
private Dictionary<string, Image> _images;

// to be used for the default node image if none found in _images
public Image FolderImage { get; }

// TODO: to be used for the default open node image if none found in _images
public Image FolderImageOpen { get; }

Tell the TreeView you wish to control some drawing

Add the following to the constructor

_images = new Dictionary<string, Image>();
DrawMode = TreeViewDrawMode.OwnerDrawText;

using my method for retrieving icons, I set the default folder icons here

ShellFileGetInfo.FolderIcons fi = ShellFileGetInfo.GetFolderIcon(Environment.GetFolderPath(Environment.SpecialFolder.Windows), false);
FolderImage = fi.closed.ToBitmap();
FolderImageOpen = fi.open.ToBitmap();

Modify the OnBeforeExpand event

protected async override void OnBeforeExpand(TreeViewCancelEventArgs e)
{
    if (!_expandedCache.Contains(e.Node.FullPath))
    {
        TreeNodeCollection tnc = e.Node.Nodes;
        _expandedCache.Add(e.Node.FullPath);
        await Task.Run(() => { 
            Dictionary<string, ShellFileGetInfo.FolderIcons> icons = new Dictionary<string, ShellFileGetInfo.FolderIcons>();
            ShellFileGetInfo.FolderIcons fi;
            string curPath;
            foreach (TreeNode n in tnc)
            {
                curPath = Path.Combine((string)Tag, n.FullPath.Replace('/', Path.DirectorySeparatorChar));
                if (File.Exists(Path.Combine(curPath, "desktop.ini")) == true)
                {
                    if (File.ReadAllText(Path.Combine(curPath, "desktop.ini")).Contains("IconResource"))
                    {
                        fi = ShellFileGetInfo.GetFolderIcon(curPath, false);
                        if (fi.closed != null || fi.open != null)
                        {
                            _images.Add(((NtfsUsnJournal.UsnEntry)n.Tag).FileReferenceNumber.ToString(), fi.closed.ToBitmap());
                        }
                    }
                }
            }
        });
    }
    base.OnBeforeExpand(e);
}

This was changed to no longer need to Invoke or call BeginUpdate/EndUpdate as it doesn't touch anything on the UI thread

Draw the Image on the Node

protected override void OnDrawNode(DrawTreeNodeEventArgs e)
{
    // get image from dictionary if found for Usn reference number
    var _image = _images.FirstOrDefault(t => t.Key == ((NtfsUsnJournal.UsnEntry)e.Node.Tag).FileReferenceNumber.ToString());

    if (_image.Key == null)
    {
        // draw default folder image
        e.Graphics.DrawImage(FolderImage, e.Node.Bounds.X - 15, e.Node.Bounds.Y + 4);
    } else
    {
        // draw image that was found in the collection
        e.Graphics.DrawImage(_image.Value, e.Node.Bounds.X - 15, e.Node.Bounds.Y + 4);
    }

    // Draw the rest of the node normally
    e.DrawDefault = true;
    base.OnDrawNode(e);
}

Given the nature of how many complaints there are regarding TreeView's limitations, this solution is a viable way around those limits to allow for virtually unlimited nodes and images (or at least a much larger upper-bound restriction) without the ridiculous lag that the standard TreeView imposes.

Since the community (on SO, etc) have helped contribute. I will finalize these classes (thus far) and pastebin them so that the community gets back a TreeView control which is more usable than the standard one.

Kraang Prime
  • 9,981
  • 10
  • 58
  • 124