For the latest version, visit the Classic Shell project on SourceForge
Introduction
| Classic Shell is a collection of features that were available in older versions of Windows but not anymore. It brings back the classic Start menu that Windows 7 doesn't support, adds a toolbar for Windows Explorer, replaces the copy UI in Vista and Windows 7 with the classic UI from Windows XP, and adds couple more smaller features. |
Classic Start Menu
Classic Start Menu is a clone of the original Start menu, which you can find in all versions of Windows from 95 to Vista. It has a variety of advanced features:
- Drag and drop to let you organize your applications.
- Options to show Favorites, expand Control Panel, etc.
- Shows recently used documents. The number of documents to display is customizable.
- Translated in 35 languages, including right-to-left support for Arabic and Hebrew.
- Does not disable the original Start menu in Windows. You can access it by Shift+Click on the Start button.
- Right-click on an item in the menu to delete, rename, sort, or perform other tasks.
- Available for 32 and 64-bit Operating Systems.
- Has support for skins, including additional third party skins.
- Fully customizable in both looks and functionality.
- Support for Microsoft’s Active Accessibility
- And last but not least – it's free!
If you have used the Start menu in older versions of Windows, you’ll feel right at home:
Classic Explorer
Classic Explorer is a plug-in for Windows Explorer that:
- Adds a toolbar to Explorer for some common operations (Go to parent folder, Cut, Copy, Paste, Delete, Properties, Email). More buttons can be added manually.
- Replaces the copy UI in Vista and Windows 7 with the more user-friendly “classic” version similar to Windows XP.
- Handles Alt+Enter in the folder panel of Windows Explorer, and shows the properties of the selected folder.
- Has options for customizing the folder panel to look more like the Windows XP version or to not fade the expand buttons.
- Can show the free disk space and the total file size in the status bar.
Toolbar for Windows Explorer
Windows Explorer in Vista doesn’t have a toolbar like the one in Windows XP. If you want to go to the parent folder, you have to use the breadcrumbs bar. If you want to copy or delete a file with the mouse, you have to right-click and look for the Delete command. The right-click menu gets bigger and bigger the more shell extensions you have installed, and finding the right command can take a while.
To solve the problem, the Classic Explorer plug-in adds a new toolbar:
Hold the Control key when clicking the Up button to open the parent folder in a new Explorer window.
Hold the Shift key when clicking the Delete button to permanently delete a file.
Additional Up Button
Some people have asked if I can make a small Up button and put it next to the Back/Forward buttons in the title bar of Explorer. If Up is the only button you need from the toolbar, this will save you screen space:
Right-click on the button to bring up the Classic Explorer settings.
New Copy UI
In Vista, when you copy files and there is a conflict, you are presented with this:
What’s wrong with it?
Well, for starters, it is half a screen full of text that you have to read. Also, it is not immediately clear what parts of it are clickable. You have to move the mouse around to discover the UI, like in a Lucas Arts adventure game. And finally, the keyboard usability is awful. To tell it “yes, I know what I’m doing, I want to overwrite all files”, you have to press Alt+D, up, up, up, Space! It is harder than performing the Akuma Kara Demon move in Street Fighter 3. There is a time and a place for that stuff, and copying files is not it.
The Classic Explorer plug-in brings back the simpler dialog box from Windows XP:
It is immediately clear what is clickable (clue – the buttons at the bottom), there is easy keyboard navigation (press Y for “Yes”, A to copy all files), and you can still see which file is newer and which is larger. And of course, just like in Windows XP, holding down Shift while clicking on the No button means "No to All" (or just press Shift+N).
If you click on More…, you will get the original dialog from Windows. From there, you will see all the details, and you’ll get an extra option to “Copy, but keep both files”.
Important note: Only the UI is replaced. The underlying system that does the actual copying is not affected.
Alt+Enter in the Folder Panel
Alt+Enter is a universal shortcut across Windows to bring up the properties of the selection. But in Vista and Windows 7, it doesn’t work in the left panel that shows the folders. It works fine on the right where the files are. This is broken compared to Windows XP where Alt+Enter works in both places.
To solve the problem, the Classic Explorer plug-in detects when you press Alt+Enter, and shows the properties for the currently selected folder.
Status Bar
In Windows 7, the status bar in Explorer doesn't show the free disk space and the size of the selected files. Classic Explorer fixes that:
When no files are selected, the total size of all the files in the folder is shown.
Alternative Start Menu Implementations
Before I decided to develop my own Start menu, I tried to look for alternatives. I couldn’t find something that is free, supports reordering of programs, shows the recent documents, etc. This article will not be complete without listing the “competition” with some (hopefully objective) pros and cons:
CSMenu is free, and provides the basic functionality – open the Programs menu, click on a program to run. It lacks advanced functions like keyboard navigation, drag/drop, recent documents, customizability, etc. Also, the localization is not quite right – for example, Help and Support, Calculator, etc., are not localized, and there is no support for right-to-left languages.
Update: Looks like development has stopped on this project and it has been abandoned by the authors.
Classic Windows Start Menu is also free, and only has basic functionality. No drag/drop, very few languages are supported, and doesn’t quite work correctly when the taskbar is not at the bottom.
Update: The latest beta supports drag/drop, and better handles the different positions of the taskbar. There is still room for improvement (drag/drop is a bit buggy, Unicode support is lacking), but looks like the project is active, and effort is being made to fix the existing problems.
Classic Start Menu, while not free (20 bucks), has a variety of advanced features. You can use drag/drop to rearrange your menus (I found it to be a bit buggy), and it has two skins to choose from (Aero and Classic). On the negative side, there is almost no keyboard navigation because there is a search box that steals all typed characters. There is some sort of shortcut system using the numeric keys, but I couldn’t get it to work reliably. There is no proper "Recent documents" menu. Also, the localization is a bit off, and right-to-left support is a bit lacking.
Vista Start Menu is another version done by the same guy as Classic Start Menu above. It tries to be much fancier, but the UI was way too busy for my taste. I gave it a quick look and found the keyboard shortcut system to work a bit more reliably. There is a free version, and a PRO version (20 bucks) that adds some more customization features. I would rate this one the highest of the bunch because of its features, if you are into this sort of UI.
Seven Classic Start is probably the worst of the bunch. It is the most expensive (25 bucks!) and only offers basic functionality. Even though it is advertised as “Complete with everything that makes the original Start menu beloved by so many users”, there is no drag and drop, expanding Control Panel, Recent documents, localization, or right-to-left support.
P.S. When you try to uninstall the trial, you get an offer to use it for free if you agree to try some other software as well.
If you are not a programmer, you can stop reading now. Just download the binaries and install them.
The rest of the article discusses how the different features are implemented.
I’ll try to keep it short and not repeat information that is readily available in other CodeProject articles or the MSDN.
How Classic Start Menu Works
So, how do we create a Start menu replacement? Let’s start from the beginning.
Implementing the Menu
Just like with the original Start menu, our implementation uses a vertical toolbar control. This gives us some advantages over a regular menu:
- the toolbar supports images directly without the need to fiddle with owner-drawn menus
- the toolbar can be placed in a pager control to make it scrollable if the items don’t fit on screen
- you can right-click on a toolbar but not on a menu
- the toolbar offers some drag/drop functionality that will come handy later
Of course, there are downsides. We need to simulate parts of the menu behavior ourselves. We need to take care of opening a sub-menu when the mouse hovers over an item, handle focus, activation, and Z-order issues, etc.
There are some problems with the toolbar control that I haven't solved yet:
- The toolbar is not compatible with glass. When it draws itself, it messes up the alpha channel. This makes it impossible to make a transparent menu.
- The toolbar doesn't handle the
WM_PRINTCLIENT
message correctly when used in right-to-left mode. Makes things like AnimateWindow
difficult. - Having multiple toolbars in one window to simulate a multi-column menu confuses screen readers like JAWS.
Because of these problems, I'm seriously considering creating my own control for the next version of Classic Shell.
Replacing the Standard Menu
The next problem we need to solve is how to show our menu instead of the built-in standard menu. There are two ways for the user to activate the menu – by pressing the Win key, and by clicking on the Start button (the orb). After doing some sniffing around with Spy++, you will notice a few things:
- The taskbar is a window of class
Shell_TrayWnd
and no name. - The Start button is a window in the same thread as the taskbar. The text of the button is localized, and depends on the current OS language.
- When you click on the Start button, it receives
WM_LBUTTONDOWN
just like any other window. - When you click on the area around the start button, the taskbar receives the
WM_NCLBUTTONDOWN
message. - There is also a window named Program Manager and class
Progman
. It is in the same process as the taskbar, but in another thread. - When you press the Win button, the
Progman
window receives a message WM_SYSCOMMAND
with wParam = SC_TASKLIST
.
So the task is simple – install WH_GETMESSAGE
hooks for the threads of the Start button and the Progman
window. Intercept the messages that activate the Start menu, and show our start menu instead.
HWND g_StartButton; HWND g_Taskbar; UINT g_StartMenuMsg;
void ToggleStartMenu(); STARTMENUAPI LRESULT CALLBACK HookProgMan( int code,
WPARAM wParam, LPARAM lParam )
{
if (code==HC_ACTION) {
MSG *msg=(MSG*)lParam;
if (msg->message==WM_SYSCOMMAND &&
(msg->wParam&0xFFF0)==SC_TASKLIST) {
PostMessage(g_StartButton,g_StartMenuMsg,0,0);
msg->message=WM_NULL;
}
}
return CallNextHookEx(NULL,code,wParam,lParam);
}
STARTMENUAPI LRESULT CALLBACK HookStartButton( int code,
WPARAM wParam, LPARAM lParam )
{
if (code==HC_ACTION && !g_bInMenu) {
MSG *msg=(MSG*)lParam;
if (msg->message==g_StartMenuMsg && msg->hwnd==g_StartButton) {
ToggleStartMenu();
msg->message=WM_NULL;
}
if (msg->message==WM_LBUTTONDOWN && msg->hwnd==g_StartButton) {
ToggleStartMenu();
msg->message=WM_NULL;
}
if (msg->message==WM_NCLBUTTONDOWN && msg->hwnd==g_Taskbar) {
ToggleStartMenu();
msg->message=WM_NULL;
}
}
return CallNextHookEx(NULL,code,wParam,lParam);
}
The hooks can be installed by a shell extension that is auto-loaded by Explorer or by an external EXE. I chose an external EXE for a few reasons. First, it is simpler since it doesn’t require registering a shell extension with all the hassles that come with it. Second, when the EXE is killed, it cleans up its hooks automatically and the Explorer is restored to its original state. And finally, when Explorer is restarted, the EXE gets a message TaskbarCreated
and it can re-install the hooks.
Of course, there are more details to consider. For example, when the mouse is over the start button, a tooltip pops up with the text “Start”. We don’t want this text to show up when our Start menu is opened. So while the menu is visible, temporarily disable the tooltip.
The next thing to keep in mind is drag and drop. When the user drags a program to add to the Start menu, he will hover over the Start button and expect the menu to open. One way to support this is to replace the IDropTarget
object associated with the Start button. We get the old drop target from the OleDropTargetInterface
property of the Start button window, set a new drop target, and when it’s time to unhook Explorer, we restore the original drop target:
CComPtr<IDropTarget> g_pOriginalTarget;
g_pOriginalTarget=(IDropTarget*)GetProp(
g_StartButton,L"OleDropTargetInterface");
if (g_pOriginalTarget)
RevokeDragDrop(g_StartButton);
CStartMenuTarget *pNewTarget=new CStartMenuTarget();
RegisterDragDrop(g_StartButton,pNewTarget);
pNewTarget->Release();
if (g_pOriginalTarget)
{
RevokeDragDrop(g_StartButton);
RegisterDragDrop(g_StartButton,g_pOriginalTarget);
g_pOriginalTarget=NULL;
}
Icons
The Start menu needs to display icons for all the shell items, as well as for command items like Run, Shutdown, etc.
For shell items, we can get the icons using the IExtractIcon
interface. First call IExtractIcon::GetIconLocation
, then pass the received location to IExtractIcon::Extract
. If Extract
returns S_FALSE
, you have to additionally call the ExtractIconEx
function.
int CIconManager::GetIcon( IShellFolder *pFolder,
PITEMID_CHILD item, bool bLarge )
{
CComPtr<IExtractIcon> pExtract;
HRESULT hr=pFolder->GetUIObjectOf(NULL,1,&item,
IID_IExtractIcon,NULL,(void**)&pExtract);
if (FAILED(hr))
return 0;
wchar_t location[_MAX_PATH];
int index=0;
UINT flags=0;
hr=pExtract->GetIconLocation(0,location,_countof(location),&index,&flags);
if (hr!=S_OK)> return 0;
HICON hIcon;
hr=pExtract->Extract(location,index,bLarge?&hIcon:NULL,bLarge?
NULL:&hIcon, MAKELONG(LARGE_ICON_SIZE,SMALL_ICON_SIZE));
if (hr==S_FALSE) {
if (ExtractIconEx(location,index,bLarge?&hIcon:NULL,bLarge?NULL:&hIcon,1)==1)
hr=S_OK;
}
index=0;
if (hr==S_OK) {
index=ImageList_AddIcon(bLarge?m_LargeIcons:m_SmallIcons,hIcon);
DestroyIcon(hIcon);
}
return index;
}
Extracting icons can be expensive because the containing exe or DLL needs to be loaded in memory first. The Control Panel is the worst offender because it contains a long list of items, each in its own file, and they are all needed at the same time.
For the command items, we can extract the icons from shell32.dll. It is already loaded in the Explorer process:
| Programs is # 326 |
| Settings is # 330 |
| Run is # 328 |
This is a bit hacky because the icon resources can certainly change between Windows versions. I have verified that the icons we need have the same resource IDs for Vista and Windows 7. We’ll see if the next version of Windows (or the next Service Pack) breaks that. The documented way to access the shell icons is with the SHGetStockIconInfo
function. Unfortunately, it doesn’t give us all the icons we need. Also, the interesting icons like SIID_STFIND
and SIID_STRUN
are marked as “Do not use” in the documentation. In the latest online docs, they are not even listed! So for now, looking at shell32.dll with a resource viewer seems to be the only workable solution (short of drawing our own icons).
It is possible for multiple items to share the same icon – for example, all text files use the same text file icon. To reuse icons, the Start menu has a global cache CIconManager
that associates each icon with a key value that is a hash of the icon’s location and index. To improve performance, the icon manager starts a background thread that crawls through the shell and pre-caches the icons. So, by the time you need to open the Control Panel, all icons should already be loaded.
The Programs Menu
The Programs menu is a combination of two folders – one for the current user and one shared by all users. The Start menu should combine the two folder trees and present them as one tree. Items with identical names should be combined into one item.
It is important to compare the internal names of the items as returned by GetDisplayNameOf(SHGDN_INFOLDER|SHGDN_FORPARSING)
but show the normal display name GetDisplayNameOf(SHGDN_INFOLDER|SHGDN_NORMAL)
in the UI. Some items can have the same display name, but should be treated as separate items - for example, foo.exe and foo.exe.lnk. The opposite is also true - some items can have the same internal name but different display names - for example, the display name of the user's Startup folder will be translated to the current language, but the common Startup folder will not be. Even though their display names differ, the two folders should be combined.
Why not use IShellMenu?
The shell has support for displaying a menu for a given shell folder. You create an instance of IShellMenu
, give it IShellFolder
, and it works quite great. The icons show up properly, drag and drop works, everything is rosy, everybody is happy. Not quite.
There are a few downsides for which I was unable to find a workaround even after a week of trying. First is that we want to show a combination of two shell folders – one for the user’s programs and one for the common programs. I created a virtual IShellFolder
that presented two folders as one. I had to use my own private PIDL
structure, and the IShellMenu
didn’t quite like that. It assumes the PIDLs it gets are compatible with the shell file system. Another problem is that the Start menu is much more than just the Programs menu. It has recent documents, system commands, separators, etc. Shoehorning that into a single IShellFolder
hierarchy turned out to be an impossible task.
So, I gave up on IShellMenu
and decided to implement the menu control from scratch. On the bright side, since we are implementing the menu ourselves, the possibilities for tweaking the look and feel are endless.
Recent Documents
Windows stores links to the recently accessed documents in the %APPDATA%\Microsoft\Windows\Recent folder. Not every link in the folder points to a document though. Some point to recently accessed folders. So, we need to filter those out. How do we distinguish between links to files and links to folders? Get an IShellLink
object, convert it to IPersistFile
, load the contents of the link with IPersistFile::Load
, get the target path and attributes with IShellLink::GetPath
, and check if it is a file or a folder.
Also, there can be quite a few items, and enumerating them using the IShellFolder
API can be very slow. In my case, it was taking about 5-8 seconds. I found it is much faster to go through them using the FindNextFile
API, sort them by time, and pick the first 15 documents.
System Commands
Besides the Programs menu, the Start menu contains many items that execute specific commands. We should try to implement as many as possible. Here’s the current list:
Command | Implementation |
Shutdown | IShellDispatch::ShutdownWindows() |
Log Off | ExitWindowsEx(EWX_LOGOFF,0) |
Undock | IShellDispatch::EjectPC() |
Run | IShellDispatch::FileRun() |
Help and Support | IShellDispatch::Help() |
Taskbar Properties | IShellDispatch::TrayProperties() |
Search for Files and Folders | IShellDispatch::FindFiles() (or execute the Command.SearchFile setting) |
Search for Printer | IShellDispatch2::FindPrinter(CComBSTR(L””), CComBSTR(L””), CComBSTR(L””)) |
Search for Computers | IShellDispatch::FindComputer() |
Search for People | This one is a bit trickier. Its implementation is part of Windows Mail. The command line is %ProgramFiles%\Windows Mail\wab.exe /find. |
Drag and Drop
This is one of the most useful features of the Start menu. It lets the user rearrange the installed programs and make them easier to find.
How is it done? When the user drags a menu item, the toolbar sends the TBN_DRAGOUT
notification. We have to create a data object and a drop source and run the SHDoDragDrop
function. We can get the data object from IShellFolder::GetUIObjectOf(IID_IDataObject)
. The drop source has nothing special about it.
To enable dropping items on the menu, we use the TBSTYLE_REGISTERDROP
style. This causes the toolbar to send the TBN_GETOBJECT
notification to request an IDropTarget
interface from us and use it during drag/drop. Our IDropTarget
must take care of things such as:
- Showing the insert mark where an item can be dropped (use the
TB_SETINSERTMARK
message). - Detect when the mouse hovers over a sub-menu and open it after some delay.
- Reorder the menu when an item is dragged and dropped into the same menu.
- Move/copy the item if it is dropped in a different menu. For that, we can get
IDropTarget
from the target folder, and manually call IDropTarget::DragOver
and IDropTarget::Drop
to get it to perform the operation.
Usually, the drop operation is asynchronous and happens in a background thread. This is not good for us because we want to know immediately when an item is moved so we can update the menu. The simplest solution is to disable asynchronous operations (that's what Windows' own Start menu does):
CComQIPtr<IAsyncOperation> pAsync=pDataObj;
if (pAsync)
pAsync->SetAsyncMode(FALSE);
This is not a big problem because the Start menu contains mostly shortcuts and they are fast to copy. I can think of two alternative solutions. Ideally, we can add an IAdviseSink
to the data object and be notified when the drag operation is complete. Unfortunately, the shell data objects don't support such notifications (IDataObject::DAdvise
returns OLE_E_ADVISENOTSUPPORTED
). The other way is to install a directory watcher with FindFirstChangeNotification
. This will work, but is way too complex for such little benefit.
Context Menu
The standard Start menu has another feature worth supporting. The user can right-click on an item, get its shell context menu, and click on commands. It is quite tricky to host the context menu correctly; fortunately, Raymond Chen has covered that in great detail on his blog: The Old New Thing: How to host an IContextMenu.
Of course, there is still work to do because we want some custom behavior. We want special handling for the “rename”, “delete”, and “link” commands. For “rename”, we are going to show our own renaming dialog box because the default implementation does nothing. For “delete” and “link”, we want to refresh the menu after the operation finishes.
Skins
Version 0.9.8 of Classic Shell supports skins for the Start menu. The skin determines the background image for the main menu and settings such as font size, color, glow, and more.
Every skin is a resource DLL that contains the skin description (a text file) and the bitmaps used to construct the menu image. Read more about skins here: How to Skin a Classic Start Menu.
Accessibility
Since we are using a toolbar instead of a menu, screen readers like Narrator say "toolbar with 11 buttons" instead of "menu with 11 items". To fix that, we have to create our own implementation of IAccessible
and implement get_accRole
to return ROLE_SYSTEM_MENUPOPUP
, ROLE_SYSTEM_MENUITEM
, and ROLE_SYSTEM_SEPARATOR
for the individual accessible pieces.
Check out the Accessibility.cpp file for the complete implementation.
How Classic Explorer Works
Classic Explorer is a single DLL that registers as three different shell extensions – a drag and drop handler, a browser helper object, and a desk band. The drag and drop handler is used for the copy UI, the browser helper object hooks into the folder tree view, and the desk band adds a toolbar to the Explorer.
Classic Copy
Classic Copy replaces the dialog box that Explorer shows when there is a conflict during a copy/move operation. It uses a drag and drop handler to ensure the DLL is loaded when a copy operation is in progress. Once loaded, the DLL installs WH_CBT
hooks into all threads of the process and waits for a dialog box with a specific title to be created.
When Explorer creates a dialog box with the title "Copy File" or "Move File", we hide it before it has a chance to be displayed and we show our own dialog box. After the user has picked the selection (Yes, No, Cancel, etc.), we have to communicate that selection to the original dialog box.
The original dialog is not an ordinary window but rather a task dialog box. This makes it very difficult to control programmatically because instead of regular buttons with control IDs, it uses windowless buttons that are painted directly on the dialog's surface. I have found the only way to control the task dialog is through the Active Accessibility API. We locate the IAccessible
interface for each item, find the important buttons by their label, and activate the right button using IAccessible::accDoDefaultAction
. It would have been so much easier if the buttons had another feature to distinguish them besides their label, but I couldn’t find any. The labels, of course, depend on the currently selected language. So, we can’t just look for a button called “Don’t Copy”. We have to find the text for the current language. The text is located in the string table of the shell32.dll.mui file. For example, “Don’t Copy” is string 13606, "Move" is 13610, etc.
So there you have it – hide the original dialog, show our own instead, and then use the Accessibility API to control the original dialog based on the user selection. It’s that simple.
Alt+Enter in the Folder View
This was easy. We subclass the tree view and listen for the WM_SYSKEYDOWN
message with wParam = VK_RETURN
. When the message comes, we find the selected item. The user data stored in the tree control item is the PIDL of the corresponding shell item. We need to go up the tree hierarchy and assemble a full PIDL. Finally, we call ShellExecuteEx
with the “properties” verb to display the properties:
HTREEITEM hItem=TreeView_GetSelection(hwndTree);
LPITEMIDLIST pidl=NULL;
while (hItem) {
TVITEMEX info={TVIF_PARAM,hItem};
TreeView_GetItem(hwndTree,&info);
LPITEMIDLIST **pidl1=(LPITEMIDLIST**)info.lParam;
if (!pidl1 || !*pidl1 || !**pidl1) {
if (pidl) ILFree(pidl);
pidl=NULL;
break;
}
LPITEMIDLIST pidl2=pidl?ILCombine(**pidl1,pidl):ILClone(**pidl1);
if (pidl) ILFree(pidl);
pidl=pidl2;
hItem=TreeView_GetParent(hwndTree,hItem);
}
if (pidl) {
SHELLEXECUTEINFO execute={sizeof(execute),
SEE_MASK_IDLIST|SEE_MASK_INVOKEIDLIST,NULL,L"properties"};
execute.lpIDList=pidl;
execute.nShow=SW_SHOWNORMAL;
ShellExecuteEx(&execute);
ILFree(pidl);
msg->message=WM_NULL;
}
Tweaking the Folder View Look and Feel
Since we are subclassing the folder view for the Alt+Enter feature, let's see if we can get it to look like the folder view in Windows XP:
To get the classic look, add the TVS_HASLINES
style, remove the TVS_SINGLEEXPAND
and TVS_TRACKSELECT
styles, and remove the TVS_EX_FADEINOUTEXPANDOS
and TVS_EX_AUTOHSCROLL
extended styles.
To get the simple look, add the TVS_SINGLEEXPAND
and TVS_TRACKSELECT
styles, remove the TVS_HASLINES
style, and remove the TVS_EX_FADEINOUTEXPANDOS
and TVS_EX_AUTOHSCROLL
extended styles.
Or, if you simply don't want the buttons to fade, just remove the TVS_EX_FADEINOUTEXPANDOS
extended style.
Adding the Toolbar
The Classic Explorer Bar is a simple desk band with a toolbar inside. You can find out how to create desk bands from this article: Internet Explorer Toolbar (Deskband) Tutorial. There are two tricky pieces that I needed to discover by myself.
First, I want the desk band to be hosted in Windows Explorer but not Internet Explorer. You do that by checking the exe name in DllMain
, and returning FALSE
if the exe is named “iexplore.exe”. Why can’t we just return TRUE
only for “explorer.exe”? Our DLL may need to be loaded by other executables like regsvr32.exe or msiexec.exe. So, it’s better to exclude iexplore.exe specifically, than to provide a specific list of allowed hosts.
extern "C" BOOL WINAPI DllMain( HINSTANCE hInstance,
DWORD dwReason, LPVOID lpReserved )
{
if (dwReason==DLL_PROCESS_ATTACH) {
wchar_t path[_MAX_PATH];
GetModuleFileName(NULL,path,_countof(path));
if (_wcsicmp(PathFindFileName(path),L"iexplore.exe")==0)
return FALSE;
}
return _AtlModule.DllMain(dwReason, lpReserved);
}
Second, there appears to be a bug in Windows 7. Each desk band is forced to be on its own row in Windows Explorer. The Explorer forces the RBBS_BREAK
style for every band. To combat that, the solution is to subclass the rebar control and force a specific value for the RBBS_BREAK
style. We remember the state when the band is hidden (IDockingWindow::ShowDW
is called with FALSE
) so we can restore it correctly the next time Explorer is opened or the band is shown. All this is specific to Windows 7. None of that hackery is needed for Vista because it restores the band states correctly.
Now that we have a toolbar, we have to write the code for each button. The Up button navigates to the parent folder:
pBrowser->BrowseObject(NULL,
(GetKeyState(VK_CONTROL)<0?SBSP_NEWBROWSER:SBSP_SAMEBROWSER)|
SBSP_DEFMODE|SBSP_PARENT);
The Back and Forward buttons work in a similar way.
The Cut, Copy, Paste, Delete, and other buttons simply send WM_COMMAND
messages to Explorer. You can discover the command codes with Spy++. The command code is different if you want to operate on a folder in the tree view or a file in the list view:
Operation | Folder tree command | File list command |
Cut | 41025 | 28696 |
Copy | 41026 | 28697 |
Paste | 41027 | 28698 |
Delete | 40995 | 28689 |
Copy To | 28702 | ----- |
Move To | 28703 | ----- |
Undo | 28699 | 28699 |
Redo | 28704 | 28704 |
Select All | 28705 | 28705 |
Invert Selection | 28706 | 28706 |
Refresh | 41504 | 41504 |
Properties | no command, see Alt+Enter | 28691 |
The Email button uses the SendMail
object as described here: SendTo mail recipient.
Adding Up a Button in the Title Bar
This turned out to be much easier than expected. The title bar of Explorer contains a rebar control. All we need to do is make a small toolbar with one button and add it as a rebar band. We need to replace the entire rendering of the toolbar for two reasons. First, for Aero modes, the background needs to be cleared to make it transparent. And second, we want our icon to take the whole space of the button, with no button border. That's done by handling the NM_CUSTOMDRAW
notification in the toolbar's parent.
The Status Bar
Windows 7 doesn't show the total size of the selected files in the status bar. Instead, you have to look at the Details pane, but it is limited to 15 files. If you select more than 15 files, you have to press "More details" to get the total size. The size disappears as soon as you select one more file. This is pretty annoying. Let's try to fix the status bar then.
First, we locate the status bar control using IShellBrowser::GetControlWindow(FCW_STATUS)
and subclass it. When Explorer wants to update the text "10 items selected", it sends the message SB_SETTEXT
with LOWORD(wParam)=0
. We catch that message and append the total disk size to the end, so it becomes "10 items selected (Disk free space: 358 GB)". Then, we calculate the total size of the selection and show it in the status bar's second part:
IShellBrowser *pBrowser=((CExplorerBHO*)uIdSubclass)->m_pBrowser;
__int64 size=-1;
CComPtr<IShellView> pView;
if (pBrowser && SUCCEEDED(pBrowser->QueryActiveShellView(&pView)))
{
CComQIPtr<IFolderView> pView2=pView;
CComPtr<IPersistFolder2> pFolder;
LPITEMIDLIST pidl;
if (pView2 && SUCCEEDED(pView2->GetFolder(IID_IPersistFolder2,(void**)&pFolder))
&& SUCCEEDED(pFolder->GetCurFolder(&pidl)))
{
CComQIPtr<IShellFolder2> pFolder2=pFolder;
UINT type=SVGIO_SELECTION;
int count;
if ((dwRefData&SPACE_TOTAL) &&
(FAILED(pView2->ItemCount(SVGIO_SELECTION,&count))
|| count==0))
type=SVGIO_ALLVIEW;
CComPtr<IEnumIDList> pEnum;
if (SUCCEEDED(pView2->Items(type,IID_IEnumIDList,(void**)&pEnum)) && pEnum)
{
PITEMID_CHILD child;
SHCOLUMNID column={PSGUID_STORAGE,PID_STG_SIZE};
while (pEnum->Next(1,&child,NULL)==S_OK)
{
CComVariant var;
if (SUCCEEDED(pFolder2->GetDetailsEx(child,&column,&var)) &&
var.vt==VT_UI8)
{
if (size<0)
size=var.ullVal;
else
size+=var.ullVal;
}
ILFree(child);
}
}
ILFree(pidl);
}
}
if (size>=0)
{
StrFormatByteSize64(size,buf,_countof(buf));
}
else
buf[0]=0;
DefSubclassProc(hWnd,SB_SETTEXT,1,(LPARAM)buf);
Of course, calculating the size every time the selection changes can be expensive. The selection can change frequently if you are selecting multiple files by holding down the Shift+Down arrow in a large folder. So, when the selection changes, we just start a timer and do the heavy calculation after 10 ms. If the selection changes in the mean time, it will restart the timer.
Note: Windows 7 has an annoying bug. When you open a new Explorer window, often the status bar has only one part. If you resize the window, the status bar resets to its correct 3-part state. To workaround the problem, the code resizes the window by one pixel, then back to the original size. Sometimes, even that is not enough. If the code detects that the status bar still has one part, it sends the Refresh command. This usually fixes the problem.
Scroll Problems in Explorer
Another bug in Windows 7 Explorer involves expanding a folder in the navigation pane. If you select a folder that hasn't been expanded before, then expand it, the navigation pane will scroll all the way up to the top, then all the way down to the selected item. The result is that the selected item ends up near the bottom (not near the top as you would expect). What happens under the covers is that Explorer sends two TVM_ENSUREVISIBLE
messages - first for the top item and second for the selected item. I have no clue what requires the first message to be sent. Maybe there is some weird situation where the scrolling to the top actually helps, but I haven't seen any.
To counter this bug, we can make the tree control ignore the first message:
if (uMsg==TVM_ENSUREVISIBLE)
{
HTREEITEM hItem=(HTREEITEM)lParam;
if (!TreeView_GetParent(hWnd,hItem) &&
!(TreeView_GetItemState(hWnd,hItem,TVIS_SELECTED)&TVIS_SELECTED))
return 0;
}
Basically, it ignores TVM_ENSUREVISIBLE
for the top level item if it is not selected. The assumption is that if Explorer really wants to focus on the top item, it will select it first.
Rant: First, there is the RBBS_BREAK
bug, then status bar problems, then this. What's happening here? Did Microsoft hire an intern to finish Explorer in Windows 7? Was QA sleeping on the job? Explorer is the most commonly used application for Windows. It deserves more attention than that! I remember when Windows XP came out, the only bug in Explorer was that it didn't correctly redraw the corner between the horizontal and vertical scroll bars. Ah, the good old days. Now, I'm crossing my fingers and waiting for Win 7 SP1 :)
Localization
Windows Vista and Windows 7 support 35 languages (if you have the Ultimate version, you can install more than one). We want to localize the UI to make it integrate seamlessly into the OS.
The first task is to localize the text. Classic Shell has two files that contain localization data: ExplorerL10N.ini has the text for Classic Explorer, and StartMenuL10N.ini has the text for the Start menu. Each language has its own section:
[ar-SA] - Arabic (Saudi Arabia)
Menu.Programs = البرا&مج
Menu.Favorites = المف&ضلة
[bg-BG] - Bulgarian (Bulgaria)
Menu.Programs = &Програми
Menu.Favorites = Пре&дпочитани
[el-GR] - Greek (Greece)
Menu.Programs = &Προγράμματα
Menu.Favorites = Αγαπ&ημένα
[en-US] - English (United States)
Menu.Programs = &Programs
Menu.Favorites = F&avorites
The name of the section ([en-US]
, [bg-BG]
, etc.) comes from the GetThreadPreferredUILanguages
function. It returns a list of language names in the order of preference. To get a localized string, call the FindTranslation
function:
const wchar_t *FindTranslation( const char *name, const wchar_t *def );
If a string is not found in the first preferred language, the next language is used. If none of the languages is recognized, the [default]
section is used. And, if the text is nowhere to be found, FindTranslation
returns the def
parameter.
Part of the localization is to support right-to-left languages like Arabic and Hebrew. We check if the language is RTL with this code:
bool IsLanguageRTL( void )
{
LOCALESIGNATURE localesig;
LANGID language=GetUserDefaultUILanguage();
if (GetLocaleInfoW(language,LOCALE_FONTSIGNATURE,(LPWSTR)&localesig,
(sizeof(localesig)/sizeof(wchar_t))) &&
(localesig.lsUsb[3]&0x08000000))
return true;
return false;
}
For RTL languages, there are a few things to be considered:
- The dialog boxes need to be mirrored. This is done by having a separate dialog resource with the RTL style.
- The Start menu needs to be mirrored. So, we set the RTL style for the menu container window. The toolbar is smart enough to reverse its orientation.
- Since the toolbars are reversed, all icons are mirrored. We don’t want that, so we need to set the
ILC_MIRROR
flag for the toolbar’s image list to mirror the icons back to normal. - Context menus also need to be mirrored. This is done by passing the
TPM_LAYOUTRTL
flag to TrackPopupMenu
. AnimateWindow
has some problems with RTL layout. It uses the WM_PRINT
and WM_PRINTCLIENT
messages, and they don’t play nice with RTL. Unfortunately, I haven’t found a fix, so for now, the menu animation is disabled for RTL languages
The Installer
The installer for Classic Shell is split into three projects. ClassicShellSetup32 builds a 32-bit MSI file, ClassicShellSetup64 builds a 64-bit MSI file. The 64-bit package contains both 32-bit and 64-bit versions of the ClassicExplorer.dll because you can run a 32-bit Explorer on 64-bit Windows.
ClassicShellSetup builds an exe that combines the two MSI files into one convenient package. Certainly, it is possible to distribute two separate MSI files, but the exe gives us some more advantages besides just being a convenient package:
- The EXE can check if we are running on older version of Windows and complain about it.
- The EXE can check if the OS is 32-bit or 64-bit. and will run the correct MSI.
- After installation, we want to launch the Start menu exe. It is not possible to do so from the MSI package because it runs in an elevated environment and the Start menu needs to run as the current user.
- The EXE can have a nice non-default icon.
Building the Solution
The included solution is for Visual Studio 2008. It needs to be built in two steps.
First, do a full build for the Setup|Win32
configuration. That will build the 32-bit modules and create the 32-bit MSI file. Second, do a full build for the Setup|x64
configuration. That will build the 64-bit modules, create the 64-bit MSI file, and build the final package ClassicShellSetup\Release\ClassicShellSetup.exe. Run the EXE to install.
Important note: The first time you build the setup projects, you may get some warning about adding dependencies for some system files like OLEACC.dll and UxTheme.dll. Make sure all Detected Dependencies are excluded, and build again. If you leave some dependencies like dwmapi.dll, they will be installed on the target machine. If it happens to be a different version of Windows (like 32-bit vs. 64-bit, or Vista vs. Windows 7), the Classic Shell will fail to start!
And Finally, Some Tips for Developers
Debugging the Start menu will be easier if you disable the #define HOOK_EXPLORER
line in ClassicStartMenu.cpp. Then, the menu will run in its own process, and will not interfere with the Explorer process. Some of the functionality will be disabled, but it is good enough for most purposes.
Building Classic Explorer for Release or Debug configuration will register it as a shell extension. The next time you open an Explorer window, it will be activated. You can attach a debugger to the explorer.exe process and debug the shell extension.
During development, it is inconvenient that the DLLs are loaded by Explorer and can't be rebuilt. So, we need a way to restart Explorer. You may restart Explorer by killing it in Task Manager and running explorer.exe again. But, an easier way to restart Explorer is to make it crash. In Release and Debug, if you hold down Shift and click on the Settings button, it will force a crash in Explorer.
Another workaround for restarting Explorer is available if you develop on 64-bit Vista. You can run the 32-bit Explorer and close it when you are done. Run C:\Windows\SysWOW64\Explorer.exe directly from Visual Studio. This doesn't work on Windows 7. The 32-bit Explorer launches the 64-bit version, and exits immediately.
Room for Improvement
Of course, there is always room for improvement. Here’s what I have planned for the future:
- Better accessibility for the Start menu. It is half-way there, but there is still work to do.
- The Start menu may track the recently used programs and provide an easier access to them.
- Better support for Glass in the Start menu, including behind the menu items and in the sub-menus.
- The Start menu should use the system sounds for opening a sub-menu or activating a menu item.
Signing off
I plan this to be the last update for the article. Its main purpose was to show how to do some tricky things with Explorer - replace the Start menu, add a toolbar, workaround bugs, that kind of stuff. The features I have planned for the future are mostly improvements of the Classic Shell internals. They do not involve interactions with Explorer, and will not make for an interesting article. Also, the article is getting pretty long. If any new feature is worth sharing, I think it's best to write a new article.
I will still be answering questions in the comments section, and will be posting updates when a new version comes out.
History
These are just the highlights of each version. For a complete list of changes, check out the history page: Classic Shell History.
- Version 1.0.1 general release (Feb, 2010)
- This is a bugfix-only release. Fixes a few rare crashes in the Start menu.
- Version 1.0.0 general release (Feb, 2010)
- Added Up button to the Explorer title bar.
- The installer supports command line options for logging or unattended install.
- Version 0.9.10 release candidate (Jan, 2010) - make your own toolbar
- The Explorer toolbar can be customized with new icons and additional buttons.
- Active Accessibility support.
- Version 0.9.9 release candidate (Jan, 2010) - make your own Start menu
- The Start menu can be customized with new icons and additional menu items.
- The skins in the Start menu can have variations.
- Added "Email" button to Explorer.
- Version 0.9.8 beta (Jan, 2010) - skins for the Start menu
- Added support for skins in the classic Start menu.
- Replaced the folder conflict dialog box with a simpler version (similar to the file conflict dialog box).
- Version 0.9.7 beta (Dec, 2009)
- Added free disk space and file size to the status bar in Windows 7 Explorer.
- Added dragging with the right mouse button for the Start menu.
- Version 0.9.6 beta (Dec, 2009)
- Added Properties button to the toolbar.
- Added settings for the look of the folder tree in Explorer - XP Classic, XP simple, etc.
- Added support for the Start menu group policies like "Hide Run", "Hide Help", "Always show Log Off", etc.
- Version 0.9.5 beta (Dec, 2009)
- Added option to remove the Documents menu and option to use an alternative search application (requested by johnohn).
- Added more buttons to the toolbar - Cut, Copy, Paste, Delete.
- Fixed a crash in the Start menu (thanks to AlexG).
- Version 0.9 beta (Nov, 2009) - first public version
- Classic Start menu.
- Replacement for the Copy UI in Vista.
- Fix for Alt+Enter in Explorer.
- Toolbar for Explorer with Up button.