返回

Windows快捷方式目标选择器:C++自定义实现指南

windows

探寻快捷方式的“芳踪”:打造你自己的位置选择器

用过 CreateFile 的伙计们都晓得,调用它之前,你得先拿到文件名。这事儿简单,你可以自个儿捣鼓一个对话框:列举个驱动器、文件夹,或者用 Shell 的命名空间溜达溜达,让用户能选个文件,然后把对话框一关,文件名就到手了。

不过,造轮子的活儿,Windows 大哥早就替咱们干了不少。它提供了一个叫 IFileOpenDialog 的通用对话框,UI 交互啥的都给你弄得明明白白。

图片

这玩意儿用起来是挺舒坦。但问题来了:

创建快捷方式有类似的“通用”对话框吗?

Windows 资源管理器里头,也有一个向导,一步步引你创建一个指向文件、文件夹、项目、URL 等等目标的快捷方式:

图片

这个创建快捷方式的向导,它是不是一个“通用”对话框,能让咱们的应用程序也调来用用呢?

提醒一下:咱的目标不是 去调用那个向导——因为那个向导会直接在硬盘上把链接文件给创建出来。咱不想要它在硬盘上存东西。咱需要的是获取 用户输入的结果,比如:

  • 一个 IShellLink 对象实例
  • 或者一个 IUniformResourceLocator 对象实例
  • 再不济,就是用户输入的“位置”(Target Location)和“标题”(Shortcut Name)。

说白了,咱需要一个“位置选择器”那样的用户界面。

剖析症结:为何“此路不通”?

直接说答案:Windows 资源管理器那个“创建快捷方式”的向导,它不是一个能被第三方应用程序直接调用的“通用对话框”

为啥呢?

  1. 设计意图不同IFileOpenDialog 这类通用对话框,设计初衷就是为了给应用程序提供一个标准的、可复用的UI组件,用来获取用户选择的文件或文件夹路径。而资源管理器的“创建快捷方式”向导,它是一个集成在 Shell 内部的功能模块,目的是完成“创建并保存 .lnk 文件”这一完整操作。它的输出是实实在在的文件,而不是供程序使用的数据。
  2. 封装程度 :通用对话框通常以 COM 接口(如 IFileOpenDialog)或 API 函数的形式暴露给开发者。资源管理器的向导,并没有提供这样的公共接口。你或许能通过一些取巧的办法(比如模拟键盘鼠标操作,或者启动某个特定的 Rundll32 命令)来“唤起”它,但这种方式非常不稳定,而且没法拿到它内部的数据。
  3. 交互的封闭性 :即使用某种方式调出了向导,你也无法在它创建快捷方式文件之前拦截到用户输入的目标路径、快捷方式名称等信息。它是一个“黑盒”,要么完整执行完毕,要么中途取消。

既然此路不通,那咱就得另辟蹊径,自己动手丰衣足食了。

柳暗花明:构建你的专属“位置选择器”

虽然没有现成的“快捷方式信息拾取”通用对话框,但咱们完全可以组合利用现有的工具,或者干脆自己动手打造一个。

方案一:借力 IFileOpenDialog (主要获取目标路径)

