第六章 快捷方式的最短路径
Windows Shell允许存储任何对象的引用到系统范围内的任何地点。例如,当你从一个文件夹拖拽可执行程序到另一个文件夹时,鼠标自动改变形状给出除拷贝和移动文件之外的第三种选择。
除非你确定,否则可执行文件是不能拷贝或移动的,相反,每次你做这样的操作时,实际拷贝或移动的是对它的物理位置的一个引用,实际所建立的不是文件的拷贝,而是它的初始位置的连接。
所有这些都是快捷方式的示例,这种东西在老版本的Windows中就已经存在—例如,程序管理器的图标就是早期的快捷方式。然而,不要弄混了,它们不是相同的,主要差别在于快捷方式具有可以指向文件对象这个更普遍的机理:不仅是可执行文件,也不仅仅是文件。在Windows9x和WindowsNT的Shell中,快捷方式是无处不在的。可以在任何文件夹中找到它们,而最多的是在系统的特殊文件夹中。如果你希望应用程序具有印象深刻的功能,比如,添加项到‘Favorites’或‘发送到’文件夹中,甚至是到‘开始’菜单中,则建立快捷方式是一个可行的方法。快捷方式是Shell的重要组成部分,也是我们在这一章里要彻底讨论的内容。在这一章中我们打算讨论:
- 快捷方式确切地是什么
- 系统怎样存储和装入快捷方式
- 怎样建立和删除快捷方式
- 可以编码处理快捷方式的函数举例
我们将要给出的例子假设你对Shell编程外围知识有一定了解,但是例子将进一步清晰地说明快捷方式的灵活性。例如,在这一章中,我们将使用热键控件和拖拽功能作为示例应用的内建功能。
什么是快捷方式
快捷方式表示一个特定文件对象的连接,并是一个具有.lnk扩展名的微小二进制文件。这里的‘微小’意思是快捷方式文件的尺寸很少达到1KB。并不是所有的快捷方式都确切地有相同的尺寸,但是它们却拥有固定的属性集:目标文件对象,描述,热键,图标等。我们将简短地检测这些内容。
快捷方式遍及整个Windows Shell,可以作为Shell提供的服务。从软件的观点分析,快捷方式是通过暴露IShellLink接口的COM服务器实现的,其接口标识是CLSID_ShellLink。通过这个接口,你可以设置快捷方式的各种属性,和调用接口方法在磁盘上保存或装入它们。
快捷方式文件类型
正如前面所说的,快捷方式是一个文件,但是,它是一种Shell以特殊方法处理的文件。Shell当然知道一个类行为‘快捷方式’的文件是一个对某件东西的引用,所以当你双击它的时候(或单击它—依赖于活动桌面的设置)返回一个被指向的对象,而不是你点击的文件。
建立快捷方式
尽管快捷方式通常都与可执行文件相关联,但这并不是规定—你可以建立目录或非可执行文件的快捷方式。就编程而言绝对没有不同。同样也能建立非文件系统对象的快捷方式(如打印机)。而此时,就有一个小的差别了,你应该使用不同的方法做这个工作。
建立一个新的.lnk文件有两个选择,头一个依赖于Shell DDE接口,它是直接从旧的程序管理器继承过来的。我们不考虑这种情况,详情请参看Shell DDE相关资料(Internet 客户端SDK)和MSDN库。如果你使用DDE编程而不是下面将要看到的技术,这些资料可以使你知道Windows3.x以来都发生了哪些改变,以及DDE接口中相对较新的特征。
使用IShellLink接口
第二个建立快捷方式的方法(也是推荐的方法)是使用IShellLink COM接口,这是一个十分容易的过程,相关的步骤是:
- 建立适当的COM服务器
- 获得IShellLink接口指针
- 通过IShellLink的方法设置属性
- 获得IPersistFile接口指针
- 使用IPersistFile的方法保存一个文件的快捷方式
建立服务器就是调用CoCreateInstance(),一定要保证在处理之前已经适当地初始化了COM库(使用CoInitialize()):
- IShellLink* pShellLink = NULL;
- HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
- IID_IShellLink, reinterpret_cast<LPVOID*>(&pShellLink));
- if(FAILED(hr))
- return hr;
CLSID在shlobj.h头文件中定义,上面的调用返回指向IShellLink接口的指针,这是处理快捷方式的关键。下面表中列出接口的所有方法,和主要的描述,我们将在随后的代码例程中指出某些可能存在的缺陷。
方法 | 描述 |
GetArguments() SetArguments() | 返回/设置命令行变量 |
GetDescription() SetDescription() | 返回/设置描述串 |
GetHotkey() SetHotkey() | 返回/设置快捷方式的热键 |
GetIconLocation() SetIconLocation() | 返回/设置路径和图标索引 |
GetIDList() SetIDList() | 返回/设置链接对象的PIDL。如果操作非文件系统对象,应该使用这两个方法代替GetPath()和SetPath() |
GetPath() SetPath() | 返回/设置链接对象的路径和文件名 |
GetShowCmd() SetShowCmd() | 返回/设置链接对象的SW_XXX标志 |
GetWorkingDirectory() SetWorkingDirectory() | 返回/设置工作目录 |
SetRelativePath() | 设置连接对象的相对路径 |
Resolve() | 恢复有快捷方式指向的文件对象 |
一旦获得了IShellLink接口的指针,你就可以开始通过设置目标对象(文件,目录,或非文件对象的PIDL),和选项属性列表,构造快捷方式了。你也可以设置描述文字,快速访问热键,特殊的图标,工作目录,命令行参数以及表示窗口(如果有)建立行为的值。下面是典型的代码段:
- pShellLink->SetPath(pszTarget);
- pShellLink->SetDescription(pszDesc);
- pShellLink->SetHotkey(wHotKey);
- pShellLink->SetIconLocation(pszIconPath, wIconIndex);
此时,对象仅仅存在于内存之中,为了使它永久存在,需要把它存进文件。就是为了这个原因,我们使用的COM服务器(标志为CLSID_ShellLink)才实现了IPersistFile接口。这是一个包含读写磁盘方法的接口,因此可以为调用者提供通常编程接口意义上的文件装入与保存服务。
- IPersistFile* pPF;
- pShellLink->QueryInterface(IID_IPersistFile, reinterpret_cast<LPVOID*>(&pPF));
- MultiByteToWideChar(CP_ACP, 0, szLnkFile, -1, wszLnkFile, MAX_PATH);
- pPF->Save(wszLnkFile, TRUE);
IPersistFile接口的两个最重要的方法Load()和Save(),二者都要求Unicode串,因而需要转换包含文件名的缓冲为宽字符串格式。
快捷方式的全程函数
我们已经给出了形成新Shell辅助函数的信息,用于建立快捷方式—显然,Windows Shell API并不提供简单而直接的函数来建立(或处理)快捷方式。另一个想法是,我们打算给函数取名为SHCreateShortcutEx()。
事实上,尽管Win32API没有,但是WindowsCE SDK中却包含了这样一个函数SHCreateShortcut(),具有下面的原型:
- BOOL SHCreateShortcut(LPTSTR szShortcut, LPTSTR szTarget);
我们的函数接受.lnk文件作为目标名,和一个包含这个快捷方式所有属性的结构:
- struct SHORTCUTSTRUCT
- {
- LPTSTR pszTarget;
- LPTSTR pszDesc;
- WORD wHotKey;
- LPTSTR pszIconPath;
- WORD wIconIndex;
- };
- typedef SHORTCUTSTRUCT* LPSHORTCUTSTRUCT;
下面是这个函数的源代码,这个函数我们将在后面的示例程序中使用:
- HRESULT SHCreateShortcutEx(LPCTSTR szLnkFile, LPSHORTCUTSTRUCT lpss)
- {
- WCHAR wszLnkFile[MAX_PATH] = {0};
- IShellLink* pShellLink = NULL;
- IPersistFile* pPF = NULL;
- // 验证SHORTCUTSTRUCT指针
- if(lpss == NULL)
- return E_FAIL;
- // 建立COM服务器,假设CoInitialize()已经被调用
- HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL,
- CLSCTX_INPROC_SERVER, IID_IShellLink,
- reinterpret_cast<LPVOID*>(&pShellLink));
- if(FAILED(hr))
- return hr;
- //设置属性
- pShellLink->SetPath(lpss->pszTarget);
- pShellLink->SetDescription(lpss->pszDesc);
- pShellLink->SetHotkey(lpss->wHotKey);
- pShellLink->SetIconLocation(lpss->pszIconPath, lpss->wIconIndex);
- //取得IPersistFile接口
- hr = pShellLink->QueryInterface(
- IID_IPersistFile, reinterpret_cast<LPVOID*>(&pPF));
- if(FAILED(hr))
- {
- pShellLink->Release();
- return hr;
- }
- // 保存LNK(Unicode名)
- MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED,
- szLnkFile, -1, wszLnkFile, MAX_PATH);
- hr = pPF->Save(wszLnkFile, TRUE);
- // 清理
- pPF->Release();
- pShellLink->Release();
- return hr;
- }
Shell脚本对象
Shell脚本对象提出了一种操作快捷方式更好的方法。这在IE4.0中引进,并且已经成为Windows98 的标准部件。在坚固的外壳下,他作为自动服务器,给出了建立和解决快捷方式的编程接口(它们还做许多其它有趣的事情…)
最有趣的是这些部件都可以用于桌面应用,HTML页面,和整个Windows脚本环境。 在第十二章中我们将详细讨论它们。
快捷方式正确的命名
在Shell的4.71版本以后,一个称之为SHGetNewLinkInfo()的新函数对程序员是可用的。然而与你所希望的不同,这个函数不能建立快捷方式。相反,它的用途在于为快捷方式安排一个正确的名字:
- BOOL SHGetNewLinkInfo(LPCTSTR pszLinkTo,
- LPCTSTR pszDir,
- LPTSTR pszName,
- BOOL* pfMustCopy,
- UINT uFlags);
这个函数接受路径名的指针或者目标对象的PIDL,这个参数存储在pszLinkTo之中。uFlags值指明它是PIDL还是路径名。目标文件夹是pszDir。
这个例程将给出正在建立的快捷方式文件的名字。这个名字由pszName参量返回,并假设其缓冲长度为MAX_PATH字符数。当你对已经存在的快捷方式建立快捷方式时,Shell并不建立新的连接,而是,简单地拷贝和修改这个目标。pfMustCopy就用于这个目的,它返回一个布尔值来表示Shell是建立了一个快捷方式文件还是处理了一个拷贝,TRUE表示pszLinkTo是一个已存在的快捷方式,此时Shell只拷贝和适当地修改它,FALSE则是建立一个全新的快捷方式。最后的可用标志是:
标志 | 描述 |
SHGNLI_PIDL | 如果设置,pszLinkTo变量将作为PIDL而不是串来考虑 |
SHGNLI_NOUNIQUE | 如果设置,Shell将首先确定快捷方式的名字,而后检查可能的冲突,如果名字与同文件夹中的另一个发生冲突,就重复操作,直到找出唯一的名字为止。 |
SHGNLI_PREFIXNAME | 如果设置,名字将总是有一个‘快捷方式到’的前缀 |
事实上,SHGetNewLinkInfo()函数努力为快捷方式提供与给定目标一致的名字。例如,对于指向DOS可执行文件,将给出.pif扩展名,否则将给出.lnk扩展名。这个函数所执行的另一个检查是关于目标驱动器是否支持长文件名。如果不支持,则函数返回8.3格式的名字。
删除快捷方式
删除快捷方式与删除文件一样容易。更重要的是不必考虑它所指向文件的命运,因为你仅仅删除了它的引用。被指向的目标完全不受影响。
解析快捷方式
建立快捷方式仅仅完成了工作的一半,很快你就会被读出快捷方式文件的内容弄糊涂。解析快捷方式并不是不同于读文件,但是这个操作一般称之为‘解析’而不是‘读’。有理由说明它们在概念上的差异,快捷方式指向一个文件对象,但是这只是一个连接—不是嵌入的。在建立快捷方式的时候,假定对象是存在的,但是在读它的时候并没有这个假设。当需要访问被引用对象时,没有东西来保证它不被删除,移动,或重命名。
读一个快捷方式简单地说明你试图访问这个.lnk文件指定的对象。解析快捷方式则说明系统将试图了解被引用对象已经移动到了什么地方,或它是怎样被重命名的。
探测器怎样解析快捷方式
我们说,解析快捷方式是从读开始的。然而,如果探测器在.lnk文件指定的位置找不到有效的文件对象,则它将在所有驱动器和磁盘目录上执行递归搜索,直到找到具有相同尺寸,建立日期以及与快捷方式指向的文件一样属性的文件为止。如果搜索失败,探测器将显示如下对话框:
这个对话框可以通过适当设置IShellLink::Resolve()的标志加以抑制。当然,如果你已经删除了引用对象,探测器也就不可能找到它们,即使它们仍然在‘回收站’中,也不能。
解析快捷方式的函数
Shell API 也缺少解析快捷方式的函数,所以还是需要我们自己写。相关的步骤是:
- 建立必要的COM服务器
- 取得IPersistFile接口指针
- 使用IPersistFile的方法从.lnk文件装入快捷方式
- 取得IShellLink接口指针
- 解析这个快捷方式
整个操作的核心是Resolve()。语法如下:
- HRESULT IShellLink::Resolve(HWND hwnd, DWORD fFlags);
头一个参数是一个父窗口的Handle,函数利用它来显示任何需要显示的对话框。另一个是dwFlags变量,它可以是下列值得组合:
标志 | 描述 |
SLR_NO_UI | 函数不显示任何对话框,即使查找指向文件失败。此时函数在默认的3秒钟之后返回。超时时间可以通过这个变量的高位字客户化地指定所期望的毫秒数。 |
SLR_ANY_MATCH | 试着解析这个连接,在失败时显示对话框 |
SLR_UPDATE | 如果设置这个标志,并且引用的对象已经被移动或重命名,则快捷方式被更新到指向新的位置,这个行为不是默认的。 |
注意,更新快捷方式,使其指向新位置上的文件对象(如果有)的行为不是自动的,必须通过传递SLR_UPDATE标志到IShellLink::Resolve()函数显式地请求。下面是SHResolveShortcut()函数的源代码,与它的姊妹例程SHCreateShortcutEx()一样,它在我们说明快捷方式编程的示例程序中被广泛地使用。
- HRESULT SHResolveShortcut(LPCTSTR szLnkFile, LPSHORTCUTSTRUCT lpss)
- {
- WCHAR wszLnkFile[MAX_PATH] = {0};
- IShellLink* pShellLink = NULL;
- IPersistFile* pPF = NULL;
- // 建立合适的COM服务器
- HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL,CLSCTX_INPROC_SERVER,
- IID_IShellLink,reinterpret_cast<LPVOID*>(&pShellLink));
- if(FAILED(hr))
- return hr;
- // 取得装入.LNK 文件的IPersistFile接口
- hr = pShellLink->QueryInterface(IID_IPersistFile,
- reinterpret_cast<LPVOID*>(&pPF));
- if(FAILED(hr))
- {
- pShellLink->Release();
- return hr;
- }
- // 装入快捷方式(Unicode 名)
- MultiByteToWideChar(CP_ACP, 0, szLnkFile, -1, wszLnkFile, MAX_PATH);
- hr = pPF->Load(wszLnkFile, STGM_READ);
- if(FAILED(hr))
- {
- pPF->Release();
- pShellLink->Release();
- return hr;
- }
- // 解析连接
- hr = pShellLink->Resolve(NULL, SLR_ANY_MATCH);
- if(FAILED(hr))
- {
- pPF->Release();
- pShellLink->Release();
- return hr;
- }
- // 抽取信息,充填到lpss
- if(lpss != NULL)
- {
- TCHAR szPath[MAX_PATH] = {0};
- TCHAR szDesc[MAX_PATH] = {0};
- TCHAR szIcon[MAX_PATH] = {0};
- WORD w = 0;
- WORD wIcon = 0;
- WIN32_FIND_DATA wfd;
- pShellLink->GetPath(szPath, MAX_PATH, &wfd, SLGP_SHORTPATH);
- pShellLink->GetDescription(szDesc, MAX_PATH);
- pShellLink->GetHotkey(&w);
- pShellLink->GetIconLocation(szIcon, MAX_PATH,
- reinterpret_cast<int*>(&wIcon));
- lpss->pszTarget = szPath;
- lpss->pszDesc = szDesc;
- lpss->pszIconPath = szIcon;
- lpss->wHotKey = w;
- lpss->wIconIndex = wIcon;
- }
- pPF->Release();
- pShellLink->Release();
- return hr;
- }
在装入文件时,我们使用了IPersistFile接口的Load()方法,它有两个变量,Unicode格式的.Lnk文件名,和表示要打开文件的访问模式的参数。
快捷方式与特殊文件夹
在绝大多数情况下,如果需要编程建立快捷方式,你都需要在一个特殊文件夹中建立它。然而,这并不复杂—仅仅是要指定这个文件夹的正确路径而已。在下一节将要讨论的程序中,就允许你在很多这种通常的特殊文件夹:‘我的文档’,‘桌面’,‘开始菜单’,‘程序’,‘发送到’和‘Favorites’中建立快捷方式。考虑第五章中的SHGetSpecialFolderPath()函数,它正好能发现非虚拟文件夹的路径。
示例程序:快捷方式管理
下图所看到的应用是一个建立和解析快捷方式的控制板管理器。它的对话框窗口分成两个部分:上面部分是解析快捷方式,下面的则是建立新的快捷方式。
这个用户界面使你能够选择打开.Lnk文件,并且可以拉动目标—即,支持拖拽快捷方式,和解析快捷方式。在这个程序中解析的每一个快捷方式都将在观察中列出报告。这里开发的例子将仅仅显示目标,描述和热键信息,要进一步增强它的功能,对于你不应该是太大的问题。
选择一个快捷方式
头一个必须考虑的问题是安排一个‘打开’对话框来选择要解析的快捷方式。麻烦是,在默认情况下,‘打开’对话框不能处理快捷方式,因此没有任何.lnk文件的名字被返回。为了在这种环境下工作,你必须在GetOpenFileName()函数中指定OFN_NODEREFERENCELINKS标志。就象下面显示的处理器函数那样,它在应用对话框上安排两个浏览按钮:
- void OnBrowse(HWND hDlg, WPARAM wID)
- {
- TCHAR szFile[MAX_PATH] = {0};
- OPENFILENAME ofn;
- ZeroMemory(&ofn, sizeof(OPENFILENAME));
- ofn.lStructSize = sizeof(OPENFILENAME);
- if(wID == IDC_SHORTCUT)
- {
- ofn.lpstrFilter = __TEXT("Shortcuts/0*.lnk/0");
- ofn.Flags = OFN_NODEREFERENCELINKS;
- }
- else
- ofn.lpstrFilter = __TEXT("All files/0*.*/0");
- ofn.nMaxFile = MAX_PATH;
- ofn.lpstrInitialDir = __TEXT("c://");
- ofn.lpstrFile = szFile;
- if(!GetOpenFileName(&ofn))
- return;
- else
- SetDlgItemText(hDlg, wID, ofn.lpstrFile);
- return;
- }
使用这个技术,如果你双击一个.lnk文件,探测器将停在那儿,并返回一个指向文件的名,而不是进入引用的文件。
Shell拖拽
我承认,即使我们是对Shell进行编程,拖拽也不是一个有重要关系的科目,但是在我们看到它操作的过程之后,这就显得有价值了。VC++资源编辑器就有拉动目标的特征(通过打开WS_EX_ACCEPTFILES位)。当然,它也能使你来规划怎样或什么时候可以处理拉动事件。我们想要限制列表观察的拖拽操作,但是如果你指派了这个特征,则我们所要面对的是必须子类化这个窗口,以便感知相关的拉动事件。反过来,我们希望使用较简单的方法:整个对话框都将可以拉动,但是当它捕捉到消息WM_DROPFILES后,应该校验列表观察中所发生的事件,否则,忽略这个事件。Shell处理拖拽的函数都定义在shellapi.h中,有DragQueryPoint(),DragQueryFile()和DragFinish(),后面我们还将继续讨论这个课题。
显示结果
这个程序在用户界面上有一个报告风格的列表观察,为了较容易的使用它,我们建立了两个辅助函数来帮助在观察中增加列和串。记住以后我们还会使用它们。
第一个函数是MakeReportView(),它转换列表观察窗口到具有指定列的报告风格的列表观察。函数原型要求传递一个列表观察的Handle,一个带有字符串名和列宽度值的数组,以及一个列数值。为了使函数尽量简洁,我们假定,数组中偶位置为名字串,奇位置为数字。
数组实际是一个串指针数组—即,一个32位值的数组,理解了这个假设之后,你就可以如下使用数组了:
- LPTSTR psz[] = {"Target", reinterpret_cast<TCHAR*>(170),
- "Description", reinterpret_cast<TCHAR*>(170),
- "Hotkey", reinterpret_cast<TCHAR*>(100)};
- MakeReportView(hwndList, psz, 3);
MakeReportView()总是以名字/宽度对方式处理数组元素,所以,列数总是等于数组尺寸的一半。
- void MakeReportView(HWND hwndList, LPTSTR* psz, int iNumOfCols)
- {
- RECT rc;
- DWORD dwStyle = GetWindowStyle(hwndList);
- SetWindowLong(hwndList, GWL_STYLE, dwStyle | LVS_REPORT);
- GetClientRect(hwndList, &rc);
- // 处理元素对,数组尺寸假设为 2 * iNumOfCols
- for(int i = 0 ; i < 2 * iNumOfCols ; i = i + 2)
- {
- LV_COLUMN lvc;
- ZeroMemory(&lvc, sizeof(LV_COLUMN));
- lvc.mask = LVCF_TEXT | LVCF_WIDTH;
- lvc.pszText = psz[i];
- if(reinterpret_cast<int>(psz[i + 1]) == 0)
- lvc.cx = rc.right / iNumOfCols;
- else
- lvc.cx = reinterpret_cast<int>(psz[i + 1]);
- ListView_InsertColumn(hwndList, i, &lvc);
- }
- return;
- }
MakeReportView()的伴随例程是AddStringToReportView(),它添加新行到指定的列表观察。由于底层编程接口的原因,充填报告风格列表观察所有列要求分几个步骤。你应该为这个新项的第一列(主列)添加指定的文字,然后依次在其他列上设置文字。所有这些步骤都由AddStringToReportView()执行,你只需要传递一个包含所有子串的NULL分隔串和在iNumOfCols指出有多少列就可以了。
- void AddStringToReportView(HWND hwndList, LPTSTR psz, int iNumOfCols)
- {
- LV_ITEM lvi;
- ZeroMemory(&lvi, sizeof(LV_ITEM));
- lvi.mask = LVIF_TEXT;
- lvi.pszText = psz;
- lvi.cchTextMax = lstrlen(psz);
- lvi.iItem = 0;
- ListView_InsertItem(hwndList, &lvi);
- // 其它列
- for(int i = 1 ; i < iNumOfCols ; i++)
- {
- psz += lstrlen(psz) + 1;
- ListView_SetItemText(hwndList, 0, i, psz);
- }
- return;
- }
在本例中列表观察有三列:‘目标’,‘描述’和‘热键’。前两列是直接的,而第三列使用了以前没用过的通用控件的一个用法。这是需要进一步说明的。
热键通用控制
Windows95引进了一个新的控件,可以使你能够图像方式选择一个键的组合(如图):
使用这个控件的方法是,敲击按键组合,它解释键盘码,并转换成相应文字,显然,在建立快捷方式环境下,这个控件有更友善的用户界面。
反过来,在解析快捷方式时,你所有的全部仅是一个由IShellLink::GetHotkey()返回的数(精确地:DWORD),这需要把它转换成一个友善格式的串。
这个字分解成两个字节,表示一个热键,高字节是修改符(Alt,Ctrl,Shift,或三者的组合),低字节是你所敲击的键码。注意,如果你按了A,这个码是65 (大写字符),不是97(小写字符)。要使用HotkeyToString()例程,你需要相对于某些已知常量检查高字节的位。下面这个函数对此进行了处理:
- void HotkeyToString(WORD wHotKey, LPTSTR pszBuf)
- {
- BYTE bKey = LOBYTE(wHotKey);
- BYTE bMod = HIBYTE(wHotKey);
- if(bMod & HOTKEYF_CONTROL)
- lstrcpy(pszBuf, __TEXT("Ctrl"));
- if(bMod & HOTKEYF_SHIFT)
- if(lstrlen(pszBuf))
- lstrcat(pszBuf, __TEXT(" + Shift"));
- else
- lstrcpy(pszBuf, __TEXT("Shift"));
- if(bMod & HOTKEYF_ALT)
- if(lstrlen(pszBuf))
- lstrcat(pszBuf, __TEXT(" + Alt"));
- else
- lstrcpy(pszBuf, __TEXT("Alt"));
- TCHAR s[2] = {0};
- wsprintf(s, __TEXT("%c"), bKey);
- if(lstrlen(pszBuf))
- {
- lstrcat(pszBuf, __TEXT(" + "));
- lstrcat(pszBuf, s);
- }
- else
- lstrcpy(pszBuf, s);
- }
HotkeyToString()函数接受热键值和一个要充填返回结构的串缓冲。它检查修改器和建立串的第一部分—如,Ctrl+Alt,然后通过关联的按键字符完成这个操作—如,Ctrl+Alt+X。下图中显示了在解析快捷方式时应用的结果:
收集建立变量
如果不是有点微妙的话,建立快捷方式对话框的这一部分没有什么可值得注意的。打开一个已存在的快捷方式(使用在桌面上的一个是比较好的),试图给它分配一个新的热键,此时,你将发现热键控件校正你的按键。A将变成Ctrl+Alt+A。
正是这个特征,而且是个重要特征,因为,如果你试图编程地分配一个非Ctrl+Alt+…形式的热键,这个热键将永远不被识别。稍微考虑一下,你就会明白,这种行为不是串—Ctrl+Alt+ … 与可能的加速器冲突。这也使我明白了,在前几个例子中为什么Alt+A是错误的了。
给出热键规则
指令热键控件自动置换某些错误或无效的按键组合,你需要使用按键规则。不管它的名字如何,这其实就是简单地发送消息到热键窗口。为了强制使它接受仅仅Ctrl+Alt前缀的按键,你必须:
- SendMessage(hwndHotkey, HKM_SETRULES,
- HKCOMB_NONE | HKCOMB_S | HKCOMB_A | HKCOMB_C,
- HOTKEYF_CONTROL | HOTKEYF_ALT);
这个‘规则’可以重新解释为:
无效的按键组合总是那些有一个修改符在wParam中列出的按键。
用在lParam中指定的组合键置换每一个无效的按键。
如果不是从空(HKCOMB_NONE),Shift(HKCOMB_S), Alt (HKCOMB_A) 或 Ctrl(HKCOMB_C)开始,则忽略它们,并用Ctrl+Alt代替之。下面图像显示了建立快捷方式时的这个过程:
源代码
现在看一下这个示例程序的剩余源代码。要正确地编译它必须确保包含shlobj.h, resource.h 和commdlg.h,以及链接comdlg32.lib和ole32.lib。另外,由于我们使用了COM,所以还需要用CoInitialize(NULL)和CoUninitialize()把WinMain()中的DialogBox()调用括起来。
DoCreateShortcut()函数
这个函数在点击‘建立’按钮时被调用。它从控件中收集参数和安排调用SHCreateShortcutEx()函数,在combo框中有一些特殊文件夹的名字。
- void DoCreateShortcut(HWND hDlg)
- {
- SHORTCUTSTRUCT ss;
- ZeroMemory(&ss, sizeof(SHORTCUTSTRUCT));
- TCHAR szTarget[MAX_PATH] = {0};
- TCHAR szDesc[MAX_PATH] = {0};
- // 取得热键
- ss.wHotKey = static_cast<WORD>(SendDlgItemMessage(
- hDlg, IDC_HOTKEY, HKM_GETHOTKEY, 0, 0));
- // 取得目标和描述
- GetDlgItemText(hDlg, IDC_TARGET, szTarget, MAX_PATH);
- GetDlgItemText(hDlg, IDC_DESCRIPTION, szDesc, MAX_PATH);
- ss.pszTarget = szTarget;
- ss.pszDesc = szDesc;
- // 确定快捷方式文件名
- // 取得目标文件夹和最后的反斜杠
- HWND hwndCbo = GetDlgItem(hDlg, IDC_SPECIAL);
- int i = ComboBox_GetCurSel(hwndCbo);
- DWORD nFolder = ComboBox_GetItemData(hwndCbo, i);
- TCHAR szPath[MAX_PATH] = {0};
- SHGetSpecialFolderPath(hDlg, szPath, nFolder, FALSE);
- if(szPath[lstrlen(szPath) - 1] != '//')
- lstrcat(szPath, __TEXT("//"));
- TCHAR szLnkFile[MAX_PATH] = {0};
- GetDlgItemText(hDlg, IDC_LNKFILE, szLnkFile, MAX_PATH);
- lstrcat(szPath, szLnkFile);
- lstrcat(szPath, __TEXT(".lnk"));
- // 建立
- SHCreateShortcutEx(szPath, &ss);
- // 更新UI
- SetDlgItemText(hDlg, IDC_SHORTCUT, szPath);
- return;
- }
DoResolveShortcut()函数
这个函数在响应‘解析’按钮的点击时调用。尽管它也接受一个附加的参数pszFile,用于表示要解析的文件。如果这个参数为NULL,则函数使用‘快捷方式’编辑框中的内容。这个变量存在的原因是使这个函数更容易解析拖拽到程序窗口上的任何文件。DoResolveShortcut()首先调用我们的函数SHResolveShortcut()解析这个快捷方式,然后更新用户界面,附加一个新行到报告列表观察中。
- void DoResolveShortcut(HWND hDlg, LPTSTR pszFile)
- {
- TCHAR szLnkFile[MAX_PATH] = {0};
- if(pszFile == NULL)
- GetDlgItemText(hDlg, IDC_SHORTCUT, szLnkFile, MAX_PATH);
- else
- lstrcpy(szLnkFile, pszFile);
- // 解析快捷方式
- SHORTCUTSTRUCT ss;
- HRESULT hr = SHResolveShortcut(szLnkFile, &ss);
- if(FAILED(hr))
- return;
- //
- // 更新UI
- // 建立列表观察串
- TCHAR pszBuf[1024] = {0};
- LPTSTR psz = pszBuf;
- lstrcpy(psz, ss.pszTarget);
- lstrcat(psz, __TEXT("/0"));
- psz += lstrlen(psz) + 1;
- lstrcpy(psz, ss.pszDesc);
- lstrcat(psz, __TEXT("/0"));
- psz += lstrlen(psz) + 1;
- // Try to get the text version of the hotkey
- TCHAR szKey[30] = {0};
- HotkeyToString(ss.wHotKey, szKey);
- lstrcpy(psz, szKey);
- lstrcat(psz, __TEXT("/0"));
- // 加一个新项到报告列表观察(3 列)
- HWND hwndList = GetDlgItem(hDlg, IDC_VIEW);
- AddStringToReportView(hwndList, pszBuf, 3);
- return;
- }
HandleFileDrop()函数
在响应WM_DROPFILES消息时调用此函数,这个函数定义了当用户拖拽文件到窗口的客户区域时所要求的操作。接受的数据是CF_HDROP类型的,这是一种从探测器窗口或从桌面拖拽文件操作时用于Shell移动文件环境下的交互格式。任何具有WS_EX_ACCEPTFILES风格设置的窗口都只对拖拽操作敏感,并以这种格式封装数据。换句话说,当源是Windows Shell,或其它以CF_HDROP格式传递参数的程序时,我们的程序也接受拖拽操作。
CF_HDROP是一种剪裁板格式,用于交换基本为文件名的数据项的格式—更多关于剪裁板格式信息和CF_HDROP数据的内部结构可以参看VC++的帮助文件。对于我们而言,重要的是,虽然它的内存Handle称为CF_HDROP,还是有一定数量的函数能够读出这种格式的数据。
当你从Shell拉动文件的时候,目标窗口接收到消息WM_DROPFILES,其中一个变量是HDROP型的Handle。我们的HandleFileDrop()函数首先检查拉动发生的窗口,如果这个窗口是列表观察,则进一步抽取和解析各种文件名。你可以拉动任何文件到这个列表观察上,但是仅仅快捷方式被正确地处理。
- void HandleFileDrop(HWND hDlg, HDROP hDrop)
- {
- // 检查拉动到的窗口
- POINT pt;
- DragQueryPoint(hDrop, &pt);
- ClientToScreen(hDlg, &pt);
- HWND hwndDrop = WindowFromPoint(pt);
- if(hwndDrop != GetDlgItem(hDlg, IDC_VIEW))
- {
- Msg(__TEXT("抱歉,你必须拉动到列表观察控件上!"));
- return;
- }
- // 检查文件
- int iNumOfFiles = DragQueryFile(hDrop, -1, NULL, 0);
- for(int i = 0 ; i < iNumOfFiles; i++)
- {
- TCHAR szFileName[MAX_PATH] = {0};
- DragQueryFile(hDrop, i, szFileName, MAX_PATH);
- DoResolveShortcut(hDlg, szFileName);
- }
- DragFinish(hDrop);
- }
DragQueryPoint()告知拉动发生时点的客户区域坐标,而DragQueryFile()则依次抽取所有包装在HDROP Handle中的文件。你也可以使用这个函数获得拉动的文件数。最后,必须调用DragFinish()函数来结束拉动操作。
APP_DlgProc()函数
这是应用主窗口过程,由于涉及到我们前面给出的几个例子,所以看一下这个处理器是有价值的:
- BOOL CALLBACK APP_DlgProc(HWND hDlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
- {
- switch(uiMsg)
- {
- case WM_INITDIALOG:
- OnInitDialog(hDlg);
- break;
- case WM_DROPFILES:
- HandleFileDrop(hDlg, reinterpret_cast<HDROP>(wParam));
- break;
- case WM_COMMAND:
- switch(wParam)
- {
- case IDC_RESOLVE:
- DoResolveShortcut(hDlg, NULL);
- return FALSE;
- case IDC_CREATE:
- DoCreateShortcut(hDlg);
- return FALSE;
- case IDC_BROWSE:
- OnBrowse(hDlg, IDC_SHORTCUT);
- return FALSE;
- case IDC_BROWSETARGET:
- OnBrowse(hDlg, IDC_TARGET);
- return FALSE;
- case IDCANCEL:
- EndDialog(hDlg, FALSE);
- return FALSE;
- }
- break;
- }
- return FALSE;
- }
OnInitDialog()函数
在这个工程(project)中有几个东西要初始化。在处理combo框时,应该有一个熟知的过程,我们还需要设置列表观察控件,以及编程热键控件,使其使用Ctrl+Alt…的格式形式。
- void OnInitDialog(HWND hDlg)
- {
- // 设置图标(T/F 大/小图标)
- SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
- SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
- // 初始化报告观察
- HWND hwndList = GetDlgItem(hDlg, IDC_VIEW);
- LPTSTR psz[] = {"Target", reinterpret_cast<TCHAR*>(170),
- "Description", reinterpret_cast<TCHAR*>(170),
- "Hotkey", reinterpret_cast<TCHAR*>(100)};
- MakeReportView(hwndList, psz, 3);
- // 可用的特殊文件夹
- HWND hwndCbo = GetDlgItem(hDlg, IDC_SPECIAL);
- int i = ComboBox_AddString(hwndCbo, "Desktop");
- ComboBox_SetItemData(hwndCbo, i, CSIDL_DESKTOP);
- i = ComboBox_AddString(hwndCbo, "Favorites");
- ComboBox_SetItemData(hwndCbo, i, CSIDL_FAVORITES);
- i = ComboBox_AddString(hwndCbo, "Programs");
- ComboBox_SetItemData(hwndCbo, i, CSIDL_PROGRAMS);
- i = ComboBox_AddString(hwndCbo, "My Documents");
- ComboBox_SetItemData(hwndCbo, i, CSIDL_PERSONAL);
- i = ComboBox_AddString(hwndCbo, "SendTo");
- ComboBox_SetItemData(hwndCbo, i, CSIDL_SENDTO);
- i = ComboBox_AddString(hwndCbo, "Start Menu");
- ComboBox_SetItemData(hwndCbo, i, CSIDL_STARTMENU);
- ComboBox_SetCurSel(hwndCbo, 0);
- // 初始化热键控件,每一件东西都有Ctrl+Alt前缀
- SendDlgItemMessage(hDlg, IDC_HOTKEY, HKM_SETRULES,
- HKCOMB_NONE | HKCOMB_S | HKCOMB_A | HKCOMB_C,
- HOTKEYF_CONTROL | HOTKEYF_ALT);
- SetDlgItemText(hDlg, IDC_TARGET, __TEXT("C://"));
- }
在系统文件夹中建立快捷方式
现在可以编译和运行的这个示例程序可以使你很容易在系统文件夹中建立快捷方式—所需要做的全部操作就是从combo框中选择一个文件夹名。如果你希望在你自己的程序中静默地做这项工作,只要知道所涉及的文件夹名,剩下的就是用这个全路径名格式化一个串。
下面是做这项工作的一个简单的函数。作为变量,它接受要建立的.lnk文件名,特殊文件夹的ID(有CSIDL_XXX格式的常量),以及指向的文件名。代码是SHCreateShortcutEx()的一个封装:
- HRESULT SHCreateSystemShortcut(LPCTSTR szLnkFile, int nFolder, LPCTSTR szFile)
- {
- WCHAR wszLnkFile[MAX_PATH] = {0};
- TCHAR szPath[MAX_PATH] = {0};
- IShellLink* pShellLink = NULL;
- IPersistFile* pPF = NULL;
- // 建立适当的COM服务器
- HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL,
- CLSCTX_INPROC_SERVER, IID_IShellLink,
- reinterpret_cast<LPVOID*>(&pShellLink));
- if(FAILED(hr))
- return hr;
- // 设置属性
- pShellLink->SetPath(szFile);
- // 取得IPersistFile接口,用于保存
- hr = pShellLink->QueryInterface(
- IID_IPersistFile, reinterpret_cast<LPVOID*>(&pPF));
- if(FAILED(hr))
- {
- pShellLink->Release();
- return hr;
- }
- // 准备快捷方式名
- SHGetSpecialFolderPath(NULL, szPath, nFolder, FALSE);
- if(szPath[lstrlen(szPath) - 1] != '//')
- lstrcat(szPath, __TEXT("//"));
- lstrcat(szPath, szLnkFile);
- // 存储LNK(Unicode 名)
- MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, szPath, -1, wszLnkFile, MAX_PATH);
- hr = pPF->Save(wszLnkFile, TRUE);
- // 清理
- pPF->Release();
- pShellLink->Release();
- return hr;
- }
使用上面的函数,在‘桌面’上,在‘开始’菜单中,在‘程序文件’中,或在‘Favorites’中建立快捷方式就非常容易了。要证明这一点,要求在‘开始’菜单中添加一个新项指向‘记事本’,则只需要:
- SHCreateSystemShortcut(__TEXT("Notepad.lnk"), CSIDL_STARTMENU,
- __TEXT("c://windows//notepad.exe"));
就可以了,显然,c:/windows/路径应该置换成你机器上的实际Windows的目录。还要注意,在Windows NT下,‘notepad.exe’存储在‘System’目录下。
你也可以建立指向目录和非可执行文件的快捷方式。事实上,引用任何系统文件对象,仅仅需要传递路径到IShellLink::SetPath(),或传递PIDL调用IShellLink::SetIDList()。
‘发送到’文件夹
‘发送到’文件夹是包含两个非快捷方式对象的文件夹,对于快捷方式是不值得注意的。如果在Windows上安装了IE4.0以上版,或在Windows98以上版系统上,‘发送到’文件夹可能包含对Email容器或桌面的引用。使用这个机理,你也可以从Shell把给定的文件作为新消息的附件直接发送到你自己的发件箱,或作为快捷方式发送到桌面。
这个截图显示有两个没有典型的快捷标记的项,它们是‘桌面快捷方式’,是一个空的.DeskLink文件,其长度为0字节。如果你搜索这个扩展名的注册表信息,就会发现,在其后有一个COM对象。
了解到它由一个COM对象支持文件是一大进步,但是,是哪一种COM对象在支持它,COM对象实现了什么接口,都还需要进一步探讨。事实上,这是一个Shell扩展,更确切地讲是一个拖动处理器。我们将在第十五章中讨论Shell扩展。现在,仅说明‘发送到’文件夹不仅包含快捷方式还有别的就可以了。使用.DeskLink串纯粹是一种表示,你也可以使用其它串来表示。
‘最近文档’文件夹
‘最近文档’文件夹收集最近打开的文档。这个目录下的内容可以通过单击‘开始’菜单的‘文档’项来查看,其物理位置在Windows目录下。然而,奇怪的是在它包含的快捷方式和通过菜单显示的项之间并不是1:1对应的。
Shell API给出了一个称之为SHAddToRecentDocs()的函数,使程序员能够把文档的链接存储到这个文件夹下。
- void SHAddToRecentDocs(UINT uFlags, LPCVOID pv);
第一个变量指出了第二个变量的类型:PIDL或路径名,可以取SHARD_PATH或SHARD_PIDL值。使用这个函数,能够成功地把你的文档引用加进菜单中。但是如果简单地在这个文件夹上建立一个快捷方式,就不对了—也就是说,建立快捷方式是必要的但不充分。SHAddToRecentDocs()显然做了更多的事情。
最终,SHAddToRecentDocs()添加项目到由‘开始菜单’使用的MRU(最近使用的)列表中,并且简单地加这个文件到‘最近文档’文件夹中。这个函数还复制这个文件夹中的快捷方式,以及处理菜单的顺序。因此,你应该坚定地使用函数,而不是其他方法,以兼容未来在某些方面实现方式的变化。
小结
这一章中探讨了快捷方式,这个在Windows的任何书和文章中都讨论过的课题。快捷方式是相对简单的,但是没有建立和解析它的单一函数,这一章中讨论并写出了这样的函数,还查看了:
快捷方式的作用
怎样建立和解析它们
某些与快捷方式一道工作的有用的函数
拖拽和热键控件
快捷方式和系统文件夹之间的关系