Chapter 8: Using Windows 7 Libraries and the Shell
Users can store documents, images, and other files in many locations—on different types of hardware installed locally or on other computers on the network. In the past, files were often physically stored in a location according to their type —for example, images in the My Pictures folder, documents in the My Documents folder, and so on. However, a far more powerful and modern way to access files is via their type rather than through their location. This is the purpose of Windows 7 Libraries. Files from many different locations can be accessed through a single logical location according to their type even though they are stored in many different locations. Libraries are user defined collections of content that are indexed to enable faster search and sorting. Hilo uses the Windows 7 Libraries feature to access the user’s images.
Using the Shell Namespace
The Windows Shell namespace provides access to a wide range of objects through a hierarchical structure. Windows Vista replaced the older constant special item ID list (CSIDL) naming of special folders with known folder IDs. In both cases the special folders contain specific types of data: video, pictures, or music, but the folders are accessed using an ID that at runtime can give access to the disk storage path. Known folders in Windows Vista offer more features than CSIDL including the ability to change the storage location without changing the applications that rely on the known folder (CSIDL only allows My Documents location to be changed by the user).
Windows 7 extends the idea of known folders with libraries. Windows 7 libraries are user-defined collections of folders, and actions that are performed on the library will be applied to all folders in the library. This means that when you search a Windows 7 library you will search all the folders that are part of the library, and when you stack the items in a library the stacks will contain items from all the folders in the library regardless of the actual location of those folders. Windows 7 indexing is applied to Windows 7 libraries which mean that searches on a library will be performed on all the folders in the library.
Windows Explorer displays libraries in the navigation pane, as shown in Figure 1. The properties of a library shows the actual folders that will be included. You can use this property dialog to add or remove folders and to determine which folder will be treated as the default save location. You can add any folders on the local disks that your account has access too, and any folders on external drives like USB drives or shares on a server. You cannot add folders on removable drives, nor on remote shares that are not available offline that (MSDN lists the folders that cannot be put in libraries).
Figure 1 Windows 7 libraries
When you select a library, Windows Explorer displays an aggregate view of the files and folders that are part of the library, as shown in Figure 2.
Figure 2 Showing the contents of the Documents library
Libraries are logical representations of user content. This means that you store files in the folders that are part of the library and not in the library itself. So for example, the Documents library is the default location for documents and contains the user’s documents in their My Documents folder and any documents in the Public Documents folder. Although Windows Explorer displays the Documents library as if it is a folder, no physical folder exists, so if a user saves a file to the Documents library then the file will actually be saved to the default save location (in Figure 1 this save location is set to My Documents).
The Windows 7 Application Programming Interface (API) provides COM-based objects used to access the contents of the libraries. You can traverse through the logical hierarchy through these shell item objects without knowing the absolute storage location paths (although it is possible to obtain the system file path). All items in the shell are represented by an object with the IShellItem interface, but specific shell items will implement other interfaces (for example, folder objects also implement the IShellFolder interface). It is very important that Windows 7 applications use the Shell API to access shell items rather than using absolute file system file paths. Equally important is that applications use the Windows 7 common file dialogs because these dialogs show the system’s libraries and provide appropriate IShellItem objects selected through the dialog.
Using Shell Items
Central to the shell namespace are objects called shell items that implement the IShellItem interface. The new common file dialogs (CLSID_FileOpenDialog or CLSID_FileSaveDialog) refer to items through shell item objects and return an IShellItem or an array of such items through the IShellItemArray interface. The caller can then use an individual IShellItem object to get a file system path or to open a stream object on the item to read or write information, or query for one of the several shell interfaces implemented for specific shell types. The use of IShellItem objects is important because these file dialogs can access items in both file system folders and other virtual folders that you find in the shell, including libraries. In addition, Windows 7 provides a new object, CLSID_ShellLibrary, specifically to access libraries.
To use the shell API in C++ you include the shobjidl.h header file. This file declares the shell interfaces and the symbols for the CLSIDs for the shell objects that you can create, and it also contains helper methods. Listing 1 shows simple code to create and use a shell item. The SHCreateItemFromParsingName method takes a system file path and returns a shell item object. In this example the shell item object is used to obtain a user readable string for the item.
Listing 1 Creating a shell item
LPWSTR szFilePath = GetFileNameFromSomewhere(); // Get a file name from somewhere.
IShellItem* pItem = nullptr;
HRESULT hr = ::SHCreateItemFromParsingName(
szFilePath, nullptr, IID_PPV_ARGS(&pItem));
if (SUCCEEDED(hr))
{
LPWSTR szName = nullptr;
hr = pItem->GetDisplayName(SIGDN_NORMALDISPLAY, &szName);
if (SUCCEEDED(hr))
{
wprintf(L"Shell item name: %s\n", szName);
::CoTaskMemFree(szName);
}
pItem->Release();
}
Shell items can represent any object that can be displayed in the shell: a file, a folder, a shortcut, or even virtual folder items like the Recycle Bin, and libraries. You can get information about the type of item by calling the IShellItem::GetAttributes method. Listing 2 shows code that accesses the attributes of a shell object. In this case the SHCreateItemInKnownFolder method is called to get a shell object for the libraries on the computer. This method creates the shell item object, it does not create the files or folders that the shell item object refers to. The first parameter of this method is a GUID for the known folder that is one of the values defined in knownfolders.h. The third parameter is the name of the item within the known folder that you want to access, or if the parameter is null (as in this case) the shell item object will be to the known folder. Listing 2 accesses the Libraries folder and as mentioned above, this folder does not physically exist on a computer, instead the shell item gives access to other libraries in the shell namespace.
Listing 2 Accessing a known folder
IShellItem* pItem = nullptr;
HRESULT hr = ::SHCreateItemInKnownFolder(
FOLDERID_Libraries, 0, nullptr, IID_PPV_ARGS(&pItem));
if (SUCCEEDED(hr))
{
DWORD dwAttr = 0;
hr = pItem->GetAttributes(SFGAO_FILESYSANCESTOR, &dwAttr);
if (SUCCEEDED(hr))
{
if (SFGAO_FILESYSANCESTOR == dwAttr)
{
wprintf(L"Item is a file system folder\n");
}
}
pItem->Release();
}
You can also use the SHGetKnownFolderItem function to get a shell item for a known folder. This function also allows you to pass an access token so that you can access known folders restricted to users other than the current logged on user.
Accessing Shell Item Properties
You can get additional information about a shell item by requesting the value of an item property. To do this you should use the methods on the IShellItem2 rather than IShellItem. Each property is identified by the values in a PROPERTKEY structure and propkey.h defines the initialized values for the properties that you can request. Properties can be strings, numeric values, or dates, and there are methods on IShellItem2 to return appropriate values. For example, Listing 3 shows how to access the date that the item was created by accessing the PKEY_DateCreated property and it assumes the pItem variable has already been assigned to an IShellItem object.
Listing 3 Accessing a property through a shell item
IShellItem2* pItem2 = nullptr;
hr = pItem->QueryInterface(&pItem2);
if (SUCCEEDED(hr))
{
FILETIME ft = {0};
pItem2->GetFileTime(PKEY_DateCreated, &ft);
SYSTEMTIME st = {0};
::FileTimeToSystemTime(&ft, &st);
wprintf(
L"Date Created: %04d-%02d-%02d %02d:%02d:%02d\n",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
pItem2->Release();
}
Binding to Handler Objects
The methods of the IShellItem and IShellItem2 interfaces give limited access to the shell object, however, you can obtain a handler object to get additional access by calling the IShellItem::BindToHandler method. There are many types of handler objects and different shell items will use different handler objects. If the item is a file then you can access a IStream handler object, if the item is a folder then you can access an IShellFolder handler. The BindToHandler method is passed a GUID (defined in shlguid.h) to the type of the handler object you want to obtain. and the method returns a pointer to the handler object. So assuming that the pItem variable is the shell item for the Libraries folder initialized in Listing 2, the code in Listing 4 obtains an enumerator object to enumerate the child items and print their names.
Listing 4 Enumerating items in a folder shell item
IEnumShellItems* pEnum = nullptr;
hr = pItem->BindToHandler(nullptr, BHID_EnumItems, IID_PPV_ARGS(&pEnum));
if (SUCCEEDED(hr))
{
IShellItem* pChildItem = nullptr;
ULONG ulFetched = 0;
do
{
hr = pEnum->Next(1, & pChildItem, &ulFetched);
if (FAILED(hr)) break;
if (ulFetched != 0)
{
LPWSTR szChildName = nullptr;
child->GetDisplayName(SIGDN_NORMALDISPLAY, &szChildName);
wprintf(L"Obtained %s\n", szChildName);
CoTaskMemFree(szChildName);
pChildItem ->Release();
}
} while (hr != S_FALSE);
pEnum->Release();
}
If the shell item is a file, it is up to you as the developer to determine how to load the data from the file. If you wish to use the Windows API functions like ReadFile to read from the file then you must obtain a handle to the file. To do this you can pass the name returned from IShellItem::GetDisplayName for the SIGDN_FILESYSPATH name type to the CreateFile function. You can also open the shell item as a stream object by requesting the BHID_Stream handler in a call to the IShellItem::BindToHandler method.
Using Common File Dialogs
In addition, other APIs will act upon shell item objects, for example you can use the Common File Dialog to obtain a shell item with the path to where you want a file saved. The Common File Dialog object does not open or save a file, instead it gives information about the shell item that the user identifies. Listing 5 shows the basic code to save a file, here the pInitialItem variable is an initialized shell item object that indicates the file that is initially shown in the Save As dialog by calling the IFileSaveAsDialog::SetSaveAsItem method. The IFileSaveDialog::Show method displays the dialog and if the user clicks the OK button this method will return S_OK, otherwise if the user clicks the Cancel button then the dialog will return ERROR_CANCELLED. If the user has specified a file in the dialog then a shell item with information about this file is obtained through a call to IFileSaveDialog::GetResult. In this case, the shell item object will not necessarily reference an actual file, the shell item object simply contains the path and file name provided by the user in the dialog. The code then has to provide its own code to copy the file referenced by the pInitialItem shell item to the location specified by the pSaveAsItem shell item.
Listing 5 Using the Save As dialog
IFileSaveDialog* pShellDialog;
hr = CoCreateInstance(
CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&pShellDialog));
if (SUCCEEDED(hr))
{
pShellDialog->SetSaveAsItem(pInitialItem);
pShellDialog->Show(nullptr);
if (SUCCEEDED(hr))
{
IShellItem* pSaveAsItem = nullptr;
hr = pShellDialog->GetResult(&pSaveAsItem);
if (SUCCEEDED(hr))
{
CopyFileTo(pInitialItem, pSaveAsItem); // Call the code to Copy copy the actual file…
pSaveAsItem->Release();
}
}
else
{
// If the user clicks cancel the return value is 0x800704c7, that is
// HRESULT_CODE(hr) == ERROR_CANCELLED
}
pShellDialog->Release();
}
The Save As dialog shown in Listing 5 will also show the Libraries folder in the navigation pane. If the user selects a library then the shell item returned from the IFileSaveDialog::GetResult method will reference the actual file system location, if necessary using the default save location for the library.
Using the Shell Library Object
The Windows 7 API provides a COM object to allow you to administer libraries. The shell library object implements the IShellLibrary interface and the Windows API provides a helper method SHCreateLibrary that creates an uninitialized object. You can call IShellLibrary::LoadLibraryFromKnownFolder to initialize the object to refer to a known folder. The shell library object can be used to add or remove folders from the library, enumerate the folders in the library, and set the default save folder. The library object is used to write to the description file (library-ms file) for the library, and once you have finished changing the library settings you must call the IShellLibrary:Commit method if the library already exists, or the IShellLibrary::Save if this is a new library.
Using Windows 7 Libraries in Hilo
The carousel and media panes in the Hilo Browser show thumbnail representations of folders and photos, so the message handlers for these classes need to be initialized with information about these items. In Hilo this is done through a struct called ThumbnailInfo that has a member which is a reference to an IShellItem object. When a user selects a folder in the carousel, the window message handler enumerates the items in the folder and displays the subfolders in the inner orbital and the photos in the media pane. To do this, Hilo has to enumerate and filter shell items.
Initializing the Browser Carousel
The BrowserApplication class has a variable called m_currentBrowseLocationItem that is a shell item to the initial folder shown by the Browser carousel. The variable is initialized when the Browser is started with the code shown in Listing 6. The first part of this code calls the SHCreateItemInKnownFolder method to get a shell item object for the Pictures library and if this call is not successful the code then calls SHGetKnownFolderItem to obtain the shell item for the Computer folder which gives access to all the hard drives (including external drives) accessible by the computer.
Listing 6 Determining the initial folder for the carousel
// No location has been defined yet, so we just load the pictures library
if (nullptr == m_currentBrowseLocationItem)
{
// Default to Libraries library
hr = ::SHCreateItemInKnownFolder(
FOLDERID_PicturesLibrary, 0, nullptr, IID_PPV_ARGS(&m_currentBrowseLocationItem));
}
// If the Pictures Library was not not found
if (FAILED(hr))
{
// Try obtaining the "Computer" known folder
hr = ::SHGetKnownFolderItem(
FOLDERID_ComputerFolder, static_cast<KNOWN_FOLDER_FLAG>(0), nullptr,
IID_PPV_ARGS(&m_currentBrowseLocationItem));
}
if (SUCCEEDED(hr))
{
ComPtr<IPane> carouselPane;
hr = carouselPaneHandler.QueryInterface(&carouselPane);
if (SUCCEEDED(hr))
{
hr = carouselPane->SetCurrentLocation(m_currentBrowseLocationItem, false);
}
}
The message handler class for the carousel pane is updated with the current location by passing the shell item to the SetCurrentLocation method. This method enumerates the folders in the selected folder and updates the carousel to show these subfolders. The carousel pane handler class then calls the SetCurrentLocation method on the media pane which enumerates the image files in the selected folder and uses this to populate the thumbnails in the media pane.
Whenever the user selects a folder in the carousel or in the history list the shell item for the newly selected item is passed to SetCurrentLocation of the carousel handler and hence the carousel and media pane are updated with the items in the selected folder.
Enumerating Folders
Hilo provides a utility class (in the Common project) called ShellItemsLoader. This class has one public static method called EnumerateFolderItems that is passed the shell item object of the folder to enumerate, a value indicating if the method should return the folders or image file items in the folder, and a parameter indicating whether the search is recursive or not. This method returns a vector of the shell item objects for the requested items.
Listing 7 shows the first part of this method, 1 indicates the type of objects to search for and is used to add named values to the itemKinds vector. When the method enumerates items in the folder it obtains the type of each item, and if the type of the item is one of those in the itemKinds vector the item is added to the shellItems vector returned to the caller.
Listing 7 Enumerating folders: initialization code
HRESULT ShellItemsLoader::EnumerateFolderItemsNonRecursive(
IShellItem* currentBrowseLocation, ShellFileType fileType,
std::vector<ComPtr<IShellItem> >& shellItems)
{
std::vector<std::wstring> itemKinds;
if ((fileType & FileTypeImage) == FileTypeImage)
{
itemKinds.push_back(L"picture");
}
if ((fileType & FileTypeImage) == FileTypeVideo)
{
itemKinds.push_back(L"video");
}
if ((fileType & FileTypeImage) == FileTypeAudio)
{
itemKinds.push_back(L"music");
}
// Followed by code to do the enumeration
The folder is enumerated by using a IShellFolder handler object and Listing 8 shows the code that obtains this object by calling the BindToHandler method.
Listing 8 Enumerating folders: creating the handler object
// Enumerate all objects in the current search folder
ComPtr<IShellFolder> searchFolder;
HRESULT hr = currentBrowseLocation->BindToHandler(
nullptr, BHID_SFObject, IID_PPV_ARGS(&searchFolder));
if (SUCCEEDED(hr))
{
// Enumeration code, see Listing 9
}
The IShellFolder object has an enumeration object that implements the IEnumIDList interface, this interface enumerates item IDs rather than shell items, but it is possible to create a shell item from an ID by calling the SHCreateItemWithParent method. Listing 9 shows the main code that enumerates items in the specified folder. First the code initializes a SHCONTF flag to indicate whether the searched items should be folders or files. This flag is passed to the IShellFolder::EnumObjects method that returns an enumeration object with the items. The code then repeatedly calls IEnumIDList::Next on this object to obtain the item’s ID and calls the SHCreateItemWithParent method to create the shell item object.
Listing 9 Enumerating folders: enumerating items
bool const isEnumFolders = (fileType & FileTypeFolder) == FileTypeFolder;
SHCONTF const flags = isEnumFolders ? SHCONTF_FOLDERS : SHCONTF_NONFOLDERS;
ComPtr<IEnumIDList> fileList;
if (S_OK == searchFolder->EnumObjects(nullptr, flags, &fileList))
{
ITEMID_CHILD* idList = nullptr;
unsigned long fetched;
while (S_OK == fileList->Next(1, &idList, &fetched))
{
ComPtr<IShellItem2> shellItem;
hr = SHCreateItemWithParent(nullptr, searchFolder, idList, IID_PPV_ARGS(&shellItem));
if (SUCCEEDED(hr))
{
// Check to see if the item should be added to the returned item vector
// See Listing 10
}
ILFree(idList);
}
}
}
return hr;
The shell item created in Listing 9 will either be for a folder or a nonfolder item. If the search is for folders then no further processing is necessary and the item is added to the shellItems vector. If the item is a nonfolder object, then the code must check to see if the item is one of the types of files requested, this code is shown in Listing 10. This code reads the PKEY_Kind property of the item to obtain the item type as a string and compares the returned value with the items in the shellItems vector.
Listing 10 Enumerating folders: checking the item type
if (isEnumFolders)
{
shellItems.push_back(static_cast<IShellItem*>(shellItem));
}
else
{
// Check if we the item is correct
wchar_t *itemType = nullptr;
hr = shellItem->GetString(PKEY_Kind, &itemType);
if (SUCCEEDED(hr))
{
auto found = std::find(itemKinds.begin(), itemKinds.end(), itemType);
if (found != itemKinds.end())
{
shellItems.push_back(static_cast<IShellItem*>(shellItem));
}
::CoTaskMemFree(itemType);
}
}
Conclusion
In this chapter you have seen how to use the shell API to access folders and items through Windows 7 libraries. In the next chapter, we will introduce the Annotator application which allows the user to easily edit their images.