IFileOpenDialog 不仅仅能选文件,稍加配置,它也能选文件夹,甚至接受任何文本输入作为路径。这能帮我们解决“目标位置”的拾取问题。

  • 原理和作用
    利用 IFileOpenDialog 的灵活性。我们可以设置对话框的选项,允许用户选择文件、文件夹,或者直接输入一个网络路径、URL。
    当用户确认选择后,对话框会返回用户选定的路径字符串。

  • 代码示例 (C++)
    这里展示如何使用 IFileOpenDialog 来获取一个目标路径。

    #include <windows.h>
    #include <shobjidl.h> // For IFileOpenDialog
    #include <atlbase.h>  // For CComPtr
    
    // 调用这个函数会弹出一个对话框,让用户选择目标
    // 成功则返回用户选择的路径,失败返回空字符串
    CString SelectTargetLocation() {
        CString selectedPath = L"";
        CComPtr<IFileOpenDialog> pFileOpen;
    
        // 创建 FileOpenDialog 实例
        HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL,
                                      IID_IFileOpenDialog, reinterpret_cast<void**>(&pFileOpen));
    
        if (SUCCEEDED(hr)) {
            // 设置对话框选项
            // FOS_PICKFOLDERS 允许选择文件夹
            // FOS_PATHMUSTEXIST 可以校验路径是否存在 (根据需求决定是否添加)
            // FOS_NOVALIDATE 对于URL或者不存在但合法的路径,可能需要这个
            DWORD dwOptions;
            pFileOpen->GetOptions(&dwOptions);
            pFileOpen->SetOptions(dwOptions | FOS_PICKFOLDERS | FOS_NOCHANGEDIR /* | FOS_FORCEFILESYSTEM  如果只想选文件系统内的东西 */);
    
            pFileOpen->SetTitle(L"请选择快捷方式的目标或输入路径");
            pFileOpen->SetOkButtonLabel(L"选定此目标");
    
            // 显示对话框
            hr = pFileOpen->Show(NULL); // 通常传入父窗口句柄
    
            if (SUCCEEDED(hr)) {
                CComPtr<IShellItem> pItem;
                hr = pFileOpen->GetResult(&pItem);
                if (SUCCEEDED(hr)) {
                    PWSTR pszFilePath = NULL;
                    hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); // 获取文件系统路径
                    // 如果目标可能是URL或其他非文件系统路径,可能需要SIGDN_URL或SIGDN_DESKTOPABSOLUTEPARSING
                    // SIGDN_DESKTOPABSOLUTEPARSING 通常能给出用户输入的最原始形态
    
                    if (SUCCEEDED(hr)) {
                        selectedPath = pszFilePath;
                        CoTaskMemFree(pszFilePath);
                    }
                }
            }
        }
        return selectedPath;
    }
    
    int main() { // 示例用法
        CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    
        CString target = SelectTargetLocation();
        if (!target.IsEmpty()) {
            // 在这里,target 变量就包含了用户选择或输入的目标路径
            // 你可以弹出一个简单的输入框让用户输入快捷方式的名称
            // MessageBox(NULL, L"目标路径: " + target, L"获取成功", MB_OK);
    
            // 接下来,可能需要一个输入框来获取快捷方式的名称
            // TCHAR shortcutName[MAX_PATH];
            // ... (代码省略,例如使用 InputBox 或者自定义的小对话框)
        }
    
        CoUninitialize();
        return 0;
    }
    
  • 说明
    上面的代码片段演示了如何使用 IFileOpenDialog 来获取一个用户指定的路径。FOS_PICKFOLDERS 选项使得对话框也能选择文件夹。如果用户在文件名编辑框里输入一个URL,理论上也是可以获取到的(但可能需要配合 FOS_NOVALIDATE 或者用 SIGDN_URL 获取名称)。
    这个方案主要解决了“目标位置”的获取。至于“快捷方式名称/标题”,你可以紧接着弹出一个简单的输入框 (例如 Windows API 的 DialogBox 创建一个带编辑框的简单对话框,或者其他UI库的输入控件) 让用户输入。

  • 进阶使用技巧

    • 过滤器设置 :通过 SetFileTypes 可以限制可选的文件类型,不过对于“快捷方式目标”来说,这可能用处不大,因为目标可以是任意类型。
    • 默认文件夹 :通过 SetFolderSetDefaultFolder 可以指定对话框打开时的初始位置。
    • 选项微调IFileOpenDialog 有很多选项 (FOS_...),可以精细控制其行为。比如 FOS_ALLOWMULTISELECT (允许选择多个目标,可能不适用于此场景)、FOS_NOVALIDATE (不验证用户输入,对URL等非本地路径有用)、FOS_FORCESHOWHIDDEN (强制显示隐藏文件和系统文件)。

方案二:定制专属对话框 (全面掌控)

