Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preload folder icon for a specific folder in Windows Icon cache, in C# or VB.NET

I need to mention a 3rd party program, or better said the source-code of WinThumbsPreloader program, which contains all the necessary code written in C# to preload the thumbnail of a file in Windows thumbnails cache as you can see in this demo:

enter image description here

The problem is that what I need to preload is the icon of a folder, but the IThumbnailCache::GetThumbnail method does not allow to pass a folder item, and it will return error code 0x8004B200 (WTS_E_FAILEDEXTRACTION):

The Shell item does not support thumbnail extraction. For example, .exe or .lnk items.

In other words, I need to do the same thing WinThumbsPreloader program does, but for folder icons instead of folder/icon thumbnails.

So, what I mean is that I have a folder with a desktop.ini file inside, which as you probably know it can serve to replace the default icon/thumbnail for the folder that stores that desktop.ini file. An example of desktop.ini file content:

[.ShellClassInfo]
IconResource=FolderPreview.ico,0

The reason why I need to preload folders is to avoid the icon cache generation every time that I navigate through a folder.

To clear doubts, I would like to avoid this slow folder icon generation:

before

And instead, get this improved speed:

after

My question is: In C# or VB.NET, how can I programmatically preload the icon of a specific folder in Windows icon cache?.

It seems that IThumbnailCache interface is not the way to go for this problem...


UPDATE 1

Following @Jimi's commentary suggestions, this is the approach I'm trying:

Public Shared Sub PreloadInIconCache(path As String, 
                                     Optional iconSize As Integer = 256)

    Dim iIdIShellItem As New Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")
    Dim shellItem As IShellItem = Nothing
    Dim shellItemIF As IShellItemImageFactory
    Dim iconFlags As IShellItemImageFactoryGetImageFlags = 
                     IShellItemImageFactoryGetImageFlags.IconOnly

    SHCreateItemFromParsingName(path, IntPtr.Zero, iIdIShellItem, shellItem)
    shellItemIF = DirectCast(shellItem, IShellItemImageFactory)
    shellItemIF.GetImage(New NativeSize(iconSize, iconSize), iconFlags, Nothing)

    Marshal.ReleaseComObject(shellItemIF)
    Marshal.ReleaseComObject(shellItem)
    shellItemIF = Nothing
    shellItem = Nothing

End Sub

It caches the icons. If I call this method for lets say a directory that contains 1000 subfolders with custom icons, then the size of iconcache_256.db increases like around 250 MB, so its a clear evidence that icons are being cached, however I'm doing something wrong because those cached icons are not used by the operating system. I mean, if after I call that method and then I manually use Explorer.exe to navigate to that directory, the operating system starts again extracting and caching the icons for all the 1000 subfolders creating NEW icon references, so iconcache_256.db doubles its file size to around 500 MB, and this is a evidence that iconcache_256.db contains both the icons I cached using the method above, and the icons cached by the operating system itself, so the icon cache references that I generate calling the method above differ in some way from the icon cache references generated by the operating system itself, and that is what I'm doing wrong...

What I'm doing wrong?.

UPDATE 2

Using WindowsAPICodePack v1.1 library (from Nuget manager) I'm having the same problem as described in the code of the UPDATE 1 using IShellItemImageFactory.GetImage: I can extract the icons and the icons are cached in iconcache_256.db file (and other *.db files too of less con cache size), but If I navigate to the directory through Explorer.exe then the operating system starts extracting and caching again the icon for the same folders I already cached...

Full code sample:

Imports Microsoft.WindowsAPICodePack.Shell
...

Dim directoryPath As String = "C:\Directory"
Dim searchPattern As String = "*"
Dim searchOption As SearchOption = SearchOption.AllDirectories

For Each dir As DirectoryInfo In New DirectoryInfo(directoryPath).EnumerateDirectories(searchPattern, searchOption)

    Console.WriteLine($"Extracting icon for directory: '{dir.FullName}'")

    Using folder As ShellFolder = DirectCast(ShellFolder.FromParsingName(dir.FullName), ShellFolder)
        folder.Thumbnail.FormatOption = ShellThumbnailFormatOption.IconOnly
        folder.Thumbnail.RetrievalOption = ShellThumbnailRetrievalOption.Default

        Using ico As Bitmap = folder.Thumbnail.Bitmap ' Or: folder.Thumbnail.Icon
            ' PictureBox1.Image = ico
            ' PictureBox1.Update()
            ' Thread.Sleep(50)
        End Using

    End Using

