![]() |
Desktop Development »
Shell and IE programming »
General
Intermediate
License: The MIT License
Classic ShellBy Ivo BeltchevClassic start menu and other shell features for Windows 7 and Vista |
C++, Windows (Vista, Win7), Win32, Win64, ATL, COM, Dev
|
||||||||||||
|
Advanced Search Add to IE Search |
|
|
||||||||||||||||||
![]() |
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 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:
If you have used the start menu in older versions of Windows you’ll feel right at home:

Classic Explorer is a plugin for Windows Explorer that:
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 plugin adds a new toolbar that has an Up button:

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 plugin 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 is 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 plugin detects when you press Alt+Enter and shows the properties for the currently selected folder.
TBSTYLE_EX_MULTICOLUMN and TBSTYLE_EX_VERTICAL. I could not get them to work at all and had to find another solution. I’m guessing they only work under very specific conditions that I failed to replicate. If somebody has succeeded using those styles, please let me know the secret. Since we can't create a multi-column toolbar, we have to simulate it with multiple toolbars placed side by side. Simple, right? Well, there are problems. Special care is needed to make multiple toolbars work seamlessly as one. For example if you are at the bottom of the first column and press down, the focus needs to switch to the second toolbar and its first item must be selected.Shell_TrayWnd and no name WM_LBUTTONDOWN just like any other window WM_NCLBUTTONDOWN messageProgram Manager and class Progman. It is in the same process as the taskbar but in another thread WM_SYSCOMMAND with wParam = SC_TASKLIST 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; // the start button window HWND g_Taskbar; // the taskbar window UINT g_StartMenuMsg; // a private message posted when the Win key is pressed void ToggleStartMenu(); // function that opens/closes our start menu 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; // stop the window from processing the message } } 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) { // activated by keyboard ToggleStartMenu(); msg->message=WM_NULL; } if (msg->message==WM_LBUTTONDOWN && msg->hwnd==g_StartButton) { // activated by mouse ToggleStartMenu(); msg->message=WM_NULL; } if (msg->message==WM_NCLBUTTONDOWN && msg->hwnd==g_Taskbar) { // activated by mouse 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 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.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; // hook g_pOriginalTarget=(IDropTarget*)GetProp(g_StartButton,L"OleDropTargetInterface"); if (g_pOriginalTarget) RevokeDragDrop(g_StartButton); CStartMenuTarget *pNewTarget=new CStartMenuTarget(); RegisterDragDrop(g_StartButton,pNewTarget); pNewTarget->Release(); // unhook if (g_pOriginalTarget) { RevokeDragDrop(g_StartButton); RegisterDragDrop(g_StartButton,g_pOriginalTarget); g_pOriginalTarget=NULL; }
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.// Retrieves an icon from a shell folder and child ID int CIconManager::GetIcon( IShellFolder *pFolder, PITEMID_CHILD item, bool bLarge ) { // get the IExtractIcon object CComPtr<IExtractIcon> pExtract; HRESULT hr=pFolder->GetUIObjectOf(NULL,1,&item, IID_IExtractIcon,NULL,(void**)&pExtract); if (FAILED(hr)) return 0; // get the icon location 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; // extract the icon HICON hIcon; hr=pExtract->Extract(location,index,bLarge?&hIcon:NULL,bLarge?NULL:&hIcon, MAKELONG(LARGE_ICON_SIZE,SMALL_ICON_SIZE)); if (hr==S_FALSE) { // the IExtractIcon object didn't do anything - use ExtractIconEx instead if (ExtractIconEx(location,index,bLarge?&hIcon:NULL,bLarge?NULL:&hIcon,1)==1) hr=S_OK; } // add to the image list 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.
shell32.dll. It is already loaded in the Explorer process:SHGetStockIconInfo function. Unfortunately it doesn’t give us all 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).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.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 2 folders should be combined.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.IShellFolder that presented 2 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.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.%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.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.| 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 |
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.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:TB_SETINSERTMARK message) IDropTarget from the target folder, and manually call IDropTarget::DragOver and IDropTarget::Drop to get it to perform the operation 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 2 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.WH_CBT hooks into all threads of the process and waits for a dialog box with a specific title to be created.IAccessible interfrace 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 have any other 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 shell32.dll.mui file. For example “Don’t Copy” is string 13606, "Move" is 13610, etc.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; }
To get the classic look, add the TVS_HASLINES style, remove 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.
| Operation | Folder tree command | File list command |
Cut |
41025 |
28696 |
Copy |
41026 |
28697 |
Paste |
41027 |
28698 |
Delete |
40995 |
28689 |
Properties |
no command, see Alt+Enter | 28691 |
DllMain and return 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.pBrowser->BrowseObject(NULL,(GetKeyState(VK_CONTROL)<0?SBSP_NEWBROWSER:SBSP_SAMEBROWSER)|SBSP_DEFMODE|SBSP_PARENT);The Cut, Copy, Paste, Delete and Properties 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:
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&avoritesThe 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 FindSetting function:const wchar_t *FindSetting( const char *name, const wchar_t *def );If a string is not found in the first prefered 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, FindSetting returns the def parameter.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 few things to consider:
ILC_MIRROR flag for the toolbar’s image list to mirror the icons back to normal 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 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 2 msi files into one convenient package. Certainly it is possible to distribute 2 separate msi files but the exe gives are some more advantages besides just being a convenient package:Setup|Win32 configuration. That will build the 32-bit modules and create the 32-bit msi file.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.OLEACC.dll and UxTheme.dll. Make sure all Detected Dependencies are excluded and build again.#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.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.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.SPI_GETSELECTIONFADE setting HISTORY.txt fileHISTORY.txt file
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 13 Dec 2009 Editor: |
Copyright 2009 by Ivo Beltchev Everything else Copyright © CodeProject, 1999-2010 Web21 | Advertise on the Code Project |