既然官方没提供,那我们就自己画一个!这能让你完全控制UI布局和交互逻辑,一步到位获取所有需要的信息。

  • 原理和作用
    创建一个自定义的对话框窗口。这个窗口上至少包含以下控件:

    1. 一个文本编辑框,用于用户输入或显示“目标位置”。
    2. 一个“浏览...”按钮,点击后可以调用方案一中提到的 IFileOpenDialog 来帮助用户选择目标位置,并将结果填入文本编辑框。
    3. 另一个文本编辑框,用于用户输入“快捷方式的名称/标题”。
    4. “确定”和“取消”按钮。
  • 操作步骤 (概念性,具体实现依赖UI框架)

    1. 设计UI布局

      • 标签:“目标位置(T):”
      • 编辑框 (ID: IDC_EDIT_TARGET_LOCATION)
      • 按钮:“浏览(B)...” (ID: IDC_BUTTON_BROWSE)
      • 标签:“快捷方式名称(N):”
      • 编辑框 (ID: IDC_EDIT_SHORTCUT_NAME)
      • 按钮:“确定” (ID: IDOK)
      • 按钮:“取消” (ID: IDCANCEL)
    2. 实现对话框逻辑 (以Win32 API + C++ 为例)

      • 在对话框初始化时 (WM_INITDIALOG):
        • 可以给“快捷方式名称”编辑框预填一个基于目标名称的建议值(比如,如果目标是 "C:\MyApp\Run.exe",建议名为 "Run")。
      • 处理“浏览...”按钮点击事件 (WM_COMMAND, LOWORD(wParam) == IDC_BUTTON_BROWSE):
        • 调用类似方案一的 SelectTargetLocation() 函数。
        • 将返回的路径设置到 IDC_EDIT_TARGET_LOCATION 编辑框。
        • 自动更新 IDC_EDIT_SHORTCUT_NAME 编辑框的建议名称 (如果为空或用户未修改过)。
      • 处理“确定”按钮点击事件 (WM_COMMAND, LOWORD(wParam) == IDOK):
        • IDC_EDIT_TARGET_LOCATION 获取目标位置字符串。
        • IDC_EDIT_SHORTCUT_NAME 获取快捷方式名称字符串。
        • 对输入进行必要的验证(比如路径不能为空,名称不能为空等)。
        • 如果验证通过,将获取到的信息保存起来,关闭对话框并返回成功状态 (e.g., EndDialog(hDlg, IDOK)).
      • 处理“取消”按钮点击事件:关闭对话框并返回取消状态。
    // 这只是一个非常简化的伪代码/概念性C++代码,展示对话框消息处理函数的大致结构
    // 完整的Win32对话框代码会更复杂,包括资源文件定义等
    
    // 假设 hDlg 是对话框句柄
    INT_PTR CALLBACK ShortcutInfoDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
        static CString targetPath; // 用于存储从浏览对话框获取的路径
        static CString shortcutName; // 用于存储用户输入的名称
    
        switch (message) {
            case WM_INITDIALOG:
                // 初始化时,可以预设一些值
                // SetDlgItemText(hDlg, IDC_EDIT_TARGET_LOCATION, L"");
                // SetDlgItemText(hDlg, IDC_EDIT_SHORTCUT_NAME, L"");
                return (INT_PTR)TRUE;
    
            case WM_COMMAND:
                if (LOWORD(wParam) == IDC_BUTTON_BROWSE) {
                    // 调用方案一的 SelectTargetLocation() 或类似逻辑
                    CString browsedPath = SelectTargetLocation(); // (需要CoInitialize等)
                    if (!browsedPath.IsEmpty()) {
                        SetDlgItemText(hDlg, IDC_EDIT_TARGET_LOCATION, browsedPath);
                        // 可以尝试从路径中提取文件名作为建议的快捷方式名
                        // TCHAR fname[_MAX_FNAME];
                        // _tsplitpath_s(browsedPath, NULL, 0, NULL, 0, fname, _MAX_FNAME, NULL, 0);
                        // if (_tcslen(fname) > 0) {
                        //    SetDlgItemText(hDlg, IDC_EDIT_SHORTCUT_NAME, fname);
                        // }
                    }
                    return (INT_PTR)TRUE;
                }
                if (LOWORD(wParam) == IDOK) {
                    TCHAR buffer[MAX_PATH];
                    GetDlgItemText(hDlg, IDC_EDIT_TARGET_LOCATION, buffer, MAX_PATH);
                    targetPath = buffer;
    
                    GetDlgItemText(hDlg, IDC_EDIT_SHORTCUT_NAME, buffer, MAX_PATH);
                    shortcutName = buffer;
    
                    if (targetPath.IsEmpty() || shortcutName.IsEmpty()) {
                        MessageBox(hDlg, L"目标位置和快捷方式名称不能为空!", L"输入错误", MB_OK | MB_ICONWARNING);
                        return (INT_PTR)TRUE; // 阻止对话框关闭
                    }
    
                    // 把 targetPath 和 shortcutName 存到你的应用程序变量中
                    // ... g_userSelectedTargetPath = targetPath;
                    // ... g_userSelectedShortcutName = shortcutName;
    
                    EndDialog(hDlg, IDOK); // 关闭对话框,返回IDOK
                    return (INT_PTR)TRUE;
                }
                if (LOWORD(wParam) == IDCANCEL) {
                    EndDialog(hDlg, IDCANCEL); // 关闭对话框,返回IDCANCEL
                    return (INT_PTR)TRUE;
                }
                break;
        }
        return (INT_PTR)FALSE; // 未处理的消息
    }
    
    // 调用自定义对话框:
    // INT_PTR result = DialogBox(hInst, MAKEINTRESOURCE(IDD_SHORTCUT_INFO_DIALOG), hWndParent, ShortcutInfoDialogProc);
    // if (result == IDOK) {
    //    // 用户点击了确定,g_userSelectedTargetPath 和 g_userSelectedShortcutName 里有数据
    // }
    
  • 安全建议

    • 输入验证 :对用户在编辑框中输入的内容进行严格验证。例如,目标路径虽然可以是URL,但也应检查其格式是否大致正确。快捷方式名称通常不能包含特殊字符(如 \ / : * ? " < > |)。
    • 路径长度 :注意 MAX_PATH 的限制,虽然现在有长路径支持,但传统快捷方式可能仍受此影响。
  • 进阶使用技巧

    • 参数和工作目录 :如果快捷方式还需要指定命令行参数或工作目录,可以在自定义对话框上增加相应的输入字段。
    • 图标选择 :更高级的功能可以允许用户为快捷方式选择一个自定义图标,这需要一个更复杂的图标选择器。
    • 即时预览/验证 :当用户输入目标路径后,可以尝试在后台轻量级地验证该路径的有效性(比如,文件是否存在,URL是否可访问),并给出提示。

方案三:内存中构建 IShellLink / IUniformResourceLocator

当你通过上述任一方案获取了用户提供的“目标位置”和“快捷方式名称”后,就可以在内存中创建并填充 IShellLink (针对文件/文件夹目标) 或 IUniformResourceLocator (针对URL目标) 对象了。这样,你便拥有了所需的数据结构,而无需在磁盘上实际创建 .lnk.url 文件。

  • 原理和作用
    使用 COM 来创建 Shell Link 或 Internet Shortcut 对象。然后调用它们的接口方法来设置路径、描述(快捷方式名)等属性。这些对象完全存在于内存中,除非你显式调用 IPersistFile::Save,否则不会写入磁盘。

  • 代码示例 (C++)
    假设你已经通过自定义对话框获取了 targetPathshortcutName

    #include <windows.h>
    #include <shobjidl.h>     // IShellLink
    #include <intshcut.h>     // IUniformResourceLocator
    #include <atlbase.h>      // CComPtr
    #include <propkey.h>      // PKEY_Title for IShellLink (optional, description is more common)
    
    // targetPath: 用户选择或输入的目标 (e.g., "C:\\path\\to\\file.exe" or "http://example.com")
    // shortcutName: 用户输入的快捷方式名称/描述
    void ProcessShortcutInfo(const CString& targetPath, const CString& shortcutName) {
        CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); // 确保COM已初始化
    
        HRESULT hr;
        bool isUrl = (targetPath.Left(7).CompareNoCase(L"http://") == 0 ||
                      targetPath.Left(8).CompareNoCase(L"https://") == 0 ||
                      targetPath.Left(6).CompareNoCase(L"ftp://") == 0);
    
        if (isUrl) {
            CComPtr<IUniformResourceLocator> pUrlLink;
            hr = CoCreateInstance(CLSID_InternetShortcut, NULL, CLSCTX_INPROC_SERVER,
                                  IID_IUniformResourceLocator, (void**)&pUrlLink);
            if (SUCCEEDED(hr)) {
                hr = pUrlLink->SetURL(targetPath, 0);
                if (SUCCEEDED(hr)) {
                    // IUniformResourceLocator 没有直接的 SetDescription 或 SetName
                    // URL文件的名称由文件名决定。
                    // 如果需要传递“名称”,通常是指如果保存成 .url 文件时,文件名会用这个。
                    // 对于内存中的对象,其“描述”更多的是外部关联的。
                    // 这里,shortcutName 更多的是 UI 上的概念。
                    // 如果要将此信息传递,可能需要封装在一个自定义结构里。
    
                    // 示例:你可以获取它(即使没保存)
                    WCHAR* pwszUrl = NULL;
                    if (SUCCEEDED(pUrlLink->GetURL(&pwszUrl))) {
                        // pwszUrl 就是目标URL
                        // TRACE(L"URL Link Target: %s, Proposed Name: %s\n", pwszUrl, shortcutName);
                        OutputDebugString(L"URL Link Target: ");
                        OutputDebugString(pwszUrl);
                        OutputDebugString(L", Proposed Name: ");
                        OutputDebugString(shortcutName);
                        OutputDebugString(L"\n");
                        CoTaskMemFree(pwszUrl);
                    }
                }
            }
        } else { // 假定为文件或文件夹
            CComPtr<IShellLink> pShellLink;
            hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
                                  IID_IShellLink, (void**)&pShellLink);
            if (SUCCEEDED(hr)) {
                hr = pShellLink->SetPath(targetPath);
                if (!shortcutName.IsEmpty()) {
                    // 设置描述,这个通常会作为快捷方式的注释或在某些视图下显示
                    hr = pShellLink->SetDescription(shortcutName);
                }
                // 你还可以设置其他属性:
                // pShellLink->SetArguments(L"-myarg");
                // pShellLink->SetWorkingDirectory(L"C:\\MyAPPDir");
                // pShellLink->SetIconLocation(L"C:\\MyApp\\app.exe", 0);
    
                // 若想进一步获取"Title" (这更像是Shell Item的属性),可能需要IShellItem2
                // IShellLink 的主要用途是定义链接行为,其“名称”是保存为.lnk文件时的文件名
                // 对于一个未保存的IShellLink,shortcutName可以认为是它的“意向名称”
                // 你可以获取其设置的路径等信息:
                WCHAR wszPath[MAX_PATH];
                if (SUCCEEDED(pShellLink->GetPath(wszPath, MAX_PATH, NULL, SLGP_UNCPRIORITY))) {
                    OutputDebugString(L"Shell Link Target: ");
                    OutputDebugString(wszPath);
                    WCHAR wszDesc[INFOTIPSIZE]; // INFOTIPSIZE is defined for description
                    if (SUCCEEDED(pShellLink->GetDescription(wszDesc, INFOTIPSIZE))) {
                         OutputDebugString(L", Description (Name): ");
                         OutputDebugString(wszDesc);
                    }
                    OutputDebugString(L"\n");
                }
    
                // 注意: 到这里,pShellLink 和 pUrlLink 对象都在内存里。
                // 你可以把这些对象传递给其他需要 IShellLink 或 IUniformResourceLocator 的代码,
                // 或者仅仅提取你需要的信息后释放它们。
                // 如果真要保存,才调用 IPersistFile::Save
                /*
                CComQIPtr<IPersistFile> pPersistFile(pShellLink); // or pUrlLink
                if (pPersistFile) {
                    // WCHAR wszSavePath[MAX_PATH];
                    // wsprintf(wszSavePath, L"C:\\path\\to\\save\\%s.%s", shortcutName, isUrl ? L"url" : L"lnk");
                    // hr = pPersistFile->Save(wszSavePath, TRUE);
                }
                */
            }
        }
        if (FAILED(hr)) {
            // 处理错误
            OutputDebugString(L"Failed to process shortcut info.\n");
        }
    
        CoUninitialize();
    }
    
    /* 示例调用:
    int main() { // (接上面的SelectTargetLocation和自定义对话框部分)
        // ... (通过UI获取了 userTargetPath 和 userShortcutName)
        CString userTargetPath = L"C:\\Windows\\notepad.exe";
        CString userShortcutName = L"My Notepad";
        ProcessShortcutInfo(userTargetPath, userShortcutName);
    
        CString userUrlTargetPath = L"https://www.microsoft.com";
        CString userUrlShortcutName = L"Microsoft Site";
        ProcessShortcutInfo(userUrlTargetPath, userUrlShortcutName);
    
        return 0;
    }
    */
    
  • 说明

    • 上面的代码展示了如何根据目标路径的类型 (URL 或 文件系统路径) 来创建相应的COM对象。
    • IShellLink::SetDescription 方法可以用来存储用户为快捷方式输入的名称或描述。当快捷方式保存后,这个描述通常在文件属性的“注释”字段或者某些文件夹视图的“标题”列可以看到。
    • 对于 IUniformResourceLocator,它本身并没有一个“描述”或“名称”的属性可供设置。.url 文件的名称由其文件名决定,其内容主要是 [InternetShortcut] 段下的 URL= 行。所以,shortcutName 对内存中的 IUniformResourceLocator 对象而言,更多的是一个外部关联的元数据。
    • 重点在于,这些COM对象被创建和填充后,它们的所有信息都在内存里。你可以随时查询它们(比如用 IShellLink::GetPath, GetDescription),或者把这个对象传递给需要它的其他模块,而无需保存到磁盘。

选择哪种方案,或者如何组合它们,取决于你的具体需求和开发环境。如果仅仅需要用户选择一个目标文件/文件夹路径,并且接受一个额外的简单输入框来获取快捷方式名称,方案一加上一个简单输入框就够了。如果希望获得更整合、更友好的用户体验,或者需要收集更多信息(如图标、参数等),那么方案二是最佳选择。无论如何,方案三是你获取用户输入后,在内存中处理这些数据的核心手段。