Next dir

I'm not sure why the operating system insists to extract new icons and cache them when I already cached them, this multiplies x2 the cached icon references in iconcache_256.db (and in the other *.db files), because if I iterate 1000 subfolders from a directory to extract and cache their icons, if after I do that I navigate to that folder through Explorer.exe then the O.S. will extract and cache again those 1.000 subfolder icons creating NEW entries in iconcache_256.db (and in the other *.db files).

I don't know how to read the database format of iconcache_256.db file, so I don't know the structure format, but if the structure takes a directory path as one of its fields then I suspect the code approaches I used maybe forces to add a directory path field that differs from what the operating system adds in the icon cache field when I navigate to the folder to cache icons through Explorer.exe, and maybe for that reason the icon cache references are multiplied x2... just I'm speculating...

UPDATE 3

I thought that maybe the real problem could be that the references added in icon cache *.db files maybe are per-process and for that reason when I navigate to the directory with Explorer.exe it starts extracting and caching again the icons that I already extracted and cached when debugging my executable in Visual Studio...

So I did a test running the same code to extract/cache folder icons from two different processes just by building the same project to two executable with different names and different assembly guids. I discovered that the icon cache references in *.db files are NOT multiplied/duplicated when I run the second executable, so the per-process idea is discarded.

I really don't know what more can I try... and what is differing in the "duplicated" references inside the *.db icon cache files.

like image 335
ElektroStudios Avatar asked May 24 '20 14:05

ElektroStudios


People also ask

How do I change the icon for multiple folders?

Right-click onto the folder you want to change and then choose "Properties" In the properties window, select the "Customize" tab. Then click the "Change Icon" button. In the opened window you can browse for the built-in icons or select your own in the ICO format.

How do I use custom folder icons?

Right click on the folder and select the "properties" option. Click on the "customize" tab. Scroll down to the folder icon section at the bottom and select "Change Icon." Choose a different pre-installed icon OR upload an icon of your choosing.


Video Answer


1 Answers

Let me summarize everything starting from the scratch, since perhaps with so much text and comments in the comments box the question and the bounty could had been very confusing for users:

The Objective

My main goal was and is to preload the folder icon from a bunch of specific folders. The icon is specified in the "desktop.ini" file inside each of these folders, this is an O.S feature on which the O.S visually represents the folder as an icon, it literally replaces the common yellow default folder icon with the file contents preview, for the icon of your choose that is specified in the "desktop.ini" file. It's a beautiful preview feature for example for folders containing videogames, music albums or movies, on which you can use the game, movie or music cover as the icon for that folder.

The code approach

Using some code to call the IShellItemImageFactory.GetImage() function, or same thing using WindowsAPICodePack library I was able to get the operating system to generate the icons and add their respective entries to the Iconcache_nnn.db file.

The problem

The problem I found was that the O.S ignored these icons, it did not wanted to use my cached icons to preview the folder icons, but instead the O.S created new entries with different hashes but being the same icon, in other words, the O.S re-caches the icons that I already cached.

The root of the problem

By trial and error I discovered that the process architecture (x86 / x64) on which my code was ran to call IShellItemImageFactory.GetImage() function, it matters a lot.

So having a 64-bit Windows I was always running my code in x86 mode, and this was causing, without me being aware of the problem, that my x86 process generated icon entries with a different hash than the entries that the O.S was creating in the Iconcache_nnn.db file, and this is the reason why icons were cached again by the O.S, because the O.S didn't recognized the hashes of my cached icons from my x86 process.

I really don't know why this happens in this way, but it is like that, so rescuing this comment from @Simon Mourier:

AFAIK, the icon/thumb hash for a path is computed using the size (doesn't exist for a folder), last modified date, and the file identifier

To that computation we need to add another factor: process architecture differentiation ?

The solution

The solution in my 64-Bit O.S is simply to make sure compile the process (that programmatically gets the folder icons) in x64 to match the same architecture of the O.S / Explorer.exe. In case of having a 32-Bit Windows there is not much to say about, run your x86 process.

And this is a simple code to preload the icon of a folder using WindowsAPICodePack library:

    ''' ----------------------------------------------------------------------------------------------------
    ''' <summary>
    ''' Preloads the icon of a directory into Windows Icon Cache system files (IconCache_xxx.db).
    ''' <para></para>
    ''' The folder must contain a "desktop.ini" file with the icon specified.
    ''' </summary>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <remarks>
    ''' Because the Windows Icon Cache system creates icon's entry hashes 
    ''' with some kind of process architecture differentiation, 
    ''' the user must make sure to call <see cref="PreloadInIconCache"/> function from
    ''' a process that matches the same architecture of the running operating system. 
    ''' <para></para>
    ''' In short, <see cref="PreloadInIconCache"/> function should be called 
    ''' from a x64 process if running under Windows x64, and  
    ''' from a x86 process if running under Windows x86.
    ''' <para></para>
    ''' Otherwise, if for example <see cref="PreloadInIconCache"/> function is called  
    ''' from a x86 process if running under Windows x64, it will happen that 
    ''' when accesing the specified folder via Explorer.exe, the operating system 
    ''' will ignore the icon cached using <see cref="PreloadInIconCache"/> function,
    ''' and it will cache again the folder, generating a new additional entry 
    ''' in the Icon Cache system.
    ''' <para></para>
    ''' Read more about this problem at: <see href="https://stackoverflow.com/a/66773765/1248295"/>
    ''' </remarks>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <example> This is a code example.
    ''' <code language="VB.NET">
    ''' Dim directoryPath As String = "C:\Movies\"
    ''' Dim searchPattern As String = "*"
    ''' Dim searchOption As SearchOption = SearchOption.TopDirectoryOnly
    ''' Dim iconSize As IconSizes = IconSizes._256x256
    ''' 
    ''' For Each dir As DirectoryInfo In New DirectoryInfo(directoryPath).EnumerateDirectories(searchPattern, searchOption)
    '''     Console.WriteLine($"Preloading icon ({CStr(iconSize)}x{CStr(iconSize)}) for directory: '{dir.FullName}'")
    '''     DirectoryUtil.PreloadInIconCache(dir.FullName, iconSize)
    ''' Next file
    ''' </code>
    ''' </example>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <param name="dirPath">
    ''' The directory path.
    ''' </param>
    ''' 
    ''' <param name="iconSize">
    ''' The requested icon size, in pixels. 
    ''' <para></para>
    ''' Default value is 256.
    ''' </param>
    ''' ----------------------------------------------------------------------------------------------------
    <DebuggerStepThrough>
    Public Shared Sub PreloadInIconCache(directory As DirectoryInfo, Optional iconSize As Integer = 256)
        PreloadInIconCache(directory.FullName, iconSize)
    End Sub

    <DebuggerStepThrough>
Public Shared Sub PreloadInIconCache(path As String, Optional iconSize As Integer = 256)
    Using folder As ShellFileSystemFolder = DirectCast(ShellObject.FromParsingName(path), ShellFileSystemFolder)
        folder.Thumbnail.FormatOption = ShellThumbnailFormatOption.IconOnly
        folder.Thumbnail.RetrievalOption = ShellThumbnailRetrievalOption.Default
        folder.Thumbnail.AllowBiggerSize = True
        folder.Thumbnail.CurrentSize = New System.Windows.Size(iconSize, iconSize)

        Using thumbnail As Bitmap = folder.Thumbnail.Bitmap
        End Using
    End Using
End Sub

Example usage:

Dim folders As DirectoryInfo() = New DirectoryInfo("C:\").GetDirectories("*", SearchOption.AllDirectories)
For Each folder As DirectoryInfo In folders
    PreloadFolderIcon(folder, 256)
Next folder

Remember, a .NET assembly containing that code must be run in x64 mode if operating under a 64-Bit Windows, otherwise that code will generate the icon entries in the Iconcache_nnn.db file but when accessing the folders via Explorer.exe the O.S will simply ignore those entries and instead re-cache again the folder icons generating new additional entries.

Lastly, don't forget that you can check and preview the icon entries in the Iconcache_nnn.db file using the tool thumbcacheviewer mentioned by @Max.

like image 88
ElektroStudios Avatar answered Sep 17 '22 15:09

ElektroStudios