Rich Integration with the Windows 7 Shell through IShellLink

Introduction

Shell links provide the ability to access objects within the Windows shell such as files, folders, printers and applications. An end-user of Windows can create a shell link through the New | Shortcut menu item in Windows Explorer, and applications can achieve the same effects through the COM interfaces IShellLink to create a link and IPersistFile to save it. Shell links have been available since Windows 95, and hence can be considered very old technology. The reason that Shell Links have received more attention recently is due to their use in Windows 7 Jump Lists, which, while using the same technology, requires slightly different implementation techniques to achieve useful outcomes.

As covered in a previous article, Windows 7 Jump Lists items fall into two distinct categories – destinations and tasks. Destinations are typically files or folders that are particularly relevant to the application exposing the jump list, and have inbuilt known categories such as Frequent and Recent documents. In contrast to noun-based destination, tasks are verb-based, and are used to initiate application action that would typically be accessible via a shortcut key or menu item. Jump list tasks are implemented using Shell links, and as the typical use for Shell links is conceptually closer to destinations than tasks, some bridging code is required to successfully use Shell links for Jump list tasks.

When considering the use of Shell links and Jump list tasks for passing commands to an application, some design is required to determine how the Shell link will determine which instance of an application that link should be routed to. There is a single Jump list per application (or more accurately, per Application ID), and multiple instances of a running application share the same Jump list. If a word processing application that allows multiple instances of an application to be started exposed a “Spell Check” task, which instance of the running application should the task be routed through to? While the obvious choice would be the top-most application, the behavior of the task would not be always intuitive to the end user, and for this reason using Shell link and Jump lists are much more appropriate for applications that allow only a single instance to be active at a time. This is not a hard requirement though, and Internet Explorer 8 exposes an “Open New Tab” task despite allows multiple application instances to be open simultaneously.

For applications that choose to use Shell links for application tasks and want to limit the application to a single running instance, some boiler-plate code is required to implement this. The first piece of the solution is to prevent multiple application launches. This can be achieved by creating a mutex object when the application starts, and if the mutex already exists, the new process simply exits.


BOOL CMyApp::InitInstance()
{
//unrelated code removed for brevity…
m_singleInstanceMutex = ::CreateMutex(NULL, FALSE, m_pszAppID);
if (GetLastError() == ERROR_ALREADY_EXISTS){
return FALSE;
}
}

In addition to returning FALSE from the overridden CWinAppEx::InitInstance method, processing the command coming in from the Shell link and sending this to the original application instance is also required. This can be achieved by passing the Shell link command in as a command-line parameter, and translating this to a custom windows message. If the Shell link is being created using a Jump list, and continuing the spell check example discussed earlier, a jump list item that initiates a spell check could be added be specifying a Jump list task that points to the current executable and also specifies a command line argument that indicates a spell check should be conducted.


const CString spellCheckCmd(_T(“/SpellCheck”));
CString fullCommandLine(GetCommandLine());
CString commandLineArgs(m_lpCmdLine);
CString fullExeName = fullCommandLine.Left(fullCommandLine.GetLength() –
 commandLineArgs.GetLength() – 1);

m_jumpList.AddTask(fullExeName, spellCheckCmd, _T(“Spell Check Document”),
 NULL, 0);


Now that a command line parameter is available to tell the new process what task needs to be triggered, InitInstance can be extended to pass this task to the process already executing.


BOOL CMyApp::InitInstance()
{
//other code removed
m_singleInstanceMutex = ::CreateMutex(NULL, FALSE, m_pszAppID);
if (GetLastError() == ERROR_ALREADY_EXISTS){
if (commandLineArgs.Find(spellCheckCmd) != -1){
SendMessageToRunningApp();
}
return FALSE;
}
}

BOOL CALLBACK EnumProc(HWND hwnd, LPARAM lParam){
DWORD processId;
GetWindowThreadProcessId(hwnd, &processId);
PROCESSENTRY32* pEntry = (PROCESSENTRY32*)lParam;

if (pEntry->th32ProcessID == processId){
 PostMessage(hwnd, WM_SPELLCHECK_MSG, 0, 0);
}
return TRUE;
}

void CMyApp::SendMessageToRunningApp()
{
HANDLE process = NULL;
HANDLE processes = CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, NULL );
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof( PROCESSENTRY32 );

if (Process32First(processes, &processEntry ) == TRUE ) {
 while (Process32Next( processes, &processEntry ) == TRUE ) {
  if (_tcscmp(processEntry.szExeFile, _T(“AppName.exe”)) == 0 ) {    
   EnumWindows(EnumProc, (LPARAM )&processEntry);
   break;
  }
 }
}

CloseHandle(process);
CloseHandle(processes);
}


The implementation of SendMessageToRunningApp is slightly clunky – each running process needs to be iterated through to find the actual process which is the first instance of the application to be started (and hence will be the one that successfully acquired the mutex and initialized successfully), and once the process ID has been identified, this needs to be converted to a HWND through the EnumWindows callback. Once the correct window handle has been located, a custom message can be posted to it. Upon receipt of this custom message, a message map can be used to route it to the appropriate command, completing the initiation of the task.


#define WM_SPELLCHECK_MSG  (WM_USER+1)
BEGIN_MESSAGE_MAP(CMainFrame, CMDIFrameWndEx)
ON_MESSAGE(WM_SPELLCHECK_MSG, OnSpellCheck)
END_MESSAGE_MAP()

LRESULT CMainFrame::OnSpellCheck(WPARAM wParam, LPARAM lParam)
{
//appropriate code here for spell check
return 0;
}


As shown in this final code snippet, the message handling code has been placed in the generated CMainFrame class. As the custom windows message is sent to the applications top-level window, the CView-derived class, which is a child window, will not receive the message.

Conlusion

Arguments could be raised against the use of Shell links to implement Jump list tasks. Being primarily designed to access Shell objects and not initiate tasks within an application, using Shell link tasks necessitates a few translation hops to get from a Shell link command line argument to a message in an application to perform some action. However, this technology decision has been made by the Windows team, and as C++ developers, there aren’t other options available for getting Jump list tasks implemented. Dealing with the slight difficulty in Shell link and Jump list task translation is a once-off hit though, and adapting the code presented in this article to automatically map Shell links and command line arguments to custom message that can be processed by a MFC application is not a difficult undertaking. By overcoming this slightly difficult implementation issue, creating a professional-looking application that integrates deeply with the Windows 7 shell can be achieved.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read