[1] 1. Introduction
Note: A more step-by-step guide is to be found farther below, see point 5.!
Recently, [Umlüx] has reminded me of his idea to be able to (visually) tag folders on Windows for specific purposes. For convenience, it’s supposed to work by right-clicking a folder in Windows Explorer, opening a submenu from the context menu, and then by picking the proper tag. After that, the folder icon should change, indicating that something’s special about this one. Windows can actually do the “tagging” part by itself using desktop.ini files, but manually writing them is a pain of course, hence the right-click idea.
The thing is, there already are tools to address folder tagging on Windows, but they’re often not feature complete, have limited XP compatibility or they lack very important features like timestamp preservation. Others have that last part, but only in paid versions of their software. So, time to do it by ourselves!
I picked his Powershell code up, and while it would run on my ancient XP x64 machines, implementing the required menu structure proved to be impossible, especially the cascading part. Ah, let me just show you the final product first, so you know what it was that I wanted (in my case, it’s meant for tagging media folders):
2. Creating the menu structure (XP + Vista compatible)
Starting with Windows 7, Microsoft introduced a new way to create submenus in the context menu, including icons, all in the Windows Registry. Older versions of Windows like Vista or XP however can’t do that, and I wanted a solution that works on all of them, from XP to Win10. So how can programs like 7-zip create such Explorer submenus with icons on legacy systems? They do so by injecting a COM .dll into the graphical shell, extending its capabilities. Typically, those are written in C++, and that’s not something I want to or can even do.
Luckily, we don’t have to develop that by ourselves, as somebody has already done it: Meet the [KuShellExtension]!
KuShellExtension – or KuShellExt in short – is a set of two library files, KuShellExtension.dll as well as KuShellExtension64.dll, the latter being meant for 64-bit Vista machines. It’s compiled for NT 5.2 though, so it’ll also work on XP x64 and Server 2003 x64. On top of that, it still works even on Windows 10! With the libraries comes an XML configuration file and some simple installer/uninstaller shell scripts.
In the example we’ve seen in the screenshot above, the respective configuration in config.xml would look like this:
expand/collapse source code<?xml version="1.0" encoding="utf-8"?><config version="1"><!-- Gobal variables --><var name="LEGACY_STYLE">false</var>
<var name="HIDE_MISSING">false</var>
<var name="ICON_DIR">${var:APPDATA}\changeicon\icons\</var>
<var name="INSTALL_DIR">${var:APPDATA}\changeicon\bin\</var>
<!-- Menu --><menu name="Tag Folder" class="folder" icon="${var:ICON_DIR}MainIcon.ico"><!-- Submenus --><menuitem name="Tag as "Currently watching"" class="folder" icon="${var:ICON_DIR}CurrentlyWatching.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "CurrentlyWatching"
</menuitem><menuitem name="Tag as "Ongoing release"" class="folder" icon="${var:ICON_DIR}WorkInProgress.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "WorkInProgress"
</menuitem><menuitem name="Tag as "Freshly completed"" class="folder" icon="${var:ICON_DIR}NewlyCompleted.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "NewlyCompleted"
</menuitem><menuitem name="----"></menuitem> <!-- Separator -->
<menuitem name="Tag as "Favorite"" class="folder" icon="${var:ICON_DIR}Favorite.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "Favorite"
</menuitem><menuitem name="Tag as "Top series"" class="folder" icon="${var:ICON_DIR}Star.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "Star"
</menuitem><menuitem name="Tag as "Keep an Eye on for later (High priority)"" class="folder" icon="${var:ICON_DIR}KeepAnEyeOnHighPrio.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "KeepAnEyeOnHighPrio"
</menuitem><menuitem name="Tag as "Keep an Eye on for later"" class="folder" icon="${var:ICON_DIR}KeepAnEyeOn.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "KeepAnEyeOn"
</menuitem><menuitem name="Tag as "Keep an Eye on for later (Low priority)"" class="folder" icon="${var:ICON_DIR}KeepAnEyeOnLowPrio.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "KeepAnEyeOnLowPrio"
</menuitem><menuitem name="Tag as "Not interested"" class="folder" icon="${var:ICON_DIR}NotInterested.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "NotInterested"
</menuitem><menuitem name="----"></menuitem> <!-- Separator -->
<menuitem name="Tag as "Fluff"" class="folder" icon="${var:ICON_DIR}SweetFluff.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "SweetFluff"
</menuitem><menuitem name="Tag as "Sweet, sweet Yuri!"" class="folder" icon="${var:ICON_DIR}Yuri.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "Yuri"
</menuitem><menuitem name="----"></menuitem> <!-- Separator -->
<menuitem name="Tag as "A/V main folder"" class="folder" icon="${var:ICON_DIR}AVMain.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "AVMain"
</menuitem><menuitem name="----"></menuitem><menuitem name="Remove tag" class="folder" icon="X:\icons\Trash.ico" action="execute" console="false" multiple="N" workdir="C:\">wscript "//B" "//Nologo" "${var:INSTALL_DIR}changeicon.vbs" "%1" "del"
</menuitem></menu></config>
As you can see, it defines the main menu with its icon and then several submenu entries with their own icons. Also, it’s not calling my modified version of Umlüx’ Powershell script, but a Visual Basic Script instead. The reason for this shall be explained in point 4., for now we only care about the menu structure.
Looking at the variables defined on top of the XML data, ICON_DIR and INSTALL_DIR, they reference an icon folder and a program folder, both in %APPDATA%\changeicon\, so I put that into the current users’ profile folder. The icons to be used have to be put into the ICON_DIR, the scripts and an unlocker program are to be put into INSTALL_DIR. That doesn’t affect KuShellExt itself though, you can install that anywhere.
Note that while KuShellExt is loaded, you can edit its configuration on the fly. The library will detect changes made to it, and reload the updated configuration automatically, so you don’t have to unload and load the .dll when making changes to the menus or icon names.
Now, we still need the scripts and [Unlocker 1.9.2] (this is just the .exe, without the adware that usually comes with version 1.9.2). As to why Unlocker is required, well, let’s talk about that and also give you the first script:
3. The timestamp issue and the Powershell script that does the tagging
Note: Windows XP or Vista users may have to install the Windows Management Framework Core package (including Powershell 2.0) first.
This whole solution will alter folder appearances by placing hidden desktop.ini files into them. Writing such a file will alter the folders’ modification time though, or in short it’s “mtime”. And that’s bad if you have software that relies on that piece of meta data. One typical example would be backup software, that decides whether files have to be backupped based on the mtime.
In my own case, the backup problem applies to my rsync backup system, but there’s even more; I’ve written myself a Perl script that walks through my entire video folder, generating an HTML report out of the data, sorted by “latest first”. That way, I can see what movies or series have been added or modified recently. That script also depends on the mtime, so having it change when tagging folders is not acceptable!
Time to look at Umlüx’ script, or rather my modified version of it, changeicon.ps1:
changeicon.ps1, expand/collapse source code# Change Icon v0.815 Karl Veratschnig 2017# Modified by Michael Lackner## Licensed under the GNU General Public license version 3.0 with express# permission by Karl Veratschnig.## usage:# .\changeicon.ps1 *dir_path* *icon*# i.e. ./changeicon.ps1 c:\testfolder Favorite## run from anywhere:# Powershell.exe set-executionpolicy remotesigned -File "path to ps1"## use "del" icon to revert folder to normal#### Update-ExplorerIcon refreshes icons in Windows Explorer by rebuilding its icon cache# Written by Idera,# http://community.idera.com/powershell/powertips/b/tips/posts/refreshing-icon-cache###function Update-ExplorerIcon {
[CmdletBinding()]
param()
$code = @'
private static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff);private const int WM_SETTINGCHANGE = 0x1a;private const int SMTO_ABORTIFHUNG = 0x0002;[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]static extern bool SendNotifyMessage(IntPtr hWnd, uint Msg, UIntPtr wParam,IntPtr lParam);[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]private static extern IntPtr SendMessageTimeout ( IntPtr hWnd, int Msg, IntPtr wParam, string lParam, uint fuFlags, uint uTimeout, IntPtr lpdwResult );[System.Runtime.InteropServices.DllImport("Shell32.dll")]private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2);public static void Refresh() {SHChangeNotify(0x8000000, 0x1000, IntPtr.Zero, IntPtr.Zero);SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, null, SMTO_ABORTIFHUNG, 100, IntPtr.Zero);}'@
Add-Type -MemberDefinition $code -Namespace MyWinAPI -Name Explorer
[MyWinAPI.Explorer]::Refresh()
}#### User-configurable block###$iconpath = "$env:APPDATA\icons\" # Where icon files reside
$installpath = "$env:APPDATA\bin\" # Where scripts & binaries reside
$unlocker = "unlocker.exe" # Name of the handle unlocker to use
$ulparams = "/S" # Unlock parameter to supply to the unlocker
#### Non-user-configurable block / main program###$folder = Get-Item $args[0] # Get folder from arguments
& $installpath$unlocker "$folder" $ulparams # Remove open handles from folder
# WARNING: If files inside *are* open in# other programs, their behavior might# become unstable. Data loss is possible!$icon = $args[1] # Get icon name from arguments
$olddate = $folder.LastWriteTime # Get modification time stamp (mtime)
$folder.attributes="Normal" # Reset folder attributes
if (Test-Path "$folder\desktop.ini") { # Check for existing desktop.ini
& $installpath$unlocker "$folder\desktop.ini" $ulparams # Unlock it, just to make sure
Remove-Item "$folder\desktop.ini" -Force # Delete it if present
}if($icon -ne "del") { # If op is to tag, not to delete...
$stream = [System.IO.StreamWriter] "$folder\desktop.ini" #... build new desktop.ini
$stream.WriteLine("[.ShellClassInfo]") # Category
$stream.WriteLine("IconFile=$iconpath\$icon.ico") # Icon file
$stream.WriteLine("IconIndex=0") # Icon #0 in file
$stream.WriteLine("IconResource=$iconpath\$icon.ico,0") # Icon #0 in file (for modern OS)
$stream.WriteLine("Infotip=$icon") # Info tip set to icon name
$stream.WriteLine("ConfirmFileOp=0") # Disable special folder handling
$stream.WriteLine("TimeStamp=$olddate") # Remark time stamp in the file for
# safety/recovery purposes$stream.close() # Close file
$folder.attributes="ReadOnly" # Set folder to RO to enable special
# desktop.ini handling(Get-Item "$folder\desktop.ini").attributes = "Hidden,System" # Set desktop.ini
# attributes to Hidden+System, also# to enable special folder handling} else { # If op is to delete, not to tag...
if (Test-Path "$folder\desktop.ini") { # Check whether desktop.ini really exists
& $installpath$unlocker "$folder\desktop.ini" $ulparams # Unlock it, just to make sure
Remove-Item "$folder\desktop.ini" -Force # Delete it if present
}}Update-ExplorerIcon # Rebuild Windows Explorers' icon cache
Start-Sleep -s 2 # This is to work around a race condition against writes/flushes on
# some systems, so we give the system a little bit of time for its# "beauty sleep" before resetting the mtime.$folder.LastWriteTime = Get-Date $olddate # Reset original mtime stamp
As you can see, the mtime is first being read from the folder by using $folder.LastWriteTime, and then reset to that value after writing the desktop.ini to the target directory. Also, it calls unlocker.exe /S on that folder to unlock it first, no matter whether there are open files within that folder or anything. This is done to avoid the dangerous situation, where the code would write the desktop.ini, but would then fail to update the folders’ timestamp due to open handles on the directory. Often this would also be caused by Windows Explorer itself, especially if you have open subfolders when tagging.
Forcefully unlocking the folder first deals with that problem. Please keep in mind that doing this may make programs relying on their open handles to behave in an undefined way however. Loss of data in that folder could be possible, e.g. if a text editor loses its handle for writing to a file in that folder.
In case something still goes wrong, the mtime is also being remarked in the desktop.ini file, in the value TimeStamp.
Here’s a sample .ini, as generated by changeicon, the format of the timestamp depends on your locale:
[.ShellClassInfo]IconFile=C:\Documents and Settings\USERNAME\Application Data\changeicon\icons\WorkInProgress.ico
IconIndex=0
IconResource=C:\Documents and Settings\USERNAME\Application Data\changeicon\icons\WorkInProgress.ico,0
Infotip=WorkInProgress
ConfirmFileOp=0
TimeStamp=09/22/2017 08:59:08
Now, there is one cosmetic issue left here…
4. Hiding that ugly terminal window
You can try to hide the window that pops up when calling a Powershell script, but it never really works reliably. So, are we going to tolerate that thing popping up every time we tag a folder? As if! That’s where the VBScript code comes in. This is an old trick I’ve used to run cmd batch scripts in a hidden way before, and this also works for calling Powershell scripts. I call this changeicon.vbs:
changeicon.vbs, expand/collapse source code' Declare variables and create shell object:Dim AppData, WinDir, PowerShell, arg0, arg1Set WshShell = Wscript.CreateObject("WScript.Shell")
'''' User-configurable part (file/folder locations)'''AppData = wshShell.ExpandEnvironmentStrings("%APPDATA%")
WinDir = wshShell.ExpandEnvironmentStrings("%WINDIR%")
PowerShell = WinDir & "\system32\WindowsPowerShell\v1.0\powershell.exe"
'''' Non-user-configurable part'''' Pull command line arguments into variables:arg0 = WScript.Arguments.Item(0)
arg1 = WScript.Arguments.Item(1)
' Execute program:WshShell.Run """" & PowerShell & """ """ & "-File" & """ """ & AppData & "\changeicon\bin\changeicon.ps1" & """ """ & arg0 & """ """ & arg1 & """", 0
Set WshShell = Nothing
5. Actually running that Frankenstein solution
In essence, you need to do the following:
- Pick a folder for changeicon.ps1, changeicon.vbs and unlocker.exe, and put those three files into it.
- Pick a folder for your icons, and place all your desired icons into it, no subfolders!
- Edit changeicon.ps1 and changeicon.vbs and change the install/icon paths.
- Install KuShellExtension and run its DLL hook script
install.cmd. - Edit KuShellExts’ settings.xml to reflect your menu structure and the corresponding menu icons and commands to execute. Better don’t delete all the comments in that file, the documentation can be pretty helpful at times.
Unfortunately I can’t share the icons I’ve created because they’re based on Microsofts’ icons, but you can easily find icons online or make your own with Microangelo or IcoFX. Both are commercial software for Windows, but you could also use the Gimp for that.
6. Enjoy tagging folders
Folders tagged with changeicon
With that, it’s much, much easier to keep track of things and to not forget what kind of stuff (*cough* tons of Anime *cough*) I still need to watch or keep an eye on for later.
But it’s not limited to that; You could use tagged folders for pretty much anything, like designating them to specific purposes or use them for document or work classification, whatever.
It’s interesting that even Windows 10 still can’t do that via the GUI by default by now…
Anyway, thanks fly out to [Umlüx] for writing the most important part at the core of this mess, the Powershell script, and also to [Idera] for the icon refreshing code I grabbed from their site! Also, if you want Umlüx’ modern solution for Windows 7+, which is based on pure Powershell code and Registry entries, you’d need to contact him directly. You may wish to do so, if you don’t need XP or Vista, because then you wouldn’t need to rely on the KuShellExtension anymore.
Happy tagging!
[1] Logo based on the Windows 10 Custom Folder Icons Pack made by Terraromaster
Tag folders by right clicking with a cascaded context menu on Windows (XP, too) © 2017 by The GAT at XIN.at is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
