相对于上一篇中任务栏特性的开发,JumpList的开发显得稍微麻烦一些。JumpList将分为两次讲解,这次先讲解如何添加用户任务(User Task)。同样以foobar2000为例,当右键点击任务栏按钮时,显示程序的JumpList。
最下方3个项目为系统任务,一般不需要我们去操作。上方的两个任务:播放、参数选项,即为自定义的用户任务。用户任务本质上是一个快捷方式,对应于程序中由IShellLink接口表示。
一、ICustomDestinationList接口
同样先创建一个窗口,然后添加一个CreateJumpList方法,在这个方法中创建JumpList。创建JumpList需要几个步骤:1、创建 ICustomDestinationList 接口,这个接口对应的就是JumpList。2、调用BeginList 方法。3、创建IObjectCollection 接口。4、向 IObjectCollection 中添加快捷方式。5、由 IObjectCollection 接口取得IObjectArray 接口。6、将 IObjectArray 加入 ICustomDestinationList 。7、调用CommitList 方法。在CreateJumpList方法中加入下面代码:
- void CreateJumpList()
- {
- HRESULT hr;
- //创建List
- ICustomDestinationList *pList = NULL;
- hr = CoCreateInstance(CLSID_DestinationList, NULL,
- CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pList));
- if(SUCCEEDED(hr))
- {
- //BeginList
- UINT uMinSlots;
- IObjectArray *pOARemoved = NULL;
- hr = pList->BeginList(&uMinSlots, IID_PPV_ARGS(&pOARemoved));
- if(SUCCEEDED(hr))
- {
- //ObjectCollection
- IObjectCollection *pOCTasks = NULL;
- hr = CoCreateInstance(CLSID_EnumerableObjectCollection, NULL,
- CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pOCTasks));
- if(SUCCEEDED(hr))
- {
- hr = AddShellLink(pOCTasks);
- if(SUCCEEDED(hr))
- {
- //ObjectArray
- IObjectArray *pOATasks = NULL;
- hr = pOCTasks->QueryInterface(IID_PPV_ARGS(&pOATasks));
- if(SUCCEEDED(hr))
- {
- hr = pList->AddUserTasks(pOATasks);
- if(SUCCEEDED(hr))
- {
- hr = pList->CommitList();
- }
- pOATasks->Release();
- }
- }
- pOCTasks->Release();
- }
- pOARemoved->Release();
- }
- pList->Release();
- }
- }
二、IShellLink接口
上面的代码还不能编译,AddShellLink方法还没有编写。这个方法用于在IObjectCollection 中加入一个IShellLink 对象。 IShellLink 接口有几个方法用于设置属性:1、SetPath :设置目标的路径。2、SetWorkingDirectory 设置工作目录。3、SetIconLocation 设置图标。4、SetArguments 设置命令行参数。5、设置标题(这一步稍微复杂一点、在独立的方法中设置)。设置完属性后,调用 IObjectCollection 的AddObject 方法,将快捷方式加入。
- HRESULT AddShellLink( IObjectCollection *pOCTasks )
- {
- HRESULT hr;
- //创建ShellLink
- IShellLink *pSLAutoRun = NULL;
- hr = CoCreateInstance(CLSID_ShellLink, NULL,
- CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pSLAutoRun));
- if(SUCCEEDED(hr))
- {
- //应用程序路径
- hr = pSLAutoRun->SetPath(_T("C://Windows//notepad.exe"));
- if(SUCCEEDED(hr))
- {
- hr = pSLAutoRun->SetWorkingDirectory(_T("C://"));
- if(SUCCEEDED(hr))
- {
- //图标
- hr = pSLAutoRun->SetIconLocation(_T("C://Windows//notepad.exe"), 0);
- if(SUCCEEDED(hr))
- {
- //命令行参数
- hr = pSLAutoRun->SetArguments(_T("Test.txt"));
- if(SUCCEEDED(hr))
- {
- hr = SetTitle(pSLAutoRun, _T("Notepad"));
- if(SUCCEEDED(hr))
- {
- hr = pOCTasks->AddObject(pSLAutoRun);
- }
- }
- }
- }
- }
- pSLAutoRun->Release();
- }
- return hr;
- }
SetTitle方法用于设置标题,由于IShellLink 接口本身不带有设置标题的方法。因此需要用到另一个接口IPropertyStore 来设置标题。首先由 IShellLink 接口得到 IPropertyStore 接口,然后由字符串初始化一个PROPVARIANT 对象,接下来将该 PROPVARIANT 对象设置为标题,最后提交。
- HRESULT SetTitle( IShellLink * pShellLink, LPCTSTR szTitle )
- {
- HRESULT hr;
- //标题
- IPropertyStore *pPS = NULL;
- hr = pShellLink->QueryInterface(IID_PPV_ARGS(&pPS));
- if(SUCCEEDED(hr))
- {
- PROPVARIANT pvTitle;
- hr = InitPropVariantFromString(szTitle,&pvTitle);
- if(SUCCEEDED(hr))
- {
- hr = pPS->SetValue(PKEY_Title, pvTitle);
- if(SUCCEEDED(hr))
- {
- hr = pPS->Commit();
- }
- PropVariantClear(&pvTitle);
- }
- pPS->Release();
- }
- return hr;
- }
上面的例子将目标路径设置为记事本的路径,并且加上命令行参数"Test.txt",当点击后,将调用记事本并传入参数"Test.txt"。
三、由当前程序实例响应用户任务
现在问题的是,在大多数情况下,用户任务应该不仅仅对应的是一个指向某个程序的快捷方式,而是应该对应的是当前程序的某个功能。比如在foobar2000中的参数选项,对应着程序中的功能。而且当选择这个用户任务的时候,应该是由当前程序的当前实例来响应这个操作,而不是由新的实例来完成这个操作。接下来要做的就是如何将用户任务反映到当前程序实例中。
这里涉及到两个问题。第一个问题是,程序必须是单实例应用程序,因为当点击用户任务时,我们不能由新的程序实例来响应。解决办法是当程序启动时,检查该程序是否已有先前实例在运行,如果有,则退出,防止第二个实例运行。第二个问题是,我们的用户任务实际是通过快捷方式传递给程序的参数来反映的。而这个参数只有第二个实例能够收到。那么在第二个实例退出之前,要想办法把参数传递到第一个实例。
我们先把之前创建的用户任务改为指向自己。修改AddShellLink方法如下,其中注释的地方即为修改过的地方:
- HRESULT AddShellLink( IObjectCollection *pOCTasks )
- {
- HRESULT hr;
- //创建ShellLink
- IShellLink *pSLAutoRun = NULL;
- hr = CoCreateInstance(CLSID_ShellLink, NULL,
- CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pSLAutoRun));
- if(SUCCEEDED(hr))
- {
- //应用程序路径
- //hr = pSLAutoRun->SetPath(_T("C://Windows//notepad.exe"));
- //获取应用程序路径
- TCHAR szPath[MAX_PATH];
- GetModuleFileName(GetModuleHandle(NULL), szPath, MAX_PATH);
- hr = pSLAutoRun->SetPath(szPath);
- if(SUCCEEDED(hr))
- {
- //hr = pSLAutoRun->SetWorkingDirectory(_T("C://"));
- if(SUCCEEDED(hr))
- {
- //图标
- //hr = pSLAutoRun->SetIconLocation(_T("C://Windows//notepad.exe"), 0);
- hr = pSLAutoRun->SetIconLocation(szPath, 0);
- if(SUCCEEDED(hr))
- {
- //命令行参数
- //hr = pSLAutoRun->SetArguments(_T("Test.txt"));
- hr = pSLAutoRun->SetArguments(_T("/Task1"));
- if(SUCCEEDED(hr))
- {
- //hr = SetTitle(pSLAutoRun, _T("Notepad"));
- hr = SetTitle(pSLAutoRun, _T("User Task 1"));
- if(SUCCEEDED(hr))
- {
- hr = pOCTasks->AddObject(pSLAutoRun);
- }
- }
- }
- }
- }
- pSLAutoRun->Release();
- }
- return hr;
- }
现在的情况是,当我们点击User Task 1时,一个新的实例启动了。接下来就来解决上面提到的问题。
第一个问题的解决方法是使用Mutex(互斥量)。当第一个实例启动时,创建一个互斥量。当第二个实例启动时,同样创建这个互斥量。由于该互斥量已经创建,所以必然导致失败,第二个实例退出。当第一个实例结束的时候,撤销该互斥量。在WinMain函数的开头加上下面代码:
- HANDLE hMutex = CreateMutexEx(NULL, _T("Local//MutexTestJumpList"), 0, 0);
- if (hMutex == NULL)
- { //Mutex创建失败,已有先前实例运行,退出当前实例
- return 0;
- }
WinMain函数的结尾加上下面代码:
- if(hMutex != NULL)
- {
- CloseHandle(hMutex);
- }
这次当我们点击User Task 1时已经看不见新的实例启动了(实际上新的实例仍然启动了,只不过检测到先前实例后,迅速退出了)。
第二个问题是如何将参数传递到第一个实例。对于Windows程序来说,最容易想到的就是通过消息发送了。但又如何得到先前实例的主窗口句柄呢?这里要用到内存映射文件(Memory Mapped File)。第一个实例运行后,将自己的主窗口句柄放入到一个内存映射文件中,第二个实例从该内存映射文件中读出先前实例的主窗口句柄。在窗口创建之后加入下面代码,将当前主窗口句柄放入内存映射文件:
- //将主窗口句柄放入内存映射文件
- HANDLE hMMFFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL,
- PAGE_READWRITE, 0, sizeof(g_hWnd), _T("Local//MMFTestJumpList"));
- LPVOID lpVoid = MapViewOfFile(hMMFFile, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
- memcpy_s(lpVoid, sizeof(g_hWnd), &g_hWnd, sizeof(g_hWnd));
- UnmapViewOfFile(hMMFFile);
该内存映射文件的名字不要和之前Mutex的名字相同,否则会创建失败。在程序退出之前要关闭文件:
- CloseHandle(hMMFFile);
在WinMain函数开头,检测到有先前实例运行之后。从这个内存映射文件读入先前实例的主窗口句柄:
- //获取先前实例主窗口句柄
- HANDLE hMMFFile = OpenFileMapping(FILE_MAP_READ, FALSE, _T("Local//MMFTestJumpList"));
- if (hMMFFile == NULL) return 0;
- LPVOID lpVoid = MapViewOfFile(hMMFFile, FILE_MAP_READ, 0, 0, 0);
- HWND hWndPrev = NULL;
- memcpy_s(&hWndPrev, sizeof(hWndPrev), lpVoid, sizeof(hWndPrev));
- UnmapViewOfFile(hMMFFile);
- CloseHandle(hMMFFile);
得到先前实例的窗口句柄后,利用WM_COPYDATA消息,将命令行参数发送到先前实例。
- //将命令行参数发送到先前实例
- COPYDATASTRUCT cpdata = {0};
- cpdata.dwData = 1;
- cpdata.lpData = szCmdLine;
- cpdata.cbData = (_tcslen(szCmdLine)+1)*sizeof(TCHAR);
- SendMessage(hWndPrev, WM_COPYDATA, 0, reinterpret_cast<LPARAM>(&cpdata));
然后加入对WM_COPYDATA消息的处理:
- case WM_COPYDATA: //从下一个实例传来命令行参数
- {
- COPYDATASTRUCT *pdata = reinterpret_cast<COPYDATASTRUCT *>(lParam);
- if(pdata->dwData == 1)
- {
- LPCTSTR szCmdLine = reinterpret_cast<LPCTSTR>(pdata->lpData);
- if(szCmdLine != NULL)
- HandleCmdLine(szCmdLine);
- }
- return 0;
- }
- break;
在HandleCmdLine中,我们只简单显示一条消息即可:
- void HandleCmdLine( LPCTSTR szCmdLine )
- {
- if(NULL != StrStr(szCmdLine, _T("/Task1")))
- {
- MessageBox(g_hWnd, _T("User Task 1 Clicked"), NULL, MB_OK);
- }
- }
验证一下现在的效果:
因为JumpList即使在程序没有运行时也是存在的,所以我们在窗口创建之后也要调用一次HandleCmdLine方法,以使第一个程序实例可以响应静止状态下的用户点击。如下面的效果:
此时程序未运行,只是锁定到任务栏,点击JumpList上的用户任务,第一个程序实例启动,并响应用户任务
为了简化问题,上面的代码中省去了很多错误处理。在实际的使用中,我将单实例运行与实例间的数据传递封装在了一个辅助的类中,源代码中将会提供这个类。
上面代码需要包含的头文件:
- #include <ShlObj.h>
- #include <propvarutil.h>
- #include <propkey.h>
需要连接的库:shlwapi.lib
原文链接:http://blog.youkuaiyun.com/ntwilford/article/details/5635381